UNPKG

@uwdata/mosaic-sql

Version:

SQL query construction and analysis.

584 lines 16.4 kB
import { DESCRIBE_QUERY, SELECT_QUERY, SET_OPERATION } from '../constants.js'; import { asNode, asVerbatim, maybeTableRef } from '../util/ast.js'; import { exprList, nodeList } from '../util/function.js'; import { unquote } from '../util/string.js'; import { isArray, isString } from '../util/type-check.js'; import { isColumnRef } from './column-ref.js'; import { FromClauseNode } from './from.js'; import { ExprNode, SQLNode, isNode } from './node.js'; import { SampleClauseNode } from './sample.js'; import { SelectClauseNode } from './select.js'; import { isTableRef } from './table-ref.js'; import { WindowClauseNode } from './window.js'; import { WithClauseNode } from './with.js'; /** * Check if a value is a selection query or set operation. * @param value The value to check. */ export function isQuery(value) { return value instanceof Query; } /** * Check if a value is a selection query. * @param value The value to check. */ export function isSelectQuery(value) { return value instanceof SelectQuery; } /** * Check if a value is a describe query. * @param value The value to check. */ export function isDescribeQuery(value) { return value instanceof DescribeQuery; } export class Query extends ExprNode { /** * Create a new WITH clause with the given CTE queries. * @param expr The WITH CTE queries. */ static with(...expr) { return new WithClause(...expr); } /** * Create a new select query with the given SELECT expressions. * @param expr The SELECT expressions. */ static select(...expr) { return new SelectQuery().select(...expr); } /** * Create a new select query with the given FROM expressions. * @param expr The FROM expressions. */ static from(...expr) { return new SelectQuery().from(...expr); } /** * Create a new UNION set operation over the given queries. * @param queries The queries. */ static union(...queries) { return new SetOperation('UNION', queries.flat()); } /** * Create a new UNION ALL set operation over the given queries. * @param queries The queries. */ static unionAll(...queries) { return new SetOperation('UNION ALL', queries.flat()); } /** * Create a new INTERSECT set operation over the given queries. * @param queries The queries. */ static intersect(...queries) { return new SetOperation('INTERSECT', queries.flat()); } /** * Create a new EXCEPT set operation over the given queries. * @param queries The queries. */ static except(...queries) { return new SetOperation('EXCEPT', queries.flat()); } /** * Create a new describe query for the given input query. * @param query The query to describe. */ static describe(query) { return new DescribeQuery(query); } _with = []; _orderby = []; _limitPerc = false; _limit; _offset; cteFor = null; /** * Instantiate a new query. */ constructor(type) { super(type); } /** * Return a list of subqueries. */ get subqueries() { return []; } /** * Clone this query. */ clone() { return this; } /** * Add a pointer to the query for which this query is a CTE. * @param query The query for which this query is a CTE. */ setCteFor(query) { this.cteFor = query; } /** * Add WITH common table expressions (CTEs). * @param expr Expressions to add. */ with(...expr) { const list = []; const add = (name, q) => { const query = q.clone(); query.setCteFor(this); list.push(new WithClauseNode(name, query)); }; expr.flat().forEach(e => { if (e instanceof WithClauseNode) list.push(e); else if (e != null) for (const name in e) add(name, e[name]); }); this._with = this._with.concat(list); return this; } /** * Add ORDER BY expressions. * @param expr Expressions to add. */ orderby(...expr) { this._orderby = this._orderby.concat(nodeList(expr)); return this; } /** * Set the query result LIMIT as a percentage value. * @param value The limit percentage value. */ limitPercent(value) { this._limitPerc = true; this._limit = asNode(value); return this; } /** * Set the query result LIMIT. * @param value The limit value. */ limit(value) { this._limitPerc = false; this._limit = asNode(value); return this; } /** * Set the query result OFFSET. * @param value The offset value. */ offset(value) { this._offset = asNode(value); return this; } } export class SelectQuery extends Query { _select = []; _from = []; _where = []; _sample; _groupby = []; _having = []; _window = []; _qualify = []; _distinct = false; /** * Instantiate a new select query. */ constructor() { super(SELECT_QUERY); } /** * Return a list of subqueries. */ get subqueries() { // build map of ctes within base query WITH clause const q = this.cteFor || this; const w = q instanceof SelectQuery ? q._with : []; const cte = w.reduce((obj, c) => (obj[c.name] = c.query, obj), {}); // extract subqueries in FROM clause // unused CTEs will be ignored const queries = []; this._from.forEach(({ expr }) => { if (isQuery(expr)) { queries.push(expr); } else if (isTableRef(expr)) { const subq = cte[expr.name]; if (subq) queries.push(subq); } }); return queries; } /** * Clone this query. */ clone() { return Object.assign(new SelectQuery(), this); } /** * Add SELECT expressions. * @param expr Expressions to add. */ select(...expr) { const keys = new Set; const list = []; const add = (v, as) => { const key = unquote(as); keys.add(key); if (v) list.push(new SelectClauseNode(asNode(v), key)); }; expr.flat().forEach(e => { if (e == null) return; else if (isString(e)) add(e, e); else if (isColumnRef(e)) add(e, e.column); else if (isArray(e)) add(e[1], e[0]); else if (isNode(e)) add(e, `${e}`); else for (const alias in e) add(e[alias], alias); }); this._select = this._select.filter(x => !keys.has(x.alias)).concat(list); return this; } /** * Set SELECT expressions, replacing any prior expressions. * @param expr Expressions to add. */ setSelect(...expr) { this._select = []; return this.select(...expr); } /** * Indicate if this query should retrieve distinct values only. * @param value The distinct flag */ distinct(value = true) { this._distinct = !!value; return this; } /** * Add table FROM expressions. * @param expr Expressions to add. */ from(...expr) { const list = []; const add = (v, as) => { list.push(new FromClauseNode(maybeTableRef(v), unquote(as))); }; expr.flat().forEach(e => { if (e == null) return; else if (e instanceof FromClauseNode) list.push(e); else if (isString(e)) add(e, e); else if (isTableRef(e)) add(e, e.name); else if (isNode(e)) add(e); else if (isArray(e)) add(e[1], e[0]); else for (const alias in e) add(e[alias], alias); }); this._from = this._from.concat(list); return this; } /** * Set FROM expressions, replacing any prior expressions. * @param expr Expressions to add. */ setFrom(...expr) { this._from = []; return this.from(...expr); } /** * Set SAMPLE settings. * @param value Either a sample clause node or the sample size as either * a row count or percentage. * @param method The sampling method to use. * @param seed The random seed. */ sample(value, method, seed) { let clause; if (typeof value === 'number') { const perc = value > 0 && value < 1; const size = perc ? value * 100 : Math.floor(value); clause = new SampleClauseNode(size, perc, method, seed); } else { clause = value; } this._sample = clause; return this; } /** * Add WHERE expressions. * @param expr Expressions to add. */ where(...expr) { this._where = this._where.concat(exprList(expr, asVerbatim)); return this; } /** * Set WHERE expressions, replacing any prior expressions. * @param expr Expressions to add. */ setWhere(...expr) { this._where = []; return this.where(...expr); } /** * Add GROUP BY expressions. * @param expr Expressions to add. */ groupby(...expr) { this._groupby = this._groupby.concat(nodeList(expr)); return this; } /** * Set GROUP BY expressions, replacing any prior expressions. * @param expr Expressions to add. */ setGroupby(...expr) { this._groupby = []; return this.groupby(...expr); } /** * Add HAVING expressions. * @param expr Expressions to add. */ having(...expr) { this._having = this._having.concat(exprList(expr, asVerbatim)); return this; } /** * Add WINDOW definitions. * @param expr Window definitions to add. */ window(...expr) { const list = []; expr.flat().forEach(e => { if (e != null) for (const name in e) { list.push(new WindowClauseNode(unquote(name), e[name])); } }); this._window = this._window.concat(list); return this; } /** * Add QUALIFY expressions. * @param expr Expressions to add. */ qualify(...expr) { this._qualify = this._qualify.concat(exprList(expr, asVerbatim)); return this; } /** * Generate a SQL query string. */ toString() { const { _with, _select, _distinct, _from, _sample, _where, _groupby, _having, _window, _qualify, _orderby, _limitPerc, _limit, _offset } = this; const sql = []; // WITH if (_with.length) sql.push(`WITH ${_with.join(', ')}`); // SELECT sql.push(`SELECT${_distinct ? ' DISTINCT' : ''} ${_select.join(', ')}`); // FROM if (_from.length) sql.push(`FROM ${_from.join(', ')}`); // WHERE if (_where.length) { const clauses = _where.map(String).filter(x => x).join(' AND '); if (clauses) sql.push(`WHERE ${clauses}`); } // SAMPLE if (_sample) sql.push(`USING SAMPLE ${_sample}`); // GROUP BY if (_groupby.length) { sql.push(`GROUP BY ${_groupby.join(', ')}`); } // HAVING if (_having.length) { const clauses = _having.map(String).filter(x => x).join(' AND '); if (clauses) sql.push(`HAVING ${clauses}`); } // WINDOW if (_window.length) sql.push(`WINDOW ${_window.join(', ')}`); // QUALIFY if (_qualify.length) { const clauses = _qualify.map(String).filter(x => x).join(' AND '); if (clauses) sql.push(`QUALIFY ${clauses}`); } // ORDER BY if (_orderby.length) sql.push(`ORDER BY ${_orderby.join(', ')}`); // LIMIT if (_limit) sql.push(`LIMIT ${_limit}${_limitPerc ? '%' : ''}`); // OFFSET if (_offset) sql.push(`OFFSET ${_offset}`); return sql.join(' '); } } export class DescribeQuery extends SQLNode { query; /** * Instantiate a describe query. * @param query The query to describe. */ constructor(query) { super(DESCRIBE_QUERY); this.query = query; } /** * Clone this describe query. */ clone() { // @ts-expect-error creates describe query return new DescribeQuery(this.query.clone()); } /** * Generate a SQL query string. */ toString() { return `DESCRIBE ${this.query}`; } } export class SetOperation extends Query { /** The set operation to perform. */ op; /** The input queries to the set operation. */ queries; /** * Instantiate a new set operation instance. * @param op The set operation. * @param queries The subqueries. */ constructor(op, queries) { super(SET_OPERATION); this.op = op; this.queries = queries; } /** * Add a pointer to the query for which this query is a CTE. * @param query The query for which this query is a CTE. */ setCteFor(query) { super.setCteFor(query); const { queries, cteFor } = this; if (cteFor) queries.forEach(q => q.setCteFor(cteFor)); } /** * Return a list of subqueries. */ get subqueries() { return this.queries; } /** * Clone this set operation. */ clone() { const { op, queries, ...rest } = this; // @ts-expect-error creates set operation return Object.assign(new SetOperation(op, queries), rest); } /** * Generate a SQL query string. */ toString() { const { op, queries, _with, _orderby, _limitPerc, _limit, _offset } = this; const sql = []; // WITH if (_with.length) sql.push(`WITH ${_with.join(', ')}`); // SUBQUERIES sql.push(queries.join(` ${op} `)); // ORDER BY if (_orderby.length) sql.push(`ORDER BY ${_orderby.join(', ')}`); // LIMIT if (_limit) sql.push(`LIMIT ${_limit}${_limitPerc ? '%' : ''}`); // OFFSET if (_offset) sql.push(`OFFSET ${_offset}`); return sql.join(' '); } } class WithClause { /** The common table expressions (CTE). */ _with; /** * Instantiate a new WITH clause instance. * @param expr The WITH CTE queries. */ constructor(...expr) { this._with = expr; } /** * Create a new select query with the given SELECT expressions. * @param expr The SELECT expressions. */ select(...expr) { return Query.select(...expr).with(...this._with); } /** * Create a new select query with the given FROM expressions. * @param expr The FROM expressions. */ from(...expr) { return Query.from(...expr).with(...this._with); } /** * Create a new UNION set operation over the given queries. * @param queries The queries. */ union(...queries) { return Query.union(...queries).with(...this._with); } /** * Create a new UNION ALL set operation over the given queries. * @param queries The queries. */ unionAll(...queries) { return Query.unionAll(...queries).with(...this._with); } /** * Create a new INTERSECT set operation over the given queries. * @param queries The queries. */ intersect(...queries) { return Query.intersect(...queries).with(...this._with); } /** * Create a new EXCEPT set operation over the given queries. * @param queries The queries. */ except(...queries) { return Query.except(...queries).with(...this._with); } } //# sourceMappingURL=query.js.map