patio
Version:
Patio query engine and ORM
1,142 lines (1,086 loc) • 106 kB
JavaScript
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);