sequelize
Version:
Multi dialect ORM for Node.JS/io.js
1,379 lines (1,192 loc) • 85.3 kB
JavaScript
'use strict';
var Utils = require('../../utils')
, SqlString = require('../../sql-string')
, Model = require('../../model')
, DataTypes = require('../../data-types')
, _ = require('lodash')
, util = require('util')
, Dottie = require('dottie')
, BelongsTo = require('../../associations/belongs-to')
, BelongsToMany = require('../../associations/belongs-to-many')
, HasMany = require('../../associations/has-many')
, uuid = require('uuid')
, semver = require('semver');
/* istanbul ignore next */
var throwMethodUndefined = function(methodName) {
throw new Error('The method "' + methodName + '" is not defined! Please add it to your sql dialect.');
};
var QueryGenerator = {
options: {},
extractTableDetails: function(tableName, options) {
options = options || {};
tableName = tableName || {};
return {
schema: tableName.schema || options.schema || 'public',
tableName: _.isPlainObject(tableName) ? tableName.tableName : tableName,
delimiter: tableName.delimiter || options.delimiter || '.'
};
},
addSchema: function(param) {
var self = this;
if (!param.$schema) return param.tableName || param;
return {
tableName: param.tableName || param,
table: param.tableName || param,
name: param.name || param,
schema: param.$schema,
delimiter: param.$schemaDelimiter || '.',
toString: function() {
return self.quoteTable(this);
}
};
},
/*
Returns a query for dropping a schema
*/
dropSchema: function(tableName, options) {
return this.dropTableQuery(tableName, options);
},
/*
Returns a query for creating a table.
Parameters:
- tableName: Name of the new table.
- attributes: An object with containing attribute-attributeType-pairs.
Attributes should have the format:
{attributeName: type, attr2: type2}
--> e.g. {title: 'VARCHAR(255)'}
- options: An object with options.
Defaults: { engine: 'InnoDB', charset: null }
*/
/* istanbul ignore next */
createTableQuery: function(tableName, attributes, options) {
throwMethodUndefined('createTableQuery');
},
versionQuery: function(tableName, attributes, options) {
throwMethodUndefined('versionQuery');
},
describeTableQuery: function(tableName, schema, schemaDelimiter) {
var table = this.quoteTable(
this.addSchema({
tableName: tableName,
$schema: schema,
$schemaDelimiter: schemaDelimiter
})
);
return 'DESCRIBE ' + table + ';';
},
/*
Returns a query for dropping a table.
*/
dropTableQuery: function(tableName, options) {
options = options || {};
var query = 'DROP TABLE IF EXISTS <%= table %>;';
return Utils._.template(query)({
table: this.quoteTable(tableName)
});
},
/*
Returns a rename table query.
Parameters:
- originalTableName: Name of the table before execution.
- futureTableName: Name of the table after execution.
*/
renameTableQuery: function(before, after) {
var query = 'ALTER TABLE <%= before %> RENAME TO <%= after %>;';
return Utils._.template(query)({
before: this.quoteTable(before),
after: this.quoteTable(after)
});
},
/*
Returns a query, which gets all available table names in the database.
*/
/* istanbul ignore next */
showTablesQuery: function() {
throwMethodUndefined('showTablesQuery');
},
/*
Returns a query, which adds an attribute to an existing table.
Parameters:
- tableName: Name of the existing table.
- attributes: A hash with attribute-attributeOptions-pairs.
- key: attributeName
- value: A hash with attribute specific options:
- type: DataType
- defaultValue: A String with the default value
- allowNull: Boolean
*/
/* istanbul ignore next */
addColumnQuery: function(tableName, attributes) {
throwMethodUndefined('addColumnQuery');
},
/*
Returns a query, which removes an attribute from an existing table.
Parameters:
- tableName: Name of the existing table
- attributeName: Name of the obsolete attribute.
*/
/* istanbul ignore next */
removeColumnQuery: function(tableName, attributeName) {
throwMethodUndefined('removeColumnQuery');
},
/*
Returns a query, which modifies an existing attribute from a table.
Parameters:
- tableName: Name of the existing table.
- attributes: A hash with attribute-attributeOptions-pairs.
- key: attributeName
- value: A hash with attribute specific options:
- type: DataType
- defaultValue: A String with the default value
- allowNull: Boolean
*/
/* istanbul ignore next */
changeColumnQuery: function(tableName, attributes) {
throwMethodUndefined('changeColumnQuery');
},
/*
Returns a query, which renames an existing attribute.
Parameters:
- tableName: Name of an existing table.
- attrNameBefore: The name of the attribute, which shall be renamed.
- attrNameAfter: The name of the attribute, after renaming.
*/
/* istanbul ignore next */
renameColumnQuery: function(tableName, attrNameBefore, attrNameAfter) {
throwMethodUndefined('renameColumnQuery');
},
/*
Returns an insert into command. Parameters: table name + hash of attribute-value-pairs.
*/
insertQuery: function(table, valueHash, modelAttributes, options) {
options = options || {};
_.defaults(options, this.options);
var query
, valueQuery = '<%= tmpTable %>INSERT<%= ignore %> INTO <%= table %> (<%= attributes %>)<%= output %> VALUES (<%= values %>)'
, emptyQuery = '<%= tmpTable %>INSERT<%= ignore %> INTO <%= table %><%= output %>'
, outputFragment
, fields = []
, values = []
, key
, value
, identityWrapperRequired = false
, modelAttributeMap = {}
, tmpTable = '' //tmpTable declaration for trigger
, selectFromTmp = '' //Select statement for trigger
, tmpColumns = '' //Columns for temp table for trigger
, outputColumns = '' //Columns to capture into temp table for trigger
, attribute //Model attribute holder
, modelKey; //key for model
if (modelAttributes) {
Utils._.each(modelAttributes, function(attribute, key) {
modelAttributeMap[key] = attribute;
if (attribute.field) {
modelAttributeMap[attribute.field] = attribute;
}
});
}
if (this._dialect.supports['DEFAULT VALUES']) {
emptyQuery += ' DEFAULT VALUES';
} else if (this._dialect.supports['VALUES ()']) {
emptyQuery += ' VALUES ()';
}
if (this._dialect.supports.returnValues && options.returning) {
if (!!this._dialect.supports.returnValues.returning) {
valueQuery += ' RETURNING *';
emptyQuery += ' RETURNING *';
} else if (!!this._dialect.supports.returnValues.output) {
outputFragment = ' OUTPUT INSERTED.*';
//To capture output rows when there is a trigger on MSSQL DB
if (modelAttributes && options.hasTrigger && this._dialect.supports.tmpTableTrigger) {
tmpTable = 'declare @tmp table (<%= columns %>); ';
for (modelKey in modelAttributes){
attribute = modelAttributes[modelKey];
if(!(attribute.type instanceof DataTypes.VIRTUAL)){
if (tmpColumns.length > 0){
tmpColumns += ',';
outputColumns += ',';
}
tmpColumns += this.quoteIdentifier(attribute.field) + ' ' + attribute.type.toSql();
outputColumns += 'INSERTED.' + this.quoteIdentifier(attribute.field);
}
}
var replacement ={
columns : tmpColumns
};
tmpTable = Utils._.template(tmpTable)(replacement).trim();
outputFragment = ' OUTPUT ' + outputColumns + ' into @tmp';
selectFromTmp = ';select * from @tmp';
valueQuery += selectFromTmp;
emptyQuery += selectFromTmp;
}
}
}
if (this._dialect.supports.EXCEPTION && options.exception) {
// Mostly for internal use, so we expect the user to know what he's doing!
// pg_temp functions are private per connection, so we never risk this function interfering with another one.
if (semver.gte(this.sequelize.options.databaseVersion, '9.2.0')) {
// >= 9.2 - Use a UUID but prefix with 'func_' (numbers first not allowed)
var delimiter = '$func_' + uuid.v4().replace(/-/g, '') + '$';
options.exception = 'WHEN unique_violation THEN GET STACKED DIAGNOSTICS sequelize_caught_exception = PG_EXCEPTION_DETAIL;';
valueQuery = 'CREATE OR REPLACE FUNCTION pg_temp.testfunc(OUT response <%= table %>, OUT sequelize_caught_exception text) RETURNS RECORD AS ' + delimiter +
' BEGIN ' + valueQuery + ' INTO response; EXCEPTION ' + options.exception + ' END ' + delimiter +
' LANGUAGE plpgsql; SELECT (testfunc.response).*, testfunc.sequelize_caught_exception FROM pg_temp.testfunc(); DROP FUNCTION IF EXISTS pg_temp.testfunc()';
} else {
options.exception = 'WHEN unique_violation THEN NULL;';
valueQuery = 'CREATE OR REPLACE FUNCTION pg_temp.testfunc() RETURNS SETOF <%= table %> AS $body$ BEGIN RETURN QUERY ' + valueQuery + '; EXCEPTION ' + options.exception + ' END; $body$ LANGUAGE plpgsql; SELECT * FROM pg_temp.testfunc(); DROP FUNCTION IF EXISTS pg_temp.testfunc();';
}
}
if (this._dialect.supports['ON DUPLICATE KEY'] && options.onDuplicate) {
valueQuery += ' ON DUPLICATE KEY ' + options.onDuplicate;
emptyQuery += ' ON DUPLICATE KEY ' + options.onDuplicate;
}
valueHash = Utils.removeNullValuesFromHash(valueHash, this.options.omitNull);
for (key in valueHash) {
if (valueHash.hasOwnProperty(key)) {
value = valueHash[key];
fields.push(this.quoteIdentifier(key));
// SERIALS' can't be NULL in postgresql, use DEFAULT where supported
if (modelAttributeMap && modelAttributeMap[key] && modelAttributeMap[key].autoIncrement === true && !value) {
if (!this._dialect.supports.autoIncrement.defaultValue) {
fields.splice(-1,1);
} else if (this._dialect.supports.DEFAULT) {
values.push('DEFAULT');
} else {
values.push(this.escape(null));
}
} else {
if (modelAttributeMap && modelAttributeMap[key] && modelAttributeMap[key].autoIncrement === true) {
identityWrapperRequired = true;
}
values.push(this.escape(value, (modelAttributeMap && modelAttributeMap[key]) || undefined, { context: 'INSERT' }));
}
}
}
var replacements = {
ignore: options.ignore ? this._dialect.supports.IGNORE : '',
table: this.quoteTable(table),
attributes: fields.join(','),
output: outputFragment,
values: values.join(','),
tmpTable: tmpTable
};
query = (replacements.attributes.length ? valueQuery : emptyQuery) + ';';
if (identityWrapperRequired && this._dialect.supports.autoIncrement.identityInsert) {
query = [
'SET IDENTITY_INSERT', this.quoteTable(table), 'ON;',
query,
'SET IDENTITY_INSERT', this.quoteTable(table), 'OFF;',
].join(' ');
}
return Utils._.template(query)(replacements);
},
/*
Returns an insert into command for multiple values.
Parameters: table name + list of hashes of attribute-value-pairs.
*/
bulkInsertQuery: function(tableName, attrValueHashes, options, rawAttributes) {
options = options || {};
rawAttributes = rawAttributes || {};
var query = 'INSERT<%= ignoreDuplicates %> INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %><%= onDuplicateKeyUpdate %><%= returning %>;'
, tuples = []
, serials = []
, allAttributes = []
, onDuplicateKeyUpdate = '';
attrValueHashes.forEach(function(attrValueHash) {
_.forOwn(attrValueHash, function(value, key) {
if (allAttributes.indexOf(key) === -1) {
allAttributes.push(key);
}
if (rawAttributes[key] && rawAttributes[key].autoIncrement === true) {
serials.push(key);
}
});
});
attrValueHashes.forEach(function(attrValueHash) {
tuples.push('(' +
allAttributes.map(function(key) {
if (this._dialect.supports.bulkDefault && serials.indexOf(key) !== -1) {
return attrValueHash[key] || 'DEFAULT';
}
return this.escape(attrValueHash[key], rawAttributes[key], { context: 'INSERT' });
}, this).join(',') +
')');
}, this);
if (this._dialect.supports.updateOnDuplicate && options.updateOnDuplicate) {
onDuplicateKeyUpdate += ' ON DUPLICATE KEY UPDATE ' + options.updateOnDuplicate.map(function(attr) {
var field = rawAttributes && rawAttributes[attr] && rawAttributes[attr].field || attr;
var key = this.quoteIdentifier(field);
return key + '=VALUES(' + key + ')';
}, this).join(',');
}
var replacements = {
ignoreDuplicates: options.ignoreDuplicates ? this._dialect.supports.ignoreDuplicates : '',
table: this.quoteTable(tableName),
attributes: allAttributes.map(function(attr) {
return this.quoteIdentifier(attr);
}, this).join(','),
tuples: tuples.join(','),
onDuplicateKeyUpdate: onDuplicateKeyUpdate,
returning: this._dialect.supports.returnValues && options.returning ? ' RETURNING *' : ''
};
return _.template(query)(replacements);
},
/*
Returns an update query.
Parameters:
- tableName -> Name of the table
- values -> A hash with attribute-value-pairs
- where -> A hash with conditions (e.g. {name: 'foo'})
OR an ID as integer
OR a string with conditions (e.g. 'name="foo"').
If you use a string, you have to escape it on your own.
*/
updateQuery: function(tableName, attrValueHash, where, options, attributes) {
options = options || {};
_.defaults(options, this.options);
attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, options.omitNull, options);
var query
, values = []
, outputFragment
, modelAttributeMap = {}
, tmpTable = '' //tmpTable declaration for trigger
, selectFromTmp = '' //Select statement for trigger
, tmpColumns = '' //Columns for temp table for trigger
, outputColumns = '' //Columns to capture into temp table for trigger
, attribute //Model attribute holder
, modelKey; //key for model
query = '<%= tmpTable %>UPDATE <%= table %> SET <%= values %><%= output %> <%= where %>';
if (this._dialect.supports['LIMIT ON UPDATE'] && options.limit) {
query += ' LIMIT ' + this.escape(options.limit) + ' ';
}
if (this._dialect.supports.returnValues) {
if (!!this._dialect.supports.returnValues.output) {
// we always need this for mssql
outputFragment = ' OUTPUT INSERTED.*';
//To capture output rows when there is a trigger on MSSQL DB
if (attributes && options.hasTrigger && this._dialect.supports.tmpTableTrigger) {
tmpTable = 'declare @tmp table (<%= columns %>); ';
for (modelKey in attributes){
attribute = attributes[modelKey];
if(!(attribute.type instanceof DataTypes.VIRTUAL)){
if (tmpColumns.length > 0){
tmpColumns += ',';
outputColumns += ',';
}
tmpColumns += this.quoteIdentifier(attribute.field) + ' ' + attribute.type.toSql();
outputColumns += 'INSERTED.' + this.quoteIdentifier(attribute.field);
}
}
var replacement ={
columns : tmpColumns
};
tmpTable = Utils._.template(tmpTable)(replacement).trim();
outputFragment = ' OUTPUT ' + outputColumns + ' into @tmp';
selectFromTmp = ';select * from @tmp';
query += selectFromTmp;
}
} else if (this._dialect.supports.returnValues && options.returning) {
// ensure that the return output is properly mapped to model fields.
options.mapToModel = true;
query += ' RETURNING *';
}
}
if (attributes) {
Utils._.each(attributes, function(attribute, key) {
modelAttributeMap[key] = attribute;
if (attribute.field) {
modelAttributeMap[attribute.field] = attribute;
}
});
}
for (var key in attrValueHash) {
if (modelAttributeMap && modelAttributeMap[key] &&
modelAttributeMap[key].autoIncrement === true &&
!this._dialect.supports.autoIncrement.update) {
// not allowed to update identity column
continue;
}
var value = attrValueHash[key];
values.push(this.quoteIdentifier(key) + '=' + this.escape(value, (modelAttributeMap && modelAttributeMap[key] || undefined), { context: 'UPDATE' }));
}
var replacements = {
table: this.quoteTable(tableName),
values: values.join(','),
output: outputFragment,
where: this.whereQuery(where),
tmpTable: tmpTable
};
if (values.length === 0) {
return '';
}
return Utils._.template(query)(replacements).trim();
},
/*
Returns an upsert query.
*/
upsertQuery: function (tableName, insertValues, updateValues, where, rawAttributes, options) {
throwMethodUndefined('upsertQuery');
},
/*
Returns a deletion query.
Parameters:
- tableName -> Name of the table
- where -> A hash with conditions (e.g. {name: 'foo'})
OR an ID as integer
OR a string with conditions (e.g. 'name="foo"').
If you use a string, you have to escape it on your own.
Options:
- limit -> Maximaum count of lines to delete
- truncate -> boolean - whether to use an 'optimized' mechanism (i.e. TRUNCATE) if available,
note that this should not be the default behaviour because TRUNCATE does not
always play nicely (e.g. InnoDB tables with FK constraints)
(@see http://dev.mysql.com/doc/refman/5.6/en/truncate-table.html).
Note that truncate must ignore limit and where
*/
/* istanbul ignore next */
deleteQuery: function(tableName, where, options) {
throwMethodUndefined('deleteQuery');
},
/*
Returns an update query.
Parameters:
- tableName -> Name of the table
- values -> A hash with attribute-value-pairs
- where -> A hash with conditions (e.g. {name: 'foo'})
OR an ID as integer
OR a string with conditions (e.g. 'name="foo"').
If you use a string, you have to escape it on your own.
*/
incrementQuery: function(tableName, attrValueHash, where, options) {
attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull);
var query
, key
, value
, values = []
, outputFragment;
query = 'UPDATE <%= table %> SET <%= values %><%= output %> <%= where %>';
if (this._dialect.supports.returnValues) {
if (!!this._dialect.supports.returnValues.returning) {
query += ' RETURNING *';
} else if (!!this._dialect.supports.returnValues.output) {
outputFragment = ' OUTPUT INSERTED.*';
}
}
for (key in attrValueHash) {
value = attrValueHash[key];
values.push(this.quoteIdentifier(key) + '=' + this.quoteIdentifier(key) + ' + ' + this.escape(value));
}
options = options || {};
for (key in options) {
value = options[key];
values.push(this.quoteIdentifier(key) + '=' + this.escape(value));
}
var replacements = {
table: this.quoteTable(tableName),
values: values.join(','),
output: outputFragment,
where: this.whereQuery(where)
};
return Utils._.template(query)(replacements);
},
nameIndexes: function (indexes, rawTablename) {
return Utils._.map(indexes, function (index) {
if (!index.hasOwnProperty('name')) {
var onlyAttributeNames = index.fields.map(function(field) {
return (typeof field === 'string') ? field : (field.name || field.attribute);
}.bind(this));
index.name = Utils.inflection.underscore(rawTablename + '_' + onlyAttributeNames.join('_'));
}
return index;
});
},
/*
Returns an add index query.
Parameters:
- tableName -> Name of an existing table, possibly with schema.
- options:
- type: UNIQUE|FULLTEXT|SPATIAL
- name: The name of the index. Default is <table>_<attr1>_<attr2>
- fields: An array of attributes as string or as hash.
If the attribute is a hash, it must have the following content:
- name: The name of the attribute/column
- length: An integer. Optional
- order: 'ASC' or 'DESC'. Optional
- parser
- rawTablename, the name of the table, without schema. Used to create the name of the index
*/
addIndexQuery: function(tableName, attributes, options, rawTablename) {
var fieldsSql;
options = options || {};
if (!Array.isArray(attributes)) {
options = attributes;
attributes = undefined;
} else {
options.fields = attributes;
}
// Backwards compatability
if (options.indexName) {
options.name = options.indexName;
}
if (options.indicesType) {
options.type = options.indicesType;
}
if (options.indexType || options.method) {
options.using = options.indexType || options.method;
}
options.prefix = options.prefix || rawTablename || tableName;
if (options.prefix && _.isString(options.prefix)) {
options.prefix = options.prefix.replace(/\./g, '_');
options.prefix = options.prefix.replace(/(\"|\')/g, '');
}
fieldsSql = options.fields.map(function(field) {
if (typeof field === 'string') {
return this.quoteIdentifier(field);
} else if (field._isSequelizeMethod) {
return this.handleSequelizeMethod(field);
} else {
var result = '';
if (field.attribute) {
field.name = field.attribute;
}
if (!field.name) {
throw new Error('The following index field has no name: ' + util.inspect(field));
}
result += this.quoteIdentifier(field.name);
if (this._dialect.supports.index.collate && field.collate) {
result += ' COLLATE ' + this.quoteIdentifier(field.collate);
}
if (this._dialect.supports.index.length && field.length) {
result += '(' + field.length + ')';
}
if (field.order) {
result += ' ' + field.order;
}
return result;
}
}.bind(this));
if (!options.name) {
// Mostly for cases where addIndex is called directly by the user without an options object (for example in migrations)
// All calls that go through sequelize should already have a name
options = this.nameIndexes([options], options.prefix)[0];
}
options = Model.prototype.$conformIndex(options);
if (!this._dialect.supports.index.type) {
delete options.type;
}
if (options.where) {
options.where = this.whereQuery(options.where);
}
if (_.isString(tableName)) {
tableName = this.quoteIdentifiers(tableName);
} else {
tableName = this.quoteTable(tableName);
}
var concurrently = this._dialect.supports.index.concurrently && options.concurrently ? 'CONCURRENTLY' : undefined
, ind;
if (this._dialect.supports.indexViaAlter) {
ind = [
'ALTER TABLE',
tableName,
concurrently,
'ADD'
];
} else {
ind = ['CREATE'];
}
ind = ind.concat(
options.unique ? 'UNIQUE' : '',
options.type, 'INDEX',
!this._dialect.supports.indexViaAlter ? concurrently : undefined,
this.quoteIdentifiers(options.name),
this._dialect.supports.index.using === 1 && options.using ? 'USING ' + options.using : '',
!this._dialect.supports.indexViaAlter ? 'ON ' + tableName : undefined,
this._dialect.supports.index.using === 2 && options.using ? 'USING ' + options.using : '',
'(' + fieldsSql.join(', ') + (options.operator ? ' '+options.operator : '') + ')',
(this._dialect.supports.index.parser && options.parser ? 'WITH PARSER ' + options.parser : undefined),
(this._dialect.supports.index.where && options.where ? options.where : undefined)
);
return Utils._.compact(ind).join(' ');
},
/*
Returns a query listing indexes for a given table.
Parameters:
- tableName: Name of an existing table.
- options:
- database: Name of the database.
*/
/* istanbul ignore next */
showIndexesQuery: function(tableName, options) {
throwMethodUndefined('showIndexesQuery');
},
/*
Returns a remove index query.
Parameters:
- tableName: Name of an existing table.
- indexNameOrAttributes: The name of the index as string or an array of attribute names.
*/
/* istanbul ignore next */
removeIndexQuery: function(tableName, indexNameOrAttributes) {
throwMethodUndefined('removeIndexQuery');
},
/*
This method transforms an array of attribute hashes into equivalent
sql attribute definition.
*/
/* istanbul ignore next */
attributesToSQL: function(attributes) {
throwMethodUndefined('attributesToSQL');
},
/*
Returns all auto increment fields of a factory.
*/
/* istanbul ignore next */
findAutoIncrementField: function(factory) {
throwMethodUndefined('findAutoIncrementField');
},
quoteTable: function(param, as) {
var table = '';
if (as === true) {
as = param.as || param.name || param;
}
if (_.isObject(param)) {
if (this._dialect.supports.schemas) {
if (param.schema) {
table += this.quoteIdentifier(param.schema) + '.';
}
table += this.quoteIdentifier(param.tableName);
} else {
if (param.schema) {
table += param.schema + (param.delimiter || '.');
}
table += param.tableName;
table = this.quoteIdentifier(table);
}
} else {
table = this.quoteIdentifier(param);
}
if (as) {
table += ' AS ' + this.quoteIdentifier(as);
}
return table;
},
/*
Quote an object based on its type. This is a more general version of quoteIdentifiers
Strings: should proxy to quoteIdentifiers
Arrays:
* Expects array in the form: [<model> (optional), <model> (optional),... String, String (optional)]
Each <model> can be a model or an object {model: Model, as: String}, matching include
* Zero or more models can be included in the array and are used to trace a path through the tree of
included nested associations. This produces the correct table name for the ORDER BY/GROUP BY SQL
and quotes it.
* If a single string is appended to end of array, it is quoted.
If two strings appended, the 1st string is quoted, the 2nd string unquoted.
Objects:
* If raw is set, that value should be returned verbatim, without quoting
* If fn is set, the string should start with the value of fn, starting paren, followed by
the values of cols (which is assumed to be an array), quoted and joined with ', ',
unless they are themselves objects
* If direction is set, should be prepended
Currently this function is only used for ordering / grouping columns and Sequelize.col(), but it could
potentially also be used for other places where we want to be able to call SQL functions (e.g. as default values)
*/
quote: function(obj, parent, force) {
if (Utils._.isString(obj)) {
return this.quoteIdentifiers(obj, force);
} else if (Array.isArray(obj)) {
// loop through array, adding table names of models to quoted
// (checking associations to see if names should be singularised or not)
var tableNames = []
, parentAssociation
, len = obj.length
, item
, model
, as
, association;
for (var i = 0; i < len - 1; i++) {
item = obj[i];
if (item._modelAttribute || Utils._.isString(item) || item._isSequelizeMethod || 'raw' in item) {
break;
}
if (item instanceof Model) {
model = item;
as = undefined;
} else {
model = item.model;
as = item.as;
}
// check if model provided is through table
if (!as && parentAssociation && parentAssociation.through && parentAssociation.through.model === model) {
association = {as: model.name};
} else {
// find applicable association for linking parent to this model
association = parent.getAssociation(model, as);
}
if (association) {
tableNames[i] = association.as;
parent = model;
parentAssociation = association;
} else {
tableNames[i] = model.tableName;
throw new Error('\'' + tableNames.join('.') + '\' in order / group clause is not valid association');
}
}
// add 1st string as quoted, 2nd as unquoted raw
var sql = (i > 0 ? this.quoteIdentifier(tableNames.join('.')) + '.' : (Utils._.isString(obj[0]) && parent ? this.quoteIdentifier(parent.name) + '.' : '')) + this.quote(obj[i], parent, force);
if (i < len - 1) {
if (obj[i + 1]._isSequelizeMethod) {
sql += this.handleSequelizeMethod(obj[i + 1]);
} else {
sql += ' ' + obj[i + 1];
}
}
return sql;
} else if (obj._modelAttribute) {
return this.quoteTable(obj.Model.name) + '.' + obj.fieldName;
} else if (obj._isSequelizeMethod) {
return this.handleSequelizeMethod(obj);
} else if (Utils._.isObject(obj) && 'raw' in obj) {
return obj.raw;
} else {
throw new Error('Unknown structure passed to order / group: ' + JSON.stringify(obj));
}
},
/*
Create a trigger
*/
/* istanbul ignore next */
createTrigger: function(tableName, triggerName, timingType, fireOnArray, functionName, functionParams, optionsArray) {
throwMethodUndefined('createTrigger');
},
/*
Drop a trigger
*/
/* istanbul ignore next */
dropTrigger: function(tableName, triggerName) {
throwMethodUndefined('dropTrigger');
},
/*
Rename a trigger
*/
/* istanbul ignore next */
renameTrigger: function(tableName, oldTriggerName, newTriggerName) {
throwMethodUndefined('renameTrigger');
},
/*
Create a function
*/
/* istanbul ignore next */
createFunction: function(functionName, params, returnType, language, body, options) {
throwMethodUndefined('createFunction');
},
/*
Drop a function
*/
/* istanbul ignore next */
dropFunction: function(functionName, params) {
throwMethodUndefined('dropFunction');
},
/*
Rename a function
*/
/* istanbul ignore next */
renameFunction: function(oldFunctionName, params, newFunctionName) {
throwMethodUndefined('renameFunction');
},
/*
Escape an identifier (e.g. a table or attribute name)
*/
/* istanbul ignore next */
quoteIdentifier: function(identifier, force) {
throwMethodUndefined('quoteIdentifier');
},
/*
Split an identifier into .-separated tokens and quote each part
*/
quoteIdentifiers: function(identifiers, force) {
if (identifiers.indexOf('.') !== -1) {
identifiers = identifiers.split('.');
return this.quoteIdentifier(identifiers.slice(0, identifiers.length - 1).join('.')) + '.' + this.quoteIdentifier(identifiers[identifiers.length - 1]);
} else {
return this.quoteIdentifier(identifiers);
}
},
/*
Escape a value (e.g. a string, number or date)
*/
escape: function(value, field, options) {
options = options || {};
if (value !== null && value !== undefined) {
if (value._isSequelizeMethod) {
return this.handleSequelizeMethod(value);
} else {
if (field && field.type) {
if (this.typeValidation && field.type.validate && value) {
if (options.isList && Array.isArray(value)) {
_.forEach(value, function(item) {
field.type.validate(item, options);
});
} else {
field.type.validate(value, options);
}
}
if (field.type.stringify) {
// Users shouldn't have to worry about these args - just give them a function that takes a single arg
var simpleEscape = _.partialRight(SqlString.escape, this.options.timezone, this.dialect);
value = field.type.stringify(value, { escape: simpleEscape, field: field, timezone: this.options.timezone });
if (field.type.escape === false) {
// The data-type already did the required escaping
return value;
}
}
}
}
}
return SqlString.escape(value, this.options.timezone, this.dialect);
},
/**
* Generates an SQL query that returns all foreign keys of a table.
*
* @param {String} tableName The name of the table.
* @param {String} schemaName The name of the schema.
* @return {String} The generated sql query.
*/
/* istanbul ignore next */
getForeignKeysQuery: function(tableName, schemaName) {
throwMethodUndefined('getForeignKeysQuery');
},
/**
* Generates an SQL query that removes a foreign key from a table.
*
* @param {String} tableName The name of the table.
* @param {String} foreignKey The name of the foreign key constraint.
* @return {String} The generated sql query.
*/
/* istanbul ignore next */
dropForeignKeyQuery: function(tableName, foreignKey) {
throwMethodUndefined('dropForeignKeyQuery');
},
/*
Returns a query for selecting elements in the table <tableName>.
Options:
- attributes -> An array of attributes (e.g. ['name', 'birthday']). Default: *
- where -> A hash with conditions (e.g. {name: 'foo'})
OR an ID as integer
OR a string with conditions (e.g. 'name="foo"').
If you use a string, you have to escape it on your own.
- order -> e.g. 'id DESC'
- group
- limit -> The maximum count you want to get.
- offset -> An offset value to start from. Only useable with limit!
*/
selectQuery: function(tableName, options, model) {
// Enter and change at your own peril -- Mick Hansen
options = options || {};
var table = null
, self = this
, query
, limit = options.limit
, mainModel = model
, mainQueryItems = []
, mainAttributes = options.attributes && options.attributes.slice()
, mainJoinQueries = []
// We'll use a subquery if we have a hasMany association and a limit
, subQuery = options.subQuery === undefined ?
limit && options.hasMultiAssociation :
options.subQuery
, subQueryItems = []
, subQueryAttributes = null
, subJoinQueries = []
, mainTableAs = null;
if (options.tableAs) {
mainTableAs = this.quoteTable(options.tableAs);
} else if (!Array.isArray(tableName) && model) {
mainTableAs = this.quoteTable(model.name);
}
table = !Array.isArray(tableName) ? this.quoteTable(tableName) : tableName.map(function(t) {
if (Array.isArray(t)) {
return this.quoteTable(t[0], t[1]);
}
return this.quoteTable(t, true);
}.bind(this)).join(', ');
if (subQuery && mainAttributes) {
model.primaryKeyAttributes.forEach(function(keyAtt) {
// Check if mainAttributes contain the primary key of the model either as a field or an aliased field
if (!_.find(mainAttributes, function (attr) {
return keyAtt === attr || keyAtt === attr[0] || keyAtt === attr[1];
})) {
mainAttributes.push(model.rawAttributes[keyAtt].field ? [keyAtt, model.rawAttributes[keyAtt].field] : keyAtt);
}
});
}
// Escape attributes
mainAttributes = mainAttributes && mainAttributes.map(function(attr) {
var addTable = true;
if (attr._isSequelizeMethod) {
return self.handleSequelizeMethod(attr);
}
if (Array.isArray(attr) && attr.length === 2) {
attr = attr.slice();
if (attr[0]._isSequelizeMethod) {
attr[0] = self.handleSequelizeMethod(attr[0]);
addTable = false;
} else if (attr[0].indexOf('(') === -1 && attr[0].indexOf(')') === -1) {
attr[0] = self.quoteIdentifier(attr[0]);
}
attr = [attr[0], self.quoteIdentifier(attr[1])].join(' AS ');
} else {
attr = attr.indexOf(Utils.TICK_CHAR) < 0 && attr.indexOf('"') < 0 ? self.quoteIdentifiers(attr) : attr;
}
if (options.include && attr.indexOf('.') === -1 && addTable) {
attr = mainTableAs + '.' + attr;
}
return attr;
});
// If no attributes specified, use *
mainAttributes = mainAttributes || (options.include ? [mainTableAs + '.*'] : ['*']);
// If subquery, we ad the mainAttributes to the subQuery and set the mainAttributes to select * from subquery
if (subQuery || options.groupedLimit) {
// We need primary keys
subQueryAttributes = mainAttributes;
mainAttributes = [(mainTableAs || table) + '.*'];
}
if (options.include) {
var generateJoinQueries = function(include, parentTable) {
var table = include.model.getTableName()
, as = include.as
, joinQueryItem = ''
, joinQueries = {
mainQuery: [],
subQuery: []
}
, attributes
, association = include.association
, through = include.through
, joinType = include.required ? ' INNER JOIN ' : ' LEFT OUTER JOIN '
, parentIsTop = !include.parent.association && include.parent.model.name === options.model.name
, whereOptions = Utils._.clone(options)
, targetWhere;
whereOptions.keysEscaped = true;
if (tableName !== parentTable && mainTableAs !== parentTable) {
as = parentTable + '.' + include.as;
}
// includeIgnoreAttributes is used by aggregate functions
if (options.includeIgnoreAttributes !== false) {
attributes = include.attributes.map(function(attr) {
var attrAs = attr,
verbatim = false;
if (Array.isArray(attr) && attr.length === 2) {
if (attr[0]._isSequelizeMethod) {
if (attr[0] instanceof Utils.literal ||
attr[0] instanceof Utils.cast ||
attr[0] instanceof Utils.fn
) {
verbatim = true;
}
}
attr = attr.map(function($attr) {
return $attr._isSequelizeMethod ? self.handleSequelizeMethod($attr) : $attr;
});
attrAs = attr[1];
attr = attr[0];
} else if (attr instanceof Utils.literal) {
return attr.val; // We trust the user to rename the field correctly
} else if (attr instanceof Utils.cast ||
attr instanceof Utils.fn
) {
throw new Error(
'Tried to select attributes using Sequelize.cast or Sequelize.fn without specifying an alias for the result, during eager loading. ' +
'This means the attribute will not be added to the returned instance'
);
}
var prefix;
if (verbatim === true) {
prefix = attr;
} else {
prefix = self.quoteIdentifier(as) + '.' + self.quoteIdentifier(attr);
}
return prefix + ' AS ' + self.quoteIdentifier(as + '.' + attrAs, true);
});
if (include.subQuery && subQuery) {
subQueryAttributes = subQueryAttributes.concat(attributes);
} else {
mainAttributes = mainAttributes.concat(attributes);
}
}
if (through) {
var throughTable = through.model.getTableName()
, throughAs = as + '.' + through.as
, throughAttributes = through.attributes.map(function(attr) {
return self.quoteIdentifier(throughAs) + '.' + self.quoteIdentifier(Array.isArray(attr) ? attr[0] : attr) +
' AS ' +
self.quoteIdentifier(throughAs + '.' + (Array.isArray(attr) ? attr[1] : attr));
})
, primaryKeysSource = association.source.primaryKeyAttributes
, tableSource = parentTable
, identSource = association.identifierField
, attrSource = primaryKeysSource[0]
, primaryKeysTarget = association.target.primaryKeyAttributes
, tableTarget = as
, identTarget = association.foreignIdentifierField
, attrTarget = association.target.rawAttributes[primaryKeysTarget[0]].field || primaryKeysTarget[0]
, sourceJoinOn
, targetJoinOn
, throughWhere;
if (options.includeIgnoreAttributes !== false) {
// Through includes are always hasMany, so we need to add the attributes to the mainAttributes no matter what (Real join will never be executed in subquery)
mainAttributes = mainAttributes.concat(throughAttributes);
}
// Figure out if we need to use field or attribute
if (!subQuery) {
attrSource = association.source.rawAttributes[primaryKeysSource[0]].field;
}
if (subQuery && !include.subQuery && !include.parent.subQuery && include.parent.model !== mainModel) {
attrSource = association.source.rawAttributes[primaryKeysSource[0]].field;
}
// Filter statement for left side of through
// Used by both join and subquery where
// If parent include was in a subquery need to join on the aliased attribute
if (subQuery && !include.subQuery && include.parent.subQuery && !parentIsTop) {
sourceJoinOn = self.quoteIdentifier(tableSource + '.' + attrSource) + ' = ';
} else {
sourceJoinOn = self.quoteTable(tableSource) + '.' + self.quoteIdentifier(attrSource) + ' = ';
}
sourceJoinOn += self.quoteIdentifier(throughAs) + '.' + self.quoteIdentifier(identSource);
// Filter statement for right side of through
// Used by both join and subquery where
targetJoinOn = self.quoteIdentifier(tableTarget) + '.' + self.quoteIdentifier(attrTarget) + ' = ';
targetJoinOn += self.quoteIdentifier(throughAs) + '.' + self.quoteIdentifier(identTarget);
if (include.through.where) {
throughWhere = self.getWhereConditions(include.through.where, self.sequelize.literal(self.quoteIdentifier(throughAs)), include.through.model);
}
if (self._dialect.supports.joinTableDependent) {
// Generate a wrapped join so that the through table join can be dependent on the target join
joinQueryItem += joinType + '(';
joinQueryItem += self.quoteTable(throughTable, throughAs);
joinQueryItem += ' INNER JOIN ' + self.quoteTable(table, as) + ' ON ';
joinQueryItem += targetJoinOn;
if (throughWhere) {
joinQueryItem += ' AND ' + throughWhere;
}
joinQueryItem += ') ON '+sourceJoinOn;
} else {
// Generate join SQL for left side of through
joinQueryItem += joinType + self.quoteTable(throughTable, throughAs) + ' ON ';
joinQueryItem += sourceJoinOn;
// Generate join SQL for right side of through
joinQueryItem += joinType + self.quoteTable(table, as) + ' ON ';
joinQueryItem += targetJoinOn;
if (throughWhere) {
joinQueryItem += ' AND ' + throughWhere;
}
}
if (include.where || include.through.where) {
if (include.where) {
targetWhere = self.getWhereConditions(include.where, self.sequelize.literal(self.quoteIdentifier(as)), include.model, whereOptions);
if (targetWhere) {
joinQueryItem += ' AND ' + targetWhere;
}
}
if (subQuery && include.required) {
if (!options.where) options.where = {};
(function (include) {
// Closure to use sane local variables
var parent = include
, child = include
, nestedIncludes = []
, topParent
, topInclude
, $query;
while (parent = parent.parent) {
nestedIncludes = [_.extend({}, child, {include: nestedIncludes})];
child = parent;
}
topInclude = nestedIncludes[0];
topParent = topInclude.parent;
if (topInclude.through && Object(topInclude.through.model) === topInclude.through.model) {
$query = self.selectQuery(topInclude.through.model.getTableName(), {
attributes: [topInclude.through.model.primaryKeyField],
include: Model.$validateIncludedElements({
model: topInclude.through.model,
include: [{
association: topInclude.association.toTarget,
required: true
}]
}).include,
model: topInclude.through.model,
where: { $and: [
self.sequelize.asIs([
self.quoteTable(topParent.model.name) + '.' + self.quoteIdentifier(topParent.model.primaryKeyField),
self.quoteIdentifier(topInclude.through.model.name) + '.' + self.quoteIdentifier(topInclude.association.identifierField)
].join(' = ')),
topInclude.through.where
]},
limit: 1,
includeIgnoreAttributes: false
}, topInclude.through.model);
} else {
var isBelongsTo = topInclude.association.associationType === 'BelongsTo';
var join = [
self.quoteTable(topParent.model.name) + '.' + self.quoteIdentifier(isBelongsTo ? topInclude.association.identifierField : topParent.model.primaryKeyAttributes[0]),
self.quoteIdentifier(topInclude.model.name) + '.' + self.quoteIdentifier(isBelongsTo ? topInclude.model.primaryKeyAttributes[0] : topInclude.association.identifierField)
].join(' = ');
$query = self.selectQuery(topInclude.model.tableName, {
attributes: [topInclude.model.primaryKeyAttributes[0]],
include: topInclude.include,
where: {
$join: self.sequelize.asIs(join)
},
limit: 1,
includeIgnoreAttributes: false
}, topInclude.model);
}
options.where['__' + throughAs] = self.sequelize.asIs([
'(',
$query.replace(/\;$/, ''),
')',
'IS NOT NULL'
].join(' '));
})(include);
}
}
} else {
if (subQuery && include.subQueryFilter) {
var associationWhere = {}
, $query
, subQueryWhere;
associationWhere[association.identifierField] = {
$raw: self.quoteTable(parentTable) + '.' + self.quoteIdentifier(association.sourceKeyField || association.source.primaryKeyField)
};
if (!options.where) options.where = {};
// Creating the as-is where for the subQuery, checks that the required association exists
$query = self.selectQuery(include.model.getTableName(), {
attributes: [association.identifierField],
where: {
$and: [
associationWhere,
include.where || {}
]
},
limit: 1
}, include.model);
subQueryWhere = self.sequelize.asIs([
'(',
$query.replace(/\;$/, ''),
')',
'IS NOT NULL'
].join(' '));
if (Utils._.isPlainObject(options.where)) {
options.where['__' + as] = subQueryWhere;
} else {
options.where = { $and: [options.where, subQueryWhere] };
}
}
joinQueryItem = ' ' + self.joinIncludeQuery({
model: mainModel,
subQuery: options.subQuery,
include: include,
groupedLimit: options.groupedLimit
});
}
if (include.subQuery && subQuery) {
joi