UNPKG

jii

Version:

Jii - Full-Stack JavaScript Framework

1,194 lines (1,055 loc) 54.9 kB
/** * @author <a href="http://www.affka.ru">Vladimir Kozhin</a> * @license MIT */ 'use strict'; const Jii = require('../BaseJii'); const NotSupportedException = require('../exceptions/NotSupportedException'); const InvalidConfigException = require('../exceptions/InvalidConfigException'); const InvalidParamException = require('../exceptions/InvalidParamException'); const Expression = require('../data/Expression'); const Query = require('../data/Query'); const _isArray = require('lodash/isArray'); const _isString = require('lodash/isString'); const _isBoolean = require('lodash/isBoolean'); const _isEmpty = require('lodash/isEmpty'); const _isObject = require('lodash/isObject'); const _isNaN = require('lodash/isNaN'); const _extend = require('lodash/extend'); const _filter = require('lodash/filter'); const _each = require('lodash/each'); const _size = require('lodash/size'); const _has = require('lodash/has'); const _map = require('lodash/map'); const _clone = require('lodash/clone'); const _values = require('lodash/values'); const _words = require('lodash/words'); const _trim = require('lodash/trim'); const _trimStart = require('lodash/trimStart'); const BaseObject = require('../base/BaseObject'); class QueryBuilder extends BaseObject { preInit(connection, config) { config = config || {}; /** * @var array map of query condition to builder methods. * These methods are used by [[buildCondition]] to build SQL conditions from array syntax. */ this._conditionBuilders = { 'NOT': 'buildNotCondition', 'AND': 'buildAndCondition', 'OR': 'buildAndCondition', 'BETWEEN': 'buildBetweenCondition', 'NOT BETWEEN': 'buildBetweenCondition', 'IN': 'buildInCondition', 'NOT IN': 'buildInCondition', 'LIKE': 'buildLikeCondition', 'NOT LIKE': 'buildLikeCondition', 'OR LIKE': 'buildLikeCondition', 'OR NOT LIKE': 'buildLikeCondition', 'EXISTS': 'buildExistsCondition', 'NOT EXISTS': 'buildExistsCondition' }; /** * @var array the abstract column types mapped to physical column types. * This is mainly used to support creating/modifying tables using DB-independent data type specifications. * Child classes should override this property to declare supported type mappings. */ this.typeMap = null; /** * @var string the separator between different fragments of a SQL statement. * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement. */ this.separator = ' '; /** * @var Connection the database connection. */ this.db = connection; super.preInit(config); } /** * Generates a SELECT SQL statement from a [[Query]] object. * @param {Query} query the [[Query]] object from which the SQL statement will be generated. * @param {object} [params] the parameters to be bound to the generated SQL statement. These parameters will * be included in the result with the additional parameters generated during the query building process. * @return {[]} the generated SQL statement (the first array element) and the corresponding * parameters to be bound to the SQL statement (the second array element). The parameters returned * include those provided in `params`. */ build(query, params) { params = params || {}; return query.prepare(this).then(query => { params = _extend(params, query.getParams()); return Promise.all([ this.buildSelect(query.getSelect(), params, query.getDistinct(), query.getSelectOption()), this.buildFrom(query.getFrom(), params), this.buildJoin(query.getJoin(), params), this.buildWhere(query.getWhere(), params), this.buildGroupBy(query.getGroupBy()), this.buildHaving(query.getHaving(), params), this.buildOrderBy(query.getOrderBy()), this.buildLimit(query.getLimit(), query.getOffset()) ]).then(clauses => { clauses = _filter(clauses, sqlPart => { return !!sqlPart; }); var sql = clauses.join(this.separator); return this.buildUnion(query.getUnion(), params).then(union => { if (union !== '') { sql = '(' + sql + ')' + this.separator + union; } return [ sql, params ]; }); }); }); } /** * Creates an INSERT SQL statement. * For example, * * ~~~ * sql = queryBuilder.insert('user', { * name: 'Sam', * age: 30 * }, params); * ~~~ * * The method will properly escape the table and column names. * * @param {string} table the table that new rows will be inserted into. * @param {object} columns the column data (name: value) to be inserted into the table. * @param {object} params the binding parameters that will be generated by this method. * They should be bound to the DB command later. * @return string the INSERT SQL */ insert(table, columns, params) { var tableSchema = this.db.getTableSchema(table); var columnSchemas = tableSchema !== null ? tableSchema.columns : {}; var names = []; var placeholders = []; _each(columns, (value, name) => { names.push(this.db.quoteColumnName(name)); if (value instanceof Expression) { placeholders.push(value.expression); params = _extend(params, value.params); } else { var phName = QueryBuilder.PARAM_PREFIX + _size(params); placeholders.push(phName); params[phName] = !_isArray(value) && _has(columnSchemas, name) ? columnSchemas[name].typecast(value) : value; } }); return Promise.resolve('INSERT INTO ' + this.db.quoteTableName(table) + ' (' + names.join(', ') + ') VALUES (' + placeholders.join(', ') + ')'); } /** * Generates a batch INSERT SQL statement. * For example, * * ~~~ * sql = queryBuilder.batchInsert('user', ['name', 'age'], { * ['Tom', 30], * ['Jane', 20], * ['Linda', 25] * }); * ~~~ * * Note that the values in each row must match the corresponding column names. * * @param {string} table the table that new rows will be inserted into. * @param {string[]} columns the column names * @param {*[]} rows the rows to be batch inserted into the table * @return {string} the batch INSERT SQL statement */ batchInsert(table, columns, rows) { var tableSchema = this.db.getTableSchema(table); var columnSchemas = tableSchema !== null ? tableSchema.columns : []; var values = []; _each(rows, row => { var rowValues = []; _each(row, (value, i) => { if (!_isArray(value) && _has(columnSchemas, columns[i])) { value = columnSchemas[columns[i]].typecast(value); } if (_isString(value)) { value = this.db.quoteValue(value); } else if (value === false) { value = 0; } else if (value === null) { value = 'NULL'; } rowValues.push(value); }); values.push('(' + rowValues.join(', ') + ')'); }); _each(columns, (name, i) => { columns[i] = this.db.quoteColumnName(name); }); return Promise.resolve('INSERT INTO ' + this.db.quoteTableName(table) + ' (' + columns.join(', ') + ') VALUES ' + values.join(', ')); } /** * Creates an UPDATE SQL statement. * For example, * * ~~~ * params = []; * sql = queryBuilder.update('user', {status: 1}, 'age > 30', params); * ~~~ * * The method will properly escape the table and column names. * * @param {string} table the table to be updated. * @param {object} columns the column data (name: value) to be updated. * @param {object|[]|string} condition the condition that will be put in the WHERE part. Please * refer to [[Query::where()]] on how to specify condition. * @param {object} params the binding parameters that will be modified by this method * so that they can be bound to the DB command later. * @return {string} the UPDATE SQL */ update(table, columns, condition, params) { var tableSchema = this.db.getTableSchema(table); var columnSchemas = tableSchema !== null ? tableSchema.columns : {}; var lines = []; _each(columns, (value, name) => { if (value instanceof Expression) { lines.push(this.db.quoteColumnName(name) + '=' + value.expression); _each(value.params, (v, n) => { params[n] = v; }); } else { var phName = QueryBuilder.PARAM_PREFIX + _size(params); lines.push(this.db.quoteColumnName(name) + '=' + phName); params[phName] = !_isArray(value) && _has(columnSchemas, name) ? columnSchemas[name].typecast(value) : value; } }); var sql = 'UPDATE ' + this.db.quoteTableName(table) + ' SET ' + lines.join(', '); return this.buildWhere(condition, params).then(where => { return where === '' ? sql : sql + ' ' + where; }); } /** * Creates a DELETE SQL statement. * For example, * * ~~~ * sql = queryBuilder.delete('user', 'status = 0'); * ~~~ * * The method will properly escape the table and column names. * * @param {string} table the table where the data will be deleted from. * @param {object|[]|string} condition the condition that will be put in the WHERE part. Please * refer to [[Query::where()]] on how to specify condition. * @param {object} params the binding parameters that will be modified by this method * so that they can be bound to the DB command later. * @return {string} the DELETE SQL */ delete(table, condition, params) { var sql = 'DELETE FROM ' + this.db.quoteTableName(table); return this.buildWhere(condition, params).then(where => { return where === '' ? sql : sql + ' ' + where; }); } /** * Builds a SQL statement for creating a new DB table. * * The columns in the new table should be specified as name-definition pairs (e.g. 'name': 'string'), * where name stands for a column name which will be properly quoted by the method, and definition * stands for the column type which can contain an abstract DB type. * The [[getColumnType()]] method will be invoked to convert any abstract type into a physical one. * * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly * inserted into the generated SQL. * * For example, * * ~~~ * sql = queryBuilder.createTable('user', [ * 'id': 'pk', * 'name': 'string', * 'age': 'integer', * ]); * ~~~ * * @param {string} table the name of the table to be created. The name will be properly quoted by the method. * @param {object} columns the columns (name: definition) in the new table. * @param {string} [options] additional SQL fragment that will be appended to the generated SQL. * @return {string} the SQL statement for creating a new DB table. */ createTable(table, columns, options) { options = options || null; var cols = []; _each(columns, (type, name) => { if (_isString(name)) { cols.push('\t' + this.db.quoteColumnName(name) + ' ' + this.getColumnType(type)); } else { cols.push('\t' + type); } }); var sql = 'CREATE TABLE ' + this.db.quoteTableName(table) + ' (\n' + cols.join(',\n') + '\n)'; return Promise.resolve(options === null ? sql : sql + ' ' + options); } /** * Builds a SQL statement for renaming a DB table. * @param {string} oldName the table to be renamed. The name will be properly quoted by the method. * @param {string} newName the new table name. The name will be properly quoted by the method. * @return {string} the SQL statement for renaming a DB table. */ renameTable(oldName, newName) { return Promise.resolve('RENAME TABLE ' + this.db.quoteTableName(oldName) + ' TO ' + this.db.quoteTableName(newName)); } /** * Builds a SQL statement for dropping a DB table. * @param {string} table the table to be dropped. The name will be properly quoted by the method. * @return {string} the SQL statement for dropping a DB table. */ dropTable(table) { return Promise.resolve('DROP TABLE ' + this.db.quoteTableName(table)); } /** * Builds a SQL statement for adding a primary key constraint to an existing table. * @param {string} name the name of the primary key constraint. * @param {string} table the table that the primary key constraint will be added to. * @param {string|[]} columns comma separated string or array of columns that the primary key will consist of. * @return {string} the SQL statement for adding a primary key constraint to an existing table. */ addPrimaryKey(name, table, columns) { if (_isString(columns)) { columns = _words(columns, /[^,]+/g); } columns = _map(columns, col => { return this.db.quoteColumnName(col); }); return Promise.resolve('ALTER TABLE ' + this.db.quoteTableName(table) + ' ADD CONSTRAINT ' + this.db.quoteColumnName(name) + ' PRIMARY KEY (' + columns.join(', ') + ' )'); } /** * Builds a SQL statement for removing a primary key constraint to an existing table. * @param {string} name the name of the primary key constraint to be removed. * @param {string} table the table that the primary key constraint will be removed from. * @return {string} the SQL statement for removing a primary key constraint from an existing table. */ dropPrimaryKey(name, table) { return Promise.resolve('ALTER TABLE ' + this.db.quoteTableName(table) + ' DROP CONSTRAINT ' + this.db.quoteColumnName(name)); } /** * Builds a SQL statement for truncating a DB table. * @param {string} table the table to be truncated. The name will be properly quoted by the method. * @return {string} the SQL statement for truncating a DB table. */ truncateTable(table) { return Promise.resolve('TRUNCATE TABLE ' + this.db.quoteTableName(table)); } /** * Builds a SQL statement for adding a new DB column. * @param {string} table the table that the new column will be added to. The table name will be properly quoted by the method. * @param {string} column the name of the new column. The name will be properly quoted by the method. * @param {string} type the column type. The [[getColumnType()]] method will be invoked to convert abstract column type (if any) * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. * @return {string} the SQL statement for adding a new column. */ addColumn(table, column, type) { return Promise.resolve('ALTER TABLE ' + this.db.quoteTableName(table) + ' ADD ' + this.db.quoteColumnName(column) + ' ' + this.getColumnType(type)); } /** * Builds a SQL statement for dropping a DB column. * @param {string} table the table whose column is to be dropped. The name will be properly quoted by the method. * @param {string} column the name of the column to be dropped. The name will be properly quoted by the method. * @return {string} the SQL statement for dropping a DB column. */ dropColumn(table, column) { return Promise.resolve('ALTER TABLE ' + this.db.quoteTableName(table) + ' DROP COLUMN ' + this.db.quoteColumnName(column)); } /** * Builds a SQL statement for renaming a column. * @param {string} table the table whose column is to be renamed. The name will be properly quoted by the method. * @param {string} oldName the old name of the column. The name will be properly quoted by the method. * @param {string} newName the new name of the column. The name will be properly quoted by the method. * @return {string} the SQL statement for renaming a DB column. */ renameColumn(table, oldName, newName) { return Promise.resolve('ALTER TABLE ' + this.db.quoteTableName(table) + ' RENAME COLUMN ' + this.db.quoteColumnName(oldName) + ' TO ' + this.db.quoteColumnName(newName)); } /** * Builds a SQL statement for changing the definition of a column. * @param {string} table the table whose column is to be changed. The table name will be properly quoted by the method. * @param {string} column the name of the column to be changed. The name will be properly quoted by the method. * @param {string} type the new column type. The [[getColumnType()]] method will be invoked to convert abstract * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null' * will become 'varchar(255) not null'. * @return {string} the SQL statement for changing the definition of a column. */ alterColumn(table, column, type) { return Promise.resolve('ALTER TABLE ' + this.db.quoteTableName(table) + ' CHANGE ' + this.db.quoteColumnName(column) + ' ' + this.db.quoteColumnName(column) + ' ' + this.getColumnType(type)); } /** * Builds a SQL statement for adding a foreign key constraint to an existing table. * The method will properly quote the table and column names. * @param {string} name the name of the foreign key constraint. * @param {string} table the table that the foreign key constraint will be added to. * @param {string|string[]} columns the name of the column to that the constraint will be added on. * If there are multiple columns, separate them with commas or use an array to represent them. * @param {string} refTable the table that the foreign key references to. * @param {string|string[]} refColumns the name of the column that the foreign key references to. * If there are multiple columns, separate them with commas or use an array to represent them. * @param {string} [deleteOption] the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL * @param {string} [updateOption] the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL * @return string the SQL statement for adding a foreign key constraint to an existing table. */ addForeignKey(name, table, columns, refTable, refColumns, deleteOption, updateOption) { deleteOption = deleteOption || null; updateOption = updateOption || null; var sql = 'ALTER TABLE ' + this.db.quoteTableName(table) + ' ADD CONSTRAINT ' + this.db.quoteColumnName(name) + ' FOREIGN KEY (' + this.buildColumns(columns) + ')' + ' REFERENCES ' + this.db.quoteTableName(refTable) + ' (' + this.buildColumns(refColumns) + ')'; if (deleteOption !== null) { sql += ' ON DELETE ' + deleteOption; } if (updateOption !== null) { sql += ' ON UPDATE ' + updateOption; } return Promise.resolve(sql); } /** * Builds a SQL statement for dropping a foreign key constraint. * @param {string} name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. * @param {string} table the table whose foreign is to be dropped. The name will be properly quoted by the method. * @return {string} the SQL statement for dropping a foreign key constraint. */ dropForeignKey(name, table) { return Promise.resolve('ALTER TABLE ' + this.db.quoteTableName(table) + ' DROP CONSTRAINT ' + this.db.quoteColumnName(name)); } /** * Builds a SQL statement for creating a new index. * @param {string} name the name of the index. The name will be properly quoted by the method. * @param {string} table the table that the new index will be created for. The table name will be properly quoted by the method. * @param {string|string[]} columns the column(s) that should be included in the index. If there are multiple columns, * separate them with commas or use an array to represent them. Each column name will be properly quoted * by the method, unless a parenthesis is found in the name. * @param {boolean} isUnique whether to add UNIQUE constraint on the created index. * @return {string} the SQL statement for creating a new index. */ createIndex(name, table, columns, isUnique) { isUnique = isUnique || false; return Promise.resolve((isUnique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ') + this.db.quoteTableName(name) + ' ON ' + this.db.quoteTableName(table) + ' (' + this.buildColumns(columns) + ')'); } /** * Builds a SQL statement for dropping an index. * @param {string} name the name of the index to be dropped. The name will be properly quoted by the method. * @param {string} table the table whose index is to be dropped. The name will be properly quoted by the method. * @return {string} the SQL statement for dropping an index. */ dropIndex(name, table) { return Promise.resolve('DROP INDEX ' + this.db.quoteTableName(name) + ' ON ' + this.db.quoteTableName(table)); } /** * Creates a SQL statement for resetting the sequence value of a table's primary key. * The sequence will be reset such that the primary key of the next new row inserted * will have the specified value or 1. * @param {string} table the name of the table whose primary key sequence will be reset * @param {[]|string} value the value for the primary key of the next new row inserted. If this is not set, * the next new row's primary key will have a value 1. * @return {string} the SQL statement for resetting sequence * @throws NotSupportedException if this is not supported by the underlying DBMS */ resetSequence(table, value) { value = value || null; throw new NotSupportedException(this.db.getDriverName() + ' does not support resetting sequence.'); } /** * Builds a SQL statement for enabling or disabling integrity check. * @param {boolean} check whether to turn on or off the integrity check. * @param {string} schema the schema of the tables. Defaults to empty string, meaning the current or default schema. * @param {string} table the table name. Defaults to empty string, meaning that no table will be changed. * @return {string} the SQL statement for checking integrity * @throws NotSupportedException if this is not supported by the underlying DBMS */ checkIntegrity(check, schema, table) { check = _isBoolean(check) ? check : true; schema = schema || ''; table = table || ''; throw new NotSupportedException(this.db.getDriverName() + ' does not support enabling/disabling integrity check.'); } /** * Converts an abstract column type into a physical column type. * The conversion is done using the type map specified in [[typeMap]]. * The following abstract column types are supported (using MySQL as an example to explain the corresponding * physical types): * * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY" * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY" * - `string`: string type, will be converted into "varchar(255)" * - `text`: a long string type, will be converted into "text" * - `smallint`: a small integer type, will be converted into "smallint(6)" * - `integer`: integer type, will be converted into "int(11)" * - `bigint`: a big integer type, will be converted into "bigint(20)" * - `boolean`: boolean type, will be converted into "tinyint(1)" * - `float``: float number type, will be converted into "float" * - `decimal`: decimal number type, will be converted into "decimal" * - `datetime`: datetime type, will be converted into "datetime" * - `timestamp`: timestamp type, will be converted into "timestamp" * - `time`: time type, will be converted into "time" * - `date`: date type, will be converted into "date" * - `money`: money type, will be converted into "decimal(19,4)" * - `binary`: binary data type, will be converted into "blob" * * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only * the first part will be converted, and the rest of the parts will be appended to the converted result. * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'. * * For some of the abstract types you can also specify a length or precision constraint * by appending it in round brackets directly to the type. * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. * If the underlying DBMS does not support these kind of constraints for a type it will * be ignored. * * If a type cannot be found in [[typeMap]], it will be returned without any change. * @param {string} type abstract column type * @return {string} physical column type. */ getColumnType(type) { if (_has(this.typeMap, type)) { return this.typeMap[type]; } var matches = /^(\w+)\((.+?)\)(.*)/.exec(type); if (matches !== null) { if (_has(this.typeMap, matches[1])) { return this.typeMap[matches[1]].replace(/\(.+\)/, '(' + matches[2] + ')') + matches[3]; } } else { var matches2 = /^(\w+)\s+/.exec(type); if (matches2 !== null && _has(this.typeMap, matches2[1])) { return type.replace(/^\w+/, this.typeMap[matches2[1]]); } } return type; } /** * @param {object} columns * @param {object} params the binding parameters to be populated * @param {boolean} distinct * @param {string} selectOption * @return {string} the SELECT clause built from [[Query::select]]. */ buildSelect(columns, params, distinct, selectOption) { distinct = distinct || false; selectOption = selectOption || null; var select = distinct ? 'SELECT DISTINCT' : 'SELECT'; if (selectOption !== null) { select += ' ' + selectOption; } if (_isEmpty(columns)) { return Promise.resolve(select + ' *'); } var normalizeColumns = []; var promises = []; _each(columns, (column, i) => { if (column instanceof Expression) { normalizeColumns.push(column.expression); params = _extend(params, column.params); } else if (column instanceof Query) { var promise = this.build(column, params).then(buildParams => { var sql = buildParams[0]; normalizeColumns.push('(' + sql + ') AS ' + this.db.quoteColumnName(i)); }); promises.push(promise); } else if (!/^[0-9]+$/.test(i)) { if (column.indexOf('(') === -1) { column = this.db.quoteColumnName(column); } normalizeColumns.push(column + ' AS ' + this.db.quoteColumnName(i)); } else if (column.indexOf('(') === -1) { var matches = /^(.*?)(?:\s+as\s+|\s+)([\w\-_\.]+)/i.exec(column); if (matches !== null) { normalizeColumns.push(this.db.quoteColumnName(matches[1]) + ' AS ' + this.db.quoteColumnName(matches[2])); } else { normalizeColumns.push(this.db.quoteColumnName(column)); } } else { normalizeColumns.push(column); } }); return Promise.all(promises).then(() => { return select + ' ' + normalizeColumns.join(', '); }); } /** * @param {object|[]} tables * @param {object} params the binding parameters to be populated * @return {string} the FROM clause built from [[Query::from]]. */ buildFrom(tables, params) { if (_isEmpty(tables)) { return Promise.resolve(''); } return this._quoteTableNames(tables, params).then(tables => { return 'FROM ' + tables.join(', '); }); } /** * @param {[]} joins * @param {object} params the binding parameters to be populated * @return string the JOIN clause built from [[Query::join]]. * @throws Exception if the joins parameter is not in proper format */ buildJoin(joins, params) { if (_isEmpty(joins)) { return Promise.resolve(''); } var promises = []; joins = _clone(joins); _each(joins, (join, i) => { if (!_isArray(join) || join.length < 1) { throw new InvalidConfigException('A join clause must be specified as an array of join type, join table, and optionally join condition.'); } // 0:join type, 1:join table, 2:on-condition (optional) var joinType = join[0]; var table = join[1]; if (!_isArray(table) && !_isObject(table)) { table = [table]; } var promise = this._quoteTableNames(table, params).then(tables => { table = _values(tables)[0]; joins[i] = joinType + ' ' + table; if (join[2]) { return this.buildCondition(join[2], params).then(condition => { if (condition !== '') { joins[i] += ' ON ' + condition; } }); } }); promises.push(promise); }); return Promise.all(promises).then(() => { return joins.join(this.separator); }); } /** * * @param {object|[]} tables * @param {object} params * @returns {object|[]} * @private */ _quoteTableNames(tables, params) { var promises = []; _each(tables, (table, i) => { if (table instanceof Query) { var promise = this.build(table, params).then(buildResult => { var sql = buildResult[0]; params = _extend(params, buildResult[1]); tables[i] = '(' + sql + ') ' + this.db.quoteTableName(i); }); promises.push(promise); } else if (_isString(i)) { if (table.indexOf('(') === -1) { table = this.db.quoteTableName(table); } tables[i] = table + ' ' + this.db.quoteTableName(i); } else if (table.indexOf('(') === -1) { var matches = /^(.*?)(?:\s+as|)\s+([^ ]+)/i.exec(table); if (matches !== null) { // with alias tables[i] = this.db.quoteTableName(matches[1]) + ' ' + this.db.quoteTableName(matches[2]); } else { tables[i] = this.db.quoteTableName(table); } } }); return Promise.all(promises).then(() => { return _isObject(tables) ? _values(tables) : tables; }); } /** * @param {string|object} condition * @param {object} params the binding parameters to be populated * @return {string} the WHERE clause built from [[Query::where]]. */ buildWhere(condition, params) { return this.buildCondition(condition, params).then(where => { return Promise.resolve(where === '' ? '' : 'WHERE ' + where); }); } /** * @param {string[]} columns * @return {string} the GROUP BY clause */ buildGroupBy(columns) { return Promise.resolve(_isEmpty(columns) ? '' : 'GROUP BY ' + this.buildColumns(columns)); } /** * @param {string|object} condition * @param {object} params the binding parameters to be populated * @return {string} the HAVING clause built from [[Query::having]]. */ buildHaving(condition, params) { return this.buildCondition(condition, params).then(having => { return Promise.resolve(having ? 'HAVING ' + having : ''); }); } /** * @param {object} columns * @return {string} the ORDER BY clause built from [[Query::orderBy]]. */ buildOrderBy(columns) { if (_isEmpty(columns)) { return Promise.resolve(''); } var orders = []; _each(columns, (direction, name) => { if (direction instanceof Expression) { orders.push(direction.expression); } else { orders.push(this.db.quoteColumnName(name) + (direction.toLowerCase() === 'desc' ? ' DESC' : '')); } }); return Promise.resolve('ORDER BY ' + orders.join(', ')); } /** * @param {number} limit * @param {number} offset * @return {string} the LIMIT and OFFSET clauses */ buildLimit(limit, offset) { var sql = ''; if (this._hasLimit(limit)) { sql = 'LIMIT ' + limit; } if (this._hasOffset(offset)) { sql += ' OFFSET ' + offset; } return Promise.resolve(_trimStart(sql)); } /** * Checks to see if the given limit is effective. * @param {*} limit the given limit * @return {boolean} whether the limit is effective */ _hasLimit(limit) { limit = parseInt(limit); return !_isNaN(limit) && limit >= 0; } /** * Checks to see if the given offset is effective. * @param {*} offset the given offset * @return {boolean} whether the offset is effective */ _hasOffset(offset) { offset = parseInt(offset); return !_isNaN(offset) && offset > 0; } /** * @param {[]} unions * @param {object} params the binding parameters to be populated * @return {Promise} the UNION clause built from [[Query::union]]. */ buildUnion(unions, params) { if (_isEmpty(unions)) { return Promise.resolve(''); } var result = ''; var promises = []; _each(unions, (union, i) => { if (union.query instanceof Query) { var promise = this.build(union.query, params).then(buildResult => { unions[i].query = buildResult[0]; params = _extend(params, buildResult[1]); }); promises.push(promise); } }); return Promise.all(promises).then(() => { _each(unions, (union, i) => { result += 'UNION ' + (union.all ? 'ALL ' : '') + '( ' + unions[i].query + ' ) '; }); return _trim(result); }); } /** * Processes columns and properly quote them if necessary. * It will join all columns into a string with comma as separators. * @param {string|string[]} columns the columns to be processed * @return {string} the processing result */ buildColumns(columns) { if (_isString(columns)) { if (columns.indexOf('(') !== -1) { return Promise.resolve(columns); } columns = _words(columns, /[^,]+/g); } columns = _map(columns, column => { if (column instanceof Expression) { return column.expression; } else if (column.indexOf('(') === -1) { return this.db.quoteColumnName(column); } return column; }); return Promise.resolve(columns.join(', ')); } /** * Parses the condition specification and generates the corresponding SQL expression. * @param {string|[]} condition the condition specification. Please refer to [[Query::where()]] * on how to specify a condition. * @param {object} params the binding parameters to be populated * @return {string} the generated SQL expression * @throws InvalidParamException if the condition is in bad format */ buildCondition(condition, params) { if (_isEmpty(condition)) { return Promise.resolve(''); } if (!_isArray(condition) && !_isObject(condition)) { return Promise.resolve(String(condition)); } if (condition[0]) { // operator format: operator, operand 1, operand 2, ... var operator = condition[0].toUpperCase(); var method = _has(this._conditionBuilders, operator) ? this._conditionBuilders[operator] : 'buildSimpleCondition'; condition = [].concat(condition); condition.shift(); return this[method].call(this, operator, condition, params); } else { // hash format: 'column1': 'value1', 'column2': 'value2', ... return this.buildHashCondition(condition, params); } } /** * Creates a condition based on column-value pairs. * @param {object} condition the condition specification. * @param {object} params the binding parameters to be populated * @return {string} the generated SQL expression */ buildHashCondition(condition, params) { var parts = []; var promises = []; _each(condition, (value, column) => { if (_isArray(value) || value instanceof Query) { // IN condition var promise = this.buildInCondition('IN', [ column, value ], params).then(condition => { parts.push(condition); }); promises.push(promise); } else { if (column.indexOf('(') === -1) { column = this.db.quoteColumnName(column); } if (value === null) { parts.push(column + ' IS NULL'); } else if (value instanceof Expression) { parts.push(column + '=' + value.expression); params = _extend(params, value.params); } else { var phName = QueryBuilder.PARAM_PREFIX + _size(params); parts.push(column + '=' + phName); params[phName] = value; } } }); return Promise.all(promises).then(() => { return parts.length === 1 ? parts[0] : '(' + parts.join(') AND (') + ')'; }); } /** * Connects two or more SQL expressions with the `AND` or `OR` operator. * @param {string} operator the operator to use for connecting the given operands * @param {[]} operands the SQL expressions to connect. * @param {object} params the binding parameters to be populated * @return {string} the generated SQL expression */ buildAndCondition(operator, operands, params) { var parts = []; var promises = []; _each(operands, operand => { if (_isArray(operand) || _isObject(operand)) { var promise = this.buildCondition(operand, params).then(condition => { parts.push(condition); }); promises.push(promise); } else if (operand) { parts.push(operand); } }); return Promise.all(promises).then(() => { return Promise.resolve(parts.length > 0 ? '(' + parts.join(') ' + operator + ' (') + ')' : ''); }); } /** * Inverts an SQL expressions with `NOT` operator. * @param {string} operator the operator to use for connecting the given operands * @param {[]} operands the SQL expressions to connect. * @param {object} params the binding parameters to be populated * @return {string} the generated SQL expression * @throws InvalidParamException if wrong number of operands have been given. */ buildNotCondition(operator, operands, params) { if (operands.length !== 1) { throw new InvalidParamException('Operator \'operator\' requires exactly one operand.'); } return Promise.resolve().then(() => { if (_isArray(operands[0])) { return this.buildCondition(operands[0], params); } return operands[0]; }).then(operand => { return operand ? operator + ' (' + operand + ')' : ''; }); } /** * Creates an SQL expressions with the `BETWEEN` operator. * @param {string} operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`) * @param {[]} operands the first operand is the column name. The second and third operands * describe the interval that column value should be in. * @param {object} params the binding parameters to be populated * @return {string} the generated SQL expression * @throws {InvalidParamException} if wrong number of operands have been given. */ buildBetweenCondition(operator, operands, params) { if (operands.length !== 3) { throw new InvalidParamException('Operator `' + operator + '` requires three operands.'); } var column = operands[0]; var value1 = operands[1]; var value2 = operands[2]; if (column.indexOf('(') === -1) { column = this.db.quoteColumnName(column); } var phName1 = null; var phName2 = null; if (value1 instanceof Expression) { _each(value1.params, (n, v) => { params[n] = v; }); phName1 = value1.expression; } else { phName1 = QueryBuilder.PARAM_PREFIX + _size(params); params[phName1] = value1; } if (value2 instanceof Expression) { _each(value2.params, (n, v) => { params[n] = v; }); phName2 = value2.expression; } else { phName2 = QueryBuilder.PARAM_PREFIX + _size(params); params[phName2] = value2; } return Promise.resolve(column + ' ' + operator + ' ' + phName1 + ' AND ' + phName2); } /** * Creates an SQL expressions with the `IN` operator. * @param {string} operator the operator to use (e.g. `IN` or `NOT IN`) * @param {[]} operands the first operand is the column name. If it is an array * a composite IN condition will be generated. * The second operand is an array of values that column value should be among. * If it is an empty array the generated expression will be a `false` value if * operator is `IN` and empty if operator is `NOT IN`. * @param {object} params the binding parameters to be populated * @return {string} the generated SQL expression * @throws {InvalidParamException} if wrong number of operands have been given. */ buildInCondition(operator, operands, params) { if (operands.length !== 2) { throw new InvalidParamException('Operator `' + operator + '` requires two operands.'); } var column = operands[0]; var values = operands[1]; if (_isEmpty(values) || _isEmpty(column)) { return Promise.resolve(operator === 'IN' ? '0=1' : ''); } if (values instanceof Query) { // sub-query return this.build(values, params).then(buildResult => { var sql = buildResult[0]; params = _extend(params, buildResult[1]); if (!_isArray(column)) { column = [column]; } _each(column, (col, i) => { if (col.indexOf('(') === -1) { column[i] = this.db.quoteColumnName(col); } }); return '(' + column.join(', ') + ') ' + operator + ' (' + sql + ')'; }); } if (!_isArray(values)) { values = [values]; } if (_isArray(column) && column.length > 1) { return this._buildCompositeInCondition(operator, column, values, params); } if (_isArray(column)) { column = column[0]; } var inValues = []; _each(values, value => { if (_isObject(value)) { value = _has(value, column) ? value[column] : null; } if (value === null) { inValues.push('NULL'); } else if (value instanceof Expression) { inValues.push(value.expression); params = _extend(params, value.params); } else { var phName = QueryBuilder.PARAM_PREFIX + _size(params); params[phName] = value; inValues.push(phName); } }); if (column.indexOf('(') === -1) { column = this.db.quoteColumnName(column); } var result = inValues.length > 1 ? column + ' ' + operator + ' (' + inValues.join(', ') + ')' : column + (operator === 'IN' ? '=' : '<>') + inValues[0]; return Promise.resolve(result); } /** * * @param {string} operator * @param {string[]} columns * @param {string[]} values * @param {object} params * @returns {string} * @private */ _buildCompositeInCondition(operator, columns, values, params) { var vss = []; _each(values, value => { var vs = []; _each(columns, column => { if (_has(value, column)) { var phName = QueryBuilder.PARAM_PREFIX + _size(params); params[phName] = value[column]; vs.push(phName); } else { vs.push('NULL'); } }); vss.push('(' + vs.join(', ') + ')'); }); _each(columns, (column, i) => { if (column.indexOf('(') === -1) { columns[i] = this.db.quoteColumnName(column); } }); return Promise.resolve('(' + columns.join(', ') + ') ' + operator + ' (' + vss.join(', ') + ')'); } /** * Creates an SQL expressions with the `LIKE` operator. * @param {string} operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`) * @param {[]} operands an array of two or three operands * * - The first operand is the column name. * - The second operand is a single value or an array of values that column value * should be compared with. If it is an empty array the generated expression will * be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator * is `NOT LIKE` or `OR NOT LIKE`. * - An optional third operand can also be provided to specify how to escape special characters * in the value(s). The operand should be an array of mappings from the special characters to their * escaped counterparts. If this operand is not provided, a default escape mapping will be used. * You may use `false` or an empty array to indicate the values are alread