init source
This commit is contained in:
+92
@@ -0,0 +1,92 @@
|
||||
'use strict';
|
||||
|
||||
const AbstractConnectionManager = require('../abstract/connection-manager');
|
||||
const Promise = require('../../promise');
|
||||
const { logger } = require('../../utils/logger');
|
||||
const debug = logger.debugContext('connection:sqlite');
|
||||
const dataTypes = require('../../data-types').sqlite;
|
||||
const sequelizeErrors = require('../../errors');
|
||||
const parserStore = require('../parserStore')('sqlite');
|
||||
|
||||
class ConnectionManager extends AbstractConnectionManager {
|
||||
constructor(dialect, sequelize) {
|
||||
super(dialect, sequelize);
|
||||
|
||||
// We attempt to parse file location from a connection uri
|
||||
// but we shouldn't match sequelize default host.
|
||||
if (this.sequelize.options.host === 'localhost') {
|
||||
delete this.sequelize.options.host;
|
||||
}
|
||||
|
||||
this.connections = {};
|
||||
this.lib = this._loadDialectModule('sqlite3').verbose();
|
||||
this.refreshTypeParser(dataTypes);
|
||||
}
|
||||
|
||||
_onProcessExit() {
|
||||
const promises = Object.getOwnPropertyNames(this.connections)
|
||||
.map(connection => Promise.fromCallback(callback => this.connections[connection].close(callback)));
|
||||
|
||||
return Promise
|
||||
.all(promises)
|
||||
.then(() => super._onProcessExit.call(this));
|
||||
}
|
||||
|
||||
// Expose this as a method so that the parsing may be updated when the user has added additional, custom types
|
||||
_refreshTypeParser(dataType) {
|
||||
parserStore.refresh(dataType);
|
||||
}
|
||||
|
||||
_clearTypeParser() {
|
||||
parserStore.clear();
|
||||
}
|
||||
|
||||
getConnection(options) {
|
||||
options = options || {};
|
||||
options.uuid = options.uuid || 'default';
|
||||
options.inMemory = (this.sequelize.options.storage || this.sequelize.options.host || ':memory:') === ':memory:' ? 1 : 0;
|
||||
|
||||
const dialectOptions = this.sequelize.options.dialectOptions;
|
||||
options.readWriteMode = dialectOptions && dialectOptions.mode;
|
||||
|
||||
if (this.connections[options.inMemory || options.uuid]) {
|
||||
return Promise.resolve(this.connections[options.inMemory || options.uuid]);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connections[options.inMemory || options.uuid] = new this.lib.Database(
|
||||
this.sequelize.options.storage || this.sequelize.options.host || ':memory:',
|
||||
options.readWriteMode || this.lib.OPEN_READWRITE | this.lib.OPEN_CREATE, // default mode
|
||||
err => {
|
||||
if (err) return reject(new sequelizeErrors.ConnectionError(err));
|
||||
debug(`connection acquired ${options.uuid}`);
|
||||
resolve(this.connections[options.inMemory || options.uuid]);
|
||||
}
|
||||
);
|
||||
}).tap(connection => {
|
||||
if (this.sequelize.config.password) {
|
||||
// Make it possible to define and use password for sqlite encryption plugin like sqlcipher
|
||||
connection.run(`PRAGMA KEY=${this.sequelize.escape(this.sequelize.config.password)}`);
|
||||
}
|
||||
if (this.sequelize.options.foreignKeys !== false) {
|
||||
// Make it possible to define and use foreign key constraints unless
|
||||
// explicitly disallowed. It's still opt-in per relation
|
||||
connection.run('PRAGMA FOREIGN_KEYS=ON');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
releaseConnection(connection, force) {
|
||||
if (connection.filename === ':memory:' && force !== true) return;
|
||||
|
||||
if (connection.uuid) {
|
||||
connection.close();
|
||||
debug(`connection released ${connection.uuid}`);
|
||||
delete this.connections[connection.uuid];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConnectionManager;
|
||||
module.exports.ConnectionManager = ConnectionManager;
|
||||
module.exports.default = ConnectionManager;
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = BaseTypes => {
|
||||
const warn = BaseTypes.ABSTRACT.warn.bind(undefined, 'https://www.sqlite.org/datatype3.html');
|
||||
|
||||
/**
|
||||
* Removes unsupported SQLite options, i.e., UNSIGNED and ZEROFILL, for the integer data types.
|
||||
*
|
||||
* @param {Object} dataType The base integer data type.
|
||||
* @private
|
||||
*/
|
||||
function removeUnsupportedIntegerOptions(dataType) {
|
||||
if (dataType._zerofill || dataType._unsigned) {
|
||||
warn(`SQLite does not support '${dataType.key}' with UNSIGNED or ZEROFILL. Plain '${dataType.key}' will be used instead.`);
|
||||
dataType._unsigned = undefined;
|
||||
dataType._zerofill = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://sqlite.org/datatype3.html
|
||||
*/
|
||||
|
||||
BaseTypes.DATE.types.sqlite = ['DATETIME'];
|
||||
BaseTypes.STRING.types.sqlite = ['VARCHAR', 'VARCHAR BINARY'];
|
||||
BaseTypes.CHAR.types.sqlite = ['CHAR', 'CHAR BINARY'];
|
||||
BaseTypes.TEXT.types.sqlite = ['TEXT'];
|
||||
BaseTypes.TINYINT.types.sqlite = ['TINYINT'];
|
||||
BaseTypes.SMALLINT.types.sqlite = ['SMALLINT'];
|
||||
BaseTypes.MEDIUMINT.types.sqlite = ['MEDIUMINT'];
|
||||
BaseTypes.INTEGER.types.sqlite = ['INTEGER'];
|
||||
BaseTypes.BIGINT.types.sqlite = ['BIGINT'];
|
||||
BaseTypes.FLOAT.types.sqlite = ['FLOAT'];
|
||||
BaseTypes.TIME.types.sqlite = ['TIME'];
|
||||
BaseTypes.DATEONLY.types.sqlite = ['DATE'];
|
||||
BaseTypes.BOOLEAN.types.sqlite = ['TINYINT'];
|
||||
BaseTypes.BLOB.types.sqlite = ['TINYBLOB', 'BLOB', 'LONGBLOB'];
|
||||
BaseTypes.DECIMAL.types.sqlite = ['DECIMAL'];
|
||||
BaseTypes.UUID.types.sqlite = ['UUID'];
|
||||
BaseTypes.ENUM.types.sqlite = false;
|
||||
BaseTypes.REAL.types.sqlite = ['REAL'];
|
||||
BaseTypes.DOUBLE.types.sqlite = ['DOUBLE PRECISION'];
|
||||
BaseTypes.GEOMETRY.types.sqlite = false;
|
||||
BaseTypes.JSON.types.sqlite = ['JSON', 'JSONB'];
|
||||
|
||||
class JSONTYPE extends BaseTypes.JSON {
|
||||
static parse(data) {
|
||||
return JSON.parse(data);
|
||||
}
|
||||
}
|
||||
|
||||
class DATE extends BaseTypes.DATE {
|
||||
static parse(date, options) {
|
||||
if (!date.includes('+')) {
|
||||
// For backwards compat. Dates inserted by sequelize < 2.0dev12 will not have a timestamp set
|
||||
return new Date(date + options.timezone);
|
||||
}
|
||||
return new Date(date); // We already have a timezone stored in the string
|
||||
}
|
||||
}
|
||||
|
||||
class DATEONLY extends BaseTypes.DATEONLY {
|
||||
static parse(date) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
class STRING extends BaseTypes.STRING {
|
||||
toSql() {
|
||||
if (this._binary) {
|
||||
return `VARCHAR BINARY(${this._length})`;
|
||||
}
|
||||
return super.toSql(this);
|
||||
}
|
||||
}
|
||||
|
||||
class TEXT extends BaseTypes.TEXT {
|
||||
toSql() {
|
||||
if (this._length) {
|
||||
warn('SQLite does not support TEXT with options. Plain `TEXT` will be used instead.');
|
||||
this._length = undefined;
|
||||
}
|
||||
return 'TEXT';
|
||||
}
|
||||
}
|
||||
|
||||
class CITEXT extends BaseTypes.CITEXT {
|
||||
toSql() {
|
||||
return 'TEXT COLLATE NOCASE';
|
||||
}
|
||||
}
|
||||
|
||||
class CHAR extends BaseTypes.CHAR {
|
||||
toSql() {
|
||||
if (this._binary) {
|
||||
return `CHAR BINARY(${this._length})`;
|
||||
}
|
||||
return super.toSql();
|
||||
}
|
||||
}
|
||||
|
||||
class NUMBER extends BaseTypes.NUMBER {
|
||||
toSql() {
|
||||
let result = this.key;
|
||||
if (this._unsigned) {
|
||||
result += ' UNSIGNED';
|
||||
}
|
||||
if (this._zerofill) {
|
||||
result += ' ZEROFILL';
|
||||
}
|
||||
if (this._length) {
|
||||
result += `(${this._length}`;
|
||||
if (typeof this._decimals === 'number') {
|
||||
result += `,${this._decimals}`;
|
||||
}
|
||||
result += ')';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class TINYINT extends BaseTypes.TINYINT {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
|
||||
class SMALLINT extends BaseTypes.SMALLINT {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
|
||||
class MEDIUMINT extends BaseTypes.MEDIUMINT {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
|
||||
class INTEGER extends BaseTypes.INTEGER {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
|
||||
class BIGINT extends BaseTypes.BIGINT {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
|
||||
class FLOAT extends BaseTypes.FLOAT {
|
||||
}
|
||||
|
||||
class DOUBLE extends BaseTypes.DOUBLE {
|
||||
}
|
||||
|
||||
class REAL extends BaseTypes.REAL { }
|
||||
|
||||
function parseFloating(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
if (value === 'NaN') {
|
||||
return NaN;
|
||||
}
|
||||
if (value === 'Infinity') {
|
||||
return Infinity;
|
||||
}
|
||||
if (value === '-Infinity') {
|
||||
return -Infinity;
|
||||
}
|
||||
}
|
||||
for (const floating of [FLOAT, DOUBLE, REAL]) {
|
||||
floating.parse = parseFloating;
|
||||
}
|
||||
|
||||
|
||||
for (const num of [FLOAT, DOUBLE, REAL, TINYINT, SMALLINT, MEDIUMINT, INTEGER, BIGINT]) {
|
||||
num.prototype.toSql = NUMBER.prototype.toSql;
|
||||
}
|
||||
|
||||
class ENUM extends BaseTypes.ENUM {
|
||||
toSql() {
|
||||
return 'TEXT';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
DATE,
|
||||
DATEONLY,
|
||||
STRING,
|
||||
CHAR,
|
||||
NUMBER,
|
||||
FLOAT,
|
||||
REAL,
|
||||
'DOUBLE PRECISION': DOUBLE,
|
||||
TINYINT,
|
||||
SMALLINT,
|
||||
MEDIUMINT,
|
||||
INTEGER,
|
||||
BIGINT,
|
||||
TEXT,
|
||||
ENUM,
|
||||
JSON: JSONTYPE,
|
||||
CITEXT
|
||||
};
|
||||
};
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const AbstractDialect = require('../abstract');
|
||||
const ConnectionManager = require('./connection-manager');
|
||||
const Query = require('./query');
|
||||
const QueryGenerator = require('./query-generator');
|
||||
const DataTypes = require('../../data-types').sqlite;
|
||||
|
||||
class SqliteDialect extends AbstractDialect {
|
||||
constructor(sequelize) {
|
||||
super();
|
||||
this.sequelize = sequelize;
|
||||
this.connectionManager = new ConnectionManager(this, sequelize);
|
||||
this.QueryGenerator = new QueryGenerator({
|
||||
_dialect: this,
|
||||
sequelize
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SqliteDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype.supports), {
|
||||
'DEFAULT': false,
|
||||
'DEFAULT VALUES': true,
|
||||
'UNION ALL': false,
|
||||
inserts: {
|
||||
ignoreDuplicates: ' OR IGNORE',
|
||||
updateOnDuplicate: ' ON CONFLICT DO UPDATE SET'
|
||||
},
|
||||
index: {
|
||||
using: false,
|
||||
where: true,
|
||||
functionBased: true
|
||||
},
|
||||
transactionOptions: {
|
||||
type: true
|
||||
},
|
||||
constraints: {
|
||||
addConstraint: false,
|
||||
dropConstraint: false
|
||||
},
|
||||
joinTableDependent: false,
|
||||
groupedLimit: false,
|
||||
JSON: true
|
||||
});
|
||||
|
||||
ConnectionManager.prototype.defaultVersion = '3.8.0';
|
||||
SqliteDialect.prototype.Query = Query;
|
||||
SqliteDialect.prototype.DataTypes = DataTypes;
|
||||
SqliteDialect.prototype.name = 'sqlite';
|
||||
SqliteDialect.prototype.TICK_CHAR = '`';
|
||||
SqliteDialect.prototype.TICK_CHAR_LEFT = SqliteDialect.prototype.TICK_CHAR;
|
||||
SqliteDialect.prototype.TICK_CHAR_RIGHT = SqliteDialect.prototype.TICK_CHAR;
|
||||
|
||||
module.exports = SqliteDialect;
|
||||
module.exports.SqliteDialect = SqliteDialect;
|
||||
module.exports.default = SqliteDialect;
|
||||
+480
@@ -0,0 +1,480 @@
|
||||
'use strict';
|
||||
|
||||
const Utils = require('../../utils');
|
||||
const Transaction = require('../../transaction');
|
||||
const _ = require('lodash');
|
||||
const MySqlQueryGenerator = require('../mysql/query-generator');
|
||||
const AbstractQueryGenerator = require('../abstract/query-generator');
|
||||
|
||||
class SQLiteQueryGenerator extends MySqlQueryGenerator {
|
||||
createSchema() {
|
||||
return "SELECT name FROM `sqlite_master` WHERE type='table' and name!='sqlite_sequence';";
|
||||
}
|
||||
|
||||
showSchemasQuery() {
|
||||
return "SELECT name FROM `sqlite_master` WHERE type='table' and name!='sqlite_sequence';";
|
||||
}
|
||||
|
||||
versionQuery() {
|
||||
return 'SELECT sqlite_version() as `version`';
|
||||
}
|
||||
|
||||
createTableQuery(tableName, attributes, options) {
|
||||
options = options || {};
|
||||
|
||||
const primaryKeys = [];
|
||||
const needsMultiplePrimaryKeys = _.values(attributes).filter(definition => definition.includes('PRIMARY KEY')).length > 1;
|
||||
const attrArray = [];
|
||||
|
||||
for (const attr in attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(attributes, attr)) {
|
||||
const dataType = attributes[attr];
|
||||
const containsAutoIncrement = dataType.includes('AUTOINCREMENT');
|
||||
|
||||
let dataTypeString = dataType;
|
||||
if (dataType.includes('PRIMARY KEY')) {
|
||||
if (dataType.includes('INT')) {
|
||||
// Only INTEGER is allowed for primary key, see https://github.com/sequelize/sequelize/issues/969 (no lenght, unsigned etc)
|
||||
dataTypeString = containsAutoIncrement ? 'INTEGER PRIMARY KEY AUTOINCREMENT' : 'INTEGER PRIMARY KEY';
|
||||
|
||||
if (dataType.includes(' REFERENCES')) {
|
||||
dataTypeString += dataType.substr(dataType.indexOf(' REFERENCES'));
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMultiplePrimaryKeys) {
|
||||
primaryKeys.push(attr);
|
||||
dataTypeString = dataType.replace('PRIMARY KEY', 'NOT NULL');
|
||||
}
|
||||
}
|
||||
attrArray.push(`${this.quoteIdentifier(attr)} ${dataTypeString}`);
|
||||
}
|
||||
}
|
||||
|
||||
const table = this.quoteTable(tableName);
|
||||
let attrStr = attrArray.join(', ');
|
||||
const pkString = primaryKeys.map(pk => this.quoteIdentifier(pk)).join(', ');
|
||||
|
||||
if (options.uniqueKeys) {
|
||||
_.each(options.uniqueKeys, columns => {
|
||||
if (columns.customIndex) {
|
||||
attrStr += `, UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pkString.length > 0) {
|
||||
attrStr += `, PRIMARY KEY (${pkString})`;
|
||||
}
|
||||
|
||||
const sql = `CREATE TABLE IF NOT EXISTS ${table} (${attrStr});`;
|
||||
return this.replaceBooleanDefaults(sql);
|
||||
}
|
||||
|
||||
booleanValue(value) {
|
||||
return value ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the statmement is json function or simple path
|
||||
*
|
||||
* @param {string} stmt The statement to validate
|
||||
* @returns {boolean} true if the given statement is json function
|
||||
* @throws {Error} throw if the statement looks like json function but has invalid token
|
||||
*/
|
||||
_checkValidJsonStatement(stmt) {
|
||||
if (typeof stmt !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://sqlite.org/json1.html
|
||||
const jsonFunctionRegex = /^\s*(json(?:_[a-z]+){0,2})\([^)]*\)/i;
|
||||
const tokenCaptureRegex = /^\s*((?:([`"'])(?:(?!\2).|\2{2})*\2)|[\w\d\s]+|[().,;+-])/i;
|
||||
|
||||
let currentIndex = 0;
|
||||
let openingBrackets = 0;
|
||||
let closingBrackets = 0;
|
||||
let hasJsonFunction = false;
|
||||
let hasInvalidToken = false;
|
||||
|
||||
while (currentIndex < stmt.length) {
|
||||
const string = stmt.substr(currentIndex);
|
||||
const functionMatches = jsonFunctionRegex.exec(string);
|
||||
if (functionMatches) {
|
||||
currentIndex += functionMatches[0].indexOf('(');
|
||||
hasJsonFunction = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const tokenMatches = tokenCaptureRegex.exec(string);
|
||||
if (tokenMatches) {
|
||||
const capturedToken = tokenMatches[1];
|
||||
if (capturedToken === '(') {
|
||||
openingBrackets++;
|
||||
} else if (capturedToken === ')') {
|
||||
closingBrackets++;
|
||||
} else if (capturedToken === ';') {
|
||||
hasInvalidToken = true;
|
||||
break;
|
||||
}
|
||||
currentIndex += tokenMatches[0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Check invalid json statement
|
||||
hasInvalidToken |= openingBrackets !== closingBrackets;
|
||||
if (hasJsonFunction && hasInvalidToken) {
|
||||
throw new Error(`Invalid json statement: ${stmt}`);
|
||||
}
|
||||
|
||||
// return true if the statement has valid json function
|
||||
return hasJsonFunction;
|
||||
}
|
||||
|
||||
//sqlite can't cast to datetime so we need to convert date values to their ISO strings
|
||||
_toJSONValue(value) {
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
if (Array.isArray(value) && value[0] instanceof Date) {
|
||||
return value.map(val => val.toISOString());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
handleSequelizeMethod(smth, tableName, factory, options, prepend) {
|
||||
if (smth instanceof Utils.Json) {
|
||||
return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
|
||||
}
|
||||
|
||||
if (smth instanceof Utils.Cast) {
|
||||
if (/timestamp/i.test(smth.type)) {
|
||||
smth.type = 'datetime';
|
||||
}
|
||||
}
|
||||
|
||||
return AbstractQueryGenerator.prototype.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend);
|
||||
}
|
||||
|
||||
addColumnQuery(table, key, dataType) {
|
||||
const attributes = {};
|
||||
attributes[key] = dataType;
|
||||
const fields = this.attributesToSQL(attributes, { context: 'addColumn' });
|
||||
const attribute = `${this.quoteIdentifier(key)} ${fields[key]}`;
|
||||
|
||||
const sql = `ALTER TABLE ${this.quoteTable(table)} ADD ${attribute};`;
|
||||
|
||||
return this.replaceBooleanDefaults(sql);
|
||||
}
|
||||
|
||||
showTablesQuery() {
|
||||
return 'SELECT name FROM `sqlite_master` WHERE type=\'table\' and name!=\'sqlite_sequence\';';
|
||||
}
|
||||
|
||||
upsertQuery(tableName, insertValues, updateValues, where, model, options) {
|
||||
options.ignoreDuplicates = true;
|
||||
|
||||
const bind = [];
|
||||
const bindParam = this.bindParam(bind);
|
||||
|
||||
const upsertOptions = _.defaults({ bindParam }, options);
|
||||
const insert = this.insertQuery(tableName, insertValues, model.rawAttributes, upsertOptions);
|
||||
const update = this.updateQuery(tableName, updateValues, where, upsertOptions, model.rawAttributes);
|
||||
|
||||
const query = `${insert.query} ${update.query}`;
|
||||
|
||||
return { query, bind };
|
||||
}
|
||||
|
||||
updateQuery(tableName, attrValueHash, where, options, attributes) {
|
||||
options = options || {};
|
||||
_.defaults(options, this.options);
|
||||
|
||||
attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, options.omitNull, options);
|
||||
|
||||
const modelAttributeMap = {};
|
||||
const values = [];
|
||||
const bind = [];
|
||||
const bindParam = options.bindParam || this.bindParam(bind);
|
||||
|
||||
if (attributes) {
|
||||
_.each(attributes, (attribute, key) => {
|
||||
modelAttributeMap[key] = attribute;
|
||||
if (attribute.field) {
|
||||
modelAttributeMap[attribute.field] = attribute;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const key in attrValueHash) {
|
||||
const value = attrValueHash[key];
|
||||
|
||||
if (value instanceof Utils.SequelizeMethod || options.bindParam === false) {
|
||||
values.push(`${this.quoteIdentifier(key)}=${this.escape(value, modelAttributeMap && modelAttributeMap[key] || undefined, { context: 'UPDATE' })}`);
|
||||
} else {
|
||||
values.push(`${this.quoteIdentifier(key)}=${this.format(value, modelAttributeMap && modelAttributeMap[key] || undefined, { context: 'UPDATE' }, bindParam)}`);
|
||||
}
|
||||
}
|
||||
|
||||
let query;
|
||||
const whereOptions = _.defaults({ bindParam }, options);
|
||||
|
||||
if (options.limit) {
|
||||
query = `UPDATE ${this.quoteTable(tableName)} SET ${values.join(',')} WHERE rowid IN (SELECT rowid FROM ${this.quoteTable(tableName)} ${this.whereQuery(where, whereOptions)} LIMIT ${this.escape(options.limit)})`;
|
||||
} else {
|
||||
query = `UPDATE ${this.quoteTable(tableName)} SET ${values.join(',')} ${this.whereQuery(where, whereOptions)}`;
|
||||
}
|
||||
|
||||
return { query, bind };
|
||||
}
|
||||
|
||||
truncateTableQuery(tableName, options = {}) {
|
||||
return [
|
||||
`DELETE FROM ${this.quoteTable(tableName)}`,
|
||||
options.restartIdentity ? `; DELETE FROM ${this.quoteTable('sqlite_sequence')} WHERE ${this.quoteIdentifier('name')} = ${Utils.addTicks(Utils.removeTicks(this.quoteTable(tableName), '`'), "'")};` : ''
|
||||
].join('');
|
||||
}
|
||||
|
||||
deleteQuery(tableName, where, options = {}, model) {
|
||||
_.defaults(options, this.options);
|
||||
|
||||
let whereClause = this.getWhereConditions(where, null, model, options);
|
||||
|
||||
if (whereClause) {
|
||||
whereClause = `WHERE ${whereClause}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
whereClause = `WHERE rowid IN (SELECT rowid FROM ${this.quoteTable(tableName)} ${whereClause} LIMIT ${this.escape(options.limit)})`;
|
||||
}
|
||||
|
||||
return `DELETE FROM ${this.quoteTable(tableName)} ${whereClause}`;
|
||||
}
|
||||
|
||||
attributesToSQL(attributes) {
|
||||
const result = {};
|
||||
|
||||
for (const name in attributes) {
|
||||
const dataType = attributes[name];
|
||||
const fieldName = dataType.field || name;
|
||||
|
||||
if (_.isObject(dataType)) {
|
||||
let sql = dataType.type.toString();
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(dataType, 'allowNull') && !dataType.allowNull) {
|
||||
sql += ' NOT NULL';
|
||||
}
|
||||
|
||||
if (Utils.defaultValueSchemable(dataType.defaultValue)) {
|
||||
// TODO thoroughly check that DataTypes.NOW will properly
|
||||
// get populated on all databases as DEFAULT value
|
||||
// i.e. mysql requires: DEFAULT CURRENT_TIMESTAMP
|
||||
sql += ` DEFAULT ${this.escape(dataType.defaultValue, dataType)}`;
|
||||
}
|
||||
|
||||
if (dataType.unique === true) {
|
||||
sql += ' UNIQUE';
|
||||
}
|
||||
|
||||
if (dataType.primaryKey) {
|
||||
sql += ' PRIMARY KEY';
|
||||
|
||||
if (dataType.autoIncrement) {
|
||||
sql += ' AUTOINCREMENT';
|
||||
}
|
||||
}
|
||||
|
||||
if (dataType.references) {
|
||||
const referencesTable = this.quoteTable(dataType.references.model);
|
||||
|
||||
let referencesKey;
|
||||
if (dataType.references.key) {
|
||||
referencesKey = this.quoteIdentifier(dataType.references.key);
|
||||
} else {
|
||||
referencesKey = this.quoteIdentifier('id');
|
||||
}
|
||||
|
||||
sql += ` REFERENCES ${referencesTable} (${referencesKey})`;
|
||||
|
||||
if (dataType.onDelete) {
|
||||
sql += ` ON DELETE ${dataType.onDelete.toUpperCase()}`;
|
||||
}
|
||||
|
||||
if (dataType.onUpdate) {
|
||||
sql += ` ON UPDATE ${dataType.onUpdate.toUpperCase()}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
result[fieldName] = sql;
|
||||
} else {
|
||||
result[fieldName] = dataType;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
showIndexesQuery(tableName) {
|
||||
return `PRAGMA INDEX_LIST(${this.quoteTable(tableName)})`;
|
||||
}
|
||||
|
||||
showConstraintsQuery(tableName, constraintName) {
|
||||
let sql = `SELECT sql FROM sqlite_master WHERE tbl_name='${tableName}'`;
|
||||
|
||||
if (constraintName) {
|
||||
sql += ` AND sql LIKE '%${constraintName}%'`;
|
||||
}
|
||||
|
||||
return `${sql};`;
|
||||
}
|
||||
|
||||
removeIndexQuery(tableName, indexNameOrAttributes) {
|
||||
let indexName = indexNameOrAttributes;
|
||||
|
||||
if (typeof indexName !== 'string') {
|
||||
indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`);
|
||||
}
|
||||
|
||||
return `DROP INDEX IF EXISTS ${this.quoteIdentifier(indexName)}`;
|
||||
}
|
||||
|
||||
describeTableQuery(tableName, schema, schemaDelimiter) {
|
||||
const table = {
|
||||
_schema: schema,
|
||||
_schemaDelimiter: schemaDelimiter,
|
||||
tableName
|
||||
};
|
||||
return `PRAGMA TABLE_INFO(${this.quoteTable(this.addSchema(table))});`;
|
||||
}
|
||||
|
||||
describeCreateTableQuery(tableName) {
|
||||
return `SELECT sql FROM sqlite_master WHERE tbl_name='${tableName}';`;
|
||||
}
|
||||
|
||||
removeColumnQuery(tableName, attributes) {
|
||||
|
||||
attributes = this.attributesToSQL(attributes);
|
||||
|
||||
let backupTableName;
|
||||
if (typeof tableName === 'object') {
|
||||
backupTableName = {
|
||||
tableName: `${tableName.tableName}_backup`,
|
||||
schema: tableName.schema
|
||||
};
|
||||
} else {
|
||||
backupTableName = `${tableName}_backup`;
|
||||
}
|
||||
|
||||
const quotedTableName = this.quoteTable(tableName);
|
||||
const quotedBackupTableName = this.quoteTable(backupTableName);
|
||||
const attributeNames = Object.keys(attributes).map(attr => this.quoteIdentifier(attr)).join(', ');
|
||||
|
||||
// Temporary table cannot work for foreign keys.
|
||||
return `${this.createTableQuery(backupTableName, attributes)
|
||||
}INSERT INTO ${quotedBackupTableName} SELECT ${attributeNames} FROM ${quotedTableName};`
|
||||
+ `DROP TABLE ${quotedTableName};${
|
||||
this.createTableQuery(tableName, attributes)
|
||||
}INSERT INTO ${quotedTableName} SELECT ${attributeNames} FROM ${quotedBackupTableName};`
|
||||
+ `DROP TABLE ${quotedBackupTableName};`;
|
||||
}
|
||||
|
||||
_alterConstraintQuery(tableName, attributes, createTableSql) {
|
||||
let backupTableName;
|
||||
|
||||
attributes = this.attributesToSQL(attributes);
|
||||
|
||||
if (typeof tableName === 'object') {
|
||||
backupTableName = {
|
||||
tableName: `${tableName.tableName}_backup`,
|
||||
schema: tableName.schema
|
||||
};
|
||||
} else {
|
||||
backupTableName = `${tableName}_backup`;
|
||||
}
|
||||
const quotedTableName = this.quoteTable(tableName);
|
||||
const quotedBackupTableName = this.quoteTable(backupTableName);
|
||||
const attributeNames = Object.keys(attributes).map(attr => this.quoteIdentifier(attr)).join(', ');
|
||||
|
||||
return `${createTableSql
|
||||
.replace(`CREATE TABLE ${quotedTableName}`, `CREATE TABLE ${quotedBackupTableName}`)
|
||||
.replace(`CREATE TABLE ${quotedTableName.replace(/`/g, '"')}`, `CREATE TABLE ${quotedBackupTableName}`)
|
||||
}INSERT INTO ${quotedBackupTableName} SELECT ${attributeNames} FROM ${quotedTableName};`
|
||||
+ `DROP TABLE ${quotedTableName};`
|
||||
+ `ALTER TABLE ${quotedBackupTableName} RENAME TO ${quotedTableName};`;
|
||||
}
|
||||
|
||||
renameColumnQuery(tableName, attrNameBefore, attrNameAfter, attributes) {
|
||||
|
||||
let backupTableName;
|
||||
|
||||
attributes = this.attributesToSQL(attributes);
|
||||
|
||||
if (typeof tableName === 'object') {
|
||||
backupTableName = {
|
||||
tableName: `${tableName.tableName}_backup`,
|
||||
schema: tableName.schema
|
||||
};
|
||||
} else {
|
||||
backupTableName = `${tableName}_backup`;
|
||||
}
|
||||
|
||||
const quotedTableName = this.quoteTable(tableName);
|
||||
const quotedBackupTableName = this.quoteTable(backupTableName);
|
||||
const attributeNamesImport = Object.keys(attributes).map(attr =>
|
||||
attrNameAfter === attr ? `${this.quoteIdentifier(attrNameBefore)} AS ${this.quoteIdentifier(attr)}` : this.quoteIdentifier(attr)
|
||||
).join(', ');
|
||||
const attributeNamesExport = Object.keys(attributes).map(attr => this.quoteIdentifier(attr)).join(', ');
|
||||
|
||||
return `${this.createTableQuery(backupTableName, attributes).replace('CREATE TABLE', 'CREATE TEMPORARY TABLE')
|
||||
}INSERT INTO ${quotedBackupTableName} SELECT ${attributeNamesImport} FROM ${quotedTableName};`
|
||||
+ `DROP TABLE ${quotedTableName};${
|
||||
this.createTableQuery(tableName, attributes)
|
||||
}INSERT INTO ${quotedTableName} SELECT ${attributeNamesExport} FROM ${quotedBackupTableName};`
|
||||
+ `DROP TABLE ${quotedBackupTableName};`;
|
||||
}
|
||||
|
||||
startTransactionQuery(transaction) {
|
||||
if (transaction.parent) {
|
||||
return `SAVEPOINT ${this.quoteIdentifier(transaction.name)};`;
|
||||
}
|
||||
|
||||
return `BEGIN ${transaction.options.type} TRANSACTION;`;
|
||||
}
|
||||
|
||||
setIsolationLevelQuery(value) {
|
||||
switch (value) {
|
||||
case Transaction.ISOLATION_LEVELS.REPEATABLE_READ:
|
||||
return '-- SQLite is not able to choose the isolation level REPEATABLE READ.';
|
||||
case Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED:
|
||||
return 'PRAGMA read_uncommitted = ON;';
|
||||
case Transaction.ISOLATION_LEVELS.READ_COMMITTED:
|
||||
return 'PRAGMA read_uncommitted = OFF;';
|
||||
case Transaction.ISOLATION_LEVELS.SERIALIZABLE:
|
||||
return '-- SQLite\'s default isolation level is SERIALIZABLE. Nothing to do.';
|
||||
default:
|
||||
throw new Error(`Unknown isolation level: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
replaceBooleanDefaults(sql) {
|
||||
return sql.replace(/DEFAULT '?false'?/g, 'DEFAULT 0').replace(/DEFAULT '?true'?/g, 'DEFAULT 1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQL query that returns all foreign keys of a table.
|
||||
*
|
||||
* @param {string} tableName The name of the table.
|
||||
* @returns {string} The generated sql query.
|
||||
* @private
|
||||
*/
|
||||
getForeignKeysQuery(tableName) {
|
||||
return `PRAGMA foreign_key_list(${tableName})`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SQLiteQueryGenerator;
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const Promise = require('../../promise');
|
||||
const sequelizeErrors = require('../../errors');
|
||||
const QueryTypes = require('../../query-types');
|
||||
|
||||
/**
|
||||
Returns an object that treats SQLite's inabilities to do certain queries.
|
||||
|
||||
@class QueryInterface
|
||||
@static
|
||||
@private
|
||||
*/
|
||||
|
||||
/**
|
||||
A wrapper that fixes SQLite's inability to remove columns from existing tables.
|
||||
It will create a backup of the table, drop the table afterwards and create a
|
||||
new table with the same name but without the obsolete column.
|
||||
|
||||
@param {QueryInterface} qi
|
||||
@param {string} tableName The name of the table.
|
||||
@param {string} attributeName The name of the attribute that we want to remove.
|
||||
@param {Object} options
|
||||
@param {boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries
|
||||
|
||||
@since 1.6.0
|
||||
@private
|
||||
*/
|
||||
function removeColumn(qi, tableName, attributeName, options) {
|
||||
options = options || {};
|
||||
|
||||
return qi.describeTable(tableName, options).then(fields => {
|
||||
delete fields[attributeName];
|
||||
|
||||
const sql = qi.QueryGenerator.removeColumnQuery(tableName, fields);
|
||||
const subQueries = sql.split(';').filter(q => q !== '');
|
||||
|
||||
return Promise.each(subQueries, subQuery => qi.sequelize.query(`${subQuery};`, Object.assign({ raw: true }, options)));
|
||||
});
|
||||
}
|
||||
exports.removeColumn = removeColumn;
|
||||
|
||||
/**
|
||||
A wrapper that fixes SQLite's inability to change columns from existing tables.
|
||||
It will create a backup of the table, drop the table afterwards and create a
|
||||
new table with the same name but with a modified version of the respective column.
|
||||
|
||||
@param {QueryInterface} qi
|
||||
@param {string} tableName The name of the table.
|
||||
@param {Object} attributes An object with the attribute's name as key and its options as value object.
|
||||
@param {Object} options
|
||||
@param {boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries
|
||||
|
||||
@since 1.6.0
|
||||
@private
|
||||
*/
|
||||
function changeColumn(qi, tableName, attributes, options) {
|
||||
const attributeName = Object.keys(attributes)[0];
|
||||
options = options || {};
|
||||
|
||||
return qi.describeTable(tableName, options).then(fields => {
|
||||
fields[attributeName] = attributes[attributeName];
|
||||
|
||||
const sql = qi.QueryGenerator.removeColumnQuery(tableName, fields);
|
||||
const subQueries = sql.split(';').filter(q => q !== '');
|
||||
|
||||
return Promise.each(subQueries, subQuery => qi.sequelize.query(`${subQuery};`, Object.assign({ raw: true }, options)));
|
||||
});
|
||||
}
|
||||
exports.changeColumn = changeColumn;
|
||||
|
||||
/**
|
||||
A wrapper that fixes SQLite's inability to rename columns from existing tables.
|
||||
It will create a backup of the table, drop the table afterwards and create a
|
||||
new table with the same name but with a renamed version of the respective column.
|
||||
|
||||
@param {QueryInterface} qi
|
||||
@param {string} tableName The name of the table.
|
||||
@param {string} attrNameBefore The name of the attribute before it was renamed.
|
||||
@param {string} attrNameAfter The name of the attribute after it was renamed.
|
||||
@param {Object} options
|
||||
@param {boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries
|
||||
|
||||
@since 1.6.0
|
||||
@private
|
||||
*/
|
||||
function renameColumn(qi, tableName, attrNameBefore, attrNameAfter, options) {
|
||||
options = options || {};
|
||||
|
||||
return qi.describeTable(tableName, options).then(fields => {
|
||||
fields[attrNameAfter] = _.clone(fields[attrNameBefore]);
|
||||
delete fields[attrNameBefore];
|
||||
|
||||
const sql = qi.QueryGenerator.renameColumnQuery(tableName, attrNameBefore, attrNameAfter, fields);
|
||||
const subQueries = sql.split(';').filter(q => q !== '');
|
||||
|
||||
return Promise.each(subQueries, subQuery => qi.sequelize.query(`${subQuery};`, Object.assign({ raw: true }, options)));
|
||||
});
|
||||
}
|
||||
exports.renameColumn = renameColumn;
|
||||
|
||||
/**
|
||||
* @param {QueryInterface} qi
|
||||
* @param {string} tableName
|
||||
* @param {string} constraintName
|
||||
* @param {Object} options
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function removeConstraint(qi, tableName, constraintName, options) {
|
||||
let createTableSql;
|
||||
|
||||
return qi.showConstraint(tableName, constraintName)
|
||||
.then(constraints => {
|
||||
// sqlite can't show only one constraint, so we find here the one to remove
|
||||
const constraint = constraints.find(constaint => constaint.constraintName === constraintName);
|
||||
|
||||
if (constraint) {
|
||||
createTableSql = constraint.sql;
|
||||
constraint.constraintName = qi.QueryGenerator.quoteIdentifier(constraint.constraintName);
|
||||
let constraintSnippet = `, CONSTRAINT ${constraint.constraintName} ${constraint.constraintType} ${constraint.constraintCondition}`;
|
||||
|
||||
if (constraint.constraintType === 'FOREIGN KEY') {
|
||||
const referenceTableName = qi.QueryGenerator.quoteTable(constraint.referenceTableName);
|
||||
constraint.referenceTableKeys = constraint.referenceTableKeys.map(columnName => qi.QueryGenerator.quoteIdentifier(columnName));
|
||||
const referenceTableKeys = constraint.referenceTableKeys.join(', ');
|
||||
constraintSnippet += ` REFERENCES ${referenceTableName} (${referenceTableKeys})`;
|
||||
constraintSnippet += ` ON UPDATE ${constraint.updateAction}`;
|
||||
constraintSnippet += ` ON DELETE ${constraint.deleteAction}`;
|
||||
}
|
||||
|
||||
createTableSql = createTableSql.replace(constraintSnippet, '');
|
||||
createTableSql += ';';
|
||||
|
||||
return qi.describeTable(tableName, options);
|
||||
}
|
||||
throw new sequelizeErrors.UnknownConstraintError({
|
||||
message: `Constraint ${constraintName} on table ${tableName} does not exist`,
|
||||
constraint: constraintName,
|
||||
table: tableName
|
||||
});
|
||||
})
|
||||
.then(fields => {
|
||||
const sql = qi.QueryGenerator._alterConstraintQuery(tableName, fields, createTableSql);
|
||||
const subQueries = sql.split(';').filter(q => q !== '');
|
||||
|
||||
return Promise.each(subQueries, subQuery => qi.sequelize.query(`${subQuery};`, Object.assign({ raw: true }, options)));
|
||||
});
|
||||
}
|
||||
exports.removeConstraint = removeConstraint;
|
||||
|
||||
/**
|
||||
* @param {QueryInterface} qi
|
||||
* @param {string} tableName
|
||||
* @param {Object} options
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function addConstraint(qi, tableName, options) {
|
||||
const constraintSnippet = qi.QueryGenerator.getConstraintSnippet(tableName, options);
|
||||
const describeCreateTableSql = qi.QueryGenerator.describeCreateTableQuery(tableName);
|
||||
let createTableSql;
|
||||
|
||||
return qi.sequelize.query(describeCreateTableSql, Object.assign({}, options, { type: QueryTypes.SELECT, raw: true }))
|
||||
.then(constraints => {
|
||||
const sql = constraints[0].sql;
|
||||
const index = sql.length - 1;
|
||||
//Replace ending ')' with constraint snippet - Simulates String.replaceAt
|
||||
//http://stackoverflow.com/questions/1431094
|
||||
createTableSql = `${sql.substr(0, index)}, ${constraintSnippet})${sql.substr(index + 1)};`;
|
||||
|
||||
return qi.describeTable(tableName, options);
|
||||
})
|
||||
.then(fields => {
|
||||
const sql = qi.QueryGenerator._alterConstraintQuery(tableName, fields, createTableSql);
|
||||
const subQueries = sql.split(';').filter(q => q !== '');
|
||||
|
||||
return Promise.each(subQueries, subQuery => qi.sequelize.query(`${subQuery};`, Object.assign({ raw: true }, options)));
|
||||
});
|
||||
}
|
||||
exports.addConstraint = addConstraint;
|
||||
|
||||
/**
|
||||
* @param {QueryInterface} qi
|
||||
* @param {string} tableName
|
||||
* @param {Object} options Query Options
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function getForeignKeyReferencesForTable(qi, tableName, options) {
|
||||
const database = qi.sequelize.config.database;
|
||||
const query = qi.QueryGenerator.getForeignKeysQuery(tableName, database);
|
||||
return qi.sequelize.query(query, options)
|
||||
.then(result => {
|
||||
return result.map(row => ({
|
||||
tableName,
|
||||
columnName: row.from,
|
||||
referencedTableName: row.table,
|
||||
referencedColumnName: row.to,
|
||||
tableCatalog: database,
|
||||
referencedTableCatalog: database
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
exports.getForeignKeyReferencesForTable = getForeignKeyReferencesForTable;
|
||||
+460
@@ -0,0 +1,460 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const Utils = require('../../utils');
|
||||
const Promise = require('../../promise');
|
||||
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.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.UPSERT) {
|
||||
return undefined;
|
||||
}
|
||||
if (this.options.type === QueryTypes.VERSION) {
|
||||
return results[0].version;
|
||||
}
|
||||
if (this.options.type === QueryTypes.RAW) {
|
||||
return [results, metaData];
|
||||
}
|
||||
if (this.isUpdateQuery() || this.isInsertQuery()) {
|
||||
return [result, metaData.changes];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
run(sql, parameters) {
|
||||
const conn = this.connection;
|
||||
this.sql = sql;
|
||||
const method = this.getDatabaseMethod();
|
||||
let complete;
|
||||
if (method === 'exec') {
|
||||
// exec does not support bind parameter
|
||||
sql = AbstractQuery.formatBindParameters(sql, this.options.bind, this.options.dialect || 'sqlite', { skipUnescape: true })[0];
|
||||
this.sql = sql;
|
||||
complete = this._logQuery(sql, debug);
|
||||
} else {
|
||||
complete = this._logQuery(sql, debug, parameters);
|
||||
}
|
||||
|
||||
|
||||
return new Promise(resolve => {
|
||||
const columnTypes = {};
|
||||
conn.serialize(() => {
|
||||
const executeSql = () => {
|
||||
if (sql.startsWith('-- ')) {
|
||||
return resolve();
|
||||
}
|
||||
resolve(new Promise((resolve, reject) => {
|
||||
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 (method === 'exec') {
|
||||
// exec does not support bind parameter
|
||||
conn[method](sql, afterExecute);
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
return Promise.map(tableNames, 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();
|
||||
});
|
||||
})
|
||||
).then(executeSql);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
handleShowIndexesQuery(data) {
|
||||
// Sqlite returns indexes so the one that was defined last is returned first. Lets reverse that!
|
||||
return 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
|
||||
}
|
||||
if (this.isInsertQuery() || this.isUpdateQuery() || 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;
|
||||
Reference in New Issue
Block a user