BE-SPAN/node_modules/sequelize-oracle/lib/dialects/abstract/query.js

588 lines
17 KiB
JavaScript
Raw Permalink Normal View History

2024-10-20 15:04:16 +00:00
'use strict';
var Utils = require('../../utils')
, Dot = require('dottie')
, QueryTypes = require('../../query-types');
var AbstractQuery = function(database, sequelize, 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 }
]
}
]
*/
/*
* 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 its performant.
* groupJoinData is a performance critical function so we prioritize perf over readability.
*/
var groupJoinData = function(rows, includeOptions, options) {
if (!rows.length) {
return [];
}
var
// Generic looping
i
, length
, $i
, $length
// Row specific looping
, rowsI
, rowsLength = rows.length
, row
// Key specific looping
, keys
, key
, keyI
, keyLength
, prevKey
, values
, topValues
, topExists
, checkExisting = options.checkExisting
// If we don't have to deduplicate we can pre-allocate the resulting array
, results = checkExisting ? [] : new Array(rowsLength)
, resultMap = {}
, includeMap = {}
, itemHash
, parentHash
, topHash
// Result variables for the respective functions
, $keyPrefix
, $keyPrefixString
, $prevKeyPrefixString
, $prevKeyPrefix
, $lastKeyPrefix
, $current
, $parent
// Map each key to an include option
, previousPiece
, buildIncludeMap = function (piece) {
if ($current.includeMap[piece]) {
includeMap[key] = $current = $current.includeMap[piece];
if (previousPiece) {
previousPiece = previousPiece+'.'+piece;
} else {
previousPiece = piece;
}
includeMap[previousPiece] = $current;
}
}
// Calcuate the last item in the array prefix ('Results' for 'User.Results.id')
, lastKeyPrefixMemo = {}
, lastKeyPrefix = function (key) {
if (!lastKeyPrefixMemo[key]) {
var prefix = keyPrefix(key)
, length = prefix.length;
lastKeyPrefixMemo[key] = !length ? '' : prefix[length - 1];
}
return lastKeyPrefixMemo[key];
}
// Calculate the string prefix of a key ('User.Results' for 'User.Results.id')
, keyPrefixStringMemo = {}
, keyPrefixString = function (key, memo) {
if (!memo[key]) {
memo[key] = key.substr(0, key.lastIndexOf('.'));
}
return memo[key];
}
// Removes the prefix from a key ('id' for 'User.Results.id')
, removeKeyPrefixMemo = {}
, removeKeyPrefix = function (key) {
if (!removeKeyPrefixMemo[key]) {
var 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')
, keyPrefixMemo = {}
, keyPrefix = function (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 (!keyPrefixMemo[key]) {
var prefixString = keyPrefixString(key, keyPrefixStringMemo);
if (!keyPrefixMemo[prefixString]) {
keyPrefixMemo[prefixString] = prefixString ? prefixString.split('.') : [];
}
keyPrefixMemo[key] = keyPrefixMemo[prefixString];
}
return keyPrefixMemo[key];
}
, primaryKeyAttributes
, 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;
if ($length === 1) {
topHash = row[includeOptions.model.primaryKeyAttributes[0]];
} else {
topHash = '';
for ($i = 0; $i < $length; $i++) {
topHash += row[includeOptions.model.primaryKeyAttributes[$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 && includeMap[key] === undefined) {
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;
if ($length === 1) {
itemHash = prefix+row[prefix+'.'+primaryKeyAttributes[0]];
} else {
itemHash = prefix;
for ($i = 0; $i < $length; $i++) {
itemHash += row[prefix+'.'+primaryKeyAttributes[$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) {
$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;
if ($length === 1) {
itemHash = prefix+row[prefix+'.'+primaryKeyAttributes[0]];
} else {
itemHash = prefix;
for ($i = 0; $i < $length; $i++) {
itemHash += row[prefix+'.'+primaryKeyAttributes[$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) {
$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;
};
/**
* Execute the passed sql query.
*
* Examples:
*
* query.run('SELECT 1')
*
* @param {String} sql - The SQL query which should be executed.
* @api public
*/
AbstractQuery.prototype.run = function() {
throw new Error('The run method wasn\'t overwritten!');
};
/**
* Check the logging option of the instance and print deprecation warnings.
*
* @return {void}
*/
AbstractQuery.prototype.checkLoggingOption = function() {
if (this.options.logging === true) {
console.log('DEPRECATION WARNING: The logging-option should be either a function or false. Default: console.log');
this.options.logging = console.log;
}
if (this.options.logging === console.log) {
// using just console.log will break in node < 0.6
this.options.logging = function(s) { console.log(s); };
}
};
/**
* Get the attributes of an insert query, which contains the just inserted id.
*
* @return {String} The field name.
*/
AbstractQuery.prototype.getInsertIdField = function() {
return 'insertId';
};
/**
* Iterate over all known tables and search their names inside the sql query.
* This method will also check association aliases ('as' option).
*
* @param {String} attribute An attribute of a SQL query. (?)
* @return {String} The found tableName / alias.
*/
AbstractQuery.prototype.findTableNameInAttribute = function(attribute) {
if (!this.options.include) {
return null;
}
if (!this.options.includeNames) {
this.options.includeNames = this.options.include.map(function(include) {
return include.as;
});
}
var tableNames = this.options.includeNames.filter(function(include) {
return attribute.indexOf(include + '.') === 0;
});
if (tableNames.length === 1) {
return tableNames[0];
} else {
return null;
}
};
AbstractQuery.prototype.getUniqueConstraintErrorMessage = function(field) {
var message = field + ' must be unique';
var self = this;
Object.keys(self.model.uniqueKeys).forEach(function(key) {
if (self.model.uniqueKeys[key].fields.indexOf(field.replace(/"/g, '')) >= 0) {
if (self.model.uniqueKeys[key].hasOwnProperty('msg')) {
message = self.model.uniqueKeys[key].msg;
}
}
});
return message;
};
AbstractQuery.prototype.isRawQuery = function () {
return this.options.type === QueryTypes.RAW;
};
AbstractQuery.prototype.isVersionQuery = function () {
return this.options.type === QueryTypes.VERSION;
};
AbstractQuery.prototype.isUpsertQuery = function () {
return this.options.type === QueryTypes.UPSERT;
};
AbstractQuery.prototype.isInsertQuery = function(results, metaData) {
var result = true;
if (this.options.type === QueryTypes.INSERT) {
return true;
}
// is insert query if sql contains insert into
result = result && (this.sql.toLowerCase().indexOf('insert into') === 0);
// is insert query if no results are passed or if the result has the inserted id
result = result && (!results || results.hasOwnProperty(this.getInsertIdField()));
// is insert query if no metadata are passed or if the metadata has the inserted id
result = result && (!metaData || metaData.hasOwnProperty(this.getInsertIdField()));
return result;
};
AbstractQuery.prototype.handleInsertQuery = function(results, metaData) {
if (this.instance) {
// add the inserted row id to the instance
var autoIncrementField = this.model.autoIncrementField
, id = null;
id = id || (results && results[this.getInsertIdField()]);
id = id || (metaData && metaData[this.getInsertIdField()]);
this.instance[autoIncrementField] = id;
}
};
AbstractQuery.prototype.isShowTablesQuery = function() {
return this.options.type === QueryTypes.SHOWTABLES;
};
AbstractQuery.prototype.handleShowTablesQuery = function(results) {
return Utils._.flatten(results.map(function(resultSet) {
return Utils._.values(resultSet);
}));
};
AbstractQuery.prototype.isShowIndexesQuery = function () {
return this.options.type === QueryTypes.SHOWINDEXES;
};
AbstractQuery.prototype.isDescribeQuery = function () {
return this.options.type === QueryTypes.DESCRIBE;
};
AbstractQuery.prototype.isSelectQuery = function() {
return this.options.type === QueryTypes.SELECT;
};
AbstractQuery.prototype.isBulkUpdateQuery = function() {
return this.options.type === QueryTypes.BULKUPDATE;
};
AbstractQuery.prototype.isBulkDeleteQuery = function() {
return this.options.type === QueryTypes.BULKDELETE;
};
AbstractQuery.prototype.isForeignKeysQuery = function() {
return this.options.type === QueryTypes.FOREIGNKEYS;
};
AbstractQuery.prototype.isUpdateQuery = function() {
return this.options.type === QueryTypes.UPDATE;
};
AbstractQuery.prototype.handleSelectQuery = function(results) {
var result = null;
// Raw queries
if (this.options.raw) {
result = results.map(function(result) {
var o = {};
for (var key in result) {
if (result.hasOwnProperty(key)) {
o[key] = result[key];
}
}
if (this.options.nest) {
o = Dot.transform(o);
}
return o;
}, this);
// Queries with include
} else if (this.options.hasJoin === true) {
results = 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.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;
};
AbstractQuery.prototype.isShowOrDescribeQuery = function() {
var result = false;
result = result || (this.sql.toLowerCase().indexOf('show') === 0);
result = result || (this.sql.toLowerCase().indexOf('describe') === 0);
return result;
};
AbstractQuery.prototype.isCallQuery = function() {
var result = false;
result = result || (this.sql.toLowerCase().indexOf('call') === 0);
return result;
};
AbstractQuery.$groupJoinData = groupJoinData;
module.exports = AbstractQuery;