436 lines
14 KiB
JavaScript
436 lines
14 KiB
JavaScript
'use strict';
|
|
|
|
var Utils = require('../../utils')
|
|
, AbstractQuery = require('../abstract/query')
|
|
, DataTypes = require('../../data-types')
|
|
, hstore = require('./hstore')
|
|
, range = require('./range')
|
|
, QueryTypes = require('../../query-types')
|
|
, Promise = require('../../promise')
|
|
, sequelizeErrors = require('../../errors.js');
|
|
|
|
var parseDialectSpecificFields,
|
|
dialectSpecificTypes;
|
|
|
|
parseDialectSpecificFields = {
|
|
// Parses hstore fields if the model has any hstore fields.
|
|
// This cannot be done in the 'pg' lib because hstore is a UDT.
|
|
hstore: function (value, options) {
|
|
if (value === null) return null;
|
|
|
|
return DataTypes.ARRAY.is(options.dataType, DataTypes.HSTORE) ?
|
|
this.lib.types.arrayParser.create(value, hstore.parse).parse() : hstore.parse(value);
|
|
},
|
|
range: function (value, options) {
|
|
if (value === null) return null;
|
|
|
|
return DataTypes.ARRAY.is(options.dataType, DataTypes.RANGE) ?
|
|
this.lib.types.arrayParser.create(value, function (v) {
|
|
return range.parse(v, options.dataType.type);
|
|
}).parse() : range.parse(value, options.dataType);
|
|
}
|
|
};
|
|
|
|
dialectSpecificTypes = Utils._.keys(parseDialectSpecificFields);
|
|
|
|
function dialectSpecificFieldDatatypeMap (options, prefix) {
|
|
if (!Utils._.isArray(options.types))
|
|
return [];
|
|
|
|
prefix = prefix || '';
|
|
|
|
var fields = {};
|
|
if (options.callee) {
|
|
if (options.as)
|
|
prefix += options.as + '.';
|
|
|
|
Utils._.each(options.types, function (type) {
|
|
Utils._.each(options.callee['_' + type + 'Attributes'], function (attrName) {
|
|
fields[prefix + attrName] = {
|
|
dataType: options.callee.attributes[attrName].type || null,
|
|
type: type
|
|
};
|
|
});
|
|
});
|
|
}
|
|
|
|
Utils._.each(options.include, function (include) {
|
|
fields = Utils._.merge(fields, dialectSpecificFieldDatatypeMap({
|
|
callee: include.model,
|
|
as: include.as,
|
|
include: include.include,
|
|
types: options.types
|
|
}, prefix));
|
|
});
|
|
|
|
return fields;
|
|
}
|
|
|
|
var Query = function(client, sequelize, options) {
|
|
this.client = client;
|
|
this.sequelize = sequelize;
|
|
this.instance = options.instance;
|
|
this.model = options.model;
|
|
this.options = Utils._.extend({
|
|
logging: console.log,
|
|
plain: false,
|
|
raw: false
|
|
}, options || {});
|
|
|
|
this.checkLoggingOption();
|
|
};
|
|
Utils.inherit(Query, AbstractQuery);
|
|
|
|
Query.prototype.parseDialectSpecificFields = parseDialectSpecificFields;
|
|
|
|
Query.prototype.run = function(sql) {
|
|
this.sql = sql;
|
|
|
|
var self = this
|
|
, receivedError = false
|
|
, query = this.client.query(sql)
|
|
, rows = [];
|
|
|
|
this.sequelize.log('Executing (' + (this.client.uuid || 'default') + '): ' + this.sql, this.options);
|
|
|
|
var promise = new Promise(function(resolve, reject) {
|
|
query.on('row', function(row) {
|
|
rows.push(row);
|
|
});
|
|
|
|
query.on('error', function(err) {
|
|
receivedError = true;
|
|
err.sql = sql;
|
|
reject(self.formatError(err));
|
|
});
|
|
|
|
query.on('end', function(result) {
|
|
if (receivedError) {
|
|
return;
|
|
}
|
|
|
|
resolve([rows, sql, result]);
|
|
});
|
|
}).spread(function(rows, sql, result) {
|
|
var results = rows
|
|
, isTableNameQuery = (sql.indexOf('SELECT table_name FROM information_schema.tables') === 0)
|
|
, isRelNameQuery = (sql.indexOf('SELECT relname FROM pg_class WHERE oid IN') === 0)
|
|
, dialectSpecificFields
|
|
, isDialectSpecificField = Utils._.memoize(function (key) { return dialectSpecificFields.hasOwnProperty(key); });
|
|
|
|
if (isTableNameQuery || isRelNameQuery) {
|
|
if (isRelNameQuery) {
|
|
results = rows.map(function(row) {
|
|
return {
|
|
name: row.relname,
|
|
tableName: row.relname.split('_')[0]
|
|
};
|
|
});
|
|
} else {
|
|
results = rows.map(function(row) { return Utils._.values(row); });
|
|
}
|
|
return results;
|
|
}
|
|
|
|
if (rows[0] && rows[0].sequelize_caught_exception !== undefined) {
|
|
if (rows[0].sequelize_caught_exception !== null) {
|
|
var err = self.formatError({
|
|
code: '23505',
|
|
detail: rows[0].sequelize_caught_exception
|
|
});
|
|
throw err;
|
|
} else {
|
|
rows = rows.map(function (row) {
|
|
delete row.sequelize_caught_exception;
|
|
return row;
|
|
});
|
|
}
|
|
}
|
|
|
|
if (self.isShowIndexesQuery()) {
|
|
results.forEach(function (result) {
|
|
var attributes = /ON .*? (?:USING .*?\s)?\((.*)\)/gi.exec(result.definition)[1].split(',')
|
|
, field
|
|
|
|
, attribute
|
|
, columns;
|
|
|
|
// Map column index in table to column name
|
|
columns = Utils._.zipObject(
|
|
result.column_indexes,
|
|
self.sequelize.queryInterface.QueryGenerator.fromArray(result.column_names)
|
|
);
|
|
delete result.column_indexes;
|
|
delete result.column_names;
|
|
|
|
// Indkey is the order of attributes in the index, specified by a string of attribute indexes
|
|
result.fields = result.indkey.split(' ').map(function (indKey, index) {
|
|
field = columns[indKey];
|
|
attribute = attributes[index];
|
|
return {
|
|
attribute: field,
|
|
collate: attribute.match(/COLLATE "(.*?)"/) ? /COLLATE "(.*?)"/.exec(attribute)[1] : undefined,
|
|
order: attribute.indexOf('DESC') !== -1 ? 'DESC' : attribute.indexOf('ASC') !== -1 ? 'ASC': undefined,
|
|
length: undefined
|
|
};
|
|
});
|
|
delete result.columns;
|
|
});
|
|
return results;
|
|
} else if (self.isForeignKeysQuery()) {
|
|
result = [];
|
|
rows.forEach(function(row) {
|
|
var defParts;
|
|
if (row.condef !== undefined && (defParts = row.condef.match(/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?/))) {
|
|
row.id = row.constraint_name;
|
|
row.table = defParts[2];
|
|
row.from = defParts[1];
|
|
row.to = defParts[3];
|
|
var i;
|
|
for (i=5;i<=8;i+=3) {
|
|
if (/(UPDATE|DELETE)/.test(defParts[i])) {
|
|
row['on_'+defParts[i].toLowerCase()] = defParts[i+1];
|
|
}
|
|
}
|
|
}
|
|
result.push(row);
|
|
});
|
|
return result;
|
|
} else if (self.isSelectQuery()) {
|
|
// Postgres will treat tables as case-insensitive, so fix the case
|
|
// of the returned values to match attributes
|
|
if (self.options.raw === false && self.sequelize.options.quoteIdentifiers === false) {
|
|
var attrsMap = Utils._.reduce(self.model.attributes, function(m, v, k) { m[k.toLowerCase()] = k; return m; }, {});
|
|
rows.forEach(function(row) {
|
|
Utils._.keys(row).forEach(function(key) {
|
|
var targetAttr = attrsMap[key];
|
|
if (typeof targetAttr === 'string' && targetAttr !== key) {
|
|
row[targetAttr] = row[key];
|
|
delete row[key];
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (!!self.model && rows.length > 0) {
|
|
dialectSpecificFields = dialectSpecificFieldDatatypeMap({
|
|
callee: self.model,
|
|
include: self.options.include,
|
|
types: dialectSpecificTypes
|
|
});
|
|
|
|
// check whether custom fields exist in responses model
|
|
if (!Utils._.isEmpty(dialectSpecificFields)) {
|
|
rows.forEach(function (row, rowId, rows) {
|
|
Utils._.each(row, function (value, key) {
|
|
if (isDialectSpecificField(key))
|
|
rows[rowId][key] =
|
|
parseDialectSpecificFields[dialectSpecificFields[key].type](value, { dataType: dialectSpecificFields[key].dataType });
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
return self.handleSelectQuery(rows);
|
|
} else if (QueryTypes.DESCRIBE === self.options.type) {
|
|
result = {};
|
|
|
|
rows.forEach(function(_result) {
|
|
result[_result.Field] = {
|
|
type: _result.Type.toUpperCase(),
|
|
allowNull: (_result.Null === 'YES'),
|
|
defaultValue: _result.Default,
|
|
special: (!!_result.special ? self.sequelize.queryInterface.QueryGenerator.fromArray(_result.special) : [])
|
|
};
|
|
|
|
if (result[_result.Field].type === 'BOOLEAN') {
|
|
result[_result.Field].defaultValue = { 'false': false, 'true': true }[result[_result.Field].defaultValue];
|
|
|
|
if (result[_result.Field].defaultValue === undefined) {
|
|
result[_result.Field].defaultValue = null;
|
|
}
|
|
}
|
|
|
|
if (typeof result[_result.Field].defaultValue === 'string') {
|
|
result[_result.Field].defaultValue = result[_result.Field].defaultValue.replace(/'/g, '');
|
|
|
|
if (result[_result.Field].defaultValue.indexOf('::') > -1) {
|
|
var split = result[_result.Field].defaultValue.split('::');
|
|
if (split[1].toLowerCase() !== 'regclass)') {
|
|
result[_result.Field].defaultValue = split[0];
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return result;
|
|
} else if (self.isShowOrDescribeQuery()) {
|
|
return results;
|
|
} else if (QueryTypes.BULKUPDATE === self.options.type) {
|
|
if (!self.options.returning) {
|
|
return parseInt(result.rowCount, 10);
|
|
}
|
|
|
|
if (!!self.model && rows.length > 0) {
|
|
dialectSpecificFields = dialectSpecificFieldDatatypeMap({
|
|
callee: self.model,
|
|
types: dialectSpecificTypes
|
|
});
|
|
|
|
// check whether custom fields exist in responses model
|
|
if (!Utils._.isEmpty(dialectSpecificFields)) {
|
|
rows.forEach(function (row, rowId, rows) {
|
|
Utils._.each(row, function (value, key) {
|
|
if (isDialectSpecificField(key))
|
|
rows[rowId][key] =
|
|
parseDialectSpecificFields[dialectSpecificFields[key].type](value, { dataType: dialectSpecificFields[key].dataType });
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
return self.handleSelectQuery(rows);
|
|
} else if (QueryTypes.BULKDELETE === self.options.type) {
|
|
return parseInt(result.rowCount, 10);
|
|
} else if (self.isUpsertQuery()) {
|
|
return rows[0].sequelize_upsert;
|
|
} else if (self.isInsertQuery() || self.isUpdateQuery()) {
|
|
if (self.instance && self.instance.dataValues) {
|
|
if (self.model) {
|
|
dialectSpecificFields = dialectSpecificFieldDatatypeMap({
|
|
callee: self.model,
|
|
types: dialectSpecificTypes
|
|
});
|
|
|
|
// check whether custom fields exist in responses model
|
|
if (!Utils._.isEmpty(dialectSpecificFields)) {
|
|
Utils._.each(rows[0], function (value, key) {
|
|
if (isDialectSpecificField(key))
|
|
rows[0][key] =
|
|
parseDialectSpecificFields[dialectSpecificFields[key].type](value, { dataType: dialectSpecificFields[key].dataType });
|
|
});
|
|
}
|
|
}
|
|
|
|
for (var key in rows[0]) {
|
|
if (rows[0].hasOwnProperty(key)) {
|
|
var record = rows[0][key];
|
|
|
|
var attr = Utils._.find(self.model.rawAttributes, function (attribute) {
|
|
return attribute.fieldName === key || attribute.field === key;
|
|
});
|
|
|
|
self.instance.dataValues[attr && attr.fieldName || key] = record;
|
|
}
|
|
}
|
|
}
|
|
|
|
return self.instance || (rows && ((self.options.plain && rows[0]) || rows)) || undefined;
|
|
} else if (self.isVersionQuery()) {
|
|
return results[0].version;
|
|
} else if (self.isRawQuery()) {
|
|
return [rows, result];
|
|
} else {
|
|
return results;
|
|
}
|
|
});
|
|
|
|
return promise;
|
|
};
|
|
|
|
Query.prototype.formatError = function (err) {
|
|
var match
|
|
, table
|
|
, index
|
|
, fields
|
|
, errors
|
|
, message
|
|
, self = this;
|
|
|
|
var code = err.code || err.sqlState
|
|
, errMessage = err.message || err.messagePrimary
|
|
, errDetail = err.detail || err.messageDetail;
|
|
|
|
switch (code) {
|
|
case '23503':
|
|
index = errMessage.match(/violates foreign key constraint \"(.+?)\"/);
|
|
index = index ? index[1] : undefined;
|
|
table = errMessage.match(/on table \"(.+?)\"/);
|
|
table = table ? table[1] : undefined;
|
|
|
|
return new sequelizeErrors.ForeignKeyConstraintError({
|
|
fields: null,
|
|
index: index,
|
|
table: table,
|
|
parent: err
|
|
});
|
|
case '23505':
|
|
// there are multiple different formats of error messages for this error code
|
|
// this regex should check at least two
|
|
match = errDetail.match(/Key \((.*?)\)=\((.*?)\)/);
|
|
|
|
if (match) {
|
|
fields = Utils._.zipObject(match[1].split(', '), match[2].split(', '));
|
|
errors = [];
|
|
message = 'Validation error';
|
|
|
|
Utils._.forOwn(fields, function(value, field) {
|
|
errors.push(new sequelizeErrors.ValidationErrorItem(
|
|
self.getUniqueConstraintErrorMessage(field),
|
|
'unique violation', field, value));
|
|
});
|
|
|
|
if (this.model && this.model.uniqueKeys) {
|
|
Utils._.forOwn(this.model.uniqueKeys, function(constraint) {
|
|
if (Utils._.isEqual(constraint.fields, Object.keys(fields)) && !!constraint.msg) {
|
|
message = constraint.msg;
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
return new sequelizeErrors.UniqueConstraintError({
|
|
message: message,
|
|
errors: errors,
|
|
parent: err,
|
|
fields: fields
|
|
});
|
|
} else {
|
|
return new sequelizeErrors.UniqueConstraintError({
|
|
message: errMessage,
|
|
parent: err
|
|
});
|
|
}
|
|
|
|
break;
|
|
case '23P01':
|
|
match = errDetail.match(/Key \((.*?)\)=\((.*?)\)/);
|
|
|
|
if (match) {
|
|
fields = Utils._.zipObject(match[1].split(', '), match[2].split(', '));
|
|
}
|
|
message = 'Exclusion constraint error';
|
|
|
|
return new sequelizeErrors.ExclusionConstraintError({
|
|
message: message,
|
|
constraint: err.constraint,
|
|
fields: fields,
|
|
table: err.table,
|
|
parent: err
|
|
});
|
|
|
|
default:
|
|
return new sequelizeErrors.DatabaseError(err);
|
|
}
|
|
};
|
|
|
|
Query.prototype.isForeignKeysQuery = function() {
|
|
return /SELECT conname as constraint_name, pg_catalog\.pg_get_constraintdef\(r\.oid, true\) as condef FROM pg_catalog\.pg_constraint r WHERE r\.conrelid = \(SELECT oid FROM pg_class WHERE relname = '.*' LIMIT 1\) AND r\.contype = 'f' ORDER BY 1;/.test(this.sql);
|
|
};
|
|
|
|
Query.prototype.getInsertIdField = function() {
|
|
return 'id';
|
|
};
|
|
|
|
module.exports = Query;
|