query-generator.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. 'use strict';
  2. const _ = require('lodash');
  3. const Utils = require('../../utils');
  4. const AbstractQueryGenerator = require('../abstract/query-generator');
  5. const util = require('util');
  6. const Op = require('../../operators');
  7. const jsonFunctionRegex = /^\s*((?:[a-z]+_){0,2}jsonb?(?:_[a-z]+){0,2})\([^)]*\)/i;
  8. const jsonOperatorRegex = /^\s*(->>?|@>|<@|\?[|&]?|\|{2}|#-)/i;
  9. const tokenCaptureRegex = /^\s*((?:([`"'])(?:(?!\2).|\2{2})*\2)|[\w\d\s]+|[().,;+-])/i;
  10. const foreignKeyFields = 'CONSTRAINT_NAME as constraint_name,'
  11. + 'CONSTRAINT_NAME as constraintName,'
  12. + 'CONSTRAINT_SCHEMA as constraintSchema,'
  13. + 'CONSTRAINT_SCHEMA as constraintCatalog,'
  14. + 'TABLE_NAME as tableName,'
  15. + 'TABLE_SCHEMA as tableSchema,'
  16. + 'TABLE_SCHEMA as tableCatalog,'
  17. + 'COLUMN_NAME as columnName,'
  18. + 'REFERENCED_TABLE_SCHEMA as referencedTableSchema,'
  19. + 'REFERENCED_TABLE_SCHEMA as referencedTableCatalog,'
  20. + 'REFERENCED_TABLE_NAME as referencedTableName,'
  21. + 'REFERENCED_COLUMN_NAME as referencedColumnName';
  22. const typeWithoutDefault = new Set(['BLOB', 'TEXT', 'GEOMETRY', 'JSON']);
  23. class MySQLQueryGenerator extends AbstractQueryGenerator {
  24. constructor(options) {
  25. super(options);
  26. this.OperatorMap = Object.assign({}, this.OperatorMap, {
  27. [Op.regexp]: 'REGEXP',
  28. [Op.notRegexp]: 'NOT REGEXP'
  29. });
  30. }
  31. createDatabaseQuery(databaseName, options) {
  32. options = Object.assign({
  33. charset: null,
  34. collate: null
  35. }, options || {});
  36. const database = this.quoteIdentifier(databaseName);
  37. const charset = options.charset ? ` DEFAULT CHARACTER SET ${this.escape(options.charset)}` : '';
  38. const collate = options.collate ? ` DEFAULT COLLATE ${this.escape(options.collate)}` : '';
  39. return `${`CREATE DATABASE IF NOT EXISTS ${database}${charset}${collate}`.trim()};`;
  40. }
  41. dropDatabaseQuery(databaseName) {
  42. return `DROP DATABASE IF EXISTS ${this.quoteIdentifier(databaseName).trim()};`;
  43. }
  44. createSchema() {
  45. return 'SHOW TABLES';
  46. }
  47. showSchemasQuery() {
  48. return 'SHOW TABLES';
  49. }
  50. versionQuery() {
  51. return 'SELECT VERSION() as `version`';
  52. }
  53. createTableQuery(tableName, attributes, options) {
  54. options = Object.assign({
  55. engine: 'InnoDB',
  56. charset: null,
  57. rowFormat: null
  58. }, options || {});
  59. const primaryKeys = [];
  60. const foreignKeys = {};
  61. const attrStr = [];
  62. for (const attr in attributes) {
  63. if (!Object.prototype.hasOwnProperty.call(attributes, attr)) continue;
  64. const dataType = attributes[attr];
  65. let match;
  66. if (dataType.includes('PRIMARY KEY')) {
  67. primaryKeys.push(attr);
  68. if (dataType.includes('REFERENCES')) {
  69. // MySQL doesn't support inline REFERENCES declarations: move to the end
  70. match = dataType.match(/^(.+) (REFERENCES.*)$/);
  71. attrStr.push(`${this.quoteIdentifier(attr)} ${match[1].replace('PRIMARY KEY', '')}`);
  72. foreignKeys[attr] = match[2];
  73. } else {
  74. attrStr.push(`${this.quoteIdentifier(attr)} ${dataType.replace('PRIMARY KEY', '')}`);
  75. }
  76. } else if (dataType.includes('REFERENCES')) {
  77. // MySQL doesn't support inline REFERENCES declarations: move to the end
  78. match = dataType.match(/^(.+) (REFERENCES.*)$/);
  79. attrStr.push(`${this.quoteIdentifier(attr)} ${match[1]}`);
  80. foreignKeys[attr] = match[2];
  81. } else {
  82. attrStr.push(`${this.quoteIdentifier(attr)} ${dataType}`);
  83. }
  84. }
  85. const table = this.quoteTable(tableName);
  86. let attributesClause = attrStr.join(', ');
  87. const comment = options.comment && typeof options.comment === 'string' ? ` COMMENT ${this.escape(options.comment)}` : '';
  88. const engine = options.engine;
  89. const charset = options.charset ? ` DEFAULT CHARSET=${options.charset}` : '';
  90. const collation = options.collate ? ` COLLATE ${options.collate}` : '';
  91. const rowFormat = options.rowFormat ? ` ROW_FORMAT=${options.rowFormat}` : '';
  92. const initialAutoIncrement = options.initialAutoIncrement ? ` AUTO_INCREMENT=${options.initialAutoIncrement}` : '';
  93. const pkString = primaryKeys.map(pk => this.quoteIdentifier(pk)).join(', ');
  94. if (options.uniqueKeys) {
  95. _.each(options.uniqueKeys, (columns, indexName) => {
  96. if (columns.customIndex) {
  97. if (typeof indexName !== 'string') {
  98. indexName = `uniq_${tableName}_${columns.fields.join('_')}`;
  99. }
  100. attributesClause += `, UNIQUE ${this.quoteIdentifier(indexName)} (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
  101. }
  102. });
  103. }
  104. if (pkString.length > 0) {
  105. attributesClause += `, PRIMARY KEY (${pkString})`;
  106. }
  107. for (const fkey in foreignKeys) {
  108. if (Object.prototype.hasOwnProperty.call(foreignKeys, fkey)) {
  109. attributesClause += `, FOREIGN KEY (${this.quoteIdentifier(fkey)}) ${foreignKeys[fkey]}`;
  110. }
  111. }
  112. return `CREATE TABLE IF NOT EXISTS ${table} (${attributesClause}) ENGINE=${engine}${comment}${charset}${collation}${initialAutoIncrement}${rowFormat};`;
  113. }
  114. describeTableQuery(tableName, schema, schemaDelimiter) {
  115. const table = this.quoteTable(
  116. this.addSchema({
  117. tableName,
  118. _schema: schema,
  119. _schemaDelimiter: schemaDelimiter
  120. })
  121. );
  122. return `SHOW FULL COLUMNS FROM ${table};`;
  123. }
  124. showTablesQuery(database) {
  125. let query = 'SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = \'BASE TABLE\'';
  126. if (database) {
  127. query += ` AND TABLE_SCHEMA = ${this.escape(database)}`;
  128. } else {
  129. query += ' AND TABLE_SCHEMA NOT IN (\'MYSQL\', \'INFORMATION_SCHEMA\', \'PERFORMANCE_SCHEMA\', \'SYS\')';
  130. }
  131. return `${query};`;
  132. }
  133. addColumnQuery(table, key, dataType) {
  134. const definition = this.attributeToSQL(dataType, {
  135. context: 'addColumn',
  136. tableName: table,
  137. foreignKey: key
  138. });
  139. return `ALTER TABLE ${this.quoteTable(table)} ADD ${this.quoteIdentifier(key)} ${definition};`;
  140. }
  141. removeColumnQuery(tableName, attributeName) {
  142. return `ALTER TABLE ${this.quoteTable(tableName)} DROP ${this.quoteIdentifier(attributeName)};`;
  143. }
  144. changeColumnQuery(tableName, attributes) {
  145. const attrString = [];
  146. const constraintString = [];
  147. for (const attributeName in attributes) {
  148. let definition = attributes[attributeName];
  149. if (definition.includes('REFERENCES')) {
  150. const attrName = this.quoteIdentifier(attributeName);
  151. definition = definition.replace(/.+?(?=REFERENCES)/, '');
  152. constraintString.push(`FOREIGN KEY (${attrName}) ${definition}`);
  153. } else {
  154. attrString.push(`\`${attributeName}\` \`${attributeName}\` ${definition}`);
  155. }
  156. }
  157. let finalQuery = '';
  158. if (attrString.length) {
  159. finalQuery += `CHANGE ${attrString.join(', ')}`;
  160. finalQuery += constraintString.length ? ' ' : '';
  161. }
  162. if (constraintString.length) {
  163. finalQuery += `ADD ${constraintString.join(', ')}`;
  164. }
  165. return `ALTER TABLE ${this.quoteTable(tableName)} ${finalQuery};`;
  166. }
  167. renameColumnQuery(tableName, attrBefore, attributes) {
  168. const attrString = [];
  169. for (const attrName in attributes) {
  170. const definition = attributes[attrName];
  171. attrString.push(`\`${attrBefore}\` \`${attrName}\` ${definition}`);
  172. }
  173. return `ALTER TABLE ${this.quoteTable(tableName)} CHANGE ${attrString.join(', ')};`;
  174. }
  175. handleSequelizeMethod(smth, tableName, factory, options, prepend) {
  176. if (smth instanceof Utils.Json) {
  177. // Parse nested object
  178. if (smth.conditions) {
  179. const conditions = this.parseConditionObject(smth.conditions).map(condition =>
  180. `${this.jsonPathExtractionQuery(condition.path[0], _.tail(condition.path))} = '${condition.value}'`
  181. );
  182. return conditions.join(' AND ');
  183. }
  184. if (smth.path) {
  185. let str;
  186. // Allow specifying conditions using the sqlite json functions
  187. if (this._checkValidJsonStatement(smth.path)) {
  188. str = smth.path;
  189. } else {
  190. // Also support json property accessors
  191. const paths = _.toPath(smth.path);
  192. const column = paths.shift();
  193. str = this.jsonPathExtractionQuery(column, paths);
  194. }
  195. if (smth.value) {
  196. str += util.format(' = %s', this.escape(smth.value));
  197. }
  198. return str;
  199. }
  200. } else if (smth instanceof Utils.Cast) {
  201. if (/timestamp/i.test(smth.type)) {
  202. smth.type = 'datetime';
  203. } else if (smth.json && /boolean/i.test(smth.type)) {
  204. // true or false cannot be casted as booleans within a JSON structure
  205. smth.type = 'char';
  206. } else if (/double precision/i.test(smth.type) || /boolean/i.test(smth.type) || /integer/i.test(smth.type)) {
  207. smth.type = 'decimal';
  208. } else if (/text/i.test(smth.type)) {
  209. smth.type = 'char';
  210. }
  211. }
  212. return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
  213. }
  214. _toJSONValue(value) {
  215. // true/false are stored as strings in mysql
  216. if (typeof value === 'boolean') {
  217. return value.toString();
  218. }
  219. // null is stored as a string in mysql
  220. if (value === null) {
  221. return 'null';
  222. }
  223. return value;
  224. }
  225. upsertQuery(tableName, insertValues, updateValues, where, model, options) {
  226. options.onDuplicate = 'UPDATE ';
  227. options.onDuplicate += Object.keys(updateValues).map(key => {
  228. key = this.quoteIdentifier(key);
  229. return `${key}=VALUES(${key})`;
  230. }).join(', ');
  231. return this.insertQuery(tableName, insertValues, model.rawAttributes, options);
  232. }
  233. truncateTableQuery(tableName) {
  234. return `TRUNCATE ${this.quoteTable(tableName)}`;
  235. }
  236. deleteQuery(tableName, where, options = {}, model) {
  237. let limit = '';
  238. let query = `DELETE FROM ${this.quoteTable(tableName)}`;
  239. if (options.limit) {
  240. limit = ` LIMIT ${this.escape(options.limit)}`;
  241. }
  242. where = this.getWhereConditions(where, null, model, options);
  243. if (where) {
  244. query += ` WHERE ${where}`;
  245. }
  246. return query + limit;
  247. }
  248. showIndexesQuery(tableName, options) {
  249. return `SHOW INDEX FROM ${this.quoteTable(tableName)}${(options || {}).database ? ` FROM \`${options.database}\`` : ''}`;
  250. }
  251. showConstraintsQuery(table, constraintName) {
  252. const tableName = table.tableName || table;
  253. const schemaName = table.schema;
  254. let sql = [
  255. 'SELECT CONSTRAINT_CATALOG AS constraintCatalog,',
  256. 'CONSTRAINT_NAME AS constraintName,',
  257. 'CONSTRAINT_SCHEMA AS constraintSchema,',
  258. 'CONSTRAINT_TYPE AS constraintType,',
  259. 'TABLE_NAME AS tableName,',
  260. 'TABLE_SCHEMA AS tableSchema',
  261. 'from INFORMATION_SCHEMA.TABLE_CONSTRAINTS',
  262. `WHERE table_name='${tableName}'`
  263. ].join(' ');
  264. if (constraintName) {
  265. sql += ` AND constraint_name = '${constraintName}'`;
  266. }
  267. if (schemaName) {
  268. sql += ` AND TABLE_SCHEMA = '${schemaName}'`;
  269. }
  270. return `${sql};`;
  271. }
  272. removeIndexQuery(tableName, indexNameOrAttributes) {
  273. let indexName = indexNameOrAttributes;
  274. if (typeof indexName !== 'string') {
  275. indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`);
  276. }
  277. return `DROP INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteTable(tableName)}`;
  278. }
  279. attributeToSQL(attribute, options) {
  280. if (!_.isPlainObject(attribute)) {
  281. attribute = {
  282. type: attribute
  283. };
  284. }
  285. const attributeString = attribute.type.toString({ escape: this.escape.bind(this) });
  286. let template = attributeString;
  287. if (attribute.allowNull === false) {
  288. template += ' NOT NULL';
  289. }
  290. if (attribute.autoIncrement) {
  291. template += ' auto_increment';
  292. }
  293. // BLOB/TEXT/GEOMETRY/JSON cannot have a default value
  294. if (!typeWithoutDefault.has(attributeString)
  295. && attribute.type._binary !== true
  296. && Utils.defaultValueSchemable(attribute.defaultValue)) {
  297. template += ` DEFAULT ${this.escape(attribute.defaultValue)}`;
  298. }
  299. if (attribute.unique === true) {
  300. template += ' UNIQUE';
  301. }
  302. if (attribute.primaryKey) {
  303. template += ' PRIMARY KEY';
  304. }
  305. if (attribute.comment) {
  306. template += ` COMMENT ${this.escape(attribute.comment)}`;
  307. }
  308. if (attribute.first) {
  309. template += ' FIRST';
  310. }
  311. if (attribute.after) {
  312. template += ` AFTER ${this.quoteIdentifier(attribute.after)}`;
  313. }
  314. if (attribute.references) {
  315. if (options && options.context === 'addColumn' && options.foreignKey) {
  316. const attrName = this.quoteIdentifier(options.foreignKey);
  317. const fkName = this.quoteIdentifier(`${options.tableName}_${attrName}_foreign_idx`);
  318. template += `, ADD CONSTRAINT ${fkName} FOREIGN KEY (${attrName})`;
  319. }
  320. template += ` REFERENCES ${this.quoteTable(attribute.references.model)}`;
  321. if (attribute.references.key) {
  322. template += ` (${this.quoteIdentifier(attribute.references.key)})`;
  323. } else {
  324. template += ` (${this.quoteIdentifier('id')})`;
  325. }
  326. if (attribute.onDelete) {
  327. template += ` ON DELETE ${attribute.onDelete.toUpperCase()}`;
  328. }
  329. if (attribute.onUpdate) {
  330. template += ` ON UPDATE ${attribute.onUpdate.toUpperCase()}`;
  331. }
  332. }
  333. return template;
  334. }
  335. attributesToSQL(attributes, options) {
  336. const result = {};
  337. for (const key in attributes) {
  338. const attribute = attributes[key];
  339. result[attribute.field || key] = this.attributeToSQL(attribute, options);
  340. }
  341. return result;
  342. }
  343. /**
  344. * Check whether the statmement is json function or simple path
  345. *
  346. * @param {string} stmt The statement to validate
  347. * @returns {boolean} true if the given statement is json function
  348. * @throws {Error} throw if the statement looks like json function but has invalid token
  349. * @private
  350. */
  351. _checkValidJsonStatement(stmt) {
  352. if (typeof stmt !== 'string') {
  353. return false;
  354. }
  355. let currentIndex = 0;
  356. let openingBrackets = 0;
  357. let closingBrackets = 0;
  358. let hasJsonFunction = false;
  359. let hasInvalidToken = false;
  360. while (currentIndex < stmt.length) {
  361. const string = stmt.substr(currentIndex);
  362. const functionMatches = jsonFunctionRegex.exec(string);
  363. if (functionMatches) {
  364. currentIndex += functionMatches[0].indexOf('(');
  365. hasJsonFunction = true;
  366. continue;
  367. }
  368. const operatorMatches = jsonOperatorRegex.exec(string);
  369. if (operatorMatches) {
  370. currentIndex += operatorMatches[0].length;
  371. hasJsonFunction = true;
  372. continue;
  373. }
  374. const tokenMatches = tokenCaptureRegex.exec(string);
  375. if (tokenMatches) {
  376. const capturedToken = tokenMatches[1];
  377. if (capturedToken === '(') {
  378. openingBrackets++;
  379. } else if (capturedToken === ')') {
  380. closingBrackets++;
  381. } else if (capturedToken === ';') {
  382. hasInvalidToken = true;
  383. break;
  384. }
  385. currentIndex += tokenMatches[0].length;
  386. continue;
  387. }
  388. break;
  389. }
  390. // Check invalid json statement
  391. if (hasJsonFunction && (hasInvalidToken || openingBrackets !== closingBrackets)) {
  392. throw new Error(`Invalid json statement: ${stmt}`);
  393. }
  394. // return true if the statement has valid json function
  395. return hasJsonFunction;
  396. }
  397. /**
  398. * Generates an SQL query that returns all foreign keys of a table.
  399. *
  400. * @param {Object} table The table.
  401. * @param {string} schemaName The name of the schema.
  402. * @returns {string} The generated sql query.
  403. * @private
  404. */
  405. getForeignKeysQuery(table, schemaName) {
  406. const tableName = table.tableName || table;
  407. 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;`;
  408. }
  409. /**
  410. * Generates an SQL query that returns the foreign key constraint of a given column.
  411. *
  412. * @param {Object} table The table.
  413. * @param {string} columnName The name of the column.
  414. * @returns {string} The generated sql query.
  415. * @private
  416. */
  417. getForeignKeyQuery(table, columnName) {
  418. const quotedSchemaName = table.schema ? wrapSingleQuote(table.schema) : '';
  419. const quotedTableName = wrapSingleQuote(table.tableName || table);
  420. const quotedColumnName = wrapSingleQuote(columnName);
  421. return `SELECT ${foreignKeyFields} FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE`
  422. + ` WHERE (REFERENCED_TABLE_NAME = ${quotedTableName}${table.schema
  423. ? ` AND REFERENCED_TABLE_SCHEMA = ${quotedSchemaName}`
  424. : ''} AND REFERENCED_COLUMN_NAME = ${quotedColumnName})`
  425. + ` OR (TABLE_NAME = ${quotedTableName}${table.schema ?
  426. ` AND TABLE_SCHEMA = ${quotedSchemaName}` : ''} AND COLUMN_NAME = ${quotedColumnName} AND REFERENCED_TABLE_NAME IS NOT NULL)`;
  427. }
  428. /**
  429. * Generates an SQL query that removes a foreign key from a table.
  430. *
  431. * @param {string} tableName The name of the table.
  432. * @param {string} foreignKey The name of the foreign key constraint.
  433. * @returns {string} The generated sql query.
  434. * @private
  435. */
  436. dropForeignKeyQuery(tableName, foreignKey) {
  437. return `ALTER TABLE ${this.quoteTable(tableName)}
  438. DROP FOREIGN KEY ${this.quoteIdentifier(foreignKey)};`;
  439. }
  440. }
  441. // private methods
  442. function wrapSingleQuote(identifier) {
  443. return Utils.addTicks(identifier, '\'');
  444. }
  445. module.exports = MySQLQueryGenerator;