import { FieldSet, QuerySource, WhereStatement, TableRef, OrderStatement, OrderDirection, HavingStatement, escape, } from '../types.ts' import {WhereBuilder} from './WhereBuilder.ts' import {applyMixins} from '../../../../lib/src/support/mixins.ts' import {TableRefBuilder} from './TableRefBuilder.ts' import {Join} from './join/Join.ts' import {LeftJoin} from './join/LeftJoin.ts' import {LeftOuterJoin} from './join/LeftOuterJoin.ts' import {CrossJoin} from './join/CrossJoin.ts' import {InnerJoin} from './join/InnerJoin.ts' import {RightJoin} from './join/RightJoin.ts' import {RightOuterJoin} from './join/RightOuterJoin.ts' import {FullOuterJoin} from './join/FullOuterJoin.ts' import {HavingBuilder} from './HavingBuilder.ts' import ConnectionExecutable from './ConnectionExecutable.ts' import {Scope} from '../Scope.ts' import {isInstantiable} from "../../../../di/src/type/Instantiable.ts"; /** * Base type for functions that operate on WhereBuilders. */ export type WhereBuilderFunction = (group: WhereBuilder) => any /** * Base type for functions that operate on HavingBuilders. */ export type HavingBuilderFunction = (group: HavingBuilder) => any /** * Base type for functions that operate on Joins. */ export type JoinFunction = (join: Join) => any /** * Error class thrown when the SQL generated will be invalid. * @extends Error */ export class MalformedSQLGrammarError extends Error {} /** * Query builder base class for SELECT queries. * @extends ConnectionExecutable * @extends TableRefBuilder * @extends WhereBuilder * @extends HavingBuilder */ export class Select extends ConnectionExecutable { /** * The fields to select. * @type Array */ protected _fields: string[] = [] /** * The source to select from. * @type QuerySource */ protected _source?: QuerySource = undefined /** * Where clauses to apply. * @type Array */ protected _wheres: WhereStatement[] = [] /** * The scopes to apply. * @type Array */ protected _scopes: Scope[] = [] /** * Having clauses to apply. * @type Array */ protected _havings: HavingStatement[] = [] /** * Max number of rows to return. * @type number */ protected _limit?: number /** * Number of rows to skip. * @type number */ protected _offset?: number /** * Join clauses to apply. * @type Array */ protected _joins: Join[] = [] /** * Include the DISTINCT operator? * @type boolean */ protected _distinct = false /** * Group by clauses to apply. * @type Array */ protected _group_by: string[] = [] /** * Order by clauses to apply. * @type Array */ protected _order: OrderStatement[] = [] /** * Include the DISTINCT operator. * @return self */ distinct() { this._distinct = true return this } sql(level = 0): string { const indent = Array(level * 2).fill(' ').join('') if ( typeof this._source === 'undefined' ) throw new MalformedSQLGrammarError(`No table reference has been provided.`) const table_ref = this.source_alias_to_table_ref(this._source) let order = '' if ( this._order.length > 0 ) { order = this._order.map(x => `${x.field} ${x.direction}`).join(', ') } const wheres = this.wheres_to_sql(this._wheres, level + 1) const havings = this.havings_to_sql(this._havings, level + 1) return [ `SELECT ${this._distinct ? 'DISTINCT ' : ''}${this._fields.join(', ')}`, `FROM ${this.serialize_table_ref(table_ref)}`, ...this._joins.map(join => join.sql(level + 1)), ...(wheres.trim() ? ['WHERE', wheres] : []), ...[typeof this._limit !== 'undefined' ? [`LIMIT ${this._limit}`] : []], ...[typeof this._offset !== 'undefined' ? [`OFFSET ${this._offset}`] : []], ...[this._group_by.length > 0 ? 'GROUP BY ' + this._group_by.join(', ') : []], ...[order ? [`ORDER BY ${order}`] : []], ...(havings.trim() ? ['HAVING', havings] : []), ].filter(x => String(x).trim()).join(`\n${indent}`) } /** * Include a field in the results. * @param {string | Select} field * @param {string} [as] - alias * @return self */ field(field: string | Select, as?: string) { if ( field instanceof Select ) { this._fields.push(`${escape(field)}${as ? ' AS '+as : ''}`) } else { this._fields.push(field) } return this } /** * Clear the selected fields. * @return self */ clear_fields() { this._fields = [] return this } /** * Get a copy of this query. * @return Select */ clone(): Select { const constructor = this.constructor as typeof Select if ( !isInstantiable>(constructor) ) { throw new TypeError(`Parent constructor is not instantiable.`) } const select = new constructor() select._fields = this._fields select._source = this._source select._wheres = this._wheres select._scopes = this._scopes select._havings = this._havings select._limit = this._limit select._offset = this._offset select._joins = this._joins select._distinct = this._distinct select._group_by = this._group_by select._order = this._order select.__target_connection = this.__target_connection select.__target_operator = this.__target_operator return select } /** * Add group by clauses to the query. * @param {...string} groupings * @return self */ group_by(...groupings: string[]) { this._group_by = groupings return this } /** * Include the given fields in the result set. * @param {...FieldSet} fields * @return self */ fields(...fields: FieldSet[]) { for ( const field_set of fields ) { if ( typeof field_set === 'string' ) { if ( !this._fields.includes(field_set) ) this._fields.push(field_set) } else { for ( const field of field_set ) { if ( !this._fields.includes(field) ) this._fields.push(field) } } } return this } /** * Set the source to select from. * @param {QuerySource} source * @param {string} [alias] * @return self */ from(source: QuerySource, alias?: string) { if ( !alias ) this._source = source else this._source = { ref: source, alias } return this } /** * Limit the returned rows. * @param {number} num * @return self */ limit(num: number) { this._limit = Number(num) return this } /** * Skip the first num rows. * @param {number} num * @return self */ offset(num: number) { this._offset = Number(num) return this } /** * Skip the first num rows. * @param {number} num * @return self */ skip(num: number) { this._offset = Number(num) return this } /** * Return only the first num rows. * @param {number} num * @return self */ take(num: number) { this._limit = Number(num) return this } /** * Add a JOIN clause to the query by alias, or using a function to build the clause. * @param {QuerySource} source * @param {string | JoinFunction} alias_or_func * @param {JoinFunction} [func] * @return self */ join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) { this._createJoin(Join, source, alias_or_func, func) return this } /** * Add a LEFT JOIN clause to the query by alias, or using a function to build the clause. * @param {QuerySource} source * @param {string | JoinFunction} alias_or_func * @param {JoinFunction} [func] * @return self */ left_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) { this._createJoin(LeftJoin, source, alias_or_func, func) return this } /** * Add a LEFT OUTER JOIN clause to the query by alias, or using a function to build the clause. * @param {QuerySource} source * @param {string | JoinFunction} alias_or_func * @param {JoinFunction} [func] * @return self */ left_outer_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) { this._createJoin(LeftOuterJoin, source, alias_or_func, func) return this } /** * Add a CROSS JOIN clause to the query by alias, or using a function to build the clause. * @param {QuerySource} source * @param {string | JoinFunction} alias_or_func * @param {JoinFunction} [func] * @return self */ cross_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) { this._createJoin(CrossJoin, source, alias_or_func, func) return this } /** * Add an INNER JOIN clause to the query by alias, or using a function to build the clause. * @param {QuerySource} source * @param {string | JoinFunction} alias_or_func * @param {JoinFunction} [func] * @return self */ inner_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) { this._createJoin(InnerJoin, source, alias_or_func, func) return this } /** * Add a RIGHT JOIN clause to the query by alias, or using a function to build the clause. * @param {QuerySource} source * @param {string | JoinFunction} alias_or_func * @param {JoinFunction} [func] * @return self */ right_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) { this._createJoin(RightJoin, source, alias_or_func, func) return this } /** * Add a RIGHT OUTER JOIN clause to the query by alias, or using a function to build the clause. * @param {QuerySource} source * @param {string | JoinFunction} alias_or_func * @param {JoinFunction} [func] * @return self */ right_outer_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) { this._createJoin(RightOuterJoin, source, alias_or_func, func) return this } /** * Add a FULL OUTER JOIN clause to the query by alias, or using a function to build the clause. * @param {QuerySource} source * @param {string | JoinFunction} alias_or_func * @param {JoinFunction} [func] * @return self */ full_outer_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) { this._createJoin(FullOuterJoin, source, alias_or_func, func) return this } /** * Internal helper for creating join clauses using query builder classes. * @param {typeof Join} Class * @param {QuerySource} source * @param {string | JoinFunction} alias_or_func * @param {JoinFunction} [func] * @private */ private _createJoin(Class: typeof Join, source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) { const [table_ref, join_func] = this.join_ref_to_join_args(source, alias_or_func, func) const join = new Class(table_ref) this._joins.push(join) join_func(join) } /** * Cast a join reference to the arguments required for the JOIN query builder. * @param {QuerySource} source * @param {string | JoinFunction} alias_or_func * @param {JoinFunction} [func] * @return Array */ join_ref_to_join_args(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction): [TableRef, JoinFunction] { let alias = undefined if ( typeof alias_or_func === 'string' ) alias = alias_or_func let join_func = undefined if ( func ) join_func = func else if ( typeof alias_or_func === 'function' ) join_func = alias_or_func else { throw new TypeError('Missing required join function handler!') } return [this.source_alias_to_table_ref(source, alias), join_func] } /** * Add an order by clause to the query. * @param {string} field * @param {string} [direction = 'ASC'] * @return self */ order_by(field: string, direction: OrderDirection = 'ASC') { this._order.push({ field, direction }) return this } /** * Add an ORDER BY ... ASC clause to the query. * @param {string} field * @return self */ order_asc(field: string) { return this.order_by(field, 'ASC') } /** * Add an ORDER BY ... DESC clause to the query. * @param {string} field * @return self */ order_desc(field: string) { return this.order_by(field, 'DESC') } } export interface Select extends WhereBuilder, TableRefBuilder, HavingBuilder {} applyMixins(Select, [WhereBuilder, TableRefBuilder, HavingBuilder])