// Builder // ------- module.exports = function(client) { var _ = require('lodash'); var FluentChain = require('fluent-chain'); var Raw = require('../raw'); var Helpers = require('../helpers'); var SqlString = require('../sqlstring'); var Formatters = require('../formatters'); var JoinClause = require('./joinclause'); // Lazy load the `./index` to avoid circular dependency issues. var BuilderInterface; // 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(builder) { this.stack = builder.__stack; this.statements = []; this.bindings = []; this.errors = []; this.flags = {}; }; // All operators used in the `where` clause generation. var operators = ['=', '<', '>', '<=', '>=', '<>', '!=', 'like', 'not like', 'between', 'ilike']; // Valid values for the `order by` clause generation. var orderBys = ['asc', 'desc']; _.extend(QueryBuilder.prototype, Formatters, { // 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` toString: 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) { this._prepStack(); return new client.QueryCompiler(this)[(target || this._method || 'select')](); }, wrap: function QueryBuilder$wrap(callback) { var sql = this._checkFn(callback); this.statements.push({ type: 'wrapped', value: '(' + sql + ')' }); }, // Sets the `tableName` on the query. from: function QueryBuilder$from() { this.table.apply(this, arguments); }, // Alias to "from", for "insert" statements // e.g. builder.insert({a: value}).into('tableName') into: function QueryBuilder$into() { this.table.apply(this, arguments); }, table: function QueryBuilder$table(tableName) { this._ensureSingle('table', { value: this._wrap(tableName) }); }, // Adds a column or columns to the list of "columns" // being selected on the query. column: function() { this.statements.push({ type: 'columns', value: this._columnize(Helpers.normalizeArr.apply(null, arguments)) }); }, columns: function() { return this.column.apply(this, arguments); }, // Adds a `distinct` clause to the query. distinct: function() { this.statements.push({ type: 'columns', value: this._columnize(Helpers.normalizeArr.apply(null, arguments)), distinct: true }); }, // 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 args = new Array(arguments.length); for (var 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(this._joinType()); first.call(join, join); } else { join = new JoinClause(this._joinType()); join.on.apply(join, args.slice(1)); } join.clauses[0][0] = ' on'; var joinSql = []; for (i = 0, l = join.clauses.length; i < l; i++) { var clause = join.clauses[i]; joinSql.push(clause[0] + ' ' + this._wrap(clause[1]) + ' ' + this._operator(clause[2]) + ' ' + this._wrap(clause[3]) ); } this.statements.push({ type: 'join', value: join.joinType + ' join ' + this._wrap(table) + joinSql.join(' '), joinData: join }); }, // 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 grouped where statement (wrapped in parens). if (_.isFunction(column)) return this._whereNested(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; } // 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); } // If the value is a function, assume it's for building a sub-select. if (_.isFunction(value)) return this._whereSub(column, operator, value); this.statements.push({ type: 'where', value: this._wrap(column) + ' ' + this._operator(operator) + ' ' + this._parameterize(value), bool: this._bool() }); }, // 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)); if (raw.bindings !== void 0) this.binding.push(raw.bindings); this.statements.push({ type: 'where', value: raw.sql, bool: this._bool() }); }, // 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, type) { this.statements.push({ type: 'where', value: (type || 'exists') + ' (' + this._compileCallback(callback) + ')', bool: this._bool(), }); }, // 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, 'not exists'); }, // Adds a `or where not exists` clause to the query. orWhereNotExists: function(callback) { return this._bool('or').whereExists(callback, 'not exists'); }, // Adds a `where in` clause to the query. whereIn: function(column, values, condition) { if (_.isFunction(values)) return this._whereInSub(column, values, condition); this.statements.push({ type: 'where', value: this._wrap(column) + (condition || ' in (') + this._parameterize(values) + ')', bool: this._bool() }); }, // 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, ' not in ('); }, // Adds a `or where not in` clause to the query. orWhereNotIn: function(column, values) { return this._bool('or').whereIn(column, values, ' not in ('); }, // Adds a `where null` clause to the query. whereNull: function(column, condition) { this.statements.push({ type: 'where', value: this._wrap(column) + (condition || ' is null'), bool: this._bool() }); }, // 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, condition) { if (values.length < 2) { this.errors.push(new Error('Invalid values ' + values + ' in whereBetween clause')); } this.binding.push(values); this.statements.push({ type: 'where', value: this._wrap(column) + (condition || ' between') + ' ? and ?', bool: this._bool() }); }, // Adds a `where not between` clause to the query. whereNotBetween: function(column, values) { return this.whereBetween(column, values, ' not between'); }, // 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({ type: 'group', value: this._columnize(Helpers.normalizeArr.apply(null, arguments)) }); }, // Adds a `order by` clause to the query. orderBy: function(column, direction) { var cols = _.isArray(column) ? column : [column]; this.statements.push({ type: 'order', value: this._columnize(cols) + ' ' + this._direction(direction) }); }, // Add a union statement to the query. union: function(callback) { this.statements.push({ type: 'union', clause: 'union', value: this._checkFn(callback) }); }, // Adds a union all statement to the query. unionAll: function(callback) { this.statements.push({ type: 'union', clause: 'union all', value: this._checkFn(callback) }); }, // Adds a `having` clause to the query. having: function(column, operator, value) { if (column instanceof Raw) return this.havingRaw(column); this.statements.push({ type: 'having', value: this._wrap(column) + ' ' + this._operator(operator) + ' ' + this._parameter(value), bool: this._bool() }); }, // Adds a raw `having` clause to the query. havingRaw: function(sql, bindings) { var raw = (sql instanceof Raw ? sql : new Raw(sql, bindings)); this.binding.push(raw.bindings); this.statements.push({ type: 'having', value: raw.sql, bool: this._bool() }); }, // 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: function(value) { this._ensureSingle('offset', { value: 'offset ' + this._parameter(value) }); }, limit: function(value) { this._ensureSingle('limit', { value: 'limit ' + this._parameter(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); }, // 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'; this.column.apply(this, Helpers.args.apply(null, arguments)); }, // Pluck a column from a query. pluck: function(column) { this._method = 'pluck'; this.column.call(this, Helpers.args.apply(null, arguments)); }, // Sets the values for an `insert` query. insert: function(values, returning) { this._method = 'insert'; var columns; var insertVals = _.map(this._prepInsert(values), function(obj, i) { if (i === 0) columns = this._columnize(_.pluck(obj, 0)); return '(' + _.pluck(obj, 1).join(', ') + ')'; }, this); this._ensureSingle('insert', { columns: '(' + columns + ')', value: insertVals.join(', ') }); if (!_.isEmpty(returning)) this.returning(returning); }, // Sets the values for an `update` query. update: function(values) { var ret, obj = {}; this._method = 'update'; var args = Helpers.args.apply(null, arguments); if (!_.isPlainObject(values)) { obj[values] = returning; if (args.length > 2) { ret = args[2]; } } else { obj = values; returning = args[1]; } obj = Helpers.sortObject(obj); var vals = []; for (var i = 0; i < obj.length; i++) { var value = obj[i]; vals.push(this._wrap(value[0]) + ' = ' + this._parameter(value[1])); } this._ensureSingle('update', { columns: vals.join(', ') }); if (!_.isEmpty(returning)) this.returning(returning); }, // Alias to del. "delete": function() { return this.del.apply(this, arguments); }, // Executes a delete statement on the query; del: function() { this._method = 'delete'; }, truncate: function() { this._method = 'truncate'; this.statements.push({ type: 'truncate' }); }, // TODO: see if there's a more consistent way to do these // Sets the returning value for the query. returning: function(returning) { var isArr = _.isArray(returning); var val = isArr ? this._columnize(returning) : this._wrapValue(returning); this._ensureSingle('returning', { value: 'returning ' + val, isArr: isArr }); this.flags.returning = returning; }, transacting: function(t) { this.flags.transacting = t; }, options: function(opts) { this.flags.options = this.flags.options || []; this.flags.options.push(opts); }, debug: function(val) { this.flags.debug = (val == null ? true : val); }, // ---------------------------------------------------------------------- // Ensures _ensureSingle: function(type, attrs) { var current = _.findWhere(this.statements, {type: type}); if (current) { _.extend(current, attrs); } else { this.statements.push(_.extend({ type: type }, attrs)); } }, // Preps the values for `insert` or `update`. _prepInsert: function(values) { var vals = _.clone(values); if (!_.isArray(vals)) vals = (values ? [vals] : []); // Allows for multi-insert objects with missing keys. // TODO: Decide if we really want this? var defaults = _.reduce(_.union.apply(_, _.map(vals, function(val) { return _.keys(val); })), function(memo, key) { memo[key] = void 0; return memo; }, {}); for (var i = 0, l = vals.length; i