You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
547 lines
18 KiB
547 lines
18 KiB
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const Utils = require('../../utils');
|
|
const AbstractQueryGenerator = require('../abstract/query-generator');
|
|
const util = require('util');
|
|
const Op = require('../../operators');
|
|
|
|
const QueryGenerator = {
|
|
__proto__: AbstractQueryGenerator,
|
|
dialect: 'mysql',
|
|
|
|
OperatorMap: Object.assign({}, AbstractQueryGenerator.OperatorMap, {
|
|
[Op.regexp]: 'REGEXP',
|
|
[Op.notRegexp]: 'NOT REGEXP'
|
|
}),
|
|
|
|
createSchema() {
|
|
return 'SHOW TABLES';
|
|
},
|
|
|
|
showSchemasQuery() {
|
|
return 'SHOW TABLES';
|
|
},
|
|
|
|
versionQuery() {
|
|
return 'SELECT VERSION() as `version`';
|
|
},
|
|
|
|
createTableQuery(tableName, attributes, options) {
|
|
options = _.extend({
|
|
engine: 'InnoDB',
|
|
charset: null,
|
|
rowFormat: null
|
|
}, options || {});
|
|
|
|
const query = 'CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>) ENGINE=<%= engine %><%= comment %><%= charset %><%= collation %><%= initialAutoIncrement %><%= rowFormat %>';
|
|
const primaryKeys = [];
|
|
const foreignKeys = {};
|
|
const attrStr = [];
|
|
|
|
for (const attr in attributes) {
|
|
if (attributes.hasOwnProperty(attr)) {
|
|
const dataType = attributes[attr];
|
|
let match;
|
|
|
|
if (_.includes(dataType, 'PRIMARY KEY')) {
|
|
primaryKeys.push(attr);
|
|
|
|
if (_.includes(dataType, '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 (_.includes(dataType, '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 values = {
|
|
table: this.quoteTable(tableName),
|
|
attributes: attrStr.join(', '),
|
|
comment: options.comment && _.isString(options.comment) ? ' COMMENT ' + this.escape(options.comment) : '',
|
|
engine: options.engine,
|
|
charset: options.charset ? ' DEFAULT CHARSET=' + options.charset : '',
|
|
collation: options.collate ? ' COLLATE ' + options.collate : '',
|
|
rowFormat: options.rowFormat ? ' ROW_FORMAT=' + options.rowFormat : '',
|
|
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 (!_.isString(indexName)) {
|
|
indexName = 'uniq_' + tableName + '_' + columns.fields.join('_');
|
|
}
|
|
values.attributes += `, UNIQUE ${this.quoteIdentifier(indexName)} (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (pkString.length > 0) {
|
|
values.attributes += `, PRIMARY KEY (${pkString})`;
|
|
}
|
|
|
|
for (const fkey in foreignKeys) {
|
|
if (foreignKeys.hasOwnProperty(fkey)) {
|
|
values.attributes += ', FOREIGN KEY (' + this.quoteIdentifier(fkey) + ') ' + foreignKeys[fkey];
|
|
}
|
|
}
|
|
|
|
return _.template(query, this._templateSettings)(values).trim() + ';';
|
|
},
|
|
|
|
showTablesQuery() {
|
|
return 'SHOW TABLES;';
|
|
},
|
|
|
|
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.match(/REFERENCES/)) {
|
|
const fkName = this.quoteIdentifier(tableName + '_' + attributeName + '_foreign_idx');
|
|
const attrName = this.quoteIdentifier(attributeName);
|
|
definition = definition.replace(/.+?(?=REFERENCES)/, '');
|
|
constraintString.push(`${fkName} 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 CONSTRAINT ' + 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 = _.map(this.parseConditionObject(smth.conditions), condition =>
|
|
`${this.jsonPathExtractionQuery(condition.path[0], _.tail(condition.path))} = '${condition.value}'`
|
|
);
|
|
|
|
return conditions.join(' and ');
|
|
} else 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 dot notation
|
|
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);
|
|
},
|
|
|
|
deleteQuery(tableName, where, options, model) {
|
|
options = options || {};
|
|
|
|
const table = this.quoteTable(tableName);
|
|
if (options.truncate === true) {
|
|
// Truncate does not allow LIMIT and WHERE
|
|
return 'TRUNCATE ' + table;
|
|
}
|
|
|
|
where = this.getWhereConditions(where, null, model, options);
|
|
let limit = '';
|
|
|
|
if (_.isUndefined(options.limit)) {
|
|
options.limit = 1;
|
|
}
|
|
|
|
if (options.limit) {
|
|
limit = ' LIMIT ' + this.escape(options.limit);
|
|
}
|
|
|
|
let query = 'DELETE FROM ' + table;
|
|
if (where) query += ' WHERE ' + where;
|
|
query += limit;
|
|
|
|
return query;
|
|
},
|
|
|
|
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 (!_.includes(['BLOB', 'TEXT', 'GEOMETRY', 'JSON'], 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.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;
|
|
},
|
|
|
|
quoteIdentifier(identifier) {
|
|
if (identifier === '*') return identifier;
|
|
return Utils.addTicks(Utils.removeTicks(identifier, '`'), '`');
|
|
},
|
|
|
|
/**
|
|
* 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 (!_.isString(stmt)) {
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* Generates an SQL query that extract JSON property of given path.
|
|
*
|
|
* @param {String} column The JSON column
|
|
* @param {String|Array<String>} [path] The path to extract (optional)
|
|
* @returns {String} The generated sql query
|
|
* @private
|
|
*/
|
|
jsonPathExtractionQuery(column, path) {
|
|
/**
|
|
* Sub paths need to be quoted as ECMAScript identifiers
|
|
*
|
|
* https://bugs.mysql.com/bug.php?id=81896
|
|
*/
|
|
const paths = _.toPath(path).map(subPath => {
|
|
return /\D/.test(subPath)
|
|
? Utils.addTicks(subPath, '"')
|
|
: subPath;
|
|
});
|
|
const pathStr = this.escape(`${['$'].concat(paths).join('.').replace(/\.(\d+)(?:(?=\.)|$)/g, (__, digit) => `[${digit}]`)}`);
|
|
const quotedColumn = this.isIdentifierQuoted(column) ? column : this.quoteIdentifier(column);
|
|
return `(${quotedColumn}->>${pathStr})`;
|
|
},
|
|
|
|
/**
|
|
* Generates fields for getForeignKeysQuery
|
|
* @returns {String} fields
|
|
* @private
|
|
*/
|
|
_getForeignKeysQueryFields() {
|
|
return [
|
|
'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(',');
|
|
},
|
|
|
|
/**
|
|
* Generates an SQL query that returns all foreign keys of a table.
|
|
*
|
|
* @param {String} tableName The name of the table.
|
|
* @param {String} schemaName The name of the schema.
|
|
* @return {String} The generated sql query.
|
|
* @private
|
|
*/
|
|
getForeignKeysQuery(tableName, schemaName) {
|
|
return 'SELECT ' + this._getForeignKeysQueryFields() + ' FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE where TABLE_NAME = \'' + tableName + /* jshint ignore: line */
|
|
'\' AND CONSTRAINT_NAME!=\'PRIMARY\' AND CONSTRAINT_SCHEMA=\'' + schemaName + '\' AND REFERENCED_TABLE_NAME IS NOT NULL;'; /* jshint ignore: line */
|
|
},
|
|
|
|
/**
|
|
* Generates an SQL query that returns the foreign key constraint of a given column.
|
|
*
|
|
* @param {String} tableName The name of the table.
|
|
* @param {String} columnName The name of the column.
|
|
* @return {String} The generated sql query.
|
|
* @private
|
|
*/
|
|
getForeignKeyQuery(table, columnName) {
|
|
const tableName = table.tableName || table;
|
|
const schemaName = table.schema;
|
|
|
|
return 'SELECT ' + this._getForeignKeysQueryFields()
|
|
+ ' FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE'
|
|
+ ' WHERE (REFERENCED_TABLE_NAME = ' + wrapSingleQuote(tableName)
|
|
+ (schemaName ? ' AND REFERENCED_TABLE_SCHEMA = ' + wrapSingleQuote(schemaName): '')
|
|
+ ' AND REFERENCED_COLUMN_NAME = ' + wrapSingleQuote(columnName)
|
|
+ ') OR (TABLE_NAME = ' + wrapSingleQuote(tableName)
|
|
+ (schemaName ? ' AND TABLE_SCHEMA = ' + wrapSingleQuote(schemaName): '')
|
|
+ ' AND COLUMN_NAME = ' + wrapSingleQuote(columnName)
|
|
+ ' 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.
|
|
* @return {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 = QueryGenerator;
|
|
|