query-generator.js 17 KB

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