123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- 'use strict';
- const _ = require('lodash');
- const Utils = require('../../utils');
- const AbstractQuery = require('../abstract/query');
- const QueryTypes = require('../../query-types');
- const sequelizeErrors = require('../../errors');
- const parserStore = require('../parserStore')('sqlite');
- const { logger } = require('../../utils/logger');
- const debug = logger.debugContext('sql:sqlite');
- class Query extends AbstractQuery {
- getInsertIdField() {
- return 'lastID';
- }
- /**
- * rewrite query with parameters.
- *
- * @param {string} sql
- * @param {Array|object} values
- * @param {string} dialect
- * @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;
- }
- _handleQueryResponse(metaData, columnTypes, err, results) {
- if (err) {
- err.sql = this.sql;
- throw this.formatError(err);
- }
- let result = this.instance;
- // add the inserted row id to the instance
- if (this.isInsertQuery(results, metaData) || this.isUpsertQuery()) {
- this.handleInsertQuery(results, metaData);
- if (!this.instance) {
- // handle bulkCreate AI primary key
- if (
- metaData.constructor.name === 'Statement'
- && this.model
- && this.model.autoIncrementAttribute
- && this.model.autoIncrementAttribute === this.model.primaryKeyAttribute
- && this.model.rawAttributes[this.model.primaryKeyAttribute]
- ) {
- const startId = metaData[this.getInsertIdField()] - metaData.changes + 1;
- result = [];
- for (let i = startId; i < startId + metaData.changes; i++) {
- result.push({ [this.model.rawAttributes[this.model.primaryKeyAttribute].field]: i });
- }
- } else {
- result = metaData[this.getInsertIdField()];
- }
- }
- }
- if (this.isShowTablesQuery()) {
- return results.map(row => row.name);
- }
- if (this.isShowConstraintsQuery()) {
- result = results;
- if (results && results[0] && results[0].sql) {
- result = this.parseConstraintsFromSql(results[0].sql);
- }
- return result;
- }
- if (this.isSelectQuery()) {
- if (this.options.raw) {
- return this.handleSelectQuery(results);
- }
- // This is a map of prefix strings to models, e.g. user.projects -> Project model
- const prefixes = this._collectModels(this.options.include);
- results = results.map(result => {
- return _.mapValues(result, (value, name) => {
- let model;
- if (name.includes('.')) {
- const lastind = name.lastIndexOf('.');
- model = prefixes[name.substr(0, lastind)];
- name = name.substr(lastind + 1);
- } else {
- model = this.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 Object.prototype.hasOwnProperty.call(tableTypes, name)
- ? this.applyParsers(tableTypes[name], value)
- : value;
- });
- });
- return this.handleSelectQuery(results);
- }
- if (this.isShowOrDescribeQuery()) {
- return results;
- }
- if (this.sql.includes('PRAGMA INDEX_LIST')) {
- return this.handleShowIndexesQuery(results);
- }
- if (this.sql.includes('PRAGMA INDEX_INFO')) {
- return results;
- }
- if (this.sql.includes('PRAGMA TABLE_INFO')) {
- // 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, '');
- }
- }
- return result;
- }
- if (this.sql.includes('PRAGMA foreign_keys;')) {
- return results[0];
- }
- if (this.sql.includes('PRAGMA foreign_keys')) {
- return results;
- }
- if (this.sql.includes('PRAGMA foreign_key_list')) {
- return results;
- }
- if ([QueryTypes.BULKUPDATE, QueryTypes.BULKDELETE].includes(this.options.type)) {
- return metaData.changes;
- }
- if (this.options.type === QueryTypes.VERSION) {
- return results[0].version;
- }
- if (this.options.type === QueryTypes.RAW) {
- return [results, metaData];
- }
- if (this.isUpsertQuery()) {
- return [result, null];
- }
- if (this.isUpdateQuery() || this.isInsertQuery()) {
- return [result, metaData.changes];
- }
- return result;
- }
- async run(sql, parameters) {
- const conn = this.connection;
- this.sql = sql;
- const method = this.getDatabaseMethod();
- const complete = this._logQuery(sql, debug, parameters);
- return new Promise((resolve, reject) => conn.serialize(async () => {
- const columnTypes = {};
- const executeSql = () => {
- if (sql.startsWith('-- ')) {
- return resolve();
- }
- const query = this;
- // cannot use arrow function here because the function is bound to the statement
- function afterExecute(executionError, results) {
- try {
- complete();
- // `this` is passed from sqlite, we have no control over this.
- // eslint-disable-next-line no-invalid-this
- resolve(query._handleQueryResponse(this, columnTypes, executionError, results));
- return;
- } catch (error) {
- reject(error);
- }
- }
- if (!parameters) parameters = [];
- conn[method](sql, parameters, afterExecute);
- 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 = tableNames.filter(tableName => !(tableName in columnTypes) && tableName !== 'sqlite_master');
- if (!tableNames.length) {
- return executeSql();
- }
- await Promise.all(tableNames.map(tableName =>
- new Promise(resolve => {
- tableName = tableName.replace(/`/g, '');
- columnTypes[tableName] = {};
- conn.all(`PRAGMA table_info(\`${tableName}\`)`, (err, results) => {
- if (!err) {
- for (const result of results) {
- columnTypes[tableName][result.name] = result.type;
- }
- }
- resolve();
- });
- })));
- }
- 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.includes('REFERENCES')) {
- //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.includes('(')) {
- // 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': {
- if (err.message.includes('FOREIGN KEY constraint failed')) {
- return new sequelizeErrors.ForeignKeyConstraintError({
- parent: err
- });
- }
- let fields = [];
- // Sqlite pre 2.2 behavior - Error: SQLITE_CONSTRAINT: columns x, y are not unique
- let 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);
- }
- }
- async handleShowIndexesQuery(data) {
- // Sqlite returns indexes so the one that was defined last is returned first. Lets reverse that!
- return Promise.all(data.reverse().map(async item => {
- item.fields = [];
- item.primary = false;
- item.unique = !!item.unique;
- item.constraintName = item.name;
- const columns = await this.run(`PRAGMA INDEX_INFO(\`${item.name}\`)`);
- for (const column of columns) {
- item.fields[column.seqno] = {
- attribute: column.name,
- length: undefined,
- order: undefined
- };
- }
- return item;
- }));
- }
- getDatabaseMethod() {
- if (this.isInsertQuery() || this.isUpdateQuery() || this.isUpsertQuery() || this.isBulkUpdateQuery() || this.sql.toLowerCase().includes('CREATE TEMPORARY TABLE'.toLowerCase()) || this.options.type === QueryTypes.BULKDELETE) {
- return 'run';
- }
- return 'all';
- }
- }
- module.exports = Query;
- module.exports.Query = Query;
- module.exports.default = Query;
|