query-generator.js 31 KB

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