init source
This commit is contained in:
+346
@@ -0,0 +1,346 @@
|
||||
'use strict';
|
||||
|
||||
const { Pool, TimeoutError } = require('sequelize-pool');
|
||||
const _ = require('lodash');
|
||||
const semver = require('semver');
|
||||
const Promise = require('../../promise');
|
||||
const errors = require('../../errors');
|
||||
const { logger } = require('../../utils/logger');
|
||||
const debug = logger.debugContext('pool');
|
||||
|
||||
/**
|
||||
* Abstract Connection Manager
|
||||
*
|
||||
* Connection manager which handles pooling & replication.
|
||||
* Uses sequelize-pool for pooling
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
class ConnectionManager {
|
||||
constructor(dialect, sequelize) {
|
||||
const config = _.cloneDeep(sequelize.config);
|
||||
|
||||
this.sequelize = sequelize;
|
||||
this.config = config;
|
||||
this.dialect = dialect;
|
||||
this.versionPromise = null;
|
||||
this.dialectName = this.sequelize.options.dialect;
|
||||
|
||||
if (config.pool === false) {
|
||||
throw new Error('Support for pool:false was removed in v4.0');
|
||||
}
|
||||
|
||||
config.pool = _.defaults(config.pool || {}, {
|
||||
max: 5,
|
||||
min: 0,
|
||||
idle: 10000,
|
||||
acquire: 60000,
|
||||
evict: 1000,
|
||||
validate: this._validate.bind(this)
|
||||
});
|
||||
|
||||
this.initPools();
|
||||
}
|
||||
|
||||
refreshTypeParser(dataTypes) {
|
||||
_.each(dataTypes, dataType => {
|
||||
if (Object.prototype.hasOwnProperty.call(dataType, 'parse')) {
|
||||
if (dataType.types[this.dialectName]) {
|
||||
this._refreshTypeParser(dataType);
|
||||
} else {
|
||||
throw new Error(`Parse function not supported for type ${dataType.key} in dialect ${this.dialectName}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to load dialect module from various configured options.
|
||||
* Priority goes like dialectModulePath > dialectModule > require(default)
|
||||
*
|
||||
* @param {string} moduleName Name of dialect module to lookup
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_loadDialectModule(moduleName) {
|
||||
try {
|
||||
if (this.sequelize.config.dialectModulePath) {
|
||||
return require(this.sequelize.config.dialectModulePath);
|
||||
}
|
||||
if (this.sequelize.config.dialectModule) {
|
||||
return this.sequelize.config.dialectModule;
|
||||
}
|
||||
return require(moduleName);
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') {
|
||||
if (this.sequelize.config.dialectModulePath) {
|
||||
throw new Error(`Unable to find dialect at ${this.sequelize.config.dialectModulePath}`);
|
||||
}
|
||||
throw new Error(`Please install ${moduleName} package manually`);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler which executes on process exit or connection manager shutdown
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onProcessExit() {
|
||||
if (!this.pool) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.pool.drain().then(() => {
|
||||
debug('connection drain due to process exit');
|
||||
return this.pool.destroyAllNow();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Drain the pool and close it permanently
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
close() {
|
||||
// Mark close of pool
|
||||
this.getConnection = function getConnection() {
|
||||
return Promise.reject(new Error('ConnectionManager.getConnection was called after the connection manager was closed!'));
|
||||
};
|
||||
|
||||
return this._onProcessExit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize connection pool. By default pool autostart is set to false, so no connection will be
|
||||
* be created unless `pool.acquire` is called.
|
||||
*/
|
||||
initPools() {
|
||||
const config = this.config;
|
||||
|
||||
if (!config.replication) {
|
||||
this.pool = new Pool({
|
||||
name: 'sequelize',
|
||||
create: () => this._connect(config),
|
||||
destroy: connection => {
|
||||
return this._disconnect(connection)
|
||||
.tap(() => { debug('connection destroy'); });
|
||||
},
|
||||
validate: config.pool.validate,
|
||||
max: config.pool.max,
|
||||
min: config.pool.min,
|
||||
acquireTimeoutMillis: config.pool.acquire,
|
||||
idleTimeoutMillis: config.pool.idle,
|
||||
reapIntervalMillis: config.pool.evict
|
||||
});
|
||||
|
||||
debug(`pool created with max/min: ${config.pool.max}/${config.pool.min}, no replication`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(config.replication.read)) {
|
||||
config.replication.read = [config.replication.read];
|
||||
}
|
||||
|
||||
// Map main connection config
|
||||
config.replication.write = _.defaults(config.replication.write, _.omit(config, 'replication'));
|
||||
|
||||
// Apply defaults to each read config
|
||||
config.replication.read = config.replication.read.map(readConfig =>
|
||||
_.defaults(readConfig, _.omit(this.config, 'replication'))
|
||||
);
|
||||
|
||||
// custom pooling for replication (original author @janmeier)
|
||||
let reads = 0;
|
||||
this.pool = {
|
||||
release: client => {
|
||||
if (client.queryType === 'read') {
|
||||
this.pool.read.release(client);
|
||||
} else {
|
||||
this.pool.write.release(client);
|
||||
}
|
||||
},
|
||||
acquire: (queryType, useMaster) => {
|
||||
useMaster = useMaster === undefined ? false : useMaster;
|
||||
if (queryType === 'SELECT' && !useMaster) {
|
||||
return this.pool.read.acquire();
|
||||
}
|
||||
return this.pool.write.acquire();
|
||||
},
|
||||
destroy: connection => {
|
||||
this.pool[connection.queryType].destroy(connection);
|
||||
debug('connection destroy');
|
||||
},
|
||||
destroyAllNow: () => {
|
||||
return Promise.join(
|
||||
this.pool.read.destroyAllNow(),
|
||||
this.pool.write.destroyAllNow()
|
||||
).tap(() => { debug('all connections destroyed'); });
|
||||
},
|
||||
drain: () => {
|
||||
return Promise.join(
|
||||
this.pool.write.drain(),
|
||||
this.pool.read.drain()
|
||||
);
|
||||
},
|
||||
read: new Pool({
|
||||
name: 'sequelize:read',
|
||||
create: () => {
|
||||
// round robin config
|
||||
const nextRead = reads++ % config.replication.read.length;
|
||||
return this._connect(config.replication.read[nextRead]).tap(connection => {
|
||||
connection.queryType = 'read';
|
||||
});
|
||||
},
|
||||
destroy: connection => this._disconnect(connection),
|
||||
validate: config.pool.validate,
|
||||
max: config.pool.max,
|
||||
min: config.pool.min,
|
||||
acquireTimeoutMillis: config.pool.acquire,
|
||||
idleTimeoutMillis: config.pool.idle,
|
||||
reapIntervalMillis: config.pool.evict
|
||||
}),
|
||||
write: new Pool({
|
||||
name: 'sequelize:write',
|
||||
create: () => {
|
||||
return this._connect(config.replication.write).tap(connection => {
|
||||
connection.queryType = 'write';
|
||||
});
|
||||
},
|
||||
destroy: connection => this._disconnect(connection),
|
||||
validate: config.pool.validate,
|
||||
max: config.pool.max,
|
||||
min: config.pool.min,
|
||||
acquireTimeoutMillis: config.pool.acquire,
|
||||
idleTimeoutMillis: config.pool.idle,
|
||||
reapIntervalMillis: config.pool.evict
|
||||
})
|
||||
};
|
||||
|
||||
debug(`pool created with max/min: ${config.pool.max}/${config.pool.min}, with replication`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection from pool. It sets database version if it's not already set.
|
||||
* Call pool.acquire to get a connection
|
||||
*
|
||||
* @param {Object} [options] Pool options
|
||||
* @param {string} [options.type] Set which replica to use. Available options are `read` and `write`
|
||||
* @param {boolean} [options.useMaster=false] Force master or write replica to get connection from
|
||||
*
|
||||
* @returns {Promise<Connection>}
|
||||
*/
|
||||
getConnection(options) {
|
||||
options = options || {};
|
||||
|
||||
let promise;
|
||||
if (this.sequelize.options.databaseVersion === 0) {
|
||||
if (this.versionPromise) {
|
||||
promise = this.versionPromise;
|
||||
} else {
|
||||
promise = this.versionPromise = this._connect(this.config.replication.write || this.config)
|
||||
.then(connection => {
|
||||
const _options = {};
|
||||
|
||||
_options.transaction = { connection }; // Cheat .query to use our private connection
|
||||
_options.logging = () => {};
|
||||
_options.logging.__testLoggingFn = true;
|
||||
|
||||
//connection might have set databaseVersion value at initialization,
|
||||
//avoiding a useless round trip
|
||||
if (this.sequelize.options.databaseVersion === 0) {
|
||||
return this.sequelize.databaseVersion(_options).then(version => {
|
||||
const parsedVersion = _.get(semver.coerce(version), 'version') || version;
|
||||
this.sequelize.options.databaseVersion = semver.valid(parsedVersion)
|
||||
? parsedVersion
|
||||
: this.defaultVersion;
|
||||
this.versionPromise = null;
|
||||
return this._disconnect(connection);
|
||||
});
|
||||
}
|
||||
|
||||
this.versionPromise = null;
|
||||
return this._disconnect(connection);
|
||||
}).catch(err => {
|
||||
this.versionPromise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
return this.pool.acquire(options.type, options.useMaster)
|
||||
.catch(error => {
|
||||
if (error instanceof TimeoutError) throw new errors.ConnectionAcquireTimeoutError(error);
|
||||
throw error;
|
||||
});
|
||||
}).tap(() => { debug('connection acquired'); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a pooled connection so it can be utilized by other connection requests
|
||||
*
|
||||
* @param {Connection} connection
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
releaseConnection(connection) {
|
||||
return Promise.try(() => {
|
||||
this.pool.release(connection);
|
||||
debug('connection released');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Call dialect library to get connection
|
||||
*
|
||||
* @param {*} config Connection config
|
||||
* @private
|
||||
* @returns {Promise<Connection>}
|
||||
*/
|
||||
_connect(config) {
|
||||
return this.sequelize.runHooks('beforeConnect', config)
|
||||
.then(() => this.dialect.connectionManager.connect(config))
|
||||
.then(connection => this.sequelize.runHooks('afterConnect', connection, config).return(connection));
|
||||
}
|
||||
|
||||
/**
|
||||
* Call dialect library to disconnect a connection
|
||||
*
|
||||
* @param {Connection} connection
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_disconnect(connection) {
|
||||
return this.sequelize.runHooks('beforeDisconnect', connection)
|
||||
.then(() => this.dialect.connectionManager.disconnect(connection))
|
||||
.then(() => this.sequelize.runHooks('afterDisconnect', connection));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a connection is still valid or not
|
||||
*
|
||||
* @param {Connection} connection
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_validate(connection) {
|
||||
if (!this.dialect.connectionManager.validate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.dialect.connectionManager.validate(connection);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConnectionManager;
|
||||
module.exports.ConnectionManager = ConnectionManager;
|
||||
module.exports.default = ConnectionManager;
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
'use strict';
|
||||
|
||||
class AbstractDialect {}
|
||||
|
||||
AbstractDialect.prototype.supports = {
|
||||
'DEFAULT': true,
|
||||
'DEFAULT VALUES': false,
|
||||
'VALUES ()': false,
|
||||
'LIMIT ON UPDATE': false,
|
||||
'ON DUPLICATE KEY': true,
|
||||
'ORDER NULLS': false,
|
||||
'UNION': true,
|
||||
'UNION ALL': true,
|
||||
|
||||
/* does the dialect support returning values for inserted/updated fields */
|
||||
returnValues: false,
|
||||
|
||||
/* features specific to autoIncrement values */
|
||||
autoIncrement: {
|
||||
/* does the dialect require modification of insert queries when inserting auto increment fields */
|
||||
identityInsert: false,
|
||||
|
||||
/* does the dialect support inserting default/null values for autoincrement fields */
|
||||
defaultValue: true,
|
||||
|
||||
/* does the dialect support updating autoincrement fields */
|
||||
update: true
|
||||
},
|
||||
/* Do we need to say DEFAULT for bulk insert */
|
||||
bulkDefault: false,
|
||||
schemas: false,
|
||||
transactions: true,
|
||||
settingIsolationLevelDuringTransaction: true,
|
||||
transactionOptions: {
|
||||
type: false
|
||||
},
|
||||
migrations: true,
|
||||
upserts: true,
|
||||
inserts: {
|
||||
ignoreDuplicates: '', /* dialect specific words for INSERT IGNORE or DO NOTHING */
|
||||
updateOnDuplicate: false, /* whether dialect supports ON DUPLICATE KEY UPDATE */
|
||||
onConflictDoNothing: '' /* dialect specific words for ON CONFLICT DO NOTHING */
|
||||
},
|
||||
constraints: {
|
||||
restrict: true,
|
||||
addConstraint: true,
|
||||
dropConstraint: true,
|
||||
unique: true,
|
||||
default: false,
|
||||
check: true,
|
||||
foreignKey: true,
|
||||
primaryKey: true
|
||||
},
|
||||
index: {
|
||||
collate: true,
|
||||
length: false,
|
||||
parser: false,
|
||||
concurrently: false,
|
||||
type: false,
|
||||
using: true,
|
||||
functionBased: false
|
||||
},
|
||||
joinTableDependent: true,
|
||||
groupedLimit: true,
|
||||
indexViaAlter: false,
|
||||
JSON: false,
|
||||
deferrableConstraints: false
|
||||
};
|
||||
|
||||
module.exports = AbstractDialect;
|
||||
module.exports.AbstractDialect = AbstractDialect;
|
||||
module.exports.default = AbstractDialect;
|
||||
+2653
File diff suppressed because it is too large
Load Diff
+87
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Quote helpers implement quote ability for all dialects.
|
||||
* These are basic block of query building
|
||||
*
|
||||
* Its better to implement all dialect implementation together here. Which will allow
|
||||
* even abstract generator to use them by just specifying dialect type.
|
||||
*
|
||||
* Defining these helpers in each query dialect will leave
|
||||
* code in dual dependency of abstract <-> specific dialect
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Utils = require('../../../../utils');
|
||||
|
||||
/**
|
||||
* list of reserved words in PostgreSQL 10
|
||||
* source: https://www.postgresql.org/docs/10/static/sql-keywords-appendix.html
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
const postgresReservedWords = 'all,analyse,analyze,and,any,array,as,asc,asymmetric,authorization,binary,both,case,cast,check,collate,collation,column,concurrently,constraint,create,cross,current_catalog,current_date,current_role,current_schema,current_time,current_timestamp,current_user,default,deferrable,desc,distinct,do,else,end,except,false,fetch,for,foreign,freeze,from,full,grant,group,having,ilike,in,initially,inner,intersect,into,is,isnull,join,lateral,leading,left,like,limit,localtime,localtimestamp,natural,not,notnull,null,offset,on,only,or,order,outer,overlaps,placing,primary,references,returning,right,select,session_user,similar,some,symmetric,table,tablesample,then,to,trailing,true,union,unique,user,using,variadic,verbose,when,where,window,with'.split(',');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} dialect Dialect name
|
||||
* @param {string} identifier Identifier to quote
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.force=false]
|
||||
* @param {boolean} [options.quoteIdentifiers=true]
|
||||
*
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
function quoteIdentifier(dialect, identifier, options) {
|
||||
if (identifier === '*') return identifier;
|
||||
|
||||
options = Utils.defaults(options || {}, {
|
||||
force: false,
|
||||
quoteIdentifiers: true
|
||||
});
|
||||
|
||||
switch (dialect) {
|
||||
case 'sqlite':
|
||||
case 'mariadb':
|
||||
case 'mysql':
|
||||
return Utils.addTicks(Utils.removeTicks(identifier, '`'), '`');
|
||||
|
||||
case 'postgres':
|
||||
const rawIdentifier = Utils.removeTicks(identifier, '"');
|
||||
|
||||
if (
|
||||
options.force !== true &&
|
||||
options.quoteIdentifiers === false &&
|
||||
!identifier.includes('.') &&
|
||||
!identifier.includes('->') &&
|
||||
!postgresReservedWords.includes(rawIdentifier.toLowerCase())
|
||||
) {
|
||||
// In Postgres, if tables or attributes are created double-quoted,
|
||||
// they are also case sensitive. If they contain any uppercase
|
||||
// characters, they must always be double-quoted. This makes it
|
||||
// impossible to write queries in portable SQL if tables are created in
|
||||
// this way. Hence, we strip quotes if we don't want case sensitivity.
|
||||
return rawIdentifier;
|
||||
}
|
||||
return Utils.addTicks(rawIdentifier, '"');
|
||||
case 'mssql':
|
||||
return `[${identifier.replace(/[[\]']+/g, '')}]`;
|
||||
|
||||
default:
|
||||
throw new Error(`Dialect "${dialect}" is not supported`);
|
||||
}
|
||||
}
|
||||
module.exports.quoteIdentifier = quoteIdentifier;
|
||||
|
||||
/**
|
||||
* Test if a give string is already quoted
|
||||
*
|
||||
* @param {string} identifier
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
function isIdentifierQuoted(identifier) {
|
||||
return /^\s*(?:([`"'])(?:(?!\1).|\1{2})*\1\.?)+\s*$/i.test(identifier);
|
||||
}
|
||||
module.exports.isIdentifierQuoted = isIdentifierQuoted;
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const Op = require('../../../operators');
|
||||
const Utils = require('../../../utils');
|
||||
|
||||
const OperatorHelpers = {
|
||||
OperatorMap: {
|
||||
[Op.eq]: '=',
|
||||
[Op.ne]: '!=',
|
||||
[Op.gte]: '>=',
|
||||
[Op.gt]: '>',
|
||||
[Op.lte]: '<=',
|
||||
[Op.lt]: '<',
|
||||
[Op.not]: 'IS NOT',
|
||||
[Op.is]: 'IS',
|
||||
[Op.in]: 'IN',
|
||||
[Op.notIn]: 'NOT IN',
|
||||
[Op.like]: 'LIKE',
|
||||
[Op.notLike]: 'NOT LIKE',
|
||||
[Op.iLike]: 'ILIKE',
|
||||
[Op.notILike]: 'NOT ILIKE',
|
||||
[Op.startsWith]: 'LIKE',
|
||||
[Op.endsWith]: 'LIKE',
|
||||
[Op.substring]: 'LIKE',
|
||||
[Op.regexp]: '~',
|
||||
[Op.notRegexp]: '!~',
|
||||
[Op.iRegexp]: '~*',
|
||||
[Op.notIRegexp]: '!~*',
|
||||
[Op.between]: 'BETWEEN',
|
||||
[Op.notBetween]: 'NOT BETWEEN',
|
||||
[Op.overlap]: '&&',
|
||||
[Op.contains]: '@>',
|
||||
[Op.contained]: '<@',
|
||||
[Op.adjacent]: '-|-',
|
||||
[Op.strictLeft]: '<<',
|
||||
[Op.strictRight]: '>>',
|
||||
[Op.noExtendRight]: '&<',
|
||||
[Op.noExtendLeft]: '&>',
|
||||
[Op.any]: 'ANY',
|
||||
[Op.all]: 'ALL',
|
||||
[Op.and]: ' AND ',
|
||||
[Op.or]: ' OR ',
|
||||
[Op.col]: 'COL',
|
||||
[Op.placeholder]: '$$PLACEHOLDER$$'
|
||||
},
|
||||
|
||||
OperatorsAliasMap: {},
|
||||
|
||||
setOperatorsAliases(aliases) {
|
||||
if (!aliases || _.isEmpty(aliases)) {
|
||||
this.OperatorsAliasMap = false;
|
||||
} else {
|
||||
this.OperatorsAliasMap = Object.assign({}, aliases);
|
||||
}
|
||||
},
|
||||
|
||||
_replaceAliases(orig) {
|
||||
const obj = {};
|
||||
if (!this.OperatorsAliasMap) {
|
||||
return orig;
|
||||
}
|
||||
|
||||
Utils.getOperators(orig).forEach(op => {
|
||||
const item = orig[op];
|
||||
if (_.isPlainObject(item)) {
|
||||
obj[op] = this._replaceAliases(item);
|
||||
} else {
|
||||
obj[op] = item;
|
||||
}
|
||||
});
|
||||
|
||||
_.forOwn(orig, (item, prop) => {
|
||||
prop = this.OperatorsAliasMap[prop] || prop;
|
||||
if (_.isPlainObject(item)) {
|
||||
item = this._replaceAliases(item);
|
||||
}
|
||||
obj[prop] = item;
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = OperatorHelpers;
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
|
||||
const uuidv4 = require('uuid/v4');
|
||||
|
||||
const TransactionQueries = {
|
||||
/**
|
||||
* Returns a query that sets the transaction isolation level.
|
||||
*
|
||||
* @param {string} value The isolation level.
|
||||
* @param {Object} options An object with options.
|
||||
* @returns {string} The generated sql query.
|
||||
* @private
|
||||
*/
|
||||
setIsolationLevelQuery(value, options) {
|
||||
if (options.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
return `SET TRANSACTION ISOLATION LEVEL ${value};`;
|
||||
},
|
||||
|
||||
generateTransactionId() {
|
||||
return uuidv4();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a query that starts a transaction.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @returns {string} The generated sql query.
|
||||
* @private
|
||||
*/
|
||||
startTransactionQuery(transaction) {
|
||||
if (transaction.parent) {
|
||||
// force quoting of savepoint identifiers for postgres
|
||||
return `SAVEPOINT ${this.quoteIdentifier(transaction.name, true)};`;
|
||||
}
|
||||
|
||||
return 'START TRANSACTION;';
|
||||
},
|
||||
|
||||
deferConstraintsQuery() {},
|
||||
|
||||
setConstraintQuery() {},
|
||||
setDeferredQuery() {},
|
||||
setImmediateQuery() {},
|
||||
|
||||
/**
|
||||
* Returns a query that commits a transaction.
|
||||
*
|
||||
* @param {Transaction} transaction An object with options.
|
||||
* @returns {string} The generated sql query.
|
||||
* @private
|
||||
*/
|
||||
commitTransactionQuery(transaction) {
|
||||
if (transaction.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
return 'COMMIT;';
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a query that rollbacks a transaction.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @returns {string} The generated sql query.
|
||||
* @private
|
||||
*/
|
||||
rollbackTransactionQuery(transaction) {
|
||||
if (transaction.parent) {
|
||||
// force quoting of savepoint identifiers for postgres
|
||||
return `ROLLBACK TO SAVEPOINT ${this.quoteIdentifier(transaction.name, true)};`;
|
||||
}
|
||||
|
||||
return 'ROLLBACK;';
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TransactionQueries;
|
||||
+741
@@ -0,0 +1,741 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const SqlString = require('../../sql-string');
|
||||
const QueryTypes = require('../../query-types');
|
||||
const Dot = require('dottie');
|
||||
const deprecations = require('../../utils/deprecations');
|
||||
const uuid = require('uuid/v4');
|
||||
|
||||
class AbstractQuery {
|
||||
|
||||
constructor(connection, sequelize, options) {
|
||||
this.uuid = uuid();
|
||||
this.connection = connection;
|
||||
this.instance = options.instance;
|
||||
this.model = options.model;
|
||||
this.sequelize = sequelize;
|
||||
this.options = Object.assign({
|
||||
plain: false,
|
||||
raw: false,
|
||||
// eslint-disable-next-line no-console
|
||||
logging: console.log
|
||||
}, options || {});
|
||||
this.checkLoggingOption();
|
||||
}
|
||||
|
||||
/**
|
||||
* rewrite query with parameters
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* query.formatBindParameters('select $1 as foo', ['fooval']);
|
||||
*
|
||||
* query.formatBindParameters('select $foo as foo', { foo: 'fooval' });
|
||||
*
|
||||
* Options
|
||||
* skipUnescape: bool, skip unescaping $$
|
||||
* skipValueReplace: bool, do not replace (but do unescape $$). Check correct syntax and if all values are available
|
||||
*
|
||||
* @param {string} sql
|
||||
* @param {Object|Array} values
|
||||
* @param {string} dialect
|
||||
* @param {Function} [replacementFunc]
|
||||
* @param {Object} [options]
|
||||
* @private
|
||||
*/
|
||||
static formatBindParameters(sql, values, dialect, replacementFunc, options) {
|
||||
if (!values) {
|
||||
return [sql, []];
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
if (typeof replacementFunc !== 'function') {
|
||||
options = replacementFunc || {};
|
||||
replacementFunc = undefined;
|
||||
}
|
||||
|
||||
if (!replacementFunc) {
|
||||
if (options.skipValueReplace) {
|
||||
replacementFunc = (match, key, values) => {
|
||||
if (values[key] !== undefined) {
|
||||
return match;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
} else {
|
||||
replacementFunc = (match, key, values, timeZone, dialect) => {
|
||||
if (values[key] !== undefined) {
|
||||
return SqlString.escape(values[key], timeZone, dialect);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
} else if (options.skipValueReplace) {
|
||||
const origReplacementFunc = replacementFunc;
|
||||
replacementFunc = (match, key, values, timeZone, dialect, options) => {
|
||||
if (origReplacementFunc(match, key, values, timeZone, dialect, options) !== undefined) {
|
||||
return match;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
const timeZone = null;
|
||||
const list = Array.isArray(values);
|
||||
|
||||
sql = sql.replace(/\$(\$|\w+)/g, (match, key) => {
|
||||
if ('$' === key) {
|
||||
return options.skipUnescape ? match : key;
|
||||
}
|
||||
|
||||
let replVal;
|
||||
if (list) {
|
||||
if (key.match(/^[1-9]\d*$/)) {
|
||||
key = key - 1;
|
||||
replVal = replacementFunc(match, key, values, timeZone, dialect, options);
|
||||
}
|
||||
} else if (!key.match(/^\d*$/)) {
|
||||
replVal = replacementFunc(match, key, values, timeZone, dialect, options);
|
||||
}
|
||||
if (replVal === undefined) {
|
||||
throw new Error(`Named bind parameter "${match}" has no value in the given object.`);
|
||||
}
|
||||
return replVal;
|
||||
});
|
||||
return [sql, []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the passed sql query.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* query.run('SELECT 1')
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
run() {
|
||||
throw new Error('The run method wasn\'t overwritten!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the logging option of the instance and print deprecation warnings.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
checkLoggingOption() {
|
||||
if (this.options.logging === true) {
|
||||
deprecations.noTrueLogging();
|
||||
// eslint-disable-next-line no-console
|
||||
this.options.logging = console.log;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributes of an insert query, which contains the just inserted id.
|
||||
*
|
||||
* @returns {string} The field name.
|
||||
* @private
|
||||
*/
|
||||
getInsertIdField() {
|
||||
return 'insertId';
|
||||
}
|
||||
|
||||
getUniqueConstraintErrorMessage(field) {
|
||||
let message = field ? `${field} must be unique` : 'Must be unique';
|
||||
|
||||
if (field && this.model) {
|
||||
for (const key of Object.keys(this.model.uniqueKeys)) {
|
||||
if (this.model.uniqueKeys[key].fields.includes(field.replace(/"/g, ''))) {
|
||||
if (this.model.uniqueKeys[key].msg) {
|
||||
message = this.model.uniqueKeys[key].msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
isRawQuery() {
|
||||
return this.options.type === QueryTypes.RAW;
|
||||
}
|
||||
|
||||
isVersionQuery() {
|
||||
return this.options.type === QueryTypes.VERSION;
|
||||
}
|
||||
|
||||
isUpsertQuery() {
|
||||
return this.options.type === QueryTypes.UPSERT;
|
||||
}
|
||||
|
||||
isInsertQuery(results, metaData) {
|
||||
let result = true;
|
||||
|
||||
if (this.options.type === QueryTypes.INSERT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// is insert query if sql contains insert into
|
||||
result = result && this.sql.toLowerCase().startsWith('insert into');
|
||||
|
||||
// is insert query if no results are passed or if the result has the inserted id
|
||||
result = result && (!results || Object.prototype.hasOwnProperty.call(results, this.getInsertIdField()));
|
||||
|
||||
// is insert query if no metadata are passed or if the metadata has the inserted id
|
||||
result = result && (!metaData || Object.prototype.hasOwnProperty.call(metaData, this.getInsertIdField()));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
handleInsertQuery(results, metaData) {
|
||||
if (this.instance) {
|
||||
// add the inserted row id to the instance
|
||||
const autoIncrementAttribute = this.model.autoIncrementAttribute;
|
||||
let id = null;
|
||||
|
||||
id = id || results && results[this.getInsertIdField()];
|
||||
id = id || metaData && metaData[this.getInsertIdField()];
|
||||
|
||||
this.instance[autoIncrementAttribute] = id;
|
||||
}
|
||||
}
|
||||
|
||||
isShowTablesQuery() {
|
||||
return this.options.type === QueryTypes.SHOWTABLES;
|
||||
}
|
||||
|
||||
handleShowTablesQuery(results) {
|
||||
return _.flatten(results.map(resultSet => _.values(resultSet)));
|
||||
}
|
||||
|
||||
isShowIndexesQuery() {
|
||||
return this.options.type === QueryTypes.SHOWINDEXES;
|
||||
}
|
||||
|
||||
isShowConstraintsQuery() {
|
||||
return this.options.type === QueryTypes.SHOWCONSTRAINTS;
|
||||
}
|
||||
|
||||
isDescribeQuery() {
|
||||
return this.options.type === QueryTypes.DESCRIBE;
|
||||
}
|
||||
|
||||
isSelectQuery() {
|
||||
return this.options.type === QueryTypes.SELECT;
|
||||
}
|
||||
|
||||
isBulkUpdateQuery() {
|
||||
return this.options.type === QueryTypes.BULKUPDATE;
|
||||
}
|
||||
|
||||
isBulkDeleteQuery() {
|
||||
return this.options.type === QueryTypes.BULKDELETE;
|
||||
}
|
||||
|
||||
isForeignKeysQuery() {
|
||||
return this.options.type === QueryTypes.FOREIGNKEYS;
|
||||
}
|
||||
|
||||
isUpdateQuery() {
|
||||
return this.options.type === QueryTypes.UPDATE;
|
||||
}
|
||||
|
||||
handleSelectQuery(results) {
|
||||
let result = null;
|
||||
|
||||
// Map raw fields to names if a mapping is provided
|
||||
if (this.options.fieldMap) {
|
||||
const fieldMap = this.options.fieldMap;
|
||||
results = results.map(result => _.reduce(fieldMap, (result, name, field) => {
|
||||
if (result[field] !== undefined && name !== field) {
|
||||
result[name] = result[field];
|
||||
delete result[field];
|
||||
}
|
||||
return result;
|
||||
}, result));
|
||||
}
|
||||
|
||||
// Raw queries
|
||||
if (this.options.raw) {
|
||||
result = results.map(result => {
|
||||
let o = {};
|
||||
|
||||
for (const key in result) {
|
||||
if (Object.prototype.hasOwnProperty.call(result, key)) {
|
||||
o[key] = result[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.nest) {
|
||||
o = Dot.transform(o);
|
||||
}
|
||||
|
||||
return o;
|
||||
});
|
||||
// Queries with include
|
||||
} else if (this.options.hasJoin === true) {
|
||||
results = AbstractQuery._groupJoinData(results, {
|
||||
model: this.model,
|
||||
includeMap: this.options.includeMap,
|
||||
includeNames: this.options.includeNames
|
||||
}, {
|
||||
checkExisting: this.options.hasMultiAssociation
|
||||
});
|
||||
|
||||
result = this.model.bulkBuild(results, {
|
||||
isNewRecord: false,
|
||||
include: this.options.include,
|
||||
includeNames: this.options.includeNames,
|
||||
includeMap: this.options.includeMap,
|
||||
includeValidated: true,
|
||||
attributes: this.options.originalAttributes || this.options.attributes,
|
||||
raw: true
|
||||
});
|
||||
// Regular queries
|
||||
} else {
|
||||
result = this.model.bulkBuild(results, {
|
||||
isNewRecord: false,
|
||||
raw: true,
|
||||
attributes: this.options.originalAttributes || this.options.attributes
|
||||
});
|
||||
}
|
||||
|
||||
// return the first real model instance if options.plain is set (e.g. Model.find)
|
||||
if (this.options.plain) {
|
||||
result = result.length === 0 ? null : result[0];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
isShowOrDescribeQuery() {
|
||||
let result = false;
|
||||
|
||||
result = result || this.sql.toLowerCase().startsWith('show');
|
||||
result = result || this.sql.toLowerCase().startsWith('describe');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
isCallQuery() {
|
||||
return this.sql.toLowerCase().startsWith('call');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sql
|
||||
* @param {Function} debugContext
|
||||
* @param {Array|Object} parameters
|
||||
* @protected
|
||||
* @returns {Function} A function to call after the query was completed.
|
||||
*/
|
||||
_logQuery(sql, debugContext, parameters) {
|
||||
const { connection, options } = this;
|
||||
const benchmark = this.sequelize.options.benchmark || options.benchmark;
|
||||
const logQueryParameters = this.sequelize.options.logQueryParameters || options.logQueryParameters;
|
||||
const startTime = Date.now();
|
||||
let logParameter = '';
|
||||
|
||||
if (logQueryParameters && parameters) {
|
||||
const delimiter = sql.endsWith(';') ? '' : ';';
|
||||
let paramStr;
|
||||
if (Array.isArray(parameters)) {
|
||||
paramStr = parameters.map(p=>JSON.stringify(p)).join(', ');
|
||||
} else {
|
||||
paramStr = JSON.stringify(parameters);
|
||||
}
|
||||
logParameter = `${delimiter} ${paramStr}`;
|
||||
}
|
||||
const fmt = `(${connection.uuid || 'default'}): ${sql}${logParameter}`;
|
||||
const msg = `Executing ${fmt}`;
|
||||
debugContext(msg);
|
||||
if (!benchmark) {
|
||||
this.sequelize.log(`Executing ${fmt}`, options);
|
||||
}
|
||||
return () => {
|
||||
const afterMsg = `Executed ${fmt}`;
|
||||
debugContext(afterMsg);
|
||||
if (benchmark) {
|
||||
this.sequelize.log(afterMsg, Date.now() - startTime, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The function takes the result of the query execution and groups
|
||||
* the associated data by the callee.
|
||||
*
|
||||
* Example:
|
||||
* groupJoinData([
|
||||
* {
|
||||
* some: 'data',
|
||||
* id: 1,
|
||||
* association: { foo: 'bar', id: 1 }
|
||||
* }, {
|
||||
* some: 'data',
|
||||
* id: 1,
|
||||
* association: { foo: 'bar', id: 2 }
|
||||
* }, {
|
||||
* some: 'data',
|
||||
* id: 1,
|
||||
* association: { foo: 'bar', id: 3 }
|
||||
* }
|
||||
* ])
|
||||
*
|
||||
* Result:
|
||||
* Something like this:
|
||||
*
|
||||
* [
|
||||
* {
|
||||
* some: 'data',
|
||||
* id: 1,
|
||||
* association: [
|
||||
* { foo: 'bar', id: 1 },
|
||||
* { foo: 'bar', id: 2 },
|
||||
* { foo: 'bar', id: 3 }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
* @param {Array} rows
|
||||
* @param {Object} includeOptions
|
||||
* @param {Object} options
|
||||
* @private
|
||||
*/
|
||||
static _groupJoinData(rows, includeOptions, options) {
|
||||
|
||||
/*
|
||||
* Assumptions
|
||||
* ID is not necessarily the first field
|
||||
* All fields for a level is grouped in the same set (i.e. Panel.id, Task.id, Panel.title is not possible)
|
||||
* Parent keys will be seen before any include/child keys
|
||||
* Previous set won't necessarily be parent set (one parent could have two children, one child would then be previous set for the other)
|
||||
*/
|
||||
|
||||
/*
|
||||
* Author (MH) comment: This code is an unreadable mess, but it's performant.
|
||||
* groupJoinData is a performance critical function so we prioritize perf over readability.
|
||||
*/
|
||||
if (!rows.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Generic looping
|
||||
let i;
|
||||
let length;
|
||||
let $i;
|
||||
let $length;
|
||||
// Row specific looping
|
||||
let rowsI;
|
||||
let row;
|
||||
const rowsLength = rows.length;
|
||||
// Key specific looping
|
||||
let keys;
|
||||
let key;
|
||||
let keyI;
|
||||
let keyLength;
|
||||
let prevKey;
|
||||
let values;
|
||||
let topValues;
|
||||
let topExists;
|
||||
const checkExisting = options.checkExisting;
|
||||
// If we don't have to deduplicate we can pre-allocate the resulting array
|
||||
let itemHash;
|
||||
let parentHash;
|
||||
let topHash;
|
||||
const results = checkExisting ? [] : new Array(rowsLength);
|
||||
const resultMap = {};
|
||||
const includeMap = {};
|
||||
// Result variables for the respective functions
|
||||
let $keyPrefix;
|
||||
let $keyPrefixString;
|
||||
let $prevKeyPrefixString; // eslint-disable-line
|
||||
let $prevKeyPrefix;
|
||||
let $lastKeyPrefix;
|
||||
let $current;
|
||||
let $parent;
|
||||
// Map each key to an include option
|
||||
let previousPiece;
|
||||
const buildIncludeMap = piece => {
|
||||
if (Object.prototype.hasOwnProperty.call($current.includeMap, piece)) {
|
||||
includeMap[key] = $current = $current.includeMap[piece];
|
||||
if (previousPiece) {
|
||||
previousPiece = `${previousPiece}.${piece}`;
|
||||
} else {
|
||||
previousPiece = piece;
|
||||
}
|
||||
includeMap[previousPiece] = $current;
|
||||
}
|
||||
};
|
||||
// Calculate the string prefix of a key ('User.Results' for 'User.Results.id')
|
||||
const keyPrefixStringMemo = {};
|
||||
const keyPrefixString = (key, memo) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(memo, key)) {
|
||||
memo[key] = key.substr(0, key.lastIndexOf('.'));
|
||||
}
|
||||
return memo[key];
|
||||
};
|
||||
// Removes the prefix from a key ('id' for 'User.Results.id')
|
||||
const removeKeyPrefixMemo = {};
|
||||
const removeKeyPrefix = key => {
|
||||
if (!Object.prototype.hasOwnProperty.call(removeKeyPrefixMemo, key)) {
|
||||
const index = key.lastIndexOf('.');
|
||||
removeKeyPrefixMemo[key] = key.substr(index === -1 ? 0 : index + 1);
|
||||
}
|
||||
return removeKeyPrefixMemo[key];
|
||||
};
|
||||
// Calculates the array prefix of a key (['User', 'Results'] for 'User.Results.id')
|
||||
const keyPrefixMemo = {};
|
||||
const keyPrefix = key => {
|
||||
// We use a double memo and keyPrefixString so that different keys with the same prefix will receive the same array instead of differnet arrays with equal values
|
||||
if (!Object.prototype.hasOwnProperty.call(keyPrefixMemo, key)) {
|
||||
const prefixString = keyPrefixString(key, keyPrefixStringMemo);
|
||||
if (!Object.prototype.hasOwnProperty.call(keyPrefixMemo, prefixString)) {
|
||||
keyPrefixMemo[prefixString] = prefixString ? prefixString.split('.') : [];
|
||||
}
|
||||
keyPrefixMemo[key] = keyPrefixMemo[prefixString];
|
||||
}
|
||||
return keyPrefixMemo[key];
|
||||
};
|
||||
// Calcuate the last item in the array prefix ('Results' for 'User.Results.id')
|
||||
const lastKeyPrefixMemo = {};
|
||||
const lastKeyPrefix = key => {
|
||||
if (!Object.prototype.hasOwnProperty.call(lastKeyPrefixMemo, key)) {
|
||||
const prefix = keyPrefix(key);
|
||||
const length = prefix.length;
|
||||
|
||||
lastKeyPrefixMemo[key] = !length ? '' : prefix[length - 1];
|
||||
}
|
||||
return lastKeyPrefixMemo[key];
|
||||
};
|
||||
const getUniqueKeyAttributes = model => {
|
||||
let uniqueKeyAttributes = _.chain(model.uniqueKeys);
|
||||
uniqueKeyAttributes = uniqueKeyAttributes
|
||||
.result(`${uniqueKeyAttributes.findKey()}.fields`)
|
||||
.map(field => _.findKey(model.attributes, chr => chr.field === field))
|
||||
.value();
|
||||
|
||||
return uniqueKeyAttributes;
|
||||
};
|
||||
const stringify = obj => obj instanceof Buffer ? obj.toString('hex') : obj;
|
||||
let primaryKeyAttributes;
|
||||
let uniqueKeyAttributes;
|
||||
let prefix;
|
||||
|
||||
for (rowsI = 0; rowsI < rowsLength; rowsI++) {
|
||||
row = rows[rowsI];
|
||||
|
||||
// Keys are the same for all rows, so only need to compute them on the first row
|
||||
if (rowsI === 0) {
|
||||
keys = Object.keys(row);
|
||||
keyLength = keys.length;
|
||||
}
|
||||
|
||||
if (checkExisting) {
|
||||
topExists = false;
|
||||
|
||||
// Compute top level hash key (this is usually just the primary key values)
|
||||
$length = includeOptions.model.primaryKeyAttributes.length;
|
||||
topHash = '';
|
||||
if ($length === 1) {
|
||||
topHash = stringify(row[includeOptions.model.primaryKeyAttributes[0]]);
|
||||
}
|
||||
else if ($length > 1) {
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
topHash += stringify(row[includeOptions.model.primaryKeyAttributes[$i]]);
|
||||
}
|
||||
}
|
||||
else if (!_.isEmpty(includeOptions.model.uniqueKeys)) {
|
||||
uniqueKeyAttributes = getUniqueKeyAttributes(includeOptions.model);
|
||||
for ($i = 0; $i < uniqueKeyAttributes.length; $i++) {
|
||||
topHash += row[uniqueKeyAttributes[$i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
topValues = values = {};
|
||||
$prevKeyPrefix = undefined;
|
||||
for (keyI = 0; keyI < keyLength; keyI++) {
|
||||
key = keys[keyI];
|
||||
|
||||
// The string prefix isn't actualy needed
|
||||
// We use it so keyPrefix for different keys will resolve to the same array if they have the same prefix
|
||||
// TODO: Find a better way?
|
||||
$keyPrefixString = keyPrefixString(key, keyPrefixStringMemo);
|
||||
$keyPrefix = keyPrefix(key);
|
||||
|
||||
// On the first row we compute the includeMap
|
||||
if (rowsI === 0 && !Object.prototype.hasOwnProperty.call(includeMap, key)) {
|
||||
if (!$keyPrefix.length) {
|
||||
includeMap[key] = includeMap[''] = includeOptions;
|
||||
} else {
|
||||
$current = includeOptions;
|
||||
previousPiece = undefined;
|
||||
$keyPrefix.forEach(buildIncludeMap);
|
||||
}
|
||||
}
|
||||
// End of key set
|
||||
if ($prevKeyPrefix !== undefined && $prevKeyPrefix !== $keyPrefix) {
|
||||
if (checkExisting) {
|
||||
// Compute hash key for this set instance
|
||||
// TODO: Optimize
|
||||
length = $prevKeyPrefix.length;
|
||||
$parent = null;
|
||||
parentHash = null;
|
||||
|
||||
if (length) {
|
||||
for (i = 0; i < length; i++) {
|
||||
prefix = $parent ? `${$parent}.${$prevKeyPrefix[i]}` : $prevKeyPrefix[i];
|
||||
primaryKeyAttributes = includeMap[prefix].model.primaryKeyAttributes;
|
||||
$length = primaryKeyAttributes.length;
|
||||
itemHash = prefix;
|
||||
if ($length === 1) {
|
||||
itemHash += stringify(row[`${prefix}.${primaryKeyAttributes[0]}`]);
|
||||
}
|
||||
else if ($length > 1) {
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
itemHash += stringify(row[`${prefix}.${primaryKeyAttributes[$i]}`]);
|
||||
}
|
||||
}
|
||||
else if (!_.isEmpty(includeMap[prefix].model.uniqueKeys)) {
|
||||
uniqueKeyAttributes = getUniqueKeyAttributes(includeMap[prefix].model);
|
||||
for ($i = 0; $i < uniqueKeyAttributes.length; $i++) {
|
||||
itemHash += row[`${prefix}.${uniqueKeyAttributes[$i]}`];
|
||||
}
|
||||
}
|
||||
if (!parentHash) {
|
||||
parentHash = topHash;
|
||||
}
|
||||
|
||||
itemHash = parentHash + itemHash;
|
||||
$parent = prefix;
|
||||
if (i < length - 1) {
|
||||
parentHash = itemHash;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
itemHash = topHash;
|
||||
}
|
||||
|
||||
if (itemHash === topHash) {
|
||||
if (!resultMap[itemHash]) {
|
||||
resultMap[itemHash] = values;
|
||||
} else {
|
||||
topExists = true;
|
||||
}
|
||||
} else if (!resultMap[itemHash]) {
|
||||
$parent = resultMap[parentHash];
|
||||
$lastKeyPrefix = lastKeyPrefix(prevKey);
|
||||
|
||||
if (includeMap[prevKey].association.isSingleAssociation) {
|
||||
if ($parent) {
|
||||
$parent[$lastKeyPrefix] = resultMap[itemHash] = values;
|
||||
}
|
||||
} else {
|
||||
if (!$parent[$lastKeyPrefix]) {
|
||||
$parent[$lastKeyPrefix] = [];
|
||||
}
|
||||
$parent[$lastKeyPrefix].push(resultMap[itemHash] = values);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset values
|
||||
values = {};
|
||||
} else {
|
||||
// If checkExisting is false it's because there's only 1:1 associations in this query
|
||||
// However we still need to map onto the appropriate parent
|
||||
// For 1:1 we map forward, initializing the value object on the parent to be filled in the next iterations of the loop
|
||||
$current = topValues;
|
||||
length = $keyPrefix.length;
|
||||
if (length) {
|
||||
for (i = 0; i < length; i++) {
|
||||
if (i === length - 1) {
|
||||
values = $current[$keyPrefix[i]] = {};
|
||||
}
|
||||
$current = $current[$keyPrefix[i]] || {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End of iteration, set value and set prev values (for next iteration)
|
||||
values[removeKeyPrefix(key)] = row[key];
|
||||
prevKey = key;
|
||||
$prevKeyPrefix = $keyPrefix;
|
||||
$prevKeyPrefixString = $keyPrefixString;
|
||||
}
|
||||
|
||||
if (checkExisting) {
|
||||
length = $prevKeyPrefix.length;
|
||||
$parent = null;
|
||||
parentHash = null;
|
||||
|
||||
if (length) {
|
||||
for (i = 0; i < length; i++) {
|
||||
prefix = $parent ? `${$parent}.${$prevKeyPrefix[i]}` : $prevKeyPrefix[i];
|
||||
primaryKeyAttributes = includeMap[prefix].model.primaryKeyAttributes;
|
||||
$length = primaryKeyAttributes.length;
|
||||
itemHash = prefix;
|
||||
if ($length === 1) {
|
||||
itemHash += stringify(row[`${prefix}.${primaryKeyAttributes[0]}`]);
|
||||
}
|
||||
else if ($length > 0) {
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
itemHash += stringify(row[`${prefix}.${primaryKeyAttributes[$i]}`]);
|
||||
}
|
||||
}
|
||||
else if (!_.isEmpty(includeMap[prefix].model.uniqueKeys)) {
|
||||
uniqueKeyAttributes = getUniqueKeyAttributes(includeMap[prefix].model);
|
||||
for ($i = 0; $i < uniqueKeyAttributes.length; $i++) {
|
||||
itemHash += row[`${prefix}.${uniqueKeyAttributes[$i]}`];
|
||||
}
|
||||
}
|
||||
if (!parentHash) {
|
||||
parentHash = topHash;
|
||||
}
|
||||
|
||||
itemHash = parentHash + itemHash;
|
||||
$parent = prefix;
|
||||
if (i < length - 1) {
|
||||
parentHash = itemHash;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
itemHash = topHash;
|
||||
}
|
||||
|
||||
if (itemHash === topHash) {
|
||||
if (!resultMap[itemHash]) {
|
||||
resultMap[itemHash] = values;
|
||||
} else {
|
||||
topExists = true;
|
||||
}
|
||||
} else if (!resultMap[itemHash]) {
|
||||
$parent = resultMap[parentHash];
|
||||
$lastKeyPrefix = lastKeyPrefix(prevKey);
|
||||
|
||||
if (includeMap[prevKey].association.isSingleAssociation) {
|
||||
if ($parent) {
|
||||
$parent[$lastKeyPrefix] = resultMap[itemHash] = values;
|
||||
}
|
||||
} else {
|
||||
if (!$parent[$lastKeyPrefix]) {
|
||||
$parent[$lastKeyPrefix] = [];
|
||||
}
|
||||
$parent[$lastKeyPrefix].push(resultMap[itemHash] = values);
|
||||
}
|
||||
}
|
||||
if (!topExists) {
|
||||
results.push(topValues);
|
||||
}
|
||||
} else {
|
||||
results[rowsI] = topValues;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AbstractQuery;
|
||||
module.exports.AbstractQuery = AbstractQuery;
|
||||
module.exports.default = AbstractQuery;
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
'use strict';
|
||||
|
||||
const AbstractConnectionManager = require('../abstract/connection-manager');
|
||||
const SequelizeErrors = require('../../errors');
|
||||
const Promise = require('../../promise');
|
||||
const { logger } = require('../../utils/logger');
|
||||
const DataTypes = require('../../data-types').mariadb;
|
||||
const momentTz = require('moment-timezone');
|
||||
const debug = logger.debugContext('connection:mariadb');
|
||||
const parserStore = require('../parserStore')('mariadb');
|
||||
|
||||
/**
|
||||
* MariaDB Connection Manager
|
||||
*
|
||||
* Get connections, validate and disconnect them.
|
||||
* AbstractConnectionManager pooling use it to handle MariaDB specific connections
|
||||
* Use https://github.com/MariaDB/mariadb-connector-nodejs to connect with MariaDB server
|
||||
*
|
||||
* @extends AbstractConnectionManager
|
||||
* @returns Class<ConnectionManager>
|
||||
* @private
|
||||
*/
|
||||
|
||||
class ConnectionManager extends AbstractConnectionManager {
|
||||
constructor(dialect, sequelize) {
|
||||
sequelize.config.port = sequelize.config.port || 3306;
|
||||
super(dialect, sequelize);
|
||||
this.lib = this._loadDialectModule('mariadb');
|
||||
this.refreshTypeParser(DataTypes);
|
||||
}
|
||||
|
||||
static _typecast(field, next) {
|
||||
if (parserStore.get(field.type)) {
|
||||
return parserStore.get(field.type)(field, this.sequelize.options, next);
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
_refreshTypeParser(dataType) {
|
||||
parserStore.refresh(dataType);
|
||||
}
|
||||
|
||||
_clearTypeParser() {
|
||||
parserStore.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect with MariaDB database based on config, Handle any errors in connection
|
||||
* Set the pool handlers on connection.error
|
||||
* Also set proper timezone once connection is connected.
|
||||
*
|
||||
* @param {Object} config
|
||||
* @returns {Promise<Connection>}
|
||||
* @private
|
||||
*/
|
||||
connect(config) {
|
||||
// Named timezone is not supported in mariadb, convert to offset
|
||||
let tzOffset = this.sequelize.options.timezone;
|
||||
tzOffset = /\//.test(tzOffset) ? momentTz.tz(tzOffset).format('Z')
|
||||
: tzOffset;
|
||||
|
||||
const connectionConfig = {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.username,
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
timezone: tzOffset,
|
||||
typeCast: ConnectionManager._typecast.bind(this),
|
||||
bigNumberStrings: false,
|
||||
supportBigNumbers: true,
|
||||
foundRows: false
|
||||
};
|
||||
|
||||
if (config.dialectOptions) {
|
||||
Object.assign(connectionConfig, config.dialectOptions);
|
||||
}
|
||||
|
||||
if (!this.sequelize.config.keepDefaultTimezone) {
|
||||
// set timezone for this connection
|
||||
if (connectionConfig.initSql) {
|
||||
if (!Array.isArray(
|
||||
connectionConfig.initSql)) {
|
||||
connectionConfig.initSql = [connectionConfig.initSql];
|
||||
}
|
||||
connectionConfig.initSql.push(`SET time_zone = '${tzOffset}'`);
|
||||
} else {
|
||||
connectionConfig.initSql = `SET time_zone = '${tzOffset}'`;
|
||||
}
|
||||
}
|
||||
|
||||
return this.lib.createConnection(connectionConfig)
|
||||
.then(connection => {
|
||||
this.sequelize.options.databaseVersion = connection.serverVersion();
|
||||
debug('connection acquired');
|
||||
connection.on('error', error => {
|
||||
switch (error.code) {
|
||||
case 'ESOCKET':
|
||||
case 'ECONNRESET':
|
||||
case 'EPIPE':
|
||||
case 'PROTOCOL_CONNECTION_LOST':
|
||||
this.pool.destroy(connection);
|
||||
}
|
||||
});
|
||||
return connection;
|
||||
})
|
||||
.catch(err => {
|
||||
switch (err.code) {
|
||||
case 'ECONNREFUSED':
|
||||
throw new SequelizeErrors.ConnectionRefusedError(err);
|
||||
case 'ER_ACCESS_DENIED_ERROR':
|
||||
case 'ER_ACCESS_DENIED_NO_PASSWORD_ERROR':
|
||||
throw new SequelizeErrors.AccessDeniedError(err);
|
||||
case 'ENOTFOUND':
|
||||
throw new SequelizeErrors.HostNotFoundError(err);
|
||||
case 'EHOSTUNREACH':
|
||||
case 'ENETUNREACH':
|
||||
case 'EADDRNOTAVAIL':
|
||||
throw new SequelizeErrors.HostNotReachableError(err);
|
||||
case 'EINVAL':
|
||||
throw new SequelizeErrors.InvalidConnectionError(err);
|
||||
default:
|
||||
throw new SequelizeErrors.ConnectionError(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
disconnect(connection) {
|
||||
// Don't disconnect connections with CLOSED state
|
||||
if (!connection.isValid()) {
|
||||
debug('connection tried to disconnect but was already at CLOSED state');
|
||||
return Promise.resolve();
|
||||
}
|
||||
//wrap native Promise into bluebird
|
||||
return Promise.resolve(connection.end());
|
||||
}
|
||||
|
||||
validate(connection) {
|
||||
return connection && connection.isValid();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConnectionManager;
|
||||
module.exports.ConnectionManager = ConnectionManager;
|
||||
module.exports.default = ConnectionManager;
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment-timezone');
|
||||
|
||||
module.exports = BaseTypes => {
|
||||
BaseTypes.ABSTRACT.prototype.dialectTypes = 'https://mariadb.com/kb/en/library/resultset/#field-types';
|
||||
|
||||
/**
|
||||
* types: [buffer_type, ...]
|
||||
* @see documentation : https://mariadb.com/kb/en/library/resultset/#field-types
|
||||
* @see connector implementation : https://github.com/MariaDB/mariadb-connector-nodejs/blob/master/lib/const/field-type.js
|
||||
*/
|
||||
|
||||
BaseTypes.DATE.types.mariadb = ['DATETIME'];
|
||||
BaseTypes.STRING.types.mariadb = ['VAR_STRING'];
|
||||
BaseTypes.CHAR.types.mariadb = ['STRING'];
|
||||
BaseTypes.TEXT.types.mariadb = ['BLOB'];
|
||||
BaseTypes.TINYINT.types.mariadb = ['TINY'];
|
||||
BaseTypes.SMALLINT.types.mariadb = ['SHORT'];
|
||||
BaseTypes.MEDIUMINT.types.mariadb = ['INT24'];
|
||||
BaseTypes.INTEGER.types.mariadb = ['LONG'];
|
||||
BaseTypes.BIGINT.types.mariadb = ['LONGLONG'];
|
||||
BaseTypes.FLOAT.types.mariadb = ['FLOAT'];
|
||||
BaseTypes.TIME.types.mariadb = ['TIME'];
|
||||
BaseTypes.DATEONLY.types.mariadb = ['DATE'];
|
||||
BaseTypes.BOOLEAN.types.mariadb = ['TINY'];
|
||||
BaseTypes.BLOB.types.mariadb = ['TINYBLOB', 'BLOB', 'LONGBLOB'];
|
||||
BaseTypes.DECIMAL.types.mariadb = ['NEWDECIMAL'];
|
||||
BaseTypes.UUID.types.mariadb = false;
|
||||
BaseTypes.ENUM.types.mariadb = false;
|
||||
BaseTypes.REAL.types.mariadb = ['DOUBLE'];
|
||||
BaseTypes.DOUBLE.types.mariadb = ['DOUBLE'];
|
||||
BaseTypes.GEOMETRY.types.mariadb = ['GEOMETRY'];
|
||||
BaseTypes.JSON.types.mariadb = ['JSON'];
|
||||
|
||||
class DECIMAL extends BaseTypes.DECIMAL {
|
||||
toSql() {
|
||||
let definition = super.toSql();
|
||||
if (this._unsigned) {
|
||||
definition += ' UNSIGNED';
|
||||
}
|
||||
if (this._zerofill) {
|
||||
definition += ' ZEROFILL';
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
|
||||
class DATE extends BaseTypes.DATE {
|
||||
toSql() {
|
||||
return `DATETIME${this._length ? `(${this._length})` : ''}`;
|
||||
}
|
||||
_stringify(date, options) {
|
||||
date = this._applyTimezone(date, options);
|
||||
return date.format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
}
|
||||
static parse(value, options) {
|
||||
value = value.string();
|
||||
if (value === null) {
|
||||
return value;
|
||||
}
|
||||
if (moment.tz.zone(options.timezone)) {
|
||||
value = moment.tz(value, options.timezone).toDate();
|
||||
}
|
||||
else {
|
||||
value = new Date(`${value} ${options.timezone}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
class DATEONLY extends BaseTypes.DATEONLY {
|
||||
static parse(value) {
|
||||
return value.string();
|
||||
}
|
||||
}
|
||||
|
||||
class UUID extends BaseTypes.UUID {
|
||||
toSql() {
|
||||
return 'CHAR(36) BINARY';
|
||||
}
|
||||
}
|
||||
|
||||
class GEOMETRY extends BaseTypes.GEOMETRY {
|
||||
constructor(type, srid) {
|
||||
super(type, srid);
|
||||
if (_.isEmpty(this.type)) {
|
||||
this.sqlType = this.key;
|
||||
}
|
||||
else {
|
||||
this.sqlType = this.type;
|
||||
}
|
||||
}
|
||||
toSql() {
|
||||
return this.sqlType;
|
||||
}
|
||||
}
|
||||
|
||||
class ENUM extends BaseTypes.ENUM {
|
||||
toSql(options) {
|
||||
return `ENUM(${this.values.map(value => options.escape(value)).join(', ')})`;
|
||||
}
|
||||
}
|
||||
|
||||
class JSONTYPE extends BaseTypes.JSON {
|
||||
_stringify(value, options) {
|
||||
return options.operation === 'where' && typeof value === 'string' ? value
|
||||
: JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ENUM,
|
||||
DATE,
|
||||
DATEONLY,
|
||||
UUID,
|
||||
GEOMETRY,
|
||||
DECIMAL,
|
||||
JSON: JSONTYPE
|
||||
};
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
'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').mariadb;
|
||||
|
||||
class MariadbDialect extends AbstractDialect {
|
||||
constructor(sequelize) {
|
||||
super();
|
||||
this.sequelize = sequelize;
|
||||
this.connectionManager = new ConnectionManager(this, sequelize);
|
||||
this.QueryGenerator = new QueryGenerator({
|
||||
_dialect: this,
|
||||
sequelize
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MariadbDialect.prototype.supports = _.merge(
|
||||
_.cloneDeep(AbstractDialect.prototype.supports), {
|
||||
'VALUES ()': true,
|
||||
'LIMIT ON UPDATE': true,
|
||||
lock: true,
|
||||
forShare: 'LOCK IN SHARE MODE',
|
||||
settingIsolationLevelDuringTransaction: false,
|
||||
schemas: true,
|
||||
inserts: {
|
||||
ignoreDuplicates: ' IGNORE',
|
||||
updateOnDuplicate: ' ON DUPLICATE KEY UPDATE'
|
||||
},
|
||||
index: {
|
||||
collate: false,
|
||||
length: true,
|
||||
parser: true,
|
||||
type: true,
|
||||
using: 1
|
||||
},
|
||||
constraints: {
|
||||
dropConstraint: false,
|
||||
check: false
|
||||
},
|
||||
indexViaAlter: true,
|
||||
NUMERIC: true,
|
||||
GEOMETRY: true,
|
||||
JSON: true,
|
||||
REGEXP: true
|
||||
});
|
||||
|
||||
ConnectionManager.prototype.defaultVersion = '5.5.3';
|
||||
MariadbDialect.prototype.Query = Query;
|
||||
MariadbDialect.prototype.QueryGenerator = QueryGenerator;
|
||||
MariadbDialect.prototype.DataTypes = DataTypes;
|
||||
MariadbDialect.prototype.name = 'mariadb';
|
||||
MariadbDialect.prototype.TICK_CHAR = '`';
|
||||
MariadbDialect.prototype.TICK_CHAR_LEFT = MariadbDialect.prototype.TICK_CHAR;
|
||||
MariadbDialect.prototype.TICK_CHAR_RIGHT = MariadbDialect.prototype.TICK_CHAR;
|
||||
|
||||
module.exports = MariadbDialect;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
const MySQLQueryGenerator = require('../mysql/query-generator');
|
||||
|
||||
class MariaDBQueryGenerator extends MySQLQueryGenerator {
|
||||
createSchema(schema, options) {
|
||||
options = Object.assign({
|
||||
charset: null,
|
||||
collate: null
|
||||
}, options || {});
|
||||
|
||||
const charset = options.charset ? ` DEFAULT CHARACTER SET ${this.escape(options.charset)}` : '';
|
||||
const collate = options.collate ? ` DEFAULT COLLATE ${this.escape(options.collate)}` : '';
|
||||
|
||||
return `CREATE SCHEMA IF NOT EXISTS ${this.quoteIdentifier(schema)}${charset}${collate};`;
|
||||
}
|
||||
|
||||
dropSchema(schema) {
|
||||
return `DROP SCHEMA IF EXISTS ${this.quoteIdentifier(schema)};`;
|
||||
}
|
||||
|
||||
showSchemasQuery(options) {
|
||||
const skip = options.skip && Array.isArray(options.skip) && options.skip.length > 0 ? options.skip : null;
|
||||
return `SELECT SCHEMA_NAME as schema_name FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('MYSQL', 'INFORMATION_SCHEMA', 'PERFORMANCE_SCHEMA'${skip ? skip.reduce( (sql, schemaName) => sql += `,${this.escape(schemaName)}`, '') : ''});`;
|
||||
}
|
||||
|
||||
showTablesQuery(database) {
|
||||
let query = 'SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = \'BASE TABLE\'';
|
||||
if (database) {
|
||||
query += ` AND TABLE_SCHEMA = ${this.escape(database)}`;
|
||||
} else {
|
||||
query += ' AND TABLE_SCHEMA NOT IN (\'MYSQL\', \'INFORMATION_SCHEMA\', \'PERFORMANCE_SCHEMA\')';
|
||||
}
|
||||
return `${query};`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MariaDBQueryGenerator;
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
'use strict';
|
||||
|
||||
const AbstractQuery = require('../abstract/query');
|
||||
const sequelizeErrors = require('../../errors');
|
||||
const _ = require('lodash');
|
||||
const DataTypes = require('../../data-types');
|
||||
const Promise = require('../../promise');
|
||||
const { logger } = require('../../utils/logger');
|
||||
|
||||
const ER_DUP_ENTRY = 1062;
|
||||
const ER_ROW_IS_REFERENCED = 1451;
|
||||
const ER_NO_REFERENCED_ROW = 1452;
|
||||
|
||||
const debug = logger.debugContext('sql:mariadb');
|
||||
|
||||
class Query extends AbstractQuery {
|
||||
constructor(connection, sequelize, options) {
|
||||
super(connection, sequelize, Object.assign({ showWarnings: false }, options));
|
||||
}
|
||||
|
||||
static formatBindParameters(sql, values, dialect) {
|
||||
const bindParam = [];
|
||||
const replacementFunc = (match, key, val) => {
|
||||
if (val[key] !== undefined) {
|
||||
bindParam.push(val[key]);
|
||||
return '?';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
sql = AbstractQuery.formatBindParameters(sql, values, dialect,
|
||||
replacementFunc)[0];
|
||||
return [sql, bindParam.length > 0 ? bindParam : undefined];
|
||||
}
|
||||
|
||||
run(sql, parameters) {
|
||||
this.sql = sql;
|
||||
const { connection, options } = this;
|
||||
|
||||
const showWarnings = this.sequelize.options.showWarnings
|
||||
|| options.showWarnings;
|
||||
|
||||
const complete = this._logQuery(sql, debug, parameters);
|
||||
|
||||
if (parameters) {
|
||||
debug('parameters(%j)', parameters);
|
||||
}
|
||||
return Promise.resolve(
|
||||
connection.query(this.sql, parameters)
|
||||
.then(results => {
|
||||
complete();
|
||||
|
||||
// Log warnings if we've got them.
|
||||
if (showWarnings && results && results.warningStatus > 0) {
|
||||
return this.logWarnings(results);
|
||||
}
|
||||
return results;
|
||||
})
|
||||
.catch(err => {
|
||||
// MariaDB automatically rolls-back transactions in the event of a deadlock
|
||||
if (options.transaction && err.errno === 1213) {
|
||||
options.transaction.finished = 'rollback';
|
||||
}
|
||||
|
||||
complete();
|
||||
|
||||
err.sql = sql;
|
||||
err.parameters = parameters;
|
||||
throw this.formatError(err);
|
||||
})
|
||||
)
|
||||
// Log warnings if we've got them.
|
||||
.then(results => {
|
||||
if (showWarnings && results && results.warningStatus > 0) {
|
||||
return this.logWarnings(results);
|
||||
}
|
||||
return results;
|
||||
})
|
||||
// Return formatted results...
|
||||
.then(results => this.formatResults(results));
|
||||
}
|
||||
|
||||
/**
|
||||
* High level function that handles the results of a query execution.
|
||||
*
|
||||
*
|
||||
* Example:
|
||||
* query.formatResults([
|
||||
* {
|
||||
* id: 1, // this is from the main table
|
||||
* attr2: 'snafu', // this is from the main table
|
||||
* Tasks.id: 1, // this is from the associated table
|
||||
* Tasks.title: 'task' // this is from the associated table
|
||||
* }
|
||||
* ])
|
||||
*
|
||||
* @param {Array} data - The result of the query execution.
|
||||
* @private
|
||||
*/
|
||||
formatResults(data) {
|
||||
let result = this.instance;
|
||||
|
||||
if (this.isBulkUpdateQuery() || this.isBulkDeleteQuery()
|
||||
|| this.isUpsertQuery()) {
|
||||
return data.affectedRows;
|
||||
}
|
||||
if (this.isInsertQuery(data)) {
|
||||
this.handleInsertQuery(data);
|
||||
|
||||
if (!this.instance) {
|
||||
// handle bulkCreate AI primary key
|
||||
if (this.model
|
||||
&& this.model.autoIncrementAttribute
|
||||
&& this.model.autoIncrementAttribute === this.model.primaryKeyAttribute
|
||||
&& this.model.rawAttributes[this.model.primaryKeyAttribute]
|
||||
) {
|
||||
//ONLY TRUE IF @auto_increment_increment is set to 1 !!
|
||||
//Doesn't work with GALERA => each node will reserve increment (x for first server, x+1 for next node ...
|
||||
const startId = data[this.getInsertIdField()];
|
||||
result = new Array(data.affectedRows);
|
||||
const pkField = this.model.rawAttributes[this.model.primaryKeyAttribute].field;
|
||||
for (let i = 0; i < data.affectedRows; i++) {
|
||||
result[i] = { [pkField]: startId + i };
|
||||
}
|
||||
return [result, data.affectedRows];
|
||||
}
|
||||
return [data[this.getInsertIdField()], data.affectedRows];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isSelectQuery()) {
|
||||
this.handleJsonSelectQuery(data);
|
||||
return this.handleSelectQuery(data);
|
||||
}
|
||||
if (this.isInsertQuery() || this.isUpdateQuery()) {
|
||||
return [result, data.affectedRows];
|
||||
}
|
||||
if (this.isCallQuery()) {
|
||||
return data[0];
|
||||
}
|
||||
if (this.isRawQuery()) {
|
||||
const meta = data.meta;
|
||||
delete data.meta;
|
||||
return [data, meta];
|
||||
}
|
||||
if (this.isShowIndexesQuery()) {
|
||||
return this.handleShowIndexesQuery(data);
|
||||
}
|
||||
if (this.isForeignKeysQuery() || this.isShowConstraintsQuery()) {
|
||||
return data;
|
||||
}
|
||||
if (this.isShowTablesQuery()) {
|
||||
return this.handleShowTablesQuery(data);
|
||||
}
|
||||
if (this.isDescribeQuery()) {
|
||||
result = {};
|
||||
|
||||
for (const _result of data) {
|
||||
result[_result.Field] = {
|
||||
type: _result.Type.toLowerCase().startsWith('enum') ? _result.Type.replace(/^enum/i,
|
||||
'ENUM') : _result.Type.toUpperCase(),
|
||||
allowNull: _result.Null === 'YES',
|
||||
defaultValue: _result.Default,
|
||||
primaryKey: _result.Key === 'PRI',
|
||||
autoIncrement: Object.prototype.hasOwnProperty.call(_result, 'Extra')
|
||||
&& _result.Extra.toLowerCase() === 'auto_increment',
|
||||
comment: _result.Comment ? _result.Comment : null
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (this.isVersionQuery()) {
|
||||
return data[0].version;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
handleJsonSelectQuery(rows) {
|
||||
if (!this.model || !this.model.fieldRawAttributesMap) {
|
||||
return;
|
||||
}
|
||||
for (const _field of Object.keys(this.model.fieldRawAttributesMap)) {
|
||||
const modelField = this.model.fieldRawAttributesMap[_field];
|
||||
if (modelField.type instanceof DataTypes.JSON) {
|
||||
//value is return as String, no JSON
|
||||
rows = rows.map(row => {
|
||||
row[modelField.fieldName] = row[modelField.fieldName] ? JSON.parse(
|
||||
row[modelField.fieldName]) : null;
|
||||
if (DataTypes.JSON.parse) {
|
||||
return DataTypes.JSON.parse(modelField, this.sequelize.options,
|
||||
row[modelField.fieldName]);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logWarnings(results) {
|
||||
return this.run('SHOW WARNINGS').then(warningResults => {
|
||||
const warningMessage = `MariaDB Warnings (${this.connection.uuid
|
||||
|| 'default'}): `;
|
||||
const messages = [];
|
||||
for (const _warningRow of warningResults) {
|
||||
if (_warningRow === undefined || typeof _warningRow[Symbol.iterator]
|
||||
!== 'function') {
|
||||
continue;
|
||||
}
|
||||
for (const _warningResult of _warningRow) {
|
||||
if (Object.prototype.hasOwnProperty.call(_warningResult, 'Message')) {
|
||||
messages.push(_warningResult.Message);
|
||||
} else {
|
||||
for (const _objectKey of _warningResult.keys()) {
|
||||
messages.push(
|
||||
[_objectKey, _warningResult[_objectKey]].join(': '));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.sequelize.log(warningMessage + messages.join('; '), this.options);
|
||||
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
formatError(err) {
|
||||
switch (err.errno) {
|
||||
case ER_DUP_ENTRY: {
|
||||
const match = err.message.match(
|
||||
/Duplicate entry '([\s\S]*)' for key '?((.|\s)*?)'?\s.*$/);
|
||||
|
||||
let fields = {};
|
||||
let message = 'Validation error';
|
||||
const values = match ? match[1].split('-') : undefined;
|
||||
const fieldKey = match ? match[2] : undefined;
|
||||
const fieldVal = match ? match[1] : undefined;
|
||||
const uniqueKey = this.model && this.model.uniqueKeys[fieldKey];
|
||||
|
||||
if (uniqueKey) {
|
||||
if (uniqueKey.msg) {
|
||||
message = uniqueKey.msg;
|
||||
}
|
||||
fields = _.zipObject(uniqueKey.fields, values);
|
||||
} else {
|
||||
fields[fieldKey] = fieldVal;
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
_.forOwn(fields, (value, field) => {
|
||||
errors.push(new sequelizeErrors.ValidationErrorItem(
|
||||
this.getUniqueConstraintErrorMessage(field),
|
||||
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
|
||||
field,
|
||||
value,
|
||||
this.instance,
|
||||
'not_unique'
|
||||
));
|
||||
});
|
||||
|
||||
return new sequelizeErrors.UniqueConstraintError(
|
||||
{ message, errors, parent: err, fields });
|
||||
}
|
||||
|
||||
case ER_ROW_IS_REFERENCED:
|
||||
case ER_NO_REFERENCED_ROW: {
|
||||
// e.g. CONSTRAINT `example_constraint_name` FOREIGN KEY (`example_id`) REFERENCES `examples` (`id`)
|
||||
const match = err.message.match(
|
||||
/CONSTRAINT ([`"])(.*)\1 FOREIGN KEY \(\1(.*)\1\) REFERENCES \1(.*)\1 \(\1(.*)\1\)/);
|
||||
const quoteChar = match ? match[1] : '`';
|
||||
const fields = match ? match[3].split(
|
||||
new RegExp(`${quoteChar}, *${quoteChar}`)) : undefined;
|
||||
return new sequelizeErrors.ForeignKeyConstraintError({
|
||||
reltype: err.errno === 1451 ? 'parent' : 'child',
|
||||
table: match ? match[4] : undefined,
|
||||
fields,
|
||||
value: fields && fields.length && this.instance
|
||||
&& this.instance[fields[0]] || undefined,
|
||||
index: match ? match[2] : undefined,
|
||||
parent: err
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return new sequelizeErrors.DatabaseError(err);
|
||||
}
|
||||
}
|
||||
|
||||
handleShowTablesQuery(results) {
|
||||
return results.map(resultSet => ({
|
||||
tableName: resultSet.TABLE_NAME,
|
||||
schema: resultSet.TABLE_SCHEMA
|
||||
}));
|
||||
}
|
||||
|
||||
handleShowIndexesQuery(data) {
|
||||
|
||||
let currItem;
|
||||
const result = [];
|
||||
|
||||
data.forEach(item => {
|
||||
if (!currItem || currItem.name !== item.Key_name) {
|
||||
currItem = {
|
||||
primary: item.Key_name === 'PRIMARY',
|
||||
fields: [],
|
||||
name: item.Key_name,
|
||||
tableName: item.Table,
|
||||
unique: item.Non_unique !== 1,
|
||||
type: item.Index_type
|
||||
};
|
||||
result.push(currItem);
|
||||
}
|
||||
|
||||
currItem.fields[item.Seq_in_index - 1] = {
|
||||
attribute: item.Column_name,
|
||||
length: item.Sub_part || undefined,
|
||||
order: item.Collation === 'A' ? 'ASC' : undefined
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Query;
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
'use strict';
|
||||
|
||||
const AbstractConnectionManager = require('../abstract/connection-manager');
|
||||
const ResourceLock = require('./resource-lock');
|
||||
const Promise = require('../../promise');
|
||||
const { logger } = require('../../utils/logger');
|
||||
const sequelizeErrors = require('../../errors');
|
||||
const DataTypes = require('../../data-types').mssql;
|
||||
const parserStore = require('../parserStore')('mssql');
|
||||
const debug = logger.debugContext('connection:mssql');
|
||||
const debugTedious = logger.debugContext('connection:mssql:tedious');
|
||||
|
||||
class ConnectionManager extends AbstractConnectionManager {
|
||||
constructor(dialect, sequelize) {
|
||||
sequelize.config.port = sequelize.config.port || 1433;
|
||||
super(dialect, sequelize);
|
||||
this.lib = this._loadDialectModule('tedious');
|
||||
this.refreshTypeParser(DataTypes);
|
||||
}
|
||||
|
||||
_refreshTypeParser(dataType) {
|
||||
parserStore.refresh(dataType);
|
||||
}
|
||||
|
||||
_clearTypeParser() {
|
||||
parserStore.clear();
|
||||
}
|
||||
|
||||
connect(config) {
|
||||
const connectionConfig = {
|
||||
server: config.host,
|
||||
authentication: {
|
||||
type: 'default',
|
||||
options: {
|
||||
userName: config.username || undefined,
|
||||
password: config.password || undefined
|
||||
}
|
||||
},
|
||||
options: {
|
||||
port: parseInt(config.port, 10),
|
||||
database: config.database,
|
||||
encrypt: false
|
||||
}
|
||||
};
|
||||
|
||||
if (config.dialectOptions) {
|
||||
// only set port if no instance name was provided
|
||||
if (
|
||||
config.dialectOptions.options &&
|
||||
config.dialectOptions.options.instanceName
|
||||
) {
|
||||
delete connectionConfig.options.port;
|
||||
}
|
||||
|
||||
if (config.dialectOptions.authentication) {
|
||||
Object.assign(connectionConfig.authentication, config.dialectOptions.authentication);
|
||||
}
|
||||
|
||||
Object.assign(connectionConfig.options, config.dialectOptions.options);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const connection = new this.lib.Connection(connectionConfig);
|
||||
connection.lib = this.lib;
|
||||
const resourceLock = new ResourceLock(connection);
|
||||
|
||||
const connectHandler = error => {
|
||||
connection.removeListener('end', endHandler);
|
||||
connection.removeListener('error', errorHandler);
|
||||
|
||||
if (error) return reject(error);
|
||||
|
||||
debug('connection acquired');
|
||||
resolve(resourceLock);
|
||||
};
|
||||
|
||||
const endHandler = () => {
|
||||
connection.removeListener('connect', connectHandler);
|
||||
connection.removeListener('error', errorHandler);
|
||||
reject(new Error('Connection was closed by remote server'));
|
||||
};
|
||||
|
||||
const errorHandler = error => {
|
||||
connection.removeListener('connect', connectHandler);
|
||||
connection.removeListener('end', endHandler);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
connection.once('error', errorHandler);
|
||||
connection.once('end', endHandler);
|
||||
connection.once('connect', connectHandler);
|
||||
|
||||
/*
|
||||
* Permanently attach this event before connection is even acquired
|
||||
* tedious sometime emits error even after connect(with error).
|
||||
*
|
||||
* If we dont attach this even that unexpected error event will crash node process
|
||||
*
|
||||
* E.g. connectTimeout is set higher than requestTimeout
|
||||
*/
|
||||
connection.on('error', error => {
|
||||
switch (error.code) {
|
||||
case 'ESOCKET':
|
||||
case 'ECONNRESET':
|
||||
this.pool.destroy(resourceLock);
|
||||
}
|
||||
});
|
||||
|
||||
if (config.dialectOptions && config.dialectOptions.debug) {
|
||||
connection.on('debug', debugTedious.log.bind(debugTedious));
|
||||
}
|
||||
}).catch(error => {
|
||||
if (!error.code) {
|
||||
throw new sequelizeErrors.ConnectionError(error);
|
||||
}
|
||||
|
||||
switch (error.code) {
|
||||
case 'ESOCKET':
|
||||
if (error.message.includes('connect EHOSTUNREACH')) {
|
||||
throw new sequelizeErrors.HostNotReachableError(error);
|
||||
}
|
||||
if (error.message.includes('connect ENETUNREACH')) {
|
||||
throw new sequelizeErrors.HostNotReachableError(error);
|
||||
}
|
||||
if (error.message.includes('connect EADDRNOTAVAIL')) {
|
||||
throw new sequelizeErrors.HostNotReachableError(error);
|
||||
}
|
||||
if (error.message.includes('getaddrinfo ENOTFOUND')) {
|
||||
throw new sequelizeErrors.HostNotFoundError(error);
|
||||
}
|
||||
if (error.message.includes('connect ECONNREFUSED')) {
|
||||
throw new sequelizeErrors.ConnectionRefusedError(error);
|
||||
}
|
||||
throw new sequelizeErrors.ConnectionError(error);
|
||||
case 'ER_ACCESS_DENIED_ERROR':
|
||||
case 'ELOGIN':
|
||||
throw new sequelizeErrors.AccessDeniedError(error);
|
||||
case 'EINVAL':
|
||||
throw new sequelizeErrors.InvalidConnectionError(error);
|
||||
default:
|
||||
throw new sequelizeErrors.ConnectionError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
disconnect(connectionLock) {
|
||||
/**
|
||||
* Abstract connection may try to disconnect raw connection used for fetching version
|
||||
*/
|
||||
const connection = connectionLock.unwrap
|
||||
? connectionLock.unwrap()
|
||||
: connectionLock;
|
||||
|
||||
// Don't disconnect a connection that is already disconnected
|
||||
if (connection.closed) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
connection.on('end', resolve);
|
||||
connection.close();
|
||||
debug('connection closed');
|
||||
});
|
||||
}
|
||||
|
||||
validate(connectionLock) {
|
||||
/**
|
||||
* Abstract connection may try to validate raw connection used for fetching version
|
||||
*/
|
||||
const connection = connectionLock.unwrap
|
||||
? connectionLock.unwrap()
|
||||
: connectionLock;
|
||||
|
||||
return connection && connection.loggedIn;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConnectionManager;
|
||||
module.exports.ConnectionManager = ConnectionManager;
|
||||
module.exports.default = ConnectionManager;
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
'use strict';
|
||||
|
||||
const moment = require('moment');
|
||||
|
||||
module.exports = BaseTypes => {
|
||||
const warn = BaseTypes.ABSTRACT.warn.bind(undefined, 'https://msdn.microsoft.com/en-us/library/ms187752%28v=sql.110%29.aspx');
|
||||
|
||||
/**
|
||||
* Removes unsupported MSSQL options, i.e., LENGTH, UNSIGNED and ZEROFILL, for the integer data types.
|
||||
*
|
||||
* @param {Object} dataType The base integer data type.
|
||||
* @private
|
||||
*/
|
||||
function removeUnsupportedIntegerOptions(dataType) {
|
||||
if (dataType._length || dataType.options.length || dataType._unsigned || dataType._zerofill) {
|
||||
warn(`MSSQL does not support '${dataType.key}' with options. Plain '${dataType.key}' will be used instead.`);
|
||||
dataType._length = undefined;
|
||||
dataType.options.length = undefined;
|
||||
dataType._unsigned = undefined;
|
||||
dataType._zerofill = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* types: [hex, ...]
|
||||
* @see hex here https://github.com/tediousjs/tedious/blob/master/src/data-type.js
|
||||
*/
|
||||
|
||||
BaseTypes.DATE.types.mssql = [43];
|
||||
BaseTypes.STRING.types.mssql = [231, 173];
|
||||
BaseTypes.CHAR.types.mssql = [175];
|
||||
BaseTypes.TEXT.types.mssql = false;
|
||||
// https://msdn.microsoft.com/en-us/library/ms187745(v=sql.110).aspx
|
||||
BaseTypes.TINYINT.types.mssql = [30];
|
||||
BaseTypes.SMALLINT.types.mssql = [34];
|
||||
BaseTypes.MEDIUMINT.types.mssql = false;
|
||||
BaseTypes.INTEGER.types.mssql = [38];
|
||||
BaseTypes.BIGINT.types.mssql = false;
|
||||
BaseTypes.FLOAT.types.mssql = [109];
|
||||
BaseTypes.TIME.types.mssql = [41];
|
||||
BaseTypes.DATEONLY.types.mssql = [40];
|
||||
BaseTypes.BOOLEAN.types.mssql = [104];
|
||||
BaseTypes.BLOB.types.mssql = [165];
|
||||
BaseTypes.DECIMAL.types.mssql = [106];
|
||||
BaseTypes.UUID.types.mssql = false;
|
||||
BaseTypes.ENUM.types.mssql = false;
|
||||
BaseTypes.REAL.types.mssql = [109];
|
||||
BaseTypes.DOUBLE.types.mssql = [109];
|
||||
// BaseTypes.GEOMETRY.types.mssql = [240]; // not yet supported
|
||||
BaseTypes.GEOMETRY.types.mssql = false;
|
||||
|
||||
class BLOB extends BaseTypes.BLOB {
|
||||
toSql() {
|
||||
if (this._length) {
|
||||
if (this._length.toLowerCase() === 'tiny') { // tiny = 2^8
|
||||
warn('MSSQL does not support BLOB with the `length` = `tiny` option. `VARBINARY(256)` will be used instead.');
|
||||
return 'VARBINARY(256)';
|
||||
}
|
||||
warn('MSSQL does not support BLOB with the `length` option. `VARBINARY(MAX)` will be used instead.');
|
||||
}
|
||||
return 'VARBINARY(MAX)';
|
||||
}
|
||||
_hexify(hex) {
|
||||
return `0x${hex}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class STRING extends BaseTypes.STRING {
|
||||
toSql() {
|
||||
if (!this._binary) {
|
||||
return `NVARCHAR(${this._length})`;
|
||||
}
|
||||
return `BINARY(${this._length})`;
|
||||
}
|
||||
_stringify(value, options) {
|
||||
if (this._binary) {
|
||||
return BLOB.prototype._stringify(value);
|
||||
}
|
||||
return options.escape(value);
|
||||
}
|
||||
_bindParam(value, options) {
|
||||
return options.bindParam(this._binary ? Buffer.from(value) : value);
|
||||
}
|
||||
}
|
||||
|
||||
STRING.prototype.escape = false;
|
||||
|
||||
class TEXT extends BaseTypes.TEXT {
|
||||
toSql() {
|
||||
// TEXT is deprecated in mssql and it would normally be saved as a non-unicode string.
|
||||
// Using unicode is just future proof
|
||||
if (this._length) {
|
||||
if (this._length.toLowerCase() === 'tiny') { // tiny = 2^8
|
||||
warn('MSSQL does not support TEXT with the `length` = `tiny` option. `NVARCHAR(256)` will be used instead.');
|
||||
return 'NVARCHAR(256)';
|
||||
}
|
||||
warn('MSSQL does not support TEXT with the `length` option. `NVARCHAR(MAX)` will be used instead.');
|
||||
}
|
||||
return 'NVARCHAR(MAX)';
|
||||
}
|
||||
}
|
||||
|
||||
class BOOLEAN extends BaseTypes.BOOLEAN {
|
||||
toSql() {
|
||||
return 'BIT';
|
||||
}
|
||||
}
|
||||
|
||||
class UUID extends BaseTypes.UUID {
|
||||
toSql() {
|
||||
return 'CHAR(36)';
|
||||
}
|
||||
}
|
||||
|
||||
class NOW extends BaseTypes.NOW {
|
||||
toSql() {
|
||||
return 'GETDATE()';
|
||||
}
|
||||
}
|
||||
|
||||
class DATE extends BaseTypes.DATE {
|
||||
toSql() {
|
||||
return 'DATETIMEOFFSET';
|
||||
}
|
||||
}
|
||||
|
||||
class DATEONLY extends BaseTypes.DATEONLY {
|
||||
static parse(value) {
|
||||
return moment(value).format('YYYY-MM-DD');
|
||||
}
|
||||
}
|
||||
|
||||
class INTEGER extends BaseTypes.INTEGER {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
class TINYINT extends BaseTypes.TINYINT {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
class SMALLINT extends BaseTypes.SMALLINT {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
class BIGINT extends BaseTypes.BIGINT {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
class REAL extends BaseTypes.REAL {
|
||||
constructor(length, decimals) {
|
||||
super(length, decimals);
|
||||
// MSSQL does not support any options for real
|
||||
if (this._length || this.options.length || this._unsigned || this._zerofill) {
|
||||
warn('MSSQL does not support REAL with options. Plain `REAL` will be used instead.');
|
||||
this._length = undefined;
|
||||
this.options.length = undefined;
|
||||
this._unsigned = undefined;
|
||||
this._zerofill = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
class FLOAT extends BaseTypes.FLOAT {
|
||||
constructor(length, decimals) {
|
||||
super(length, decimals);
|
||||
// MSSQL does only support lengths as option.
|
||||
// Values between 1-24 result in 7 digits precision (4 bytes storage size)
|
||||
// Values between 25-53 result in 15 digits precision (8 bytes storage size)
|
||||
// If decimals are provided remove these and print a warning
|
||||
if (this._decimals) {
|
||||
warn('MSSQL does not support Float with decimals. Plain `FLOAT` will be used instead.');
|
||||
this._length = undefined;
|
||||
this.options.length = undefined;
|
||||
}
|
||||
if (this._unsigned) {
|
||||
warn('MSSQL does not support Float unsigned. `UNSIGNED` was removed.');
|
||||
this._unsigned = undefined;
|
||||
}
|
||||
if (this._zerofill) {
|
||||
warn('MSSQL does not support Float zerofill. `ZEROFILL` was removed.');
|
||||
this._zerofill = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
class ENUM extends BaseTypes.ENUM {
|
||||
toSql() {
|
||||
return 'VARCHAR(255)';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
BLOB,
|
||||
BOOLEAN,
|
||||
ENUM,
|
||||
STRING,
|
||||
UUID,
|
||||
DATE,
|
||||
DATEONLY,
|
||||
NOW,
|
||||
TINYINT,
|
||||
SMALLINT,
|
||||
INTEGER,
|
||||
BIGINT,
|
||||
REAL,
|
||||
FLOAT,
|
||||
TEXT
|
||||
};
|
||||
};
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
'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').mssql;
|
||||
|
||||
class MssqlDialect extends AbstractDialect {
|
||||
constructor(sequelize) {
|
||||
super();
|
||||
this.sequelize = sequelize;
|
||||
this.connectionManager = new ConnectionManager(this, sequelize);
|
||||
this.QueryGenerator = new QueryGenerator({
|
||||
_dialect: this,
|
||||
sequelize
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MssqlDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype.supports), {
|
||||
'DEFAULT': true,
|
||||
'DEFAULT VALUES': true,
|
||||
'LIMIT ON UPDATE': true,
|
||||
'ORDER NULLS': false,
|
||||
lock: false,
|
||||
transactions: true,
|
||||
migrations: false,
|
||||
returnValues: {
|
||||
output: true
|
||||
},
|
||||
schemas: true,
|
||||
autoIncrement: {
|
||||
identityInsert: true,
|
||||
defaultValue: false,
|
||||
update: false
|
||||
},
|
||||
constraints: {
|
||||
restrict: false,
|
||||
default: true
|
||||
},
|
||||
index: {
|
||||
collate: false,
|
||||
length: false,
|
||||
parser: false,
|
||||
type: true,
|
||||
using: false,
|
||||
where: true
|
||||
},
|
||||
NUMERIC: true,
|
||||
tmpTableTrigger: true
|
||||
});
|
||||
|
||||
ConnectionManager.prototype.defaultVersion = '12.0.2000'; // SQL Server 2014 Express
|
||||
MssqlDialect.prototype.Query = Query;
|
||||
MssqlDialect.prototype.name = 'mssql';
|
||||
MssqlDialect.prototype.TICK_CHAR = '"';
|
||||
MssqlDialect.prototype.TICK_CHAR_LEFT = '[';
|
||||
MssqlDialect.prototype.TICK_CHAR_RIGHT = ']';
|
||||
MssqlDialect.prototype.DataTypes = DataTypes;
|
||||
|
||||
module.exports = MssqlDialect;
|
||||
+899
@@ -0,0 +1,899 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const Utils = require('../../utils');
|
||||
const DataTypes = require('../../data-types');
|
||||
const TableHints = require('../../table-hints');
|
||||
const AbstractQueryGenerator = require('../abstract/query-generator');
|
||||
const randomBytes = require('crypto').randomBytes;
|
||||
const semver = require('semver');
|
||||
const Op = require('../../operators');
|
||||
|
||||
/* istanbul ignore next */
|
||||
const throwMethodUndefined = function(methodName) {
|
||||
throw new Error(`The method "${methodName}" is not defined! Please add it to your sql dialect.`);
|
||||
};
|
||||
|
||||
class MSSQLQueryGenerator extends AbstractQueryGenerator {
|
||||
createDatabaseQuery(databaseName, options) {
|
||||
options = Object.assign({
|
||||
collate: null
|
||||
}, options || {});
|
||||
|
||||
const collation = options.collate ? `COLLATE ${this.escape(options.collate)}` : '';
|
||||
|
||||
return [
|
||||
'IF NOT EXISTS (SELECT * FROM sys.databases WHERE name =', wrapSingleQuote(databaseName), ')',
|
||||
'BEGIN',
|
||||
'CREATE DATABASE', this.quoteIdentifier(databaseName),
|
||||
`${collation};`,
|
||||
'END;'
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
dropDatabaseQuery(databaseName) {
|
||||
return [
|
||||
'IF EXISTS (SELECT * FROM sys.databases WHERE name =', wrapSingleQuote(databaseName), ')',
|
||||
'BEGIN',
|
||||
'DROP DATABASE', this.quoteIdentifier(databaseName), ';',
|
||||
'END;'
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
createSchema(schema) {
|
||||
return [
|
||||
'IF NOT EXISTS (SELECT schema_name',
|
||||
'FROM information_schema.schemata',
|
||||
'WHERE schema_name =', wrapSingleQuote(schema), ')',
|
||||
'BEGIN',
|
||||
"EXEC sp_executesql N'CREATE SCHEMA",
|
||||
this.quoteIdentifier(schema),
|
||||
";'",
|
||||
'END;'
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
dropSchema(schema) {
|
||||
// Mimics Postgres CASCADE, will drop objects belonging to the schema
|
||||
const quotedSchema = wrapSingleQuote(schema);
|
||||
return [
|
||||
'IF EXISTS (SELECT schema_name',
|
||||
'FROM information_schema.schemata',
|
||||
'WHERE schema_name =', quotedSchema, ')',
|
||||
'BEGIN',
|
||||
'DECLARE @id INT, @ms_sql NVARCHAR(2000);',
|
||||
'DECLARE @cascade TABLE (',
|
||||
'id INT NOT NULL IDENTITY PRIMARY KEY,',
|
||||
'ms_sql NVARCHAR(2000) NOT NULL );',
|
||||
'INSERT INTO @cascade ( ms_sql )',
|
||||
"SELECT CASE WHEN o.type IN ('F','PK')",
|
||||
"THEN N'ALTER TABLE ['+ s.name + N'].[' + p.name + N'] DROP CONSTRAINT [' + o.name + N']'",
|
||||
"ELSE N'DROP TABLE ['+ s.name + N'].[' + o.name + N']' END",
|
||||
'FROM sys.objects o',
|
||||
'JOIN sys.schemas s on o.schema_id = s.schema_id',
|
||||
'LEFT OUTER JOIN sys.objects p on o.parent_object_id = p.object_id',
|
||||
"WHERE o.type IN ('F', 'PK', 'U') AND s.name = ", quotedSchema,
|
||||
'ORDER BY o.type ASC;',
|
||||
'SELECT TOP 1 @id = id, @ms_sql = ms_sql FROM @cascade ORDER BY id;',
|
||||
'WHILE @id IS NOT NULL',
|
||||
'BEGIN',
|
||||
'BEGIN TRY EXEC sp_executesql @ms_sql; END TRY',
|
||||
'BEGIN CATCH BREAK; THROW; END CATCH;',
|
||||
'DELETE FROM @cascade WHERE id = @id;',
|
||||
'SELECT @id = NULL, @ms_sql = NULL;',
|
||||
'SELECT TOP 1 @id = id, @ms_sql = ms_sql FROM @cascade ORDER BY id;',
|
||||
'END',
|
||||
"EXEC sp_executesql N'DROP SCHEMA", this.quoteIdentifier(schema), ";'",
|
||||
'END;'
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
showSchemasQuery() {
|
||||
return [
|
||||
'SELECT "name" as "schema_name" FROM sys.schemas as s',
|
||||
'WHERE "s"."name" NOT IN (',
|
||||
"'INFORMATION_SCHEMA', 'dbo', 'guest', 'sys', 'archive'",
|
||||
')', 'AND', '"s"."name" NOT LIKE', "'db_%'"
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
versionQuery() {
|
||||
// Uses string manipulation to convert the MS Maj.Min.Patch.Build to semver Maj.Min.Patch
|
||||
return [
|
||||
'DECLARE @ms_ver NVARCHAR(20);',
|
||||
"SET @ms_ver = REVERSE(CONVERT(NVARCHAR(20), SERVERPROPERTY('ProductVersion')));",
|
||||
"SELECT REVERSE(SUBSTRING(@ms_ver, CHARINDEX('.', @ms_ver)+1, 20)) AS 'version'"
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
createTableQuery(tableName, attributes, options) {
|
||||
const query = (table, attrs) => `IF OBJECT_ID('${table}', 'U') IS NULL CREATE TABLE ${table} (${attrs})`,
|
||||
primaryKeys = [],
|
||||
foreignKeys = {},
|
||||
attrStr = [];
|
||||
|
||||
let commentStr = '';
|
||||
|
||||
for (const attr in attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(attributes, attr)) {
|
||||
let dataType = attributes[attr];
|
||||
let match;
|
||||
|
||||
if (dataType.includes('COMMENT ')) {
|
||||
const commentMatch = dataType.match(/^(.+) (COMMENT.*)$/);
|
||||
const commentText = commentMatch[2].replace('COMMENT', '').trim();
|
||||
commentStr += this.commentTemplate(commentText, tableName, attr);
|
||||
// remove comment related substring from dataType
|
||||
dataType = commentMatch[1];
|
||||
}
|
||||
|
||||
if (dataType.includes('PRIMARY KEY')) {
|
||||
primaryKeys.push(attr);
|
||||
|
||||
if (dataType.includes('REFERENCES')) {
|
||||
// MSSQL doesn't support inline REFERENCES declarations: move to the end
|
||||
match = dataType.match(/^(.+) (REFERENCES.*)$/);
|
||||
attrStr.push(`${this.quoteIdentifier(attr)} ${match[1].replace('PRIMARY KEY', '')}`);
|
||||
foreignKeys[attr] = match[2];
|
||||
} else {
|
||||
attrStr.push(`${this.quoteIdentifier(attr)} ${dataType.replace('PRIMARY KEY', '')}`);
|
||||
}
|
||||
} else if (dataType.includes('REFERENCES')) {
|
||||
// MSSQL doesn't support inline REFERENCES declarations: move to the end
|
||||
match = dataType.match(/^(.+) (REFERENCES.*)$/);
|
||||
attrStr.push(`${this.quoteIdentifier(attr)} ${match[1]}`);
|
||||
foreignKeys[attr] = match[2];
|
||||
} else {
|
||||
attrStr.push(`${this.quoteIdentifier(attr)} ${dataType}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let attributesClause = attrStr.join(', ');
|
||||
const pkString = primaryKeys.map(pk => this.quoteIdentifier(pk)).join(', ');
|
||||
|
||||
if (options.uniqueKeys) {
|
||||
_.each(options.uniqueKeys, (columns, indexName) => {
|
||||
if (columns.customIndex) {
|
||||
if (typeof indexName !== 'string') {
|
||||
indexName = `uniq_${tableName}_${columns.fields.join('_')}`;
|
||||
}
|
||||
attributesClause += `, CONSTRAINT ${this.quoteIdentifier(indexName)} UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pkString.length > 0) {
|
||||
attributesClause += `, PRIMARY KEY (${pkString})`;
|
||||
}
|
||||
|
||||
for (const fkey in foreignKeys) {
|
||||
if (Object.prototype.hasOwnProperty.call(foreignKeys, fkey)) {
|
||||
attributesClause += `, FOREIGN KEY (${this.quoteIdentifier(fkey)}) ${foreignKeys[fkey]}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `${query(this.quoteTable(tableName), attributesClause)};${commentStr}`;
|
||||
}
|
||||
|
||||
describeTableQuery(tableName, schema) {
|
||||
let sql = [
|
||||
'SELECT',
|
||||
"c.COLUMN_NAME AS 'Name',",
|
||||
"c.DATA_TYPE AS 'Type',",
|
||||
"c.CHARACTER_MAXIMUM_LENGTH AS 'Length',",
|
||||
"c.IS_NULLABLE as 'IsNull',",
|
||||
"COLUMN_DEFAULT AS 'Default',",
|
||||
"pk.CONSTRAINT_TYPE AS 'Constraint',",
|
||||
"COLUMNPROPERTY(OBJECT_ID(c.TABLE_SCHEMA+'.'+c.TABLE_NAME), c.COLUMN_NAME, 'IsIdentity') as 'IsIdentity',",
|
||||
"prop.value AS 'Comment'",
|
||||
'FROM',
|
||||
'INFORMATION_SCHEMA.TABLES t',
|
||||
'INNER JOIN',
|
||||
'INFORMATION_SCHEMA.COLUMNS c ON t.TABLE_NAME = c.TABLE_NAME AND t.TABLE_SCHEMA = c.TABLE_SCHEMA',
|
||||
'LEFT JOIN (SELECT tc.table_schema, tc.table_name, ',
|
||||
'cu.column_name, tc.constraint_type ',
|
||||
'FROM information_schema.TABLE_CONSTRAINTS tc ',
|
||||
'JOIN information_schema.KEY_COLUMN_USAGE cu ',
|
||||
'ON tc.table_schema=cu.table_schema and tc.table_name=cu.table_name ',
|
||||
'and tc.constraint_name=cu.constraint_name ',
|
||||
'and tc.constraint_type=\'PRIMARY KEY\') pk ',
|
||||
'ON pk.table_schema=c.table_schema ',
|
||||
'AND pk.table_name=c.table_name ',
|
||||
'AND pk.column_name=c.column_name ',
|
||||
'INNER JOIN sys.columns AS sc',
|
||||
"ON sc.object_id = object_id(t.table_schema + '.' + t.table_name) AND sc.name = c.column_name",
|
||||
'LEFT JOIN sys.extended_properties prop ON prop.major_id = sc.object_id',
|
||||
'AND prop.minor_id = sc.column_id',
|
||||
"AND prop.name = 'MS_Description'",
|
||||
'WHERE t.TABLE_NAME =', wrapSingleQuote(tableName)
|
||||
].join(' ');
|
||||
|
||||
if (schema) {
|
||||
sql += `AND t.TABLE_SCHEMA =${wrapSingleQuote(schema)}`;
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
renameTableQuery(before, after) {
|
||||
return `EXEC sp_rename ${this.quoteTable(before)}, ${this.quoteTable(after)};`;
|
||||
}
|
||||
|
||||
showTablesQuery() {
|
||||
return "SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE';";
|
||||
}
|
||||
|
||||
dropTableQuery(tableName) {
|
||||
const qouteTbl = this.quoteTable(tableName);
|
||||
return `IF OBJECT_ID('${qouteTbl}', 'U') IS NOT NULL DROP TABLE ${qouteTbl};`;
|
||||
}
|
||||
|
||||
addColumnQuery(table, key, dataType) {
|
||||
// FIXME: attributeToSQL SHOULD be using attributes in addColumnQuery
|
||||
// but instead we need to pass the key along as the field here
|
||||
dataType.field = key;
|
||||
let commentStr = '';
|
||||
|
||||
if (dataType.comment && _.isString(dataType.comment)) {
|
||||
commentStr = this.commentTemplate(dataType.comment, table, key);
|
||||
// attributeToSQL will try to include `COMMENT 'Comment Text'` when it returns if the comment key
|
||||
// is present. This is needed for createTable statement where that part is extracted with regex.
|
||||
// Here we can intercept the object and remove comment property since we have the original object.
|
||||
delete dataType['comment'];
|
||||
}
|
||||
|
||||
const def = this.attributeToSQL(dataType, {
|
||||
context: 'addColumn'
|
||||
});
|
||||
return `ALTER TABLE ${this.quoteTable(table)} ADD ${this.quoteIdentifier(key)} ${def};${commentStr}`;
|
||||
}
|
||||
|
||||
commentTemplate(comment, table, column) {
|
||||
return ' EXEC sp_addextendedproperty ' +
|
||||
`@name = N'MS_Description', @value = ${this.escape(comment)}, ` +
|
||||
'@level0type = N\'Schema\', @level0name = \'dbo\', ' +
|
||||
`@level1type = N'Table', @level1name = ${this.quoteIdentifier(table)}, ` +
|
||||
`@level2type = N'Column', @level2name = ${this.quoteIdentifier(column)};`;
|
||||
}
|
||||
|
||||
removeColumnQuery(tableName, attributeName) {
|
||||
return `ALTER TABLE ${this.quoteTable(tableName)} DROP COLUMN ${this.quoteIdentifier(attributeName)};`;
|
||||
}
|
||||
|
||||
changeColumnQuery(tableName, attributes) {
|
||||
const attrString = [],
|
||||
constraintString = [];
|
||||
let commentString = '';
|
||||
|
||||
for (const attributeName in attributes) {
|
||||
const quotedAttrName = this.quoteIdentifier(attributeName);
|
||||
let definition = attributes[attributeName];
|
||||
if (definition.includes('COMMENT ')) {
|
||||
const commentMatch = definition.match(/^(.+) (COMMENT.*)$/);
|
||||
const commentText = commentMatch[2].replace('COMMENT', '').trim();
|
||||
commentString += this.commentTemplate(commentText, tableName, attributeName);
|
||||
// remove comment related substring from dataType
|
||||
definition = commentMatch[1];
|
||||
}
|
||||
if (definition.includes('REFERENCES')) {
|
||||
constraintString.push(`FOREIGN KEY (${quotedAttrName}) ${definition.replace(/.+?(?=REFERENCES)/, '')}`);
|
||||
} else {
|
||||
attrString.push(`${quotedAttrName} ${definition}`);
|
||||
}
|
||||
}
|
||||
|
||||
let finalQuery = '';
|
||||
if (attrString.length) {
|
||||
finalQuery += `ALTER COLUMN ${attrString.join(', ')}`;
|
||||
finalQuery += constraintString.length ? ' ' : '';
|
||||
}
|
||||
if (constraintString.length) {
|
||||
finalQuery += `ADD ${constraintString.join(', ')}`;
|
||||
}
|
||||
|
||||
return `ALTER TABLE ${this.quoteTable(tableName)} ${finalQuery};${commentString}`;
|
||||
}
|
||||
|
||||
renameColumnQuery(tableName, attrBefore, attributes) {
|
||||
const newName = Object.keys(attributes)[0];
|
||||
return `EXEC sp_rename '${this.quoteTable(tableName)}.${attrBefore}', '${newName}', 'COLUMN';`;
|
||||
}
|
||||
|
||||
bulkInsertQuery(tableName, attrValueHashes, options, attributes) {
|
||||
const quotedTable = this.quoteTable(tableName);
|
||||
options = options || {};
|
||||
attributes = attributes || {};
|
||||
|
||||
const tuples = [];
|
||||
const allAttributes = [];
|
||||
const allQueries = [];
|
||||
|
||||
|
||||
|
||||
let needIdentityInsertWrapper = false,
|
||||
outputFragment = '';
|
||||
|
||||
if (options.returning) {
|
||||
outputFragment = ' OUTPUT INSERTED.*';
|
||||
}
|
||||
|
||||
const emptyQuery = `INSERT INTO ${quotedTable}${outputFragment} DEFAULT VALUES`;
|
||||
|
||||
attrValueHashes.forEach(attrValueHash => {
|
||||
// special case for empty objects with primary keys
|
||||
const fields = Object.keys(attrValueHash);
|
||||
const firstAttr = attributes[fields[0]];
|
||||
if (fields.length === 1 && firstAttr && firstAttr.autoIncrement && attrValueHash[fields[0]] === null) {
|
||||
allQueries.push(emptyQuery);
|
||||
return;
|
||||
}
|
||||
|
||||
// normal case
|
||||
_.forOwn(attrValueHash, (value, key) => {
|
||||
if (value !== null && attributes[key] && attributes[key].autoIncrement) {
|
||||
needIdentityInsertWrapper = true;
|
||||
}
|
||||
|
||||
if (!allAttributes.includes(key)) {
|
||||
if (value === null && attributes[key] && attributes[key].autoIncrement)
|
||||
return;
|
||||
|
||||
allAttributes.push(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (allAttributes.length > 0) {
|
||||
attrValueHashes.forEach(attrValueHash => {
|
||||
tuples.push(`(${
|
||||
allAttributes.map(key =>
|
||||
this.escape(attrValueHash[key])).join(',')
|
||||
})`);
|
||||
});
|
||||
|
||||
const quotedAttributes = allAttributes.map(attr => this.quoteIdentifier(attr)).join(',');
|
||||
allQueries.push(tupleStr => `INSERT INTO ${quotedTable} (${quotedAttributes})${outputFragment} VALUES ${tupleStr};`);
|
||||
}
|
||||
const commands = [];
|
||||
let offset = 0;
|
||||
const batch = Math.floor(250 / (allAttributes.length + 1)) + 1;
|
||||
while (offset < Math.max(tuples.length, 1)) {
|
||||
const tupleStr = tuples.slice(offset, Math.min(tuples.length, offset + batch));
|
||||
let generatedQuery = allQueries.map(v => typeof v === 'string' ? v : v(tupleStr)).join(';');
|
||||
if (needIdentityInsertWrapper) {
|
||||
generatedQuery = `SET IDENTITY_INSERT ${quotedTable} ON; ${generatedQuery}; SET IDENTITY_INSERT ${quotedTable} OFF;`;
|
||||
}
|
||||
commands.push(generatedQuery);
|
||||
offset += batch;
|
||||
}
|
||||
return commands.join(';');
|
||||
}
|
||||
|
||||
updateQuery(tableName, attrValueHash, where, options, attributes) {
|
||||
const sql = super.updateQuery(tableName, attrValueHash, where, options, attributes);
|
||||
if (options.limit) {
|
||||
const updateArgs = `UPDATE TOP(${this.escape(options.limit)})`;
|
||||
sql.query = sql.query.replace('UPDATE', updateArgs);
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
upsertQuery(tableName, insertValues, updateValues, where, model) {
|
||||
const targetTableAlias = this.quoteTable(`${tableName}_target`);
|
||||
const sourceTableAlias = this.quoteTable(`${tableName}_source`);
|
||||
const primaryKeysAttrs = [];
|
||||
const identityAttrs = [];
|
||||
const uniqueAttrs = [];
|
||||
const tableNameQuoted = this.quoteTable(tableName);
|
||||
let needIdentityInsertWrapper = false;
|
||||
|
||||
//Obtain primaryKeys, uniquekeys and identity attrs from rawAttributes as model is not passed
|
||||
for (const key in model.rawAttributes) {
|
||||
if (model.rawAttributes[key].primaryKey) {
|
||||
primaryKeysAttrs.push(model.rawAttributes[key].field || key);
|
||||
}
|
||||
if (model.rawAttributes[key].unique) {
|
||||
uniqueAttrs.push(model.rawAttributes[key].field || key);
|
||||
}
|
||||
if (model.rawAttributes[key].autoIncrement) {
|
||||
identityAttrs.push(model.rawAttributes[key].field || key);
|
||||
}
|
||||
}
|
||||
|
||||
//Add unique indexes defined by indexes option to uniqueAttrs
|
||||
for (const index of model._indexes) {
|
||||
if (index.unique && index.fields) {
|
||||
for (const field of index.fields) {
|
||||
const fieldName = typeof field === 'string' ? field : field.name || field.attribute;
|
||||
if (!uniqueAttrs.includes(fieldName) && model.rawAttributes[fieldName]) {
|
||||
uniqueAttrs.push(fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateKeys = Object.keys(updateValues);
|
||||
const insertKeys = Object.keys(insertValues);
|
||||
const insertKeysQuoted = insertKeys.map(key => this.quoteIdentifier(key)).join(', ');
|
||||
const insertValuesEscaped = insertKeys.map(key => this.escape(insertValues[key])).join(', ');
|
||||
const sourceTableQuery = `VALUES(${insertValuesEscaped})`; //Virtual Table
|
||||
let joinCondition;
|
||||
|
||||
//IDENTITY_INSERT Condition
|
||||
identityAttrs.forEach(key => {
|
||||
if (updateValues[key] && updateValues[key] !== null) {
|
||||
needIdentityInsertWrapper = true;
|
||||
/*
|
||||
* IDENTITY_INSERT Column Cannot be updated, only inserted
|
||||
* http://stackoverflow.com/a/30176254/2254360
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
//Filter NULL Clauses
|
||||
const clauses = where[Op.or].filter(clause => {
|
||||
let valid = true;
|
||||
/*
|
||||
* Exclude NULL Composite PK/UK. Partial Composite clauses should also be excluded as it doesn't guarantee a single row
|
||||
*/
|
||||
for (const key in clause) {
|
||||
if (!clause[key]) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
});
|
||||
|
||||
/*
|
||||
* Generate ON condition using PK(s).
|
||||
* If not, generate using UK(s). Else throw error
|
||||
*/
|
||||
const getJoinSnippet = array => {
|
||||
return array.map(key => {
|
||||
key = this.quoteIdentifier(key);
|
||||
return `${targetTableAlias}.${key} = ${sourceTableAlias}.${key}`;
|
||||
});
|
||||
};
|
||||
|
||||
if (clauses.length === 0) {
|
||||
throw new Error('Primary Key or Unique key should be passed to upsert query');
|
||||
} else {
|
||||
// Search for primary key attribute in clauses -- Model can have two separate unique keys
|
||||
for (const key in clauses) {
|
||||
const keys = Object.keys(clauses[key]);
|
||||
if (primaryKeysAttrs.includes(keys[0])) {
|
||||
joinCondition = getJoinSnippet(primaryKeysAttrs).join(' AND ');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!joinCondition) {
|
||||
joinCondition = getJoinSnippet(uniqueAttrs).join(' AND ');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the IDENTITY_INSERT Column from update
|
||||
const updateSnippet = updateKeys.filter(key => !identityAttrs.includes(key))
|
||||
.map(key => {
|
||||
const value = this.escape(updateValues[key]);
|
||||
key = this.quoteIdentifier(key);
|
||||
return `${targetTableAlias}.${key} = ${value}`;
|
||||
}).join(', ');
|
||||
|
||||
const insertSnippet = `(${insertKeysQuoted}) VALUES(${insertValuesEscaped})`;
|
||||
let query = `MERGE INTO ${tableNameQuoted} WITH(HOLDLOCK) AS ${targetTableAlias} USING (${sourceTableQuery}) AS ${sourceTableAlias}(${insertKeysQuoted}) ON ${joinCondition}`;
|
||||
query += ` WHEN MATCHED THEN UPDATE SET ${updateSnippet} WHEN NOT MATCHED THEN INSERT ${insertSnippet} OUTPUT $action, INSERTED.*;`;
|
||||
if (needIdentityInsertWrapper) {
|
||||
query = `SET IDENTITY_INSERT ${tableNameQuoted} ON; ${query} SET IDENTITY_INSERT ${tableNameQuoted} OFF;`;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
truncateTableQuery(tableName) {
|
||||
return `TRUNCATE TABLE ${this.quoteTable(tableName)}`;
|
||||
}
|
||||
|
||||
deleteQuery(tableName, where, options = {}, model) {
|
||||
const table = this.quoteTable(tableName);
|
||||
|
||||
let whereClause = this.getWhereConditions(where, null, model, options);
|
||||
let limit = '';
|
||||
|
||||
if (options.limit) {
|
||||
limit = ` TOP(${this.escape(options.limit)})`;
|
||||
}
|
||||
|
||||
if (whereClause) {
|
||||
whereClause = ` WHERE ${whereClause}`;
|
||||
}
|
||||
|
||||
return `DELETE${limit} FROM ${table}${whereClause}; SELECT @@ROWCOUNT AS AFFECTEDROWS;`;
|
||||
}
|
||||
|
||||
showIndexesQuery(tableName) {
|
||||
return `EXEC sys.sp_helpindex @objname = N'${this.quoteTable(tableName)}';`;
|
||||
}
|
||||
|
||||
showConstraintsQuery(tableName) {
|
||||
return `EXEC sp_helpconstraint @objname = ${this.escape(this.quoteTable(tableName))};`;
|
||||
}
|
||||
|
||||
removeIndexQuery(tableName, indexNameOrAttributes) {
|
||||
let indexName = indexNameOrAttributes;
|
||||
|
||||
if (typeof indexName !== 'string') {
|
||||
indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`);
|
||||
}
|
||||
|
||||
return `DROP INDEX ${this.quoteIdentifiers(indexName)} ON ${this.quoteIdentifiers(tableName)}`;
|
||||
}
|
||||
|
||||
attributeToSQL(attribute) {
|
||||
if (!_.isPlainObject(attribute)) {
|
||||
attribute = {
|
||||
type: attribute
|
||||
};
|
||||
}
|
||||
|
||||
// handle self referential constraints
|
||||
if (attribute.references) {
|
||||
|
||||
if (attribute.Model && attribute.Model.tableName === attribute.references.model) {
|
||||
this.sequelize.log('MSSQL does not support self referencial constraints, '
|
||||
+ 'we will remove it but we recommend restructuring your query');
|
||||
attribute.onDelete = '';
|
||||
attribute.onUpdate = '';
|
||||
}
|
||||
}
|
||||
|
||||
let template;
|
||||
|
||||
if (attribute.type instanceof DataTypes.ENUM) {
|
||||
if (attribute.type.values && !attribute.values) attribute.values = attribute.type.values;
|
||||
|
||||
// enums are a special case
|
||||
template = attribute.type.toSql();
|
||||
template += ` CHECK (${this.quoteIdentifier(attribute.field)} IN(${attribute.values.map(value => {
|
||||
return this.escape(value);
|
||||
}).join(', ') }))`;
|
||||
return template;
|
||||
}
|
||||
template = attribute.type.toString();
|
||||
|
||||
if (attribute.allowNull === false) {
|
||||
template += ' NOT NULL';
|
||||
} else if (!attribute.primaryKey && !Utils.defaultValueSchemable(attribute.defaultValue)) {
|
||||
template += ' NULL';
|
||||
}
|
||||
|
||||
if (attribute.autoIncrement) {
|
||||
template += ' IDENTITY(1,1)';
|
||||
}
|
||||
|
||||
// Blobs/texts cannot have a defaultValue
|
||||
if (attribute.type !== 'TEXT' && attribute.type._binary !== true &&
|
||||
Utils.defaultValueSchemable(attribute.defaultValue)) {
|
||||
template += ` DEFAULT ${this.escape(attribute.defaultValue)}`;
|
||||
}
|
||||
|
||||
if (attribute.unique === true) {
|
||||
template += ' UNIQUE';
|
||||
}
|
||||
|
||||
if (attribute.primaryKey) {
|
||||
template += ' PRIMARY KEY';
|
||||
}
|
||||
|
||||
if (attribute.references) {
|
||||
template += ` REFERENCES ${this.quoteTable(attribute.references.model)}`;
|
||||
|
||||
if (attribute.references.key) {
|
||||
template += ` (${this.quoteIdentifier(attribute.references.key)})`;
|
||||
} else {
|
||||
template += ` (${this.quoteIdentifier('id')})`;
|
||||
}
|
||||
|
||||
if (attribute.onDelete) {
|
||||
template += ` ON DELETE ${attribute.onDelete.toUpperCase()}`;
|
||||
}
|
||||
|
||||
if (attribute.onUpdate) {
|
||||
template += ` ON UPDATE ${attribute.onUpdate.toUpperCase()}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (attribute.comment && typeof attribute.comment === 'string') {
|
||||
template += ` COMMENT ${attribute.comment}`;
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
attributesToSQL(attributes, options) {
|
||||
const result = {},
|
||||
existingConstraints = [];
|
||||
let key,
|
||||
attribute;
|
||||
|
||||
for (key in attributes) {
|
||||
attribute = attributes[key];
|
||||
|
||||
if (attribute.references) {
|
||||
|
||||
if (existingConstraints.includes(attribute.references.model.toString())) {
|
||||
// no cascading constraints to a table more than once
|
||||
attribute.onDelete = '';
|
||||
attribute.onUpdate = '';
|
||||
} else {
|
||||
existingConstraints.push(attribute.references.model.toString());
|
||||
|
||||
// NOTE: this really just disables cascading updates for all
|
||||
// definitions. Can be made more robust to support the
|
||||
// few cases where MSSQL actually supports them
|
||||
attribute.onUpdate = '';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (key && !attribute.field) attribute.field = key;
|
||||
result[attribute.field || key] = this.attributeToSQL(attribute, options);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
createTrigger() {
|
||||
throwMethodUndefined('createTrigger');
|
||||
}
|
||||
|
||||
dropTrigger() {
|
||||
throwMethodUndefined('dropTrigger');
|
||||
}
|
||||
|
||||
renameTrigger() {
|
||||
throwMethodUndefined('renameTrigger');
|
||||
}
|
||||
|
||||
createFunction() {
|
||||
throwMethodUndefined('createFunction');
|
||||
}
|
||||
|
||||
dropFunction() {
|
||||
throwMethodUndefined('dropFunction');
|
||||
}
|
||||
|
||||
renameFunction() {
|
||||
throwMethodUndefined('renameFunction');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate common SQL prefix for ForeignKeysQuery.
|
||||
*
|
||||
* @param {string} catalogName
|
||||
* @returns {string}
|
||||
*/
|
||||
_getForeignKeysQueryPrefix(catalogName) {
|
||||
return `${'SELECT ' +
|
||||
'constraint_name = OBJ.NAME, ' +
|
||||
'constraintName = OBJ.NAME, '}${
|
||||
catalogName ? `constraintCatalog = '${catalogName}', ` : ''
|
||||
}constraintSchema = SCHEMA_NAME(OBJ.SCHEMA_ID), ` +
|
||||
'tableName = TB.NAME, ' +
|
||||
`tableSchema = SCHEMA_NAME(TB.SCHEMA_ID), ${
|
||||
catalogName ? `tableCatalog = '${catalogName}', ` : ''
|
||||
}columnName = COL.NAME, ` +
|
||||
`referencedTableSchema = SCHEMA_NAME(RTB.SCHEMA_ID), ${
|
||||
catalogName ? `referencedCatalog = '${catalogName}', ` : ''
|
||||
}referencedTableName = RTB.NAME, ` +
|
||||
'referencedColumnName = RCOL.NAME ' +
|
||||
'FROM sys.foreign_key_columns FKC ' +
|
||||
'INNER JOIN sys.objects OBJ ON OBJ.OBJECT_ID = FKC.CONSTRAINT_OBJECT_ID ' +
|
||||
'INNER JOIN sys.tables TB ON TB.OBJECT_ID = FKC.PARENT_OBJECT_ID ' +
|
||||
'INNER JOIN sys.columns COL ON COL.COLUMN_ID = PARENT_COLUMN_ID AND COL.OBJECT_ID = TB.OBJECT_ID ' +
|
||||
'INNER JOIN sys.tables RTB ON RTB.OBJECT_ID = FKC.REFERENCED_OBJECT_ID ' +
|
||||
'INNER JOIN sys.columns RCOL ON RCOL.COLUMN_ID = REFERENCED_COLUMN_ID AND RCOL.OBJECT_ID = RTB.OBJECT_ID';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQL query that returns all foreign keys details of a table.
|
||||
*
|
||||
* @param {string|Object} table
|
||||
* @param {string} catalogName database name
|
||||
* @returns {string}
|
||||
*/
|
||||
getForeignKeysQuery(table, catalogName) {
|
||||
const tableName = table.tableName || table;
|
||||
let sql = `${this._getForeignKeysQueryPrefix(catalogName)
|
||||
} WHERE TB.NAME =${wrapSingleQuote(tableName)}`;
|
||||
|
||||
if (table.schema) {
|
||||
sql += ` AND SCHEMA_NAME(TB.SCHEMA_ID) =${wrapSingleQuote(table.schema)}`;
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
getForeignKeyQuery(table, attributeName) {
|
||||
const tableName = table.tableName || table;
|
||||
let sql = `${this._getForeignKeysQueryPrefix()
|
||||
} WHERE TB.NAME =${wrapSingleQuote(tableName)
|
||||
} AND COL.NAME =${wrapSingleQuote(attributeName)}`;
|
||||
|
||||
if (table.schema) {
|
||||
sql += ` AND SCHEMA_NAME(TB.SCHEMA_ID) =${wrapSingleQuote(table.schema)}`;
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
getPrimaryKeyConstraintQuery(table, attributeName) {
|
||||
const tableName = wrapSingleQuote(table.tableName || table);
|
||||
return [
|
||||
'SELECT K.TABLE_NAME AS tableName,',
|
||||
'K.COLUMN_NAME AS columnName,',
|
||||
'K.CONSTRAINT_NAME AS constraintName',
|
||||
'FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS C',
|
||||
'JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS K',
|
||||
'ON C.TABLE_NAME = K.TABLE_NAME',
|
||||
'AND C.CONSTRAINT_CATALOG = K.CONSTRAINT_CATALOG',
|
||||
'AND C.CONSTRAINT_SCHEMA = K.CONSTRAINT_SCHEMA',
|
||||
'AND C.CONSTRAINT_NAME = K.CONSTRAINT_NAME',
|
||||
'WHERE C.CONSTRAINT_TYPE = \'PRIMARY KEY\'',
|
||||
`AND K.COLUMN_NAME = ${wrapSingleQuote(attributeName)}`,
|
||||
`AND K.TABLE_NAME = ${tableName};`
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
dropForeignKeyQuery(tableName, foreignKey) {
|
||||
return `ALTER TABLE ${this.quoteTable(tableName)} DROP ${this.quoteIdentifier(foreignKey)}`;
|
||||
}
|
||||
|
||||
getDefaultConstraintQuery(tableName, attributeName) {
|
||||
const quotedTable = this.quoteTable(tableName);
|
||||
return 'SELECT name FROM sys.default_constraints ' +
|
||||
`WHERE PARENT_OBJECT_ID = OBJECT_ID('${quotedTable}', 'U') ` +
|
||||
`AND PARENT_COLUMN_ID = (SELECT column_id FROM sys.columns WHERE NAME = ('${attributeName}') ` +
|
||||
`AND object_id = OBJECT_ID('${quotedTable}', 'U'));`;
|
||||
}
|
||||
|
||||
dropConstraintQuery(tableName, constraintName) {
|
||||
return `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.quoteIdentifier(constraintName)};`;
|
||||
}
|
||||
|
||||
setIsolationLevelQuery() {
|
||||
|
||||
}
|
||||
|
||||
generateTransactionId() {
|
||||
return randomBytes(10).toString('hex');
|
||||
}
|
||||
|
||||
startTransactionQuery(transaction) {
|
||||
if (transaction.parent) {
|
||||
return `SAVE TRANSACTION ${this.quoteIdentifier(transaction.name)};`;
|
||||
}
|
||||
|
||||
return 'BEGIN TRANSACTION;';
|
||||
}
|
||||
|
||||
commitTransactionQuery(transaction) {
|
||||
if (transaction.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
return 'COMMIT TRANSACTION;';
|
||||
}
|
||||
|
||||
rollbackTransactionQuery(transaction) {
|
||||
if (transaction.parent) {
|
||||
return `ROLLBACK TRANSACTION ${this.quoteIdentifier(transaction.name)};`;
|
||||
}
|
||||
|
||||
return 'ROLLBACK TRANSACTION;';
|
||||
}
|
||||
|
||||
selectFromTableFragment(options, model, attributes, tables, mainTableAs, where) {
|
||||
let topFragment = '';
|
||||
let mainFragment = `SELECT ${attributes.join(', ')} FROM ${tables}`;
|
||||
|
||||
// Handle SQL Server 2008 with TOP instead of LIMIT
|
||||
if (semver.valid(this.sequelize.options.databaseVersion) && semver.lt(this.sequelize.options.databaseVersion, '11.0.0')) {
|
||||
if (options.limit) {
|
||||
topFragment = `TOP ${options.limit} `;
|
||||
}
|
||||
if (options.offset) {
|
||||
const offset = options.offset || 0,
|
||||
isSubQuery = options.hasIncludeWhere || options.hasIncludeRequired || options.hasMultiAssociation;
|
||||
let orders = { mainQueryOrder: [] };
|
||||
if (options.order) {
|
||||
orders = this.getQueryOrders(options, model, isSubQuery);
|
||||
}
|
||||
|
||||
if (!orders.mainQueryOrder.length) {
|
||||
orders.mainQueryOrder.push(this.quoteIdentifier(model.primaryKeyField));
|
||||
}
|
||||
|
||||
const tmpTable = mainTableAs ? mainTableAs : 'OffsetTable';
|
||||
const whereFragment = where ? ` WHERE ${where}` : '';
|
||||
|
||||
/*
|
||||
* For earlier versions of SQL server, we need to nest several queries
|
||||
* in order to emulate the OFFSET behavior.
|
||||
*
|
||||
* 1. The outermost query selects all items from the inner query block.
|
||||
* This is due to a limitation in SQL server with the use of computed
|
||||
* columns (e.g. SELECT ROW_NUMBER()...AS x) in WHERE clauses.
|
||||
* 2. The next query handles the LIMIT and OFFSET behavior by getting
|
||||
* the TOP N rows of the query where the row number is > OFFSET
|
||||
* 3. The innermost query is the actual set we want information from
|
||||
*/
|
||||
const fragment = `SELECT TOP 100 PERCENT ${attributes.join(', ')} FROM ` +
|
||||
`(SELECT ${topFragment}*` +
|
||||
` FROM (SELECT ROW_NUMBER() OVER (ORDER BY ${orders.mainQueryOrder.join(', ')}) as row_num, * ` +
|
||||
` FROM ${tables} AS ${tmpTable}${whereFragment})` +
|
||||
` AS ${tmpTable} WHERE row_num > ${offset})` +
|
||||
` AS ${tmpTable}`;
|
||||
return fragment;
|
||||
}
|
||||
mainFragment = `SELECT ${topFragment}${attributes.join(', ')} FROM ${tables}`;
|
||||
}
|
||||
|
||||
if (mainTableAs) {
|
||||
mainFragment += ` AS ${mainTableAs}`;
|
||||
}
|
||||
|
||||
if (options.tableHint && TableHints[options.tableHint]) {
|
||||
mainFragment += ` WITH (${TableHints[options.tableHint]})`;
|
||||
}
|
||||
|
||||
return mainFragment;
|
||||
}
|
||||
|
||||
addLimitAndOffset(options, model) {
|
||||
// Skip handling of limit and offset as postfixes for older SQL Server versions
|
||||
if (semver.valid(this.sequelize.options.databaseVersion) && semver.lt(this.sequelize.options.databaseVersion, '11.0.0')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const offset = options.offset || 0;
|
||||
const isSubQuery = options.subQuery === undefined
|
||||
? options.hasIncludeWhere || options.hasIncludeRequired || options.hasMultiAssociation
|
||||
: options.subQuery;
|
||||
|
||||
let fragment = '';
|
||||
let orders = {};
|
||||
|
||||
if (options.order) {
|
||||
orders = this.getQueryOrders(options, model, isSubQuery);
|
||||
}
|
||||
|
||||
if (options.limit || options.offset) {
|
||||
if (!options.order || options.include && !orders.subQueryOrder.length) {
|
||||
fragment += options.order && !isSubQuery ? ', ' : ' ORDER BY ';
|
||||
fragment += `${this.quoteTable(options.tableAs || model.name)}.${this.quoteIdentifier(model.primaryKeyField)}`;
|
||||
}
|
||||
|
||||
if (options.offset || options.limit) {
|
||||
fragment += ` OFFSET ${this.escape(offset)} ROWS`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
fragment += ` FETCH NEXT ${this.escape(options.limit)} ROWS ONLY`;
|
||||
}
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
booleanValue(value) {
|
||||
return value ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// private methods
|
||||
function wrapSingleQuote(identifier) {
|
||||
return Utils.addTicks(Utils.removeTicks(identifier, "'"), "'");
|
||||
}
|
||||
|
||||
module.exports = MSSQLQueryGenerator;
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
Returns an object that treats MSSQL's inabilities to do certain queries.
|
||||
|
||||
@class QueryInterface
|
||||
@static
|
||||
@private
|
||||
*/
|
||||
|
||||
/**
|
||||
A wrapper that fixes MSSQL's inability to cleanly remove columns from existing tables if they have a default constraint.
|
||||
|
||||
|
||||
@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
|
||||
|
||||
@private
|
||||
*/
|
||||
const removeColumn = function(qi, tableName, attributeName, options) {
|
||||
options = Object.assign({ raw: true }, options || {});
|
||||
|
||||
const findConstraintSql = qi.QueryGenerator.getDefaultConstraintQuery(tableName, attributeName);
|
||||
return qi.sequelize.query(findConstraintSql, options)
|
||||
.then(([results]) => {
|
||||
if (!results.length) {
|
||||
// No default constraint found -- we can cleanly remove the column
|
||||
return;
|
||||
}
|
||||
const dropConstraintSql = qi.QueryGenerator.dropConstraintQuery(tableName, results[0].name);
|
||||
return qi.sequelize.query(dropConstraintSql, options);
|
||||
})
|
||||
.then(() => {
|
||||
const findForeignKeySql = qi.QueryGenerator.getForeignKeyQuery(tableName, attributeName);
|
||||
return qi.sequelize.query(findForeignKeySql, options);
|
||||
})
|
||||
.then(([results]) => {
|
||||
if (!results.length) {
|
||||
// No foreign key constraints found, so we can remove the column
|
||||
return;
|
||||
}
|
||||
const dropForeignKeySql = qi.QueryGenerator.dropForeignKeyQuery(tableName, results[0].constraint_name);
|
||||
return qi.sequelize.query(dropForeignKeySql, options);
|
||||
})
|
||||
.then(() => {
|
||||
//Check if the current column is a primaryKey
|
||||
const primaryKeyConstraintSql = qi.QueryGenerator.getPrimaryKeyConstraintQuery(tableName, attributeName);
|
||||
return qi.sequelize.query(primaryKeyConstraintSql, options);
|
||||
})
|
||||
.then(([result]) => {
|
||||
if (!result.length) {
|
||||
return;
|
||||
}
|
||||
const dropConstraintSql = qi.QueryGenerator.dropConstraintQuery(tableName, result[0].constraintName);
|
||||
return qi.sequelize.query(dropConstraintSql, options);
|
||||
})
|
||||
.then(() => {
|
||||
const removeSql = qi.QueryGenerator.removeColumnQuery(tableName, attributeName);
|
||||
return qi.sequelize.query(removeSql, options);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
removeColumn
|
||||
};
|
||||
+389
@@ -0,0 +1,389 @@
|
||||
'use strict';
|
||||
|
||||
const Promise = require('../../promise');
|
||||
const AbstractQuery = require('../abstract/query');
|
||||
const sequelizeErrors = require('../../errors');
|
||||
const parserStore = require('../parserStore')('mssql');
|
||||
const _ = require('lodash');
|
||||
const { logger } = require('../../utils/logger');
|
||||
|
||||
const debug = logger.debugContext('sql:mssql');
|
||||
|
||||
class Query extends AbstractQuery {
|
||||
getInsertIdField() {
|
||||
return 'id';
|
||||
}
|
||||
|
||||
getSQLTypeFromJsType(value, TYPES) {
|
||||
const paramType = { type: TYPES.VarChar, typeOptions: {} };
|
||||
paramType.type = TYPES.NVarChar;
|
||||
if (typeof value === 'number') {
|
||||
if (Number.isInteger(value)) {
|
||||
if (value >= -2147483648 && value <= 2147483647) {
|
||||
paramType.type = TYPES.Int;
|
||||
} else {
|
||||
paramType.type = TYPES.BigInt;
|
||||
}
|
||||
} else {
|
||||
paramType.type = TYPES.Numeric;
|
||||
//Default to a reasonable numeric precision/scale pending more sophisticated logic
|
||||
paramType.typeOptions = { precision: 30, scale: 15 };
|
||||
}
|
||||
}
|
||||
if (Buffer.isBuffer(value)) {
|
||||
paramType.type = TYPES.VarBinary;
|
||||
}
|
||||
return paramType;
|
||||
}
|
||||
|
||||
_run(connection, sql, parameters) {
|
||||
this.sql = sql;
|
||||
const { options } = this;
|
||||
|
||||
const complete = this._logQuery(sql, debug, parameters);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const handleTransaction = err => {
|
||||
if (err) {
|
||||
reject(this.formatError(err));
|
||||
return;
|
||||
}
|
||||
resolve(this.formatResults());
|
||||
};
|
||||
// TRANSACTION SUPPORT
|
||||
if (sql.startsWith('BEGIN TRANSACTION')) {
|
||||
return connection.beginTransaction(handleTransaction, options.transaction.name, connection.lib.ISOLATION_LEVEL[options.isolationLevel]);
|
||||
}
|
||||
if (sql.startsWith('COMMIT TRANSACTION')) {
|
||||
return connection.commitTransaction(handleTransaction);
|
||||
}
|
||||
if (sql.startsWith('ROLLBACK TRANSACTION')) {
|
||||
return connection.rollbackTransaction(handleTransaction, options.transaction.name);
|
||||
}
|
||||
if (sql.startsWith('SAVE TRANSACTION')) {
|
||||
return connection.saveTransaction(handleTransaction, options.transaction.name);
|
||||
}
|
||||
const results = [];
|
||||
const request = new connection.lib.Request(sql, (err, rowCount) => {
|
||||
|
||||
complete();
|
||||
|
||||
if (err) {
|
||||
err.sql = sql;
|
||||
err.parameters = parameters;
|
||||
reject(this.formatError(err));
|
||||
} else {
|
||||
resolve(this.formatResults(results, rowCount));
|
||||
}
|
||||
});
|
||||
|
||||
if (parameters) {
|
||||
_.forOwn(parameters, (value, key) => {
|
||||
const paramType = this.getSQLTypeFromJsType(value, connection.lib.TYPES);
|
||||
request.addParameter(key, paramType.type, value, paramType.typeOptions);
|
||||
});
|
||||
}
|
||||
|
||||
request.on('row', columns => {
|
||||
const row = {};
|
||||
for (const column of columns) {
|
||||
const typeid = column.metadata.type.id;
|
||||
const parse = parserStore.get(typeid);
|
||||
let value = column.value;
|
||||
|
||||
if (value !== null & !!parse) {
|
||||
value = parse(value);
|
||||
}
|
||||
row[column.metadata.colName] = value;
|
||||
}
|
||||
|
||||
results.push(row);
|
||||
});
|
||||
|
||||
connection.execSql(request);
|
||||
});
|
||||
}
|
||||
|
||||
run(sql, parameters) {
|
||||
return Promise.using(this.connection.lock(), connection => this._run(connection, sql, parameters));
|
||||
}
|
||||
|
||||
static formatBindParameters(sql, values, dialect) {
|
||||
const bindParam = {};
|
||||
const replacementFunc = (match, key, values) => {
|
||||
if (values[key] !== undefined) {
|
||||
bindParam[key] = values[key];
|
||||
return `@${key}`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0];
|
||||
|
||||
return [sql, bindParam];
|
||||
}
|
||||
|
||||
/**
|
||||
* High level function that handles the results of a query execution.
|
||||
*
|
||||
* @param {Array} data - The result of the query execution.
|
||||
* @param {number} rowCount
|
||||
* @private
|
||||
* @example
|
||||
* Example:
|
||||
* query.formatResults([
|
||||
* {
|
||||
* id: 1, // this is from the main table
|
||||
* attr2: 'snafu', // this is from the main table
|
||||
* Tasks.id: 1, // this is from the associated table
|
||||
* Tasks.title: 'task' // this is from the associated table
|
||||
* }
|
||||
* ])
|
||||
*/
|
||||
formatResults(data, rowCount) {
|
||||
let result = this.instance;
|
||||
if (this.isInsertQuery(data)) {
|
||||
this.handleInsertQuery(data);
|
||||
|
||||
if (!this.instance) {
|
||||
if (this.options.plain) {
|
||||
// NOTE: super contrived. This just passes the newly added query-interface
|
||||
// test returning only the PK. There isn't a way in MSSQL to identify
|
||||
// that a given return value is the PK, and we have no schema information
|
||||
// because there was no calling Model.
|
||||
const record = data[0];
|
||||
result = record[Object.keys(record)[0]];
|
||||
} else {
|
||||
result = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isShowTablesQuery()) {
|
||||
return this.handleShowTablesQuery(data);
|
||||
}
|
||||
if (this.isDescribeQuery()) {
|
||||
result = {};
|
||||
for (const _result of data) {
|
||||
if (_result.Default) {
|
||||
_result.Default = _result.Default.replace("('", '').replace("')", '').replace(/'/g, '');
|
||||
}
|
||||
|
||||
result[_result.Name] = {
|
||||
type: _result.Type.toUpperCase(),
|
||||
allowNull: _result.IsNull === 'YES' ? true : false,
|
||||
defaultValue: _result.Default,
|
||||
primaryKey: _result.Constraint === 'PRIMARY KEY',
|
||||
autoIncrement: _result.IsIdentity === 1,
|
||||
comment: _result.Comment
|
||||
};
|
||||
|
||||
if (
|
||||
result[_result.Name].type.includes('CHAR')
|
||||
&& _result.Length
|
||||
) {
|
||||
if (_result.Length === -1) {
|
||||
result[_result.Name].type += '(MAX)';
|
||||
} else {
|
||||
result[_result.Name].type += `(${_result.Length})`;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if (this.isSelectQuery()) {
|
||||
return this.handleSelectQuery(data);
|
||||
}
|
||||
if (this.isShowIndexesQuery()) {
|
||||
return this.handleShowIndexesQuery(data);
|
||||
}
|
||||
if (this.isUpsertQuery()) {
|
||||
return data[0];
|
||||
}
|
||||
if (this.isCallQuery()) {
|
||||
return data[0];
|
||||
}
|
||||
if (this.isBulkUpdateQuery()) {
|
||||
return data.length;
|
||||
}
|
||||
if (this.isBulkDeleteQuery()) {
|
||||
return data[0] && data[0].AFFECTEDROWS;
|
||||
}
|
||||
if (this.isVersionQuery()) {
|
||||
return data[0].version;
|
||||
}
|
||||
if (this.isForeignKeysQuery()) {
|
||||
return data;
|
||||
}
|
||||
if (this.isInsertQuery() || this.isUpdateQuery()) {
|
||||
return [result, rowCount];
|
||||
}
|
||||
if (this.isShowConstraintsQuery()) {
|
||||
return this.handleShowConstraintsQuery(data);
|
||||
}
|
||||
if (this.isRawQuery()) {
|
||||
// MSSQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
|
||||
return [data, data];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
handleShowTablesQuery(results) {
|
||||
return results.map(resultSet => {
|
||||
return {
|
||||
tableName: resultSet.TABLE_NAME,
|
||||
schema: resultSet.TABLE_SCHEMA
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
handleShowConstraintsQuery(data) {
|
||||
//Convert snake_case keys to camelCase as it's generated by stored procedure
|
||||
return data.slice(1).map(result => {
|
||||
const constraint = {};
|
||||
for (const key in result) {
|
||||
constraint[_.camelCase(key)] = result[key];
|
||||
}
|
||||
return constraint;
|
||||
});
|
||||
}
|
||||
|
||||
formatError(err) {
|
||||
let match;
|
||||
|
||||
match = err.message.match(/Violation of (?:UNIQUE|PRIMARY) KEY constraint '((.|\s)*)'. Cannot insert duplicate key in object '.*'.(:? The duplicate key value is \((.*)\).)?/);
|
||||
match = match || err.message.match(/Cannot insert duplicate key row in object .* with unique index '(.*)'/);
|
||||
if (match && match.length > 1) {
|
||||
let fields = {};
|
||||
const uniqueKey = this.model && this.model.uniqueKeys[match[1]];
|
||||
let message = 'Validation error';
|
||||
|
||||
if (uniqueKey && !!uniqueKey.msg) {
|
||||
message = uniqueKey.msg;
|
||||
}
|
||||
if (match[4]) {
|
||||
const values = match[4].split(',').map(part => part.trim());
|
||||
if (uniqueKey) {
|
||||
fields = _.zipObject(uniqueKey.fields, values);
|
||||
} else {
|
||||
fields[match[1]] = match[4];
|
||||
}
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
_.forOwn(fields, (value, field) => {
|
||||
errors.push(new sequelizeErrors.ValidationErrorItem(
|
||||
this.getUniqueConstraintErrorMessage(field),
|
||||
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
|
||||
field,
|
||||
value,
|
||||
this.instance,
|
||||
'not_unique'
|
||||
));
|
||||
});
|
||||
|
||||
return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields });
|
||||
}
|
||||
|
||||
match = err.message.match(/Failed on step '(.*)'.Could not create constraint. See previous errors./) ||
|
||||
err.message.match(/The DELETE statement conflicted with the REFERENCE constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./) ||
|
||||
err.message.match(/The (?:INSERT|MERGE|UPDATE) statement conflicted with the FOREIGN KEY constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./);
|
||||
if (match && match.length > 0) {
|
||||
return new sequelizeErrors.ForeignKeyConstraintError({
|
||||
fields: null,
|
||||
index: match[1],
|
||||
parent: err
|
||||
});
|
||||
}
|
||||
|
||||
match = err.message.match(/Could not drop constraint. See previous errors./);
|
||||
if (match && match.length > 0) {
|
||||
let constraint = err.sql.match(/(?:constraint|index) \[(.+?)\]/i);
|
||||
constraint = constraint ? constraint[1] : undefined;
|
||||
let table = err.sql.match(/table \[(.+?)\]/i);
|
||||
table = table ? table[1] : undefined;
|
||||
|
||||
return new sequelizeErrors.UnknownConstraintError({
|
||||
message: match[1],
|
||||
constraint,
|
||||
table,
|
||||
parent: err
|
||||
});
|
||||
}
|
||||
|
||||
return new sequelizeErrors.DatabaseError(err);
|
||||
}
|
||||
|
||||
isShowOrDescribeQuery() {
|
||||
let result = false;
|
||||
|
||||
result = result || this.sql.toLowerCase().startsWith("select c.column_name as 'name', c.data_type as 'type', c.is_nullable as 'isnull'");
|
||||
result = result || this.sql.toLowerCase().startsWith('select tablename = t.name, name = ind.name,');
|
||||
result = result || this.sql.toLowerCase().startsWith('exec sys.sp_helpindex @objname');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
isShowIndexesQuery() {
|
||||
return this.sql.toLowerCase().startsWith('exec sys.sp_helpindex @objname');
|
||||
}
|
||||
|
||||
handleShowIndexesQuery(data) {
|
||||
// Group by index name, and collect all fields
|
||||
data = data.reduce((acc, item) => {
|
||||
if (!(item.index_name in acc)) {
|
||||
acc[item.index_name] = item;
|
||||
item.fields = [];
|
||||
}
|
||||
|
||||
item.index_keys.split(',').forEach(column => {
|
||||
let columnName = column.trim();
|
||||
if (columnName.includes('(-)')) {
|
||||
columnName = columnName.replace('(-)', '');
|
||||
}
|
||||
|
||||
acc[item.index_name].fields.push({
|
||||
attribute: columnName,
|
||||
length: undefined,
|
||||
order: column.includes('(-)') ? 'DESC' : 'ASC',
|
||||
collate: undefined
|
||||
});
|
||||
});
|
||||
delete item.index_keys;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return _.map(data, item => ({
|
||||
primary: item.index_name.toLowerCase().startsWith('pk'),
|
||||
fields: item.fields,
|
||||
name: item.index_name,
|
||||
tableName: undefined,
|
||||
unique: item.index_description.toLowerCase().includes('unique'),
|
||||
type: undefined
|
||||
}));
|
||||
}
|
||||
|
||||
handleInsertQuery(results, metaData) {
|
||||
if (this.instance) {
|
||||
// add the inserted row id to the instance
|
||||
const autoIncrementAttribute = this.model.autoIncrementAttribute;
|
||||
let id = null;
|
||||
let autoIncrementAttributeAlias = null;
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(this.model.rawAttributes, autoIncrementAttribute) &&
|
||||
this.model.rawAttributes[autoIncrementAttribute].field !== undefined)
|
||||
autoIncrementAttributeAlias = this.model.rawAttributes[autoIncrementAttribute].field;
|
||||
|
||||
id = id || results && results[0][this.getInsertIdField()];
|
||||
id = id || metaData && metaData[this.getInsertIdField()];
|
||||
id = id || results && results[0][autoIncrementAttribute];
|
||||
id = id || autoIncrementAttributeAlias && results && results[0][autoIncrementAttributeAlias];
|
||||
|
||||
this.instance[autoIncrementAttribute] = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Query;
|
||||
module.exports.Query = Query;
|
||||
module.exports.default = Query;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
const Promise = require('../../promise');
|
||||
|
||||
class ResourceLock {
|
||||
constructor(resource) {
|
||||
this.resource = resource;
|
||||
this.previous = Promise.resolve(resource);
|
||||
}
|
||||
|
||||
unwrap() {
|
||||
return this.resource;
|
||||
}
|
||||
|
||||
lock() {
|
||||
const lock = this.previous;
|
||||
let resolve;
|
||||
this.previous = new Promise(r => {
|
||||
resolve = r;
|
||||
});
|
||||
return lock.disposer(resolve);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResourceLock;
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
'use strict';
|
||||
|
||||
const AbstractConnectionManager = require('../abstract/connection-manager');
|
||||
const SequelizeErrors = require('../../errors');
|
||||
const Promise = require('../../promise');
|
||||
const { logger } = require('../../utils/logger');
|
||||
const DataTypes = require('../../data-types').mysql;
|
||||
const momentTz = require('moment-timezone');
|
||||
const debug = logger.debugContext('connection:mysql');
|
||||
const parserStore = require('../parserStore')('mysql');
|
||||
|
||||
/**
|
||||
* MySQL Connection Manager
|
||||
*
|
||||
* Get connections, validate and disconnect them.
|
||||
* AbstractConnectionManager pooling use it to handle MySQL specific connections
|
||||
* Use https://github.com/sidorares/node-mysql2 to connect with MySQL server
|
||||
*
|
||||
* @extends AbstractConnectionManager
|
||||
* @returns Class<ConnectionManager>
|
||||
* @private
|
||||
*/
|
||||
|
||||
class ConnectionManager extends AbstractConnectionManager {
|
||||
constructor(dialect, sequelize) {
|
||||
sequelize.config.port = sequelize.config.port || 3306;
|
||||
super(dialect, sequelize);
|
||||
this.lib = this._loadDialectModule('mysql2');
|
||||
this.refreshTypeParser(DataTypes);
|
||||
}
|
||||
|
||||
_refreshTypeParser(dataType) {
|
||||
parserStore.refresh(dataType);
|
||||
}
|
||||
|
||||
_clearTypeParser() {
|
||||
parserStore.clear();
|
||||
}
|
||||
|
||||
static _typecast(field, next) {
|
||||
if (parserStore.get(field.type)) {
|
||||
return parserStore.get(field.type)(field, this.sequelize.options, next);
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect with MySQL database based on config, Handle any errors in connection
|
||||
* Set the pool handlers on connection.error
|
||||
* Also set proper timezone once connection is connected.
|
||||
*
|
||||
* @param {Object} config
|
||||
* @returns {Promise<Connection>}
|
||||
* @private
|
||||
*/
|
||||
connect(config) {
|
||||
const connectionConfig = Object.assign({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.username,
|
||||
flags: '-FOUND_ROWS',
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
timezone: this.sequelize.options.timezone,
|
||||
typeCast: ConnectionManager._typecast.bind(this),
|
||||
bigNumberStrings: false,
|
||||
supportBigNumbers: true
|
||||
}, config.dialectOptions);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const connection = this.lib.createConnection(connectionConfig);
|
||||
|
||||
const errorHandler = e => {
|
||||
// clean up connect & error event if there is error
|
||||
connection.removeListener('connect', connectHandler);
|
||||
connection.removeListener('error', connectHandler);
|
||||
reject(e);
|
||||
};
|
||||
|
||||
const connectHandler = () => {
|
||||
// clean up error event if connected
|
||||
connection.removeListener('error', errorHandler);
|
||||
resolve(connection);
|
||||
};
|
||||
|
||||
// don't use connection.once for error event handling here
|
||||
// mysql2 emit error two times in case handshake was failed
|
||||
// first error is protocol_lost and second is timeout
|
||||
// if we will use `once.error` node process will crash on 2nd error emit
|
||||
connection.on('error', errorHandler);
|
||||
connection.once('connect', connectHandler);
|
||||
})
|
||||
.tap(() => { debug('connection acquired'); })
|
||||
.then(connection => {
|
||||
connection.on('error', error => {
|
||||
switch (error.code) {
|
||||
case 'ESOCKET':
|
||||
case 'ECONNRESET':
|
||||
case 'EPIPE':
|
||||
case 'PROTOCOL_CONNECTION_LOST':
|
||||
this.pool.destroy(connection);
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.sequelize.config.keepDefaultTimezone) {
|
||||
// set timezone for this connection
|
||||
// but named timezone are not directly supported in mysql, so get its offset first
|
||||
let tzOffset = this.sequelize.options.timezone;
|
||||
tzOffset = /\//.test(tzOffset) ? momentTz.tz(tzOffset).format('Z') : tzOffset;
|
||||
return connection.query(`SET time_zone = '${tzOffset}'`, err => {
|
||||
if (err) { reject(err); } else { resolve(connection); }
|
||||
});
|
||||
}
|
||||
|
||||
// return connection without executing SET time_zone query
|
||||
resolve(connection);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
switch (err.code) {
|
||||
case 'ECONNREFUSED':
|
||||
throw new SequelizeErrors.ConnectionRefusedError(err);
|
||||
case 'ER_ACCESS_DENIED_ERROR':
|
||||
throw new SequelizeErrors.AccessDeniedError(err);
|
||||
case 'ENOTFOUND':
|
||||
throw new SequelizeErrors.HostNotFoundError(err);
|
||||
case 'EHOSTUNREACH':
|
||||
throw new SequelizeErrors.HostNotReachableError(err);
|
||||
case 'EINVAL':
|
||||
throw new SequelizeErrors.InvalidConnectionError(err);
|
||||
default:
|
||||
throw new SequelizeErrors.ConnectionError(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
disconnect(connection) {
|
||||
// Don't disconnect connections with CLOSED state
|
||||
if (connection._closing) {
|
||||
debug('connection tried to disconnect but was already at CLOSED state');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.fromCallback(callback => connection.end(callback));
|
||||
}
|
||||
|
||||
validate(connection) {
|
||||
return connection
|
||||
&& !connection._fatalError
|
||||
&& !connection._protocolError
|
||||
&& !connection._closing
|
||||
&& !connection.stream.destroyed;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConnectionManager;
|
||||
module.exports.ConnectionManager = ConnectionManager;
|
||||
module.exports.default = ConnectionManager;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
'use strict';
|
||||
|
||||
const wkx = require('wkx');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment-timezone');
|
||||
module.exports = BaseTypes => {
|
||||
BaseTypes.ABSTRACT.prototype.dialectTypes = 'https://dev.mysql.com/doc/refman/5.7/en/data-types.html';
|
||||
|
||||
/**
|
||||
* types: [buffer_type, ...]
|
||||
* @see buffer_type here https://dev.mysql.com/doc/refman/5.7/en/c-api-prepared-statement-type-codes.html
|
||||
* @see hex here https://github.com/sidorares/node-mysql2/blob/master/lib/constants/types.js
|
||||
*/
|
||||
|
||||
BaseTypes.DATE.types.mysql = ['DATETIME'];
|
||||
BaseTypes.STRING.types.mysql = ['VAR_STRING'];
|
||||
BaseTypes.CHAR.types.mysql = ['STRING'];
|
||||
BaseTypes.TEXT.types.mysql = ['BLOB'];
|
||||
BaseTypes.TINYINT.types.mysql = ['TINY'];
|
||||
BaseTypes.SMALLINT.types.mysql = ['SHORT'];
|
||||
BaseTypes.MEDIUMINT.types.mysql = ['INT24'];
|
||||
BaseTypes.INTEGER.types.mysql = ['LONG'];
|
||||
BaseTypes.BIGINT.types.mysql = ['LONGLONG'];
|
||||
BaseTypes.FLOAT.types.mysql = ['FLOAT'];
|
||||
BaseTypes.TIME.types.mysql = ['TIME'];
|
||||
BaseTypes.DATEONLY.types.mysql = ['DATE'];
|
||||
BaseTypes.BOOLEAN.types.mysql = ['TINY'];
|
||||
BaseTypes.BLOB.types.mysql = ['TINYBLOB', 'BLOB', 'LONGBLOB'];
|
||||
BaseTypes.DECIMAL.types.mysql = ['NEWDECIMAL'];
|
||||
BaseTypes.UUID.types.mysql = false;
|
||||
BaseTypes.ENUM.types.mysql = false;
|
||||
BaseTypes.REAL.types.mysql = ['DOUBLE'];
|
||||
BaseTypes.DOUBLE.types.mysql = ['DOUBLE'];
|
||||
BaseTypes.GEOMETRY.types.mysql = ['GEOMETRY'];
|
||||
BaseTypes.JSON.types.mysql = ['JSON'];
|
||||
|
||||
class DECIMAL extends BaseTypes.DECIMAL {
|
||||
toSql() {
|
||||
let definition = super.toSql();
|
||||
if (this._unsigned) {
|
||||
definition += ' UNSIGNED';
|
||||
}
|
||||
if (this._zerofill) {
|
||||
definition += ' ZEROFILL';
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
|
||||
class DATE extends BaseTypes.DATE {
|
||||
toSql() {
|
||||
return `DATETIME${this._length ? `(${this._length})` : ''}`;
|
||||
}
|
||||
_stringify(date, options) {
|
||||
date = this._applyTimezone(date, options);
|
||||
// Fractional DATETIMEs only supported on MySQL 5.6.4+
|
||||
if (this._length) {
|
||||
return date.format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
}
|
||||
return date.format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
static parse(value, options) {
|
||||
value = value.string();
|
||||
if (value === null) {
|
||||
return value;
|
||||
}
|
||||
if (moment.tz.zone(options.timezone)) {
|
||||
value = moment.tz(value, options.timezone).toDate();
|
||||
}
|
||||
else {
|
||||
value = new Date(`${value} ${options.timezone}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
class DATEONLY extends BaseTypes.DATEONLY {
|
||||
static parse(value) {
|
||||
return value.string();
|
||||
}
|
||||
}
|
||||
class UUID extends BaseTypes.UUID {
|
||||
toSql() {
|
||||
return 'CHAR(36) BINARY';
|
||||
}
|
||||
}
|
||||
|
||||
const SUPPORTED_GEOMETRY_TYPES = ['POINT', 'LINESTRING', 'POLYGON'];
|
||||
|
||||
class GEOMETRY extends BaseTypes.GEOMETRY {
|
||||
constructor(type, srid) {
|
||||
super(type, srid);
|
||||
if (_.isEmpty(this.type)) {
|
||||
this.sqlType = this.key;
|
||||
return;
|
||||
}
|
||||
if (SUPPORTED_GEOMETRY_TYPES.includes(this.type)) {
|
||||
this.sqlType = this.type;
|
||||
return;
|
||||
}
|
||||
throw new Error(`Supported geometry types are: ${SUPPORTED_GEOMETRY_TYPES.join(', ')}`);
|
||||
}
|
||||
static parse(value) {
|
||||
value = value.buffer();
|
||||
// Empty buffer, MySQL doesn't support POINT EMPTY
|
||||
// check, https://dev.mysql.com/worklog/task/?id=2381
|
||||
if (!value || value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
// For some reason, discard the first 4 bytes
|
||||
value = value.slice(4);
|
||||
return wkx.Geometry.parse(value).toGeoJSON();
|
||||
}
|
||||
toSql() {
|
||||
return this.sqlType;
|
||||
}
|
||||
}
|
||||
|
||||
class ENUM extends BaseTypes.ENUM {
|
||||
toSql(options) {
|
||||
return `ENUM(${this.values.map(value => options.escape(value)).join(', ')})`;
|
||||
}
|
||||
}
|
||||
|
||||
class JSONTYPE extends BaseTypes.JSON {
|
||||
_stringify(value, options) {
|
||||
return options.operation === 'where' && typeof value === 'string' ? value : JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ENUM,
|
||||
DATE,
|
||||
DATEONLY,
|
||||
UUID,
|
||||
GEOMETRY,
|
||||
DECIMAL,
|
||||
JSON: JSONTYPE
|
||||
};
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
'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').mysql;
|
||||
|
||||
class MysqlDialect extends AbstractDialect {
|
||||
constructor(sequelize) {
|
||||
super();
|
||||
this.sequelize = sequelize;
|
||||
this.connectionManager = new ConnectionManager(this, sequelize);
|
||||
this.QueryGenerator = new QueryGenerator({
|
||||
_dialect: this,
|
||||
sequelize
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MysqlDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype.supports), {
|
||||
'VALUES ()': true,
|
||||
'LIMIT ON UPDATE': true,
|
||||
lock: true,
|
||||
forShare: 'LOCK IN SHARE MODE',
|
||||
settingIsolationLevelDuringTransaction: false,
|
||||
inserts: {
|
||||
ignoreDuplicates: ' IGNORE',
|
||||
updateOnDuplicate: ' ON DUPLICATE KEY UPDATE'
|
||||
},
|
||||
index: {
|
||||
collate: false,
|
||||
length: true,
|
||||
parser: true,
|
||||
type: true,
|
||||
using: 1
|
||||
},
|
||||
constraints: {
|
||||
dropConstraint: false,
|
||||
check: false
|
||||
},
|
||||
indexViaAlter: true,
|
||||
indexHints: true,
|
||||
NUMERIC: true,
|
||||
GEOMETRY: true,
|
||||
JSON: true,
|
||||
REGEXP: true
|
||||
});
|
||||
|
||||
ConnectionManager.prototype.defaultVersion = '5.6.0';
|
||||
MysqlDialect.prototype.Query = Query;
|
||||
MysqlDialect.prototype.QueryGenerator = QueryGenerator;
|
||||
MysqlDialect.prototype.DataTypes = DataTypes;
|
||||
MysqlDialect.prototype.name = 'mysql';
|
||||
MysqlDialect.prototype.TICK_CHAR = '`';
|
||||
MysqlDialect.prototype.TICK_CHAR_LEFT = MysqlDialect.prototype.TICK_CHAR;
|
||||
MysqlDialect.prototype.TICK_CHAR_RIGHT = MysqlDialect.prototype.TICK_CHAR;
|
||||
|
||||
module.exports = MysqlDialect;
|
||||
+541
@@ -0,0 +1,541 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const Utils = require('../../utils');
|
||||
const AbstractQueryGenerator = require('../abstract/query-generator');
|
||||
const util = require('util');
|
||||
const Op = require('../../operators');
|
||||
|
||||
|
||||
const jsonFunctionRegex = /^\s*((?:[a-z]+_){0,2}jsonb?(?:_[a-z]+){0,2})\([^)]*\)/i;
|
||||
const jsonOperatorRegex = /^\s*(->>?|@>|<@|\?[|&]?|\|{2}|#-)/i;
|
||||
const tokenCaptureRegex = /^\s*((?:([`"'])(?:(?!\2).|\2{2})*\2)|[\w\d\s]+|[().,;+-])/i;
|
||||
const foreignKeyFields = 'CONSTRAINT_NAME as constraint_name,'
|
||||
+ 'CONSTRAINT_NAME as constraintName,'
|
||||
+ 'CONSTRAINT_SCHEMA as constraintSchema,'
|
||||
+ 'CONSTRAINT_SCHEMA as constraintCatalog,'
|
||||
+ 'TABLE_NAME as tableName,'
|
||||
+ 'TABLE_SCHEMA as tableSchema,'
|
||||
+ 'TABLE_SCHEMA as tableCatalog,'
|
||||
+ 'COLUMN_NAME as columnName,'
|
||||
+ 'REFERENCED_TABLE_SCHEMA as referencedTableSchema,'
|
||||
+ 'REFERENCED_TABLE_SCHEMA as referencedTableCatalog,'
|
||||
+ 'REFERENCED_TABLE_NAME as referencedTableName,'
|
||||
+ 'REFERENCED_COLUMN_NAME as referencedColumnName';
|
||||
|
||||
const typeWithoutDefault = new Set(['BLOB', 'TEXT', 'GEOMETRY', 'JSON']);
|
||||
|
||||
class MySQLQueryGenerator extends AbstractQueryGenerator {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.OperatorMap = Object.assign({}, this.OperatorMap, {
|
||||
[Op.regexp]: 'REGEXP',
|
||||
[Op.notRegexp]: 'NOT REGEXP'
|
||||
});
|
||||
}
|
||||
|
||||
createDatabaseQuery(databaseName, options) {
|
||||
options = Object.assign({
|
||||
charset: null,
|
||||
collate: null
|
||||
}, options || {});
|
||||
|
||||
const database = this.quoteIdentifier(databaseName);
|
||||
const charset = options.charset ? ` DEFAULT CHARACTER SET ${this.escape(options.charset)}` : '';
|
||||
const collate = options.collate ? ` DEFAULT COLLATE ${this.escape(options.collate)}` : '';
|
||||
|
||||
return `${`CREATE DATABASE IF NOT EXISTS ${database}${charset}${collate}`.trim()};`;
|
||||
}
|
||||
|
||||
dropDatabaseQuery(databaseName) {
|
||||
return `DROP DATABASE IF EXISTS ${this.quoteIdentifier(databaseName).trim()};`;
|
||||
}
|
||||
|
||||
createSchema() {
|
||||
return 'SHOW TABLES';
|
||||
}
|
||||
|
||||
showSchemasQuery() {
|
||||
return 'SHOW TABLES';
|
||||
}
|
||||
|
||||
versionQuery() {
|
||||
return 'SELECT VERSION() as `version`';
|
||||
}
|
||||
|
||||
createTableQuery(tableName, attributes, options) {
|
||||
options = Object.assign({
|
||||
engine: 'InnoDB',
|
||||
charset: null,
|
||||
rowFormat: null
|
||||
}, options || {});
|
||||
|
||||
const primaryKeys = [];
|
||||
const foreignKeys = {};
|
||||
const attrStr = [];
|
||||
|
||||
for (const attr in attributes) {
|
||||
if (!Object.prototype.hasOwnProperty.call(attributes, attr)) continue;
|
||||
const dataType = attributes[attr];
|
||||
let match;
|
||||
|
||||
if (dataType.includes('PRIMARY KEY')) {
|
||||
primaryKeys.push(attr);
|
||||
|
||||
if (dataType.includes('REFERENCES')) {
|
||||
// MySQL doesn't support inline REFERENCES declarations: move to the end
|
||||
match = dataType.match(/^(.+) (REFERENCES.*)$/);
|
||||
attrStr.push(`${this.quoteIdentifier(attr)} ${match[1].replace('PRIMARY KEY', '')}`);
|
||||
foreignKeys[attr] = match[2];
|
||||
} else {
|
||||
attrStr.push(`${this.quoteIdentifier(attr)} ${dataType.replace('PRIMARY KEY', '')}`);
|
||||
}
|
||||
} else if (dataType.includes('REFERENCES')) {
|
||||
// MySQL doesn't support inline REFERENCES declarations: move to the end
|
||||
match = dataType.match(/^(.+) (REFERENCES.*)$/);
|
||||
attrStr.push(`${this.quoteIdentifier(attr)} ${match[1]}`);
|
||||
foreignKeys[attr] = match[2];
|
||||
} else {
|
||||
attrStr.push(`${this.quoteIdentifier(attr)} ${dataType}`);
|
||||
}
|
||||
}
|
||||
|
||||
const table = this.quoteTable(tableName);
|
||||
let attributesClause = attrStr.join(', ');
|
||||
const comment = options.comment && typeof options.comment === 'string' ? ` COMMENT ${this.escape(options.comment)}` : '';
|
||||
const engine = options.engine;
|
||||
const charset = options.charset ? ` DEFAULT CHARSET=${options.charset}` : '';
|
||||
const collation = options.collate ? ` COLLATE ${options.collate}` : '';
|
||||
const rowFormat = options.rowFormat ? ` ROW_FORMAT=${options.rowFormat}` : '';
|
||||
const initialAutoIncrement = options.initialAutoIncrement ? ` AUTO_INCREMENT=${options.initialAutoIncrement}` : '';
|
||||
const pkString = primaryKeys.map(pk => this.quoteIdentifier(pk)).join(', ');
|
||||
|
||||
if (options.uniqueKeys) {
|
||||
_.each(options.uniqueKeys, (columns, indexName) => {
|
||||
if (columns.customIndex) {
|
||||
if (typeof indexName !== 'string') {
|
||||
indexName = `uniq_${tableName}_${columns.fields.join('_')}`;
|
||||
}
|
||||
attributesClause += `, UNIQUE ${this.quoteIdentifier(indexName)} (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pkString.length > 0) {
|
||||
attributesClause += `, PRIMARY KEY (${pkString})`;
|
||||
}
|
||||
|
||||
for (const fkey in foreignKeys) {
|
||||
if (Object.prototype.hasOwnProperty.call(foreignKeys, fkey)) {
|
||||
attributesClause += `, FOREIGN KEY (${this.quoteIdentifier(fkey)}) ${foreignKeys[fkey]}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `CREATE TABLE IF NOT EXISTS ${table} (${attributesClause}) ENGINE=${engine}${comment}${charset}${collation}${initialAutoIncrement}${rowFormat};`;
|
||||
}
|
||||
|
||||
|
||||
describeTableQuery(tableName, schema, schemaDelimiter) {
|
||||
const table = this.quoteTable(
|
||||
this.addSchema({
|
||||
tableName,
|
||||
_schema: schema,
|
||||
_schemaDelimiter: schemaDelimiter
|
||||
})
|
||||
);
|
||||
|
||||
return `SHOW FULL COLUMNS FROM ${table};`;
|
||||
}
|
||||
|
||||
showTablesQuery(database) {
|
||||
let query = 'SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = \'BASE TABLE\'';
|
||||
if (database) {
|
||||
query += ` AND TABLE_SCHEMA = ${this.escape(database)}`;
|
||||
} else {
|
||||
query += ' AND TABLE_SCHEMA NOT IN (\'MYSQL\', \'INFORMATION_SCHEMA\', \'PERFORMANCE_SCHEMA\', \'SYS\')';
|
||||
}
|
||||
return `${query};`;
|
||||
}
|
||||
|
||||
addColumnQuery(table, key, dataType) {
|
||||
const definition = this.attributeToSQL(dataType, {
|
||||
context: 'addColumn',
|
||||
tableName: table,
|
||||
foreignKey: key
|
||||
});
|
||||
|
||||
return `ALTER TABLE ${this.quoteTable(table)} ADD ${this.quoteIdentifier(key)} ${definition};`;
|
||||
}
|
||||
|
||||
removeColumnQuery(tableName, attributeName) {
|
||||
return `ALTER TABLE ${this.quoteTable(tableName)} DROP ${this.quoteIdentifier(attributeName)};`;
|
||||
}
|
||||
|
||||
changeColumnQuery(tableName, attributes) {
|
||||
const attrString = [];
|
||||
const constraintString = [];
|
||||
|
||||
for (const attributeName in attributes) {
|
||||
let definition = attributes[attributeName];
|
||||
if (definition.includes('REFERENCES')) {
|
||||
const attrName = this.quoteIdentifier(attributeName);
|
||||
definition = definition.replace(/.+?(?=REFERENCES)/, '');
|
||||
constraintString.push(`FOREIGN KEY (${attrName}) ${definition}`);
|
||||
} else {
|
||||
attrString.push(`\`${attributeName}\` \`${attributeName}\` ${definition}`);
|
||||
}
|
||||
}
|
||||
|
||||
let finalQuery = '';
|
||||
if (attrString.length) {
|
||||
finalQuery += `CHANGE ${attrString.join(', ')}`;
|
||||
finalQuery += constraintString.length ? ' ' : '';
|
||||
}
|
||||
if (constraintString.length) {
|
||||
finalQuery += `ADD ${constraintString.join(', ')}`;
|
||||
}
|
||||
|
||||
return `ALTER TABLE ${this.quoteTable(tableName)} ${finalQuery};`;
|
||||
}
|
||||
|
||||
renameColumnQuery(tableName, attrBefore, attributes) {
|
||||
const attrString = [];
|
||||
|
||||
for (const attrName in attributes) {
|
||||
const definition = attributes[attrName];
|
||||
attrString.push(`\`${attrBefore}\` \`${attrName}\` ${definition}`);
|
||||
}
|
||||
|
||||
return `ALTER TABLE ${this.quoteTable(tableName)} CHANGE ${attrString.join(', ')};`;
|
||||
}
|
||||
|
||||
handleSequelizeMethod(smth, tableName, factory, options, prepend) {
|
||||
if (smth instanceof Utils.Json) {
|
||||
// Parse nested object
|
||||
if (smth.conditions) {
|
||||
const conditions = this.parseConditionObject(smth.conditions).map(condition =>
|
||||
`${this.jsonPathExtractionQuery(condition.path[0], _.tail(condition.path))} = '${condition.value}'`
|
||||
);
|
||||
|
||||
return conditions.join(' AND ');
|
||||
}
|
||||
if (smth.path) {
|
||||
let str;
|
||||
|
||||
// Allow specifying conditions using the sqlite json functions
|
||||
if (this._checkValidJsonStatement(smth.path)) {
|
||||
str = smth.path;
|
||||
} else {
|
||||
// Also support json property accessors
|
||||
const paths = _.toPath(smth.path);
|
||||
const column = paths.shift();
|
||||
str = this.jsonPathExtractionQuery(column, paths);
|
||||
}
|
||||
|
||||
if (smth.value) {
|
||||
str += util.format(' = %s', this.escape(smth.value));
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
} else if (smth instanceof Utils.Cast) {
|
||||
if (/timestamp/i.test(smth.type)) {
|
||||
smth.type = 'datetime';
|
||||
} else if (smth.json && /boolean/i.test(smth.type)) {
|
||||
// true or false cannot be casted as booleans within a JSON structure
|
||||
smth.type = 'char';
|
||||
} else if (/double precision/i.test(smth.type) || /boolean/i.test(smth.type) || /integer/i.test(smth.type)) {
|
||||
smth.type = 'decimal';
|
||||
} else if (/text/i.test(smth.type)) {
|
||||
smth.type = 'char';
|
||||
}
|
||||
}
|
||||
|
||||
return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
|
||||
}
|
||||
|
||||
_toJSONValue(value) {
|
||||
// true/false are stored as strings in mysql
|
||||
if (typeof value === 'boolean') {
|
||||
return value.toString();
|
||||
}
|
||||
// null is stored as a string in mysql
|
||||
if (value === null) {
|
||||
return 'null';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
upsertQuery(tableName, insertValues, updateValues, where, model, options) {
|
||||
options.onDuplicate = 'UPDATE ';
|
||||
|
||||
options.onDuplicate += Object.keys(updateValues).map(key => {
|
||||
key = this.quoteIdentifier(key);
|
||||
return `${key}=VALUES(${key})`;
|
||||
}).join(', ');
|
||||
|
||||
return this.insertQuery(tableName, insertValues, model.rawAttributes, options);
|
||||
}
|
||||
|
||||
truncateTableQuery(tableName) {
|
||||
return `TRUNCATE ${this.quoteTable(tableName)}`;
|
||||
}
|
||||
|
||||
deleteQuery(tableName, where, options = {}, model) {
|
||||
let limit = '';
|
||||
let query = `DELETE FROM ${this.quoteTable(tableName)}`;
|
||||
|
||||
if (options.limit) {
|
||||
limit = ` LIMIT ${this.escape(options.limit)}`;
|
||||
}
|
||||
|
||||
where = this.getWhereConditions(where, null, model, options);
|
||||
|
||||
if (where) {
|
||||
query += ` WHERE ${where}`;
|
||||
}
|
||||
|
||||
return query + limit;
|
||||
}
|
||||
|
||||
showIndexesQuery(tableName, options) {
|
||||
return `SHOW INDEX FROM ${this.quoteTable(tableName)}${(options || {}).database ? ` FROM \`${options.database}\`` : ''}`;
|
||||
}
|
||||
|
||||
showConstraintsQuery(table, constraintName) {
|
||||
const tableName = table.tableName || table;
|
||||
const schemaName = table.schema;
|
||||
|
||||
let sql = [
|
||||
'SELECT CONSTRAINT_CATALOG AS constraintCatalog,',
|
||||
'CONSTRAINT_NAME AS constraintName,',
|
||||
'CONSTRAINT_SCHEMA AS constraintSchema,',
|
||||
'CONSTRAINT_TYPE AS constraintType,',
|
||||
'TABLE_NAME AS tableName,',
|
||||
'TABLE_SCHEMA AS tableSchema',
|
||||
'from INFORMATION_SCHEMA.TABLE_CONSTRAINTS',
|
||||
`WHERE table_name='${tableName}'`
|
||||
].join(' ');
|
||||
|
||||
if (constraintName) {
|
||||
sql += ` AND constraint_name = '${constraintName}'`;
|
||||
}
|
||||
|
||||
if (schemaName) {
|
||||
sql += ` AND TABLE_SCHEMA = '${schemaName}'`;
|
||||
}
|
||||
|
||||
return `${sql};`;
|
||||
}
|
||||
|
||||
removeIndexQuery(tableName, indexNameOrAttributes) {
|
||||
let indexName = indexNameOrAttributes;
|
||||
|
||||
if (typeof indexName !== 'string') {
|
||||
indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`);
|
||||
}
|
||||
|
||||
return `DROP INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteTable(tableName)}`;
|
||||
}
|
||||
|
||||
attributeToSQL(attribute, options) {
|
||||
if (!_.isPlainObject(attribute)) {
|
||||
attribute = {
|
||||
type: attribute
|
||||
};
|
||||
}
|
||||
|
||||
const attributeString = attribute.type.toString({ escape: this.escape.bind(this) });
|
||||
let template = attributeString;
|
||||
|
||||
if (attribute.allowNull === false) {
|
||||
template += ' NOT NULL';
|
||||
}
|
||||
|
||||
if (attribute.autoIncrement) {
|
||||
template += ' auto_increment';
|
||||
}
|
||||
|
||||
// BLOB/TEXT/GEOMETRY/JSON cannot have a default value
|
||||
if (!typeWithoutDefault.has(attributeString)
|
||||
&& attribute.type._binary !== true
|
||||
&& Utils.defaultValueSchemable(attribute.defaultValue)) {
|
||||
template += ` DEFAULT ${this.escape(attribute.defaultValue)}`;
|
||||
}
|
||||
|
||||
if (attribute.unique === true) {
|
||||
template += ' UNIQUE';
|
||||
}
|
||||
|
||||
if (attribute.primaryKey) {
|
||||
template += ' PRIMARY KEY';
|
||||
}
|
||||
|
||||
if (attribute.comment) {
|
||||
template += ` COMMENT ${this.escape(attribute.comment)}`;
|
||||
}
|
||||
|
||||
if (attribute.first) {
|
||||
template += ' FIRST';
|
||||
}
|
||||
if (attribute.after) {
|
||||
template += ` AFTER ${this.quoteIdentifier(attribute.after)}`;
|
||||
}
|
||||
|
||||
if (attribute.references) {
|
||||
|
||||
if (options && options.context === 'addColumn' && options.foreignKey) {
|
||||
const attrName = this.quoteIdentifier(options.foreignKey);
|
||||
const fkName = this.quoteIdentifier(`${options.tableName}_${attrName}_foreign_idx`);
|
||||
|
||||
template += `, ADD CONSTRAINT ${fkName} FOREIGN KEY (${attrName})`;
|
||||
}
|
||||
|
||||
template += ` REFERENCES ${this.quoteTable(attribute.references.model)}`;
|
||||
|
||||
if (attribute.references.key) {
|
||||
template += ` (${this.quoteIdentifier(attribute.references.key)})`;
|
||||
} else {
|
||||
template += ` (${this.quoteIdentifier('id')})`;
|
||||
}
|
||||
|
||||
if (attribute.onDelete) {
|
||||
template += ` ON DELETE ${attribute.onDelete.toUpperCase()}`;
|
||||
}
|
||||
|
||||
if (attribute.onUpdate) {
|
||||
template += ` ON UPDATE ${attribute.onUpdate.toUpperCase()}`;
|
||||
}
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
attributesToSQL(attributes, options) {
|
||||
const result = {};
|
||||
|
||||
for (const key in attributes) {
|
||||
const attribute = attributes[key];
|
||||
result[attribute.field || key] = this.attributeToSQL(attribute, options);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @private
|
||||
*/
|
||||
_checkValidJsonStatement(stmt) {
|
||||
if (typeof stmt !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 operatorMatches = jsonOperatorRegex.exec(string);
|
||||
if (operatorMatches) {
|
||||
currentIndex += operatorMatches[0].length;
|
||||
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
|
||||
if (hasJsonFunction && (hasInvalidToken || openingBrackets !== closingBrackets)) {
|
||||
throw new Error(`Invalid json statement: ${stmt}`);
|
||||
}
|
||||
|
||||
// return true if the statement has valid json function
|
||||
return hasJsonFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQL query that returns all foreign keys of a table.
|
||||
*
|
||||
* @param {Object} table The table.
|
||||
* @param {string} schemaName The name of the schema.
|
||||
* @returns {string} The generated sql query.
|
||||
* @private
|
||||
*/
|
||||
getForeignKeysQuery(table, schemaName) {
|
||||
const tableName = table.tableName || table;
|
||||
return `SELECT ${foreignKeyFields} FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE where TABLE_NAME = '${tableName}' AND CONSTRAINT_NAME!='PRIMARY' AND CONSTRAINT_SCHEMA='${schemaName}' AND REFERENCED_TABLE_NAME IS NOT NULL;`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQL query that returns the foreign key constraint of a given column.
|
||||
*
|
||||
* @param {Object} table The table.
|
||||
* @param {string} columnName The name of the column.
|
||||
* @returns {string} The generated sql query.
|
||||
* @private
|
||||
*/
|
||||
getForeignKeyQuery(table, columnName) {
|
||||
const quotedSchemaName = table.schema ? wrapSingleQuote(table.schema) : '';
|
||||
const quotedTableName = wrapSingleQuote(table.tableName || table);
|
||||
const quotedColumnName = wrapSingleQuote(columnName);
|
||||
|
||||
return `SELECT ${foreignKeyFields} FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE`
|
||||
+ ` WHERE (REFERENCED_TABLE_NAME = ${quotedTableName}${table.schema
|
||||
? ` AND REFERENCED_TABLE_SCHEMA = ${quotedSchemaName}`
|
||||
: ''} AND REFERENCED_COLUMN_NAME = ${quotedColumnName})`
|
||||
+ ` OR (TABLE_NAME = ${quotedTableName}${table.schema ?
|
||||
` AND TABLE_SCHEMA = ${quotedSchemaName}` : ''} AND COLUMN_NAME = ${quotedColumnName} AND REFERENCED_TABLE_NAME IS NOT NULL)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQL query that removes a foreign key from a table.
|
||||
*
|
||||
* @param {string} tableName The name of the table.
|
||||
* @param {string} foreignKey The name of the foreign key constraint.
|
||||
* @returns {string} The generated sql query.
|
||||
* @private
|
||||
*/
|
||||
dropForeignKeyQuery(tableName, foreignKey) {
|
||||
return `ALTER TABLE ${this.quoteTable(tableName)}
|
||||
DROP FOREIGN KEY ${this.quoteIdentifier(foreignKey)};`;
|
||||
}
|
||||
}
|
||||
|
||||
// private methods
|
||||
function wrapSingleQuote(identifier) {
|
||||
return Utils.addTicks(identifier, '\'');
|
||||
}
|
||||
|
||||
module.exports = MySQLQueryGenerator;
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
Returns an object that treats MySQL's inabilities to do certain queries.
|
||||
|
||||
@class QueryInterface
|
||||
@static
|
||||
@private
|
||||
*/
|
||||
|
||||
const Promise = require('../../promise');
|
||||
const sequelizeErrors = require('../../errors');
|
||||
|
||||
/**
|
||||
A wrapper that fixes MySQL's inability to cleanly remove columns from existing tables if they have a foreign key constraint.
|
||||
|
||||
@param {QueryInterface} qi
|
||||
@param {string} tableName The name of the table.
|
||||
@param {string} columnName The name of the attribute that we want to remove.
|
||||
@param {Object} options
|
||||
|
||||
@private
|
||||
*/
|
||||
function removeColumn(qi, tableName, columnName, options) {
|
||||
options = options || {};
|
||||
|
||||
return qi.sequelize.query(
|
||||
qi.QueryGenerator.getForeignKeyQuery(tableName.tableName ? tableName : {
|
||||
tableName,
|
||||
schema: qi.sequelize.config.database
|
||||
}, columnName),
|
||||
Object.assign({ raw: true }, options)
|
||||
)
|
||||
.then(([results]) => {
|
||||
//Exclude primary key constraint
|
||||
if (!results.length || results[0].constraint_name === 'PRIMARY') {
|
||||
// No foreign key constraints found, so we can remove the column
|
||||
return;
|
||||
}
|
||||
return Promise.map(results, constraint => qi.sequelize.query(
|
||||
qi.QueryGenerator.dropForeignKeyQuery(tableName, constraint.constraint_name),
|
||||
Object.assign({ raw: true }, options)
|
||||
));
|
||||
})
|
||||
.then(() => qi.sequelize.query(
|
||||
qi.QueryGenerator.removeColumnQuery(tableName, columnName),
|
||||
Object.assign({ raw: true }, options)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryInterface} qi
|
||||
* @param {string} tableName
|
||||
* @param {string} constraintName
|
||||
* @param {Object} options
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function removeConstraint(qi, tableName, constraintName, options) {
|
||||
const sql = qi.QueryGenerator.showConstraintsQuery(
|
||||
tableName.tableName ? tableName : {
|
||||
tableName,
|
||||
schema: qi.sequelize.config.database
|
||||
}, constraintName);
|
||||
|
||||
return qi.sequelize.query(sql, Object.assign({}, options,
|
||||
{ type: qi.sequelize.QueryTypes.SHOWCONSTRAINTS }))
|
||||
.then(constraints => {
|
||||
const constraint = constraints[0];
|
||||
let query;
|
||||
if (!constraint || !constraint.constraintType) {
|
||||
throw new sequelizeErrors.UnknownConstraintError(
|
||||
{
|
||||
message: `Constraint ${constraintName} on table ${tableName} does not exist`,
|
||||
constraint: constraintName,
|
||||
table: tableName
|
||||
});
|
||||
}
|
||||
|
||||
if (constraint.constraintType === 'FOREIGN KEY') {
|
||||
query = qi.QueryGenerator.dropForeignKeyQuery(tableName, constraintName);
|
||||
} else {
|
||||
query = qi.QueryGenerator.removeIndexQuery(constraint.tableName, constraint.constraintName);
|
||||
}
|
||||
|
||||
return qi.sequelize.query(query, options);
|
||||
});
|
||||
}
|
||||
|
||||
exports.removeConstraint = removeConstraint;
|
||||
exports.removeColumn = removeColumn;
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
'use strict';
|
||||
|
||||
const Utils = require('../../utils');
|
||||
const AbstractQuery = require('../abstract/query');
|
||||
const sequelizeErrors = require('../../errors');
|
||||
const _ = require('lodash');
|
||||
const { logger } = require('../../utils/logger');
|
||||
|
||||
const debug = logger.debugContext('sql:mysql');
|
||||
|
||||
|
||||
class Query extends AbstractQuery {
|
||||
constructor(connection, sequelize, options) {
|
||||
super(connection, sequelize, Object.assign({ showWarnings: false }, options));
|
||||
}
|
||||
|
||||
static formatBindParameters(sql, values, dialect) {
|
||||
const bindParam = [];
|
||||
const replacementFunc = (match, key, values) => {
|
||||
if (values[key] !== undefined) {
|
||||
bindParam.push(values[key]);
|
||||
return '?';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0];
|
||||
return [sql, bindParam.length > 0 ? bindParam : undefined];
|
||||
}
|
||||
|
||||
run(sql, parameters) {
|
||||
this.sql = sql;
|
||||
const { connection, options } = this;
|
||||
|
||||
//do we need benchmark for this query execution
|
||||
const showWarnings = this.sequelize.options.showWarnings || options.showWarnings;
|
||||
|
||||
const complete = this._logQuery(sql, debug, parameters);
|
||||
|
||||
return new Utils.Promise((resolve, reject) => {
|
||||
const handler = (err, results) => {
|
||||
complete();
|
||||
|
||||
if (err) {
|
||||
// MySQL automatically rolls-back transactions in the event of a deadlock
|
||||
if (options.transaction && err.errno === 1213) {
|
||||
options.transaction.finished = 'rollback';
|
||||
}
|
||||
err.sql = sql;
|
||||
err.parameters = parameters;
|
||||
|
||||
reject(this.formatError(err));
|
||||
} else {
|
||||
resolve(results);
|
||||
}
|
||||
};
|
||||
if (parameters) {
|
||||
debug('parameters(%j)', parameters);
|
||||
connection.execute(sql, parameters, handler).setMaxListeners(100);
|
||||
} else {
|
||||
connection.query({ sql }, handler).setMaxListeners(100);
|
||||
}
|
||||
})
|
||||
// Log warnings if we've got them.
|
||||
.then(results => {
|
||||
if (showWarnings && results && results.warningStatus > 0) {
|
||||
return this.logWarnings(results);
|
||||
}
|
||||
return results;
|
||||
})
|
||||
// Return formatted results...
|
||||
.then(results => this.formatResults(results));
|
||||
}
|
||||
|
||||
/**
|
||||
* High level function that handles the results of a query execution.
|
||||
*
|
||||
*
|
||||
* Example:
|
||||
* query.formatResults([
|
||||
* {
|
||||
* id: 1, // this is from the main table
|
||||
* attr2: 'snafu', // this is from the main table
|
||||
* Tasks.id: 1, // this is from the associated table
|
||||
* Tasks.title: 'task' // this is from the associated table
|
||||
* }
|
||||
* ])
|
||||
*
|
||||
* @param {Array} data - The result of the query execution.
|
||||
* @private
|
||||
*/
|
||||
formatResults(data) {
|
||||
let result = this.instance;
|
||||
|
||||
if (this.isInsertQuery(data)) {
|
||||
this.handleInsertQuery(data);
|
||||
|
||||
if (!this.instance) {
|
||||
// handle bulkCreate AI primiary key
|
||||
if (
|
||||
data.constructor.name === 'ResultSetHeader'
|
||||
&& this.model
|
||||
&& this.model.autoIncrementAttribute
|
||||
&& this.model.autoIncrementAttribute === this.model.primaryKeyAttribute
|
||||
&& this.model.rawAttributes[this.model.primaryKeyAttribute]
|
||||
) {
|
||||
const startId = data[this.getInsertIdField()];
|
||||
result = [];
|
||||
for (let i = startId; i < startId + data.affectedRows; i++) {
|
||||
result.push({ [this.model.rawAttributes[this.model.primaryKeyAttribute].field]: i });
|
||||
}
|
||||
} else {
|
||||
result = data[this.getInsertIdField()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isSelectQuery()) {
|
||||
return this.handleSelectQuery(data);
|
||||
}
|
||||
if (this.isShowTablesQuery()) {
|
||||
return this.handleShowTablesQuery(data);
|
||||
}
|
||||
if (this.isDescribeQuery()) {
|
||||
result = {};
|
||||
|
||||
for (const _result of data) {
|
||||
const enumRegex = /^enum/i;
|
||||
result[_result.Field] = {
|
||||
type: enumRegex.test(_result.Type) ? _result.Type.replace(enumRegex, 'ENUM') : _result.Type.toUpperCase(),
|
||||
allowNull: _result.Null === 'YES',
|
||||
defaultValue: _result.Default,
|
||||
primaryKey: _result.Key === 'PRI',
|
||||
autoIncrement: Object.prototype.hasOwnProperty.call(_result, 'Extra') && _result.Extra.toLowerCase() === 'auto_increment',
|
||||
comment: _result.Comment ? _result.Comment : null
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (this.isShowIndexesQuery()) {
|
||||
return this.handleShowIndexesQuery(data);
|
||||
}
|
||||
if (this.isCallQuery()) {
|
||||
return data[0];
|
||||
}
|
||||
if (this.isBulkUpdateQuery() || this.isBulkDeleteQuery() || this.isUpsertQuery()) {
|
||||
return data.affectedRows;
|
||||
}
|
||||
if (this.isVersionQuery()) {
|
||||
return data[0].version;
|
||||
}
|
||||
if (this.isForeignKeysQuery()) {
|
||||
return data;
|
||||
}
|
||||
if (this.isInsertQuery() || this.isUpdateQuery()) {
|
||||
return [result, data.affectedRows];
|
||||
}
|
||||
if (this.isShowConstraintsQuery()) {
|
||||
return data;
|
||||
}
|
||||
if (this.isRawQuery()) {
|
||||
// MySQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
|
||||
return [data, data];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
logWarnings(results) {
|
||||
return this.run('SHOW WARNINGS').then(warningResults => {
|
||||
const warningMessage = `MySQL Warnings (${this.connection.uuid || 'default'}): `;
|
||||
const messages = [];
|
||||
for (const _warningRow of warningResults) {
|
||||
if (_warningRow === undefined || typeof _warningRow[Symbol.iterator] !== 'function') continue;
|
||||
for (const _warningResult of _warningRow) {
|
||||
if (Object.prototype.hasOwnProperty.call(_warningResult, 'Message')) {
|
||||
messages.push(_warningResult.Message);
|
||||
} else {
|
||||
for (const _objectKey of _warningResult.keys()) {
|
||||
messages.push([_objectKey, _warningResult[_objectKey]].join(': '));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.sequelize.log(warningMessage + messages.join('; '), this.options);
|
||||
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
formatError(err) {
|
||||
const errCode = err.errno || err.code;
|
||||
|
||||
switch (errCode) {
|
||||
case 1062: {
|
||||
const match = err.message.match(/Duplicate entry '([\s\S]*)' for key '?((.|\s)*?)'?$/);
|
||||
let fields = {};
|
||||
let message = 'Validation error';
|
||||
const values = match ? match[1].split('-') : undefined;
|
||||
const fieldKey = match ? match[2] : undefined;
|
||||
const fieldVal = match ? match[1] : undefined;
|
||||
const uniqueKey = this.model && this.model.uniqueKeys[fieldKey];
|
||||
|
||||
if (uniqueKey) {
|
||||
if (uniqueKey.msg) message = uniqueKey.msg;
|
||||
fields = _.zipObject(uniqueKey.fields, values);
|
||||
} else {
|
||||
fields[fieldKey] = fieldVal;
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
_.forOwn(fields, (value, field) => {
|
||||
errors.push(new sequelizeErrors.ValidationErrorItem(
|
||||
this.getUniqueConstraintErrorMessage(field),
|
||||
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
|
||||
field,
|
||||
value,
|
||||
this.instance,
|
||||
'not_unique'
|
||||
));
|
||||
});
|
||||
|
||||
return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields });
|
||||
}
|
||||
|
||||
case 1451:
|
||||
case 1452: {
|
||||
// e.g. CONSTRAINT `example_constraint_name` FOREIGN KEY (`example_id`) REFERENCES `examples` (`id`)
|
||||
const match = err.message.match(/CONSTRAINT ([`"])(.*)\1 FOREIGN KEY \(\1(.*)\1\) REFERENCES \1(.*)\1 \(\1(.*)\1\)/);
|
||||
const quoteChar = match ? match[1] : '`';
|
||||
const fields = match ? match[3].split(new RegExp(`${quoteChar}, *${quoteChar}`)) : undefined;
|
||||
|
||||
return new sequelizeErrors.ForeignKeyConstraintError({
|
||||
reltype: String(errCode) === '1451' ? 'parent' : 'child',
|
||||
table: match ? match[4] : undefined,
|
||||
fields,
|
||||
value: fields && fields.length && this.instance && this.instance[fields[0]] || undefined,
|
||||
index: match ? match[2] : undefined,
|
||||
parent: err
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return new sequelizeErrors.DatabaseError(err);
|
||||
}
|
||||
}
|
||||
|
||||
handleShowIndexesQuery(data) {
|
||||
// Group by index name, and collect all fields
|
||||
data = data.reduce((acc, item) => {
|
||||
if (!(item.Key_name in acc)) {
|
||||
acc[item.Key_name] = item;
|
||||
item.fields = [];
|
||||
}
|
||||
|
||||
acc[item.Key_name].fields[item.Seq_in_index - 1] = {
|
||||
attribute: item.Column_name,
|
||||
length: item.Sub_part || undefined,
|
||||
order: item.Collation === 'A' ? 'ASC' : undefined
|
||||
};
|
||||
delete item.column_name;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return _.map(data, item => ({
|
||||
primary: item.Key_name === 'PRIMARY',
|
||||
fields: item.fields,
|
||||
name: item.Key_name,
|
||||
tableName: item.Table,
|
||||
unique: item.Non_unique !== 1,
|
||||
type: item.Index_type
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Query;
|
||||
module.exports.Query = Query;
|
||||
module.exports.default = Query;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
const stores = new Map();
|
||||
|
||||
module.exports = dialect => {
|
||||
if (!stores.has(dialect)) {
|
||||
stores.set(dialect, new Map());
|
||||
}
|
||||
|
||||
return {
|
||||
clear() {
|
||||
stores.get(dialect).clear();
|
||||
},
|
||||
refresh(dataType) {
|
||||
for (const type of dataType.types[dialect]) {
|
||||
stores.get(dialect).set(type, dataType.parse);
|
||||
}
|
||||
},
|
||||
get(type) {
|
||||
return stores.get(dialect).get(type);
|
||||
}
|
||||
};
|
||||
};
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const AbstractConnectionManager = require('../abstract/connection-manager');
|
||||
const { logger } = require('../../utils/logger');
|
||||
const debug = logger.debugContext('connection:pg');
|
||||
const Promise = require('../../promise');
|
||||
const sequelizeErrors = require('../../errors');
|
||||
const semver = require('semver');
|
||||
const dataTypes = require('../../data-types');
|
||||
const moment = require('moment-timezone');
|
||||
|
||||
class ConnectionManager extends AbstractConnectionManager {
|
||||
constructor(dialect, sequelize) {
|
||||
sequelize.config.port = sequelize.config.port || 5432;
|
||||
super(dialect, sequelize);
|
||||
|
||||
const pgLib = this._loadDialectModule('pg');
|
||||
this.lib = this.sequelize.config.native ? pgLib.native : pgLib;
|
||||
|
||||
this._clearDynamicOIDs();
|
||||
this._clearTypeParser();
|
||||
this.refreshTypeParser(dataTypes.postgres);
|
||||
}
|
||||
|
||||
// Expose this as a method so that the parsing may be updated when the user has added additional, custom types
|
||||
_refreshTypeParser(dataType) {
|
||||
const arrayParserBuilder = parser => {
|
||||
return value => this.lib.types.arrayParser.create(value, parser).parse();
|
||||
};
|
||||
const rangeParserBuilder = parser => {
|
||||
return value => dataType.parse(value, { parser });
|
||||
};
|
||||
|
||||
// Set range parsers
|
||||
if (dataType.key.toLowerCase() === 'range') {
|
||||
for (const name in this.nameOidMap) {
|
||||
const entry = this.nameOidMap[name];
|
||||
if (! entry.rangeOid) continue;
|
||||
|
||||
const rangeParser = rangeParserBuilder(this.getTypeParser(entry.oid));
|
||||
const arrayRangeParser = arrayParserBuilder(rangeParser);
|
||||
|
||||
this.oidParserMap.set(entry.rangeOid, rangeParser);
|
||||
if (! entry.arrayRangeOid) continue;
|
||||
this.oidParserMap.set(entry.arrayRangeOid, arrayRangeParser);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create parsers for normal or enum data types
|
||||
const parser = value => dataType.parse(value);
|
||||
const arrayParser = arrayParserBuilder(parser);
|
||||
|
||||
// Set enum parsers
|
||||
if (dataType.key.toLowerCase() === 'enum') {
|
||||
this.enumOids.oids.forEach(oid => {
|
||||
this.oidParserMap.set(oid, parser);
|
||||
});
|
||||
this.enumOids.arrayOids.forEach(arrayOid => {
|
||||
this.oidParserMap.set(arrayOid, arrayParser);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Set parsers for normal data types
|
||||
dataType.types.postgres.forEach(name => {
|
||||
if (! this.nameOidMap[name]) return;
|
||||
this.oidParserMap.set(this.nameOidMap[name].oid, parser);
|
||||
|
||||
if (! this.nameOidMap[name].arrayOid) return;
|
||||
this.oidParserMap.set(this.nameOidMap[name].arrayOid, arrayParser);
|
||||
});
|
||||
}
|
||||
|
||||
_clearTypeParser() {
|
||||
this.oidParserMap = new Map();
|
||||
}
|
||||
|
||||
getTypeParser(oid, ...args) {
|
||||
if (this.oidParserMap.get(oid)) return this.oidParserMap.get(oid);
|
||||
|
||||
return this.lib.types.getTypeParser(oid, ...args);
|
||||
}
|
||||
|
||||
connect(config) {
|
||||
config.user = config.username;
|
||||
const connectionConfig = _.pick(config, [
|
||||
'user', 'password', 'host', 'database', 'port'
|
||||
]);
|
||||
|
||||
connectionConfig.types = {
|
||||
getTypeParser: ConnectionManager.prototype.getTypeParser.bind(this)
|
||||
};
|
||||
|
||||
if (config.dialectOptions) {
|
||||
_.merge(connectionConfig,
|
||||
_.pick(config.dialectOptions, [
|
||||
// see [http://www.postgresql.org/docs/9.3/static/runtime-config-logging.html#GUC-APPLICATION-NAME]
|
||||
'application_name',
|
||||
// choose the SSL mode with the PGSSLMODE environment variable
|
||||
// object format: [https://github.com/brianc/node-postgres/blob/master/lib/connection.js#L79]
|
||||
// see also [http://www.postgresql.org/docs/9.3/static/libpq-ssl.html]
|
||||
'ssl',
|
||||
// In addition to the values accepted by the corresponding server,
|
||||
// you can use "auto" to determine the right encoding from the
|
||||
// current locale in the client (LC_CTYPE environment variable on Unix systems)
|
||||
'client_encoding',
|
||||
// !! DO NOT SET THIS TO TRUE !!
|
||||
// (unless you know what you're doing)
|
||||
// see [http://www.postgresql.org/message-id/flat/bc9549a50706040852u27633f41ib1e6b09f8339d845@mail.gmail.com#bc9549a50706040852u27633f41ib1e6b09f8339d845@mail.gmail.com]
|
||||
'binary',
|
||||
// This should help with backends incorrectly considering idle clients to be dead and prematurely disconnecting them.
|
||||
// this feature has been added in pg module v6.0.0, check pg/CHANGELOG.md
|
||||
'keepAlive',
|
||||
// Times out queries after a set time in milliseconds. Added in pg v7.3
|
||||
'statement_timeout'
|
||||
]));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let responded = false;
|
||||
|
||||
const connection = new this.lib.Client(connectionConfig);
|
||||
|
||||
const parameterHandler = message => {
|
||||
switch (message.parameterName) {
|
||||
case 'server_version':
|
||||
if (this.sequelize.options.databaseVersion === 0) {
|
||||
const version = semver.coerce(message.parameterValue).version;
|
||||
this.sequelize.options.databaseVersion = semver.valid(version)
|
||||
? version
|
||||
: this.defaultVersion;
|
||||
}
|
||||
break;
|
||||
case 'standard_conforming_strings':
|
||||
connection['standard_conforming_strings'] = message.parameterValue;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const endHandler = () => {
|
||||
debug('connection timeout');
|
||||
if (!responded) {
|
||||
reject(new sequelizeErrors.ConnectionTimedOutError(new Error('Connection timed out')));
|
||||
}
|
||||
};
|
||||
|
||||
// If we didn't ever hear from the client.connect() callback the connection timeout
|
||||
// node-postgres does not treat this as an error since no active query was ever emitted
|
||||
connection.once('end', endHandler);
|
||||
|
||||
if (!this.sequelize.config.native) {
|
||||
// Receive various server parameters for further configuration
|
||||
connection.connection.on('parameterStatus', parameterHandler);
|
||||
}
|
||||
|
||||
connection.connect(err => {
|
||||
responded = true;
|
||||
|
||||
if (!this.sequelize.config.native) {
|
||||
// remove parameter handler
|
||||
connection.connection.removeListener('parameterStatus', parameterHandler);
|
||||
}
|
||||
|
||||
if (err) {
|
||||
if (err.code) {
|
||||
switch (err.code) {
|
||||
case 'ECONNREFUSED':
|
||||
reject(new sequelizeErrors.ConnectionRefusedError(err));
|
||||
break;
|
||||
case 'ENOTFOUND':
|
||||
reject(new sequelizeErrors.HostNotFoundError(err));
|
||||
break;
|
||||
case 'EHOSTUNREACH':
|
||||
reject(new sequelizeErrors.HostNotReachableError(err));
|
||||
break;
|
||||
case 'EINVAL':
|
||||
reject(new sequelizeErrors.InvalidConnectionError(err));
|
||||
break;
|
||||
default:
|
||||
reject(new sequelizeErrors.ConnectionError(err));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
reject(new sequelizeErrors.ConnectionError(err));
|
||||
}
|
||||
} else {
|
||||
debug('connection acquired');
|
||||
connection.removeListener('end', endHandler);
|
||||
resolve(connection);
|
||||
}
|
||||
});
|
||||
}).tap(connection => {
|
||||
let query = '';
|
||||
|
||||
if (this.sequelize.options.standardConformingStrings !== false && connection['standard_conforming_strings'] !== 'on') {
|
||||
// Disable escape characters in strings
|
||||
// see https://github.com/sequelize/sequelize/issues/3545 (security issue)
|
||||
// see https://www.postgresql.org/docs/current/static/runtime-config-compatible.html#GUC-STANDARD-CONFORMING-STRINGS
|
||||
query += 'SET standard_conforming_strings=on;';
|
||||
}
|
||||
|
||||
if (this.sequelize.options.clientMinMessages !== false) {
|
||||
query += `SET client_min_messages TO ${this.sequelize.options.clientMinMessages};`;
|
||||
}
|
||||
|
||||
if (!this.sequelize.config.keepDefaultTimezone) {
|
||||
const isZone = !!moment.tz.zone(this.sequelize.options.timezone);
|
||||
if (isZone) {
|
||||
query += `SET TIME ZONE '${this.sequelize.options.timezone}';`;
|
||||
} else {
|
||||
query += `SET TIME ZONE INTERVAL '${this.sequelize.options.timezone}' HOUR TO MINUTE;`;
|
||||
}
|
||||
}
|
||||
|
||||
if (query) {
|
||||
return connection.query(query);
|
||||
}
|
||||
}).tap(connection => {
|
||||
if (Object.keys(this.nameOidMap).length === 0 &&
|
||||
this.enumOids.oids.length === 0 &&
|
||||
this.enumOids.arrayOids.length === 0) {
|
||||
return this._refreshDynamicOIDs(connection);
|
||||
}
|
||||
}).tap(connection => {
|
||||
// Don't let a Postgres restart (or error) to take down the whole app
|
||||
connection.on('error', error => {
|
||||
connection._invalid = true;
|
||||
debug(`connection error ${error.code || error.message}`);
|
||||
this.pool.destroy(connection);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
disconnect(connection) {
|
||||
if (connection._ending) {
|
||||
debug('connection tried to disconnect but was already at ENDING state');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.fromCallback(callback => connection.end(callback));
|
||||
}
|
||||
|
||||
validate(connection) {
|
||||
return !connection._invalid && !connection._ending;
|
||||
}
|
||||
|
||||
_refreshDynamicOIDs(connection) {
|
||||
const databaseVersion = this.sequelize.options.databaseVersion;
|
||||
const supportedVersion = '8.3.0';
|
||||
|
||||
// Check for supported version
|
||||
if ( (databaseVersion && semver.gte(databaseVersion, supportedVersion)) === false) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Refresh dynamic OIDs for some types
|
||||
// These include Geometry / Geography / HStore / Enum / Citext / Range
|
||||
return (connection || this.sequelize).query(
|
||||
'WITH ranges AS (' +
|
||||
' SELECT pg_range.rngtypid, pg_type.typname AS rngtypname,' +
|
||||
' pg_type.typarray AS rngtyparray, pg_range.rngsubtype' +
|
||||
' FROM pg_range LEFT OUTER JOIN pg_type ON pg_type.oid = pg_range.rngtypid' +
|
||||
')' +
|
||||
'SELECT pg_type.typname, pg_type.typtype, pg_type.oid, pg_type.typarray,' +
|
||||
' ranges.rngtypname, ranges.rngtypid, ranges.rngtyparray' +
|
||||
' FROM pg_type LEFT OUTER JOIN ranges ON pg_type.oid = ranges.rngsubtype' +
|
||||
' WHERE (pg_type.typtype IN(\'b\', \'e\'));'
|
||||
).then(results => {
|
||||
let result = Array.isArray(results) ? results.pop() : results;
|
||||
|
||||
// When searchPath is prepended then two statements are executed and the result is
|
||||
// an array of those two statements. First one is the SET search_path and second is
|
||||
// the SELECT query result.
|
||||
if (Array.isArray(result)) {
|
||||
if (result[0].command === 'SET') {
|
||||
result = result.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const newNameOidMap = {};
|
||||
const newEnumOids = { oids: [], arrayOids: [] };
|
||||
|
||||
for (const row of result.rows) {
|
||||
// Mapping enums, handled separatedly
|
||||
if (row.typtype === 'e') {
|
||||
newEnumOids.oids.push(row.oid);
|
||||
if (row.typarray) newEnumOids.arrayOids.push(row.typarray);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mapping base types and their arrays
|
||||
newNameOidMap[row.typname] = { oid: row.oid };
|
||||
if (row.typarray) newNameOidMap[row.typname].arrayOid = row.typarray;
|
||||
|
||||
// Mapping ranges(of base types) and their arrays
|
||||
if (row.rngtypid) {
|
||||
newNameOidMap[row.typname].rangeOid = row.rngtypid;
|
||||
if (row.rngtyparray) newNameOidMap[row.typname].arrayRangeOid = row.rngtyparray;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace all OID mappings. Avoids temporary empty OID mappings.
|
||||
this.nameOidMap = newNameOidMap;
|
||||
this.enumOids = newEnumOids;
|
||||
|
||||
this.refreshTypeParser(dataTypes.postgres);
|
||||
});
|
||||
}
|
||||
|
||||
_clearDynamicOIDs() {
|
||||
this.nameOidMap = {};
|
||||
this.enumOids = { oids: [], arrayOids: [] };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConnectionManager;
|
||||
module.exports.ConnectionManager = ConnectionManager;
|
||||
module.exports.default = ConnectionManager;
|
||||
+526
@@ -0,0 +1,526 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const wkx = require('wkx');
|
||||
|
||||
module.exports = BaseTypes => {
|
||||
const warn = BaseTypes.ABSTRACT.warn.bind(undefined, 'http://www.postgresql.org/docs/9.4/static/datatype.html');
|
||||
|
||||
/**
|
||||
* Removes unsupported Postgres options, i.e., LENGTH, UNSIGNED and ZEROFILL, for the integer data types.
|
||||
*
|
||||
* @param {Object} dataType The base integer data type.
|
||||
* @private
|
||||
*/
|
||||
function removeUnsupportedIntegerOptions(dataType) {
|
||||
if (dataType._length || dataType.options.length || dataType._unsigned || dataType._zerofill) {
|
||||
warn(`PostgresSQL does not support '${dataType.key}' with LENGTH, UNSIGNED or ZEROFILL. Plain '${dataType.key}' will be used instead.`);
|
||||
dataType._length = undefined;
|
||||
dataType.options.length = undefined;
|
||||
dataType._unsigned = undefined;
|
||||
dataType._zerofill = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* types:
|
||||
* {
|
||||
* oids: [oid],
|
||||
* array_oids: [oid]
|
||||
* }
|
||||
* @see oid here https://github.com/lib/pq/blob/master/oid/types.go
|
||||
*/
|
||||
|
||||
BaseTypes.UUID.types.postgres = ['uuid'];
|
||||
BaseTypes.CIDR.types.postgres = ['cidr'];
|
||||
BaseTypes.INET.types.postgres = ['inet'];
|
||||
BaseTypes.MACADDR.types.postgres = ['macaddr'];
|
||||
BaseTypes.JSON.types.postgres = ['json'];
|
||||
BaseTypes.JSONB.types.postgres = ['jsonb'];
|
||||
BaseTypes.TIME.types.postgres = ['time'];
|
||||
|
||||
class DATEONLY extends BaseTypes.DATEONLY {
|
||||
_stringify(value, options) {
|
||||
if (value === Infinity) {
|
||||
return 'Infinity';
|
||||
}
|
||||
if (value === -Infinity) {
|
||||
return '-Infinity';
|
||||
}
|
||||
return super._stringify(value, options);
|
||||
}
|
||||
_sanitize(value, options) {
|
||||
if ((!options || options && !options.raw) && value !== Infinity && value !== -Infinity) {
|
||||
if (typeof value === 'string') {
|
||||
const lower = value.toLowerCase();
|
||||
if (lower === 'infinity') {
|
||||
return Infinity;
|
||||
}
|
||||
if (lower === '-infinity') {
|
||||
return -Infinity;
|
||||
}
|
||||
}
|
||||
return super._sanitize(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
static parse(value) {
|
||||
if (value === 'infinity') {
|
||||
return Infinity;
|
||||
}
|
||||
if (value === '-infinity') {
|
||||
return -Infinity;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
BaseTypes.DATEONLY.types.postgres = ['date'];
|
||||
|
||||
class DECIMAL extends BaseTypes.DECIMAL {
|
||||
static parse(value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// numeric
|
||||
BaseTypes.DECIMAL.types.postgres = ['numeric'];
|
||||
|
||||
class STRING extends BaseTypes.STRING {
|
||||
toSql() {
|
||||
if (this._binary) {
|
||||
return 'BYTEA';
|
||||
}
|
||||
return super.toSql();
|
||||
}
|
||||
}
|
||||
|
||||
BaseTypes.STRING.types.postgres = ['varchar'];
|
||||
|
||||
class TEXT extends BaseTypes.TEXT {
|
||||
toSql() {
|
||||
if (this._length) {
|
||||
warn('PostgreSQL does not support TEXT with options. Plain `TEXT` will be used instead.');
|
||||
this._length = undefined;
|
||||
}
|
||||
return 'TEXT';
|
||||
}
|
||||
}
|
||||
|
||||
BaseTypes.TEXT.types.postgres = ['text'];
|
||||
|
||||
class CITEXT extends BaseTypes.CITEXT {
|
||||
static parse(value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
BaseTypes.CITEXT.types.postgres = ['citext'];
|
||||
|
||||
class CHAR extends BaseTypes.CHAR {
|
||||
toSql() {
|
||||
if (this._binary) {
|
||||
return 'BYTEA';
|
||||
}
|
||||
return super.toSql();
|
||||
}
|
||||
}
|
||||
|
||||
BaseTypes.CHAR.types.postgres = ['char', 'bpchar'];
|
||||
|
||||
class BOOLEAN extends BaseTypes.BOOLEAN {
|
||||
toSql() {
|
||||
return 'BOOLEAN';
|
||||
}
|
||||
_sanitize(value) {
|
||||
if (value !== null && value !== undefined) {
|
||||
if (Buffer.isBuffer(value) && value.length === 1) {
|
||||
// Bit fields are returned as buffers
|
||||
value = value[0];
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
// Only take action on valid boolean strings.
|
||||
return value === 'true' || value === 't' ? true : value === 'false' || value === 'f' ? false : value;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
// Only take action on valid boolean integers.
|
||||
return value === 1 ? true : value === 0 ? false : value;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
BOOLEAN.parse = BOOLEAN.prototype._sanitize;
|
||||
|
||||
BaseTypes.BOOLEAN.types.postgres = ['bool'];
|
||||
|
||||
class DATE extends BaseTypes.DATE {
|
||||
toSql() {
|
||||
return 'TIMESTAMP WITH TIME ZONE';
|
||||
}
|
||||
validate(value) {
|
||||
if (value !== Infinity && value !== -Infinity) {
|
||||
return super.validate(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
_stringify(value, options) {
|
||||
if (value === Infinity) {
|
||||
return 'Infinity';
|
||||
}
|
||||
if (value === -Infinity) {
|
||||
return '-Infinity';
|
||||
}
|
||||
return super._stringify(value, options);
|
||||
}
|
||||
_sanitize(value, options) {
|
||||
if ((!options || options && !options.raw) && !(value instanceof Date) && !!value && value !== Infinity && value !== -Infinity) {
|
||||
if (typeof value === 'string') {
|
||||
const lower = value.toLowerCase();
|
||||
if (lower === 'infinity') {
|
||||
return Infinity;
|
||||
}
|
||||
if (lower === '-infinity') {
|
||||
return -Infinity;
|
||||
}
|
||||
}
|
||||
return new Date(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
BaseTypes.DATE.types.postgres = ['timestamptz'];
|
||||
|
||||
class TINYINT extends BaseTypes.TINYINT {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
// int2
|
||||
BaseTypes.TINYINT.types.postgres = ['int2'];
|
||||
|
||||
class SMALLINT extends BaseTypes.SMALLINT {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
// int2
|
||||
BaseTypes.SMALLINT.types.postgres = ['int2'];
|
||||
|
||||
class INTEGER extends BaseTypes.INTEGER {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
INTEGER.parse = function parse(value) {
|
||||
return parseInt(value, 10);
|
||||
};
|
||||
|
||||
// int4
|
||||
BaseTypes.INTEGER.types.postgres = ['int4'];
|
||||
|
||||
class BIGINT extends BaseTypes.BIGINT {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
// int8
|
||||
BaseTypes.BIGINT.types.postgres = ['int8'];
|
||||
|
||||
class REAL extends BaseTypes.REAL {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
// float4
|
||||
BaseTypes.REAL.types.postgres = ['float4'];
|
||||
|
||||
class DOUBLE extends BaseTypes.DOUBLE {
|
||||
constructor(length) {
|
||||
super(length);
|
||||
removeUnsupportedIntegerOptions(this);
|
||||
}
|
||||
}
|
||||
// float8
|
||||
BaseTypes.DOUBLE.types.postgres = ['float8'];
|
||||
|
||||
class FLOAT extends BaseTypes.FLOAT {
|
||||
constructor(length, decimals) {
|
||||
super(length, decimals);
|
||||
// POSTGRES does only support lengths as parameter.
|
||||
// Values between 1-24 result in REAL
|
||||
// Values between 25-53 result in DOUBLE PRECISION
|
||||
// If decimals are provided remove these and print a warning
|
||||
if (this._decimals) {
|
||||
warn('PostgreSQL does not support FLOAT with decimals. Plain `FLOAT` will be used instead.');
|
||||
this._length = undefined;
|
||||
this.options.length = undefined;
|
||||
this._decimals = undefined;
|
||||
}
|
||||
if (this._unsigned) {
|
||||
warn('PostgreSQL does not support FLOAT unsigned. `UNSIGNED` was removed.');
|
||||
this._unsigned = undefined;
|
||||
}
|
||||
if (this._zerofill) {
|
||||
warn('PostgreSQL does not support FLOAT zerofill. `ZEROFILL` was removed.');
|
||||
this._zerofill = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
delete FLOAT.parse; // Float has no separate type in PG
|
||||
|
||||
class BLOB extends BaseTypes.BLOB {
|
||||
toSql() {
|
||||
if (this._length) {
|
||||
warn('PostgreSQL does not support BLOB (BYTEA) with options. Plain `BYTEA` will be used instead.');
|
||||
this._length = undefined;
|
||||
}
|
||||
return 'BYTEA';
|
||||
}
|
||||
_hexify(hex) {
|
||||
// bytea hex format http://www.postgresql.org/docs/current/static/datatype-binary.html
|
||||
return `E'\\\\x${hex}'`;
|
||||
}
|
||||
}
|
||||
|
||||
BaseTypes.BLOB.types.postgres = ['bytea'];
|
||||
|
||||
class GEOMETRY extends BaseTypes.GEOMETRY {
|
||||
toSql() {
|
||||
let result = this.key;
|
||||
if (this.type) {
|
||||
result += `(${this.type}`;
|
||||
if (this.srid) {
|
||||
result += `,${this.srid}`;
|
||||
}
|
||||
result += ')';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static parse(value) {
|
||||
const b = Buffer.from(value, 'hex');
|
||||
return wkx.Geometry.parse(b).toGeoJSON();
|
||||
}
|
||||
_stringify(value, options) {
|
||||
return `ST_GeomFromGeoJSON(${options.escape(JSON.stringify(value))})`;
|
||||
}
|
||||
_bindParam(value, options) {
|
||||
return `ST_GeomFromGeoJSON(${options.bindParam(value)})`;
|
||||
}
|
||||
}
|
||||
|
||||
BaseTypes.GEOMETRY.types.postgres = ['geometry'];
|
||||
|
||||
|
||||
class GEOGRAPHY extends BaseTypes.GEOGRAPHY {
|
||||
toSql() {
|
||||
let result = 'GEOGRAPHY';
|
||||
if (this.type) {
|
||||
result += `(${this.type}`;
|
||||
if (this.srid) {
|
||||
result += `,${this.srid}`;
|
||||
}
|
||||
result += ')';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static parse(value) {
|
||||
const b = Buffer.from(value, 'hex');
|
||||
return wkx.Geometry.parse(b).toGeoJSON();
|
||||
}
|
||||
_stringify(value, options) {
|
||||
return `ST_GeomFromGeoJSON(${options.escape(JSON.stringify(value))})`;
|
||||
}
|
||||
bindParam(value, options) {
|
||||
return `ST_GeomFromGeoJSON(${options.bindParam(value)})`;
|
||||
}
|
||||
}
|
||||
|
||||
BaseTypes.GEOGRAPHY.types.postgres = ['geography'];
|
||||
|
||||
let hstore;
|
||||
|
||||
class HSTORE extends BaseTypes.HSTORE {
|
||||
constructor() {
|
||||
super();
|
||||
if (!hstore) {
|
||||
// All datatype files are loaded at import - make sure we don't load the hstore parser before a hstore is instantiated
|
||||
hstore = require('./hstore');
|
||||
}
|
||||
}
|
||||
_value(value) {
|
||||
if (!hstore) {
|
||||
// All datatype files are loaded at import - make sure we don't load the hstore parser before a hstore is instantiated
|
||||
hstore = require('./hstore');
|
||||
}
|
||||
return hstore.stringify(value);
|
||||
}
|
||||
_stringify(value) {
|
||||
return `'${this._value(value)}'`;
|
||||
}
|
||||
_bindParam(value, options) {
|
||||
return options.bindParam(this._value(value));
|
||||
}
|
||||
static parse(value) {
|
||||
if (!hstore) {
|
||||
// All datatype files are loaded at import - make sure we don't load the hstore parser before a hstore is instantiated
|
||||
hstore = require('./hstore');
|
||||
}
|
||||
return hstore.parse(value);
|
||||
}
|
||||
}
|
||||
|
||||
HSTORE.prototype.escape = false;
|
||||
|
||||
BaseTypes.HSTORE.types.postgres = ['hstore'];
|
||||
|
||||
class RANGE extends BaseTypes.RANGE {
|
||||
_value(values, options) {
|
||||
if (!Array.isArray(values)) {
|
||||
return this.options.subtype.stringify(values, options);
|
||||
}
|
||||
const valueInclusivity = [true, false];
|
||||
const valuesStringified = values.map((value, index) => {
|
||||
if (_.isObject(value) && Object.prototype.hasOwnProperty.call(value, 'value')) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, 'inclusive')) {
|
||||
valueInclusivity[index] = value.inclusive;
|
||||
}
|
||||
value = value.value;
|
||||
}
|
||||
if (value === null || value === -Infinity || value === Infinity) {
|
||||
// Pass through "unbounded" bounds unchanged
|
||||
return value;
|
||||
}
|
||||
if (this.options.subtype.stringify) {
|
||||
return this.options.subtype.stringify(value, options);
|
||||
}
|
||||
return options.escape(value);
|
||||
});
|
||||
// Array.map does not preserve extra array properties
|
||||
valuesStringified.inclusive = valueInclusivity;
|
||||
return range.stringify(valuesStringified);
|
||||
}
|
||||
_stringify(values, options) {
|
||||
const value = this._value(values, options);
|
||||
if (!Array.isArray(values)) {
|
||||
return `'${value}'::${this.toCastType()}`;
|
||||
}
|
||||
return `'${value}'`;
|
||||
}
|
||||
_bindParam(values, options) {
|
||||
const value = this._value(values, options);
|
||||
if (!Array.isArray(values)) {
|
||||
return `${options.bindParam(value)}::${this.toCastType()}`;
|
||||
}
|
||||
return options.bindParam(value);
|
||||
}
|
||||
toSql() {
|
||||
return BaseTypes.RANGE.types.postgres.subtypes[this._subtype.toLowerCase()];
|
||||
}
|
||||
toCastType() {
|
||||
return BaseTypes.RANGE.types.postgres.castTypes[this._subtype.toLowerCase()];
|
||||
}
|
||||
static parse(value, options = { parser: val => val }) {
|
||||
return range.parse(value, options.parser);
|
||||
}
|
||||
}
|
||||
const range = require('./range');
|
||||
|
||||
RANGE.prototype.escape = false;
|
||||
|
||||
BaseTypes.RANGE.types.postgres = {
|
||||
subtypes: {
|
||||
integer: 'int4range',
|
||||
decimal: 'numrange',
|
||||
date: 'tstzrange',
|
||||
dateonly: 'daterange',
|
||||
bigint: 'int8range'
|
||||
},
|
||||
castTypes: {
|
||||
integer: 'int4',
|
||||
decimal: 'numeric',
|
||||
date: 'timestamptz',
|
||||
dateonly: 'date',
|
||||
bigint: 'int8'
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Why are base types being manipulated??
|
||||
BaseTypes.ARRAY.prototype.escape = false;
|
||||
BaseTypes.ARRAY.prototype._value = function _value(values, options) {
|
||||
return values.map(value => {
|
||||
if (options && options.bindParam && this.type && this.type._value) {
|
||||
return this.type._value(value, options);
|
||||
}
|
||||
if (this.type && this.type.stringify) {
|
||||
value = this.type.stringify(value, options);
|
||||
|
||||
if (this.type.escape === false) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return options.escape(value);
|
||||
}, this);
|
||||
};
|
||||
BaseTypes.ARRAY.prototype._stringify = function _stringify(values, options) {
|
||||
let str = `ARRAY[${this._value(values, options).join(',')}]`;
|
||||
|
||||
if (this.type) {
|
||||
const Utils = require('../../utils');
|
||||
let castKey = this.toSql();
|
||||
|
||||
if (this.type instanceof BaseTypes.ENUM) {
|
||||
castKey = `${Utils.addTicks(
|
||||
Utils.generateEnumName(options.field.Model.getTableName(), options.field.fieldName),
|
||||
'"'
|
||||
) }[]`;
|
||||
}
|
||||
|
||||
str += `::${castKey}`;
|
||||
}
|
||||
|
||||
return str;
|
||||
};
|
||||
BaseTypes.ARRAY.prototype._bindParam = function _bindParam(values, options) {
|
||||
return options.bindParam(this._value(values, options));
|
||||
};
|
||||
|
||||
class ENUM extends BaseTypes.ENUM {
|
||||
static parse(value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
BaseTypes.ENUM.types.postgres = [null];
|
||||
|
||||
return {
|
||||
DECIMAL,
|
||||
BLOB,
|
||||
STRING,
|
||||
CHAR,
|
||||
TEXT,
|
||||
CITEXT,
|
||||
TINYINT,
|
||||
SMALLINT,
|
||||
INTEGER,
|
||||
BIGINT,
|
||||
BOOLEAN,
|
||||
DATE,
|
||||
DATEONLY,
|
||||
REAL,
|
||||
'DOUBLE PRECISION': DOUBLE,
|
||||
FLOAT,
|
||||
GEOMETRY,
|
||||
GEOGRAPHY,
|
||||
HSTORE,
|
||||
RANGE,
|
||||
ENUM
|
||||
};
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
const hstore = require('pg-hstore')({ sanitize: true });
|
||||
|
||||
function stringify(data) {
|
||||
if (data === null) return null;
|
||||
return hstore.stringify(data);
|
||||
}
|
||||
exports.stringify = stringify;
|
||||
|
||||
function parse(value) {
|
||||
if (value === null) return null;
|
||||
return hstore.parse(value);
|
||||
}
|
||||
exports.parse = parse;
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
'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').postgres;
|
||||
|
||||
class PostgresDialect extends AbstractDialect {
|
||||
constructor(sequelize) {
|
||||
super();
|
||||
this.sequelize = sequelize;
|
||||
this.connectionManager = new ConnectionManager(this, sequelize);
|
||||
this.QueryGenerator = new QueryGenerator({
|
||||
_dialect: this,
|
||||
sequelize
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
PostgresDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype.supports), {
|
||||
'DEFAULT VALUES': true,
|
||||
'EXCEPTION': true,
|
||||
'ON DUPLICATE KEY': false,
|
||||
'ORDER NULLS': true,
|
||||
returnValues: {
|
||||
returning: true
|
||||
},
|
||||
bulkDefault: true,
|
||||
schemas: true,
|
||||
lock: true,
|
||||
lockOf: true,
|
||||
lockKey: true,
|
||||
lockOuterJoinFailure: true,
|
||||
skipLocked: true,
|
||||
forShare: 'FOR SHARE',
|
||||
index: {
|
||||
concurrently: true,
|
||||
using: 2,
|
||||
where: true,
|
||||
functionBased: true
|
||||
},
|
||||
inserts: {
|
||||
onConflictDoNothing: ' ON CONFLICT DO NOTHING',
|
||||
updateOnDuplicate: ' ON CONFLICT DO UPDATE SET'
|
||||
},
|
||||
NUMERIC: true,
|
||||
ARRAY: true,
|
||||
RANGE: true,
|
||||
GEOMETRY: true,
|
||||
REGEXP: true,
|
||||
GEOGRAPHY: true,
|
||||
JSON: true,
|
||||
JSONB: true,
|
||||
HSTORE: true,
|
||||
deferrableConstraints: true,
|
||||
searchPath: true
|
||||
});
|
||||
|
||||
ConnectionManager.prototype.defaultVersion = '9.4.0';
|
||||
PostgresDialect.prototype.Query = Query;
|
||||
PostgresDialect.prototype.DataTypes = DataTypes;
|
||||
PostgresDialect.prototype.name = 'postgres';
|
||||
PostgresDialect.prototype.TICK_CHAR = '"';
|
||||
PostgresDialect.prototype.TICK_CHAR_LEFT = PostgresDialect.prototype.TICK_CHAR;
|
||||
PostgresDialect.prototype.TICK_CHAR_RIGHT = PostgresDialect.prototype.TICK_CHAR;
|
||||
|
||||
module.exports = PostgresDialect;
|
||||
module.exports.default = PostgresDialect;
|
||||
module.exports.PostgresDialect = PostgresDialect;
|
||||
+945
@@ -0,0 +1,945 @@
|
||||
'use strict';
|
||||
|
||||
const Utils = require('../../utils');
|
||||
const util = require('util');
|
||||
const DataTypes = require('../../data-types');
|
||||
const AbstractQueryGenerator = require('../abstract/query-generator');
|
||||
const semver = require('semver');
|
||||
const _ = require('lodash');
|
||||
|
||||
class PostgresQueryGenerator extends AbstractQueryGenerator {
|
||||
setSearchPath(searchPath) {
|
||||
return `SET search_path to ${searchPath};`;
|
||||
}
|
||||
|
||||
createDatabaseQuery(databaseName, options) {
|
||||
options = Object.assign({
|
||||
encoding: null,
|
||||
collate: null
|
||||
}, options || {});
|
||||
|
||||
const values = {
|
||||
database: this.quoteTable(databaseName),
|
||||
encoding: options.encoding ? ` ENCODING = ${this.escape(options.encoding)}` : '',
|
||||
collation: options.collate ? ` LC_COLLATE = ${this.escape(options.collate)}` : '',
|
||||
ctype: options.ctype ? ` LC_CTYPE = ${this.escape(options.ctype)}` : '',
|
||||
template: options.template ? ` TEMPLATE = ${this.escape(options.template)}` : ''
|
||||
};
|
||||
|
||||
return `CREATE DATABASE ${values.database}${values.encoding}${values.collation}${values.ctype}${values.template};`;
|
||||
}
|
||||
|
||||
dropDatabaseQuery(databaseName) {
|
||||
return `DROP DATABASE IF EXISTS ${this.quoteTable(databaseName)};`;
|
||||
}
|
||||
|
||||
createSchema(schema) {
|
||||
const databaseVersion = _.get(this, 'sequelize.options.databaseVersion', 0);
|
||||
|
||||
if (databaseVersion && semver.gte(databaseVersion, '9.2.0')) {
|
||||
return `CREATE SCHEMA IF NOT EXISTS ${schema};`;
|
||||
}
|
||||
|
||||
return `CREATE SCHEMA ${schema};`;
|
||||
}
|
||||
|
||||
dropSchema(schema) {
|
||||
return `DROP SCHEMA IF EXISTS ${schema} CASCADE;`;
|
||||
}
|
||||
|
||||
showSchemasQuery() {
|
||||
return "SELECT schema_name FROM information_schema.schemata WHERE schema_name <> 'information_schema' AND schema_name != 'public' AND schema_name !~ E'^pg_';";
|
||||
}
|
||||
|
||||
versionQuery() {
|
||||
return 'SHOW SERVER_VERSION';
|
||||
}
|
||||
|
||||
createTableQuery(tableName, attributes, options) {
|
||||
options = Object.assign({}, options || {});
|
||||
|
||||
//Postgres 9.0 does not support CREATE TABLE IF NOT EXISTS, 9.1 and above do
|
||||
const databaseVersion = _.get(this, 'sequelize.options.databaseVersion', 0);
|
||||
const attrStr = [];
|
||||
let comments = '';
|
||||
let columnComments = '';
|
||||
|
||||
const quotedTable = this.quoteTable(tableName);
|
||||
|
||||
if (options.comment && typeof options.comment === 'string') {
|
||||
comments += `; COMMENT ON TABLE ${quotedTable} IS ${this.escape(options.comment)}`;
|
||||
}
|
||||
|
||||
for (const attr in attributes) {
|
||||
const quotedAttr = this.quoteIdentifier(attr);
|
||||
const i = attributes[attr].indexOf('COMMENT ');
|
||||
if (i !== -1) {
|
||||
// Move comment to a separate query
|
||||
const escapedCommentText = this.escape(attributes[attr].substring(i + 8));
|
||||
columnComments += `; COMMENT ON COLUMN ${quotedTable}.${quotedAttr} IS ${escapedCommentText}`;
|
||||
attributes[attr] = attributes[attr].substring(0, i);
|
||||
}
|
||||
|
||||
const dataType = this.dataTypeMapping(tableName, attr, attributes[attr]);
|
||||
attrStr.push(`${quotedAttr} ${dataType}`);
|
||||
}
|
||||
|
||||
|
||||
let attributesClause = attrStr.join(', ');
|
||||
|
||||
if (options.uniqueKeys) {
|
||||
_.each(options.uniqueKeys, columns => {
|
||||
if (columns.customIndex) {
|
||||
attributesClause += `, UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const pks = _.reduce(attributes, (acc, attribute, key) => {
|
||||
if (attribute.includes('PRIMARY KEY')) {
|
||||
acc.push(this.quoteIdentifier(key));
|
||||
}
|
||||
return acc;
|
||||
}, []).join(',');
|
||||
|
||||
if (pks.length > 0) {
|
||||
attributesClause += `, PRIMARY KEY (${pks})`;
|
||||
}
|
||||
|
||||
return `CREATE TABLE ${databaseVersion === 0 || semver.gte(databaseVersion, '9.1.0') ? 'IF NOT EXISTS ' : ''}${quotedTable} (${attributesClause})${comments}${columnComments};`;
|
||||
}
|
||||
|
||||
dropTableQuery(tableName, options) {
|
||||
options = options || {};
|
||||
return `DROP TABLE IF EXISTS ${this.quoteTable(tableName)}${options.cascade ? ' CASCADE' : ''};`;
|
||||
}
|
||||
|
||||
showTablesQuery() {
|
||||
return "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type LIKE '%TABLE' AND table_name != 'spatial_ref_sys';";
|
||||
}
|
||||
|
||||
describeTableQuery(tableName, schema) {
|
||||
if (!schema) schema = 'public';
|
||||
|
||||
return 'SELECT ' +
|
||||
'pk.constraint_type as "Constraint",' +
|
||||
'c.column_name as "Field", ' +
|
||||
'c.column_default as "Default",' +
|
||||
'c.is_nullable as "Null", ' +
|
||||
'(CASE WHEN c.udt_name = \'hstore\' THEN c.udt_name ELSE c.data_type END) || (CASE WHEN c.character_maximum_length IS NOT NULL THEN \'(\' || c.character_maximum_length || \')\' ELSE \'\' END) as "Type", ' +
|
||||
'(SELECT array_agg(e.enumlabel) FROM pg_catalog.pg_type t JOIN pg_catalog.pg_enum e ON t.oid=e.enumtypid WHERE t.typname=c.udt_name) AS "special", ' +
|
||||
'(SELECT pgd.description FROM pg_catalog.pg_statio_all_tables AS st INNER JOIN pg_catalog.pg_description pgd on (pgd.objoid=st.relid) WHERE c.ordinal_position=pgd.objsubid AND c.table_name=st.relname) AS "Comment" ' +
|
||||
'FROM information_schema.columns c ' +
|
||||
'LEFT JOIN (SELECT tc.table_schema, tc.table_name, ' +
|
||||
'cu.column_name, tc.constraint_type ' +
|
||||
'FROM information_schema.TABLE_CONSTRAINTS tc ' +
|
||||
'JOIN information_schema.KEY_COLUMN_USAGE cu ' +
|
||||
'ON tc.table_schema=cu.table_schema and tc.table_name=cu.table_name ' +
|
||||
'and tc.constraint_name=cu.constraint_name ' +
|
||||
'and tc.constraint_type=\'PRIMARY KEY\') pk ' +
|
||||
'ON pk.table_schema=c.table_schema ' +
|
||||
'AND pk.table_name=c.table_name ' +
|
||||
'AND pk.column_name=c.column_name ' +
|
||||
`WHERE c.table_name = ${this.escape(tableName)} AND c.table_schema = ${this.escape(schema)} `;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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://www.postgresql.org/docs/current/static/functions-json.html
|
||||
const jsonFunctionRegex = /^\s*((?:[a-z]+_){0,2}jsonb?(?:_[a-z]+){0,2})\([^)]*\)/i;
|
||||
const jsonOperatorRegex = /^\s*(->>?|#>>?|@>|<@|\?[|&]?|\|{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 operatorMatches = jsonOperatorRegex.exec(string);
|
||||
if (operatorMatches) {
|
||||
currentIndex += operatorMatches[0].length;
|
||||
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;
|
||||
}
|
||||
|
||||
handleSequelizeMethod(smth, tableName, factory, options, prepend) {
|
||||
if (smth instanceof Utils.Json) {
|
||||
// Parse nested object
|
||||
if (smth.conditions) {
|
||||
const conditions = this.parseConditionObject(smth.conditions).map(condition =>
|
||||
`${this.jsonPathExtractionQuery(condition.path[0], _.tail(condition.path))} = '${condition.value}'`
|
||||
);
|
||||
|
||||
return conditions.join(' AND ');
|
||||
}
|
||||
if (smth.path) {
|
||||
let str;
|
||||
|
||||
// Allow specifying conditions using the postgres json syntax
|
||||
if (this._checkValidJsonStatement(smth.path)) {
|
||||
str = smth.path;
|
||||
} else {
|
||||
// Also support json property accessors
|
||||
const paths = _.toPath(smth.path);
|
||||
const column = paths.shift();
|
||||
str = this.jsonPathExtractionQuery(column, paths);
|
||||
}
|
||||
|
||||
if (smth.value) {
|
||||
str += util.format(' = %s', this.escape(smth.value));
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
return super.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend);
|
||||
}
|
||||
|
||||
addColumnQuery(table, key, dataType) {
|
||||
|
||||
const dbDataType = this.attributeToSQL(dataType, { context: 'addColumn', table, key });
|
||||
const definition = this.dataTypeMapping(table, key, dbDataType);
|
||||
const quotedKey = this.quoteIdentifier(key);
|
||||
const quotedTable = this.quoteTable(this.extractTableDetails(table));
|
||||
|
||||
let query = `ALTER TABLE ${quotedTable} ADD COLUMN ${quotedKey} ${definition};`;
|
||||
|
||||
if (dataType.type && dataType.type instanceof DataTypes.ENUM || dataType instanceof DataTypes.ENUM) {
|
||||
query = this.pgEnum(table, key, dataType) + query;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
removeColumnQuery(tableName, attributeName) {
|
||||
const quotedTableName = this.quoteTable(this.extractTableDetails(tableName));
|
||||
const quotedAttributeName = this.quoteIdentifier(attributeName);
|
||||
return `ALTER TABLE ${quotedTableName} DROP COLUMN ${quotedAttributeName};`;
|
||||
}
|
||||
|
||||
changeColumnQuery(tableName, attributes) {
|
||||
const query = subQuery => `ALTER TABLE ${this.quoteTable(tableName)} ALTER COLUMN ${subQuery};`;
|
||||
const sql = [];
|
||||
for (const attributeName in attributes) {
|
||||
let definition = this.dataTypeMapping(tableName, attributeName, attributes[attributeName]);
|
||||
let attrSql = '';
|
||||
|
||||
if (definition.includes('NOT NULL')) {
|
||||
attrSql += query(`${this.quoteIdentifier(attributeName)} SET NOT NULL`);
|
||||
|
||||
definition = definition.replace('NOT NULL', '').trim();
|
||||
} else if (!definition.includes('REFERENCES')) {
|
||||
attrSql += query(`${this.quoteIdentifier(attributeName)} DROP NOT NULL`);
|
||||
}
|
||||
|
||||
if (definition.includes('DEFAULT')) {
|
||||
attrSql += query(`${this.quoteIdentifier(attributeName)} SET DEFAULT ${definition.match(/DEFAULT ([^;]+)/)[1]}`);
|
||||
|
||||
definition = definition.replace(/(DEFAULT[^;]+)/, '').trim();
|
||||
} else if (!definition.includes('REFERENCES')) {
|
||||
attrSql += query(`${this.quoteIdentifier(attributeName)} DROP DEFAULT`);
|
||||
}
|
||||
|
||||
if (attributes[attributeName].startsWith('ENUM(')) {
|
||||
attrSql += this.pgEnum(tableName, attributeName, attributes[attributeName]);
|
||||
definition = definition.replace(/^ENUM\(.+\)/, this.pgEnumName(tableName, attributeName, { schema: false }));
|
||||
definition += ` USING (${this.quoteIdentifier(attributeName)}::${this.pgEnumName(tableName, attributeName)})`;
|
||||
}
|
||||
|
||||
if (definition.match(/UNIQUE;*$/)) {
|
||||
definition = definition.replace(/UNIQUE;*$/, '');
|
||||
attrSql += query(`ADD UNIQUE (${this.quoteIdentifier(attributeName)})`).replace('ALTER COLUMN', '');
|
||||
}
|
||||
|
||||
if (definition.includes('REFERENCES')) {
|
||||
definition = definition.replace(/.+?(?=REFERENCES)/, '');
|
||||
attrSql += query(`ADD FOREIGN KEY (${this.quoteIdentifier(attributeName)}) ${definition}`).replace('ALTER COLUMN', '');
|
||||
} else {
|
||||
attrSql += query(`${this.quoteIdentifier(attributeName)} TYPE ${definition}`);
|
||||
}
|
||||
|
||||
sql.push(attrSql);
|
||||
}
|
||||
|
||||
return sql.join('');
|
||||
}
|
||||
|
||||
renameColumnQuery(tableName, attrBefore, attributes) {
|
||||
|
||||
const attrString = [];
|
||||
|
||||
for (const attributeName in attributes) {
|
||||
attrString.push(`${this.quoteIdentifier(attrBefore)} TO ${this.quoteIdentifier(attributeName)}`);
|
||||
}
|
||||
|
||||
return `ALTER TABLE ${this.quoteTable(tableName)} RENAME COLUMN ${attrString.join(', ')};`;
|
||||
}
|
||||
|
||||
fn(fnName, tableName, parameters, body, returns, language) {
|
||||
fnName = fnName || 'testfunc';
|
||||
language = language || 'plpgsql';
|
||||
returns = returns ? `RETURNS ${returns}` : '';
|
||||
parameters = parameters || '';
|
||||
|
||||
return `CREATE OR REPLACE FUNCTION pg_temp.${fnName}(${parameters}) ${returns} AS $func$ BEGIN ${body} END; $func$ LANGUAGE ${language}; SELECT * FROM pg_temp.${fnName}();`;
|
||||
}
|
||||
|
||||
exceptionFn(fnName, tableName, parameters, main, then, when, returns, language) {
|
||||
when = when || 'unique_violation';
|
||||
|
||||
const body = `${main} EXCEPTION WHEN ${when} THEN ${then};`;
|
||||
|
||||
return this.fn(fnName, tableName, parameters, body, returns, language);
|
||||
}
|
||||
|
||||
upsertQuery(tableName, insertValues, updateValues, where, model, options) {
|
||||
const primaryField = this.quoteIdentifier(model.primaryKeyField);
|
||||
|
||||
const upsertOptions = _.defaults({ bindParam: false }, options);
|
||||
const insert = this.insertQuery(tableName, insertValues, model.rawAttributes, upsertOptions);
|
||||
const update = this.updateQuery(tableName, updateValues, where, upsertOptions, model.rawAttributes);
|
||||
|
||||
insert.query = insert.query.replace('RETURNING *', `RETURNING ${primaryField} INTO primary_key`);
|
||||
update.query = update.query.replace('RETURNING *', `RETURNING ${primaryField} INTO primary_key`);
|
||||
|
||||
return this.exceptionFn(
|
||||
'sequelize_upsert',
|
||||
tableName,
|
||||
'OUT created boolean, OUT primary_key text',
|
||||
`${insert.query} created := true;`,
|
||||
`${update.query}; created := false`
|
||||
);
|
||||
}
|
||||
|
||||
truncateTableQuery(tableName, options = {}) {
|
||||
return [
|
||||
`TRUNCATE ${this.quoteTable(tableName)}`,
|
||||
options.restartIdentity ? ' RESTART IDENTITY' : '',
|
||||
options.cascade ? ' CASCADE' : ''
|
||||
].join('');
|
||||
}
|
||||
|
||||
deleteQuery(tableName, where, options = {}, model) {
|
||||
const table = this.quoteTable(tableName);
|
||||
let whereClause = this.getWhereConditions(where, null, model, options);
|
||||
const limit = options.limit ? ` LIMIT ${this.escape(options.limit)}` : '';
|
||||
let primaryKeys = '';
|
||||
let primaryKeysSelection = '';
|
||||
|
||||
if (whereClause) {
|
||||
whereClause = ` WHERE ${whereClause}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
if (!model) {
|
||||
throw new Error('Cannot LIMIT delete without a model.');
|
||||
}
|
||||
|
||||
const pks = _.values(model.primaryKeys).map(pk => this.quoteIdentifier(pk.field)).join(',');
|
||||
|
||||
primaryKeys = model.primaryKeyAttributes.length > 1 ? `(${pks})` : pks;
|
||||
primaryKeysSelection = pks;
|
||||
|
||||
return `DELETE FROM ${table} WHERE ${primaryKeys} IN (SELECT ${primaryKeysSelection} FROM ${table}${whereClause}${limit})`;
|
||||
}
|
||||
return `DELETE FROM ${table}${whereClause}`;
|
||||
}
|
||||
|
||||
showIndexesQuery(tableName) {
|
||||
let schemaJoin = '';
|
||||
let schemaWhere = '';
|
||||
if (typeof tableName !== 'string') {
|
||||
schemaJoin = ', pg_namespace s';
|
||||
schemaWhere = ` AND s.oid = t.relnamespace AND s.nspname = '${tableName.schema}'`;
|
||||
tableName = tableName.tableName;
|
||||
}
|
||||
|
||||
// This is ARCANE!
|
||||
return 'SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, ' +
|
||||
'array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) ' +
|
||||
`AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a${schemaJoin} ` +
|
||||
'WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND ' +
|
||||
`t.relkind = 'r' and t.relname = '${tableName}'${schemaWhere} ` +
|
||||
'GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;';
|
||||
}
|
||||
|
||||
showConstraintsQuery(tableName) {
|
||||
//Postgres converts camelCased alias to lowercase unless quoted
|
||||
return [
|
||||
'SELECT constraint_catalog AS "constraintCatalog",',
|
||||
'constraint_schema AS "constraintSchema",',
|
||||
'constraint_name AS "constraintName",',
|
||||
'table_catalog AS "tableCatalog",',
|
||||
'table_schema AS "tableSchema",',
|
||||
'table_name AS "tableName",',
|
||||
'constraint_type AS "constraintType",',
|
||||
'is_deferrable AS "isDeferrable",',
|
||||
'initially_deferred AS "initiallyDeferred"',
|
||||
'from INFORMATION_SCHEMA.table_constraints',
|
||||
`WHERE table_name='${tableName}';`
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
removeIndexQuery(tableName, indexNameOrAttributes) {
|
||||
let indexName = indexNameOrAttributes;
|
||||
|
||||
if (typeof indexName !== 'string') {
|
||||
indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`);
|
||||
}
|
||||
|
||||
return `DROP INDEX IF EXISTS ${this.quoteIdentifiers(indexName)}`;
|
||||
}
|
||||
|
||||
addLimitAndOffset(options) {
|
||||
let fragment = '';
|
||||
/* eslint-disable */
|
||||
if (options.limit != null) {
|
||||
fragment += ' LIMIT ' + this.escape(options.limit);
|
||||
}
|
||||
if (options.offset != null) {
|
||||
fragment += ' OFFSET ' + this.escape(options.offset);
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
attributeToSQL(attribute, options) {
|
||||
if (!_.isPlainObject(attribute)) {
|
||||
attribute = {
|
||||
type: attribute
|
||||
};
|
||||
}
|
||||
|
||||
let type;
|
||||
if (
|
||||
attribute.type instanceof DataTypes.ENUM ||
|
||||
attribute.type instanceof DataTypes.ARRAY && attribute.type.type instanceof DataTypes.ENUM
|
||||
) {
|
||||
const enumType = attribute.type.type || attribute.type;
|
||||
let values = attribute.values;
|
||||
|
||||
if (enumType.values && !attribute.values) {
|
||||
values = enumType.values;
|
||||
}
|
||||
|
||||
if (Array.isArray(values) && values.length > 0) {
|
||||
type = `ENUM(${values.map(value => this.escape(value)).join(', ')})`;
|
||||
|
||||
if (attribute.type instanceof DataTypes.ARRAY) {
|
||||
type += '[]';
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error("Values for ENUM haven't been defined.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
type = attribute.type;
|
||||
}
|
||||
|
||||
let sql = type.toString();
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(attribute, 'allowNull') && !attribute.allowNull) {
|
||||
sql += ' NOT NULL';
|
||||
}
|
||||
|
||||
if (attribute.autoIncrement) {
|
||||
if (attribute.autoIncrementIdentity) {
|
||||
sql += ' GENERATED BY DEFAULT AS IDENTITY';
|
||||
} else {
|
||||
sql += ' SERIAL';
|
||||
}
|
||||
}
|
||||
|
||||
if (Utils.defaultValueSchemable(attribute.defaultValue)) {
|
||||
sql += ` DEFAULT ${this.escape(attribute.defaultValue, attribute)}`;
|
||||
}
|
||||
|
||||
if (attribute.unique === true) {
|
||||
sql += ' UNIQUE';
|
||||
}
|
||||
|
||||
if (attribute.primaryKey) {
|
||||
sql += ' PRIMARY KEY';
|
||||
}
|
||||
|
||||
if (attribute.references) {
|
||||
let referencesTable = this.quoteTable(attribute.references.model);
|
||||
let schema;
|
||||
|
||||
if (options.schema) {
|
||||
schema = options.schema;
|
||||
} else if (
|
||||
(!attribute.references.model || typeof attribute.references.model == 'string')
|
||||
&& options.table
|
||||
&& options.table.schema
|
||||
) {
|
||||
schema = options.table.schema;
|
||||
}
|
||||
|
||||
if (schema) {
|
||||
referencesTable = this.quoteTable(this.addSchema({
|
||||
tableName: referencesTable,
|
||||
_schema: schema
|
||||
}));
|
||||
}
|
||||
|
||||
let referencesKey;
|
||||
|
||||
if (attribute.references.key) {
|
||||
referencesKey = this.quoteIdentifiers(attribute.references.key);
|
||||
} else {
|
||||
referencesKey = this.quoteIdentifier('id');
|
||||
}
|
||||
|
||||
sql += ` REFERENCES ${referencesTable} (${referencesKey})`;
|
||||
|
||||
if (attribute.onDelete) {
|
||||
sql += ` ON DELETE ${attribute.onDelete.toUpperCase()}`;
|
||||
}
|
||||
|
||||
if (attribute.onUpdate) {
|
||||
sql += ` ON UPDATE ${attribute.onUpdate.toUpperCase()}`;
|
||||
}
|
||||
|
||||
if (attribute.references.deferrable) {
|
||||
sql += ` ${attribute.references.deferrable.toString(this)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (attribute.comment && typeof attribute.comment === 'string') {
|
||||
if (options && (options.context === 'addColumn' || options.context === 'changeColumn')) {
|
||||
const quotedAttr = this.quoteIdentifier(options.key);
|
||||
const escapedCommentText = this.escape(attribute.comment);
|
||||
sql += `; COMMENT ON COLUMN ${this.quoteTable(options.table)}.${quotedAttr} IS ${escapedCommentText}`;
|
||||
} else {
|
||||
// for createTable event which does it's own parsing
|
||||
// TODO: centralize creation of comment statements here
|
||||
sql += ` COMMENT ${attribute.comment}`;
|
||||
}
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
deferConstraintsQuery(options) {
|
||||
return options.deferrable.toString(this);
|
||||
}
|
||||
|
||||
setConstraintQuery(columns, type) {
|
||||
let columnFragment = 'ALL';
|
||||
|
||||
if (columns) {
|
||||
columnFragment = columns.map(column => this.quoteIdentifier(column)).join(', ');
|
||||
}
|
||||
|
||||
return `SET CONSTRAINTS ${columnFragment} ${type}`;
|
||||
}
|
||||
|
||||
setDeferredQuery(columns) {
|
||||
return this.setConstraintQuery(columns, 'DEFERRED');
|
||||
}
|
||||
|
||||
setImmediateQuery(columns) {
|
||||
return this.setConstraintQuery(columns, 'IMMEDIATE');
|
||||
}
|
||||
|
||||
attributesToSQL(attributes, options) {
|
||||
const result = {};
|
||||
|
||||
for (const key in attributes) {
|
||||
const attribute = attributes[key];
|
||||
result[attribute.field || key] = this.attributeToSQL(attribute, Object.assign({ key }, options || {}));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
createTrigger(tableName, triggerName, eventType, fireOnSpec, functionName, functionParams, optionsArray) {
|
||||
const decodedEventType = this.decodeTriggerEventType(eventType);
|
||||
const eventSpec = this.expandTriggerEventSpec(fireOnSpec);
|
||||
const expandedOptions = this.expandOptions(optionsArray);
|
||||
const paramList = this.expandFunctionParamList(functionParams);
|
||||
|
||||
return `CREATE ${this.triggerEventTypeIsConstraint(eventType)}TRIGGER ${this.quoteIdentifier(triggerName)} ${decodedEventType} ${
|
||||
eventSpec} ON ${this.quoteTable(tableName)}${expandedOptions ? ` ${expandedOptions}` : ''} EXECUTE PROCEDURE ${functionName}(${paramList});`;
|
||||
}
|
||||
|
||||
dropTrigger(tableName, triggerName) {
|
||||
return `DROP TRIGGER ${this.quoteIdentifier(triggerName)} ON ${this.quoteTable(tableName)} RESTRICT;`;
|
||||
}
|
||||
|
||||
renameTrigger(tableName, oldTriggerName, newTriggerName) {
|
||||
return `ALTER TRIGGER ${this.quoteIdentifier(oldTriggerName)} ON ${this.quoteTable(tableName)} RENAME TO ${this.quoteIdentifier(newTriggerName)};`;
|
||||
}
|
||||
|
||||
createFunction(functionName, params, returnType, language, body, optionsArray, options) {
|
||||
if (!functionName || !returnType || !language || !body) throw new Error('createFunction missing some parameters. Did you pass functionName, returnType, language and body?');
|
||||
|
||||
const paramList = this.expandFunctionParamList(params);
|
||||
const variableList = options && options.variables ? this.expandFunctionVariableList(options.variables) : '';
|
||||
const expandedOptionsArray = this.expandOptions(optionsArray);
|
||||
|
||||
const statement = options && options.force ? 'CREATE OR REPLACE FUNCTION' : 'CREATE FUNCTION';
|
||||
|
||||
return `${statement} ${functionName}(${paramList}) RETURNS ${returnType} AS $func$ ${variableList} BEGIN ${body} END; $func$ language '${language}'${expandedOptionsArray};`;
|
||||
}
|
||||
|
||||
dropFunction(functionName, params) {
|
||||
if (!functionName) throw new Error('requires functionName');
|
||||
// RESTRICT is (currently, as of 9.2) default but we'll be explicit
|
||||
const paramList = this.expandFunctionParamList(params);
|
||||
return `DROP FUNCTION ${functionName}(${paramList}) RESTRICT;`;
|
||||
}
|
||||
|
||||
renameFunction(oldFunctionName, params, newFunctionName) {
|
||||
const paramList = this.expandFunctionParamList(params);
|
||||
return `ALTER FUNCTION ${oldFunctionName}(${paramList}) RENAME TO ${newFunctionName};`;
|
||||
}
|
||||
|
||||
databaseConnectionUri(config) {
|
||||
let uri = `${config.protocol}://${config.user}:${config.password}@${config.host}`;
|
||||
if (config.port) {
|
||||
uri += `:${config.port}`;
|
||||
}
|
||||
uri += `/${config.database}`;
|
||||
if (config.ssl) {
|
||||
uri += `?ssl=${config.ssl}`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
pgEscapeAndQuote(val) {
|
||||
return this.quoteIdentifier(Utils.removeTicks(this.escape(val), "'"));
|
||||
}
|
||||
|
||||
expandFunctionParamList(params) {
|
||||
if (params === undefined || !Array.isArray(params)) {
|
||||
throw new Error('expandFunctionParamList: function parameters array required, including an empty one for no arguments');
|
||||
}
|
||||
|
||||
const paramList = [];
|
||||
params.forEach(curParam => {
|
||||
const paramDef = [];
|
||||
if (curParam.type) {
|
||||
if (curParam.direction) { paramDef.push(curParam.direction); }
|
||||
if (curParam.name) { paramDef.push(curParam.name); }
|
||||
paramDef.push(curParam.type);
|
||||
} else {
|
||||
throw new Error('function or trigger used with a parameter without any type');
|
||||
}
|
||||
|
||||
const joined = paramDef.join(' ');
|
||||
if (joined) paramList.push(joined);
|
||||
|
||||
});
|
||||
|
||||
return paramList.join(', ');
|
||||
}
|
||||
|
||||
expandFunctionVariableList(variables) {
|
||||
if (!Array.isArray(variables)) {
|
||||
throw new Error('expandFunctionVariableList: function variables must be an array');
|
||||
}
|
||||
const variableDefinitions = [];
|
||||
variables.forEach(variable => {
|
||||
if (!variable.name || !variable.type) {
|
||||
throw new Error('function variable must have a name and type');
|
||||
}
|
||||
let variableDefinition = `DECLARE ${variable.name} ${variable.type}`;
|
||||
if (variable.default) {
|
||||
variableDefinition += ` := ${variable.default}`;
|
||||
}
|
||||
variableDefinition += ';';
|
||||
variableDefinitions.push(variableDefinition);
|
||||
});
|
||||
return variableDefinitions.join(' ');
|
||||
}
|
||||
|
||||
expandOptions(options) {
|
||||
return options === undefined || _.isEmpty(options) ?
|
||||
'' : options.join(' ');
|
||||
}
|
||||
|
||||
decodeTriggerEventType(eventSpecifier) {
|
||||
const EVENT_DECODER = {
|
||||
'after': 'AFTER',
|
||||
'before': 'BEFORE',
|
||||
'instead_of': 'INSTEAD OF',
|
||||
'after_constraint': 'AFTER'
|
||||
};
|
||||
|
||||
if (!EVENT_DECODER[eventSpecifier]) {
|
||||
throw new Error(`Invalid trigger event specified: ${eventSpecifier}`);
|
||||
}
|
||||
|
||||
return EVENT_DECODER[eventSpecifier];
|
||||
}
|
||||
|
||||
triggerEventTypeIsConstraint(eventSpecifier) {
|
||||
return eventSpecifier === 'after_constraint' ? 'CONSTRAINT ' : '';
|
||||
}
|
||||
|
||||
expandTriggerEventSpec(fireOnSpec) {
|
||||
if (_.isEmpty(fireOnSpec)) {
|
||||
throw new Error('no table change events specified to trigger on');
|
||||
}
|
||||
|
||||
return _.map(fireOnSpec, (fireValue, fireKey) => {
|
||||
const EVENT_MAP = {
|
||||
'insert': 'INSERT',
|
||||
'update': 'UPDATE',
|
||||
'delete': 'DELETE',
|
||||
'truncate': 'TRUNCATE'
|
||||
};
|
||||
|
||||
if (!EVENT_MAP[fireValue]) {
|
||||
throw new Error(`parseTriggerEventSpec: undefined trigger event ${fireKey}`);
|
||||
}
|
||||
|
||||
let eventSpec = EVENT_MAP[fireValue];
|
||||
if (eventSpec === 'UPDATE') {
|
||||
if (Array.isArray(fireValue) && fireValue.length > 0) {
|
||||
eventSpec += ` OF ${fireValue.join(', ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
return eventSpec;
|
||||
}).join(' OR ');
|
||||
}
|
||||
|
||||
pgEnumName(tableName, attr, options) {
|
||||
options = options || {};
|
||||
|
||||
const tableDetails = this.extractTableDetails(tableName, options);
|
||||
let enumName = Utils.addTicks(Utils.generateEnumName(tableDetails.tableName, attr), '"');
|
||||
|
||||
// pgListEnums requires the enum name only, without the schema
|
||||
if (options.schema !== false && tableDetails.schema) {
|
||||
enumName = this.quoteIdentifier(tableDetails.schema) + tableDetails.delimiter + enumName;
|
||||
}
|
||||
|
||||
return enumName;
|
||||
}
|
||||
|
||||
pgListEnums(tableName, attrName, options) {
|
||||
let enumName = '';
|
||||
const tableDetails = this.extractTableDetails(tableName, options);
|
||||
|
||||
if (tableDetails.tableName && attrName) {
|
||||
enumName = ` AND t.typname=${this.pgEnumName(tableDetails.tableName, attrName, { schema: false }).replace(/"/g, "'")}`;
|
||||
}
|
||||
|
||||
return 'SELECT t.typname enum_name, array_agg(e.enumlabel ORDER BY enumsortorder) enum_value FROM pg_type t ' +
|
||||
'JOIN pg_enum e ON t.oid = e.enumtypid ' +
|
||||
'JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace ' +
|
||||
`WHERE n.nspname = '${tableDetails.schema}'${enumName} GROUP BY 1`;
|
||||
}
|
||||
|
||||
pgEnum(tableName, attr, dataType, options) {
|
||||
const enumName = this.pgEnumName(tableName, attr, options);
|
||||
let values;
|
||||
|
||||
if (dataType.values) {
|
||||
values = `ENUM(${dataType.values.map(value => this.escape(value)).join(', ')})`;
|
||||
} else {
|
||||
values = dataType.toString().match(/^ENUM\(.+\)/)[0];
|
||||
}
|
||||
|
||||
let sql = `CREATE TYPE ${enumName} AS ${values};`;
|
||||
if (!!options && options.force === true) {
|
||||
sql = this.pgEnumDrop(tableName, attr) + sql;
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
pgEnumAdd(tableName, attr, value, options) {
|
||||
const enumName = this.pgEnumName(tableName, attr);
|
||||
let sql = `ALTER TYPE ${enumName} ADD VALUE `;
|
||||
|
||||
if (semver.gte(this.sequelize.options.databaseVersion, '9.3.0')) {
|
||||
sql += 'IF NOT EXISTS ';
|
||||
}
|
||||
sql += this.escape(value);
|
||||
|
||||
if (options.before) {
|
||||
sql += ` BEFORE ${this.escape(options.before)}`;
|
||||
} else if (options.after) {
|
||||
sql += ` AFTER ${this.escape(options.after)}`;
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
pgEnumDrop(tableName, attr, enumName) {
|
||||
enumName = enumName || this.pgEnumName(tableName, attr);
|
||||
return `DROP TYPE IF EXISTS ${enumName}; `;
|
||||
}
|
||||
|
||||
fromArray(text) {
|
||||
text = text.replace(/^{/, '').replace(/}$/, '');
|
||||
let matches = text.match(/("(?:\\.|[^"\\\\])*"|[^,]*)(?:\s*,\s*|\s*$)/ig);
|
||||
|
||||
if (matches.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
matches = matches.map(m => m.replace(/",$/, '').replace(/,$/, '').replace(/(^"|"$)/, ''));
|
||||
|
||||
return matches.slice(0, -1);
|
||||
}
|
||||
|
||||
padInt(i) {
|
||||
return i < 10 ? `0${i.toString()}` : i.toString();
|
||||
}
|
||||
|
||||
dataTypeMapping(tableName, attr, dataType) {
|
||||
if (dataType.includes('PRIMARY KEY')) {
|
||||
dataType = dataType.replace('PRIMARY KEY', '');
|
||||
}
|
||||
|
||||
if (dataType.includes('SERIAL')) {
|
||||
if (dataType.includes('BIGINT')) {
|
||||
dataType = dataType.replace('SERIAL', 'BIGSERIAL');
|
||||
dataType = dataType.replace('BIGINT', '');
|
||||
} else if (dataType.includes('SMALLINT')) {
|
||||
dataType = dataType.replace('SERIAL', 'SMALLSERIAL');
|
||||
dataType = dataType.replace('SMALLINT', '');
|
||||
} else {
|
||||
dataType = dataType.replace('INTEGER', '');
|
||||
}
|
||||
dataType = dataType.replace('NOT NULL', '');
|
||||
}
|
||||
|
||||
if (dataType.startsWith('ENUM(')) {
|
||||
dataType = dataType.replace(/^ENUM\(.+\)/, this.pgEnumName(tableName, attr));
|
||||
}
|
||||
|
||||
return dataType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 'SELECT conname as constraint_name, pg_catalog.pg_get_constraintdef(r.oid, true) as condef FROM pg_catalog.pg_constraint r ' +
|
||||
`WHERE r.conrelid = (SELECT oid FROM pg_class WHERE relname = '${tableName}' LIMIT 1) AND r.contype = 'f' ORDER BY 1;`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate common SQL prefix for getForeignKeyReferencesQuery.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
_getForeignKeyReferencesQueryPrefix() {
|
||||
return 'SELECT ' +
|
||||
'DISTINCT tc.constraint_name as constraint_name, ' +
|
||||
'tc.constraint_schema as constraint_schema, ' +
|
||||
'tc.constraint_catalog as constraint_catalog, ' +
|
||||
'tc.table_name as table_name,' +
|
||||
'tc.table_schema as table_schema,' +
|
||||
'tc.table_catalog as table_catalog,' +
|
||||
'kcu.column_name as column_name,' +
|
||||
'ccu.table_schema AS referenced_table_schema,' +
|
||||
'ccu.table_catalog AS referenced_table_catalog,' +
|
||||
'ccu.table_name AS referenced_table_name,' +
|
||||
'ccu.column_name AS referenced_column_name ' +
|
||||
'FROM information_schema.table_constraints AS tc ' +
|
||||
'JOIN information_schema.key_column_usage AS kcu ' +
|
||||
'ON tc.constraint_name = kcu.constraint_name ' +
|
||||
'JOIN information_schema.constraint_column_usage AS ccu ' +
|
||||
'ON ccu.constraint_name = tc.constraint_name ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQL query that returns all foreign keys details of a table.
|
||||
*
|
||||
* As for getForeignKeysQuery is not compatible with getForeignKeyReferencesQuery, so add a new function.
|
||||
*
|
||||
* @param {string} tableName
|
||||
* @param {string} catalogName
|
||||
* @param {string} schemaName
|
||||
*/
|
||||
getForeignKeyReferencesQuery(tableName, catalogName, schemaName) {
|
||||
return `${this._getForeignKeyReferencesQueryPrefix()
|
||||
}WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '${tableName}'${
|
||||
catalogName ? ` AND tc.table_catalog = '${catalogName}'` : ''
|
||||
}${schemaName ? ` AND tc.table_schema = '${schemaName}'` : ''}`;
|
||||
}
|
||||
|
||||
getForeignKeyReferenceQuery(table, columnName) {
|
||||
const tableName = table.tableName || table;
|
||||
const schema = table.schema;
|
||||
return `${this._getForeignKeyReferencesQueryPrefix()
|
||||
}WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name='${tableName}' AND kcu.column_name = '${columnName}'${
|
||||
schema ? ` AND tc.table_schema = '${schema}'` : ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQL query that removes a foreign key from a table.
|
||||
*
|
||||
* @param {string} tableName The name of the table.
|
||||
* @param {string} foreignKey The name of the foreign key constraint.
|
||||
* @returns {string} The generated sql query.
|
||||
* @private
|
||||
*/
|
||||
dropForeignKeyQuery(tableName, foreignKey) {
|
||||
return `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.quoteIdentifier(foreignKey)};`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PostgresQueryGenerator;
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
'use strict';
|
||||
|
||||
const DataTypes = require('../../data-types');
|
||||
const Promise = require('../../promise');
|
||||
const QueryTypes = require('../../query-types');
|
||||
const _ = require('lodash');
|
||||
|
||||
|
||||
/**
|
||||
Returns an object that handles Postgres special needs to do certain queries.
|
||||
|
||||
@class QueryInterface
|
||||
@static
|
||||
@private
|
||||
*/
|
||||
|
||||
/**
|
||||
* Ensure enum and their values.
|
||||
*
|
||||
* @param {QueryInterface} qi
|
||||
* @param {string} tableName Name of table to create
|
||||
* @param {Object} attributes Object representing a list of normalized table attributes
|
||||
* @param {Object} [options]
|
||||
* @param {Model} [model]
|
||||
*
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
function ensureEnums(qi, tableName, attributes, options, model) {
|
||||
const keys = Object.keys(attributes);
|
||||
const keyLen = keys.length;
|
||||
|
||||
let sql = '';
|
||||
let promises = [];
|
||||
let i = 0;
|
||||
|
||||
for (i = 0; i < keyLen; i++) {
|
||||
const attribute = attributes[keys[i]];
|
||||
const type = attribute.type;
|
||||
|
||||
if (
|
||||
type instanceof DataTypes.ENUM ||
|
||||
type instanceof DataTypes.ARRAY && type.type instanceof DataTypes.ENUM //ARRAY sub type is ENUM
|
||||
) {
|
||||
sql = qi.QueryGenerator.pgListEnums(tableName, attribute.field || keys[i], options);
|
||||
promises.push(qi.sequelize.query(
|
||||
sql,
|
||||
Object.assign({}, options, { plain: true, raw: true, type: QueryTypes.SELECT })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(results => {
|
||||
promises = [];
|
||||
let enumIdx = 0;
|
||||
|
||||
// This little function allows us to re-use the same code that prepends or appends new value to enum array
|
||||
const addEnumValue = (field, value, relativeValue, position = 'before', spliceStart = promises.length) => {
|
||||
const valueOptions = _.clone(options);
|
||||
valueOptions.before = null;
|
||||
valueOptions.after = null;
|
||||
|
||||
switch (position) {
|
||||
case 'after':
|
||||
valueOptions.after = relativeValue;
|
||||
break;
|
||||
case 'before':
|
||||
default:
|
||||
valueOptions.before = relativeValue;
|
||||
break;
|
||||
}
|
||||
|
||||
promises.splice(spliceStart, 0, () => {
|
||||
return qi.sequelize.query(qi.QueryGenerator.pgEnumAdd(
|
||||
tableName, field, value, valueOptions
|
||||
), valueOptions);
|
||||
});
|
||||
};
|
||||
|
||||
for (i = 0; i < keyLen; i++) {
|
||||
const attribute = attributes[keys[i]];
|
||||
const type = attribute.type;
|
||||
const enumType = type.type || type;
|
||||
const field = attribute.field || keys[i];
|
||||
|
||||
if (
|
||||
type instanceof DataTypes.ENUM ||
|
||||
type instanceof DataTypes.ARRAY && enumType instanceof DataTypes.ENUM //ARRAY sub type is ENUM
|
||||
) {
|
||||
// If the enum type doesn't exist then create it
|
||||
if (!results[enumIdx]) {
|
||||
promises.push(() => {
|
||||
return qi.sequelize.query(qi.QueryGenerator.pgEnum(tableName, field, enumType, options), Object.assign({}, options, { raw: true }));
|
||||
});
|
||||
} else if (!!results[enumIdx] && !!model) {
|
||||
const enumVals = qi.QueryGenerator.fromArray(results[enumIdx].enum_value);
|
||||
const vals = enumType.values;
|
||||
|
||||
// Going through already existing values allows us to make queries that depend on those values
|
||||
// We will prepend all new values between the old ones, but keep in mind - we can't change order of already existing values
|
||||
// Then we append the rest of new values AFTER the latest already existing value
|
||||
// E.g.: [1,2] -> [0,2,1] ==> [1,0,2]
|
||||
// E.g.: [1,2,3] -> [2,1,3,4] ==> [1,2,3,4]
|
||||
// E.g.: [1] -> [0,2,3] ==> [1,0,2,3]
|
||||
let lastOldEnumValue;
|
||||
let rightestPosition = -1;
|
||||
for (let oldIndex = 0; oldIndex < enumVals.length; oldIndex++) {
|
||||
const enumVal = enumVals[oldIndex];
|
||||
const newIdx = vals.indexOf(enumVal);
|
||||
lastOldEnumValue = enumVal;
|
||||
|
||||
if (newIdx === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newValuesBefore = vals.slice(0, newIdx);
|
||||
const promisesLength = promises.length;
|
||||
// we go in reverse order so we could stop when we meet old value
|
||||
for (let reverseIdx = newValuesBefore.length - 1; reverseIdx >= 0; reverseIdx--) {
|
||||
if (~enumVals.indexOf(newValuesBefore[reverseIdx])) {
|
||||
break;
|
||||
}
|
||||
|
||||
addEnumValue(field, newValuesBefore[reverseIdx], lastOldEnumValue, 'before', promisesLength);
|
||||
}
|
||||
|
||||
// we detect the most 'right' position of old value in new enum array so we can append new values to it
|
||||
if (newIdx > rightestPosition) {
|
||||
rightestPosition = newIdx;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastOldEnumValue && rightestPosition < vals.length - 1) {
|
||||
const remainingEnumValues = vals.slice(rightestPosition + 1);
|
||||
for (let reverseIdx = remainingEnumValues.length - 1; reverseIdx >= 0; reverseIdx--) {
|
||||
addEnumValue(field, remainingEnumValues[reverseIdx], lastOldEnumValue, 'after');
|
||||
}
|
||||
}
|
||||
|
||||
enumIdx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return promises
|
||||
.reduce((promise, asyncFunction) => promise.then(asyncFunction), Promise.resolve())
|
||||
.tap(() => {
|
||||
// If ENUM processed, then refresh OIDs
|
||||
if (promises.length) {
|
||||
return qi.sequelize.dialect.connectionManager._refreshDynamicOIDs();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
exports.ensureEnums = ensureEnums;
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
'use strict';
|
||||
|
||||
const AbstractQuery = require('../abstract/query');
|
||||
const QueryTypes = require('../../query-types');
|
||||
const Promise = require('../../promise');
|
||||
const sequelizeErrors = require('../../errors');
|
||||
const _ = require('lodash');
|
||||
const { logger } = require('../../utils/logger');
|
||||
|
||||
const debug = logger.debugContext('sql:pg');
|
||||
|
||||
|
||||
class Query extends AbstractQuery {
|
||||
/**
|
||||
* Rewrite query with parameters.
|
||||
*
|
||||
* @param {string} sql
|
||||
* @param {Array|Object} values
|
||||
* @param {string} dialect
|
||||
* @private
|
||||
*/
|
||||
static formatBindParameters(sql, values, dialect) {
|
||||
const stringReplaceFunc = value => typeof value === 'string' ? value.replace(/\0/g, '\\0') : value;
|
||||
|
||||
let bindParam;
|
||||
if (Array.isArray(values)) {
|
||||
bindParam = values.map(stringReplaceFunc);
|
||||
sql = AbstractQuery.formatBindParameters(sql, values, dialect, { skipValueReplace: true })[0];
|
||||
} else {
|
||||
bindParam = [];
|
||||
let i = 0;
|
||||
const seen = {};
|
||||
const replacementFunc = (match, key, values) => {
|
||||
if (seen[key] !== undefined) {
|
||||
return seen[key];
|
||||
}
|
||||
if (values[key] !== undefined) {
|
||||
i = i + 1;
|
||||
bindParam.push(stringReplaceFunc(values[key]));
|
||||
seen[key] = `$${i}`;
|
||||
return `$${i}`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0];
|
||||
}
|
||||
return [sql, bindParam];
|
||||
}
|
||||
|
||||
run(sql, parameters) {
|
||||
const { connection } = this;
|
||||
|
||||
if (!_.isEmpty(this.options.searchPath)) {
|
||||
sql = this.sequelize.getQueryInterface().QueryGenerator.setSearchPath(this.options.searchPath) + sql;
|
||||
}
|
||||
this.sql = sql;
|
||||
|
||||
const query = parameters && parameters.length
|
||||
? new Promise((resolve, reject) => connection.query(sql, parameters, (error, result) => error ? reject(error) : resolve(result)))
|
||||
: new Promise((resolve, reject) => connection.query(sql, (error, result) => error ? reject(error) : resolve(result)));
|
||||
|
||||
const complete = this._logQuery(sql, debug, parameters);
|
||||
|
||||
return query.catch(err => {
|
||||
// set the client so that it will be reaped if the connection resets while executing
|
||||
if (err.code === 'ECONNRESET') {
|
||||
connection._invalid = true;
|
||||
}
|
||||
|
||||
err.sql = sql;
|
||||
err.parameters = parameters;
|
||||
throw this.formatError(err);
|
||||
})
|
||||
.then(queryResult => {
|
||||
complete();
|
||||
|
||||
let rows = Array.isArray(queryResult)
|
||||
? queryResult.reduce((allRows, r) => allRows.concat(r.rows || []), [])
|
||||
: queryResult.rows;
|
||||
const rowCount = Array.isArray(queryResult)
|
||||
? queryResult.reduce(
|
||||
(count, r) => Number.isFinite(r.rowCount) ? count + r.rowCount : count,
|
||||
0
|
||||
)
|
||||
: queryResult.rowCount;
|
||||
|
||||
if (this.sequelize.options.minifyAliases && this.options.aliasesMapping) {
|
||||
rows = rows
|
||||
.map(row => _.toPairs(row)
|
||||
.reduce((acc, [key, value]) => {
|
||||
const mapping = this.options.aliasesMapping.get(key);
|
||||
acc[mapping || key] = value;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
}
|
||||
|
||||
const isTableNameQuery = sql.startsWith('SELECT table_name FROM information_schema.tables');
|
||||
const isRelNameQuery = sql.startsWith('SELECT relname FROM pg_class WHERE oid IN');
|
||||
|
||||
if (isRelNameQuery) {
|
||||
return rows.map(row => ({
|
||||
name: row.relname,
|
||||
tableName: row.relname.split('_')[0]
|
||||
}));
|
||||
}
|
||||
if (isTableNameQuery) {
|
||||
return rows.map(row => _.values(row));
|
||||
}
|
||||
|
||||
if (rows[0] && rows[0].sequelize_caught_exception !== undefined) {
|
||||
if (rows[0].sequelize_caught_exception !== null) {
|
||||
throw this.formatError({
|
||||
code: '23505',
|
||||
detail: rows[0].sequelize_caught_exception
|
||||
});
|
||||
}
|
||||
for (const row of rows) {
|
||||
delete row.sequelize_caught_exception;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isShowIndexesQuery()) {
|
||||
for (const row of rows) {
|
||||
const attributes = /ON .*? (?:USING .*?\s)?\(([^]*)\)/gi.exec(row.definition)[1].split(',');
|
||||
|
||||
// Map column index in table to column name
|
||||
const columns = _.zipObject(
|
||||
row.column_indexes,
|
||||
this.sequelize.getQueryInterface().QueryGenerator.fromArray(row.column_names)
|
||||
);
|
||||
delete row.column_indexes;
|
||||
delete row.column_names;
|
||||
|
||||
let field;
|
||||
let attribute;
|
||||
|
||||
// Indkey is the order of attributes in the index, specified by a string of attribute indexes
|
||||
row.fields = row.indkey.split(' ').map((indKey, index) => {
|
||||
field = columns[indKey];
|
||||
// for functional indices indKey = 0
|
||||
if (!field) {
|
||||
return null;
|
||||
}
|
||||
attribute = attributes[index];
|
||||
return {
|
||||
attribute: field,
|
||||
collate: attribute.match(/COLLATE "(.*?)"/) ? /COLLATE "(.*?)"/.exec(attribute)[1] : undefined,
|
||||
order: attribute.includes('DESC') ? 'DESC' : attribute.includes('ASC') ? 'ASC' : undefined,
|
||||
length: undefined
|
||||
};
|
||||
}).filter(n => n !== null);
|
||||
delete row.columns;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
if (this.isForeignKeysQuery()) {
|
||||
const result = [];
|
||||
for (const row of rows) {
|
||||
let defParts;
|
||||
if (row.condef !== undefined && (defParts = row.condef.match(/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?/))) {
|
||||
row.id = row.constraint_name;
|
||||
row.table = defParts[2];
|
||||
row.from = defParts[1];
|
||||
row.to = defParts[3];
|
||||
let i;
|
||||
for (i = 5; i <= 8; i += 3) {
|
||||
if (/(UPDATE|DELETE)/.test(defParts[i])) {
|
||||
row[`on_${defParts[i].toLowerCase()}`] = defParts[i + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
result.push(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (this.isSelectQuery()) {
|
||||
let result = rows;
|
||||
// Postgres will treat tables as case-insensitive, so fix the case
|
||||
// of the returned values to match attributes
|
||||
if (this.options.raw === false && this.sequelize.options.quoteIdentifiers === false) {
|
||||
const attrsMap = _.reduce(this.model.rawAttributes, (m, v, k) => {
|
||||
m[k.toLowerCase()] = k;
|
||||
return m;
|
||||
}, {});
|
||||
result = rows.map(row => {
|
||||
return _.mapKeys(row, (value, key) => {
|
||||
const targetAttr = attrsMap[key];
|
||||
if (typeof targetAttr === 'string' && targetAttr !== key) {
|
||||
return targetAttr;
|
||||
}
|
||||
return key;
|
||||
});
|
||||
});
|
||||
}
|
||||
return this.handleSelectQuery(result);
|
||||
}
|
||||
if (QueryTypes.DESCRIBE === this.options.type) {
|
||||
const result = {};
|
||||
|
||||
for (const row of rows) {
|
||||
result[row.Field] = {
|
||||
type: row.Type.toUpperCase(),
|
||||
allowNull: row.Null === 'YES',
|
||||
defaultValue: row.Default,
|
||||
comment: row.Comment,
|
||||
special: row.special ? this.sequelize.getQueryInterface().QueryGenerator.fromArray(row.special) : [],
|
||||
primaryKey: row.Constraint === 'PRIMARY KEY'
|
||||
};
|
||||
|
||||
if (result[row.Field].type === 'BOOLEAN') {
|
||||
result[row.Field].defaultValue = { 'false': false, 'true': true }[result[row.Field].defaultValue];
|
||||
|
||||
if (result[row.Field].defaultValue === undefined) {
|
||||
result[row.Field].defaultValue = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof result[row.Field].defaultValue === 'string') {
|
||||
result[row.Field].defaultValue = result[row.Field].defaultValue.replace(/'/g, '');
|
||||
|
||||
if (result[row.Field].defaultValue.includes('::')) {
|
||||
const split = result[row.Field].defaultValue.split('::');
|
||||
if (split[1].toLowerCase() !== 'regclass)') {
|
||||
result[row.Field].defaultValue = split[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
if (this.isVersionQuery()) {
|
||||
return rows[0].server_version;
|
||||
}
|
||||
if (this.isShowOrDescribeQuery()) {
|
||||
return rows;
|
||||
}
|
||||
if (QueryTypes.BULKUPDATE === this.options.type) {
|
||||
if (!this.options.returning) {
|
||||
return parseInt(rowCount, 10);
|
||||
}
|
||||
return this.handleSelectQuery(rows);
|
||||
}
|
||||
if (QueryTypes.BULKDELETE === this.options.type) {
|
||||
return parseInt(rowCount, 10);
|
||||
}
|
||||
if (this.isUpsertQuery()) {
|
||||
return rows[0];
|
||||
}
|
||||
if (this.isInsertQuery() || this.isUpdateQuery()) {
|
||||
if (this.instance && this.instance.dataValues) {
|
||||
for (const key in rows[0]) {
|
||||
if (Object.prototype.hasOwnProperty.call(rows[0], key)) {
|
||||
const record = rows[0][key];
|
||||
|
||||
const attr = _.find(this.model.rawAttributes, attribute => attribute.fieldName === key || attribute.field === key);
|
||||
|
||||
this.instance.dataValues[attr && attr.fieldName || key] = record;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
this.instance || rows && (this.options.plain && rows[0] || rows) || undefined,
|
||||
rowCount
|
||||
];
|
||||
}
|
||||
if (this.isRawQuery()) {
|
||||
return [rows, queryResult];
|
||||
}
|
||||
return rows;
|
||||
});
|
||||
}
|
||||
|
||||
formatError(err) {
|
||||
let match;
|
||||
let table;
|
||||
let index;
|
||||
let fields;
|
||||
let errors;
|
||||
let message;
|
||||
|
||||
const code = err.code || err.sqlState;
|
||||
const errMessage = err.message || err.messagePrimary;
|
||||
const errDetail = err.detail || err.messageDetail;
|
||||
|
||||
switch (code) {
|
||||
case '23503':
|
||||
index = errMessage.match(/violates foreign key constraint "(.+?)"/);
|
||||
index = index ? index[1] : undefined;
|
||||
table = errMessage.match(/on table "(.+?)"/);
|
||||
table = table ? table[1] : undefined;
|
||||
|
||||
return new sequelizeErrors.ForeignKeyConstraintError({ message: errMessage, fields: null, index, table, parent: err });
|
||||
case '23505':
|
||||
// there are multiple different formats of error messages for this error code
|
||||
// this regex should check at least two
|
||||
if (errDetail && (match = errDetail.replace(/"/g, '').match(/Key \((.*?)\)=\((.*?)\)/))) {
|
||||
fields = _.zipObject(match[1].split(', '), match[2].split(', '));
|
||||
errors = [];
|
||||
message = 'Validation error';
|
||||
|
||||
_.forOwn(fields, (value, field) => {
|
||||
errors.push(new sequelizeErrors.ValidationErrorItem(
|
||||
this.getUniqueConstraintErrorMessage(field),
|
||||
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
|
||||
field,
|
||||
value,
|
||||
this.instance,
|
||||
'not_unique'
|
||||
));
|
||||
});
|
||||
|
||||
if (this.model && this.model.uniqueKeys) {
|
||||
_.forOwn(this.model.uniqueKeys, constraint => {
|
||||
if (_.isEqual(constraint.fields, Object.keys(fields)) && !!constraint.msg) {
|
||||
message = constraint.msg;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields });
|
||||
}
|
||||
|
||||
return new sequelizeErrors.UniqueConstraintError({
|
||||
message: errMessage,
|
||||
parent: err
|
||||
});
|
||||
|
||||
case '23P01':
|
||||
match = errDetail.match(/Key \((.*?)\)=\((.*?)\)/);
|
||||
|
||||
if (match) {
|
||||
fields = _.zipObject(match[1].split(', '), match[2].split(', '));
|
||||
}
|
||||
message = 'Exclusion constraint error';
|
||||
|
||||
return new sequelizeErrors.ExclusionConstraintError({
|
||||
message,
|
||||
constraint: err.constraint,
|
||||
fields,
|
||||
table: err.table,
|
||||
parent: err
|
||||
});
|
||||
|
||||
case '42704':
|
||||
if (err.sql && /(CONSTRAINT|INDEX)/gi.test(err.sql)) {
|
||||
message = 'Unknown constraint error';
|
||||
index = errMessage.match(/(?:constraint|index) "(.+?)"/i);
|
||||
index = index ? index[1] : undefined;
|
||||
table = errMessage.match(/relation "(.+?)"/i);
|
||||
table = table ? table[1] : undefined;
|
||||
|
||||
throw new sequelizeErrors.UnknownConstraintError({
|
||||
message,
|
||||
constraint: index,
|
||||
fields,
|
||||
table,
|
||||
parent: err
|
||||
});
|
||||
}
|
||||
// falls through
|
||||
default:
|
||||
return new sequelizeErrors.DatabaseError(err);
|
||||
}
|
||||
}
|
||||
|
||||
isForeignKeysQuery() {
|
||||
return /SELECT conname as constraint_name, pg_catalog\.pg_get_constraintdef\(r\.oid, true\) as condef FROM pg_catalog\.pg_constraint r WHERE r\.conrelid = \(SELECT oid FROM pg_class WHERE relname = '.*' LIMIT 1\) AND r\.contype = 'f' ORDER BY 1;/.test(this.sql);
|
||||
}
|
||||
|
||||
getInsertIdField() {
|
||||
return 'id';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = Query;
|
||||
module.exports.Query = Query;
|
||||
module.exports.default = Query;
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
function stringifyRangeBound(bound) {
|
||||
if (bound === null) {
|
||||
return '' ;
|
||||
}
|
||||
if (bound === Infinity || bound === -Infinity) {
|
||||
return bound.toString().toLowerCase();
|
||||
}
|
||||
return JSON.stringify(bound);
|
||||
}
|
||||
|
||||
function parseRangeBound(bound, parseType) {
|
||||
if (!bound) {
|
||||
return null;
|
||||
}
|
||||
if (bound === 'infinity') {
|
||||
return Infinity;
|
||||
}
|
||||
if (bound === '-infinity') {
|
||||
return -Infinity;
|
||||
}
|
||||
return parseType(bound);
|
||||
|
||||
}
|
||||
|
||||
function stringify(data) {
|
||||
if (data === null) return null;
|
||||
|
||||
if (!Array.isArray(data)) throw new Error('range must be an array');
|
||||
if (!data.length) return 'empty';
|
||||
if (data.length !== 2) throw new Error('range array length must be 0 (empty) or 2 (lower and upper bounds)');
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'inclusive')) {
|
||||
if (data.inclusive === false) data.inclusive = [false, false];
|
||||
else if (!data.inclusive) data.inclusive = [true, false];
|
||||
else if (data.inclusive === true) data.inclusive = [true, true];
|
||||
} else {
|
||||
data.inclusive = [true, false];
|
||||
}
|
||||
|
||||
_.each(data, (value, index) => {
|
||||
if (_.isObject(value)) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, 'inclusive')) data.inclusive[index] = !!value.inclusive;
|
||||
if (Object.prototype.hasOwnProperty.call(value, 'value')) data[index] = value.value;
|
||||
}
|
||||
});
|
||||
|
||||
const lowerBound = stringifyRangeBound(data[0]);
|
||||
const upperBound = stringifyRangeBound(data[1]);
|
||||
|
||||
return `${(data.inclusive[0] ? '[' : '(') + lowerBound},${upperBound}${data.inclusive[1] ? ']' : ')'}`;
|
||||
}
|
||||
exports.stringify = stringify;
|
||||
|
||||
function parse(value, parser) {
|
||||
if (value === null) return null;
|
||||
if (value === 'empty') {
|
||||
return [];
|
||||
}
|
||||
|
||||
let result = value
|
||||
.substring(1, value.length - 1)
|
||||
.split(',', 2);
|
||||
|
||||
if (result.length !== 2) return value;
|
||||
|
||||
result = result.map((item, index) => {
|
||||
return {
|
||||
value: parseRangeBound(item, parser),
|
||||
inclusive: index === 0 ? value[0] === '[' : value[value.length - 1] === ']'
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
exports.parse = parse;
|
||||
+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