sequelize
Version:
Multi dialect ORM for Node.JS/io.js
386 lines (330 loc) • 13.4 kB
JavaScript
;
var Utils = require('../../utils')
, _ = require('lodash')
, Promise = require('../../promise')
, AbstractQuery = require('../abstract/query')
, QueryTypes = require('../../query-types')
, sequelizeErrors = require('../../errors.js')
, parserStore = require('../parserStore')('sqlite');
var Query = function(database, sequelize, options) {
this.database = database;
this.sequelize = sequelize;
this.instance = options.instance;
this.model = options.model;
this.options = _.extend({
logging: console.log,
plain: false,
raw: false
}, options || {});
this.checkLoggingOption();
};
Utils.inherit(Query, AbstractQuery);
Query.prototype.getInsertIdField = function() {
return 'lastID';
};
/**
* rewrite query with parameters
*/
Query.formatBindParameters = function(sql, values, dialect) {
var bindParam = [];
if (Array.isArray(values)) {
bindParam = {};
values.forEach(function(v, i) {
bindParam['$'+(i+1)] = v;
});
sql = AbstractQuery.formatBindParameters(sql, values, dialect, { skipValueReplace: true })[0];
} else {
bindParam = {};
if (typeof values === 'object') {
Object.keys(values).forEach(function(k) {
bindParam['$'+k] = values[k];
});
}
sql = AbstractQuery.formatBindParameters(sql, values, dialect, { skipValueReplace: true })[0];
}
return [sql, bindParam];
};
Query.prototype.$collectModels = function(include, prefix) {
var ret = {};
if (include) {
include.forEach(function (include) {
var key;
if (!prefix) {
key = include.as;
} else {
key = prefix + '.' + include.as;
}
ret[key] = include.model;
if (include.include) {
_.merge(ret, this.$collectModels(include.include, key));
}
}, this);
}
return ret;
};
Query.prototype.run = function(sql, parameters) {
var self = this
, promise;
this.sql = sql;
var method = self.getDatabaseMethod();
if (method === 'exec') {
// exec does not support bind parameter
sql = AbstractQuery.formatBindParameters(sql, self.options.bind, self.options.dialect, { skipUnescape: true })[0];
this.sql = sql;
}
//do we need benchmark for this query execution
var benchmark = this.sequelize.options.benchmark || this.options.benchmark;
if (benchmark) {
var queryBegin = Date.now();
} else {
this.sequelize.log('Executing (' + (this.database.uuid || 'default') + '): ' + this.sql, this.options);
}
promise = new Promise(function(resolve) {
var columnTypes = {};
self.database.serialize(function() {
var executeSql = function() {
if (self.sql.indexOf('-- ') === 0) {
return resolve();
} else {
resolve(new Promise(function(resolve, reject) {
var afterExecute = function(err, results) {
if (benchmark) {
self.sequelize.log('Executed (' + (self.database.uuid || 'default') + '): ' + self.sql, (Date.now() - queryBegin), self.options);
}
if (err) {
err.sql = self.sql;
reject(self.formatError(err));
} else {
var metaData = this
, result = self.instance;
// add the inserted row id to the instance
if (self.isInsertQuery(results, metaData)) {
self.handleInsertQuery(results, metaData);
if (!self.instance) {
result = metaData[self.getInsertIdField()];
}
}
if (self.sql.indexOf('sqlite_master') !== -1) {
result = results.map(function(resultSet) { return resultSet.name; });
} else if (self.isSelectQuery()) {
if (!self.options.raw) {
// This is a map of prefix strings to models, e.g. user.projects -> Project model
var prefixes = self.$collectModels(self.options.include);
results = results.map(function(result) {
return _.mapValues(result, function (value, name) {
var model;
if (name.indexOf('.') !== -1) {
var lastind = name.lastIndexOf('.');
model = prefixes[name.substr(0, lastind)];
name = name.substr(lastind + 1);
} else {
model = self.options.model;
}
var tableName = model.getTableName().toString().replace(/`/g, '')
, tableTypes = columnTypes[tableName] || {};
if (tableTypes && !(name in tableTypes)) {
// The column is aliased
_.forOwn(model.rawAttributes, function (attribute, key) {
if (name === key && attribute.field) {
name = attribute.field;
return false;
}
});
}
var type = tableTypes[name];
if (type) {
if (type.indexOf('(') !== -1) {
// Remove the lenght part
type = type.substr(0, type.indexOf('('));
}
type = type.replace('UNSIGNED', '').replace('ZEROFILL', '');
type = type.trim().toUpperCase();
var parse = parserStore.get(type);
if (value !== null && parse) {
return parse(value, { timezone: self.sequelize.options.timezone});
}
}
return value;
});
});
}
result = self.handleSelectQuery(results);
} else if (self.isShowOrDescribeQuery()) {
result = results;
} else if (self.sql.indexOf('PRAGMA INDEX_LIST') !== -1) {
result = self.handleShowIndexesQuery(results);
} else if (self.sql.indexOf('PRAGMA INDEX_INFO') !== -1) {
result = results;
} else if (self.sql.indexOf('PRAGMA TABLE_INFO') !== -1) {
// this is the sqlite way of getting the metadata of a table
result = {};
var defaultValue;
results.forEach(function(_result) {
if (_result.dflt_value === null) {
// Column schema omits any "DEFAULT ..."
defaultValue = undefined;
} else if (_result.dflt_value === 'NULL') {
// Column schema is a "DEFAULT NULL"
defaultValue = null;
} else {
defaultValue = _result.dflt_value;
}
result[_result.name] = {
type: _result.type,
allowNull: (_result.notnull === 0),
defaultValue: defaultValue,
primaryKey : (_result.pk === 1)
};
if (result[_result.name].type === 'TINYINT(1)') {
result[_result.name].defaultValue = { '0': false, '1': true }[result[_result.name].defaultValue];
}
if (typeof result[_result.name].defaultValue === 'string') {
result[_result.name].defaultValue = result[_result.name].defaultValue.replace(/'/g, '');
}
});
} else if (self.sql.indexOf('PRAGMA foreign_keys;') !== -1) {
result = results[0];
} else if (self.sql.indexOf('PRAGMA foreign_keys') !== -1) {
result = results;
} else if (self.sql.indexOf('PRAGMA foreign_key_list') !== -1) {
result = results;
} else if ([QueryTypes.BULKUPDATE, QueryTypes.BULKDELETE].indexOf(self.options.type) !== -1) {
result = metaData.changes;
} else if (self.options.type === QueryTypes.UPSERT) {
result = undefined;
} else if (self.options.type === QueryTypes.VERSION) {
result = results[0].version;
} else if (self.options.type === QueryTypes.RAW) {
result = [results, metaData];
}
resolve(result);
}
};
if (method === 'exec') {
// exec does not support bind parameter
self.database[method](self.sql, afterExecute);
} else {
if (!parameters) parameters = [];
self.database[method](self.sql, parameters, afterExecute);
}
}));
return null;
}
};
if ((self.getDatabaseMethod() === 'all')) {
var tableNames = [];
if (self.options && self.options.tableNames) {
tableNames = self.options.tableNames;
} else if (/FROM `(.*?)`/i.exec(self.sql)) {
tableNames.push(/FROM `(.*?)`/i.exec(self.sql)[1]);
}
// If we already have the metadata for the table, there's no need to ask for it again
tableNames = _.filter(tableNames, function (tableName) {
return !(tableName in columnTypes) && tableName !== 'sqlite_master';
});
if (!tableNames.length) {
return executeSql();
} else {
return Promise.map(tableNames, function(tableName) {
return new Promise(function(resolve) {
tableName = tableName.replace(/`/g, '');
columnTypes[tableName] = {};
self.database.all('PRAGMA table_info(`' + tableName + '`)', function(err, results) {
if (!err) {
results.forEach(function (result) {
columnTypes[tableName][result.name] = result.type;
});
}
resolve();
});
});
}).then(executeSql);
}
} else {
return executeSql();
}
});
});
return promise;
};
Query.prototype.formatError = function (err) {
var match;
switch (err.code) {
case 'SQLITE_CONSTRAINT':
match = err.message.match(/FOREIGN KEY constraint failed/);
if (match !== null) {
return new sequelizeErrors.ForeignKeyConstraintError({
parent :err
});
}
var fields = [];
// Sqlite pre 2.2 behavior - Error: SQLITE_CONSTRAINT: columns x, y are not unique
match = err.message.match(/columns (.*?) are/);
if (match !== null && match.length >= 2) {
fields = match[1].split(', ');
} else {
// Sqlite post 2.2 behavior - Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: table.x, table.y
match = err.message.match(/UNIQUE constraint failed: (.*)/);
if (match !== null && match.length >= 2) {
fields = match[1].split(', ').map(function (columnWithTable) {
return columnWithTable.split('.')[1];
});
}
}
var errors = []
, self = this
, message = 'Validation error';
fields.forEach(function(field) {
errors.push(new sequelizeErrors.ValidationErrorItem(
self.getUniqueConstraintErrorMessage(field),
'unique violation', field, self.instance && self.instance[field]));
});
if (this.model) {
_.forOwn(this.model.uniqueKeys, function(constraint) {
if (_.isEqual(constraint.fields, fields) && !!constraint.msg) {
message = constraint.msg;
return false;
}
});
}
return new sequelizeErrors.UniqueConstraintError({
message: message,
errors: errors,
parent: err,
fields: fields
});
case 'SQLITE_BUSY':
return new sequelizeErrors.TimeoutError(err);
default:
return new sequelizeErrors.DatabaseError(err);
}
};
Query.prototype.handleShowIndexesQuery = function (data) {
var self = this;
// Sqlite returns indexes so the one that was defined last is returned first. Lets reverse that!
return this.sequelize.Promise.map(data.reverse(), function (item) {
item.fields = [];
item.primary = false;
item.unique = !!item.unique;
return self.run('PRAGMA INDEX_INFO(`' + item.name + '`)').then(function (columns) {
columns.forEach(function (column) {
item.fields[column.seqno] = {
attribute: column.name,
length: undefined,
order: undefined,
};
});
return item;
});
});
};
Query.prototype.getDatabaseMethod = function() {
if (this.isUpsertQuery()) {
return 'exec'; // Needed to run multiple queries in one
} else if (this.isInsertQuery() || this.isUpdateQuery() || this.isBulkUpdateQuery() || (this.sql.toLowerCase().indexOf('CREATE TEMPORARY TABLE'.toLowerCase()) !== -1) || this.options.type === QueryTypes.BULKDELETE) {
return 'run';
} else {
return 'all';
}
};
module.exports = Query;