lib/dialects/mysql/query-generator.js
'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;