不要怂,就是干,撸起袖子干!

Commit 0fb4bb11 by Pedro Augusto de Paula Barbosa Committed by GitHub

refactor(lib): new `joinSQLFragments` util (#11865)

1 parent 91416e3b
......@@ -10,6 +10,7 @@ const moment = require('moment');
const { logger } = require('./utils/logger');
const warnings = {};
const { classToInvokable } = require('./utils/classToInvokable');
const { joinSQLFragments } = require('./utils/join-sql-fragments');
class ABSTRACT {
toString(options) {
......@@ -62,7 +63,10 @@ class STRING extends ABSTRACT {
this._length = options.length || 255;
}
toSql() {
return `VARCHAR(${this._length})${this._binary ? ' BINARY' : ''}`;
return joinSQLFragments([
`VARCHAR(${this._length})`,
this._binary && 'BINARY'
]);
}
validate(value) {
if (Object.prototype.toString.call(value) !== '[object String]') {
......@@ -97,7 +101,10 @@ class CHAR extends STRING {
super(typeof length === 'object' && length || { length, binary });
}
toSql() {
return `CHAR(${this._length})${this._binary ? ' BINARY' : ''}`;
return joinSQLFragments([
`CHAR(${this._length})`,
this._binary && 'BINARY'
]);
}
}
......
......@@ -310,7 +310,19 @@ class QueryGenerator {
returning += returnValues.returningFragment;
}
return `INSERT${ignoreDuplicates} INTO ${this.quoteTable(tableName)} (${attributes}) VALUES ${tuples.join(',')}${onDuplicateKeyUpdate}${onConflictDoNothing}${returning};`;
return Utils.joinSQLFragments([
'INSERT',
ignoreDuplicates,
'INTO',
this.quoteTable(tableName),
`(${attributes})`,
'VALUES',
tuples.join(','),
onDuplicateKeyUpdate,
onConflictDoNothing,
returning,
';'
]);
}
/**
......@@ -446,7 +458,15 @@ class QueryGenerator {
updateSetSqlFragments.push(`${quotedField}=${escapedValue}`);
}
return `UPDATE ${this.quoteTable(tableName)} SET ${updateSetSqlFragments.join(',')}${outputFragment} ${this.whereQuery(where)}${returningFragment}`.trim();
return Utils.joinSQLFragments([
'UPDATE',
this.quoteTable(tableName),
'SET',
updateSetSqlFragments.join(','),
outputFragment,
this.whereQuery(where),
returningFragment
]);
}
/*
......@@ -570,16 +590,19 @@ class QueryGenerator {
}
addConstraintQuery(tableName, options) {
options = options || {};
const constraintSnippet = this.getConstraintSnippet(tableName, options);
if (typeof tableName === 'string') {
tableName = this.quoteIdentifiers(tableName);
} else {
tableName = this.quoteTable(tableName);
}
return `ALTER TABLE ${tableName} ADD ${constraintSnippet};`;
return Utils.joinSQLFragments([
'ALTER TABLE',
tableName,
'ADD',
this.getConstraintSnippet(tableName, options || {}),
';'
]);
}
getConstraintSnippet(tableName, options) {
......@@ -660,7 +683,12 @@ class QueryGenerator {
tableName = this.quoteTable(tableName);
}
return `ALTER TABLE ${tableName} DROP CONSTRAINT ${this.quoteIdentifiers(constraintName)}`;
return Utils.joinSQLFragments([
'ALTER TABLE',
tableName,
'DROP CONSTRAINT',
this.quoteIdentifiers(constraintName)
]);
}
/*
......@@ -1486,7 +1514,11 @@ class QueryGenerator {
alias = this._getMinifiedAlias(alias, includeAs.internalAs, topLevelInfo.options);
}
return `${prefix} AS ${this.quoteIdentifier(alias, true)}`;
return Utils.joinSQLFragments([
prefix,
'AS',
this.quoteIdentifier(alias, true)
]);
});
if (include.subQuery && topLevelInfo.subQuery) {
for (const attr of includeAttributes) {
......@@ -1750,9 +1782,11 @@ class QueryGenerator {
alias = this._getMinifiedAlias(alias, throughAs, topLevelInfo.options);
}
return `${this.quoteIdentifier(throughAs)}.${this.quoteIdentifier(Array.isArray(attr) ? attr[0] : attr)
} AS ${
this.quoteIdentifier(alias)}`;
return Utils.joinSQLFragments([
`${this.quoteIdentifier(throughAs)}.${this.quoteIdentifier(Array.isArray(attr) ? attr[0] : attr)}`,
'AS',
this.quoteIdentifier(alias)
]);
});
const association = include.association;
const parentIsTop = !include.parent.association && include.parent.model.name === topLevelInfo.options.model.name;
......@@ -2130,15 +2164,17 @@ class QueryGenerator {
return `CAST(${result} AS ${smth.type.toUpperCase()})`;
}
if (smth instanceof Utils.Fn) {
return `${smth.fn}(${smth.args.map(arg => {
if (arg instanceof Utils.SequelizeMethod) {
return this.handleSequelizeMethod(arg, tableName, factory, options, prepend);
}
if (_.isPlainObject(arg)) {
return this.whereItemsQuery(arg);
}
return this.escape(typeof arg === 'string' ? arg.replace('$', '$$$') : arg);
}).join(', ')})`;
return `${smth.fn}(${
smth.args.map(arg => {
if (arg instanceof Utils.SequelizeMethod) {
return this.handleSequelizeMethod(arg, tableName, factory, options, prepend);
}
if (_.isPlainObject(arg)) {
return this.whereItemsQuery(arg);
}
return this.escape(typeof arg === 'string' ? arg.replace('$', '$$$') : arg);
}).join(', ')
})`;
}
if (smth instanceof Utils.Col) {
if (Array.isArray(smth.col) && !factory) {
......
......@@ -51,7 +51,7 @@ module.exports = BaseTypes => {
class DATE extends BaseTypes.DATE {
toSql() {
return `DATETIME${this._length ? `(${this._length})` : ''}`;
return this._length ? `DATETIME(${this._length})` : 'DATETIME';
}
_stringify(date, options) {
date = this._applyTimezone(date, options);
......
'use strict';
const MySQLQueryGenerator = require('../mysql/query-generator');
const Utils = require('./../../utils');
class MariaDBQueryGenerator extends MySQLQueryGenerator {
createSchema(schema, options) {
......@@ -9,10 +10,13 @@ class MariaDBQueryGenerator extends MySQLQueryGenerator {
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};`;
return Utils.joinSQLFragments([
'CREATE SCHEMA IF NOT EXISTS',
this.quoteIdentifier(schema),
options.charset && `DEFAULT CHARACTER SET ${this.escape(options.charset)}`,
options.collate && `DEFAULT COLLATE ${this.escape(options.collate)}`,
';'
]);
}
dropSchema(schema) {
......@@ -20,8 +24,22 @@ class MariaDBQueryGenerator extends MySQLQueryGenerator {
}
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)}`, '') : ''});`;
const schemasToSkip = [
'\'MYSQL\'',
'\'INFORMATION_SCHEMA\'',
'\'PERFORMANCE_SCHEMA\''
];
if (options.skip && Array.isArray(options.skip) && options.skip.length > 0) {
for (const schemaName of options.skip) {
schemasToSkip.push(this.escape(schemaName));
}
}
return Utils.joinSQLFragments([
'SELECT SCHEMA_NAME as schema_name',
'FROM INFORMATION_SCHEMA.SCHEMATA',
`WHERE SCHEMA_NAME NOT IN (${schemasToSkip.join(', ')})`,
';'
]);
}
showTablesQuery(database) {
......
......@@ -107,10 +107,9 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator {
}
createTableQuery(tableName, attributes, options) {
const query = (table, attrs) => `IF OBJECT_ID('${table}', 'U') IS NULL CREATE TABLE ${table} (${attrs})`,
primaryKeys = [],
const primaryKeys = [],
foreignKeys = {},
attrStr = [];
attributesClauseParts = [];
let commentStr = '';
......@@ -133,24 +132,22 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator {
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', '')}`);
attributesClauseParts.push(`${this.quoteIdentifier(attr)} ${match[1].replace('PRIMARY KEY', '')}`);
foreignKeys[attr] = match[2];
} else {
attrStr.push(`${this.quoteIdentifier(attr)} ${dataType.replace('PRIMARY KEY', '')}`);
attributesClauseParts.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]}`);
attributesClauseParts.push(`${this.quoteIdentifier(attr)} ${match[1]}`);
foreignKeys[attr] = match[2];
} else {
attrStr.push(`${this.quoteIdentifier(attr)} ${dataType}`);
attributesClauseParts.push(`${this.quoteIdentifier(attr)} ${dataType}`);
}
}
}
let attributesClause = attrStr.join(', ');
const pkString = primaryKeys.map(pk => this.quoteIdentifier(pk)).join(', ');
if (options.uniqueKeys) {
......@@ -159,22 +156,33 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator {
if (typeof indexName !== 'string') {
indexName = `uniq_${tableName}_${columns.fields.join('_')}`;
}
attributesClause += `, CONSTRAINT ${this.quoteIdentifier(indexName)} UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
attributesClauseParts.push(`CONSTRAINT ${
this.quoteIdentifier(indexName)
} UNIQUE (${
columns.fields.map(field => this.quoteIdentifier(field)).join(', ')
})`);
}
});
}
if (pkString.length > 0) {
attributesClause += `, PRIMARY KEY (${pkString})`;
attributesClauseParts.push(`PRIMARY KEY (${pkString})`);
}
for (const fkey in foreignKeys) {
if (Object.prototype.hasOwnProperty.call(foreignKeys, fkey)) {
attributesClause += `, FOREIGN KEY (${this.quoteIdentifier(fkey)}) ${foreignKeys[fkey]}`;
attributesClauseParts.push(`FOREIGN KEY (${this.quoteIdentifier(fkey)}) ${foreignKeys[fkey]}`);
}
}
return `${query(this.quoteTable(tableName), attributesClause)};${commentStr}`;
const quotedTableName = this.quoteTable(tableName);
return Utils.joinSQLFragments([
`IF OBJECT_ID('${quotedTableName}', 'U') IS NULL`,
`CREATE TABLE ${quotedTableName} (${attributesClauseParts.join(', ')})`,
';',
commentStr
]);
}
describeTableQuery(tableName, schema) {
......@@ -226,8 +234,13 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator {
}
dropTableQuery(tableName) {
const qouteTbl = this.quoteTable(tableName);
return `IF OBJECT_ID('${qouteTbl}', 'U') IS NOT NULL DROP TABLE ${qouteTbl};`;
const quoteTbl = this.quoteTable(tableName);
return Utils.joinSQLFragments([
`IF OBJECT_ID('${quoteTbl}', 'U') IS NOT NULL`,
'DROP TABLE',
quoteTbl,
';'
]);
}
addColumnQuery(table, key, dataType) {
......@@ -244,10 +257,15 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator {
delete dataType['comment'];
}
const def = this.attributeToSQL(dataType, {
context: 'addColumn'
});
return `ALTER TABLE ${this.quoteTable(table)} ADD ${this.quoteIdentifier(key)} ${def};${commentStr}`;
return Utils.joinSQLFragments([
'ALTER TABLE',
this.quoteTable(table),
'ADD',
this.quoteIdentifier(key),
this.attributeToSQL(dataType, { context: 'addColumn' }),
';',
commentStr
]);
}
commentTemplate(comment, table, column) {
......@@ -259,7 +277,13 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator {
}
removeColumnQuery(tableName, attributeName) {
return `ALTER TABLE ${this.quoteTable(tableName)} DROP COLUMN ${this.quoteIdentifier(attributeName)};`;
return Utils.joinSQLFragments([
'ALTER TABLE',
this.quoteTable(tableName),
'DROP COLUMN',
this.quoteIdentifier(attributeName),
';'
]);
}
changeColumnQuery(tableName, attributes) {
......@@ -284,21 +308,25 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator {
}
}
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}`;
return Utils.joinSQLFragments([
'ALTER TABLE',
this.quoteTable(tableName),
attrString.length && `ALTER COLUMN ${attrString.join(', ')}`,
constraintString.length && `ADD ${constraintString.join(', ')}`,
';',
commentString
]);
}
renameColumnQuery(tableName, attrBefore, attributes) {
const newName = Object.keys(attributes)[0];
return `EXEC sp_rename '${this.quoteTable(tableName)}.${attrBefore}', '${newName}', 'COLUMN';`;
return Utils.joinSQLFragments([
'EXEC sp_rename',
`'${this.quoteTable(tableName)}.${attrBefore}',`,
`'${newName}',`,
"'COLUMN'",
';'
]);
}
bulkInsertQuery(tableName, attrValueHashes, options, attributes) {
......@@ -499,19 +527,18 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator {
deleteQuery(tableName, where, options = {}, model) {
const table = this.quoteTable(tableName);
const whereClause = this.getWhereConditions(where, null, model, options);
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;`;
return Utils.joinSQLFragments([
'DELETE',
options.limit && `TOP(${this.escape(options.limit)})`,
'FROM',
table,
whereClause && `WHERE ${whereClause}`,
';',
'SELECT @@ROWCOUNT AS AFFECTEDROWS',
';'
]);
}
showIndexesQuery(tableName) {
......@@ -718,20 +745,19 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator {
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;
return Utils.joinSQLFragments([
this._getForeignKeysQueryPrefix(),
'WHERE',
`TB.NAME =${wrapSingleQuote(tableName)}`,
'AND',
`COL.NAME =${wrapSingleQuote(attributeName)}`,
table.schema && `AND SCHEMA_NAME(TB.SCHEMA_ID) =${wrapSingleQuote(table.schema)}`
]);
}
getPrimaryKeyConstraintQuery(table, attributeName) {
const tableName = wrapSingleQuote(table.tableName || table);
return [
return Utils.joinSQLFragments([
'SELECT K.TABLE_NAME AS tableName,',
'K.COLUMN_NAME AS columnName,',
'K.CONSTRAINT_NAME AS constraintName',
......@@ -743,24 +769,39 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator {
'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(' ');
`AND K.TABLE_NAME = ${tableName}`,
';'
]);
}
dropForeignKeyQuery(tableName, foreignKey) {
return `ALTER TABLE ${this.quoteTable(tableName)} DROP ${this.quoteIdentifier(foreignKey)}`;
return Utils.joinSQLFragments([
'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'));`;
return Utils.joinSQLFragments([
'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)};`;
return Utils.joinSQLFragments([
'ALTER TABLE',
this.quoteTable(tableName),
'DROP CONSTRAINT',
this.quoteIdentifier(constraintName),
';'
]);
}
setIsolationLevelQuery() {
......@@ -796,60 +837,64 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator {
}
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} `;
const dbVersion = this.sequelize.options.databaseVersion;
const isSQLServer2008 = semver.valid(dbVersion) && semver.lt(dbVersion, '11.0.0');
if (isSQLServer2008 && options.offset) {
// 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 offset = options.offset || 0;
const isSubQuery = options.hasIncludeWhere || options.hasIncludeRequired || options.hasMultiAssociation;
let orders = { mainQueryOrder: [] };
if (options.order) {
orders = this.getQueryOrders(options, model, isSubQuery);
}
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;
if (orders.mainQueryOrder.length === 0) {
orders.mainQueryOrder.push(this.quoteIdentifier(model.primaryKeyField));
}
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;
const tmpTable = mainTableAs || 'OffsetTable';
return Utils.joinSQLFragments([
'SELECT TOP 100 PERCENT',
attributes.join(', '),
'FROM (',
[
'SELECT',
options.limit && `TOP ${options.limit}`,
'* FROM (',
[
'SELECT ROW_NUMBER() OVER (',
[
'ORDER BY',
orders.mainQueryOrder.join(', ')
],
`) as row_num, * FROM ${tables} AS ${tmpTable}`,
where && `WHERE ${where}`
],
`) AS ${tmpTable} WHERE row_num > ${offset}`
],
`) AS ${tmpTable}`
]);
}
return Utils.joinSQLFragments([
'SELECT',
isSQLServer2008 && options.limit && `TOP ${options.limit}`,
attributes.join(', '),
`FROM ${tables}`,
mainTableAs && `AS ${mainTableAs}`,
options.tableHint && TableHints[options.tableHint] && `WITH (${TableHints[options.tableHint]})`
]);
}
addLimitAndOffset(options, model) {
......
......@@ -50,7 +50,7 @@ module.exports = BaseTypes => {
class DATE extends BaseTypes.DATE {
toSql() {
return `DATETIME${this._length ? `(${this._length})` : ''}`;
return this._length ? `DATETIME(${this._length})` : 'DATETIME';
}
_stringify(date, options) {
date = this._applyTimezone(date, options);
......
......@@ -7,21 +7,23 @@ 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 JSON_FUNCTION_REGEX = /^\s*((?:[a-z]+_){0,2}jsonb?(?:_[a-z]+){0,2})\([^)]*\)/i;
const JSON_OPERATOR_REGEX = /^\s*(->>?|@>|<@|\?[|&]?|\|{2}|#-)/i;
const TOKEN_CAPTURE_REGEX = /^\s*((?:([`"'])(?:(?!\2).|\2{2})*\2)|[\w\d\s]+|[().,;+-])/i;
const FOREIGN_KEY_FIELDS = [
'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'
].join(',');
const typeWithoutDefault = new Set(['BLOB', 'TEXT', 'GEOMETRY', 'JSON']);
......@@ -41,15 +43,17 @@ class MySQLQueryGenerator extends AbstractQueryGenerator {
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()};`;
return Utils.joinSQLFragments([
'CREATE DATABASE IF NOT EXISTS',
this.quoteIdentifier(databaseName),
options.charset && `DEFAULT CHARACTER SET ${this.escape(options.charset)}`,
options.collate && `DEFAULT COLLATE ${this.escape(options.collate)}`,
';'
]);
}
dropDatabaseQuery(databaseName) {
return `DROP DATABASE IF EXISTS ${this.quoteIdentifier(databaseName).trim()};`;
return `DROP DATABASE IF EXISTS ${this.quoteIdentifier(databaseName)};`;
}
createSchema() {
......@@ -103,12 +107,6 @@ class MySQLQueryGenerator extends AbstractQueryGenerator {
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) {
......@@ -132,7 +130,18 @@ class MySQLQueryGenerator extends AbstractQueryGenerator {
}
}
return `CREATE TABLE IF NOT EXISTS ${table} (${attributesClause}) ENGINE=${engine}${comment}${charset}${collation}${initialAutoIncrement}${rowFormat};`;
return Utils.joinSQLFragments([
'CREATE TABLE IF NOT EXISTS',
table,
`(${attributesClause})`,
`ENGINE=${options.engine}`,
options.comment && typeof options.comment === 'string' && `COMMENT ${this.escape(options.comment)}`,
options.charset && `DEFAULT CHARSET=${options.charset}`,
options.collate && `COLLATE ${options.collate}`,
options.initialAutoIncrement && `AUTO_INCREMENT=${options.initialAutoIncrement}`,
options.rowFormat && `ROW_FORMAT=${options.rowFormat}`,
';'
]);
}
......@@ -159,17 +168,28 @@ class MySQLQueryGenerator extends AbstractQueryGenerator {
}
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};`;
return Utils.joinSQLFragments([
'ALTER TABLE',
this.quoteTable(table),
'ADD',
this.quoteIdentifier(key),
this.attributeToSQL(dataType, {
context: 'addColumn',
tableName: table,
foreignKey: key
}),
';'
]);
}
removeColumnQuery(tableName, attributeName) {
return `ALTER TABLE ${this.quoteTable(tableName)} DROP ${this.quoteIdentifier(attributeName)};`;
return Utils.joinSQLFragments([
'ALTER TABLE',
this.quoteTable(tableName),
'DROP',
this.quoteIdentifier(attributeName),
';'
]);
}
changeColumnQuery(tableName, attributes) {
......@@ -187,16 +207,13 @@ class MySQLQueryGenerator extends AbstractQueryGenerator {
}
}
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};`;
return Utils.joinSQLFragments([
'ALTER TABLE',
this.quoteTable(tableName),
attrString.length && `CHANGE ${attrString.join(', ')}`,
constraintString.length && `ADD ${constraintString.join(', ')}`,
';'
]);
}
renameColumnQuery(tableName, attrBefore, attributes) {
......@@ -207,7 +224,13 @@ class MySQLQueryGenerator extends AbstractQueryGenerator {
attrString.push(`\`${attrBefore}\` \`${attrName}\` ${definition}`);
}
return `ALTER TABLE ${this.quoteTable(tableName)} CHANGE ${attrString.join(', ')};`;
return Utils.joinSQLFragments([
'ALTER TABLE',
this.quoteTable(tableName),
'CHANGE',
attrString.join(', '),
';'
]);
}
handleSequelizeMethod(smth, tableName, factory, options, prepend) {
......@@ -300,14 +323,17 @@ class MySQLQueryGenerator extends AbstractQueryGenerator {
}
showIndexesQuery(tableName, options) {
return `SHOW INDEX FROM ${this.quoteTable(tableName)}${(options || {}).database ? ` FROM \`${options.database}\`` : ''}`;
return Utils.joinSQLFragments([
`SHOW INDEX FROM ${this.quoteTable(tableName)}`,
options && options.database && `FROM \`${options.database}\``
]);
}
showConstraintsQuery(table, constraintName) {
const tableName = table.tableName || table;
const schemaName = table.schema;
let sql = [
return Utils.joinSQLFragments([
'SELECT CONSTRAINT_CATALOG AS constraintCatalog,',
'CONSTRAINT_NAME AS constraintName,',
'CONSTRAINT_SCHEMA AS constraintSchema,',
......@@ -315,18 +341,11 @@ class MySQLQueryGenerator extends AbstractQueryGenerator {
'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};`;
`WHERE table_name='${tableName}'`,
constraintName && `AND constraint_name = '${constraintName}'`,
schemaName && `AND TABLE_SCHEMA = '${schemaName}'`,
';'
]);
}
removeIndexQuery(tableName, indexNameOrAttributes) {
......@@ -336,7 +355,12 @@ class MySQLQueryGenerator extends AbstractQueryGenerator {
indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`);
}
return `DROP INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteTable(tableName)}`;
return Utils.joinSQLFragments([
'DROP INDEX',
this.quoteIdentifier(indexName),
'ON',
this.quoteTable(tableName)
]);
}
attributeToSQL(attribute, options) {
......@@ -444,21 +468,21 @@ class MySQLQueryGenerator extends AbstractQueryGenerator {
while (currentIndex < stmt.length) {
const string = stmt.substr(currentIndex);
const functionMatches = jsonFunctionRegex.exec(string);
const functionMatches = JSON_FUNCTION_REGEX.exec(string);
if (functionMatches) {
currentIndex += functionMatches[0].indexOf('(');
hasJsonFunction = true;
continue;
}
const operatorMatches = jsonOperatorRegex.exec(string);
const operatorMatches = JSON_OPERATOR_REGEX.exec(string);
if (operatorMatches) {
currentIndex += operatorMatches[0].length;
hasJsonFunction = true;
continue;
}
const tokenMatches = tokenCaptureRegex.exec(string);
const tokenMatches = TOKEN_CAPTURE_REGEX.exec(string);
if (tokenMatches) {
const capturedToken = tokenMatches[1];
if (capturedToken === '(') {
......@@ -495,7 +519,14 @@ class MySQLQueryGenerator extends AbstractQueryGenerator {
*/
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;`;
return Utils.joinSQLFragments([
'SELECT',
FOREIGN_KEY_FIELDS,
`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',
';'
]);
}
/**
......@@ -511,12 +542,25 @@ class MySQLQueryGenerator extends AbstractQueryGenerator {
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)`;
return Utils.joinSQLFragments([
'SELECT',
FOREIGN_KEY_FIELDS,
'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'
],
')'
]);
}
/**
......@@ -528,8 +572,13 @@ class MySQLQueryGenerator extends AbstractQueryGenerator {
* @private
*/
dropForeignKeyQuery(tableName, foreignKey) {
return `ALTER TABLE ${this.quoteTable(tableName)}
DROP FOREIGN KEY ${this.quoteIdentifier(foreignKey)};`;
return Utils.joinSQLFragments([
'ALTER TABLE',
this.quoteTable(tableName),
'DROP FOREIGN KEY',
this.quoteIdentifier(foreignKey),
';'
]);
}
}
......
......@@ -12,6 +12,7 @@ const operatorsSet = new Set(_.values(operators));
let inflection = require('inflection');
exports.classToInvokable = require('./utils/classToInvokable').classToInvokable;
exports.joinSQLFragments = require('./utils/join-sql-fragments').joinSQLFragments;
exports.Promise = Promise;
......
'use strict';
function doesNotWantLeadingSpace(str) {
return /^[;,)]/.test(str);
}
function doesNotWantTrailingSpace(str) {
return /\($/.test(str);
}
/**
* Joins an array of strings with a single space between them,
* except for:
*
* - Strings starting with ';', ',' and ')', which do not get a leading space.
* - Strings ending with '(', which do not get a trailing space.
*
* @param {string[]} parts
* @returns {string}
* @private
*/
function singleSpaceJoinHelper(parts) {
return parts.reduce(({ skipNextLeadingSpace, result }, part) => {
if (skipNextLeadingSpace || doesNotWantLeadingSpace(part)) {
result += part.trim();
} else {
result += ` ${part.trim()}`;
}
return {
skipNextLeadingSpace: doesNotWantTrailingSpace(part),
result
};
}, {
skipNextLeadingSpace: true,
result: ''
}).result;
}
/**
* Joins an array with a single space, auto trimming when needed.
*
* Certain elements do not get leading/trailing spaces.
*
* @param {any[]} array The array to be joined. Falsy values are skipped. If an
* element is another array, this function will be called recursively on that array.
* Otherwise, if a non-string, non-falsy value is present, a TypeError will be thrown.
*
* @returns {string} The joined string.
*
* @private
*/
function joinSQLFragments(array) {
if (array.length === 0) return '';
// Skip falsy fragments
array = array.filter(x => x);
// Resolve recursive calls
array = array.map(fragment => {
if (Array.isArray(fragment)) {
return joinSQLFragments(fragment);
}
return fragment;
});
// Ensure strings
for (const fragment of array) {
if (fragment && typeof fragment !== 'string') {
const error = new TypeError(`Tried to construct a SQL string with a non-string, non-falsy fragment (${fragment}).`);
error.args = array;
error.fragment = fragment;
throw error;
}
}
// Trim fragments
array = array.map(x => x.trim());
// Skip full-whitespace fragments (empty after the above trim)
array = array.filter(x => x !== '');
return singleSpaceJoinHelper(array);
}
exports.joinSQLFragments = joinSQLFragments;
......@@ -71,11 +71,11 @@ if (dialect === 'mariadb') {
},
{
arguments: [{ skip: ['test'] }],
expectation: 'SELECT SCHEMA_NAME as schema_name FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN (\'MYSQL\', \'INFORMATION_SCHEMA\', \'PERFORMANCE_SCHEMA\',\'test\');'
expectation: 'SELECT SCHEMA_NAME as schema_name FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN (\'MYSQL\', \'INFORMATION_SCHEMA\', \'PERFORMANCE_SCHEMA\', \'test\');'
},
{
arguments: [{ skip: ['test', 'Te\'st2'] }],
expectation: 'SELECT SCHEMA_NAME as schema_name FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN (\'MYSQL\', \'INFORMATION_SCHEMA\', \'PERFORMANCE_SCHEMA\',\'test\',\'Te\\\'st2\');'
expectation: 'SELECT SCHEMA_NAME as schema_name FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN (\'MYSQL\', \'INFORMATION_SCHEMA\', \'PERFORMANCE_SCHEMA\', \'test\', \'Te\\\'st2\');'
}
],
......
......@@ -136,12 +136,12 @@ if (current.dialect.name === 'mssql') {
// With offset
expectsql(modifiedGen.selectFromTableFragment({ offset: 10 }, { primaryKeyField: 'id' }, ['id', 'name'], 'myTable', 'myOtherName'), {
mssql: 'SELECT TOP 100 PERCENT id, name FROM (SELECT * FROM (SELECT ROW_NUMBER() OVER (ORDER BY [id]) as row_num, * FROM myTable AS myOtherName) AS myOtherName WHERE row_num > 10) AS myOtherName'
mssql: 'SELECT TOP 100 PERCENT id, name FROM (SELECT * FROM (SELECT ROW_NUMBER() OVER (ORDER BY [id]) as row_num, * FROM myTable AS myOtherName) AS myOtherName WHERE row_num > 10) AS myOtherName'
});
// With both limit and offset
expectsql(modifiedGen.selectFromTableFragment({ limit: 10, offset: 10 }, { primaryKeyField: 'id' }, ['id', 'name'], 'myTable', 'myOtherName'), {
mssql: 'SELECT TOP 100 PERCENT id, name FROM (SELECT TOP 10 * FROM (SELECT ROW_NUMBER() OVER (ORDER BY [id]) as row_num, * FROM myTable AS myOtherName) AS myOtherName WHERE row_num > 10) AS myOtherName'
mssql: 'SELECT TOP 100 PERCENT id, name FROM (SELECT TOP 10 * FROM (SELECT ROW_NUMBER() OVER (ORDER BY [id]) as row_num, * FROM myTable AS myOtherName) AS myOtherName WHERE row_num > 10) AS myOtherName'
});
});
......
......@@ -56,7 +56,7 @@ if (dialect.startsWith('postgres')) {
{
title: 'Should use the plus operator',
arguments: ['+', 'myTable', {}, { foo: 'bar' }, {}, {}],
expectation: 'UPDATE "myTable" SET "foo"="foo"+ \'bar\' RETURNING *'
expectation: 'UPDATE "myTable" SET "foo"="foo"+ \'bar\' RETURNING *'
},
{
title: 'Should use the plus operator with where clause',
......@@ -71,12 +71,12 @@ if (dialect.startsWith('postgres')) {
{
title: 'Should use the minus operator',
arguments: ['-', 'myTable', {}, { foo: 'bar' }, {}, {}],
expectation: 'UPDATE "myTable" SET "foo"="foo"- \'bar\' RETURNING *'
expectation: 'UPDATE "myTable" SET "foo"="foo"- \'bar\' RETURNING *'
},
{
title: 'Should use the minus operator with negative value',
arguments: ['-', 'myTable', {}, { foo: -1 }, {}, {}],
expectation: 'UPDATE "myTable" SET "foo"="foo"- -1 RETURNING *'
expectation: 'UPDATE "myTable" SET "foo"="foo"- -1 RETURNING *'
},
{
title: 'Should use the minus operator with where clause',
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!