UNPKG

patio

Version:
1,263 lines (1,167 loc) 64.5 kB
var comb = require("comb"), define = comb.define, array = comb.array, toArray = array.toArray, intersect = array.intersect, compact = array.compact, string = comb.string, format = string.format, argsToArray = comb.argsToArray, isInstanceOf = comb.isInstanceOf, isArray = comb.isArray, isNumber = comb.isNumber, isDate = comb.isDate, isNull = comb.isNull, isBoolean = comb.isBoolean, isFunction = comb.isFunction, isUndefined = comb.isUndefined, isObject = comb.isObject, isHash = comb.isHash, isEmpty = comb.isEmpty, merge = comb.merge, hitch = comb.hitch, isUndefinedOrNull = comb.isUndefinedOrNull, isString = comb.isString, sql = require("../sql").sql, Json = sql.Json, JsonArray = sql.JsonArray, Expression = sql.Expression, ComplexExpression = sql.ComplexExpression, AliasedExpression = sql.AliasedExpression, Identifier = sql.Identifier, QualifiedIdentifier = sql.QualifiedIdentifier, OrderedExpression = sql.OrderedExpression, CaseExpression = sql.CaseExpression, SubScript = sql.SubScript, NumericExpression = sql.NumericExpression, ColumnAll = sql.ColumnAll, Cast = sql.Cast, StringExpression = sql.StringExpression, BooleanExpression = sql.BooleanExpression, SQLFunction = sql.SQLFunction, LiteralString = sql.LiteralString, PlaceHolderLiteralString = sql.PlaceHolderLiteralString, QueryError = require("../errors").QueryError, patio; var Dataset; var clauseMethods = function (type, clauses) { if (isString(clauses)) { clauses = clauses.split(" "); } return clauses.map(function (clause) { return ["_", type, clause.charAt(0).toUpperCase(), clause.substr(1), "Sql"].join(""); }); }; define({ instance: { /**@lends patio.Dataset.prototype*/ /** * Dataset mixin that provides functions to the {@link patio.dataset.Dataset} to * create SELECT, UPDATE, CREATE, and DELETE SQL statements, based off of the the * methods invoked in the {@link patio.dataset._Query} * mixin. This class should not be used directly by * * @constructs * @memberOf patio.dataset * @name _Query * * @property {String} sql Readonly property that returns a SELECT statement. * @property {String} deleteSql DELETE SQL query string. See {@link patio.dataset._Actions#delete}. * * <pre class="code"> * dataset.filter(function(){ * return this.price.gte(100); * }).deleteSql; * // => "DELETE FROM items WHERE (price >= 100)" * </pre> * @property {String} selectSql Returns a SELECT SQL query string. * * <pre class="code"> * * DB.from("items").selectSql; * //=> "SELECT * FROM items" * </pre> * @property {String} truncateSql Returns a TRUNCATE SQL query string. See {@link patio.dataset._Actions#truncate} * * <pre class="code"> * * DB.from("items").truncateSql(); * //=> 'TRUNCATE items' * </pre> * @property {String} exists Returns an EXISTS clause for the dataset as a {@link patio.sql.LiteralString}. * * <pre class="code"> * * var ds = DB.from("test"); * ds.filter(ds.filter(sql.price.lt(100))).exists()).sql; * //=> 'SELECT * FROM test WHERE (EXISTS (SELECT * FROM test WHERE (price < 100)))' * * </pre> **/ constructor: function () { //We initialize these here because otherwise //the will be blank because of recursive dependencies. !patio && (patio = require("../index")); !Dataset && (Dataset = patio.Dataset); this.outputIdentifier = hitch(this, this.outputIdentifier); this._super(arguments); }, /** * Returns an INSERT SQL query string. See {@link patio.dataset._Actions#insert} * * @example * * * DB.from("items").insertSql({a : 1}); * //=> INSERT INTO items (a) VALUES (1) * * var ds = DB.from("test"); * * //default values * ds.insertSql(); * //=> INSERT INTO test DEFAULT VALUES * * //with hash * ds.insertSql({name:'wxyz', price:342}); * //=> INSERT INTO test (name, price) VALUES ('wxyz', 342) * ds.insertSql({}); * //=> INSERT INTO test DEFAULT VALUES * * //object that has a values property * ds.insertSql({values:{a:1}}); * //=> INSERT INTO test (a) VALUES (1) * * //arbitrary value * ds.insertSql(123); * //=> INSERT INTO test VALUES (123) * * //with dataset * ds.insertSql(DB.from("something").filter({x:2})); * //=> INSERT INTO test SELECT * FROM something WHERE (x = 2) * * //with array * ds.insertSql('a', 2, 6.5); * //=> INSERT INTO test VALUES ('a', 2, 6.5) * * @throws {patio.QueryError} if there are Different number of values and columns given to insertSql or * if an invalid BooleanExpresion is given. * * @param {patio.Dataset|patio.sql.LiteralString|Array|Object|patio.sql.BooleanExpression|...} values values to * insert into the database. The INSERT statement generated depends on the type. * <ul> * <li>Empty object| Or no arugments: then DEFAULT VALUES is used.</li> * <li>Object: the keys will be used as the columns, and values will be the values inserted.</li> * <li>Single {@link patio.Dataset} : an insert with subselect will be performed.</li> * <li>Array with {@link patio.Dataset} : The array will be used for columns and a subselect will performed with the dataset for the values.</li> * <li>{@link patio.sql.LiteralString} : the literal value will be used.</li> * <li>Single Array : the values in the array will be used as the VALUES clause.</li> * <li>Two Arrays: the first array is the columns the second array is the values.</li> * <li>{@link patio.sql.BooleanExpression} : the expression will be used as the values. * <li>An arbitrary number of arguments : the {@link patio.Dataset#literal} version of the values will be used</li> * </ul> * * * @return {String} a INSERT SQL query string */ insertSql: function (values) { values = argsToArray(arguments); var opts = this.__opts; if (opts.sql) { return this._staticSql(opts.sql); } this.__checkModificationAllowed(); var columns = []; switch (values.length) { case 0 : //we have no values return this.insertSql({}); case 1 : var vals = values[0], v; if (isInstanceOf(vals, Dataset, LiteralString) || isArray(vals)) { values = vals; } else if (vals.hasOwnProperty("values") && isObject((v = vals.values))) { return this.insertSql(v); } else if (isHash(vals)) { vals = merge({}, opts.defaults || {}, vals); vals = merge({}, vals, opts.overrides || {}); values = []; for (var i in vals) { columns.push(i); values.push(vals[i]); } } else if (isInstanceOf(vals, BooleanExpression)) { var op = vals.op; values = []; if (!isUndefinedOrNull(this._static.TWO_ARITY_OPERATORS[op])) { var args = vals.args; columns.push(args[0]); values.push(args[1]); } else { throw new QueryError("Invalid Expression op: " + op); } } break; case 2 : var v0 = values[0], v1 = values[1]; if (isArray(v0) && isArray(v1) || isInstanceOf(v1, Dataset, LiteralString)) { columns = v0, values = v1; if (isArray(values) && columns.length !== values.length) { throw new QueryError("Different number of values and columns given to insertSql"); } } break; } columns = columns.map(function (k) { return isString(k) ? new Identifier(k) : k; }, this); return this.mergeOptions({columns: columns, values: values})._insertSql(); }, /** * Returns an array of insert statements for inserting multiple records. * This method is used by {@link patio.dataset._Actions#multiInsert} to format insert statements. * <b>This method is not typically used directly.</b> * * <p> * <b>Note:</b>This method should be overridden by descendants if there is support for * inserting multiple records in a single SQL statement. * </p> * * @param {Array} columns The columns to insert values for. * This array will be used as the base for each values item in the values array. * @param {Array[Array]} values Array of arrays of values to insert into the columns. * * @return {String[]} array of insert statements. */ multiInsertSql: function (columns, values) { return values.map(function (r) { return this.insertSql(columns, r); }, this); }, /** *Formats an UPDATE statement using the given values. See {@link patio.dataset._Actions#update}. * * @example * * DB.from("items").updateSql({price : 100, category : 'software'}); * //=> "UPDATE items SET price = 100, category = 'software' * * @throw {QueryError} If the dataset is grouped or includes more than one table. * * @param {*...} Variable number of values to update the table with. * The UPDATE statement created depends on the values passed in. * <ul> * <li>Object : the keys will be used as the columns and the values will be the values to set to columns to</li> * <li>{@link patio.sql.Expression} : the {@link patio.dataset._Sql#literal} representation of the * {@link patio.sql.Expression} will be used as the value * </li> * </li> Other : the {@link patio.dataset._Sql#literal} value will be used as the value</li> * </ul> * * @return {String} the UPDATE statement. */ updateSql: function (values) { values = argsToArray(arguments); var update; if (this.__opts.sql) { update = this._staticSql(this.__opts.sql); } else { this.__checkModificationAllowed(); update = this.mergeOptions({values: values})._updateSql(); } return update; }, /** * Returns a qualified column name (including a table name) if the column * name isn't already qualified. * * @example * * dataset.qualifiedColumnName("b1", "items"); * //=> items.b1 * * dataset.qualifiedColumnName("ccc__b")); * //=> 'ccc.b' * * dataset.qualifiedColumnName("ccc__b", "items")); * //=> 'ccc.b' * * @param {String} column the column to qualify. If the column is already qualified (e.g. ccc__b) then the * table name (e.g. ccc) will override the provided table. * * @param {String} table the name of the table to qualify the column to. * * @return {String} the qualified column name.. * */ qualifiedColumnName: function (column, table) { if (isString(column)) { var parts = this._splitString(column); var columnTable = parts[0], alias = parts[2], schema, tableAlias; column = parts[1]; if (!columnTable) { if (isInstanceOf(table, Identifier)) { table = table.value; } if (isInstanceOf(table, AliasedExpression)) { tableAlias = table.alias; } else if (isInstanceOf(table, QualifiedIdentifier)) { tableAlias = table; } else { parts = this._splitString(table); schema = parts[0]; tableAlias = parts[2]; table = parts[1]; if (schema) { tableAlias = new Identifier(tableAlias) || new QualifiedIdentifier(schema, table); } } columnTable = tableAlias || table; } return new QualifiedIdentifier(columnTable, column); } else if (isInstanceOf(column, Identifier)) { return column.qualify(table); } else { return column; } }, /** * Creates a unique table alias that hasn't already been used in this dataset. * * @example * * DB.from("table").unusedTableAlias("t"); * //=> "t" * * DB.from("table").unusedTableAlias("table"); * //=> "table0" * * DB.from("table", "table0"]).unusedTableAlias("table"); * //=> "table1" * * @param {String|patio.sql.Identifier} tableAlias the table to get an unused alias for. * * @return {String} the implicit alias that is in tableAlias with a possible "N" * if the alias has already been used, where N is an integer starting at 0. */ unusedTableAlias: function (tableAlias) { tableAlias = this._toTableName(tableAlias); var usedAliases = [], from, join; if ((from = this.__opts.from) != null) { usedAliases = usedAliases.concat(from.map(function (n) { return this._toTableName(n); }, this)); } if ((join = this.__opts.join) != null) { usedAliases = usedAliases.concat(join.map(function (join) { if (join.tableAlias) { return this.__toAliasedTableName(join.tableAlias); } else { return this._toTableName(join.table); } }, this)); } if (usedAliases.indexOf(tableAlias) !== -1) { var base = tableAlias, i = 0; do { tableAlias = string.format("%s%d", base, i++); } while (usedAliases.indexOf(tableAlias) !== -1); } return tableAlias; }, /** * Returns a literal representation of a value to be used as part * of an SQL expression. * * @example * * DB.from("items").literal("abc'def\\") //=> "'abc''def\\\\'" * DB.from("items").literal("items__id") //=> "items.id" * DB.from("items").literal([1, 2, 3]) //=> "(1, 2, 3)" * DB.from("items").literal(DB.from("items")) //=> "(SELECT * FROM items)" * DB.from("items").literal(sql.x.plus(1).gt("y")); //=> "((x + 1) > y)" * * @throws {patio.QueryError} If an unsupported object is given. * @param {*} v the value to convert the the SQL literal representation * * @return {String} a literal representation of the value. */ literal: function (v) { if (isInstanceOf(v, Json, JsonArray)) { return this._literalJson(v); } else if (isInstanceOf(v, LiteralString)) { return "" + v; } else if (isString(v)) { return this._literalString(v); } else if (isNumber(v)) { return this._literalNumber(v); } else if (isInstanceOf(v, Expression)) { return this._literalExpression(v); } else if (isInstanceOf(v, Dataset)) { return this._literalDataset(v); } else if (isArray(v)) { return this._literalArray(v); } else if (isInstanceOf(v, sql.Year)) { return this._literalYear(v); } else if (isInstanceOf(v, sql.TimeStamp, sql.DateTime)) { return this._literalTimestamp(v); } else if (isDate(v)) { return this._literalDate(v); } else if (isInstanceOf(v, sql.Time)) { return this._literalTime(v); } else if (Buffer.isBuffer(v)) { return this._literalBuffer(v); } else if (isNull(v)) { return this._literalNull(); } else if (isBoolean(v)) { return this._literalBoolean(v); } else if (isHash(v)) { return this._literalObject(v); } else { return this._literalOther(v); } }, //BEGIN PROTECTED /** * * Qualify the given expression to the given table. * @param {patio.sql.Expression} column the expression to qualify * @param table the table to qualify the expression to */ _qualifiedExpression: function (e, table) { var h, i, args; if (isString(e)) { //this should not be hit but here just for completeness return this.stringToIdentifier(e); } else if (isArray(e)) { return e.map(function (exp) { return this._qualifiedExpression(exp, table); }, this); } else if (isInstanceOf(e, Identifier)) { return new QualifiedIdentifier(table, e); } else if (isInstanceOf(e, OrderedExpression)) { return new OrderedExpression(this._qualifiedExpression(e.expression, table), e.descending, {nulls: e.nulls}); } else if (isInstanceOf(e, AliasedExpression)) { return new AliasedExpression(this._qualifiedExpression(e.expression, table), e.alias); } else if (isInstanceOf(e, CaseExpression)) { args = [this._qualifiedExpression(e.conditions, table), this._qualifiedExpression(e.def, table)]; if (e.hasExpression) { args.push(this._qualifiedExpression(e.expression, table)); } return CaseExpression.fromArgs(args); } else if (isInstanceOf(e, Cast)) { return new Cast(this._qualifiedExpression(e.expr, table), e.type); } else if (isInstanceOf(e, SQLFunction)) { return SQLFunction.fromArgs([e.f].concat(this._qualifiedExpression(e.args, table))); } else if (isInstanceOf(e, ComplexExpression)) { return ComplexExpression.fromArgs([e.op].concat(this._qualifiedExpression(e.args, table))); } else if (isInstanceOf(e, SubScript)) { return new SubScript(this._qualifiedExpression(e.f, table), this._qualifiedExpression(e.sub, table)); } else if (isInstanceOf(e, PlaceHolderLiteralString)) { args = []; var eArgs = e.args; if (isHash(eArgs)) { h = {}; for (i in eArgs) { h[i] = this._qualifiedExpression(eArgs[i], table); } args = h; } else { args = this._qualifiedExpression(eArgs, table); } return new PlaceHolderLiteralString(e.str, args, e.parens); } else if (isHash(e)) { h = {}; for (i in e) { h[this._qualifiedExpression(i, table) + ""] = this._qualifiedExpression(e[i], table); } return h; } else { return e; } }, /** * Returns a string that is the name of the table. * * @throws {patio.QueryError} If the name is not a String {@link patio.sql.Identifier}, * {@link patio.sql.QualifiedIdentifier} or {@link patio.sql.AliasedExpression}. * * @param {String|patio.sql.Identifier|patio.sql.QualifiedIdentifier|patio.sql.AliasedExpression} name * the object to get the table name from. * * @return {String} the name of the table. */ _toTableName: function (name) { var ret; if (isString(name)) { var parts = this._splitString(name); var schema = parts[0], table = parts[1], alias = parts[2]; ret = (schema || alias) ? alias || table : table; } else if (isInstanceOf(name, Identifier)) { ret = name.value; } else if (isInstanceOf(name, QualifiedIdentifier)) { ret = this._toTableName(name.column); } else if (isInstanceOf(name, AliasedExpression)) { ret = this.__toAliasedTableName(name.alias); } else { throw new QueryError("Invalid object to retrieve the table name from"); } return ret; }, /** * Return the unaliased part of the identifier. Handles both * implicit aliases in strings, as well as {@link patio.sql.AliasedExpression}s. * Other objects are returned as is. * * @param {String|patio.sql.AliasedExpression|*} tableAlias the object to un alias * * @return {patio.sql.QualifiedIdentifier|String|*} the unaliased portion of the identifier */ _unaliasedIdentifier: function (c) { if (isString(c)) { var parts = this._splitString(c); var table = parts[0], column = parts[1]; if (table) { return new QualifiedIdentifier(table, column); } return column; } else if (isInstanceOf(c, AliasedExpression)) { return c.expression; } else { return c; } }, /** * Return a [@link patio.sql._Query#fromSelf} dataset if an order or limit is specified, so it works as expected * with UNION, EXCEPT, and INTERSECT clauses. */ _compoundFromSelf: function () { var opts = this.__opts; return (opts["limit"] || opts["order"]) ? this.fromSelf() : this; }, /** * Return true if the dataset has a non-null value for any key in opts. * @param opts the options to compate this datasets options to * * @return {Boolean} true if the dataset has a non-null value for any key in opts. */ _optionsOverlap: function (opts) { var o = []; for (var i in this.__opts) { if (!isUndefinedOrNull(this.__opts[i])) { o.push(i); } } return intersect(compact(o), opts).length !== 0; }, //Formats in INSERT statement using the stored columns and values. _insertSql: function () { return this._clauseSql("insert"); }, //Formats an UPDATE statement using the stored values. _updateSql: function () { return this._clauseSql("update"); }, //Formats the truncate statement. Assumes the table given has already been //literalized. _truncateSql: function (table) { return "TRUNCATE TABLE" + table; }, //Prepares an SQL statement by calling all clause methods for the given statement type. _clauseSql: function (type) { var sql = [("" + type).toUpperCase()]; try { this._static[sql + "_CLAUSE_METHODS"].forEach(function (m) { if (m.match("With")) { this[m](sql); } else { var sqlRet = this[m](); if (sqlRet) { sql.push(sqlRet); } } }, this); } catch (e) { throw e; } return sql.join(""); }, //SQL fragment specifying the table to insert INTO _insertIntoSql: function (sql) { return string.format(" INTO%s", this._sourceList(this.__opts.from)); }, //SQL fragment specifying the columns to insert into _insertColumnsSql: function (sql) { var columns = this.__opts.columns, ret = ""; if (columns && columns.length) { ret = " (" + columns.map( function (c) { return c.toString(this); }, this).join(this._static.COMMA_SEPARATOR) + ")"; } return ret; }, //SQL fragment specifying the values to insert. _insertValuesSql: function () { var values = this.__opts.values, ret = []; if (isArray(values)) { ret.push(values.length === 0 ? " DEFAULT VALUES" : " VALUES " + this.literal(values)); } else if (isInstanceOf(values, Dataset)) { ret.push(" " + this._subselectSql(values)); } else if (isInstanceOf(values, LiteralString)) { ret.push(" " + values.toString(this)); } else { throw new QueryError("Unsupported INSERT values type, should be an array or dataset"); } return ret.join(""); }, //SQL fragment for Array _arraySql: function (a) { return !a.length ? '(NULL)' : "(" + this.__expressionList(a) + ")"; }, //This method quotes the given name with the SQL standard double quote. //should be overridden by subclasses to provide quoting not matching the //SQL standard, such as backtick (used by MySQL and SQLite). _quotedIdentifier: function (name) { return string.format("\"%s\"", ("" + name).replace('"', '""')); }, /* This section is for easier adapter overrides of sql formatting. These metthods are used by patio.sql.* toString methods to generate sql. */ /** * @private For internal use by patio * * SQL fragment for AliasedExpression */ aliasedExpressionSql: function (ae) { return this.__asSql(this.literal(ae.expression), ae.alias); }, /** * @private For internal use by patio * SQL fragment for BooleanConstants * */ booleanConstantSql: function (constant) { return this.literal(constant); }, /** * @private For internal use by patio * SQL fragment for CaseExpression */ caseExpressionSql: function (ce) { var sql = ['(CASE ']; if (ce.expression) { sql.push(this.literal(ce.expression), " "); } var conds = ce.conditions; if (isArray(conds)) { conds.forEach(function (cond) { sql.push(format("WHEN %s THEN %s", this.literal(cond[0]), this.literal(cond[1]))); }, this); } else if (isHash(conds)) { for (var i in conds) { sql.push(format("WHEN %s THEN %s", this.literal(i), this.literal(conds[i]))); } } return format("%s ELSE %s END)", sql.join(""), this.literal(ce.def)); }, /** * @private For internal use by patio * SQL fragment for the SQL CAST expression * */ castSql: function (expr, type) { return string.format("CAST(%s AS %s)", this.literal(expr), this.db.castTypeLiteral(type)); }, /** * @private For internal use by patio * SQL fragment for specifying all columns in a given table **/ columnAllSql: function (ca) { return string.format("%s.*", this.quoteSchemaTable(ca.table)); }, /** * @private For internal use by patio * SQL fragment for complex expressions **/ complexExpressionSql: function (op, args) { var newOp; var isOperators = this._static.IS_OPERATORS, isLiterals = this._static.IS_LITERALS, l; if ((newOp = isOperators[op]) != null) { var r = args[1], v = isNull(r) ? isLiterals.NULL : isLiterals[r]; if (r == null || this.supportsIsTrue) { if (isUndefined(v)) { throw new QueryError(string.format("Invalid argument('%s') used for IS operator", r)); } l = args[0]; return string.format("(%s %s %s)", isString(l) ? l : this.literal(l), newOp, v); } else if (op === "IS") { return this.complexExpressionSql("EQ", args); } else { return this.complexExpressionSql("OR", [BooleanExpression.fromArgs(["NEQ"].concat(args)), new BooleanExpression("IS", args[0], null)]); } } else if (["IN", "NOTIN"].indexOf(op) !== -1) { var cols = args[0], vals = args[1], colArray = isArray(cols), valArray = false, emptyValArray = false; if (isArray(vals)) { valArray = true; emptyValArray = vals.length === 0; } if (emptyValArray) { if (op === "IN") { // if the array is empty, we return an expression that will be false return new BooleanExpression("EQ", 1, 0); } else { return this.literal({1: 1}); } } if (colArray) { if (!this.supportsMultipleColumnIn) { if (valArray) { var expr = BooleanExpression.fromArgs(["OR"].concat(vals.map(function (vs) { return BooleanExpression.fromValuePairs(array.zip(cols, vs)); }))); return this.literal(op === "IN" ? expr : expr.invert()); } } else { //If the columns and values are both arrays, use _arraySql instead of //literal so that if values is an array of two element arrays, it //will be treated as a value list instead of a condition specifier. return format("(%s %s %s)", isString(cols) ? cols : this.literal(cols), ComplexExpression.IN_OPERATORS[op], valArray ? this._arraySql(vals) : this.literal(vals)); } } else { return format("(%s %s %s)", isString(cols) ? cols : this.literal(cols), ComplexExpression.IN_OPERATORS[op], this.literal(vals)); } } else if ((newOp = this._static.TWO_ARITY_OPERATORS[op]) != null) { l = args[0]; return format("(%s %s %s)", isString(l) ? l : this.literal(l), newOp, this.literal(args[1])); } else if ((newOp = this._static.N_ARITY_OPERATORS[op]) != null) { return string.format("(%s)", args.map(this.literal, this).join(" " + newOp + " ")); } else if (op === "NOT") { return string.format("NOT %s", this.literal(args[0])); } else if (op === "NOOP") { return this.literal(args[0]); } else { throw new QueryError("Invalid operator " + op); } }, /** * @private For internal use by patio * * SQL fragment for constants * */ constantSql: function (constant) { return "" + constant; }, /** * @private For internal use by patio * * SQL fragment specifying an SQL function call * */ functionSql: function (f) { var args = f.args; return string.format("%s%s", f.f, args.length === 0 ? '()' : this.literal(args)); }, /** * @private For internal use by patio * SQL fragment specifying a JOIN clause without ON or USING. * */ joinClauseSql: function (jc) { var table = jc.table, tableAlias = jc.tableAlias; if (table === tableAlias) { tableAlias = null; } var tref = this.__tableRef(table); return string.format(" %s %s", this._joinTypeSql(jc.joinType), tableAlias ? this.__asSql(tref, tableAlias) : tref); }, /** * @private For internal use by patio * SQL fragment specifying a JOIN clause with ON. **/ joinOnClauseSql: function (jc) { return string.format("%s ON %s", this.joinClauseSql(jc), this.literal(this._filterExpr(jc.on))); }, /** * @private For internal use by patio * SQL fragment specifying a JOIN clause with USING. **/ joinUsingClauseSql: function (jc) { return string.format("%s USING (%s)", this.joinClauseSql(jc), this.__columnList(jc.using)); }, /** * @private For internal use by patio * SQL fragment for NegativeBooleanConstants. **/ negativeBooleanConstantSql: function (constant) { return string.format("NOT %s", this.booleanConstantSql(constant)); }, /** * @private For internal use by patio * * SQL fragment for the ordered expression, used in the ORDER BY * clause. */ orderedExpressionSql: function (oe) { var s = string.format("%s %s", this.literal(oe.expression), oe.descending ? "DESC" : "ASC"); if (oe.nulls) { s = string.format("%s NULLS %s", s, oe.nulls === "first" ? "FIRST" : "LAST"); } return s; }, /** * @private For internal use by patio * SQL fragment for a literal string with placeholders * */ placeholderLiteralStringSql: function (pls) { var args = pls.args; var s; if (isHash(args)) { for (var i in args) { args[i] = this.literal(args[i]); } s = string.format(pls.str, args); } else { s = pls.str.replace(this._static.QUESTION_MARK, "%s"); args = toArray(args).map(this.literal, this); s = string.format(s, args); } if (pls.parens) { s = string.format("(%s)", s); } return s; }, /** * @private For internal use by patio * SQL fragment for the qualifed identifier, specifying * a table and a column (or schema and table). */ qualifiedIdentifierSql: function (qcr) { return [qcr.table, qcr.column].map(function (x) { var isLiteral = [QualifiedIdentifier, Identifier, String].some(function (c) { return x instanceof c; }), ret; if (isLiteral) { ret = this.literal(x); } else { ret = this.quoteIdentifier(x); } return ret; }, this).join('.'); }, /** * @private For internal use by patio * * Adds quoting to identifiers (columns and tables). If identifiers are not * being quoted, returns name as a string. If identifiers are being quoted * quote the name with {@link patio.dataset._Sql#_quotedIdentifier}. */ quoteIdentifier: function (name) { if (isInstanceOf(name, LiteralString)) { return name; } else { if (isInstanceOf(name, Identifier)) { name = name.value; } name = this.inputIdentifier(name); if (this.quoteIdentifiers) { name = this._quotedIdentifier(name); } } return name; }, /** * @private For internal use by patio * * Modify the identifier returned from the database based on the * identifierOutputMethod. */ inputIdentifier: function (v) { var i = this.__identifierInputMethod; v = v.toString(this); return !isUndefinedOrNull(i) ? isFunction(v[i]) ? v[i]() : isFunction(comb[i]) ? comb[i](v) : v : v; }, /** * @private For internal use by patio * * Modify the identifier sent to the database based on the * identifierOutputMethod. */ outputIdentifier: function (v) { (v === '' && (v = 'untitled')); var i = this.__identifierOutputMethod; return !isUndefinedOrNull(i) ? isFunction(v[i]) ? v[i]() : isFunction(comb[i]) ? comb[i](v) : v : v; }, /** * @private For For internal use by patio * * Separates the schema from the table and returns a string with them * quoted (if quoting identifiers) */ quoteSchemaTable: function (table) { var parts = this.schemaAndTable(table); var schema = parts[0]; table = parts[1]; return string.format("%s%s", schema ? this.quoteIdentifier(schema) + "." : "", this.quoteIdentifier(table)); }, /** * @private For For internal use by patio * Split the schema information from the table */ schemaAndTable: function (tableName) { var sch = this.db ? this.db.defaultSchema || null : null; if (isString(tableName)) { var parts = this._splitString(tableName); var s = parts[0], table = parts[1]; return [s || sch, table]; } else if (isInstanceOf(tableName, QualifiedIdentifier)) { return [tableName.table, tableName.column]; } else if (isInstanceOf(tableName, Identifier)) { return [null, tableName.value]; } else if (isInstanceOf(tableName, LiteralString)) { return [null, tableName]; } else { throw new QueryError("table should be a QualifiedIdentifier, Identifier, or String"); } }, /** * @private For For internal use by patio * SQL fragment for specifying subscripts (SQL array accesses) * */ subscriptSql: function (s) { return string.format("%s[%s]", this.literal(s.f), this.__expressionList(s.sub)); }, /** * Do a simple join of the arguments (which should be strings) separated by commas * */ __argumentList: function (args) { return args.join(this._static.COMMA_SEPARATOR); }, /** * SQL fragment for specifying an alias. expression should already be literalized. */ __asSql: function (expression, alias) { return string.format("%s AS %s", expression, this.quoteIdentifier(alias)); }, /** * Converts an array of column names into a comma seperated string of * column names. If the array is empty, a wildcard (*) is returned. */ __columnList: function (columns) { return (!columns || columns.length === 0) ? this._static.WILDCARD : this.__expressionList(columns); }, /** * The alias to use for datasets, takes a number to make sure the name is unique. * */ _datasetAlias: function (number) { return this._static.DATASET_ALIAS_BASE_NAME + number; }, /** * Converts an array of expressions into a comma separated string of * expressions. */ __expressionList: function (columns) { return columns.map(this.literal, this).join(this._static.COMMA_SEPARATOR); }, //Format the timestamp based on the default_timestamp_format, with a couple //of modifiers. First, allow %N to be used for fractions seconds (if the //database supports them), and override %z to always use a numeric offset //of hours and minutes. formatTimestamp: function (v, format) { return this.literal(patio.dateToString(v, format)); }, /** * SQL fragment specifying a JOIN type, splits a camelCased join type * and converts to uppercase/ */ _joinTypeSql: function (joinType) { return (joinType || "").replace(/([a-z]+)|([A-Z][a-z]+)/g, function (m) { return m.toUpperCase() + " "; }).trimRight() + " JOIN"; }, /* Methods for converting types to a SQL . */ /** * @return SQL fragment for a type of object not handled by {@link patio.dataset._Sql#literal}. * If object has a method sqlLiteral then it is called with this dataset as the first argument, * otherwise raises an error. Classes implementing sqlLiteral should call a class-specific method * on the dataset provided and should add that method to {@link patio.dataset.Dataset}, allowing for adapters * to provide customized literalizations. * If a database specific type is allowed, this should be overriden in a subclass. */ _literalOther: function (v) { if (isFunction(v.sqlLiteral)) { return v.sqlLiteral(this); } else { throw new QueryError(string.format("can't express %j as a SQL literal", [v])); } }, /** *@return SQL fragment for Buffer, treated as an expression * */ _literalBuffer: function (b) { return "X'" + b.toString("hex") + "'"; }, /** *@return SQL fragment for Hash, treated as an expression * */ _literalObject: function (v) { return this._literalExpression(BooleanExpression.fromValuePairs(v)); }, /** * @return SQL fragment for Array. Treats as an expression if an array of all two pairs, or as a SQL array otherwise. */ _literalArray: function (v) { return Expression.isConditionSpecifier(v) ? this._literalExpression(BooleanExpression.fromValuePairs(v)) : this._arraySql(v); }, /** * @return SQL fragment for a number. */ _literalNumber: function (num) { var ret = "" + num; if (isNaN(num) || num === Infinity) { ret = string.format("'%s'", ret); } return ret; }, /** * @return SQL fragment for Dataset. Does a subselect inside parantheses. */ _literalDataset: function (dataset) { return string.format("(%s)", this._subselectSql(dataset)); }, /** * @return SQL fragment for Date, using the ISO8601 format. */ _literalDate: function (date) { return (this.requiresSqlStandardDateTimes ? "DATE '" : "'") + patio.dateToString(date) + "'"; }, /** *@return SQL fragment for a year. */ _literalYear: function (o) { return patio.dateToString(o, this._static.YEAR_FORMAT); }, /** *@return SQL fragment for a timestamp, using the ISO8601 format. */ _literalTimestamp: function (v) { return this.formatTimestamp(v, this._static.TIMESTAMP_FORMAT); }, /** *@return SQL fragment for a timestamp, using the ISO8601 format. */ _literalTime: function (v) { return this.formatTimestamp(v, this._static.TIME_FORMAT); }, /** * @return SQL fragment for a boolean. */ _literalBoolean: function (b) { return b ? this._static.BOOL_TRUE : this._static.BOOL_FALSE; }, /** * @return SQL fragment for SQL::Expression, result depends on the specific type of expression. * */ _literalExpression: function (v) { return v.toString(this); }, /** *@return SQL fragment for Hash, treated as an expression * */ _literalHash: function (v) { return this._literalExpression(BooleanExpression.fromValuePairs(v)); }, /**@return SQL fragment for null*/ _literalNull: function () { return this._static.NULL; }, /** * @return SQL fragment for String. Doubles \ and ' by default. * */ _literalString: function (v) { var parts = this._splitString(v); var table = parts[0], column = parts[1], alias = parts[2], ret; if (!alias) { if (column && table) { ret = this._literalExpression(QualifiedIdentifier.fromArgs([table, column])); } else { ret = "'" + v.replace(/\\/g, "\\\\").replace(/'/g, "''") + "'"; } } else { if (column && table) { ret = new AliasedExpression(QualifiedIdentifier.fromArgs([table, column]), alias); } else { ret = new AliasedExpression(new Identifier(column), alias); } ret = this.literal(ret); } return ret; }, /** * @return SQL fragment for json. Doubles ' by default. * */ _literalJson: function (v) { throw new QueryError("Json not supported."); }, /*SQL STATEMENT CREATION METHODS*/ _selectQualifySql: function () { var o = this.__opts; var table = this.__opts.alwaysQualify; if (table && !o.sql) { array.intersect(Object.keys(o), this._static.QUALIFY_KEYS).forEach(function (k) { o[k] = this._qualifiedExpression(o[k], table); }, this); if (!o.select || isEmpty(o.select)) { o.select = [new ColumnAll(table)]; } } }, _deleteQualifySql: function () { return this._selectQualifySql.apply(this, arguments); }, /** * @return the columns selected * */ _selectColumnsSql: function () { return " " + this.__columnList(this.__opts.select); }, /**@return the DISTINCT clause.*/ _selectDistinctSql: function () { var distinct = this.__opts.distinct, ret = []; if (distinct) { ret.push(" DISTINCT"); if (distinct.length) { ret.push(format(" ON (%s)", this.__expressionList(distinct))); } } return ret.join(""); }, /** * @return the EXCEPT, INTERSECT, or UNION clause. * This uses a subselect for the compound datasets used, because using parantheses doesn't * work on all databases. **/ _selectCompoundsSql: function () { var opts = this.__opts, compounds = opts.compounds, ret = []; if (compounds) { compounds.forEach(function (c) { var type = c[0], dataset = c[1], all = c[2]; ret.push(string.format(" %s%s %s", type.toUpperCase(), all ? " ALL" : "", this._subselectSql(dataset)));