You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
470 lines
17 KiB
470 lines
17 KiB
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const Utils = require('../../utils');
|
|
const debug = Utils.getLogger().debugContext('sql:sqlite');
|
|
const Promise = require('../../promise');
|
|
const AbstractQuery = require('../abstract/query');
|
|
const QueryTypes = require('../../query-types');
|
|
const sequelizeErrors = require('../../errors.js');
|
|
const parserStore = require('../parserStore')('sqlite');
|
|
|
|
class Query extends AbstractQuery {
|
|
|
|
constructor(database, sequelize, options) {
|
|
super();
|
|
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();
|
|
}
|
|
|
|
getInsertIdField() {
|
|
return 'lastID';
|
|
}
|
|
|
|
/**
|
|
* rewrite query with parameters
|
|
* @private
|
|
*/
|
|
static formatBindParameters(sql, values, dialect) {
|
|
let bindParam;
|
|
if (Array.isArray(values)) {
|
|
bindParam = {};
|
|
values.forEach((v, i) => {
|
|
bindParam['$'+(i+1)] = v;
|
|
});
|
|
sql = AbstractQuery.formatBindParameters(sql, values, dialect, { skipValueReplace: true })[0];
|
|
} else {
|
|
bindParam = {};
|
|
if (typeof values === 'object') {
|
|
for (const k of Object.keys(values)) {
|
|
bindParam['$'+k] = values[k];
|
|
}
|
|
}
|
|
sql = AbstractQuery.formatBindParameters(sql, values, dialect, { skipValueReplace: true })[0];
|
|
}
|
|
return [sql, bindParam];
|
|
}
|
|
|
|
_collectModels(include, prefix) {
|
|
const ret = {};
|
|
|
|
if (include) {
|
|
for (const _include of include) {
|
|
let 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));
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
run(sql, parameters) {
|
|
this.sql = sql;
|
|
const method = this.getDatabaseMethod();
|
|
if (method === 'exec') {
|
|
// exec does not support bind parameter
|
|
sql = AbstractQuery.formatBindParameters(sql, this.options.bind, this.options.dialect, { skipUnescape: true })[0];
|
|
this.sql = sql;
|
|
}
|
|
|
|
//do we need benchmark for this query execution
|
|
const benchmark = this.sequelize.options.benchmark || this.options.benchmark;
|
|
|
|
let queryBegin;
|
|
if (benchmark) {
|
|
queryBegin = Date.now();
|
|
} else {
|
|
this.sequelize.log('Executing (' + (this.database.uuid || 'default') + '): ' + this.sql, this.options);
|
|
}
|
|
|
|
debug(`executing(${this.database.uuid || 'default'}) : ${this.sql}`);
|
|
|
|
return new Promise(resolve => {
|
|
const columnTypes = {};
|
|
this.database.serialize(() => {
|
|
const executeSql = () => {
|
|
if (this.sql.indexOf('-- ') === 0) {
|
|
return resolve();
|
|
} else {
|
|
resolve(new Promise((resolve, reject) => {
|
|
const query = this;
|
|
// cannot use arrow function here because the function is bound to the statement
|
|
function afterExecute(err, results) {
|
|
debug(`executed(${query.database.uuid || 'default'}) : ${query.sql}`);
|
|
|
|
if (benchmark) {
|
|
query.sequelize.log('Executed (' + (query.database.uuid || 'default') + '): ' + query.sql, Date.now() - queryBegin, query.options);
|
|
}
|
|
|
|
if (err) {
|
|
err.sql = query.sql;
|
|
reject(query.formatError(err));
|
|
} else {
|
|
const metaData = this;
|
|
let result = query.instance;
|
|
|
|
// add the inserted row id to the instance
|
|
if (query.isInsertQuery(results, metaData)) {
|
|
query.handleInsertQuery(results, metaData);
|
|
if (!query.instance) {
|
|
// handle bulkCreate AI primary key
|
|
if (
|
|
metaData.constructor.name === 'Statement'
|
|
&& query.model
|
|
&& query.model.autoIncrementAttribute
|
|
&& query.model.autoIncrementAttribute === query.model.primaryKeyAttribute
|
|
&& query.model.rawAttributes[query.model.primaryKeyAttribute]
|
|
) {
|
|
const startId = metaData[query.getInsertIdField()] - metaData.changes + 1;
|
|
result = [];
|
|
for (let i = startId; i < startId + metaData.changes; i++) {
|
|
result.push({ [query.model.rawAttributes[query.model.primaryKeyAttribute].field]: i });
|
|
}
|
|
} else {
|
|
result = metaData[query.getInsertIdField()];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (query.sql.indexOf('sqlite_master') !== -1) {
|
|
if (query.sql.indexOf('SELECT sql FROM sqlite_master WHERE tbl_name') !== -1) {
|
|
result = results;
|
|
if (result && result[0] && result[0].sql.indexOf('CONSTRAINT') !== -1) {
|
|
result = query.parseConstraintsFromSql(results[0].sql);
|
|
}
|
|
} else {
|
|
result = results.map(resultSet => resultSet.name);
|
|
}
|
|
} else if (query.isSelectQuery()) {
|
|
if (!query.options.raw) {
|
|
// This is a map of prefix strings to models, e.g. user.projects -> Project model
|
|
const prefixes = query._collectModels(query.options.include);
|
|
|
|
results = results.map(result => {
|
|
return _.mapValues(result, (value, name) => {
|
|
let model;
|
|
if (name.indexOf('.') !== -1) {
|
|
const lastind = name.lastIndexOf('.');
|
|
|
|
model = prefixes[name.substr(0, lastind)];
|
|
|
|
name = name.substr(lastind + 1);
|
|
} else {
|
|
model = query.options.model;
|
|
}
|
|
|
|
const tableName = model.getTableName().toString().replace(/`/g, '');
|
|
const tableTypes = columnTypes[tableName] || {};
|
|
|
|
if (tableTypes && !(name in tableTypes)) {
|
|
// The column is aliased
|
|
_.forOwn(model.rawAttributes, (attribute, key) => {
|
|
if (name === key && attribute.field) {
|
|
name = attribute.field;
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
return tableTypes.hasOwnProperty(name)
|
|
? query.applyParsers(tableTypes[name], value)
|
|
: value;
|
|
});
|
|
});
|
|
}
|
|
|
|
result = query.handleSelectQuery(results);
|
|
} else if (query.isShowOrDescribeQuery()) {
|
|
result = results;
|
|
} else if (query.sql.indexOf('PRAGMA INDEX_LIST') !== -1) {
|
|
result = query.handleShowIndexesQuery(results);
|
|
} else if (query.sql.indexOf('PRAGMA INDEX_INFO') !== -1) {
|
|
result = results;
|
|
} else if (query.sql.indexOf('PRAGMA TABLE_INFO') !== -1) {
|
|
// this is the sqlite way of getting the metadata of a table
|
|
result = {};
|
|
|
|
let defaultValue;
|
|
for (const _result of results) {
|
|
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,
|
|
primaryKey: _result.pk !== 0
|
|
};
|
|
|
|
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 (query.sql.indexOf('PRAGMA foreign_keys;') !== -1) {
|
|
result = results[0];
|
|
} else if (query.sql.indexOf('PRAGMA foreign_keys') !== -1) {
|
|
result = results;
|
|
} else if (query.sql.indexOf('PRAGMA foreign_key_list') !== -1) {
|
|
result = results;
|
|
} else if ([QueryTypes.BULKUPDATE, QueryTypes.BULKDELETE].indexOf(query.options.type) !== -1) {
|
|
result = metaData.changes;
|
|
} else if (query.options.type === QueryTypes.UPSERT) {
|
|
result = undefined;
|
|
} else if (query.options.type === QueryTypes.VERSION) {
|
|
result = results[0].version;
|
|
} else if (query.options.type === QueryTypes.RAW) {
|
|
result = [results, metaData];
|
|
} else if (query.isUpdateQuery() || query.isInsertQuery()) {
|
|
result = [result, metaData.changes];
|
|
}
|
|
|
|
resolve(result);
|
|
}
|
|
}
|
|
|
|
function wrappedAfterExecute() { // See #11862
|
|
try {
|
|
return afterExecute.apply(this, arguments);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
}
|
|
|
|
if (method === 'exec') {
|
|
// exec does not support bind parameter
|
|
this.database[method](this.sql, wrappedAfterExecute);
|
|
} else {
|
|
if (!parameters) parameters = [];
|
|
this.database[method](this.sql, parameters, wrappedAfterExecute);
|
|
}
|
|
}));
|
|
return null;
|
|
}
|
|
};
|
|
|
|
if (this.getDatabaseMethod() === 'all') {
|
|
let tableNames = [];
|
|
if (this.options && this.options.tableNames) {
|
|
tableNames = this.options.tableNames;
|
|
} else if (/FROM `(.*?)`/i.exec(this.sql)) {
|
|
tableNames.push(/FROM `(.*?)`/i.exec(this.sql)[1]);
|
|
}
|
|
|
|
// If we already have the metadata for the table, there's no need to ask for it again
|
|
tableNames = _.filter(tableNames, tableName => !(tableName in columnTypes) && tableName !== 'sqlite_master');
|
|
|
|
if (!tableNames.length) {
|
|
return executeSql();
|
|
} else {
|
|
return Promise.map(tableNames, tableName =>
|
|
new Promise(resolve => {
|
|
tableName = tableName.replace(/`/g, '');
|
|
columnTypes[tableName] = {};
|
|
|
|
this.database.all('PRAGMA table_info(`' + tableName + '`)', (err, results) => {
|
|
if (!err) {
|
|
for (const result of results) {
|
|
columnTypes[tableName][result.name] = result.type;
|
|
}
|
|
}
|
|
resolve();
|
|
});
|
|
})
|
|
).then(executeSql);
|
|
}
|
|
} else {
|
|
return executeSql();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
parseConstraintsFromSql(sql) {
|
|
let constraints = sql.split('CONSTRAINT ');
|
|
let referenceTableName, referenceTableKeys, updateAction, deleteAction;
|
|
constraints.splice(0, 1);
|
|
constraints = constraints.map(constraintSql => {
|
|
//Parse foreign key snippets
|
|
if (constraintSql.indexOf('REFERENCES') !== -1) {
|
|
//Parse out the constraint condition form sql string
|
|
updateAction = constraintSql.match(/ON UPDATE (CASCADE|SET NULL|RESTRICT|NO ACTION|SET DEFAULT){1}/);
|
|
deleteAction = constraintSql.match(/ON DELETE (CASCADE|SET NULL|RESTRICT|NO ACTION|SET DEFAULT){1}/);
|
|
|
|
if (updateAction) {
|
|
updateAction = updateAction[1];
|
|
}
|
|
|
|
if (deleteAction) {
|
|
deleteAction = deleteAction[1];
|
|
}
|
|
|
|
const referencesRegex = /REFERENCES.+\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/;
|
|
const referenceConditions = constraintSql.match(referencesRegex)[0].split(' ');
|
|
referenceTableName = Utils.removeTicks(referenceConditions[1]);
|
|
let columnNames = referenceConditions[2];
|
|
columnNames = columnNames.replace(/\(|\)/g, '').split(', ');
|
|
referenceTableKeys = columnNames.map(column => Utils.removeTicks(column));
|
|
}
|
|
|
|
const constraintCondition = constraintSql.match(/\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/)[0];
|
|
constraintSql = constraintSql.replace(/\(.+\)/, '');
|
|
const constraint = constraintSql.split(' ');
|
|
|
|
if (constraint[1] === 'PRIMARY' || constraint[1] === 'FOREIGN') {
|
|
constraint[1]+= ' KEY';
|
|
}
|
|
|
|
return {
|
|
constraintName: Utils.removeTicks(constraint[0]),
|
|
constraintType: constraint[1],
|
|
updateAction,
|
|
deleteAction,
|
|
sql: sql.replace(/\"/g, '\`'), //Sqlite returns double quotes for table name
|
|
constraintCondition,
|
|
referenceTableName,
|
|
referenceTableKeys
|
|
};
|
|
});
|
|
|
|
return constraints;
|
|
}
|
|
|
|
applyParsers(type, value) {
|
|
if (type.indexOf('(') !== -1) {
|
|
// Remove the length part
|
|
type = type.substr(0, type.indexOf('('));
|
|
}
|
|
type = type.replace('UNSIGNED', '').replace('ZEROFILL', '');
|
|
type = type.trim().toUpperCase();
|
|
const parse = parserStore.get(type);
|
|
|
|
if (value !== null && parse) {
|
|
return parse(value, { timezone: this.sequelize.options.timezone });
|
|
}
|
|
return value;
|
|
}
|
|
|
|
formatError(err) {
|
|
|
|
switch (err.code) {
|
|
case 'SQLITE_CONSTRAINT': {
|
|
let match = err.message.match(/FOREIGN KEY constraint failed/);
|
|
if (match !== null) {
|
|
return new sequelizeErrors.ForeignKeyConstraintError({
|
|
parent: err
|
|
});
|
|
}
|
|
|
|
let 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(columnWithTable => columnWithTable.split('.')[1]);
|
|
}
|
|
}
|
|
|
|
const errors = [];
|
|
let message = 'Validation error';
|
|
|
|
for (const field of fields) {
|
|
errors.push(new sequelizeErrors.ValidationErrorItem(
|
|
this.getUniqueConstraintErrorMessage(field),
|
|
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
|
|
field,
|
|
this.instance && this.instance[field],
|
|
this.instance,
|
|
'not_unique'
|
|
));
|
|
}
|
|
|
|
if (this.model) {
|
|
_.forOwn(this.model.uniqueKeys, constraint => {
|
|
if (_.isEqual(constraint.fields, fields) && !!constraint.msg) {
|
|
message = constraint.msg;
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
return new sequelizeErrors.UniqueConstraintError({message, errors, parent: err, fields});
|
|
}
|
|
case 'SQLITE_BUSY':
|
|
return new sequelizeErrors.TimeoutError(err);
|
|
|
|
default:
|
|
return new sequelizeErrors.DatabaseError(err);
|
|
}
|
|
}
|
|
|
|
handleShowIndexesQuery(data) {
|
|
|
|
// Sqlite returns indexes so the one that was defined last is returned first. Lets reverse that!
|
|
return this.sequelize.Promise.map(data.reverse(), item => {
|
|
item.fields = [];
|
|
item.primary = false;
|
|
item.unique = !!item.unique;
|
|
item.constraintName = item.name;
|
|
return this.run('PRAGMA INDEX_INFO(`' + item.name + '`)').then(columns => {
|
|
for (const column of columns) {
|
|
item.fields[column.seqno] = {
|
|
attribute: column.name,
|
|
length: undefined,
|
|
order: undefined
|
|
};
|
|
}
|
|
|
|
return item;
|
|
});
|
|
});
|
|
}
|
|
|
|
getDatabaseMethod() {
|
|
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;
|
|
module.exports.Query = Query;
|
|
module.exports.default = Query;
|
|
|