UNPKG

patio

Version:
461 lines (436 loc) 21.7 kB
var comb = require("comb"), logging = comb.logging, Logger = logging.Logger, errors = require("../errors"), QueryError = errors.QueryError, DatasetError = errors.DatasetError, Promise = comb.Promise, PromiseList = comb.PromiseList, isUndefined = comb.isUndefined, isUndefinedOrNull = comb.isUndefinedOrNull, isString = comb.isString, isInstanceOf = comb.isInstanceOf, isString = comb.isString, isFunction = comb.isFunction, isNull = comb.isNull, merge = comb.merge, define = comb.define, graph = require("./graph"), actions = require("./actions"), features = require("./features"), query = require("./query"), sql = require("./sql"), SQL = require("../sql").sql, AliasedExpression = SQL.AliasedExpression, Identifier = SQL.Identifier, QualifiedIdentifier = SQL.QualifiedIdentifier; var LOGGER = comb.logger("patio.Dataset"); define([actions, graph, features, query, sql], { instance: { /**@lends patio.Dataset.prototype*/ /** * Class that is used for querying/retrieving datasets from a database. * * <p> Dynamically generated methods include * <ul> * <li>Join methods from {@link patio.Dataset.CONDITIONED_JOIN_TYPES} and * {@link patio.Dataset.UNCONDITIONED_JOIN_TYPES}, these methods handle the type call * to {@link patio.Dataset#joinTable}, so to invoke include all arguments that * {@link patio.Dataset#joinTable} requires except the type parameter. The default list includes. * <ul> * <li>Conditioned join types that accept conditions. * <ul> * <li>inner - INNER JOIN</li> * <li>fullOuter - FULL OUTER</li> * <li>rightOuter - RIGHT OUTER JOIN</li> * <li>leftOuter - LEFT OUTER JOIN</li> * <li>full - FULL JOIN</li> * <li>right - RIGHT JOIN</li> * <li>left - LEFT JOIN</li> * </ul> * </li> * <li>Unconditioned join types that do not accept join conditions * <ul> * <li>natural - NATURAL JOIN</li> * <li>naturalLeft - NATURAL LEFT JOIN</li> * <li>naturalRight - NATURAL RIGHT JOIN</li> * <li>naturalFull - NATURA FULLL JOIN</li> * <li>cross - CROSS JOIN</li> * </ul> * </li> * </ul> * </li> * </li> * </ul> * * <p> * <h4>Features:</h4> * <p> * Features that a particular {@link patio.Dataset} supports are shown in the example below. * If you wish to implement an adapter please override these values depending on the database that * you are developing the adapter for. * </p> * <pre class="code"> * var ds = DB.from("test"); * * //The default values returned * * //Whether this dataset quotes identifiers. * //Whether this dataset quotes identifiers. * ds.quoteIdentifiers //=>true * * //Whether this dataset will provide accurate number of rows matched for * //delete and update statements. Accurate in this case is the number of * //rows matched by the dataset's filter. * ds.providesAccurateRowsMatched; //=>true * * //Times Whether the dataset requires SQL standard datetimes (false by default, * // as most allow strings with ISO 8601 format). * ds.requiresSqlStandardDate; //=>false * * //Whether the dataset supports common table expressions (the WITH clause). * ds.supportsCte; //=>true * * //Whether the dataset supports the DISTINCT ON clause, false by default. * ds.supportsDistinctOn; //=>false * * //Whether the dataset supports the INTERSECT and EXCEPT compound operations, true by default. * ds.supportsIntersectExcept; //=>true * * //Whether the dataset supports the INTERSECT ALL and EXCEPT ALL compound operations, true by default * ds.supportsIntersectExceptAll; //=>true * * //Whether the dataset supports the IS TRUE syntax. * ds.supportsIsTrue; //=>true * * //Whether the dataset supports the JOIN table USING (column1, ...) syntax. * ds.supportsJoinUsing; //=>true * * //Whether modifying joined datasets is supported. * ds.supportsModifyingJoin; //=>false * * //Whether the IN/NOT IN operators support multiple columns when an * ds.supportsMultipleColumnIn; //=>true * * //Whether the dataset supports timezones in literal timestamps * ds.supportsTimestampTimezone; //=>false * * //Whether the dataset supports fractional seconds in literal timestamps * ds.supportsTimestampUsecs; //=>true * * //Whether the dataset supports window functions. * ds.supportsWindowFunctions; //=>false * </pre> * <p> * <p> * <h4>Actions</h4> * <p> * Each dataset does not actually send any query to the database until an action method has * been called upon it(with the exception of {@link patio.Dataset#graph} because columns * from the other table might need retrived in order to set up the graph). Each action * returns a <i>comb.Promise</i> that will be resolved with the result or errback, it is important * that you account for errors otherwise it can be difficult to track down issues. * The list of action methods is: * <ul> * <li>{@link patio.Dataset#all}</li> * <li>{@link patio.Dataset#one}</li> * <li>{@link patio.Dataset#avg}</li> * <li>{@link patio.Dataset#count}</li> * <li>{@link patio.Dataset#columns}</li> * <li>{@link patio.Dataset#remove}</li> * <li>{@link patio.Dataset#forEach}</li> * <li>{@link patio.Dataset#empty}</li> * <li>{@link patio.Dataset#first}</li> * <li>{@link patio.Dataset#get}</li> * <li>{@link patio.Dataset#import}</li> * <li>{@link patio.Dataset#insert}</li> * <li>{@link patio.Dataset#save}</li> * <li>{@link patio.Dataset#insertMultiple}</li> * <li>{@link patio.Dataset#saveMultiple}</li> * <li>{@link patio.Dataset#interval}</li> * <li>{@link patio.Dataset#last}</li> * <li>{@link patio.Dataset#map}</li> * <li>{@link patio.Dataset#max}</li> * <li>{@link patio.Dataset#min}</li> * <li>{@link patio.Dataset#multiInsert}</li> * <li>{@link patio.Dataset#range}</li> * <li>{@link patio.Dataset#selectHash}</li> * <li>{@link patio.Dataset#selectMap}</li> * <li>{@link patio.Dataset#selectOrderMap}</li> * <li>{@link patio.Dataset#set}</li> * <li>{@link patio.Dataset#singleRecord}</li> * <li>{@link patio.Dataset#singleValue}</li> * <li>{@link patio.Dataset#sum}</li> * <li>{@link patio.Dataset#toCsv}</li> * <li>{@link patio.Dataset#toHash}</li> * <li>{@link patio.Dataset#truncate}</li> * <li>{@link patio.Dataset#update}</li> * </ul> * * </p> * </p> * * @constructs * * * @param {patio.Database} db the database this dataset should use when querying for data. * @param {Object} opts options to set on this dataset instance * * @property {Function} rowCb callback to be invoked for each row returned from the database. * the return value will be used as the result of query. The rowCb can also return a promise, * The resolved value of the promise will be used as result. * * @property {String} identifierInputMethod this is the method that will be called on each identifier returned from the database. * This value will be defaulted to whatever the identifierInputMethod * is on the database used in initialization. * * @property {String} identifierOutputMethod this is the method that will be called on each identifier sent to the database. * This value will be defaulted to whatever the identifierOutputMethod * is on the database used in initialization. * @property {String} firstSourceAlias The first source (primary table) for this dataset. If the table is aliased, returns the aliased name. * throws a {patio.DatasetError} tf the dataset doesn't have a table. * <pre class="code"> * DB.from("table").firstSourceAlias; * //=> "table" * * DB.from("table___t").firstSourceAlias; * //=> "t" * </pre> * * @property {String} firstSourceTable The first source (primary table) for this dataset. If the dataset doesn't * have a table, raises a {@link patio.erros.DatasetError}. *<pre class="code"> * * DB.from("table").firstSourceTable; * //=> "table" * * DB.from("table___t").firstSourceTable; * //=> "t" * </pre> * @property {Boolean} isSimpleSelectAll Returns true if this dataset is a simple SELECT * FROM {table}, otherwise false. * <pre class="code"> * DB.from("items").isSimpleSelectAll; //=> true * DB.from("items").filter({a : 1}).isSimpleSelectAll; //=> false * </pre> * @property {boolean} [quoteIdentifiers=true] Whether this dataset quotes identifiers. * @property {boolean} [providesAccurateRowsMatched=true] Whether this dataset will provide accurate number of rows matched for * delete and update statements. Accurate in this case is the number of * rows matched by the dataset's filter. * @property {boolean} [requiresSqlStandardDate=false] Whether the dataset requires SQL standard datetimes (false by default, * as most allow strings with ISO 8601 format). * @property {boolean} [supportsCte=true] Whether the dataset supports common table expressions (the WITH clause). * @property {boolean} [supportsDistinctOn=false] Whether the dataset supports the DISTINCT ON clause, false by default. * @property {boolean} [supportsIntersectExcept=true] Whether the dataset supports the INTERSECT and EXCEPT compound operations, true by default. * @property {boolean} [supportsIntersectExceptAll=true] Whether the dataset supports the INTERSECT ALL and EXCEPT ALL compound operations, true by default. * @property {boolean} [supportsIsTrue=true] Whether the dataset supports the IS TRUE syntax. * @property {boolean} [supportsJoinUsing=true] Whether the dataset supports the JOIN table USING (column1, ...) syntax. * @property {boolean} [supportsModifyingJoin=false] Whether modifying joined datasets is supported. * @property {boolean} [supportsMultipleColumnIn=true] Whether the IN/NOT IN operators support multiple columns when an * @property {boolean} [supportsTimestampTimezone=false] Whether the dataset supports timezones in literal timestamps * @property {boolean} [supportsTimestampUsecs=true] Whether the dataset supports fractional seconds in literal timestamps * @property {boolean} [supportsWindowFunctions=false] Whether the dataset supports window functions. * @property {patio.sql.Identifier[]} [sourceList=[]] a list of sources for this dataset. * @property {patio.sql.Identifier[]} [joinSourceList=[]] a list of join sources * @property {Boolean} hasSelectSource true if this dataset already has a select sources. */ constructor: function (db, opts) { this._super(arguments); this.db = db; this.__opts = {}; this.__rowCb = null; if (db) { this.__quoteIdentifiers = db.quoteIdentifiers; this.__identifierInputMethod = db.identifierInputMethod; this.__identifierOutputMethod = db.identifierOutputMethod; } }, /** * Returns a new clone of the dataset with with the given options merged into the current datasets options. * If the options changed include options in {@link patio.dataset.Query#COLUMN_CHANGE_OPTS}, the cached * columns are deleted. This method should generally not be called * directly by user code. * * @param {Object} opts options to merge into the curred datasets options and applied to the returned dataset. * @return [patio.Dataset] a cloned dataset with the merged options **/ mergeOptions: function (opts) { opts = isUndefined(opts) ? {} : opts; var ds = new this._static(this.db, {}); ds.rowCb = this.rowCb; this._static.FEATURES.forEach(function (f) { ds[f] = this[f]; }, this); var dsOpts = ds.__opts = merge({}, this.__opts, opts); ds.identifierInputMethod = this.identifierInputMethod; ds.identifierOutputMethod = this.identifierOutputMethod; var columnChangeOpts = this._static.COLUMN_CHANGE_OPTS; if (Object.keys(opts).some(function (o) { return columnChangeOpts.indexOf(o) !== -1; })) { dsOpts.columns = null; } return ds; }, /** * Converts a string to an {@link patio.sql.Identifier}, {@link patio.sql.QualifiedIdentifier}, * or {@link patio.sql.AliasedExpression}, depending on the format: * * <ul> * <li>For columns : table__column___alias.</li> * <li>For tables : schema__table___alias.</li> * </ul> * each portion of the identifier is optional. See example below * * @example * * ds.stringToIdentifier("a") //= > new patio.sql.Identifier("a"); * ds.stringToIdentifier("table__column"); //=> new patio.sql.QualifiedIdentifier(table, column); * ds.stringToIdentifier("table__column___alias"); * //=> new patio.sql.AliasedExpression(new patio.sql.QualifiedIdentifier(table, column), alias); * * @param {String} name the name to covert to an an {@link patio.sql.Identifier}, {@link patio.sql.QualifiedIdentifier}, * or {@link patio.sql.AliasedExpression}. * * @return {patio.sql.Identifier|patio.sql.QualifiedIdentifier|patio.sql.AliasedExpression} an identifier generated based on the name string. */ stringToIdentifier: function (name) { if (isString(name)) { var parts = this._splitString(name), schema = parts[0], table = parts[1], alias = parts[2], identifier; if (schema && table && alias) { identifier = new AliasedExpression(new QualifiedIdentifier(schema, table), alias); } else if (schema && table) { identifier = new QualifiedIdentifier(schema, table); } else if (table && alias) { identifier = new AliasedExpression(new Identifier(table), alias); } else { identifier = new Identifier(table); } return identifier; } else { return name; } }, /** * Can either be a string or null. * * * @example * //columns * table__column___alias //=> table.column as alias * table__column //=> table.column * //tables * schema__table___alias //=> schema.table as alias * schema__table //=> schema.table * * //name and alias * columnOrTable___alias //=> columnOrTable as alias * * * * @return {String[]} an array with the elements being: * <ul> * <li>For columns :[table, column, alias].</li> * <li>For tables : [schema, table, alias].</li> * </ul> */ _splitString: function (s) { var ret, m; if ((m = s.match(this._static.COLUMN_REF_RE1)) !== null) { ret = m.slice(1); } else if ((m = s.match(this._static.COLUMN_REF_RE2)) !== null) { ret = [null, m[1], m[2]]; } else if ((m = s.match(this._static.COLUMN_REF_RE3)) !== null) { ret = [m[1], m[2], null]; } else { ret = [null, s, null]; } return ret; }, /** * @ignore **/ getters: { rowCb: function () { return this.__rowCb; }, identifierInputMethod: function () { return this.__identifierInputMethod; }, identifierOutputMethod: function () { return this.__identifierOutputMethod; }, firstSourceAlias: function () { var source = this.__opts.from; if (isUndefinedOrNull(source) || !source.length) { throw new DatasetError("No source specified for the query"); } source = source[0]; if (isInstanceOf(source, AliasedExpression)) { return source.alias; } else if (isString(source)) { var parts = this._splitString(source); var alias = parts[2]; return alias ? alias : source; } else { return source; } }, firstSourceTable: function () { var source = this.__opts.from; if (isUndefinedOrNull(source) || !source.length) { throw new QueryError("No source specified for the query"); } source = source[0]; if (isInstanceOf(source, AliasedExpression)) { return source.expression; } else if (isString(source)) { var parts = this._splitString(source); return source; } else { return source; } }, sourceList: function () { return (this.__opts.from || []).map(this.stringToIdentifier, this); }, joinSourceList: function () { return (this.__opts.join || []).map(function (join) { return this.stringToIdentifier(join.tableAlias || join.table); }, this); }, hasSelectSource: function () { var select = this.__opts.select; return !(isUndefinedOrNull(select) || select.length === 0); } }, /** * @ignore **/ setters: { /**@lends patio.Dataset.prototype*/ identifierInputMethod: function (meth) { this.__identifierInputMethod = meth; }, identifierOutputMethod: function (meth) { this.__identifierOutputMethod = meth; }, rowCb: function (cb) { if (isFunction(cb) || isNull(cb)) { this.__rowCb = cb; } else { throw new DatasetError("rowCb mus be a function"); } } } }, static: { COLUMN_REF_RE1: /^(\w+)__(\w+)___(\w+)$/, COLUMN_REF_RE2: /^(\w+)___(\w+)$/, COLUMN_REF_RE3: /^(\w+)__(\w+)$/ } }).as(module);