init source
This commit is contained in:
+346
@@ -0,0 +1,346 @@
|
||||
'use strict';
|
||||
|
||||
const { Pool, TimeoutError } = require('sequelize-pool');
|
||||
const _ = require('lodash');
|
||||
const semver = require('semver');
|
||||
const Promise = require('../../promise');
|
||||
const errors = require('../../errors');
|
||||
const { logger } = require('../../utils/logger');
|
||||
const debug = logger.debugContext('pool');
|
||||
|
||||
/**
|
||||
* Abstract Connection Manager
|
||||
*
|
||||
* Connection manager which handles pooling & replication.
|
||||
* Uses sequelize-pool for pooling
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
class ConnectionManager {
|
||||
constructor(dialect, sequelize) {
|
||||
const config = _.cloneDeep(sequelize.config);
|
||||
|
||||
this.sequelize = sequelize;
|
||||
this.config = config;
|
||||
this.dialect = dialect;
|
||||
this.versionPromise = null;
|
||||
this.dialectName = this.sequelize.options.dialect;
|
||||
|
||||
if (config.pool === false) {
|
||||
throw new Error('Support for pool:false was removed in v4.0');
|
||||
}
|
||||
|
||||
config.pool = _.defaults(config.pool || {}, {
|
||||
max: 5,
|
||||
min: 0,
|
||||
idle: 10000,
|
||||
acquire: 60000,
|
||||
evict: 1000,
|
||||
validate: this._validate.bind(this)
|
||||
});
|
||||
|
||||
this.initPools();
|
||||
}
|
||||
|
||||
refreshTypeParser(dataTypes) {
|
||||
_.each(dataTypes, dataType => {
|
||||
if (Object.prototype.hasOwnProperty.call(dataType, 'parse')) {
|
||||
if (dataType.types[this.dialectName]) {
|
||||
this._refreshTypeParser(dataType);
|
||||
} else {
|
||||
throw new Error(`Parse function not supported for type ${dataType.key} in dialect ${this.dialectName}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to load dialect module from various configured options.
|
||||
* Priority goes like dialectModulePath > dialectModule > require(default)
|
||||
*
|
||||
* @param {string} moduleName Name of dialect module to lookup
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_loadDialectModule(moduleName) {
|
||||
try {
|
||||
if (this.sequelize.config.dialectModulePath) {
|
||||
return require(this.sequelize.config.dialectModulePath);
|
||||
}
|
||||
if (this.sequelize.config.dialectModule) {
|
||||
return this.sequelize.config.dialectModule;
|
||||
}
|
||||
return require(moduleName);
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') {
|
||||
if (this.sequelize.config.dialectModulePath) {
|
||||
throw new Error(`Unable to find dialect at ${this.sequelize.config.dialectModulePath}`);
|
||||
}
|
||||
throw new Error(`Please install ${moduleName} package manually`);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler which executes on process exit or connection manager shutdown
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onProcessExit() {
|
||||
if (!this.pool) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.pool.drain().then(() => {
|
||||
debug('connection drain due to process exit');
|
||||
return this.pool.destroyAllNow();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Drain the pool and close it permanently
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
close() {
|
||||
// Mark close of pool
|
||||
this.getConnection = function getConnection() {
|
||||
return Promise.reject(new Error('ConnectionManager.getConnection was called after the connection manager was closed!'));
|
||||
};
|
||||
|
||||
return this._onProcessExit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize connection pool. By default pool autostart is set to false, so no connection will be
|
||||
* be created unless `pool.acquire` is called.
|
||||
*/
|
||||
initPools() {
|
||||
const config = this.config;
|
||||
|
||||
if (!config.replication) {
|
||||
this.pool = new Pool({
|
||||
name: 'sequelize',
|
||||
create: () => this._connect(config),
|
||||
destroy: connection => {
|
||||
return this._disconnect(connection)
|
||||
.tap(() => { debug('connection destroy'); });
|
||||
},
|
||||
validate: config.pool.validate,
|
||||
max: config.pool.max,
|
||||
min: config.pool.min,
|
||||
acquireTimeoutMillis: config.pool.acquire,
|
||||
idleTimeoutMillis: config.pool.idle,
|
||||
reapIntervalMillis: config.pool.evict
|
||||
});
|
||||
|
||||
debug(`pool created with max/min: ${config.pool.max}/${config.pool.min}, no replication`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(config.replication.read)) {
|
||||
config.replication.read = [config.replication.read];
|
||||
}
|
||||
|
||||
// Map main connection config
|
||||
config.replication.write = _.defaults(config.replication.write, _.omit(config, 'replication'));
|
||||
|
||||
// Apply defaults to each read config
|
||||
config.replication.read = config.replication.read.map(readConfig =>
|
||||
_.defaults(readConfig, _.omit(this.config, 'replication'))
|
||||
);
|
||||
|
||||
// custom pooling for replication (original author @janmeier)
|
||||
let reads = 0;
|
||||
this.pool = {
|
||||
release: client => {
|
||||
if (client.queryType === 'read') {
|
||||
this.pool.read.release(client);
|
||||
} else {
|
||||
this.pool.write.release(client);
|
||||
}
|
||||
},
|
||||
acquire: (queryType, useMaster) => {
|
||||
useMaster = useMaster === undefined ? false : useMaster;
|
||||
if (queryType === 'SELECT' && !useMaster) {
|
||||
return this.pool.read.acquire();
|
||||
}
|
||||
return this.pool.write.acquire();
|
||||
},
|
||||
destroy: connection => {
|
||||
this.pool[connection.queryType].destroy(connection);
|
||||
debug('connection destroy');
|
||||
},
|
||||
destroyAllNow: () => {
|
||||
return Promise.join(
|
||||
this.pool.read.destroyAllNow(),
|
||||
this.pool.write.destroyAllNow()
|
||||
).tap(() => { debug('all connections destroyed'); });
|
||||
},
|
||||
drain: () => {
|
||||
return Promise.join(
|
||||
this.pool.write.drain(),
|
||||
this.pool.read.drain()
|
||||
);
|
||||
},
|
||||
read: new Pool({
|
||||
name: 'sequelize:read',
|
||||
create: () => {
|
||||
// round robin config
|
||||
const nextRead = reads++ % config.replication.read.length;
|
||||
return this._connect(config.replication.read[nextRead]).tap(connection => {
|
||||
connection.queryType = 'read';
|
||||
});
|
||||
},
|
||||
destroy: connection => this._disconnect(connection),
|
||||
validate: config.pool.validate,
|
||||
max: config.pool.max,
|
||||
min: config.pool.min,
|
||||
acquireTimeoutMillis: config.pool.acquire,
|
||||
idleTimeoutMillis: config.pool.idle,
|
||||
reapIntervalMillis: config.pool.evict
|
||||
}),
|
||||
write: new Pool({
|
||||
name: 'sequelize:write',
|
||||
create: () => {
|
||||
return this._connect(config.replication.write).tap(connection => {
|
||||
connection.queryType = 'write';
|
||||
});
|
||||
},
|
||||
destroy: connection => this._disconnect(connection),
|
||||
validate: config.pool.validate,
|
||||
max: config.pool.max,
|
||||
min: config.pool.min,
|
||||
acquireTimeoutMillis: config.pool.acquire,
|
||||
idleTimeoutMillis: config.pool.idle,
|
||||
reapIntervalMillis: config.pool.evict
|
||||
})
|
||||
};
|
||||
|
||||
debug(`pool created with max/min: ${config.pool.max}/${config.pool.min}, with replication`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection from pool. It sets database version if it's not already set.
|
||||
* Call pool.acquire to get a connection
|
||||
*
|
||||
* @param {Object} [options] Pool options
|
||||
* @param {string} [options.type] Set which replica to use. Available options are `read` and `write`
|
||||
* @param {boolean} [options.useMaster=false] Force master or write replica to get connection from
|
||||
*
|
||||
* @returns {Promise<Connection>}
|
||||
*/
|
||||
getConnection(options) {
|
||||
options = options || {};
|
||||
|
||||
let promise;
|
||||
if (this.sequelize.options.databaseVersion === 0) {
|
||||
if (this.versionPromise) {
|
||||
promise = this.versionPromise;
|
||||
} else {
|
||||
promise = this.versionPromise = this._connect(this.config.replication.write || this.config)
|
||||
.then(connection => {
|
||||
const _options = {};
|
||||
|
||||
_options.transaction = { connection }; // Cheat .query to use our private connection
|
||||
_options.logging = () => {};
|
||||
_options.logging.__testLoggingFn = true;
|
||||
|
||||
//connection might have set databaseVersion value at initialization,
|
||||
//avoiding a useless round trip
|
||||
if (this.sequelize.options.databaseVersion === 0) {
|
||||
return this.sequelize.databaseVersion(_options).then(version => {
|
||||
const parsedVersion = _.get(semver.coerce(version), 'version') || version;
|
||||
this.sequelize.options.databaseVersion = semver.valid(parsedVersion)
|
||||
? parsedVersion
|
||||
: this.defaultVersion;
|
||||
this.versionPromise = null;
|
||||
return this._disconnect(connection);
|
||||
});
|
||||
}
|
||||
|
||||
this.versionPromise = null;
|
||||
return this._disconnect(connection);
|
||||
}).catch(err => {
|
||||
this.versionPromise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
return this.pool.acquire(options.type, options.useMaster)
|
||||
.catch(error => {
|
||||
if (error instanceof TimeoutError) throw new errors.ConnectionAcquireTimeoutError(error);
|
||||
throw error;
|
||||
});
|
||||
}).tap(() => { debug('connection acquired'); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a pooled connection so it can be utilized by other connection requests
|
||||
*
|
||||
* @param {Connection} connection
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
releaseConnection(connection) {
|
||||
return Promise.try(() => {
|
||||
this.pool.release(connection);
|
||||
debug('connection released');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Call dialect library to get connection
|
||||
*
|
||||
* @param {*} config Connection config
|
||||
* @private
|
||||
* @returns {Promise<Connection>}
|
||||
*/
|
||||
_connect(config) {
|
||||
return this.sequelize.runHooks('beforeConnect', config)
|
||||
.then(() => this.dialect.connectionManager.connect(config))
|
||||
.then(connection => this.sequelize.runHooks('afterConnect', connection, config).return(connection));
|
||||
}
|
||||
|
||||
/**
|
||||
* Call dialect library to disconnect a connection
|
||||
*
|
||||
* @param {Connection} connection
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_disconnect(connection) {
|
||||
return this.sequelize.runHooks('beforeDisconnect', connection)
|
||||
.then(() => this.dialect.connectionManager.disconnect(connection))
|
||||
.then(() => this.sequelize.runHooks('afterDisconnect', connection));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a connection is still valid or not
|
||||
*
|
||||
* @param {Connection} connection
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_validate(connection) {
|
||||
if (!this.dialect.connectionManager.validate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.dialect.connectionManager.validate(connection);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConnectionManager;
|
||||
module.exports.ConnectionManager = ConnectionManager;
|
||||
module.exports.default = ConnectionManager;
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
'use strict';
|
||||
|
||||
class AbstractDialect {}
|
||||
|
||||
AbstractDialect.prototype.supports = {
|
||||
'DEFAULT': true,
|
||||
'DEFAULT VALUES': false,
|
||||
'VALUES ()': false,
|
||||
'LIMIT ON UPDATE': false,
|
||||
'ON DUPLICATE KEY': true,
|
||||
'ORDER NULLS': false,
|
||||
'UNION': true,
|
||||
'UNION ALL': true,
|
||||
|
||||
/* does the dialect support returning values for inserted/updated fields */
|
||||
returnValues: false,
|
||||
|
||||
/* features specific to autoIncrement values */
|
||||
autoIncrement: {
|
||||
/* does the dialect require modification of insert queries when inserting auto increment fields */
|
||||
identityInsert: false,
|
||||
|
||||
/* does the dialect support inserting default/null values for autoincrement fields */
|
||||
defaultValue: true,
|
||||
|
||||
/* does the dialect support updating autoincrement fields */
|
||||
update: true
|
||||
},
|
||||
/* Do we need to say DEFAULT for bulk insert */
|
||||
bulkDefault: false,
|
||||
schemas: false,
|
||||
transactions: true,
|
||||
settingIsolationLevelDuringTransaction: true,
|
||||
transactionOptions: {
|
||||
type: false
|
||||
},
|
||||
migrations: true,
|
||||
upserts: true,
|
||||
inserts: {
|
||||
ignoreDuplicates: '', /* dialect specific words for INSERT IGNORE or DO NOTHING */
|
||||
updateOnDuplicate: false, /* whether dialect supports ON DUPLICATE KEY UPDATE */
|
||||
onConflictDoNothing: '' /* dialect specific words for ON CONFLICT DO NOTHING */
|
||||
},
|
||||
constraints: {
|
||||
restrict: true,
|
||||
addConstraint: true,
|
||||
dropConstraint: true,
|
||||
unique: true,
|
||||
default: false,
|
||||
check: true,
|
||||
foreignKey: true,
|
||||
primaryKey: true
|
||||
},
|
||||
index: {
|
||||
collate: true,
|
||||
length: false,
|
||||
parser: false,
|
||||
concurrently: false,
|
||||
type: false,
|
||||
using: true,
|
||||
functionBased: false
|
||||
},
|
||||
joinTableDependent: true,
|
||||
groupedLimit: true,
|
||||
indexViaAlter: false,
|
||||
JSON: false,
|
||||
deferrableConstraints: false
|
||||
};
|
||||
|
||||
module.exports = AbstractDialect;
|
||||
module.exports.AbstractDialect = AbstractDialect;
|
||||
module.exports.default = AbstractDialect;
|
||||
+2653
File diff suppressed because it is too large
Load Diff
+87
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Quote helpers implement quote ability for all dialects.
|
||||
* These are basic block of query building
|
||||
*
|
||||
* Its better to implement all dialect implementation together here. Which will allow
|
||||
* even abstract generator to use them by just specifying dialect type.
|
||||
*
|
||||
* Defining these helpers in each query dialect will leave
|
||||
* code in dual dependency of abstract <-> specific dialect
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Utils = require('../../../../utils');
|
||||
|
||||
/**
|
||||
* list of reserved words in PostgreSQL 10
|
||||
* source: https://www.postgresql.org/docs/10/static/sql-keywords-appendix.html
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
const postgresReservedWords = 'all,analyse,analyze,and,any,array,as,asc,asymmetric,authorization,binary,both,case,cast,check,collate,collation,column,concurrently,constraint,create,cross,current_catalog,current_date,current_role,current_schema,current_time,current_timestamp,current_user,default,deferrable,desc,distinct,do,else,end,except,false,fetch,for,foreign,freeze,from,full,grant,group,having,ilike,in,initially,inner,intersect,into,is,isnull,join,lateral,leading,left,like,limit,localtime,localtimestamp,natural,not,notnull,null,offset,on,only,or,order,outer,overlaps,placing,primary,references,returning,right,select,session_user,similar,some,symmetric,table,tablesample,then,to,trailing,true,union,unique,user,using,variadic,verbose,when,where,window,with'.split(',');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} dialect Dialect name
|
||||
* @param {string} identifier Identifier to quote
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.force=false]
|
||||
* @param {boolean} [options.quoteIdentifiers=true]
|
||||
*
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
function quoteIdentifier(dialect, identifier, options) {
|
||||
if (identifier === '*') return identifier;
|
||||
|
||||
options = Utils.defaults(options || {}, {
|
||||
force: false,
|
||||
quoteIdentifiers: true
|
||||
});
|
||||
|
||||
switch (dialect) {
|
||||
case 'sqlite':
|
||||
case 'mariadb':
|
||||
case 'mysql':
|
||||
return Utils.addTicks(Utils.removeTicks(identifier, '`'), '`');
|
||||
|
||||
case 'postgres':
|
||||
const rawIdentifier = Utils.removeTicks(identifier, '"');
|
||||
|
||||
if (
|
||||
options.force !== true &&
|
||||
options.quoteIdentifiers === false &&
|
||||
!identifier.includes('.') &&
|
||||
!identifier.includes('->') &&
|
||||
!postgresReservedWords.includes(rawIdentifier.toLowerCase())
|
||||
) {
|
||||
// In Postgres, if tables or attributes are created double-quoted,
|
||||
// they are also case sensitive. If they contain any uppercase
|
||||
// characters, they must always be double-quoted. This makes it
|
||||
// impossible to write queries in portable SQL if tables are created in
|
||||
// this way. Hence, we strip quotes if we don't want case sensitivity.
|
||||
return rawIdentifier;
|
||||
}
|
||||
return Utils.addTicks(rawIdentifier, '"');
|
||||
case 'mssql':
|
||||
return `[${identifier.replace(/[[\]']+/g, '')}]`;
|
||||
|
||||
default:
|
||||
throw new Error(`Dialect "${dialect}" is not supported`);
|
||||
}
|
||||
}
|
||||
module.exports.quoteIdentifier = quoteIdentifier;
|
||||
|
||||
/**
|
||||
* Test if a give string is already quoted
|
||||
*
|
||||
* @param {string} identifier
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
function isIdentifierQuoted(identifier) {
|
||||
return /^\s*(?:([`"'])(?:(?!\1).|\1{2})*\1\.?)+\s*$/i.test(identifier);
|
||||
}
|
||||
module.exports.isIdentifierQuoted = isIdentifierQuoted;
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const Op = require('../../../operators');
|
||||
const Utils = require('../../../utils');
|
||||
|
||||
const OperatorHelpers = {
|
||||
OperatorMap: {
|
||||
[Op.eq]: '=',
|
||||
[Op.ne]: '!=',
|
||||
[Op.gte]: '>=',
|
||||
[Op.gt]: '>',
|
||||
[Op.lte]: '<=',
|
||||
[Op.lt]: '<',
|
||||
[Op.not]: 'IS NOT',
|
||||
[Op.is]: 'IS',
|
||||
[Op.in]: 'IN',
|
||||
[Op.notIn]: 'NOT IN',
|
||||
[Op.like]: 'LIKE',
|
||||
[Op.notLike]: 'NOT LIKE',
|
||||
[Op.iLike]: 'ILIKE',
|
||||
[Op.notILike]: 'NOT ILIKE',
|
||||
[Op.startsWith]: 'LIKE',
|
||||
[Op.endsWith]: 'LIKE',
|
||||
[Op.substring]: 'LIKE',
|
||||
[Op.regexp]: '~',
|
||||
[Op.notRegexp]: '!~',
|
||||
[Op.iRegexp]: '~*',
|
||||
[Op.notIRegexp]: '!~*',
|
||||
[Op.between]: 'BETWEEN',
|
||||
[Op.notBetween]: 'NOT BETWEEN',
|
||||
[Op.overlap]: '&&',
|
||||
[Op.contains]: '@>',
|
||||
[Op.contained]: '<@',
|
||||
[Op.adjacent]: '-|-',
|
||||
[Op.strictLeft]: '<<',
|
||||
[Op.strictRight]: '>>',
|
||||
[Op.noExtendRight]: '&<',
|
||||
[Op.noExtendLeft]: '&>',
|
||||
[Op.any]: 'ANY',
|
||||
[Op.all]: 'ALL',
|
||||
[Op.and]: ' AND ',
|
||||
[Op.or]: ' OR ',
|
||||
[Op.col]: 'COL',
|
||||
[Op.placeholder]: '$$PLACEHOLDER$$'
|
||||
},
|
||||
|
||||
OperatorsAliasMap: {},
|
||||
|
||||
setOperatorsAliases(aliases) {
|
||||
if (!aliases || _.isEmpty(aliases)) {
|
||||
this.OperatorsAliasMap = false;
|
||||
} else {
|
||||
this.OperatorsAliasMap = Object.assign({}, aliases);
|
||||
}
|
||||
},
|
||||
|
||||
_replaceAliases(orig) {
|
||||
const obj = {};
|
||||
if (!this.OperatorsAliasMap) {
|
||||
return orig;
|
||||
}
|
||||
|
||||
Utils.getOperators(orig).forEach(op => {
|
||||
const item = orig[op];
|
||||
if (_.isPlainObject(item)) {
|
||||
obj[op] = this._replaceAliases(item);
|
||||
} else {
|
||||
obj[op] = item;
|
||||
}
|
||||
});
|
||||
|
||||
_.forOwn(orig, (item, prop) => {
|
||||
prop = this.OperatorsAliasMap[prop] || prop;
|
||||
if (_.isPlainObject(item)) {
|
||||
item = this._replaceAliases(item);
|
||||
}
|
||||
obj[prop] = item;
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = OperatorHelpers;
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
|
||||
const uuidv4 = require('uuid/v4');
|
||||
|
||||
const TransactionQueries = {
|
||||
/**
|
||||
* Returns a query that sets the transaction isolation level.
|
||||
*
|
||||
* @param {string} value The isolation level.
|
||||
* @param {Object} options An object with options.
|
||||
* @returns {string} The generated sql query.
|
||||
* @private
|
||||
*/
|
||||
setIsolationLevelQuery(value, options) {
|
||||
if (options.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
return `SET TRANSACTION ISOLATION LEVEL ${value};`;
|
||||
},
|
||||
|
||||
generateTransactionId() {
|
||||
return uuidv4();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a query that starts a transaction.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @returns {string} The generated sql query.
|
||||
* @private
|
||||
*/
|
||||
startTransactionQuery(transaction) {
|
||||
if (transaction.parent) {
|
||||
// force quoting of savepoint identifiers for postgres
|
||||
return `SAVEPOINT ${this.quoteIdentifier(transaction.name, true)};`;
|
||||
}
|
||||
|
||||
return 'START TRANSACTION;';
|
||||
},
|
||||
|
||||
deferConstraintsQuery() {},
|
||||
|
||||
setConstraintQuery() {},
|
||||
setDeferredQuery() {},
|
||||
setImmediateQuery() {},
|
||||
|
||||
/**
|
||||
* Returns a query that commits a transaction.
|
||||
*
|
||||
* @param {Transaction} transaction An object with options.
|
||||
* @returns {string} The generated sql query.
|
||||
* @private
|
||||
*/
|
||||
commitTransactionQuery(transaction) {
|
||||
if (transaction.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
return 'COMMIT;';
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a query that rollbacks a transaction.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @returns {string} The generated sql query.
|
||||
* @private
|
||||
*/
|
||||
rollbackTransactionQuery(transaction) {
|
||||
if (transaction.parent) {
|
||||
// force quoting of savepoint identifiers for postgres
|
||||
return `ROLLBACK TO SAVEPOINT ${this.quoteIdentifier(transaction.name, true)};`;
|
||||
}
|
||||
|
||||
return 'ROLLBACK;';
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TransactionQueries;
|
||||
+741
@@ -0,0 +1,741 @@
|
||||
'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 = Object.assign({
|
||||
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(/\$(\$|\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 => _.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;
|
||||
Reference in New Issue
Block a user