You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

458 lines
13 KiB

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<T> extends ConnectionExecutable<T> {
/**
* The fields to select.
* @type Array<string>
*/
protected _fields: string[] = []
/**
* The source to select from.
* @type QuerySource
*/
protected _source?: QuerySource = undefined
/**
* Where clauses to apply.
* @type Array<WhereStatement>
*/
protected _wheres: WhereStatement[] = []
/**
* The scopes to apply.
* @type Array<Scope>
*/
protected _scopes: Scope[] = []
/**
* Having clauses to apply.
* @type Array<HavingStatement>
*/
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<Join>
*/
protected _joins: Join[] = []
/**
* Include the DISTINCT operator?
* @type boolean
*/
protected _distinct = false
/**
* Group by clauses to apply.
* @type Array<string>
*/
protected _group_by: string[] = []
/**
* Order by clauses to apply.
* @type Array<OrderStatement>
*/
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<any>, 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<T> {
const constructor = this.constructor as typeof Select
if ( !isInstantiable<Select<T>>(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<T> extends WhereBuilder, TableRefBuilder, HavingBuilder {}
applyMixins(Select, [WhereBuilder, TableRefBuilder, HavingBuilder])