query-generator.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. 'use strict';
  2. const Utils = require('../../utils');
  3. const Transaction = require('../../transaction');
  4. const _ = require('lodash');
  5. const MySqlQueryGenerator = require('../mysql/query-generator');
  6. const AbstractQueryGenerator = require('../abstract/query-generator');
  7. class SQLiteQueryGenerator extends MySqlQueryGenerator {
  8. createSchema() {
  9. return "SELECT name FROM `sqlite_master` WHERE type='table' and name!='sqlite_sequence';";
  10. }
  11. showSchemasQuery() {
  12. return "SELECT name FROM `sqlite_master` WHERE type='table' and name!='sqlite_sequence';";
  13. }
  14. versionQuery() {
  15. return 'SELECT sqlite_version() as `version`';
  16. }
  17. createTableQuery(tableName, attributes, options) {
  18. options = options || {};
  19. const primaryKeys = [];
  20. const needsMultiplePrimaryKeys = _.values(attributes).filter(definition => definition.includes('PRIMARY KEY')).length > 1;
  21. const attrArray = [];
  22. for (const attr in attributes) {
  23. if (Object.prototype.hasOwnProperty.call(attributes, attr)) {
  24. const dataType = attributes[attr];
  25. const containsAutoIncrement = dataType.includes('AUTOINCREMENT');
  26. let dataTypeString = dataType;
  27. if (dataType.includes('PRIMARY KEY')) {
  28. if (dataType.includes('INT')) {
  29. // Only INTEGER is allowed for primary key, see https://github.com/sequelize/sequelize/issues/969 (no lenght, unsigned etc)
  30. dataTypeString = containsAutoIncrement ? 'INTEGER PRIMARY KEY AUTOINCREMENT' : 'INTEGER PRIMARY KEY';
  31. if (dataType.includes(' REFERENCES')) {
  32. dataTypeString += dataType.substr(dataType.indexOf(' REFERENCES'));
  33. }
  34. }
  35. if (needsMultiplePrimaryKeys) {
  36. primaryKeys.push(attr);
  37. if (dataType.includes('NOT NULL')) {
  38. dataTypeString = dataType.replace(' PRIMARY KEY', '');
  39. } else {
  40. dataTypeString = dataType.replace('PRIMARY KEY', 'NOT NULL');
  41. }
  42. }
  43. }
  44. attrArray.push(`${this.quoteIdentifier(attr)} ${dataTypeString}`);
  45. }
  46. }
  47. const table = this.quoteTable(tableName);
  48. let attrStr = attrArray.join(', ');
  49. const pkString = primaryKeys.map(pk => this.quoteIdentifier(pk)).join(', ');
  50. if (options.uniqueKeys) {
  51. _.each(options.uniqueKeys, columns => {
  52. if (columns.customIndex) {
  53. attrStr += `, UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
  54. }
  55. });
  56. }
  57. if (pkString.length > 0) {
  58. attrStr += `, PRIMARY KEY (${pkString})`;
  59. }
  60. const sql = `CREATE TABLE IF NOT EXISTS ${table} (${attrStr});`;
  61. return this.replaceBooleanDefaults(sql);
  62. }
  63. booleanValue(value) {
  64. return value ? 1 : 0;
  65. }
  66. /**
  67. * Check whether the statmement is json function or simple path
  68. *
  69. * @param {string} stmt The statement to validate
  70. * @returns {boolean} true if the given statement is json function
  71. * @throws {Error} throw if the statement looks like json function but has invalid token
  72. */
  73. _checkValidJsonStatement(stmt) {
  74. if (typeof stmt !== 'string') {
  75. return false;
  76. }
  77. // https://sqlite.org/json1.html
  78. const jsonFunctionRegex = /^\s*(json(?:_[a-z]+){0,2})\([^)]*\)/i;
  79. const tokenCaptureRegex = /^\s*((?:([`"'])(?:(?!\2).|\2{2})*\2)|[\w\d\s]+|[().,;+-])/i;
  80. let currentIndex = 0;
  81. let openingBrackets = 0;
  82. let closingBrackets = 0;
  83. let hasJsonFunction = false;
  84. let hasInvalidToken = false;
  85. while (currentIndex < stmt.length) {
  86. const string = stmt.substr(currentIndex);
  87. const functionMatches = jsonFunctionRegex.exec(string);
  88. if (functionMatches) {
  89. currentIndex += functionMatches[0].indexOf('(');
  90. hasJsonFunction = true;
  91. continue;
  92. }
  93. const tokenMatches = tokenCaptureRegex.exec(string);
  94. if (tokenMatches) {
  95. const capturedToken = tokenMatches[1];
  96. if (capturedToken === '(') {
  97. openingBrackets++;
  98. } else if (capturedToken === ')') {
  99. closingBrackets++;
  100. } else if (capturedToken === ';') {
  101. hasInvalidToken = true;
  102. break;
  103. }
  104. currentIndex += tokenMatches[0].length;
  105. continue;
  106. }
  107. break;
  108. }
  109. // Check invalid json statement
  110. hasInvalidToken |= openingBrackets !== closingBrackets;
  111. if (hasJsonFunction && hasInvalidToken) {
  112. throw new Error(`Invalid json statement: ${stmt}`);
  113. }
  114. // return true if the statement has valid json function
  115. return hasJsonFunction;
  116. }
  117. //sqlite can't cast to datetime so we need to convert date values to their ISO strings
  118. _toJSONValue(value) {
  119. if (value instanceof Date) {
  120. return value.toISOString();
  121. }
  122. if (Array.isArray(value) && value[0] instanceof Date) {
  123. return value.map(val => val.toISOString());
  124. }
  125. return value;
  126. }
  127. handleSequelizeMethod(smth, tableName, factory, options, prepend) {
  128. if (smth instanceof Utils.Json) {
  129. return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
  130. }
  131. if (smth instanceof Utils.Cast) {
  132. if (/timestamp/i.test(smth.type)) {
  133. smth.type = 'datetime';
  134. }
  135. }
  136. return AbstractQueryGenerator.prototype.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend);
  137. }
  138. addColumnQuery(table, key, dataType) {
  139. const attributes = {};
  140. attributes[key] = dataType;
  141. const fields = this.attributesToSQL(attributes, { context: 'addColumn' });
  142. const attribute = `${this.quoteIdentifier(key)} ${fields[key]}`;
  143. const sql = `ALTER TABLE ${this.quoteTable(table)} ADD ${attribute};`;
  144. return this.replaceBooleanDefaults(sql);
  145. }
  146. showTablesQuery() {
  147. return 'SELECT name FROM `sqlite_master` WHERE type=\'table\' and name!=\'sqlite_sequence\';';
  148. }
  149. upsertQuery(tableName, insertValues, updateValues, where, model, options) {
  150. options.ignoreDuplicates = true;
  151. const bind = [];
  152. const bindParam = this.bindParam(bind);
  153. const upsertOptions = _.defaults({ bindParam }, options);
  154. const insert = this.insertQuery(tableName, insertValues, model.rawAttributes, upsertOptions);
  155. const update = this.updateQuery(tableName, updateValues, where, upsertOptions, model.rawAttributes);
  156. const query = `${insert.query} ${update.query}`;
  157. return { query, bind };
  158. }
  159. updateQuery(tableName, attrValueHash, where, options, attributes) {
  160. options = options || {};
  161. _.defaults(options, this.options);
  162. attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, options.omitNull, options);
  163. const modelAttributeMap = {};
  164. const values = [];
  165. const bind = [];
  166. const bindParam = options.bindParam || this.bindParam(bind);
  167. if (attributes) {
  168. _.each(attributes, (attribute, key) => {
  169. modelAttributeMap[key] = attribute;
  170. if (attribute.field) {
  171. modelAttributeMap[attribute.field] = attribute;
  172. }
  173. });
  174. }
  175. for (const key in attrValueHash) {
  176. const value = attrValueHash[key];
  177. if (value instanceof Utils.SequelizeMethod || options.bindParam === false) {
  178. values.push(`${this.quoteIdentifier(key)}=${this.escape(value, modelAttributeMap && modelAttributeMap[key] || undefined, { context: 'UPDATE' })}`);
  179. } else {
  180. values.push(`${this.quoteIdentifier(key)}=${this.format(value, modelAttributeMap && modelAttributeMap[key] || undefined, { context: 'UPDATE' }, bindParam)}`);
  181. }
  182. }
  183. let query;
  184. const whereOptions = _.defaults({ bindParam }, options);
  185. if (options.limit) {
  186. query = `UPDATE ${this.quoteTable(tableName)} SET ${values.join(',')} WHERE rowid IN (SELECT rowid FROM ${this.quoteTable(tableName)} ${this.whereQuery(where, whereOptions)} LIMIT ${this.escape(options.limit)})`;
  187. } else {
  188. query = `UPDATE ${this.quoteTable(tableName)} SET ${values.join(',')} ${this.whereQuery(where, whereOptions)}`;
  189. }
  190. return { query, bind };
  191. }
  192. truncateTableQuery(tableName, options = {}) {
  193. return [
  194. `DELETE FROM ${this.quoteTable(tableName)}`,
  195. options.restartIdentity ? `; DELETE FROM ${this.quoteTable('sqlite_sequence')} WHERE ${this.quoteIdentifier('name')} = ${Utils.addTicks(Utils.removeTicks(this.quoteTable(tableName), '`'), "'")};` : ''
  196. ].join('');
  197. }
  198. deleteQuery(tableName, where, options = {}, model) {
  199. _.defaults(options, this.options);
  200. let whereClause = this.getWhereConditions(where, null, model, options);
  201. if (whereClause) {
  202. whereClause = `WHERE ${whereClause}`;
  203. }
  204. if (options.limit) {
  205. whereClause = `WHERE rowid IN (SELECT rowid FROM ${this.quoteTable(tableName)} ${whereClause} LIMIT ${this.escape(options.limit)})`;
  206. }
  207. return `DELETE FROM ${this.quoteTable(tableName)} ${whereClause}`;
  208. }
  209. attributesToSQL(attributes) {
  210. const result = {};
  211. for (const name in attributes) {
  212. const dataType = attributes[name];
  213. const fieldName = dataType.field || name;
  214. if (_.isObject(dataType)) {
  215. let sql = dataType.type.toString();
  216. if (Object.prototype.hasOwnProperty.call(dataType, 'allowNull') && !dataType.allowNull) {
  217. sql += ' NOT NULL';
  218. }
  219. if (Utils.defaultValueSchemable(dataType.defaultValue)) {
  220. // TODO thoroughly check that DataTypes.NOW will properly
  221. // get populated on all databases as DEFAULT value
  222. // i.e. mysql requires: DEFAULT CURRENT_TIMESTAMP
  223. sql += ` DEFAULT ${this.escape(dataType.defaultValue, dataType)}`;
  224. }
  225. if (dataType.unique === true) {
  226. sql += ' UNIQUE';
  227. }
  228. if (dataType.primaryKey) {
  229. sql += ' PRIMARY KEY';
  230. if (dataType.autoIncrement) {
  231. sql += ' AUTOINCREMENT';
  232. }
  233. }
  234. if (dataType.references) {
  235. const referencesTable = this.quoteTable(dataType.references.model);
  236. let referencesKey;
  237. if (dataType.references.key) {
  238. referencesKey = this.quoteIdentifier(dataType.references.key);
  239. } else {
  240. referencesKey = this.quoteIdentifier('id');
  241. }
  242. sql += ` REFERENCES ${referencesTable} (${referencesKey})`;
  243. if (dataType.onDelete) {
  244. sql += ` ON DELETE ${dataType.onDelete.toUpperCase()}`;
  245. }
  246. if (dataType.onUpdate) {
  247. sql += ` ON UPDATE ${dataType.onUpdate.toUpperCase()}`;
  248. }
  249. }
  250. result[fieldName] = sql;
  251. } else {
  252. result[fieldName] = dataType;
  253. }
  254. }
  255. return result;
  256. }
  257. showIndexesQuery(tableName) {
  258. return `PRAGMA INDEX_LIST(${this.quoteTable(tableName)})`;
  259. }
  260. showConstraintsQuery(tableName, constraintName) {
  261. let sql = `SELECT sql FROM sqlite_master WHERE tbl_name='${tableName}'`;
  262. if (constraintName) {
  263. sql += ` AND sql LIKE '%${constraintName}%'`;
  264. }
  265. return `${sql};`;
  266. }
  267. removeIndexQuery(tableName, indexNameOrAttributes) {
  268. let indexName = indexNameOrAttributes;
  269. if (typeof indexName !== 'string') {
  270. indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`);
  271. }
  272. return `DROP INDEX IF EXISTS ${this.quoteIdentifier(indexName)}`;
  273. }
  274. describeTableQuery(tableName, schema, schemaDelimiter) {
  275. const table = {
  276. _schema: schema,
  277. _schemaDelimiter: schemaDelimiter,
  278. tableName
  279. };
  280. return `PRAGMA TABLE_INFO(${this.quoteTable(this.addSchema(table))});`;
  281. }
  282. describeCreateTableQuery(tableName) {
  283. return `SELECT sql FROM sqlite_master WHERE tbl_name='${tableName}';`;
  284. }
  285. removeColumnQuery(tableName, attributes) {
  286. attributes = this.attributesToSQL(attributes);
  287. let backupTableName;
  288. if (typeof tableName === 'object') {
  289. backupTableName = {
  290. tableName: `${tableName.tableName}_backup`,
  291. schema: tableName.schema
  292. };
  293. } else {
  294. backupTableName = `${tableName}_backup`;
  295. }
  296. const quotedTableName = this.quoteTable(tableName);
  297. const quotedBackupTableName = this.quoteTable(backupTableName);
  298. const attributeNames = Object.keys(attributes).map(attr => this.quoteIdentifier(attr)).join(', ');
  299. // Temporary table cannot work for foreign keys.
  300. return `${this.createTableQuery(backupTableName, attributes)
  301. }INSERT INTO ${quotedBackupTableName} SELECT ${attributeNames} FROM ${quotedTableName};`
  302. + `DROP TABLE ${quotedTableName};${
  303. this.createTableQuery(tableName, attributes)
  304. }INSERT INTO ${quotedTableName} SELECT ${attributeNames} FROM ${quotedBackupTableName};`
  305. + `DROP TABLE ${quotedBackupTableName};`;
  306. }
  307. _alterConstraintQuery(tableName, attributes, createTableSql) {
  308. let backupTableName;
  309. attributes = this.attributesToSQL(attributes);
  310. if (typeof tableName === 'object') {
  311. backupTableName = {
  312. tableName: `${tableName.tableName}_backup`,
  313. schema: tableName.schema
  314. };
  315. } else {
  316. backupTableName = `${tableName}_backup`;
  317. }
  318. const quotedTableName = this.quoteTable(tableName);
  319. const quotedBackupTableName = this.quoteTable(backupTableName);
  320. const attributeNames = Object.keys(attributes).map(attr => this.quoteIdentifier(attr)).join(', ');
  321. return `${createTableSql
  322. .replace(`CREATE TABLE ${quotedTableName}`, `CREATE TABLE ${quotedBackupTableName}`)
  323. .replace(`CREATE TABLE ${quotedTableName.replace(/`/g, '"')}`, `CREATE TABLE ${quotedBackupTableName}`)
  324. }INSERT INTO ${quotedBackupTableName} SELECT ${attributeNames} FROM ${quotedTableName};`
  325. + `DROP TABLE ${quotedTableName};`
  326. + `ALTER TABLE ${quotedBackupTableName} RENAME TO ${quotedTableName};`;
  327. }
  328. renameColumnQuery(tableName, attrNameBefore, attrNameAfter, attributes) {
  329. let backupTableName;
  330. attributes = this.attributesToSQL(attributes);
  331. if (typeof tableName === 'object') {
  332. backupTableName = {
  333. tableName: `${tableName.tableName}_backup`,
  334. schema: tableName.schema
  335. };
  336. } else {
  337. backupTableName = `${tableName}_backup`;
  338. }
  339. const quotedTableName = this.quoteTable(tableName);
  340. const quotedBackupTableName = this.quoteTable(backupTableName);
  341. const attributeNamesImport = Object.keys(attributes).map(attr =>
  342. attrNameAfter === attr ? `${this.quoteIdentifier(attrNameBefore)} AS ${this.quoteIdentifier(attr)}` : this.quoteIdentifier(attr)
  343. ).join(', ');
  344. const attributeNamesExport = Object.keys(attributes).map(attr => this.quoteIdentifier(attr)).join(', ');
  345. // Temporary tables don't support foreign keys, so creating a temporary table will not allow foreign keys to be preserved
  346. return `${this.createTableQuery(backupTableName, attributes)
  347. }INSERT INTO ${quotedBackupTableName} SELECT ${attributeNamesImport} FROM ${quotedTableName};`
  348. + `DROP TABLE ${quotedTableName};${
  349. this.createTableQuery(tableName, attributes)
  350. }INSERT INTO ${quotedTableName} SELECT ${attributeNamesExport} FROM ${quotedBackupTableName};`
  351. + `DROP TABLE ${quotedBackupTableName};`;
  352. }
  353. startTransactionQuery(transaction) {
  354. if (transaction.parent) {
  355. return `SAVEPOINT ${this.quoteIdentifier(transaction.name)};`;
  356. }
  357. return `BEGIN ${transaction.options.type} TRANSACTION;`;
  358. }
  359. setIsolationLevelQuery(value) {
  360. switch (value) {
  361. case Transaction.ISOLATION_LEVELS.REPEATABLE_READ:
  362. return '-- SQLite is not able to choose the isolation level REPEATABLE READ.';
  363. case Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED:
  364. return 'PRAGMA read_uncommitted = ON;';
  365. case Transaction.ISOLATION_LEVELS.READ_COMMITTED:
  366. return 'PRAGMA read_uncommitted = OFF;';
  367. case Transaction.ISOLATION_LEVELS.SERIALIZABLE:
  368. return '-- SQLite\'s default isolation level is SERIALIZABLE. Nothing to do.';
  369. default:
  370. throw new Error(`Unknown isolation level: ${value}`);
  371. }
  372. }
  373. replaceBooleanDefaults(sql) {
  374. return sql.replace(/DEFAULT '?false'?/g, 'DEFAULT 0').replace(/DEFAULT '?true'?/g, 'DEFAULT 1');
  375. }
  376. /**
  377. * Generates an SQL query that returns all foreign keys of a table.
  378. *
  379. * @param {string} tableName The name of the table.
  380. * @returns {string} The generated sql query.
  381. * @private
  382. */
  383. getForeignKeysQuery(tableName) {
  384. return `PRAGMA foreign_key_list(${tableName})`;
  385. }
  386. }
  387. module.exports = SQLiteQueryGenerator;