123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741 |
- 'use strict';
- 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;
|