knex/lib/query/index.js
2014-03-14 16:56:55 -04:00

676 lines
19 KiB
JavaScript

// Builder
// -------
module.exports = function(client) {
var _ = require('lodash');
var Raw = require('../raw');
var SqlString = require('../sqlstring');
var JoinClause = require('./joinclause');
var helpers = require('../helpers');
var bindings = helpers.bindings;
var single = helpers.single;
// Constructor for the builder instance, typically called from
// `knex.builder`, accepting the current `knex` instance,
// and pulling out the `client` and `grammar` from the current
// knex instance.
var QueryBuilder = function() {
this.statements = [];
this.errors = [];
this.flags = {};
// The "method" defaults to select unless otherwise specified.
this._method = 'select';
// Internal flags used in the builder.
this._joinFlag = 'inner';
this._boolFlag = 'and';
};
QueryBuilder.prototype = {
constructor: QueryBuilder,
toString: function() {
return '[object Knex:QueryBuilder]';
},
// Convert the current object `toString`. Assumes there's a `toSql`
// method returning an array of objects, each of which contains an
// "sql" and "bindings" property. These are passed into the `SqlString.format`
toQuery: function() {
var data = this.toSql();
if (this.errors.length > 0) throw this.errors[0];
if (!_.isArray(data)) data = [data];
return _.map(data, function(statement) {
return SqlString.format(statement.sql, statement.bindings);
}).join(';\n');
},
toSql: function(target) {
return new client.QueryCompiler(this).compiled(target || this._method);
},
// Useful for debugging, allows tapping into the sql chain at a specific
// point in the query building process.
tapSql: function(handler) {
var sql = this.toSql();
handler(sql.sql, sql.bindings);
return this;
},
// Create a shallow clone of the current query builder.
clone: function() {
var cloned = new client.QueryBuilder();
cloned.statements = this.statements.slice();
cloned.errors = this.errors.slice();
cloned.flags = JSON.parse(JSON.stringify(this.flags));
return cloned;
},
// Sets the `tableName` on the query.
from: function() {
return this.table.apply(this, arguments);
},
// Alias to "from", for "insert" statements
// e.g. builder.insert({a: value}).into('tableName')
into: function() {
return this.table.apply(this, arguments);
},
table: single(function(tableName) {
return {
grouping: 'table',
value: tableName
};
}),
// Adds a column or columns to the list of "columns"
// being selected on the query.
column: function() {
this.statements.push({
grouping: 'columns',
value: helpers.normalizeArr.apply(null, arguments)
});
return this;
},
columns: function() {
return this.column.apply(this, arguments);
},
// Adds a `distinct` clause to the query.
distinct: function() {
this.statements.push({
grouping: 'columns',
value: helpers.normalizeArr.apply(null, arguments),
distinct: true
});
return this;
},
// Adds a join clause to the query, allowing for advanced joins
// with an anonymous function as the second argument.
join: function(table, first, operator, second) {
var i, l, args = new Array(arguments.length);
for (i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
if (args.length === 5) {
helpers.deprecate('The five argument join syntax is now deprecated, ' +
'please check the docs and update your code.');
return this._joinType(args[4]).join(table, first, operator, second);
}
var join;
if (_.isFunction(first)) {
if (args.length > 2) {
helpers.deprecate('The [table, fn, type] join syntax is deprecated, ' +
'please check the docs and update your code.');
return this._joinType(args[2]).join(table, first);
}
join = new JoinClause(table, this._joinType());
first.call(join, join);
} else {
join = new JoinClause(table, this._joinType());
join.on.apply(join, args.slice(1));
}
this.statements.push(join);
return this;
},
// JOIN blocks:
innerJoin: function() {
return this._joinType('inner').join.apply(this, arguments);
},
leftJoin: function() {
return this._joinType('left').join.apply(this, arguments);
},
leftOuterJoin: function() {
return this._joinType('left outer').join.apply(this, arguments);
},
rightJoin: function() {
return this._joinType('right').join.apply(this, arguments);
},
rightOuterJoin: function() {
return this._joinType('right outer').join.apply(this, arguments);
},
outerJoin: function() {
return this._joinType('outer').join.apply(this, arguments);
},
fullOuterJoin: function() {
return this._joinType('full outer').join.apply(this, arguments);
},
crossJoin: function() {
return this._joinType('cross').join.apply(this, arguments);
},
// The where function can be used in several ways:
// The most basic is `where(key, value)`, which expands to
// where key = value.
where: function(column, operator, value) {
// Check if the column is a function, in which case it's
// a where statement wrapped in parens.
if (_.isFunction(column)) {
return this.whereWrapped(column);
}
// Allow a raw statement to be passed along to the query.
if (column instanceof Raw) return this.whereRaw(column);
// Allows `where({id: 2})` syntax.
if (_.isObject(column)) {
var boolVal = this._bool();
for (var key in column) {
this[boolVal + 'Where'](key, '=', column[key]);
}
return this;
}
// Enable the where('key', value) syntax, only when there
// are explicitly two arguments passed, so it's not possible to
// do where('key', '!=') and have that turn into where key != null
if (arguments.length === 2) {
value = operator;
operator = '=';
}
// If the value is null, and the operator is equals, assume that we're
// going for a `whereNull` statement here.
if (value === null && operator === '=') {
return this.whereNull(column);
}
this.statements.push({
grouping: 'where',
type: 'whereBasic',
column: column,
operator: operator,
value: value,
bool: this._bool()
});
return this;
},
// Alias to `where`, for internal builder consistency.
andWhere: function() {
return this.where.apply(this, arguments);
},
// Adds an `or where` clause to the query.
orWhere: function() {
return this._bool('or').where.apply(this, arguments);
},
// Adds a raw `where` clause to the query.
whereRaw: function(sql, bindings) {
var raw = (sql instanceof Raw ? sql : new Raw(sql, bindings));
this.statements.push({
grouping: 'where',
type: 'whereRaw',
value: raw,
bool: this._bool()
});
return this;
},
// Helper for compiling any advanced `where` queries.
whereWrapped: function(callback) {
this.statements.push({
grouping: 'where',
type: 'whereWrapped',
value: callback,
bool: this._bool()
});
return this;
},
// Adds a raw `or where` clause to the query.
orWhereRaw: function(sql, bindings) {
return this._bool('or').whereRaw(sql, bindings);
},
// Adds a `where exists` clause to the query.
whereExists: function(callback, not) {
this.statements.push({
grouping: 'where',
type: 'whereExists',
value: callback,
not: not || false,
bool: this._bool(),
});
return this;
},
// Adds an `or where exists` clause to the query.
orWhereExists: function(callback) {
return this._bool('or').whereExists(callback);
},
// Adds a `where not exists` clause to the query.
whereNotExists: function(callback) {
return this.whereExists(callback, true);
},
// Adds a `or where not exists` clause to the query.
orWhereNotExists: function(callback) {
return this._bool('or').whereExists(callback, true);
},
// Adds a `where in` clause to the query.
whereIn: function(column, values, not) {
this.statements.push({
grouping: 'where',
type: 'whereIn',
column: column,
value: values,
not: not || false,
bool: this._bool()
});
return this;
},
// Adds a `or where in` clause to the query.
orWhereIn: function(column, values) {
return this._bool('or').whereIn(column, values);
},
// Adds a `where not in` clause to the query.
whereNotIn: function(column, values) {
return this.whereIn(column, values, true);
},
// Adds a `or where not in` clause to the query.
orWhereNotIn: function(column, values) {
return this._bool('or').whereIn(column, values, true);
},
// Adds a `where null` clause to the query.
whereNull: function(column, not) {
this.statements.push({
grouping: 'where',
type: 'whereNull',
column: column,
not: not || false,
bool: this._bool()
});
return this;
},
// Adds a `or where null` clause to the query.
orWhereNull: function(column) {
return this._bool('or').whereNull(column);
},
// Adds a `where not null` clause to the query.
whereNotNull: function(column) {
return this.whereNull(column, ' is not null');
},
// Adds a `or where not null` clause to the query.
orWhereNotNull: function(column) {
return this._bool('or').whereNull(column, ' is not null');
},
// Adds a `where between` clause to the query.
whereBetween: function(column, values, not) {
if (!_.isArray(values)) {
return this._error('The second argument to whereBetween must be an array.');
}
if (values.length !== 2) {
return this._error('You must specify 2 values for the whereBetween clause');
}
this.statements.push({
grouping: 'where',
type: 'whereBetween',
column: column,
value: values,
not: not || false,
bool: this._bool()
});
return this;
},
// Adds a `where not between` clause to the query.
whereNotBetween: function(column, values) {
return this.whereBetween(column, values, true);
},
// Adds a `or where between` clause to the query.
orWhereBetween: function(column, values) {
return this._bool('or').whereBetween(column, values);
},
// Adds a `or where not between` clause to the query.
orWhereNotBetween: function(column, values) {
return this._bool('or').whereNotBetwen(column, values);
},
// Adds a `group by` clause to the query.
groupBy: function() {
this.statements.push({
grouping: 'group',
value: helpers.normalizeArr.apply(null, arguments)
});
return this;
},
// Adds a `order by` clause to the query.
orderBy: function(column, direction) {
this.statements.push({
grouping: 'order',
value: column,
direction: direction
});
return this;
},
// Add a union statement to the query.
union: function(callback, wrap) {
if (arguments.length > 1) {
var args = new Array(arguments.length);
for (var i = 0, l = args.length; i < l; i++) {
args[i] = arguments[i];
this.union(args[i]);
}
return this;
}
this.statements.push({
grouping: 'union',
clause: 'union',
value: callback,
wrap: wrap || false
});
return this;
},
// Adds a union all statement to the query.
unionAll: function(callback, wrap) {
this.statements.push({
grouping: 'union',
clause: 'union all',
value: callback,
wrap: wrap || false
});
return this;
},
// Adds a `having` clause to the query.
having: function(column, operator, value) {
if (column instanceof Raw && arguments.length === 1) {
return this.havingRaw(column);
}
this.statements.push({
grouping: 'having',
type: 'havingBasic',
column: column,
operator: operator,
value: value,
bool: this._bool()
});
return this;
},
// Adds a raw `having` clause to the query.
havingRaw: function(sql, bindings) {
var raw = (sql instanceof Raw ? sql : new Raw(sql, bindings));
this.statements.push({
grouping: 'having',
type: 'havingRaw',
value: raw,
bool: this._bool()
});
return this;
},
// Adds an `or having` clause to the query.
orHaving: function(column, operator, value) {
return this._bool('or').having(column, operator, value);
},
// Adds a raw `or having` clause to the query.
orHavingRaw: function(sql, bindings) {
return this._bool('or').havingRaw(sql, bindings);
},
offset: single(function(value) {
return {
grouping: 'offset',
value: value
};
}),
limit: single(function(value) {
return {
grouping: 'limit',
value: value
};
}),
// Retrieve the "count" result of the query.
count: function(column) {
return this._aggregate('count', (column || '*'));
},
// Retrieve the minimum value of a given column.
min: function(column) {
return this._aggregate('min', column);
},
// Retrieve the maximum value of a given column.
max: function(column) {
return this._aggregate('max', column);
},
// Retrieve the sum of the values of a given column.
sum: function(column) {
return this._aggregate('sum', column);
},
// Retrieve the average of the values of a given column.
avg: function(column) {
return this._aggregate('avg', column);
},
// Increments a column's value by the specified amount.
increment: function(column, amount) {
return this._counter(column, amount);
},
// Decrements a column's value by the specified amount.
decrement: function(column, amount) {
return this._counter(column, amount, '-');
},
// Sets the values for a `select` query.
select: function() {
this._method = 'select';
return this.column.apply(this, helpers.args.apply(null, arguments));
},
// Pluck a column from a query.
pluck: function(column) {
this._method = 'pluck';
this.statements.push({
grouping: 'column',
type: 'pluck',
value: column
});
return this;
},
// Sets the values for an `insert` query.
insert: single(function(values, returning) {
this._method = 'insert';
if (!_.isEmpty(returning)) this.returning(returning);
return {
grouping: 'insert',
value: values
};
}),
// Sets the values for an `update` query.
update: single(function(values, returning) {
var ret, obj = {};
this._method = 'update';
var args = helpers.args.apply(null, arguments);
if (_.isString(values)) {
obj[values] = returning;
if (args.length > 2) {
ret = args[2];
}
} else {
obj = values;
ret = args[1];
}
if (!_.isEmpty(ret)) this.returning(ret);
return {
grouping: 'update',
columns: helpers.sortObject(obj)
};
}),
// Alias to del.
"delete": function() {
return this.del.apply(this, arguments);
},
// Executes a delete statement on the query;
del: function() {
this._method = 'delete';
return this;
},
// Truncates a table, ends the query chain.
truncate: function() {
this._method = 'truncate';
return this;
},
// TODO: see if there's a more consistent way to do these
// Sets the returning value for the query.
returning: single(function(returning) {
// var isArr = _.isArray(returning);
// var val = isArr ? this._columnize(returning) : f.wrapValue(returning);
this.flags.returning = returning;
return {
grouping: 'returning',
value: val,
isArr: isArr
};
}),
transacting: function(t) {
this.flags.transacting = t;
return this;
},
options: function(opts) {
this.flags.options = this.flags.options || [];
this.flags.options.push(opts);
return this;
},
debug: function(val) {
this.flags.debug = (val == null ? true : val);
return this;
},
// ----------------------------------------------------------------------
// Runs a "query chain" as created from the fluent-chain module.
_runChain: function(chain) {
var stack = chain.__stack;
// Go over each of the items in the query stack,
// and call the necessary method to process.
for (var i = 0, l = stack.length; i < l; i++) {
var len = this.statements.length;
var obj = stack[i];
this[obj.method].apply(this, obj.args);
}
return this;
},
// Helper for the incrementing/decrementing queries.
_counter: function(column, amount, symbol) {
var amt = parseInt(amount, 10);
if (isNaN(amt)) amt = 1;
var toUpdate = {};
toUpdate[column] = new Raw(f.wrap(column) + ' ' + (symbol || '+') + ' ' + amt);
return this.update(toUpdate);
},
// Helper to get or set the "boolFlag" value.
_bool: function(val) {
if (arguments.length === 1) {
this._boolFlag = val;
return this;
}
var ret = this._boolFlag;
this._boolFlag = 'and';
return ret;
},
// Helper to get or set the "joinFlag" value.
_joinType: function (val) {
if (arguments.length === 1) {
this._joinFlag = val;
return this;
}
var ret = this._joinFlag || 'inner';
this._joinFlag = 'inner';
return ret;
},
// Helper for compiling any aggregate queries.
_aggregate: function(method, column) {
this.statements.push({
grouping: 'columns',
type: 'aggregate',
method: method,
value: column
});
return this;
},
// Helper for notifying on errors in the query chain.
_error: function(err) {
this.errors.push(new Error(err));
return this;
}
};
// Attach the "then" method.
QueryBuilder.prototype.then = function(onFulfilled, onRejected) {
return client.runThen(this).then(onFulfilled, onRejected);
};
// Attach all of the top level promise methods that should be chainable.
require('../coerceable')(QueryBuilder);
QueryBuilder.extend = require('simple-extend');
return QueryBuilder;
};