sequelize
Version:
Sequelize is a promise-based Node.js ORM tool for Postgres, MySQL, MariaDB, SQLite, Microsoft SQL Server, Amazon Redshift and Snowflake’s Data Cloud. It features solid transaction support, relations, eager and lazy loading, read replication and more.
742 lines (666 loc) • 23.1 kB
JavaScript
;
const _ = require('lodash');
const SqlString = require('../../sql-string');
const QueryTypes = require('../../query-types');
const Dot = require('dottie');
const deprecations = require('../../utils/deprecations');
const uuid = require('uuid').v4;
class AbstractQuery {
constructor(connection, sequelize, options) {
this.uuid = uuid();
this.connection = connection;
this.instance = options.instance;
this.model = options.model;
this.sequelize = sequelize;
this.options = {
plain: false,
raw: false,
// eslint-disable-next-line no-console
logging: console.log,
...options
};
this.checkLoggingOption();
}
/**
* rewrite query with parameters
*
* Examples:
*
* query.formatBindParameters('select $1 as foo', ['fooval']);
*
* query.formatBindParameters('select $foo as foo', { foo: 'fooval' });
*
* Options
* skipUnescape: bool, skip unescaping $$
* skipValueReplace: bool, do not replace (but do unescape $$). Check correct syntax and if all values are available
*
* @param {string} sql
* @param {object|Array} values
* @param {string} dialect
* @param {Function} [replacementFunc]
* @param {object} [options]
* @private
*/
static formatBindParameters(sql, values, dialect, replacementFunc, options) {
if (!values) {
return [sql, []];
}
options = options || {};
if (typeof replacementFunc !== 'function') {
options = replacementFunc || {};
replacementFunc = undefined;
}
if (!replacementFunc) {
if (options.skipValueReplace) {
replacementFunc = (match, key, values) => {
if (values[key] !== undefined) {
return match;
}
return undefined;
};
} else {
replacementFunc = (match, key, values, timeZone, dialect) => {
if (values[key] !== undefined) {
return SqlString.escape(values[key], timeZone, dialect);
}
return undefined;
};
}
} else if (options.skipValueReplace) {
const origReplacementFunc = replacementFunc;
replacementFunc = (match, key, values, timeZone, dialect, options) => {
if (origReplacementFunc(match, key, values, timeZone, dialect, options) !== undefined) {
return match;
}
return undefined;
};
}
const timeZone = null;
const list = Array.isArray(values);
sql = sql.replace(/\B\$(\$|\w+)/g, (match, key) => {
if ('$' === key) {
return options.skipUnescape ? match : key;
}
let replVal;
if (list) {
if (key.match(/^[1-9]\d*$/)) {
key = key - 1;
replVal = replacementFunc(match, key, values, timeZone, dialect, options);
}
} else if (!key.match(/^\d*$/)) {
replVal = replacementFunc(match, key, values, timeZone, dialect, options);
}
if (replVal === undefined) {
throw new Error(`Named bind parameter "${match}" has no value in the given object.`);
}
return replVal;
});
return [sql, []];
}
/**
* Execute the passed sql query.
*
* Examples:
*
* query.run('SELECT 1')
*
* @private
*/
run() {
throw new Error('The run method wasn\'t overwritten!');
}
/**
* Check the logging option of the instance and print deprecation warnings.
*
* @private
*/
checkLoggingOption() {
if (this.options.logging === true) {
deprecations.noTrueLogging();
// eslint-disable-next-line no-console
this.options.logging = console.log;
}
}
/**
* Get the attributes of an insert query, which contains the just inserted id.
*
* @returns {string} The field name.
* @private
*/
getInsertIdField() {
return 'insertId';
}
getUniqueConstraintErrorMessage(field) {
let message = field ? `${field} must be unique` : 'Must be unique';
if (field && this.model) {
for (const key of Object.keys(this.model.uniqueKeys)) {
if (this.model.uniqueKeys[key].fields.includes(field.replace(/"/g, ''))) {
if (this.model.uniqueKeys[key].msg) {
message = this.model.uniqueKeys[key].msg;
}
}
}
}
return message;
}
isRawQuery() {
return this.options.type === QueryTypes.RAW;
}
isVersionQuery() {
return this.options.type === QueryTypes.VERSION;
}
isUpsertQuery() {
return this.options.type === QueryTypes.UPSERT;
}
isInsertQuery(results, metaData) {
let result = true;
if (this.options.type === QueryTypes.INSERT) {
return true;
}
// is insert query if sql contains insert into
result = result && this.sql.toLowerCase().startsWith('insert into');
// is insert query if no results are passed or if the result has the inserted id
result = result && (!results || Object.prototype.hasOwnProperty.call(results, this.getInsertIdField()));
// is insert query if no metadata are passed or if the metadata has the inserted id
result = result && (!metaData || Object.prototype.hasOwnProperty.call(metaData, this.getInsertIdField()));
return result;
}
handleInsertQuery(results, metaData) {
if (this.instance) {
// add the inserted row id to the instance
const autoIncrementAttribute = this.model.autoIncrementAttribute;
let id = null;
id = id || results && results[this.getInsertIdField()];
id = id || metaData && metaData[this.getInsertIdField()];
this.instance[autoIncrementAttribute] = id;
}
}
isShowTablesQuery() {
return this.options.type === QueryTypes.SHOWTABLES;
}
handleShowTablesQuery(results) {
return _.flatten(results.map(resultSet => Object.values(resultSet)));
}
isShowIndexesQuery() {
return this.options.type === QueryTypes.SHOWINDEXES;
}
isShowConstraintsQuery() {
return this.options.type === QueryTypes.SHOWCONSTRAINTS;
}
isDescribeQuery() {
return this.options.type === QueryTypes.DESCRIBE;
}
isSelectQuery() {
return this.options.type === QueryTypes.SELECT;
}
isBulkUpdateQuery() {
return this.options.type === QueryTypes.BULKUPDATE;
}
isBulkDeleteQuery() {
return this.options.type === QueryTypes.BULKDELETE;
}
isForeignKeysQuery() {
return this.options.type === QueryTypes.FOREIGNKEYS;
}
isUpdateQuery() {
return this.options.type === QueryTypes.UPDATE;
}
handleSelectQuery(results) {
let result = null;
// Map raw fields to names if a mapping is provided
if (this.options.fieldMap) {
const fieldMap = this.options.fieldMap;
results = results.map(result => _.reduce(fieldMap, (result, name, field) => {
if (result[field] !== undefined && name !== field) {
result[name] = result[field];
delete result[field];
}
return result;
}, result));
}
// Raw queries
if (this.options.raw) {
result = results.map(result => {
let o = {};
for (const key in result) {
if (Object.prototype.hasOwnProperty.call(result, key)) {
o[key] = result[key];
}
}
if (this.options.nest) {
o = Dot.transform(o);
}
return o;
});
// Queries with include
} else if (this.options.hasJoin === true) {
results = AbstractQuery._groupJoinData(results, {
model: this.model,
includeMap: this.options.includeMap,
includeNames: this.options.includeNames
}, {
checkExisting: this.options.hasMultiAssociation
});
result = this.model.bulkBuild(results, {
isNewRecord: false,
include: this.options.include,
includeNames: this.options.includeNames,
includeMap: this.options.includeMap,
includeValidated: true,
attributes: this.options.originalAttributes || this.options.attributes,
raw: true
});
// Regular queries
} else {
result = this.model.bulkBuild(results, {
isNewRecord: false,
raw: true,
attributes: this.options.originalAttributes || this.options.attributes
});
}
// return the first real model instance if options.plain is set (e.g. Model.find)
if (this.options.plain) {
result = result.length === 0 ? null : result[0];
}
return result;
}
isShowOrDescribeQuery() {
let result = false;
result = result || this.sql.toLowerCase().startsWith('show');
result = result || this.sql.toLowerCase().startsWith('describe');
return result;
}
isCallQuery() {
return this.sql.toLowerCase().startsWith('call');
}
/**
* @param {string} sql
* @param {Function} debugContext
* @param {Array|object} parameters
* @protected
* @returns {Function} A function to call after the query was completed.
*/
_logQuery(sql, debugContext, parameters) {
const { connection, options } = this;
const benchmark = this.sequelize.options.benchmark || options.benchmark;
const logQueryParameters = this.sequelize.options.logQueryParameters || options.logQueryParameters;
const startTime = Date.now();
let logParameter = '';
if (logQueryParameters && parameters) {
const delimiter = sql.endsWith(';') ? '' : ';';
let paramStr;
if (Array.isArray(parameters)) {
paramStr = parameters.map(p=>JSON.stringify(p)).join(', ');
} else {
paramStr = JSON.stringify(parameters);
}
logParameter = `${delimiter} ${paramStr}`;
}
const fmt = `(${connection.uuid || 'default'}): ${sql}${logParameter}`;
const msg = `Executing ${fmt}`;
debugContext(msg);
if (!benchmark) {
this.sequelize.log(`Executing ${fmt}`, options);
}
return () => {
const afterMsg = `Executed ${fmt}`;
debugContext(afterMsg);
if (benchmark) {
this.sequelize.log(afterMsg, Date.now() - startTime, options);
}
};
}
/**
* The function takes the result of the query execution and groups
* the associated data by the callee.
*
* Example:
* groupJoinData([
* {
* some: 'data',
* id: 1,
* association: { foo: 'bar', id: 1 }
* }, {
* some: 'data',
* id: 1,
* association: { foo: 'bar', id: 2 }
* }, {
* some: 'data',
* id: 1,
* association: { foo: 'bar', id: 3 }
* }
* ])
*
* Result:
* Something like this:
*
* [
* {
* some: 'data',
* id: 1,
* association: [
* { foo: 'bar', id: 1 },
* { foo: 'bar', id: 2 },
* { foo: 'bar', id: 3 }
* ]
* }
* ]
*
* @param {Array} rows
* @param {object} includeOptions
* @param {object} options
* @private
*/
static _groupJoinData(rows, includeOptions, options) {
/*
* Assumptions
* ID is not necessarily the first field
* All fields for a level is grouped in the same set (i.e. Panel.id, Task.id, Panel.title is not possible)
* Parent keys will be seen before any include/child keys
* Previous set won't necessarily be parent set (one parent could have two children, one child would then be previous set for the other)
*/
/*
* Author (MH) comment: This code is an unreadable mess, but it's performant.
* groupJoinData is a performance critical function so we prioritize perf over readability.
*/
if (!rows.length) {
return [];
}
// Generic looping
let i;
let length;
let $i;
let $length;
// Row specific looping
let rowsI;
let row;
const rowsLength = rows.length;
// Key specific looping
let keys;
let key;
let keyI;
let keyLength;
let prevKey;
let values;
let topValues;
let topExists;
const checkExisting = options.checkExisting;
// If we don't have to deduplicate we can pre-allocate the resulting array
let itemHash;
let parentHash;
let topHash;
const results = checkExisting ? [] : new Array(rowsLength);
const resultMap = {};
const includeMap = {};
// Result variables for the respective functions
let $keyPrefix;
let $keyPrefixString;
let $prevKeyPrefixString; // eslint-disable-line
let $prevKeyPrefix;
let $lastKeyPrefix;
let $current;
let $parent;
// Map each key to an include option
let previousPiece;
const buildIncludeMap = piece => {
if (Object.prototype.hasOwnProperty.call($current.includeMap, piece)) {
includeMap[key] = $current = $current.includeMap[piece];
if (previousPiece) {
previousPiece = `${previousPiece}.${piece}`;
} else {
previousPiece = piece;
}
includeMap[previousPiece] = $current;
}
};
// Calculate the string prefix of a key ('User.Results' for 'User.Results.id')
const keyPrefixStringMemo = {};
const keyPrefixString = (key, memo) => {
if (!Object.prototype.hasOwnProperty.call(memo, key)) {
memo[key] = key.substr(0, key.lastIndexOf('.'));
}
return memo[key];
};
// Removes the prefix from a key ('id' for 'User.Results.id')
const removeKeyPrefixMemo = {};
const removeKeyPrefix = key => {
if (!Object.prototype.hasOwnProperty.call(removeKeyPrefixMemo, key)) {
const index = key.lastIndexOf('.');
removeKeyPrefixMemo[key] = key.substr(index === -1 ? 0 : index + 1);
}
return removeKeyPrefixMemo[key];
};
// Calculates the array prefix of a key (['User', 'Results'] for 'User.Results.id')
const keyPrefixMemo = {};
const keyPrefix = key => {
// We use a double memo and keyPrefixString so that different keys with the same prefix will receive the same array instead of differnet arrays with equal values
if (!Object.prototype.hasOwnProperty.call(keyPrefixMemo, key)) {
const prefixString = keyPrefixString(key, keyPrefixStringMemo);
if (!Object.prototype.hasOwnProperty.call(keyPrefixMemo, prefixString)) {
keyPrefixMemo[prefixString] = prefixString ? prefixString.split('.') : [];
}
keyPrefixMemo[key] = keyPrefixMemo[prefixString];
}
return keyPrefixMemo[key];
};
// Calcuate the last item in the array prefix ('Results' for 'User.Results.id')
const lastKeyPrefixMemo = {};
const lastKeyPrefix = key => {
if (!Object.prototype.hasOwnProperty.call(lastKeyPrefixMemo, key)) {
const prefix = keyPrefix(key);
const length = prefix.length;
lastKeyPrefixMemo[key] = !length ? '' : prefix[length - 1];
}
return lastKeyPrefixMemo[key];
};
const getUniqueKeyAttributes = model => {
let uniqueKeyAttributes = _.chain(model.uniqueKeys);
uniqueKeyAttributes = uniqueKeyAttributes
.result(`${uniqueKeyAttributes.findKey()}.fields`)
.map(field => _.findKey(model.attributes, chr => chr.field === field))
.value();
return uniqueKeyAttributes;
};
const stringify = obj => obj instanceof Buffer ? obj.toString('hex') : obj;
let primaryKeyAttributes;
let uniqueKeyAttributes;
let prefix;
for (rowsI = 0; rowsI < rowsLength; rowsI++) {
row = rows[rowsI];
// Keys are the same for all rows, so only need to compute them on the first row
if (rowsI === 0) {
keys = Object.keys(row);
keyLength = keys.length;
}
if (checkExisting) {
topExists = false;
// Compute top level hash key (this is usually just the primary key values)
$length = includeOptions.model.primaryKeyAttributes.length;
topHash = '';
if ($length === 1) {
topHash = stringify(row[includeOptions.model.primaryKeyAttributes[0]]);
}
else if ($length > 1) {
for ($i = 0; $i < $length; $i++) {
topHash += stringify(row[includeOptions.model.primaryKeyAttributes[$i]]);
}
}
else if (!_.isEmpty(includeOptions.model.uniqueKeys)) {
uniqueKeyAttributes = getUniqueKeyAttributes(includeOptions.model);
for ($i = 0; $i < uniqueKeyAttributes.length; $i++) {
topHash += row[uniqueKeyAttributes[$i]];
}
}
}
topValues = values = {};
$prevKeyPrefix = undefined;
for (keyI = 0; keyI < keyLength; keyI++) {
key = keys[keyI];
// The string prefix isn't actualy needed
// We use it so keyPrefix for different keys will resolve to the same array if they have the same prefix
// TODO: Find a better way?
$keyPrefixString = keyPrefixString(key, keyPrefixStringMemo);
$keyPrefix = keyPrefix(key);
// On the first row we compute the includeMap
if (rowsI === 0 && !Object.prototype.hasOwnProperty.call(includeMap, key)) {
if (!$keyPrefix.length) {
includeMap[key] = includeMap[''] = includeOptions;
} else {
$current = includeOptions;
previousPiece = undefined;
$keyPrefix.forEach(buildIncludeMap);
}
}
// End of key set
if ($prevKeyPrefix !== undefined && $prevKeyPrefix !== $keyPrefix) {
if (checkExisting) {
// Compute hash key for this set instance
// TODO: Optimize
length = $prevKeyPrefix.length;
$parent = null;
parentHash = null;
if (length) {
for (i = 0; i < length; i++) {
prefix = $parent ? `${$parent}.${$prevKeyPrefix[i]}` : $prevKeyPrefix[i];
primaryKeyAttributes = includeMap[prefix].model.primaryKeyAttributes;
$length = primaryKeyAttributes.length;
itemHash = prefix;
if ($length === 1) {
itemHash += stringify(row[`${prefix}.${primaryKeyAttributes[0]}`]);
}
else if ($length > 1) {
for ($i = 0; $i < $length; $i++) {
itemHash += stringify(row[`${prefix}.${primaryKeyAttributes[$i]}`]);
}
}
else if (!_.isEmpty(includeMap[prefix].model.uniqueKeys)) {
uniqueKeyAttributes = getUniqueKeyAttributes(includeMap[prefix].model);
for ($i = 0; $i < uniqueKeyAttributes.length; $i++) {
itemHash += row[`${prefix}.${uniqueKeyAttributes[$i]}`];
}
}
if (!parentHash) {
parentHash = topHash;
}
itemHash = parentHash + itemHash;
$parent = prefix;
if (i < length - 1) {
parentHash = itemHash;
}
}
} else {
itemHash = topHash;
}
if (itemHash === topHash) {
if (!resultMap[itemHash]) {
resultMap[itemHash] = values;
} else {
topExists = true;
}
} else if (!resultMap[itemHash]) {
$parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey);
if (includeMap[prevKey].association.isSingleAssociation) {
if ($parent) {
$parent[$lastKeyPrefix] = resultMap[itemHash] = values;
}
} else {
if (!$parent[$lastKeyPrefix]) {
$parent[$lastKeyPrefix] = [];
}
$parent[$lastKeyPrefix].push(resultMap[itemHash] = values);
}
}
// Reset values
values = {};
} else {
// If checkExisting is false it's because there's only 1:1 associations in this query
// However we still need to map onto the appropriate parent
// For 1:1 we map forward, initializing the value object on the parent to be filled in the next iterations of the loop
$current = topValues;
length = $keyPrefix.length;
if (length) {
for (i = 0; i < length; i++) {
if (i === length - 1) {
values = $current[$keyPrefix[i]] = {};
}
$current = $current[$keyPrefix[i]] || {};
}
}
}
}
// End of iteration, set value and set prev values (for next iteration)
values[removeKeyPrefix(key)] = row[key];
prevKey = key;
$prevKeyPrefix = $keyPrefix;
$prevKeyPrefixString = $keyPrefixString;
}
if (checkExisting) {
length = $prevKeyPrefix.length;
$parent = null;
parentHash = null;
if (length) {
for (i = 0; i < length; i++) {
prefix = $parent ? `${$parent}.${$prevKeyPrefix[i]}` : $prevKeyPrefix[i];
primaryKeyAttributes = includeMap[prefix].model.primaryKeyAttributes;
$length = primaryKeyAttributes.length;
itemHash = prefix;
if ($length === 1) {
itemHash += stringify(row[`${prefix}.${primaryKeyAttributes[0]}`]);
}
else if ($length > 0) {
for ($i = 0; $i < $length; $i++) {
itemHash += stringify(row[`${prefix}.${primaryKeyAttributes[$i]}`]);
}
}
else if (!_.isEmpty(includeMap[prefix].model.uniqueKeys)) {
uniqueKeyAttributes = getUniqueKeyAttributes(includeMap[prefix].model);
for ($i = 0; $i < uniqueKeyAttributes.length; $i++) {
itemHash += row[`${prefix}.${uniqueKeyAttributes[$i]}`];
}
}
if (!parentHash) {
parentHash = topHash;
}
itemHash = parentHash + itemHash;
$parent = prefix;
if (i < length - 1) {
parentHash = itemHash;
}
}
} else {
itemHash = topHash;
}
if (itemHash === topHash) {
if (!resultMap[itemHash]) {
resultMap[itemHash] = values;
} else {
topExists = true;
}
} else if (!resultMap[itemHash]) {
$parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey);
if (includeMap[prevKey].association.isSingleAssociation) {
if ($parent) {
$parent[$lastKeyPrefix] = resultMap[itemHash] = values;
}
} else {
if (!$parent[$lastKeyPrefix]) {
$parent[$lastKeyPrefix] = [];
}
$parent[$lastKeyPrefix].push(resultMap[itemHash] = values);
}
}
if (!topExists) {
results.push(topValues);
}
} else {
results[rowsI] = topValues;
}
}
return results;
}
}
module.exports = AbstractQuery;
module.exports.AbstractQuery = AbstractQuery;
module.exports.default = AbstractQuery;