290 lines
9.3 KiB
JavaScript
290 lines
9.3 KiB
JavaScript
'use strict';
|
|
|
|
var Utils = require('../../utils')
|
|
, AbstractQuery = require('../abstract/query')
|
|
, sequelizeErrors = require('../../errors.js');
|
|
|
|
var Query = function(connection, sequelize, options) {
|
|
this.connection = connection;
|
|
this.instance = options.instance;
|
|
this.model = options.model;
|
|
this.sequelize = sequelize;
|
|
this.options = Utils._.extend({
|
|
logging: console.log,
|
|
plain: false,
|
|
raw: false
|
|
}, options || {});
|
|
|
|
this.checkLoggingOption();
|
|
};
|
|
|
|
Utils.inherit(Query, AbstractQuery);
|
|
|
|
Query.prototype.getInsertIdField = function() {
|
|
return 'id';
|
|
};
|
|
|
|
Query.prototype.run = function(sql) {
|
|
var self = this;
|
|
this.sql = sql;
|
|
|
|
this.sequelize.log('Executing (' + (this.connection.uuid || 'default') + '): ' + this.sql, this.options);
|
|
|
|
var promise = new Utils.Promise(function(resolve, reject) {
|
|
// TRANSACTION SUPPORT
|
|
if (Utils._.contains(self.sql, 'BEGIN TRANSACTION')) {
|
|
self.connection.beginTransaction(function(err) {
|
|
if (!!err) {
|
|
reject(self.formatError(err));
|
|
} else {
|
|
resolve(self.formatResults());
|
|
}
|
|
} /* name, isolation_level */);
|
|
} else if (Utils._.contains(self.sql, 'COMMIT TRANSACTION')) {
|
|
self.connection.commitTransaction(function(err) {
|
|
if (!!err) {
|
|
reject(self.formatError(err));
|
|
} else {
|
|
resolve(self.formatResults());
|
|
}
|
|
});
|
|
} else if (Utils._.contains(self.sql, 'ROLLBACK TRANSACTION')) {
|
|
self.connection.rollbackTransaction(function(err) {
|
|
if (!!err) {
|
|
reject(self.formatError(err));
|
|
} else {
|
|
resolve(self.formatResults());
|
|
}
|
|
});
|
|
} else {
|
|
// QUERY SUPPORT
|
|
var results = [];
|
|
|
|
var request = new self.connection.lib.Request(self.sql, function(err) {
|
|
if (err) {
|
|
err.sql = sql;
|
|
reject(self.formatError(err));
|
|
} else {
|
|
resolve(self.formatResults(results));
|
|
}
|
|
});
|
|
|
|
request.on('row', function(columns) {
|
|
var row = {};
|
|
columns.forEach(function(column) {
|
|
row[column.metadata.colName] = column.value;
|
|
});
|
|
|
|
results.push(row);
|
|
});
|
|
|
|
self.connection.execSql(request);
|
|
}
|
|
});
|
|
|
|
return promise;
|
|
};
|
|
|
|
/**
|
|
* High level function that handles the results of a query execution.
|
|
*
|
|
*
|
|
* Example:
|
|
* query.formatResults([
|
|
* {
|
|
* id: 1, // this is from the main table
|
|
* attr2: 'snafu', // this is from the main table
|
|
* Tasks.id: 1, // this is from the associated table
|
|
* Tasks.title: 'task' // this is from the associated table
|
|
* }
|
|
* ])
|
|
*
|
|
* @param {Array} data - The result of the query execution.
|
|
*/
|
|
Query.prototype.formatResults = function(data) {
|
|
var result = this.instance;
|
|
if (this.isInsertQuery(data)) {
|
|
this.handleInsertQuery(data);
|
|
|
|
if (!this.instance) {
|
|
if (this.options.plain) {
|
|
// NOTE: super contrived. This just passes the newly added query-interface
|
|
// test returning only the PK. There isn't a way in MSSQL to identify
|
|
// that a given return value is the PK, and we have no schema information
|
|
// because there was no calling Model.
|
|
var record = data[0];
|
|
result = record[Object.keys(record)[0]];
|
|
} else {
|
|
result = data;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.isShowTablesQuery()) {
|
|
result = this.handleShowTablesQuery(data);
|
|
} else if (this.isDescribeQuery()) {
|
|
result = {};
|
|
data.forEach(function(_result) {
|
|
if (_result.Default)
|
|
_result.Default = _result.Default.replace("('",'').replace("')",'').replace(/'/g,''); /* jshint ignore: line */
|
|
|
|
result[_result.Name] = {
|
|
type: _result.Type.toUpperCase(),
|
|
allowNull: (_result.IsNull === 'YES' ? true : false),
|
|
defaultValue: _result.Default
|
|
};
|
|
});
|
|
} else if (this.isShowIndexesQuery()) {
|
|
result = this.handleShowIndexesQuery(data);
|
|
} else if (this.isSelectQuery()) {
|
|
result = this.handleSelectQuery(data);
|
|
} else if (this.isCallQuery()) {
|
|
result = data[0];
|
|
} else if (this.isBulkUpdateQuery()) {
|
|
result = data.length;
|
|
} else if (this.isBulkDeleteQuery()){
|
|
result = data[0] && data[0].AFFECTEDROWS;
|
|
} else if (this.isVersionQuery()) {
|
|
result = data[0].version;
|
|
} else if (this.isForeignKeysQuery()) {
|
|
result = data;
|
|
} else if (this.isRawQuery()) {
|
|
// MSSQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
|
|
result = [data, data];
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
Query.prototype.handleShowTablesQuery = function(results) {
|
|
return results.map(function(resultSet) {
|
|
return {
|
|
tableName: resultSet.TABLE_NAME,
|
|
schema: resultSet.TABLE_SCHEMA
|
|
};
|
|
});
|
|
};
|
|
|
|
Query.prototype.formatError = function (err) {
|
|
var match;
|
|
match = err.message.match(/Violation of UNIQUE KEY constraint '((.|\s)*)'. Cannot insert duplicate key in object '.*'. The duplicate key value is \((.*)\)./);
|
|
match = match || err.message.match(/Cannot insert duplicate key row in object .* with unique index '(.*)'/);
|
|
if (match && match.length > 1) {
|
|
var fields = {}
|
|
, message = 'Validation error'
|
|
, uniqueKey = this.model.uniqueKeys[match[1]];
|
|
|
|
if (!!uniqueKey.msg) message = uniqueKey.msg;
|
|
if (!!match[2]) {
|
|
var values = match[2].split(',').map(Function.prototype.call, String.prototype.trim);
|
|
if (!!uniqueKey) {
|
|
fields = Utils._.zipObject(uniqueKey.fields, values);
|
|
} else {
|
|
fields[match[1]] = match[2];
|
|
}
|
|
}
|
|
|
|
var errors = [];
|
|
var self = this;
|
|
Utils._.forOwn(fields, function(value, field) {
|
|
errors.push(new sequelizeErrors.ValidationErrorItem(
|
|
self.getUniqueConstraintErrorMessage(field),
|
|
'unique violation', field, value));
|
|
});
|
|
|
|
return new sequelizeErrors.UniqueConstraintError({
|
|
message: message,
|
|
errors: errors,
|
|
parent: err,
|
|
fields: fields
|
|
});
|
|
}
|
|
|
|
match = err.message.match(/Failed on step '(.*)'.Could not create constraint. See previous errors./) ||
|
|
err.message.match(/The DELETE statement conflicted with the REFERENCE constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./) ||
|
|
err.message.match(/The INSERT statement conflicted with the FOREIGN KEY constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./) ||
|
|
err.message.match(/The UPDATE statement conflicted with the FOREIGN KEY constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./);
|
|
if (match && match.length > 0) {
|
|
return new sequelizeErrors.ForeignKeyConstraintError({
|
|
fields: null,
|
|
index: match[1],
|
|
parent: err
|
|
});
|
|
}
|
|
|
|
return new sequelizeErrors.DatabaseError(err);
|
|
};
|
|
|
|
Query.prototype.isShowOrDescribeQuery = function() {
|
|
var result = false;
|
|
|
|
result = result || (this.sql.toLowerCase().indexOf("select c.column_name as 'name', c.data_type as 'type', c.is_nullable as 'isnull'") === 0); /* jshint ignore: line */
|
|
result = result || (this.sql.toLowerCase().indexOf('select tablename = t.name, name = ind.name,') === 0);
|
|
result = result || (this.sql.toLowerCase().indexOf('exec sys.sp_helpindex @objname') === 0);
|
|
|
|
return result;
|
|
};
|
|
|
|
Query.prototype.isShowIndexesQuery = function () {
|
|
return this.sql.toLowerCase().indexOf('exec sys.sp_helpindex @objname') === 0;
|
|
};
|
|
|
|
Query.prototype.handleShowIndexesQuery = function (data) {
|
|
// Group by index name, and collect all fields
|
|
data = Utils._.foldl(data, function (acc, item) {
|
|
if (!(item.index_name in acc)) {
|
|
acc[item.index_name] = item;
|
|
item.fields = [];
|
|
}
|
|
|
|
Utils._.forEach(item.index_keys.split(','), function(column) {
|
|
var columnName = column.trim();
|
|
if (columnName.indexOf('(-)') !== -1) {
|
|
columnName = columnName.replace('(-)','');
|
|
}
|
|
|
|
acc[item.index_name].fields.push({
|
|
attribute: columnName,
|
|
length: undefined,
|
|
order: (column.indexOf('(-)') !== -1 ? 'DESC' : 'ASC'),
|
|
collate: undefined
|
|
});
|
|
});
|
|
delete item.index_keys;
|
|
return acc;
|
|
}, {});
|
|
|
|
return Utils._.map(data, function(item) {
|
|
return {
|
|
primary: (item.index_name.toLowerCase().indexOf('pk') === 0),
|
|
fields: item.fields,
|
|
name: item.index_name,
|
|
tableName: undefined,
|
|
unique: (item.index_description.toLowerCase().indexOf('unique') !== -1),
|
|
type: undefined,
|
|
};
|
|
});
|
|
};
|
|
|
|
Query.prototype.handleInsertQuery = function(results, metaData) {
|
|
if (this.instance) {
|
|
// add the inserted row id to the instance
|
|
var autoIncrementField = this.model.autoIncrementField
|
|
, autoIncrementFieldAlias = null
|
|
, id = null;
|
|
|
|
if (this.model.rawAttributes.hasOwnProperty(autoIncrementField) &&
|
|
this.model.rawAttributes[autoIncrementField].field !== undefined)
|
|
autoIncrementFieldAlias = this.model.rawAttributes[autoIncrementField].field ;
|
|
|
|
id = id || (results && results[0][this.getInsertIdField()]);
|
|
id = id || (metaData && metaData[this.getInsertIdField()]);
|
|
id = id || (results && results[0][autoIncrementField]);
|
|
id = id || (autoIncrementFieldAlias && results && results[0][autoIncrementFieldAlias]);
|
|
|
|
this.instance[autoIncrementField] = id;
|
|
}
|
|
};
|
|
|
|
module.exports = Query;
|