UNPKG

patio

Version:
1,142 lines (1,086 loc) 106 kB
var comb = require("comb"), hitch = comb.hitch, array = comb.array, flatten = array.flatten, compact = array.compact, define = comb.define, argsToArray = comb.argsToArray, isString = comb.isString, isEmpty = comb.isEmpty, isNull = comb.isNull, isBoolean = comb.isBoolean, isNumber = comb.isNumber, merge = comb.merge, isObject = comb.isObject, isFunction = comb.isFunction, isUndefined = comb.isUndefined, isHash = comb.isHash, isInstanceOf = comb.isInstanceOf, sql = require("../sql").sql, LiteralString = sql.LiteralString, Expression = sql.Expression, ComplexExpression = sql.ComplexExpression, BooleanExpression = sql.BooleanExpression, PlaceHolderLiteralString = sql.PlaceHolderLiteralString, Identifier = sql.Identifier, QualifiedIdentifier = sql.QualifiedIdentifier, AliasedExpression = sql.AliasedExpression, StringExpression = sql.StringExpression, NumericExpression = sql.NumericExpression, OrderedExpression = sql.OrderedExpression, JoinClause = sql.JoinClause, JoinOnClause = sql.JoinOnClause, JoinUsingClause = sql.JoinUsingClause, ColumnAll = sql.ColumnAll, QueryError = require("../errors").QueryError; var Dataset; function conditionedJoin(type, ...args) { return this.joinTable.apply(this, [type].concat(args)); } function unConditionJoin(type, table) { return this.joinTable.apply(this, [type, table]); } define({ /**@ignore*/ instance: { /**@lends patio.Dataset.prototype*/ /** * @ignore */ constructor: function () { !Dataset && (Dataset = require("../index").Dataset); this._super(arguments); this._static.CONDITIONED_JOIN_TYPES.forEach(function (type) { if (!this[type + "Join"]) { this[type + "Join"] = hitch(this, conditionedJoin, type); } }, this); this._static.UNCONDITIONED_JOIN_TYPES.forEach(function (type) { if (!this[type + "Join"]) { this[type + "Join"] = hitch(this, unConditionJoin, type); } }, this); }, /** * Adds a RETURNING clause, which is not supported by all databases. If returning is * used instead of returning the autogenerated primary key or update/delete returning the number of rows modified. * * @example * * ds.from("items").returning() //"RETURNING *" * ds.from("items").returning(null) //"RETURNING NULL" * ds.from("items").returning("id", "name") //"RETURNING id, name" * ds.from("items").returning(["id", "name"]) //"RETURNING id, name" * * @param values columns to return. If values is an array then the array is assumed to contain the columns to * return. Otherwise the arguments will be used. * @return {patio.Dataset} a new dataset with the retuning option added. */ returning: function (values) { var args; if (Array.isArray(values)) { args = values; } else { args = argsToArray(arguments); } return this.mergeOptions({returning: args.map(function (v) { return isString(v) ? sql.stringToIdentifier(v) : v; })}); }, /** * Adds a further filter to an existing filter using AND. This method is identical to {@link patio.Dataset#filter} * except it expects an existing filter. * * <p> * <b>For parameter types see {@link patio.Dataset#filter}.</b> * </p> * * @example * DB.from("table").filter("a").and("b").sql; * //=>SELECT * FROM table WHERE a AND b * * @throws {patio.QueryError} If no WHERE?HAVING clause exists. * * @return {patio.Dataset} a cloned dataset with the condition added to the WHERE/HAVING clause added. */ and: function () { var tOpts = this.__opts, clauseObj = tOpts[tOpts.having ? "having" : "where"]; if (clauseObj) { return this.filter.apply(this, arguments); } else { throw new QueryError("No existing filter found"); } }, as: function (alias) { return new AliasedExpression(this, alias); }, /** * Adds an alternate filter to an existing WHERE/HAVING using OR. * * <p> * <b>For parameter types see {@link patio.Dataset#filter}.</b> * </p> * * @example * * DB.from("items").filter("a").or("b") * //=> SELECT * FROM items WHERE a OR b * * @throws {patio.QueryError} If no WHERE?HAVING clause exists. * @return {patio.Dataset} a cloned dataset with the condition added to the WHERE/HAVING clause added. */ or: function () { var tOpts = this.__opts; var clause = (tOpts.having ? "having" : "where"), clauseObj = tOpts[clause]; if (clauseObj) { var args = argsToArray(arguments); args = args.length === 1 ? args[0] : args; var opts = {}; opts[clause] = new BooleanExpression("OR", clauseObj, this._filterExpr(args)); return this.mergeOptions(opts); } else { throw new QueryError("No existing filter found"); } }, /** * Adds a group of ORed conditions wrapped in parens, connected to an existing where/having clause by an AND. * If the where/having clause doesn't yet exist, a where clause is created with the ORed group. * * <p> * <b>For parameter types see {@link patio.Dataset#filter}.</b> * </p> * * @example * * DB.from("items").filter({id, [1,2,3]}).andGroupedOr([{price: {lt : 0}}, {price: {gt: 10}]).sql; * //=> SELECT * * * FROM * items * WHERE * ((id IN (1, 2, 3)) AND ((price < 0) OR (price > 10))) * * DB.from("items").andGroupedOr([{price: {lt : 0}}, {price: {gt: 10}]).sql; * //=> SELECT * * * FROM * items * WHERE * ((price < 0) OR (price > 10)) * * DB.from("items").filter({x:1}).andGroupedOr([{a:1, b:2}, {c:3, d:4}]).sql; * //=> SELECT * * * FROM * items * WHERE * ((x = 1) AND (((a = 1) AND (b = 2)) OR ((c = 3) AND (d = 4))) * * @return {patio.Dataset} a cloned dataset with the condition 'or group' added to the WHERE/HAVING clause. */ andGroupedOr: function (filterExp) { return this._addGroupedCondition("AND", "OR", filterExp); }, /** * Adds a group of ANDed conditions wrapped in parens to an existing where/having clause by an AND. If there isn't * yet a clause, a where clause is created with the ANDed conditions * * <p> * <b>For parameter types see {@link patio.Dataset#filter}.</b> * </p> * * @example * * DB.from("items").filter({id, [1,2,3]}).andGroupedAnd([{price: {gt : 0}}, {price: {lt: 10}]).sql; * //=> SELECT * * * FROM * items * WHERE * ((id IN (1, 2, 3)) AND ((price > 0) AND (price < 10))) * * DB.from("items").andGroupedAnd([{price: {gt : 0}}, {price: {lt: 10}]).sql; * //=> SELECT * * * FROM * items * WHERE * ((price > 0) AND (price < 10)) * * @return {patio.Dataset} a cloned dataset with the condition 'and group' added to the WHERE/HAVING clause. */ andGroupedAnd: function (filterExp) { return this._addGroupedCondition("AND", "AND", filterExp); }, /** * Adds a group of ANDed conditions wrapped in parens to an existing where/having clause by an OR. If there isn't * a where/having clause, a where clause is created with the ANDed conditions. * * <p> * <b>For parameter types see {@link patio.Dataset#filter}.</b> * </p> * * @example * * DB.from("items").filter({id, [1,2,3]}).orGroupedAnd([{price: {lt : 0}}, {price: {gt: 10}]).sql; * //=> SELECT * * * FROM * items * WHERE * ((id IN (1, 2, 3)) OR ((price > 0) AND (price < 10))) * * DB.from("items").orGroupedAnd([{price: {gt : 0}}, {price: {gt: 10}]).sql; * //=> SELECT * * * FROM * items * WHERE * ((price > 0) AND (price < 10)) * * @return {patio.Dataset} a cloned dataset with the condition 'and group' added to the WHERE/HAVING clause. */ orGroupedAnd: function () { var tOpts = this.__opts, clause = (tOpts.having ? "having" : "where"), clauseObj = tOpts[clause]; if (clauseObj) { return this.or.apply(this, arguments); } else { var args = argsToArray(arguments); args = args.length === 1 ? args[0] : args; var opts = {}; opts[clause] = this._filterExpr(args, null, "AND"); return this.mergeOptions(opts); } }, /** * Adds a group of ORed conditions wrapped in parens to an existing having/where clause with an OR. If there isn't * already a clause, a where clause is created with the ORed group. * * <p> * <b>For parameter types see {@link patio.Dataset#filter}.</b> * </p> * * @example * * DB.from("items").filter({id, [1,2,3]}).andGroupedOr([{price: {lt : 0}}, {price: {gt: 10}]).sql; * //=> SELECT * * * FROM * items * WHERE * ((id IN (1, 2, 3)) AND ((price < 0) OR (price > 10))) * * DB.from("items").orGroupedOr([{price: {lt : 0}}, {price: {gt: 10}]).sql; * //=> SELECT * * * FROM * items * WHERE * ((price < 0) OR (price > 10)) * * @return {patio.Dataset} a cloned dataset with the condition 'or group' added to the WHERE/HAVING clause. */ orGroupedOr: function (filterExp) { return this._addGroupedCondition("OR", "OR", filterExp); }, /** * Returns a copy of the dataset with the SQL DISTINCT clause. * The DISTINCT clause is used to remove duplicate rows from the * output. If arguments are provided, uses a DISTINCT ON clause, * in which case it will only be distinct on those columns, instead * of all returned columns. * * @example * * DB.from("items").distinct().sqll * //=> SELECT DISTINCT * FROM items * DB.from("items").order("id").distinct("id").sql; * //=> SELECT DISTINCT ON (id) * FROM items ORDER BY id * * @throws {patio.QueryError} If arguments are given and DISTINCT ON is not supported. * @param {...String|...patio.sql.Identifier} args variable number of arguments used to create * the DISTINCT ON clause. * @return {patio.Dataset} a cloned dataset with the DISTINCT/DISTINCT ON clause added. */ distinct: function (args) { args = argsToArray(arguments); if (args.length && !this.supportsDistinctOn) { throw new QueryError("DISTICT ON is not supported"); } args = args.map(function (a) { return isString(a) ? new Identifier(a) : a; }); return this.mergeOptions({distinct: args}); }, /** * Allows the loading of another query and combining them in one patio command. * Queries can be related or completely unrelated. * All data comes back as JSON NOT Patio models. * * @example * * DB.from('company').filter({name: 'Amazon'}) * .eager({ * // company from parent query is passed in and usable in the eager query. * leader: (company) => DB.from('leader').filter({id: company.leaderId}).one() * }) * // Load completely unrelated data. * .eager({org: () => DB.from('organization').one() }) * .one()}) * * { id: 1, * name: 'Amazon.com', * leader: { * id: 1, * name: 'Jeff' * }, * org: { * id: 1, * name: 'Google Inc.' * } * } * * Can do one to many loading for every item in the parent dataset. * Be careful doing this because it can lead to lots of extra queries. * * DB.from('company').filter({state: 'IA'}) * .eager({invoices: (company) => DB.from('invoices').filter({companyId: company.id}).one() }) * .all()}) * * [ * { id: 1, * name: 'Principal', * invoices: [ * { id: 1, amount: 200}, * { id: 2, amount: 300}, * ] * }, * { id: 2, * name: 'John Deere', * invoices: [ * { id: 3, amount: 200}, * { id: 4, amount: 300}, * ] * } * ] * */ eager: function(includeDatasets, fromModel) { var ds = this.mergeOptions({}), originalRowCb = ds.rowCb; if(!ds.__opts._eagerAssoc) { ds.__opts._eagerAssoc = includeDatasets; ds.rowCb = function (topLevelResults) { function toObject(thing) { if (!thing) { return comb.when(thing); } if (Array.isArray(thing)) { return comb.when(thing.map(function(item) { return toObject(item); })); } if ('toObject' in thing) { return comb.when(thing.toObject()); } return comb.when(thing); } var eagerResults = {}, whens = []; if (!originalRowCb) { // pass through for when topLevelResults is already resolved originalRowCb = function(r){return r;}; } return comb.when(originalRowCb(topLevelResults)).chain(function(maybeModel) { whens = Object.keys(ds.__opts._eagerAssoc).map(function(key) { return ds.__opts._eagerAssoc[key](maybeModel).chain(function(result) { return toObject(result).chain(function(res) { eagerResults[key] = res; return result; }); }); }); return comb.when(whens).chain(function () { return toObject(maybeModel).chain(function(json) { // merge associations on to main data return Object.assign(json, eagerResults); }); }); }); }; } return ds.mergeOptions({ _eagerAssoc: Object.assign(ds.__opts._eagerAssoc, includeDatasets) }); }, /** * Adds an EXCEPT clause using a second dataset object. * An EXCEPT compound dataset returns all rows in the current dataset * that are not in the given dataset. * * @example * * DB.from("items").except(DB.from("other_items")).sql; * //=> SELECT * FROM items EXCEPT SELECT * FROM other_items * * DB.from("items").except(DB.from("other_items"), * {all : true, fromSelf : false}).sql; * //=> SELECT * FROM items EXCEPT ALL SELECT * FROM other_items * * DB.from("items").except(DB.from("other_items"), * {alias : "i"}).sql; * //=>SELECT * FROM (SELECT * FROM items EXCEPT SELECT * FROM other_items) AS i * * @throws {patio.QueryError} if the operation is not supported. * @param {patio.Dataset} dataset the dataset to use to create the EXCEPT clause. * @param {Object} [opts] options to use when creating the EXCEPT clause * @param {String|patio.sql.Identifier} [opt.alias] Use the given value as the {@link patio.Dataset#fromSelf} alias. * @param {Boolean} [opts.all] Set to true to use EXCEPT ALL instead of EXCEPT, so duplicate rows can occur * @param {Boolean} [opts.fromSelf] Set to false to not wrap the returned dataset in a {@link patio.Dataset#fromSelf}, use with care. * * @return {patio.Dataset} a cloned dataset with the EXCEPT clause added. */ except: function (dataset, opts) { opts = isUndefined(opts) ? {} : opts; if (!isHash(opts)) { opts = {all: true}; } if (!this.supportsIntersectExcept) { throw new QueryError("EXCEPT not supoorted"); } else if (opts.hasOwnProperty("all") && !this.supportsIntersectExceptAll) { throw new QueryError("EXCEPT ALL not supported"); } return this.compoundClone("except", dataset, opts); }, /** * Performs the inverse of {@link patio.Dataset#filter}. Note that if you have multiple filter * conditions, this is not the same as a negation of all conditions. For argument types see * {@link patio.Dataset#filter} * * @example * * DB.from("items").exclude({category : "software").sql; * //=> SELECT * FROM items WHERE (category != 'software') * * DB.from("items").exclude({category : 'software', id : 3}).sql; * //=> SELECT * FROM items WHERE ((category != 'software') OR (id != 3)) * @return {patio.Dataset} a cloned dataset with the excluded conditions applied to the HAVING/WHERE clause. */ exclude: function () { var cond = argsToArray(arguments), tOpts = this.__opts; var clause = (tOpts["having"] ? "having" : "where"), clauseObj = tOpts[clause]; cond = cond.length > 1 ? cond : cond[0]; cond = this._filterExpr.call(this, cond); cond = BooleanExpression.invert(cond); if (clauseObj) { cond = new BooleanExpression("AND", clauseObj, cond); } var opts = {}; opts[clause] = cond; return this.mergeOptions(opts); }, /** * Returns a copy of the dataset with the given conditions applied to it. * If the query already has a HAVING clause, then the conditions are applied to the * HAVING clause otherwise they are applied to the WHERE clause. * * @example * * DB.from("items").filter({id : 3}).sql; * //=> SELECT * FROM items WHERE (id = 3) * * DB.from("items").filter('price < ?', 100) * //=> SELECT * FROM items WHERE price < 100 * * DB.from("items").filter({id, [1,2,3]}).filter({id : {between : [0, 10]}}).sql; * //=> SELECT * * * FROM * items * WHERE * ((id IN (1, 2, 3)) AND ((id >= 0) AND (id <= 10))) * * DB.from("items").filter('price < 100'); * //=> SELECT * FROM items WHERE price < 100 * * DB.from("items").filter("active").sql; * //=> SELECT * FROM items WHERE active * * DB.from("items").filter(function(){ * return this.price.lt(100); * }); * //=> SELECT * FROM items WHERE (price < 100) * * //Multiple filter calls can be chained for scoping: * DB.from("items").filter(:category => 'software').filter{price < 100} * //=> SELECT * FROM items WHERE ((category = 'software') AND (price < 100)) * * * @param {Object|Array|String|patio.sql.Identifier|patio.sql.BooleanExpression} args filters to apply to the * WHERE/HAVING clause. Description of each: * <ul> * <li>Hash - list of equality/inclusion expressions</li> * <li>Array - depends: * <ul> * <li>If first member is a string, assumes the rest of the arguments * are parameters and interpolates them into the string.</li> * <li>If all members are arrays of length two, treats the same way * as a hash, except it allows for duplicate keys to be * specified.</li> * <li>Otherwise, treats each argument as a separate condition.</li> * </ul> * </li> * <li>String - taken literally</li> * <li>{@link patio.sql.Identifier} - taken as a boolean column argument (e.g. WHERE active)</li> * <li>{@link patio.sql.BooleanExpression} - an existing condition expression, * probably created using the patio.sql methods. * </li> * * @param {Function} [cb] filter also takes a cb, which should return one of the above argument * types, and is treated the same way. This block is called with an {@link patio.sql} object which can be used to dynaically create expression. For more details * on the sql object see {@link patio.sql} * * <p> * <b>NOTE:</b>If both a cb and regular arguments are provided, they get ANDed together. * </p> * * @return {patio.Dataset} a cloned dataset with the filter arumgents applied to the WHERE/HAVING clause. **/ filter: function (args, cb) { args = [this.__opts["having"] ? "having" : "where"].concat(argsToArray(arguments)); return this._filter.apply(this, args); }, /** * @see patio.Dataset#filter */ find: function () { var args = [this.__opts["having"] ? "having" : "where"].concat(argsToArray(arguments)); return this._filter.apply(this, args); }, /** * @example * DB.from("table").forUpdate() * //=> SELECT * FROM table FOR UPDATE * @return {patio.Dataset} a cloned dataset with a "update" lock style. */ forUpdate: function () { return this.lockStyle("update"); }, /** * Returns a copy of the dataset with the source changed. If no * source is given, removes all tables. If multiple sources * are given, it is the same as using a CROSS JOIN (cartesian product) between all tables. * * @example * var dataset = DB.from("items"); * * dataset.from().sql; * //=> SELECT * * * dataset.from("blah").sql * //=> SELECT * FROM blah * * dataset.from("blah", "foo") * //=> SELECT * FROM blah, foo * * dataset.from({a:"b"}).sql; * //=> SELECT * FROM a AS b * * dataset.from(dataset.from("a").group("b").as("c")).sql; * //=> "SELECT * FROM (SELECT * FROM a GROUP BY b) AS c" * * @param {...String|...patio.sql.Identifier|...patio.Dataset|...Object} [source] tables to select from * * @return {patio.Dataset} a cloned dataset with the FROM clause overridden. */ from: function (source) { source = argsToArray(arguments); var tableAliasNum = 0, sources = []; source.forEach(function (s) { if (isInstanceOf(s, Dataset)) { sources.push(new AliasedExpression(s, this._datasetAlias(++tableAliasNum))); } else if (isHash(s)) { for (var i in s) { sources.push(new AliasedExpression(new Identifier(i), s[i])); } } else if (isString(s)) { sources.push(this.stringToIdentifier(s)); } else { sources.push(s); } }, this); var o = {from: sources.length ? sources : null}; if (tableAliasNum) { o.numDatasetSources = tableAliasNum; } return this.mergeOptions(o); }, /** * Returns a dataset selecting from the current dataset. * Supplying the alias option controls the alias of the result. * * @example * * ds = DB.from("items").order("name").select("id", "name") * //=> SELECT id,name FROM items ORDER BY name * * ds.fromSelf().sql; * //=> SELECT * FROM (SELECT id, name FROM items ORDER BY name) AS t1 * * ds.fromSelf({alias : "foo"}).sql; * //=> SELECT * FROM (SELECT id, name FROM items ORDER BY name) AS foo * * @param {Object} [opts] options * @param {String|patio.sql.Identifier} [opts.alias] alias to use * * @return {patio.Dataset} a cloned dataset with the FROM clause set as the current dataset. */ fromSelf: function (opts) { opts = isUndefined(opts) ? {} : opts; var fs = {}; var nonSqlOptions = this._static.NON_SQL_OPTIONS; Object.keys(this.__opts).forEach(function (k) { if (nonSqlOptions.indexOf(k) === -1) { fs[k] = null; } }); return this.mergeOptions(fs).from(opts["alias"] ? this.as(opts["alias"]) : this); }, /** * Match any of the columns to any of the patterns. The terms can be * strings (which use LIKE) or regular expressions (which are only * supported on MySQL and PostgreSQL). Note that the total number of * pattern matches will be columns[].length * terms[].length, * which could cause performance issues. * * @example * * DB.from("items").grep("a", "%test%").sql; * //=> SELECT * FROM items WHERE (a LIKE '%test%'); * * DB.from("items").grep(["a", "b"], ["%test%" "foo"]).sql; * //=> SELECT * FROM items WHERE ((a LIKE '%test%') OR (a LIKE 'foo') OR (b LIKE '%test%') OR (b LIKE 'foo')) * * DB.from("items").grep(['a', 'b'], ["%foo%", "%bar%"], {allPatterns : true}).sql; * //=> SELECT * FROM a WHERE (((a LIKE '%foo%') OR (b LIKE '%foo%')) AND ((a LIKE '%bar%') OR (b LIKE '%bar%'))) * * DB.from("items").grep(["a", "b"], ['%foo%", "%bar%", {allColumns : true})sql; * //=> SELECT * FROM a WHERE (((a LIKE '%foo%') OR (a LIKE '%bar%')) AND ((b LIKE '%foo%') OR (b LIKE '%bar%'))) * * DB.from("items").grep(["a", "b"], ["%foo%", "%bar%"], {allPatterns : true, allColumns : true}).sql; * //=> SELECT * FROM a WHERE ((a LIKE '%foo%') AND (b LIKE '%foo%') AND (a LIKE '%bar%') AND (b LIKE '%bar%')) * * @param {String[]|patio.sql.Identifier[]} columns columns to search * @param {String|RegExp} patterns patters to search with * @param {Object} [opts] options to use when searching. NOTE If both allColumns and allPatterns are true, all columns must match all patterns * @param {Boolean} [opts.allColumns] All columns must be matched to any of the given patterns. * @param {Boolean} [opts.allPatterns] All patterns must match at least one of the columns. * @param {Boolean} [opts.caseInsensitive] Use a case insensitive pattern match (the default is * case sensitive if the database supports it). * @return {patio.Dataset} a dataset with the LIKE clauses added */ grep: function (columns, patterns, opts) { opts = isUndefined(opts) ? {} : opts; var conds; if (opts.hasOwnProperty("allPatterns")) { conds = array.toArray(patterns).map(function (pat) { return BooleanExpression.fromArgs( [(opts.allColumns ? "AND" : "OR")] .concat(array.toArray(columns) .map(function (c) { return StringExpression.like(c, pat, opts); }))); }); return this.filter(BooleanExpression.fromArgs([opts.allPatterns ? "AND" : "OR"].concat(conds))); } else { conds = array.toArray(columns) .map(function (c) { return BooleanExpression.fromArgs(["OR"].concat(array.toArray(patterns).map(function (pat) { return StringExpression.like(c, pat, opts); }))); }); return this.filter(BooleanExpression.fromArgs([opts.allColumns ? "AND" : "OR"].concat(conds))); } }, /** * @see patio.Dataset#grep */ like: function () { return this.grep.apply(this, arguments); }, /** * Returns a copy of the dataset with the results grouped by the value of * the given columns. * @example * DB.from("items").group("id") * //=>SELECT * FROM items GROUP BY id * DB.from("items").group("id", "name") * //=> SELECT * FROM items GROUP BY id, name * @param {String...|patio.sql.Identifier...} columns columns to group by. * * @return {patio.Dataset} a cloned dataset with the GROUP BY clause added. **/ group: function (columns) { columns = argsToArray(arguments); var self = this; return this.mergeOptions({group: (array.compact(columns).length === 0 ? null : columns.map(function (c) { return isString(c) ? self.stringToIdentifier(c) : c; }))}); }, /** * @see patio.Dataset#group */ groupBy: function () { return this.group.apply(this, arguments); }, /** * Returns a dataset grouped by the given column with count by group. * Column aliases may be supplied, and will be included in the select clause. * * @example * * DB.from("items").groupAndCount("name").all() * //=> SELECT name, count(*) AS count FROM items GROUP BY name * //=> [{name : 'a', count : 1}, ...] * * DB.from("items").groupAndCount("first_name", "last_name").all() * //SELECT first_name, last_name, count(*) AS count FROM items GROUP BY first_name, last_name * //=> [{first_name : 'a', last_name : 'b', count : 1}, ...] * * DB.from("items").groupAndCount("first_name___name").all() * //=> SELECT first_name AS name, count(*) AS count FROM items GROUP BY first_name * //=> [{name : 'a', count:1}, ...] * @param {String...|patio.sql.Identifier...} columns columns to croup and count on. * * @return {patio.Dataset} a cloned dataset with the GROUP clause and count added. */ groupAndCount: function (columns) { columns = argsToArray(arguments); var group = this.group.apply(this, columns.map(function (c) { return this._unaliasedIdentifier(c); }, this)); return group.select.apply(group, columns.concat([this._static.COUNT_OF_ALL_AS_COUNT])); }, /** * Returns a copy of the dataset with the HAVING conditions changed. See {@link patio.Dataset#filter} for argument types. * * @example * DB.from("items").group("sum").having({sum : 10}).sql; * //=> SELECT * FROM items GROUP BY sum HAVING (sum = 10) * * @return {patio.Dataset} a cloned dataset with HAVING clause changed or added. **/ having: function () { var cond = argsToArray(arguments).map(function (s) { return isString(s) && s !== '' ? this.stringToIdentifier(s) : s; }, this); return this._filter.apply(this, ["having"].concat(cond)); }, /** * Adds an INTERSECT clause using a second dataset object. * An INTERSECT compound dataset returns all rows in both the current dataset * and the given dataset. * * @example * * DB.from("items").intersect(DB.from("other_items")).sql; * //=> SELECT * FROM (SELECT * FROM items INTERSECT SELECT * FROM other_items) AS t1 * * DB.from("items").intersect(DB.from("other_items"), {all : true, fromSelf : false}).sql; * //=> SELECT * FROM items INTERSECT ALL SELECT * FROM other_items * * DB.from("items").intersect(DB.from("other_items"), {alias : "i"}).sql; * //=> SELECT * FROM (SELECT * FROM items INTERSECT SELECT * FROM other_items) AS i * * @throws {patio.QueryError} if the operation is not supported. * @param {patio.Dataset} dataset the dataset to intersect * @param {Object} [opts] options * @param {String|patio.sql.Identifier} [opts.alias] Use the given value as the {@link patio.Dataset#fromSelf} alias * @param {Boolean} [opts.all] Set to true to use INTERSECT ALL instead of INTERSECT, so duplicate rows can occur * @param {Boolean} [opts.fromSelf] Set to false to not wrap the returned dataset in a {@link patio.Dataset#fromSelf}. * * @return {patio.Dataset} a cloned dataset with the INTERSECT clause. **/ intersect: function (dataset, opts) { opts = isUndefined(opts) ? {} : opts; if (!isHash(opts)) { opts = {all: opts}; } if (!this.supportsIntersectExcept) { throw new QueryError("INTERSECT not supported"); } else if (opts.all && !this.supportsIntersectExceptAll) { throw new QueryError("INTERSECT ALL not supported"); } return this.compoundClone("intersect", dataset, opts); }, /** * Inverts the current filter. * * @example * DB.from("items").filter({category : 'software'}).invert() * //=> SELECT * FROM items WHERE (category != 'software') * * @example * DB.from("items").filter({category : 'software', id : 3}).invert() * //=> SELECT * FROM items WHERE ((category != 'software') OR (id != 3)) * * @return {patio.Dataset} a cloned dataset with the filter inverted. **/ invert: function () { var having = this.__opts.having, where = this.__opts.where; if (!(having || where)) { throw new QueryError("No current filter"); } var o = {}; if (having) { o.having = BooleanExpression.invert(having); } if (where) { o.where = BooleanExpression.invert(where); } return this.mergeOptions(o); }, /** * Returns a cloned dataset with an inner join applied. * * @see patio.Dataset#joinTable */ join: function () { return this.innerJoin.apply(this, arguments); }, /** * Returns a joined dataset. Uses the following arguments: * * @example * * DB.from("items").joinTable("leftOuter", "categories", [["categoryId", "id"],["categoryId", [1, 2, 3]]]).sql; * //=>'SELECT * * * FROM * `items` * LEFT OUTER JOIN * `categories` ON ( * (`categories`.`categoryId` = `items`.`id`) * AND * (`categories`.`categoryId` IN (1,2, 3)) * ) * DB.from("items").leftOuter("categories", [["categoryId", "id"],["categoryId", [1, 2, 3]]]).sql; * //=>'SELECT * * * FROM * `items` * LEFT OUTER JOIN * `categories` ON ( * (`categories`.`categoryId` = `items`.`id`) * AND * (`categories`.`categoryId` IN (1,2, 3)) * ) * * DB.from("items").leftOuterJoin("categories", {categoryId:"id"}).sql * //=> SELECT * FROM "items" LEFT OUTER JOIN "categories" ON ("categories"."categoryId" = "items"."id") * * DB.from("items").rightOuterJoin("categories", {categoryId:"id"}).sql * //=> SELECT * FROM "items" RIGHT OUTER JOIN "categories" ON ("categories"."categoryId" = "items"."id") * * DB.from("items").fullOuterJoin("categories", {categoryId:"id"}).sql * //=> SELECT * FROM "items" FULL OUTER JOIN "categories" ON ("categories"."categoryId" = "items"."id") * * DB.from("items").innerJoin("categories", {categoryId:"id"}).sql * //=> SELECT * FROM "items" INNER JOIN "categories" ON ("categories"."categoryId" = "items"."id") * * DB.from("items").leftJoin("categories", {categoryId:"id"}).sql * //=> SELECT * FROM "items" LEFT JOIN "categories" ON ("categories"."categoryId" = "items"."id") * * DB.from("items").rightJoin("categories", {categoryId:"id"}).sql * //=> SELECT * FROM "items" RIGHT JOIN "categories" ON ("categories"."categoryId" = "items"."id") * * DB.from("items").fullJoin("categories", {categoryId:"id"}).sql * //=> SELECT * FROM "items" FULL JOIN "categories" ON ("categories"."categoryId" = "items"."id") * * DB.from("items").naturalJoin("categories").sql * //=> SELECT * FROM "items" NATURAL JOIN "categories" * * DB.from("items").naturalLeftJoin("categories").sql * //=> SELECT * FROM "items" NATURAL LEFT JOIN "categories" * * DB.from("items").naturalRightJoin("categories").sql * //=> SELECT * FROM "items" NATURAL RIGHT JOIN "categories" * * DB.from("items").naturalFullJoin("categories").sql * //=> SELECT * FROM "items" NATURAL FULL JOIN "categories"' * * DB.from("items").crossJoin("categories").sql * //=> SELECT * FROM "items" CROSS JOIN "categories" * * @param {String} type the type of join to do. * @param {String|patio.sql.Identifier|patio.Dataset|Object} table depends on the type. * <ul> * <li>{@link patio.Dataset} - a subselect is performed with an alias</li> * <li>Object - an object that has a tableName property.</li> * <li>String|{@link patio.sql.Identifier} - the name of the table</li> * </ul> * @param [expr] - depends on type * <ul> * <li>Object|Array of two element arrays - Assumes key (1st arg) is column of joined table (unless already * qualified), and value (2nd arg) is column of the last joined or primary table (or the * implicitQualifier option</li>. * <li>Array - If all members of the array are string or {@link patio.sql.Identifier}, considers * them as columns and uses a JOIN with a USING clause. Most databases will remove duplicate columns from * the result set if this is used.</li> * <li>null|undefined(not passed in) - If a cb is not given, doesn't use ON or USING, so the JOIN should be a NATURAL * or CROSS join. If a block is given, uses an ON clause based on the block, see below.</li> * <li>Everything else - pretty much the same as a using the argument in a call to {@link patio.Dataset#filter}, * so strings are considered literal, {@link patio.sql.Identifiers} specify boolean columns, and patio.sql * expressions can be used. Uses a JOIN with an ON clause.</li> * </ul> * @param {Object} options an object of options. * @param {String|patio.sql.Identifier} [options.tableAlias=undefined] the name of the table's alias when joining, necessary for joining * to the same table more than once. No alias is used by default. * @param {String|patio.sql.Identifier} [options.implicitQualifier=undefined] The name to use for qualifying implicit conditions. By default, * the last joined or primary table is used. * @param {Function} [cb] cb - The cb argument should only be given if a JOIN with an ON clause is used, * in which case it is called with * <ul> * <li>table alias/name for the table currently being joined</li> * <li> the table alias/name for the last joined (or first table) * <li>array of previous</li> * </ul> * the cb should return an expression to be used in the ON clause. * * @return {patio.Dataset} a cloned dataset joined using the arguments. */ joinTable: function (type, table, expr, options, cb) { var args = argsToArray(arguments); if (isFunction(args[args.length - 1]) && !(args[args.length -1] instanceof Identifier)) { cb = args[args.length - 1]; args.pop(); } else { cb = null; } type = args.shift(), table = args.shift(), expr = args.shift(), options = args.shift(); expr = isUndefined(expr) ? null : expr, options = isUndefined(options) ? {} : options; var h; var usingJoin = Array.isArray(expr) && expr.length && expr.every(function (x) { return isString(x) || isInstanceOf(x, Identifier); }); if (usingJoin && !this.supportsJoinUsing) { h = {}; expr.forEach(function (s) { h[s] = s; }); return this.joinTable(type, table, h, options); } var tableAlias, lastAlias; if (isHash(options)) { tableAlias = options.tableAlias; lastAlias = options.implicitQualifier; } else if (isString(options) || isInstanceOf(options, Identifier)) { tableAlias = options; lastAlias = null; } else { throw new QueryError("Invalid options format for joinTable %j4", [options]); } var tableAliasNum, tableName; if (isInstanceOf(table, Dataset)) { if (!tableAlias) { tableAliasNum = (this.__opts.numDatasetSources || 0) + 1; tableAlias = this._datasetAlias(tableAliasNum); } tableName = tableAlias; } else { if (!isUndefined(table.tableName)) { table = table.tableName; } if (Array.isArray(table)) { table = table.map(this.stringToIdentifier, this); } else { table = isString(table) ? this.stringToIdentifier(table) : table; var parts = this._splitAlias(table), implicitTableAlias = parts[1]; table = parts[0]; tableAlias = tableAlias || implicitTableAlias; tableName = tableAlias || table; } } var join; if (!expr && !cb) { join = new JoinClause(type, table, tableAlias); } else if (usingJoin) { if (cb) { throw new QueryError("cant use a cb if an array is given"); } join = new JoinUsingClause(expr, type, table, tableAlias); } else { lastAlias = lastAlias || this.__opts["lastJoinedTable"] || this.firstSourceAlias; if (Expression.isConditionSpecifier(expr)) { var newExpr = []; for (var i in expr) { var val = expr[i]; if (Array.isArray(val) && val.length === 2) { i = val[0], val = val[1]; } var k = this.qualifiedColumnName(i, tableName), v; if (isInstanceOf(val, Identifier)) { v = val.qualify(lastAlias); } else { v = val; } newExpr.push([k, v]); } expr = newExpr; } if (isFunction(cb) && !(cb instanceof Identifier)) { var expr2 = cb.apply(sql, [tableName, lastAlias, this.__opts.join || []]); expr = expr ? new BooleanExpression("AND", expr, expr2) : expr2; } join = new JoinOnClause(expr, type, table, tableAlias); } var opts = {join: (this.__opts.join || []).concat([join]), lastJoinedTable: tableName}; if (tableAliasNum) { opts.numDatasetSources = tableAliasNum; } return this.mergeOptions(opts); }, /** * If given an integer, the dataset will contain only the first l results. If a second argument is given, it is used as an offset. To use * an offset without a limit, pass null as the first argument. * * @example * * DB.from("items").limit(10) * //=> SELECT * FROM items LIMIT 10 * DB.from("items").limit(10, 20) * //=> SELECT * FROM items LIMIT 10 OFFSET 20 * DB.from("items").limit([3, 7]).sql * //=> SELECT * FROM items LIMIT 5 OFFSET 3'); * DB.from("items").limit(null, 20) * //=> SELECT * FROM items OFFSET 20 * * DB.from("items").limit('6', sql['a() - 1']).sql * => 'SELECT * FROM items LIMIT 6 OFFSET a() - 1'); * * @param {Number|String|Number[]} limit the limit to apply * @param {Number|String|patio.sql.LiteralString} offset the offset to apply * * @return {patio.Dataset} a cloned dataset witht the LIMIT and OFFSET applied. **/ limit: function (limit, offset) { if (this.__opts.sql) { return this.fromSelf().limit(limit, offset); } if (Array.isArray(limit) && limit.length === 2) { offset = limit[0]; limit = limit[1] - limit[0] + 1; } if (isString(limit) || isInstanceOf(limit, LiteralString)) { limit = parseInt("" + limit, 10); } if (isNumber(limit) && limit < 1) { throw new QueryError("Limit must be >= 1"); } var opts = {limit: limit}; if (offset) { if (isString(offset) || isInstanceOf(offset, LiteralString)) { offset = parseInt("" + offset, 10);