knex/src/query/compiler.js

572 lines
16 KiB
JavaScript
Raw Normal View History

2015-05-09 13:58:18 -04:00
// Query Compiler
// -------
import * as helpers from '../helpers';
import Raw from '../raw';
import JoinClause from './joinclause';
2016-09-16 16:04:11 -04:00
import debug from 'debug'
2016-05-11 15:26:53 +02:00
import {
assign, bind, compact, groupBy, isEmpty, isString, isUndefined, map, omitBy,
reduce
} from 'lodash';
2015-05-09 13:58:18 -04:00
2016-06-17 09:19:20 -07:00
import uuid from 'node-uuid';
2016-09-16 16:04:11 -04:00
const debugBindings = debug('knex:bindings')
2015-05-09 13:58:18 -04:00
// The "QueryCompiler" takes all of the query statements which
// have been gathered in the "QueryBuilder" and turns them into a
// properly formatted / bound query string.
function QueryCompiler(client, builder) {
this.client = client
this.method = builder._method || 'select';
this.options = builder._options;
this.single = builder._single;
this.timeout = builder._timeout || false;
2016-05-26 11:06:33 -07:00
this.cancelOnTimeout = builder._cancelOnTimeout || false;
this.grouped = groupBy(builder._statements, 'grouping');
2016-02-15 17:06:08 +01:00
this.formatter = client.formatter()
2015-05-09 13:58:18 -04:00
}
const components = [
2015-05-09 13:58:18 -04:00
'columns', 'join', 'where', 'union', 'group',
'having', 'order', 'limit', 'offset', 'lock'
];
assign(QueryCompiler.prototype, {
// Used when the insert call is empty.
_emptyInsertValue: 'default values',
// Collapse the builder into a single object
toSQL(method, tz) {
this._undefinedInWhereClause = false;
2015-05-09 13:58:18 -04:00
method = method || this.method
let val = this[method]()
const defaults = {
method,
2015-05-09 13:58:18 -04:00
options: reduce(this.options, assign, {}),
2016-02-15 17:06:08 +01:00
timeout: this.timeout,
2016-05-26 11:06:33 -07:00
cancelOnTimeout: this.cancelOnTimeout,
2016-06-17 08:31:42 -07:00
bindings: this.formatter.bindings,
2016-06-17 09:33:03 -07:00
__knexQueryUid: uuid.v4()
2015-05-09 13:58:18 -04:00
};
2016-03-02 16:52:32 +01:00
if (isString(val)) {
2015-05-09 13:58:18 -04:00
val = {sql: val};
}
defaults.bindings = defaults.bindings || [];
if (method === 'select') {
if(this.single.as) {
defaults.as = this.single.as;
}
}
if(this._undefinedInWhereClause) {
2016-09-16 16:04:11 -04:00
debugBindings(defaults.bindings)
2016-06-14 19:50:51 +10:00
throw new Error(
`Undefined binding(s) detected when compiling ` +
`${method.toUpperCase()} query: ${val.sql}`
);
2015-05-09 13:58:18 -04:00
}
2015-05-09 13:58:18 -04:00
return assign(defaults, val);
},
// Compiles the `select` statement, or nested sub-selects by calling each of
// the component compilers, trimming out the empties, and returning a
// generated query string.
select() {
let sql = this.with();
const statements = components.map(component =>
this[component](this)
);
sql += compact(statements).join(' ');
return sql;
2015-05-09 13:58:18 -04:00
},
pluck() {
2016-09-13 09:56:53 -04:00
let toPluck = this.single.pluck
if (toPluck.indexOf('.') !== -1) {
toPluck = toPluck.split('.').slice(-1)[0]
}
2015-05-09 13:58:18 -04:00
return {
sql: this.select(),
2016-09-13 09:56:53 -04:00
pluck: toPluck
2015-05-09 13:58:18 -04:00
};
},
// Compiles an "insert" query, allowing for multiple
// inserts using a single query statement.
insert() {
const insertValues = this.single.insert || [];
let sql = this.with() + `insert into ${this.tableName} `;
2015-05-09 13:58:18 -04:00
if (Array.isArray(insertValues)) {
if (insertValues.length === 0) {
return ''
}
2016-03-02 16:52:32 +01:00
} else if (typeof insertValues === 'object' && isEmpty(insertValues)) {
2015-05-09 13:58:18 -04:00
return sql + this._emptyInsertValue
}
const insertData = this._prepInsert(insertValues);
2015-05-09 13:58:18 -04:00
if (typeof insertData === 'string') {
sql += insertData;
} else {
if (insertData.columns.length) {
sql += `(${this.formatter.columnize(insertData.columns)}`
2015-05-09 13:58:18 -04:00
sql += ') values ('
let i = -1
2015-05-09 13:58:18 -04:00
while (++i < insertData.values.length) {
if (i !== 0) sql += '), ('
sql += this.formatter.parameterize(insertData.values[i], this.client.valueForUndefined)
2015-05-09 13:58:18 -04:00
}
sql += ')';
} else if (insertValues.length === 1 && insertValues[0]) {
sql += this._emptyInsertValue
} else {
sql = ''
}
}
return sql;
},
// Compiles the "update" query.
update() {
2015-05-09 13:58:18 -04:00
// Make sure tableName is processed by the formatter first.
const { tableName } = this;
const updateData = this._prepUpdate(this.single.update);
const wheres = this.where();
return this.with() + `update ${tableName}` +
2015-05-09 13:58:18 -04:00
' set ' + updateData.join(', ') +
(wheres ? ` ${wheres}` : '');
2015-05-09 13:58:18 -04:00
},
// Compiles the columns in the query, specifying if an item was distinct.
columns() {
let distinct = false;
2015-05-09 13:58:18 -04:00
if (this.onlyUnions()) return ''
const columns = this.grouped.columns || []
let i = -1, sql = [];
2015-05-09 13:58:18 -04:00
if (columns) {
while (++i < columns.length) {
const stmt = columns[i];
2015-05-09 13:58:18 -04:00
if (stmt.distinct) distinct = true
if (stmt.type === 'aggregate') {
sql.push(this.aggregate(stmt))
}
2015-05-09 13:58:18 -04:00
else if (stmt.value && stmt.value.length > 0) {
sql.push(this.formatter.columnize(stmt.value))
}
}
}
if (sql.length === 0) sql = ['*'];
return `select ${distinct ? 'distinct ' : ''}` +
sql.join(', ') + (this.tableName ? ` from ${this.tableName}` : '');
2015-05-09 13:58:18 -04:00
},
aggregate(stmt) {
const val = stmt.value;
const splitOn = val.toLowerCase().indexOf(' as ');
const distinct = stmt.aggregateDistinct ? 'distinct ' : '';
2015-05-09 13:58:18 -04:00
// Allows us to speciy an alias for the aggregate types.
if (splitOn !== -1) {
const col = val.slice(0, splitOn);
const alias = val.slice(splitOn + 4);
return (
`${stmt.method}(${distinct + this.formatter.wrap(col)}) ` +
`as ${this.formatter.wrap(alias)}`
);
2015-05-09 13:58:18 -04:00
}
return `${stmt.method}(${distinct + this.formatter.wrap(val)})`;
2015-05-09 13:58:18 -04:00
},
// Compiles all each of the `join` clauses on the query,
// including any nested join queries.
join() {
let sql = '';
let i = -1;
const joins = this.grouped.join;
2015-05-09 13:58:18 -04:00
if (!joins) return '';
while (++i < joins.length) {
const join = joins[i];
const table = join.schema ? `${join.schema}.${join.table}` : join.table;
2015-05-09 13:58:18 -04:00
if (i > 0) sql += ' '
if (join.joinType === 'raw') {
sql += this.formatter.unwrapRaw(join.table)
} else {
2015-08-09 20:22:39 -03:00
sql += join.joinType + ' join ' + this.formatter.wrap(table)
let ii = -1
2015-05-09 13:58:18 -04:00
while (++ii < join.clauses.length) {
const clause = join.clauses[ii]
2016-05-11 16:22:15 +02:00
if (ii > 0) {
sql += ` ${clause.bool} `;
2016-05-11 16:22:15 +02:00
} else {
sql += ` ${clause.type === 'onUsing' ? 'using' : 'on'} `;
2016-05-11 16:22:15 +02:00
}
const val = this[clause.type].call(this, clause);
2016-05-11 15:26:53 +02:00
if (val) {
sql += val;
}
2015-05-09 13:58:18 -04:00
}
}
}
return sql;
},
// Compiles all `where` statements on the query.
where() {
const wheres = this.grouped.where;
2015-05-09 13:58:18 -04:00
if (!wheres) return;
const sql = [];
let i = -1;
2015-05-09 13:58:18 -04:00
while (++i < wheres.length) {
const stmt = wheres[i]
2016-05-30 21:09:15 +02:00
if(stmt.hasOwnProperty('value') && helpers.containsUndefined(stmt.value)) {
this._undefinedInWhereClause = true;
}
const val = this[stmt.type](stmt)
2015-05-09 13:58:18 -04:00
if (val) {
if (sql.length === 0) {
sql[0] = 'where'
} else {
sql.push(stmt.bool)
}
sql.push(val)
}
}
return sql.length > 1 ? sql.join(' ') : '';
},
group() {
2015-05-09 13:58:18 -04:00
return this._groupsOrders('group');
},
order() {
2015-05-09 13:58:18 -04:00
return this._groupsOrders('order');
},
// Compiles the `having` statements.
having() {
const havings = this.grouped.having;
2015-05-09 13:58:18 -04:00
if (!havings) return '';
const sql = ['having'];
for (let i = 0, l = havings.length; i < l; i++) {
let str = '';
const s = havings[i];
2015-05-09 13:58:18 -04:00
if (i !== 0) str = s.bool + ' ';
if (s.type === 'havingBasic') {
sql.push(str + this.formatter.columnize(s.column) + ' ' +
this.formatter.operator(s.operator) + ' ' + this.formatter.parameter(s.value));
} else {
if(s.type === 'whereWrapped'){
const val = this.whereWrapped(s)
2015-05-09 13:58:18 -04:00
if (val) sql.push(val)
} else {
sql.push(str + this.formatter.unwrapRaw(s.value));
}
}
}
return sql.length > 1 ? sql.join(' ') : '';
},
// Compile the "union" queries attached to the main query.
union() {
const onlyUnions = this.onlyUnions();
const unions = this.grouped.union;
2015-05-09 13:58:18 -04:00
if (!unions) return '';
let sql = '';
for (let i = 0, l = unions.length; i < l; i++) {
const union = unions[i];
2015-05-09 13:58:18 -04:00
if (i > 0) sql += ' ';
if (i > 0 || !onlyUnions) sql += union.clause + ' ';
const statement = this.formatter.rawOrFn(union.value);
2015-05-09 13:58:18 -04:00
if (statement) {
if (union.wrap) sql += '(';
sql += statement;
if (union.wrap) sql += ')';
}
}
return sql;
},
// If we haven't specified any columns or a `tableName`, we're assuming this
// is only being used for unions.
onlyUnions() {
2015-05-09 13:58:18 -04:00
return (!this.grouped.columns && this.grouped.union && !this.tableName);
},
limit() {
const noLimit = !this.single.limit && this.single.limit !== 0;
2015-05-09 13:58:18 -04:00
if (noLimit) return '';
return `limit ${this.formatter.parameter(this.single.limit)}`;
2015-05-09 13:58:18 -04:00
},
offset() {
2015-05-09 13:58:18 -04:00
if (!this.single.offset) return '';
return `offset ${this.formatter.parameter(this.single.offset)}`;
2015-05-09 13:58:18 -04:00
},
// Compiles a `delete` query.
del() {
2015-05-09 13:58:18 -04:00
// Make sure tableName is processed by the formatter first.
const { tableName } = this;
const wheres = this.where();
return this.with() + `delete from ${tableName}` +
(wheres ? ` ${wheres}` : '');
2015-05-09 13:58:18 -04:00
},
// Compiles a `truncate` query.
truncate() {
return `truncate ${this.tableName}`;
2015-05-09 13:58:18 -04:00
},
// Compiles the "locks".
lock() {
2015-05-09 13:58:18 -04:00
if (this.single.lock) {
if (!this.client.transacting) {
helpers.warn('You are attempting to perform a "lock" command outside of a transaction.')
} else {
return this[this.single.lock]()
}
}
},
// Compile the "counter".
counter() {
const { counter } = this.single;
const toUpdate = {};
2015-05-09 13:58:18 -04:00
toUpdate[counter.column] = this.client.raw(this.formatter.wrap(counter.column) +
' ' + (counter.symbol || '+') +
' ' + counter.amount);
this.single.update = toUpdate;
return this.update();
},
2016-05-11 15:26:53 +02:00
// On Clause
// ------
onWrapped(clause) {
const self = this;
2016-05-11 15:26:53 +02:00
const wrapJoin = new JoinClause();
2016-05-12 09:09:42 +02:00
clause.value.call(wrapJoin, wrapJoin);
2016-05-11 15:26:53 +02:00
let sql = '';
2016-05-11 15:26:53 +02:00
wrapJoin.clauses.forEach(function(wrapClause, ii) {
if (ii > 0) {
sql += ` ${wrapClause.bool} `;
2016-05-11 15:26:53 +02:00
}
const val = self[wrapClause.type](wrapClause);
2016-05-11 15:26:53 +02:00
if (val) {
sql += val;
}
});
if (sql.length) {
return `(${sql})`;
2016-05-11 15:26:53 +02:00
}
return '';
},
onBasic(clause) {
return (
this.formatter.wrap(clause.column) + ' ' +
this.formatter.operator(clause.operator) + ' ' +
this.formatter.wrap(clause.value)
);
2016-05-11 15:26:53 +02:00
},
onRaw(clause) {
return this.formatter.unwrapRaw(clause.value);
},
2016-05-11 16:22:15 +02:00
onUsing(clause) {
return this.formatter.wrap(clause.column);
},
2015-05-09 13:58:18 -04:00
// Where Clause
// ------
whereIn(statement) {
2015-05-09 13:58:18 -04:00
if (Array.isArray(statement.column)) return this.multiWhereIn(statement);
return this.formatter.wrap(statement.column) + ' ' + this._not(statement, 'in ') +
this.wrap(this.formatter.parameterize(statement.value));
},
multiWhereIn(statement) {
let i = -1, sql = `(${this.formatter.columnize(statement.column)}) `
2015-05-09 13:58:18 -04:00
sql += this._not(statement, 'in ') + '(('
while (++i < statement.value.length) {
if (i !== 0) sql += '),('
sql += this.formatter.parameterize(statement.value[i])
}
return sql + '))'
},
whereNull(statement) {
2015-05-09 13:58:18 -04:00
return this.formatter.wrap(statement.column) + ' is ' + this._not(statement, 'null');
},
// Compiles a basic "where" clause.
whereBasic(statement) {
2015-05-09 13:58:18 -04:00
return this._not(statement, '') +
this.formatter.wrap(statement.column) + ' ' +
this.formatter.operator(statement.operator) + ' ' +
this.formatter.parameter(statement.value);
},
whereExists(statement) {
2015-05-09 13:58:18 -04:00
return this._not(statement, 'exists') + ' (' + this.formatter.rawOrFn(statement.value) + ')';
},
whereWrapped(statement) {
const val = this.formatter.rawOrFn(statement.value, 'where')
2015-05-09 13:58:18 -04:00
return val && this._not(statement, '') + '(' + val.slice(6) + ')' || '';
},
whereBetween(statement) {
2015-05-09 13:58:18 -04:00
return this.formatter.wrap(statement.column) + ' ' + this._not(statement, 'between') + ' ' +
2016-03-02 16:52:32 +01:00
map(statement.value, bind(this.formatter.parameter, this.formatter)).join(' and ');
2015-05-09 13:58:18 -04:00
},
// Compiles a "whereRaw" query.
whereRaw(statement) {
return this._not(statement, '') + this.formatter.unwrapRaw(statement.value);
2015-05-09 13:58:18 -04:00
},
wrap(str) {
if (str.charAt(0) !== '(') return `(${str})`;
2015-05-09 13:58:18 -04:00
return str;
},
// Compiles all `with` statements on the query.
with() {
if(!this.grouped.with || !this.grouped.with.length) {
return '';
}
const withs = this.grouped.with;
if (!withs) return;
const sql = [];
let i = -1;
while (++i < withs.length) {
const stmt = withs[i]
const val = this[stmt.type](stmt)
sql.push(val);
}
return 'with ' + sql.join(', ') + ' ';
},
withWrapped(statement) {
const val = this.formatter.rawOrFn(statement.value);
return val && this.formatter.columnize(statement.alias) + ' as (' + val + ')' || '';
},
withRaw(statement) {
return this.formatter.columnize(statement.alias) + ' as (' +
this.formatter.unwrapRaw(statement.value) + ')';
},
2015-05-09 13:58:18 -04:00
// Determines whether to add a "not" prefix to the where clause.
_not(statement, str) {
if (statement.not) return `not ${str}`;
2015-05-09 13:58:18 -04:00
return str;
},
_prepInsert(data) {
const isRaw = this.formatter.rawOrFn(data);
2015-05-09 13:58:18 -04:00
if (isRaw) return isRaw;
let columns = [];
const values = [];
2015-05-09 13:58:18 -04:00
if (!Array.isArray(data)) data = data ? [data] : [];
let i = -1
2015-05-09 13:58:18 -04:00
while (++i < data.length) {
if (data[i] == null) break;
if (i === 0) columns = Object.keys(data[i]).sort()
const row = new Array(columns.length)
const keys = Object.keys(data[i])
let j = -1
2015-05-09 13:58:18 -04:00
while (++j < keys.length) {
const key = keys[j];
let idx = columns.indexOf(key);
2015-05-09 13:58:18 -04:00
if (idx === -1) {
columns = columns.concat(key).sort()
idx = columns.indexOf(key)
let k = -1
2015-05-09 13:58:18 -04:00
while (++k < values.length) {
values[k].splice(idx, 0, undefined)
}
row.splice(idx, 0, undefined)
}
row[idx] = data[i][key]
}
values.push(row)
}
return {
columns,
values
2015-05-09 13:58:18 -04:00
};
},
// "Preps" the update.
_prepUpdate(data) {
2016-03-02 16:52:32 +01:00
data = omitBy(data, isUndefined)
const vals = []
const sorted = Object.keys(data).sort()
let i = -1
2015-05-09 13:58:18 -04:00
while (++i < sorted.length) {
vals.push(
this.formatter.wrap(sorted[i]) +
' = ' +
this.formatter.parameter(data[sorted[i]])
);
2015-05-09 13:58:18 -04:00
}
return vals;
},
// Compiles the `order by` statements.
_groupsOrders(type) {
const items = this.grouped[type];
2015-05-09 13:58:18 -04:00
if (!items) return '';
const { formatter } = this;
const sql = items.map(item => {
const column = item.value instanceof Raw
? formatter.unwrapRaw(item.value)
: formatter.columnize(item.value);
const direction = type === 'order' && item.type !== 'orderByRaw'
? ` ${formatter.direction(item.direction)}`
: '';
return column + direction;
2015-05-09 13:58:18 -04:00
});
return sql.length ? type + ' by ' + sql.join(', ') : '';
}
})
QueryCompiler.prototype.first = QueryCompiler.prototype.select;
// Get the table name, wrapping it if necessary.
// Implemented as a property to prevent ordering issues as described in #704.
Object.defineProperty(QueryCompiler.prototype, 'tableName', {
get() {
2015-05-09 13:58:18 -04:00
if(!this._tableName) {
// Only call this.formatter.wrap() the first time this property is accessed.
let tableName = this.single.table;
const schemaName = this.single.schema;
2015-08-09 20:22:39 -03:00
if (tableName && schemaName) tableName = `${schemaName}.${tableName}`;
this._tableName = tableName ? this.formatter.wrap(tableName) : '';
2015-05-09 13:58:18 -04:00
}
return this._tableName;
}
});
export default QueryCompiler;