'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;