query-generator.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949
  1. 'use strict';
  2. const Utils = require('../../utils');
  3. const util = require('util');
  4. const DataTypes = require('../../data-types');
  5. const AbstractQueryGenerator = require('../abstract/query-generator');
  6. const semver = require('semver');
  7. const _ = require('lodash');
  8. class PostgresQueryGenerator extends AbstractQueryGenerator {
  9. setSearchPath(searchPath) {
  10. return `SET search_path to ${searchPath};`;
  11. }
  12. createDatabaseQuery(databaseName, options) {
  13. options = Object.assign({
  14. encoding: null,
  15. collate: null
  16. }, options || {});
  17. const values = {
  18. database: this.quoteTable(databaseName),
  19. encoding: options.encoding ? ` ENCODING = ${this.escape(options.encoding)}` : '',
  20. collation: options.collate ? ` LC_COLLATE = ${this.escape(options.collate)}` : '',
  21. ctype: options.ctype ? ` LC_CTYPE = ${this.escape(options.ctype)}` : '',
  22. template: options.template ? ` TEMPLATE = ${this.escape(options.template)}` : ''
  23. };
  24. return `CREATE DATABASE ${values.database}${values.encoding}${values.collation}${values.ctype}${values.template};`;
  25. }
  26. dropDatabaseQuery(databaseName) {
  27. return `DROP DATABASE IF EXISTS ${this.quoteTable(databaseName)};`;
  28. }
  29. createSchema(schema) {
  30. const databaseVersion = _.get(this, 'sequelize.options.databaseVersion', 0);
  31. if (databaseVersion && semver.gte(databaseVersion, '9.2.0')) {
  32. return `CREATE SCHEMA IF NOT EXISTS ${schema};`;
  33. }
  34. return `CREATE SCHEMA ${schema};`;
  35. }
  36. dropSchema(schema) {
  37. return `DROP SCHEMA IF EXISTS ${schema} CASCADE;`;
  38. }
  39. showSchemasQuery() {
  40. return "SELECT schema_name FROM information_schema.schemata WHERE schema_name <> 'information_schema' AND schema_name != 'public' AND schema_name !~ E'^pg_';";
  41. }
  42. versionQuery() {
  43. return 'SHOW SERVER_VERSION';
  44. }
  45. createTableQuery(tableName, attributes, options) {
  46. options = Object.assign({}, options || {});
  47. //Postgres 9.0 does not support CREATE TABLE IF NOT EXISTS, 9.1 and above do
  48. const databaseVersion = _.get(this, 'sequelize.options.databaseVersion', 0);
  49. const attrStr = [];
  50. let comments = '';
  51. let columnComments = '';
  52. const quotedTable = this.quoteTable(tableName);
  53. if (options.comment && typeof options.comment === 'string') {
  54. comments += `; COMMENT ON TABLE ${quotedTable} IS ${this.escape(options.comment)}`;
  55. }
  56. for (const attr in attributes) {
  57. const quotedAttr = this.quoteIdentifier(attr);
  58. const i = attributes[attr].indexOf('COMMENT ');
  59. if (i !== -1) {
  60. // Move comment to a separate query
  61. const escapedCommentText = this.escape(attributes[attr].substring(i + 8));
  62. columnComments += `; COMMENT ON COLUMN ${quotedTable}.${quotedAttr} IS ${escapedCommentText}`;
  63. attributes[attr] = attributes[attr].substring(0, i);
  64. }
  65. const dataType = this.dataTypeMapping(tableName, attr, attributes[attr]);
  66. attrStr.push(`${quotedAttr} ${dataType}`);
  67. }
  68. let attributesClause = attrStr.join(', ');
  69. if (options.uniqueKeys) {
  70. _.each(options.uniqueKeys, columns => {
  71. if (columns.customIndex) {
  72. attributesClause += `, UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
  73. }
  74. });
  75. }
  76. const pks = _.reduce(attributes, (acc, attribute, key) => {
  77. if (attribute.includes('PRIMARY KEY')) {
  78. acc.push(this.quoteIdentifier(key));
  79. }
  80. return acc;
  81. }, []).join(',');
  82. if (pks.length > 0) {
  83. attributesClause += `, PRIMARY KEY (${pks})`;
  84. }
  85. return `CREATE TABLE ${databaseVersion === 0 || semver.gte(databaseVersion, '9.1.0') ? 'IF NOT EXISTS ' : ''}${quotedTable} (${attributesClause})${comments}${columnComments};`;
  86. }
  87. dropTableQuery(tableName, options) {
  88. options = options || {};
  89. return `DROP TABLE IF EXISTS ${this.quoteTable(tableName)}${options.cascade ? ' CASCADE' : ''};`;
  90. }
  91. showTablesQuery() {
  92. return "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type LIKE '%TABLE' AND table_name != 'spatial_ref_sys';";
  93. }
  94. describeTableQuery(tableName, schema) {
  95. if (!schema) schema = 'public';
  96. return 'SELECT ' +
  97. 'pk.constraint_type as "Constraint",' +
  98. 'c.column_name as "Field", ' +
  99. 'c.column_default as "Default",' +
  100. 'c.is_nullable as "Null", ' +
  101. '(CASE WHEN c.udt_name = \'hstore\' THEN c.udt_name ELSE c.data_type END) || (CASE WHEN c.character_maximum_length IS NOT NULL THEN \'(\' || c.character_maximum_length || \')\' ELSE \'\' END) as "Type", ' +
  102. '(SELECT array_agg(e.enumlabel) FROM pg_catalog.pg_type t JOIN pg_catalog.pg_enum e ON t.oid=e.enumtypid WHERE t.typname=c.udt_name) AS "special", ' +
  103. '(SELECT pgd.description FROM pg_catalog.pg_statio_all_tables AS st INNER JOIN pg_catalog.pg_description pgd on (pgd.objoid=st.relid) WHERE c.ordinal_position=pgd.objsubid AND c.table_name=st.relname) AS "Comment" ' +
  104. 'FROM information_schema.columns c ' +
  105. 'LEFT JOIN (SELECT tc.table_schema, tc.table_name, ' +
  106. 'cu.column_name, tc.constraint_type ' +
  107. 'FROM information_schema.TABLE_CONSTRAINTS tc ' +
  108. 'JOIN information_schema.KEY_COLUMN_USAGE cu ' +
  109. 'ON tc.table_schema=cu.table_schema and tc.table_name=cu.table_name ' +
  110. 'and tc.constraint_name=cu.constraint_name ' +
  111. 'and tc.constraint_type=\'PRIMARY KEY\') pk ' +
  112. 'ON pk.table_schema=c.table_schema ' +
  113. 'AND pk.table_name=c.table_name ' +
  114. 'AND pk.column_name=c.column_name ' +
  115. `WHERE c.table_name = ${this.escape(tableName)} AND c.table_schema = ${this.escape(schema)} `;
  116. }
  117. /**
  118. * Check whether the statmement is json function or simple path
  119. *
  120. * @param {string} stmt The statement to validate
  121. * @returns {boolean} true if the given statement is json function
  122. * @throws {Error} throw if the statement looks like json function but has invalid token
  123. */
  124. _checkValidJsonStatement(stmt) {
  125. if (typeof stmt !== 'string') {
  126. return false;
  127. }
  128. // https://www.postgresql.org/docs/current/static/functions-json.html
  129. const jsonFunctionRegex = /^\s*((?:[a-z]+_){0,2}jsonb?(?:_[a-z]+){0,2})\([^)]*\)/i;
  130. const jsonOperatorRegex = /^\s*(->>?|#>>?|@>|<@|\?[|&]?|\|{2}|#-)/i;
  131. const tokenCaptureRegex = /^\s*((?:([`"'])(?:(?!\2).|\2{2})*\2)|[\w\d\s]+|[().,;+-])/i;
  132. let currentIndex = 0;
  133. let openingBrackets = 0;
  134. let closingBrackets = 0;
  135. let hasJsonFunction = false;
  136. let hasInvalidToken = false;
  137. while (currentIndex < stmt.length) {
  138. const string = stmt.substr(currentIndex);
  139. const functionMatches = jsonFunctionRegex.exec(string);
  140. if (functionMatches) {
  141. currentIndex += functionMatches[0].indexOf('(');
  142. hasJsonFunction = true;
  143. continue;
  144. }
  145. const operatorMatches = jsonOperatorRegex.exec(string);
  146. if (operatorMatches) {
  147. currentIndex += operatorMatches[0].length;
  148. hasJsonFunction = true;
  149. continue;
  150. }
  151. const tokenMatches = tokenCaptureRegex.exec(string);
  152. if (tokenMatches) {
  153. const capturedToken = tokenMatches[1];
  154. if (capturedToken === '(') {
  155. openingBrackets++;
  156. } else if (capturedToken === ')') {
  157. closingBrackets++;
  158. } else if (capturedToken === ';') {
  159. hasInvalidToken = true;
  160. break;
  161. }
  162. currentIndex += tokenMatches[0].length;
  163. continue;
  164. }
  165. break;
  166. }
  167. // Check invalid json statement
  168. hasInvalidToken |= openingBrackets !== closingBrackets;
  169. if (hasJsonFunction && hasInvalidToken) {
  170. throw new Error(`Invalid json statement: ${stmt}`);
  171. }
  172. // return true if the statement has valid json function
  173. return hasJsonFunction;
  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 postgres json syntax
  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. }
  201. return super.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend);
  202. }
  203. addColumnQuery(table, key, dataType) {
  204. const dbDataType = this.attributeToSQL(dataType, { context: 'addColumn', table, key });
  205. const definition = this.dataTypeMapping(table, key, dbDataType);
  206. const quotedKey = this.quoteIdentifier(key);
  207. const quotedTable = this.quoteTable(this.extractTableDetails(table));
  208. let query = `ALTER TABLE ${quotedTable} ADD COLUMN ${quotedKey} ${definition};`;
  209. if (dataType.type && dataType.type instanceof DataTypes.ENUM || dataType instanceof DataTypes.ENUM) {
  210. query = this.pgEnum(table, key, dataType) + query;
  211. }
  212. return query;
  213. }
  214. removeColumnQuery(tableName, attributeName) {
  215. const quotedTableName = this.quoteTable(this.extractTableDetails(tableName));
  216. const quotedAttributeName = this.quoteIdentifier(attributeName);
  217. return `ALTER TABLE ${quotedTableName} DROP COLUMN ${quotedAttributeName};`;
  218. }
  219. changeColumnQuery(tableName, attributes) {
  220. const query = subQuery => `ALTER TABLE ${this.quoteTable(tableName)} ALTER COLUMN ${subQuery};`;
  221. const sql = [];
  222. for (const attributeName in attributes) {
  223. let definition = this.dataTypeMapping(tableName, attributeName, attributes[attributeName]);
  224. let attrSql = '';
  225. if (definition.includes('NOT NULL')) {
  226. attrSql += query(`${this.quoteIdentifier(attributeName)} SET NOT NULL`);
  227. definition = definition.replace('NOT NULL', '').trim();
  228. } else if (!definition.includes('REFERENCES')) {
  229. attrSql += query(`${this.quoteIdentifier(attributeName)} DROP NOT NULL`);
  230. }
  231. if (definition.includes('DEFAULT')) {
  232. attrSql += query(`${this.quoteIdentifier(attributeName)} SET DEFAULT ${definition.match(/DEFAULT ([^;]+)/)[1]}`);
  233. definition = definition.replace(/(DEFAULT[^;]+)/, '').trim();
  234. } else if (!definition.includes('REFERENCES')) {
  235. attrSql += query(`${this.quoteIdentifier(attributeName)} DROP DEFAULT`);
  236. }
  237. if (attributes[attributeName].startsWith('ENUM(')) {
  238. attrSql += this.pgEnum(tableName, attributeName, attributes[attributeName]);
  239. definition = definition.replace(/^ENUM\(.+\)/, this.pgEnumName(tableName, attributeName, { schema: false }));
  240. definition += ` USING (${this.quoteIdentifier(attributeName)}::${this.pgEnumName(tableName, attributeName)})`;
  241. }
  242. if (definition.match(/UNIQUE;*$/)) {
  243. definition = definition.replace(/UNIQUE;*$/, '');
  244. attrSql += query(`ADD UNIQUE (${this.quoteIdentifier(attributeName)})`).replace('ALTER COLUMN', '');
  245. }
  246. if (definition.includes('REFERENCES')) {
  247. definition = definition.replace(/.+?(?=REFERENCES)/, '');
  248. attrSql += query(`ADD FOREIGN KEY (${this.quoteIdentifier(attributeName)}) ${definition}`).replace('ALTER COLUMN', '');
  249. } else {
  250. attrSql += query(`${this.quoteIdentifier(attributeName)} TYPE ${definition}`);
  251. }
  252. sql.push(attrSql);
  253. }
  254. return sql.join('');
  255. }
  256. renameColumnQuery(tableName, attrBefore, attributes) {
  257. const attrString = [];
  258. for (const attributeName in attributes) {
  259. attrString.push(`${this.quoteIdentifier(attrBefore)} TO ${this.quoteIdentifier(attributeName)}`);
  260. }
  261. return `ALTER TABLE ${this.quoteTable(tableName)} RENAME COLUMN ${attrString.join(', ')};`;
  262. }
  263. fn(fnName, tableName, parameters, body, returns, language) {
  264. fnName = fnName || 'testfunc';
  265. language = language || 'plpgsql';
  266. returns = returns ? `RETURNS ${returns}` : '';
  267. parameters = parameters || '';
  268. return `CREATE OR REPLACE FUNCTION pg_temp.${fnName}(${parameters}) ${returns} AS $func$ BEGIN ${body} END; $func$ LANGUAGE ${language}; SELECT * FROM pg_temp.${fnName}();`;
  269. }
  270. exceptionFn(fnName, tableName, parameters, main, then, when, returns, language) {
  271. when = when || 'unique_violation';
  272. const body = `${main} EXCEPTION WHEN ${when} THEN ${then};`;
  273. return this.fn(fnName, tableName, parameters, body, returns, language);
  274. }
  275. upsertQuery(tableName, insertValues, updateValues, where, model, options) {
  276. const primaryField = this.quoteIdentifier(model.primaryKeyField);
  277. const upsertOptions = _.defaults({ bindParam: false }, options);
  278. const insert = this.insertQuery(tableName, insertValues, model.rawAttributes, upsertOptions);
  279. const update = this.updateQuery(tableName, updateValues, where, upsertOptions, model.rawAttributes);
  280. if (options.returning) {
  281. const returningRegex = /RETURNING \*(?![\s\S]*RETURNING \*)/;
  282. insert.query = insert.query.replace(returningRegex, `RETURNING ${primaryField} INTO primary_key`);
  283. update.query = update.query.replace(returningRegex, `RETURNING ${primaryField} INTO primary_key`);
  284. }
  285. return this.exceptionFn(
  286. 'sequelize_upsert',
  287. tableName,
  288. 'OUT created boolean, OUT primary_key text',
  289. `${insert.query} created := true;`,
  290. `${update.query}; created := false`
  291. );
  292. }
  293. truncateTableQuery(tableName, options = {}) {
  294. return [
  295. `TRUNCATE ${this.quoteTable(tableName)}`,
  296. options.restartIdentity ? ' RESTART IDENTITY' : '',
  297. options.cascade ? ' CASCADE' : ''
  298. ].join('');
  299. }
  300. deleteQuery(tableName, where, options = {}, model) {
  301. const table = this.quoteTable(tableName);
  302. let whereClause = this.getWhereConditions(where, null, model, options);
  303. const limit = options.limit ? ` LIMIT ${this.escape(options.limit)}` : '';
  304. let primaryKeys = '';
  305. let primaryKeysSelection = '';
  306. if (whereClause) {
  307. whereClause = ` WHERE ${whereClause}`;
  308. }
  309. if (options.limit) {
  310. if (!model) {
  311. throw new Error('Cannot LIMIT delete without a model.');
  312. }
  313. const pks = _.values(model.primaryKeys).map(pk => this.quoteIdentifier(pk.field)).join(',');
  314. primaryKeys = model.primaryKeyAttributes.length > 1 ? `(${pks})` : pks;
  315. primaryKeysSelection = pks;
  316. return `DELETE FROM ${table} WHERE ${primaryKeys} IN (SELECT ${primaryKeysSelection} FROM ${table}${whereClause}${limit})`;
  317. }
  318. return `DELETE FROM ${table}${whereClause}`;
  319. }
  320. showIndexesQuery(tableName) {
  321. let schemaJoin = '';
  322. let schemaWhere = '';
  323. if (typeof tableName !== 'string') {
  324. schemaJoin = ', pg_namespace s';
  325. schemaWhere = ` AND s.oid = t.relnamespace AND s.nspname = '${tableName.schema}'`;
  326. tableName = tableName.tableName;
  327. }
  328. // This is ARCANE!
  329. return 'SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, ' +
  330. 'array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) ' +
  331. `AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a${schemaJoin} ` +
  332. 'WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND ' +
  333. `t.relkind = 'r' and t.relname = '${tableName}'${schemaWhere} ` +
  334. 'GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;';
  335. }
  336. showConstraintsQuery(tableName) {
  337. //Postgres converts camelCased alias to lowercase unless quoted
  338. return [
  339. 'SELECT constraint_catalog AS "constraintCatalog",',
  340. 'constraint_schema AS "constraintSchema",',
  341. 'constraint_name AS "constraintName",',
  342. 'table_catalog AS "tableCatalog",',
  343. 'table_schema AS "tableSchema",',
  344. 'table_name AS "tableName",',
  345. 'constraint_type AS "constraintType",',
  346. 'is_deferrable AS "isDeferrable",',
  347. 'initially_deferred AS "initiallyDeferred"',
  348. 'from INFORMATION_SCHEMA.table_constraints',
  349. `WHERE table_name='${tableName}';`
  350. ].join(' ');
  351. }
  352. removeIndexQuery(tableName, indexNameOrAttributes) {
  353. let indexName = indexNameOrAttributes;
  354. if (typeof indexName !== 'string') {
  355. indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`);
  356. }
  357. return `DROP INDEX IF EXISTS ${this.quoteIdentifiers(indexName)}`;
  358. }
  359. addLimitAndOffset(options) {
  360. let fragment = '';
  361. /* eslint-disable */
  362. if (options.limit != null) {
  363. fragment += ' LIMIT ' + this.escape(options.limit);
  364. }
  365. if (options.offset != null) {
  366. fragment += ' OFFSET ' + this.escape(options.offset);
  367. }
  368. /* eslint-enable */
  369. return fragment;
  370. }
  371. attributeToSQL(attribute, options) {
  372. if (!_.isPlainObject(attribute)) {
  373. attribute = {
  374. type: attribute
  375. };
  376. }
  377. let type;
  378. if (
  379. attribute.type instanceof DataTypes.ENUM ||
  380. attribute.type instanceof DataTypes.ARRAY && attribute.type.type instanceof DataTypes.ENUM
  381. ) {
  382. const enumType = attribute.type.type || attribute.type;
  383. let values = attribute.values;
  384. if (enumType.values && !attribute.values) {
  385. values = enumType.values;
  386. }
  387. if (Array.isArray(values) && values.length > 0) {
  388. type = `ENUM(${values.map(value => this.escape(value)).join(', ')})`;
  389. if (attribute.type instanceof DataTypes.ARRAY) {
  390. type += '[]';
  391. }
  392. } else {
  393. throw new Error("Values for ENUM haven't been defined.");
  394. }
  395. }
  396. if (!type) {
  397. type = attribute.type;
  398. }
  399. let sql = type.toString();
  400. if (Object.prototype.hasOwnProperty.call(attribute, 'allowNull') && !attribute.allowNull) {
  401. sql += ' NOT NULL';
  402. }
  403. if (attribute.autoIncrement) {
  404. if (attribute.autoIncrementIdentity) {
  405. sql += ' GENERATED BY DEFAULT AS IDENTITY';
  406. } else {
  407. sql += ' SERIAL';
  408. }
  409. }
  410. if (Utils.defaultValueSchemable(attribute.defaultValue)) {
  411. sql += ` DEFAULT ${this.escape(attribute.defaultValue, attribute)}`;
  412. }
  413. if (attribute.unique === true) {
  414. sql += ' UNIQUE';
  415. }
  416. if (attribute.primaryKey) {
  417. sql += ' PRIMARY KEY';
  418. }
  419. if (attribute.references) {
  420. let referencesTable = this.quoteTable(attribute.references.model);
  421. let schema;
  422. if (options.schema) {
  423. schema = options.schema;
  424. } else if (
  425. (!attribute.references.model || typeof attribute.references.model == 'string')
  426. && options.table
  427. && options.table.schema
  428. ) {
  429. schema = options.table.schema;
  430. }
  431. if (schema) {
  432. referencesTable = this.quoteTable(this.addSchema({
  433. tableName: referencesTable,
  434. _schema: schema
  435. }));
  436. }
  437. let referencesKey;
  438. if (attribute.references.key) {
  439. referencesKey = this.quoteIdentifiers(attribute.references.key);
  440. } else {
  441. referencesKey = this.quoteIdentifier('id');
  442. }
  443. sql += ` REFERENCES ${referencesTable} (${referencesKey})`;
  444. if (attribute.onDelete) {
  445. sql += ` ON DELETE ${attribute.onDelete.toUpperCase()}`;
  446. }
  447. if (attribute.onUpdate) {
  448. sql += ` ON UPDATE ${attribute.onUpdate.toUpperCase()}`;
  449. }
  450. if (attribute.references.deferrable) {
  451. sql += ` ${attribute.references.deferrable.toString(this)}`;
  452. }
  453. }
  454. if (attribute.comment && typeof attribute.comment === 'string') {
  455. if (options && (options.context === 'addColumn' || options.context === 'changeColumn')) {
  456. const quotedAttr = this.quoteIdentifier(options.key);
  457. const escapedCommentText = this.escape(attribute.comment);
  458. sql += `; COMMENT ON COLUMN ${this.quoteTable(options.table)}.${quotedAttr} IS ${escapedCommentText}`;
  459. } else {
  460. // for createTable event which does it's own parsing
  461. // TODO: centralize creation of comment statements here
  462. sql += ` COMMENT ${attribute.comment}`;
  463. }
  464. }
  465. return sql;
  466. }
  467. deferConstraintsQuery(options) {
  468. return options.deferrable.toString(this);
  469. }
  470. setConstraintQuery(columns, type) {
  471. let columnFragment = 'ALL';
  472. if (columns) {
  473. columnFragment = columns.map(column => this.quoteIdentifier(column)).join(', ');
  474. }
  475. return `SET CONSTRAINTS ${columnFragment} ${type}`;
  476. }
  477. setDeferredQuery(columns) {
  478. return this.setConstraintQuery(columns, 'DEFERRED');
  479. }
  480. setImmediateQuery(columns) {
  481. return this.setConstraintQuery(columns, 'IMMEDIATE');
  482. }
  483. attributesToSQL(attributes, options) {
  484. const result = {};
  485. for (const key in attributes) {
  486. const attribute = attributes[key];
  487. result[attribute.field || key] = this.attributeToSQL(attribute, Object.assign({ key }, options || {}));
  488. }
  489. return result;
  490. }
  491. createTrigger(tableName, triggerName, eventType, fireOnSpec, functionName, functionParams, optionsArray) {
  492. const decodedEventType = this.decodeTriggerEventType(eventType);
  493. const eventSpec = this.expandTriggerEventSpec(fireOnSpec);
  494. const expandedOptions = this.expandOptions(optionsArray);
  495. const paramList = this.expandFunctionParamList(functionParams);
  496. return `CREATE ${this.triggerEventTypeIsConstraint(eventType)}TRIGGER ${this.quoteIdentifier(triggerName)} ${decodedEventType} ${
  497. eventSpec} ON ${this.quoteTable(tableName)}${expandedOptions ? ` ${expandedOptions}` : ''} EXECUTE PROCEDURE ${functionName}(${paramList});`;
  498. }
  499. dropTrigger(tableName, triggerName) {
  500. return `DROP TRIGGER ${this.quoteIdentifier(triggerName)} ON ${this.quoteTable(tableName)} RESTRICT;`;
  501. }
  502. renameTrigger(tableName, oldTriggerName, newTriggerName) {
  503. return `ALTER TRIGGER ${this.quoteIdentifier(oldTriggerName)} ON ${this.quoteTable(tableName)} RENAME TO ${this.quoteIdentifier(newTriggerName)};`;
  504. }
  505. createFunction(functionName, params, returnType, language, body, optionsArray, options) {
  506. if (!functionName || !returnType || !language || !body) throw new Error('createFunction missing some parameters. Did you pass functionName, returnType, language and body?');
  507. const paramList = this.expandFunctionParamList(params);
  508. const variableList = options && options.variables ? this.expandFunctionVariableList(options.variables) : '';
  509. const expandedOptionsArray = this.expandOptions(optionsArray);
  510. const statement = options && options.force ? 'CREATE OR REPLACE FUNCTION' : 'CREATE FUNCTION';
  511. return `${statement} ${functionName}(${paramList}) RETURNS ${returnType} AS $func$ ${variableList} BEGIN ${body} END; $func$ language '${language}'${expandedOptionsArray};`;
  512. }
  513. dropFunction(functionName, params) {
  514. if (!functionName) throw new Error('requires functionName');
  515. // RESTRICT is (currently, as of 9.2) default but we'll be explicit
  516. const paramList = this.expandFunctionParamList(params);
  517. return `DROP FUNCTION ${functionName}(${paramList}) RESTRICT;`;
  518. }
  519. renameFunction(oldFunctionName, params, newFunctionName) {
  520. const paramList = this.expandFunctionParamList(params);
  521. return `ALTER FUNCTION ${oldFunctionName}(${paramList}) RENAME TO ${newFunctionName};`;
  522. }
  523. databaseConnectionUri(config) {
  524. let uri = `${config.protocol}://${config.user}:${config.password}@${config.host}`;
  525. if (config.port) {
  526. uri += `:${config.port}`;
  527. }
  528. uri += `/${config.database}`;
  529. if (config.ssl) {
  530. uri += `?ssl=${config.ssl}`;
  531. }
  532. return uri;
  533. }
  534. pgEscapeAndQuote(val) {
  535. return this.quoteIdentifier(Utils.removeTicks(this.escape(val), "'"));
  536. }
  537. expandFunctionParamList(params) {
  538. if (params === undefined || !Array.isArray(params)) {
  539. throw new Error('expandFunctionParamList: function parameters array required, including an empty one for no arguments');
  540. }
  541. const paramList = [];
  542. params.forEach(curParam => {
  543. const paramDef = [];
  544. if (curParam.type) {
  545. if (curParam.direction) { paramDef.push(curParam.direction); }
  546. if (curParam.name) { paramDef.push(curParam.name); }
  547. paramDef.push(curParam.type);
  548. } else {
  549. throw new Error('function or trigger used with a parameter without any type');
  550. }
  551. const joined = paramDef.join(' ');
  552. if (joined) paramList.push(joined);
  553. });
  554. return paramList.join(', ');
  555. }
  556. expandFunctionVariableList(variables) {
  557. if (!Array.isArray(variables)) {
  558. throw new Error('expandFunctionVariableList: function variables must be an array');
  559. }
  560. const variableDefinitions = [];
  561. variables.forEach(variable => {
  562. if (!variable.name || !variable.type) {
  563. throw new Error('function variable must have a name and type');
  564. }
  565. let variableDefinition = `DECLARE ${variable.name} ${variable.type}`;
  566. if (variable.default) {
  567. variableDefinition += ` := ${variable.default}`;
  568. }
  569. variableDefinition += ';';
  570. variableDefinitions.push(variableDefinition);
  571. });
  572. return variableDefinitions.join(' ');
  573. }
  574. expandOptions(options) {
  575. return options === undefined || _.isEmpty(options) ?
  576. '' : options.join(' ');
  577. }
  578. decodeTriggerEventType(eventSpecifier) {
  579. const EVENT_DECODER = {
  580. 'after': 'AFTER',
  581. 'before': 'BEFORE',
  582. 'instead_of': 'INSTEAD OF',
  583. 'after_constraint': 'AFTER'
  584. };
  585. if (!EVENT_DECODER[eventSpecifier]) {
  586. throw new Error(`Invalid trigger event specified: ${eventSpecifier}`);
  587. }
  588. return EVENT_DECODER[eventSpecifier];
  589. }
  590. triggerEventTypeIsConstraint(eventSpecifier) {
  591. return eventSpecifier === 'after_constraint' ? 'CONSTRAINT ' : '';
  592. }
  593. expandTriggerEventSpec(fireOnSpec) {
  594. if (_.isEmpty(fireOnSpec)) {
  595. throw new Error('no table change events specified to trigger on');
  596. }
  597. return _.map(fireOnSpec, (fireValue, fireKey) => {
  598. const EVENT_MAP = {
  599. 'insert': 'INSERT',
  600. 'update': 'UPDATE',
  601. 'delete': 'DELETE',
  602. 'truncate': 'TRUNCATE'
  603. };
  604. if (!EVENT_MAP[fireValue]) {
  605. throw new Error(`parseTriggerEventSpec: undefined trigger event ${fireKey}`);
  606. }
  607. let eventSpec = EVENT_MAP[fireValue];
  608. if (eventSpec === 'UPDATE') {
  609. if (Array.isArray(fireValue) && fireValue.length > 0) {
  610. eventSpec += ` OF ${fireValue.join(', ')}`;
  611. }
  612. }
  613. return eventSpec;
  614. }).join(' OR ');
  615. }
  616. pgEnumName(tableName, attr, options) {
  617. options = options || {};
  618. const tableDetails = this.extractTableDetails(tableName, options);
  619. let enumName = Utils.addTicks(Utils.generateEnumName(tableDetails.tableName, attr), '"');
  620. // pgListEnums requires the enum name only, without the schema
  621. if (options.schema !== false && tableDetails.schema) {
  622. enumName = this.quoteIdentifier(tableDetails.schema) + tableDetails.delimiter + enumName;
  623. }
  624. return enumName;
  625. }
  626. pgListEnums(tableName, attrName, options) {
  627. let enumName = '';
  628. const tableDetails = this.extractTableDetails(tableName, options);
  629. if (tableDetails.tableName && attrName) {
  630. enumName = ` AND t.typname=${this.pgEnumName(tableDetails.tableName, attrName, { schema: false }).replace(/"/g, "'")}`;
  631. }
  632. return 'SELECT t.typname enum_name, array_agg(e.enumlabel ORDER BY enumsortorder) enum_value FROM pg_type t ' +
  633. 'JOIN pg_enum e ON t.oid = e.enumtypid ' +
  634. 'JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace ' +
  635. `WHERE n.nspname = '${tableDetails.schema}'${enumName} GROUP BY 1`;
  636. }
  637. pgEnum(tableName, attr, dataType, options) {
  638. const enumName = this.pgEnumName(tableName, attr, options);
  639. let values;
  640. if (dataType.values) {
  641. values = `ENUM(${dataType.values.map(value => this.escape(value)).join(', ')})`;
  642. } else {
  643. values = dataType.toString().match(/^ENUM\(.+\)/)[0];
  644. }
  645. let sql = `CREATE TYPE ${enumName} AS ${values};`;
  646. if (!!options && options.force === true) {
  647. sql = this.pgEnumDrop(tableName, attr) + sql;
  648. }
  649. return sql;
  650. }
  651. pgEnumAdd(tableName, attr, value, options) {
  652. const enumName = this.pgEnumName(tableName, attr);
  653. let sql = `ALTER TYPE ${enumName} ADD VALUE `;
  654. if (semver.gte(this.sequelize.options.databaseVersion, '9.3.0')) {
  655. sql += 'IF NOT EXISTS ';
  656. }
  657. sql += this.escape(value);
  658. if (options.before) {
  659. sql += ` BEFORE ${this.escape(options.before)}`;
  660. } else if (options.after) {
  661. sql += ` AFTER ${this.escape(options.after)}`;
  662. }
  663. return sql;
  664. }
  665. pgEnumDrop(tableName, attr, enumName) {
  666. enumName = enumName || this.pgEnumName(tableName, attr);
  667. return `DROP TYPE IF EXISTS ${enumName}; `;
  668. }
  669. fromArray(text) {
  670. text = text.replace(/^{/, '').replace(/}$/, '');
  671. let matches = text.match(/("(?:\\.|[^"\\\\])*"|[^,]*)(?:\s*,\s*|\s*$)/ig);
  672. if (matches.length < 1) {
  673. return [];
  674. }
  675. matches = matches.map(m => m.replace(/",$/, '').replace(/,$/, '').replace(/(^"|"$)/g, ''));
  676. return matches.slice(0, -1);
  677. }
  678. padInt(i) {
  679. return i < 10 ? `0${i.toString()}` : i.toString();
  680. }
  681. dataTypeMapping(tableName, attr, dataType) {
  682. if (dataType.includes('PRIMARY KEY')) {
  683. dataType = dataType.replace('PRIMARY KEY', '');
  684. }
  685. if (dataType.includes('SERIAL')) {
  686. if (dataType.includes('BIGINT')) {
  687. dataType = dataType.replace('SERIAL', 'BIGSERIAL');
  688. dataType = dataType.replace('BIGINT', '');
  689. } else if (dataType.includes('SMALLINT')) {
  690. dataType = dataType.replace('SERIAL', 'SMALLSERIAL');
  691. dataType = dataType.replace('SMALLINT', '');
  692. } else {
  693. dataType = dataType.replace('INTEGER', '');
  694. }
  695. dataType = dataType.replace('NOT NULL', '');
  696. }
  697. if (dataType.startsWith('ENUM(')) {
  698. dataType = dataType.replace(/^ENUM\(.+\)/, this.pgEnumName(tableName, attr));
  699. }
  700. return dataType;
  701. }
  702. /**
  703. * Generates an SQL query that returns all foreign keys of a table.
  704. *
  705. * @param {string} tableName The name of the table.
  706. * @returns {string} The generated sql query.
  707. * @private
  708. */
  709. getForeignKeysQuery(tableName) {
  710. return 'SELECT conname as constraint_name, pg_catalog.pg_get_constraintdef(r.oid, true) as condef FROM pg_catalog.pg_constraint r ' +
  711. `WHERE r.conrelid = (SELECT oid FROM pg_class WHERE relname = '${tableName}' LIMIT 1) AND r.contype = 'f' ORDER BY 1;`;
  712. }
  713. /**
  714. * Generate common SQL prefix for getForeignKeyReferencesQuery.
  715. *
  716. * @returns {string}
  717. */
  718. _getForeignKeyReferencesQueryPrefix() {
  719. return 'SELECT ' +
  720. 'DISTINCT tc.constraint_name as constraint_name, ' +
  721. 'tc.constraint_schema as constraint_schema, ' +
  722. 'tc.constraint_catalog as constraint_catalog, ' +
  723. 'tc.table_name as table_name,' +
  724. 'tc.table_schema as table_schema,' +
  725. 'tc.table_catalog as table_catalog,' +
  726. 'kcu.column_name as column_name,' +
  727. 'ccu.table_schema AS referenced_table_schema,' +
  728. 'ccu.table_catalog AS referenced_table_catalog,' +
  729. 'ccu.table_name AS referenced_table_name,' +
  730. 'ccu.column_name AS referenced_column_name ' +
  731. 'FROM information_schema.table_constraints AS tc ' +
  732. 'JOIN information_schema.key_column_usage AS kcu ' +
  733. 'ON tc.constraint_name = kcu.constraint_name ' +
  734. 'JOIN information_schema.constraint_column_usage AS ccu ' +
  735. 'ON ccu.constraint_name = tc.constraint_name ';
  736. }
  737. /**
  738. * Generates an SQL query that returns all foreign keys details of a table.
  739. *
  740. * As for getForeignKeysQuery is not compatible with getForeignKeyReferencesQuery, so add a new function.
  741. *
  742. * @param {string} tableName
  743. * @param {string} catalogName
  744. * @param {string} schemaName
  745. */
  746. getForeignKeyReferencesQuery(tableName, catalogName, schemaName) {
  747. return `${this._getForeignKeyReferencesQueryPrefix()
  748. }WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '${tableName}'${
  749. catalogName ? ` AND tc.table_catalog = '${catalogName}'` : ''
  750. }${schemaName ? ` AND tc.table_schema = '${schemaName}'` : ''}`;
  751. }
  752. getForeignKeyReferenceQuery(table, columnName) {
  753. const tableName = table.tableName || table;
  754. const schema = table.schema;
  755. return `${this._getForeignKeyReferencesQueryPrefix()
  756. }WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name='${tableName}' AND kcu.column_name = '${columnName}'${
  757. schema ? ` AND tc.table_schema = '${schema}'` : ''}`;
  758. }
  759. /**
  760. * Generates an SQL query that removes a foreign key from a table.
  761. *
  762. * @param {string} tableName The name of the table.
  763. * @param {string} foreignKey The name of the foreign key constraint.
  764. * @returns {string} The generated sql query.
  765. * @private
  766. */
  767. dropForeignKeyQuery(tableName, foreignKey) {
  768. return `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.quoteIdentifier(foreignKey)};`;
  769. }
  770. }
  771. module.exports = PostgresQueryGenerator;