update.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. /*
  2. * Copyright (c) 2015-present, Vitaly Tomilov
  3. *
  4. * See the LICENSE file at the top-level directory of this distribution
  5. * for licensing information.
  6. *
  7. * Removal or modification of this copyright notice is prohibited.
  8. */
  9. const {assert} = require('../../assert');
  10. const {TableName} = require('../table-name');
  11. const {ColumnSet} = require('../column-set');
  12. const npm = {
  13. formatting: require('../../formatting'),
  14. utils: require('../../utils')
  15. };
  16. /**
  17. * @method helpers.update
  18. * @description
  19. * Generates a simplified `UPDATE` query for either one object or an array of objects.
  20. *
  21. * The resulting query needs a `WHERE` clause to be appended to it, to specify the update logic.
  22. * This is to allow for update conditions of any complexity that are easy to add.
  23. *
  24. * @param {object|object[]} data
  25. * An update object with properties for update values, or an array of such objects.
  26. *
  27. * When `data` is not a non-null object and not an array, it will throw {@link external:TypeError TypeError} = `Invalid parameter 'data' specified.`
  28. *
  29. * When `data` is an empty array, it will throw {@link external:TypeError TypeError} = `Cannot generate an UPDATE from an empty array.`
  30. *
  31. * When `data` is an array that contains a non-object value, the method will throw {@link external:Error Error} =
  32. * `Invalid update object at index N.`
  33. *
  34. * @param {array|helpers.Column|helpers.ColumnSet} [columns]
  35. * Set of columns to be updated.
  36. *
  37. * It is optional when `data` is a single object, and required when `data` is an array of objects. If not specified for an array
  38. * of objects, the method will throw {@link external:TypeError TypeError} = `Parameter 'columns' is required when updating multiple records.`
  39. *
  40. * When `columns` is not a {@link helpers.ColumnSet ColumnSet} object, a temporary {@link helpers.ColumnSet ColumnSet}
  41. * is created - from the value of `columns` (if it was specified), or from the value of `data` (if it is not an array).
  42. *
  43. * When the final {@link helpers.ColumnSet ColumnSet} is empty (no columns in it), the method will throw
  44. * {@link external:Error Error} = `Cannot generate an UPDATE without any columns.`, unless option `emptyUpdate` was specified.
  45. *
  46. * @param {helpers.TableName|Table|string} [table]
  47. * Table to be updated.
  48. *
  49. * It is normally a required parameter. But when `columns` is passed in as a {@link helpers.ColumnSet ColumnSet} object
  50. * with `table` set in it, that will be used when this parameter isn't specified. When neither is available, the method
  51. * will throw {@link external:Error Error} = `Table name is unknown.`
  52. *
  53. * @param {{}} [options]
  54. * An object with formatting options for multi-row `UPDATE` queries.
  55. *
  56. * @param {string} [options.tableAlias=t]
  57. * Name of the SQL variable that represents the destination table.
  58. *
  59. * @param {string} [options.valueAlias=v]
  60. * Name of the SQL variable that represents the values.
  61. *
  62. * @param {*} [options.emptyUpdate]
  63. * This is a convenience option, to avoid throwing an error when generating a conditional update results in no columns.
  64. *
  65. * When present, regardless of the value, this option overrides the method's behavior when applying `skip` logic results in no columns,
  66. * i.e. when every column is being skipped.
  67. *
  68. * By default, in that situation the method throws {@link external:Error Error} = `Cannot generate an UPDATE without any columns.`
  69. * But when this option is present, the method will instead return whatever value the option was passed.
  70. *
  71. * @returns {*}
  72. * An `UPDATE` query string that needs a `WHERE` condition appended.
  73. *
  74. * If it results in an empty update, and option `emptyUpdate` was passed in, then the method returns the value
  75. * to which the option was set.
  76. *
  77. * @see
  78. * {@link helpers.Column Column},
  79. * {@link helpers.ColumnSet ColumnSet},
  80. * {@link helpers.TableName TableName}
  81. *
  82. * @example
  83. *
  84. * const pgp = require('pg-promise')({
  85. * capSQL: true // if you want all generated SQL capitalized
  86. * });
  87. * const {update} = pgp.helpers;
  88. *
  89. * const dataSingle = {id: 1, val: 123, msg: 'hello'};
  90. * const dataMulti = [{id: 1, val: 123, msg: 'hello'}, {id: 2, val: 456, msg: 'world!'}];
  91. *
  92. * // Although column details can be taken from the data object, it is not
  93. * // a likely scenario for an update, unless updating the whole table:
  94. *
  95. * update(dataSingle, null, 'my-table');
  96. * //=> UPDATE "my-table" SET "id"=1,"val"=123,"msg"='hello'
  97. *
  98. * @example
  99. *
  100. * // A typical single-object update:
  101. *
  102. * // Dynamic conditions must be escaped/formatted properly:
  103. * const condition = pgp.as.format(' WHERE id = ${id}', dataSingle);
  104. *
  105. * update(dataSingle, ['val', 'msg'], 'my-table') + condition;
  106. * //=> UPDATE "my-table" SET "val"=123,"msg"='hello' WHERE id = 1
  107. *
  108. * @example
  109. *
  110. * // Column details are required for a multi-row `UPDATE`;
  111. * // Adding '?' in front of a column name means it is only for a WHERE condition:
  112. *
  113. * update(dataMulti, ['?id', 'val', 'msg'], 'my-table') + ' WHERE v.id = t.id';
  114. * //=> UPDATE "my-table" AS t SET "val"=v."val","msg"=v."msg" FROM (VALUES(1,123,'hello'),(2,456,'world!'))
  115. * // AS v("id","val","msg") WHERE v.id = t.id
  116. *
  117. * @example
  118. *
  119. * // Column details from a reusable ColumnSet (recommended for performance):
  120. * const {ColumnSet, update} = pgp.helpers;
  121. *
  122. * const cs = new ColumnSet(['?id', 'val', 'msg'], {table: 'my-table'});
  123. *
  124. * update(dataMulti, cs) + ' WHERE v.id = t.id';
  125. * //=> UPDATE "my-table" AS t SET "val"=v."val","msg"=v."msg" FROM (VALUES(1,123,'hello'),(2,456,'world!'))
  126. * // AS v("id","val","msg") WHERE v.id = t.id
  127. *
  128. * @example
  129. *
  130. * // Using parameter `options` to change the default alias names:
  131. *
  132. * update(dataMulti, cs, null, {tableAlias: 'X', valueAlias: 'Y'}) + ' WHERE Y.id = X.id';
  133. * //=> UPDATE "my-table" AS X SET "val"=Y."val","msg"=Y."msg" FROM (VALUES(1,123,'hello'),(2,456,'world!'))
  134. * // AS Y("id","val","msg") WHERE Y.id = X.id
  135. *
  136. * @example
  137. *
  138. * // Handling an empty update
  139. * const {ColumnSet, update} = pgp.helpers;
  140. *
  141. * const cs = new ColumnSet(['?id', '?name'], {table: 'tt'}); // no actual update-able columns
  142. * const result = update(dataMulti, cs, null, {emptyUpdate: 123});
  143. * if(result === 123) {
  144. * // We know the update is empty, i.e. no columns that can be updated;
  145. * // And it didn't throw because we specified `emptyUpdate` option.
  146. * }
  147. */
  148. function update(data, columns, table, options, capSQL) {
  149. if (!data || typeof data !== 'object') {
  150. throw new TypeError('Invalid parameter \'data\' specified.');
  151. }
  152. const isArray = Array.isArray(data);
  153. if (isArray && !data.length) {
  154. throw new TypeError('Cannot generate an UPDATE from an empty array.');
  155. }
  156. if (columns instanceof ColumnSet) {
  157. if (npm.utils.isNull(table)) {
  158. table = columns.table;
  159. }
  160. } else {
  161. if (isArray && npm.utils.isNull(columns)) {
  162. throw new TypeError('Parameter \'columns\' is required when updating multiple records.');
  163. }
  164. columns = new ColumnSet(columns || data);
  165. }
  166. options = assert(options, ['tableAlias', 'valueAlias', 'emptyUpdate']);
  167. const format = npm.formatting.as.format,
  168. useEmptyUpdate = 'emptyUpdate' in options,
  169. fmOptions = {capSQL};
  170. if (isArray) {
  171. const tableAlias = npm.formatting.as.alias(npm.utils.isNull(options.tableAlias) ? 't' : options.tableAlias);
  172. const valueAlias = npm.formatting.as.alias(npm.utils.isNull(options.valueAlias) ? 'v' : options.valueAlias);
  173. const q = capSQL ? sql.multi.capCase : sql.multi.lowCase;
  174. const actualColumns = columns.columns.filter(c => !c.cnd);
  175. if (checkColumns(actualColumns)) {
  176. return options.emptyUpdate;
  177. }
  178. checkTable();
  179. const targetCols = actualColumns.map(c => c.escapedName + '=' + valueAlias + '.' + c.escapedName).join();
  180. const values = data.map((d, index) => {
  181. if (!d || typeof d !== 'object') {
  182. throw new Error(`Invalid update object at index ${index}.`);
  183. }
  184. return '(' + format(columns.variables, columns.prepare(d), fmOptions) + ')';
  185. }).join();
  186. return format(q, [table.name, tableAlias, targetCols, values, valueAlias, columns.names], fmOptions);
  187. }
  188. const updates = columns.assign({source: data});
  189. if (checkColumns(updates)) {
  190. return options.emptyUpdate;
  191. }
  192. checkTable();
  193. const query = capSQL ? sql.single.capCase : sql.single.lowCase;
  194. return format(query, table.name) + format(updates, columns.prepare(data), fmOptions);
  195. function checkTable() {
  196. if (table && !(table instanceof TableName)) {
  197. table = new TableName(table);
  198. }
  199. if (!table) {
  200. throw new Error('Table name is unknown.');
  201. }
  202. }
  203. function checkColumns(cols) {
  204. if (!cols.length) {
  205. if (useEmptyUpdate) {
  206. return true;
  207. }
  208. throw new Error('Cannot generate an UPDATE without any columns.');
  209. }
  210. }
  211. }
  212. const sql = {
  213. single: {
  214. lowCase: 'update $1^ set ',
  215. capCase: 'UPDATE $1^ SET '
  216. },
  217. multi: {
  218. lowCase: 'update $1^ as $2^ set $3^ from (values$4^) as $5^($6^)',
  219. capCase: 'UPDATE $1^ AS $2^ SET $3^ FROM (VALUES$4^) AS $5^($6^)'
  220. }
  221. };
  222. module.exports = {update};