@axinom/mosaic-db-common 0.14.1-rc.6 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,23 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.15.0](https://dev.azure.com/axinom/CMS/_git/Navy/branchCompare?baseVersion=GT@axinom/mosaic-db-common@0.14.0&targetVersion=GT@axinom/mosaic-db-common@0.15.0) (2022-04-01)
7
+
8
+
9
+ ### Features
10
+
11
+ * ax-define functions length checking ([6c4a4ae](https://dev.azure.com/axinom/CMS/_git/Navy/commit/6c4a4ae4b188a710e7d8defdab10304bc334844c)), closes [#34155](https://dev.azure.com/axinom/CMS/_workitems/edit/34155)
12
+ * ax-define timestamp propogation ([b9a69b8](https://dev.azure.com/axinom/CMS/_git/Navy/commit/b9a69b859e449e54aef4dc20a0bad3be69ccf676)), closes [#34380](https://dev.azure.com/axinom/CMS/_workitems/edit/34380)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * ax-define user trigger creation fails ([e989bd2](https://dev.azure.com/axinom/CMS/_git/Navy/commit/e989bd2dd9bc951f6d3b8492d7c0f43fd07f4a1d))
18
+ * generate-vscode-sql-snippets can now be launched ([5908776](https://dev.azure.com/axinom/CMS/_git/Navy/commit/59087766f02332de6ca3a78d0d4143ad27a74780)), closes [#34157](https://dev.azure.com/axinom/CMS/_workitems/edit/34157)
19
+ * minor PR comment adjustment ([b5ad8f4](https://dev.azure.com/axinom/CMS/_git/Navy/commit/b5ad8f446c35322e797e804c018255e6d051f8c2)), closes [#34157](https://dev.azure.com/axinom/CMS/_workitems/edit/34157)
20
+
21
+
22
+
6
23
  ## [0.14.0](https://dev.azure.com/axinom/CMS/_git/Navy/branchCompare?baseVersion=GT@axinom/mosaic-db-common@0.13.0&targetVersion=GT@axinom/mosaic-db-common@0.14.0) (2022-03-16)
7
24
 
8
25
 
@@ -28,3 +28,32 @@ BEGIN
28
28
  RAISE EXCEPTION '%', format(error_message, VARIADIC placeholder_values) using errcode = error_code;
29
29
  END;
30
30
  $$;
31
+
32
+ /*-snippet
33
+ {
34
+ "body": [
35
+ "PERFORM ax_utils.validate_identifier_length('${1:identifier}');"
36
+ ],
37
+ "description": [
38
+ "Raises an exception if the provided identifer exceeds the PostgreSQL max length (63 bytes).",
39
+ "This can be useful to prevent name truncation, identifier collisions and silently dropped constraints.",
40
+ "If called from a function, the calling function will be referenced in the error message.",
41
+ "An optional second 'hint' parameter can be provided which will be appended to the error message."
42
+ ]
43
+ }
44
+ snippet-*/
45
+ CREATE OR REPLACE FUNCTION ax_utils.validate_identifier_length(identifier text, hint text default '') RETURNS void
46
+ LANGUAGE plpgsql
47
+ AS $$
48
+ DECLARE
49
+ stack text; fcesig text; callinfo text;
50
+ BEGIN
51
+ IF LENGTH(identifier) > 63 THEN
52
+ -- get sig of calling function
53
+ GET DIAGNOSTICS stack = PG_CONTEXT;
54
+ fcesig := substring(substring(stack from 'function (.*)') from 'function (.*?) line');
55
+ callinfo := CASE WHEN fcesig IS NULL THEN '' ELSE 'Invalid identifier in ' || fcesig::regprocedure::text || '. ' END;
56
+ perform ax_utils.raise_error('%sIdentifier "%s" exceeds 63 bytes. %s', 'SETUP', callinfo, identifier, hint);
57
+ END IF;
58
+ END;
59
+ $$;
@@ -71,9 +71,10 @@ $$;
71
71
  "SELECT ax_define.define_timestamps_trigger('${1:table_name}', '${2:app_public}');"
72
72
  ],
73
73
  "description": [
74
- "Defines timestamp trigger which automatically populates 'created_date' and 'updated_date' columns.\n",
75
- "N.B! It is recommended to use 'ax_define.define_audit_date_fields_on_table' to define both columns and triggers in one go.",
76
- "Use this one if you really need to define triggers separately."
74
+ "Defines timestamp trigger which automatically populates 'created_date' and 'updated_date' columns.",
75
+ "It is recommended to use 'ax_define.define_audit_date_fields_on_table' to define both columns and triggers in one go.",
76
+ "Use this one if you really need to define triggers separately.\n",
77
+ "N.B! The trigger function is not compatible with JSON columns. Use JSONB instead."
77
78
  ]
78
79
  }
79
80
  snippet-*/
@@ -82,6 +83,7 @@ CREATE OR REPLACE FUNCTION ax_define.define_timestamps_trigger(tableName text, s
82
83
  AS $$
83
84
  BEGIN
84
85
  PERFORM ax_define.drop_timestamps_trigger(tableName, schemaName);
86
+ -- Full row comparison is not compatible with column type JSON. Expect error: "could not identify an equality operator for type json"
85
87
  EXECUTE 'CREATE trigger _100_timestamps BEFORE UPDATE ON ' || schemaName || '.' || tableName ||
86
88
  ' for each ROW when (old.* is distinct from new.*) EXECUTE PROCEDURE ax_utils.tg__timestamps();';
87
89
  END;
@@ -102,6 +104,7 @@ CREATE OR REPLACE FUNCTION ax_define.drop_users_trigger(tableName text, schemaNa
102
104
  AS $$
103
105
  BEGIN
104
106
  EXECUTE 'DROP trigger IF EXISTS _200_username on ' || schemaName || '.' || tableName || ';';
107
+ EXECUTE 'DROP trigger IF EXISTS _200_username_before_insert on ' || schemaName || '.' || tableName || ';';
105
108
  END;
106
109
  $$;
107
110
 
@@ -111,9 +114,10 @@ $$;
111
114
  "SELECT ax_define.define_users_trigger('${1:table_name}', '${2:app_public}');"
112
115
  ],
113
116
  "description": [
114
- "Defines users trigger which automatically populates 'created_user' and 'updated_user' columns.\n",
115
- "N.B! It is recommended to use 'ax_define.define_audit_user_fields_on_table' to define both columns and triggers in one go.",
116
- "Use this one if you really need to define triggers separately."
117
+ "Defines users trigger which automatically populates 'created_user' and 'updated_user' columns.",
118
+ "It is recommended to use 'ax_define.define_audit_user_fields_on_table' to define both columns and triggers in one go.",
119
+ "Use this one if you really need to define triggers separately.\n",
120
+ "N.B! The trigger function is not compatible with JSON columns. Use JSONB instead."
117
121
  ]
118
122
  }
119
123
  snippet-*/
@@ -122,8 +126,12 @@ CREATE OR REPLACE FUNCTION ax_define.define_users_trigger(tableName text, schema
122
126
  AS $$
123
127
  BEGIN
124
128
  PERFORM ax_define.drop_users_trigger(tableName, schemaName);
125
- EXECUTE 'CREATE trigger _200_username BEFORE INSERT OR UPDATE ON ' || schemaName || '.' || tableName ||
129
+ -- Full row comparison is not compatible with column type JSON. Expect error: "could not identify an equality operator for type json"
130
+ EXECUTE 'CREATE trigger _200_username BEFORE UPDATE ON ' || schemaName || '.' || tableName ||
126
131
  ' for each ROW when (old.* is distinct from new.*) EXECUTE PROCEDURE ax_utils.tg__username();';
132
+ -- INSERT trigger's WHEN condition cannot reference OLD values
133
+ EXECUTE 'CREATE trigger _200_username_before_insert BEFORE INSERT ON ' || schemaName || '.' || tableName ||
134
+ ' EXECUTE PROCEDURE ax_utils.tg__username();';
127
135
  END;
128
136
  $$;
129
137
 
@@ -216,15 +224,18 @@ $$
216
224
  "SELECT ax_define.drop_index('${1:column_name}', '${2:table_name}');"
217
225
  ],
218
226
  "description": [
219
- "Drops previously defined index. Applies to both regular and unique indexes."
227
+ "Drops previously defined index. Applies to both regular and unique indexes.\n",
228
+ "Third (optional) parameter: the unique name for this index. If the value is NULL then the name will be generated from the table & field name."
220
229
  ]
221
230
  }
222
231
  snippet-*/
223
- CREATE OR REPLACE FUNCTION ax_define.drop_index(fieldName text, tableName text) RETURNS void
232
+ CREATE OR REPLACE FUNCTION ax_define.drop_index(fieldName text, tableName text, indexName text default NULL) RETURNS void
224
233
  LANGUAGE plpgsql
225
234
  AS $$
226
235
  BEGIN
227
- EXECUTE 'DROP INDEX IF EXISTS idx_' || tableName || '_' || fieldName || ' cascade;';
236
+ SELECT COALESCE(indexName, 'idx_' || tableName || '_' || fieldName) INTO indexName;
237
+ PERFORM ax_utils.validate_identifier_length(indexName, 'If the auto-generated name is too long then an "indexName" argument must be provided.');
238
+ EXECUTE 'DROP INDEX IF EXISTS ' || indexName || ' cascade;';
228
239
  END;
229
240
  $$;
230
241
 
@@ -235,16 +246,18 @@ $$;
235
246
  ],
236
247
  "description": [
237
248
  "Defines a regular index on a column.\n",
238
- "N.B! Keep in mind, that the maximum index name length is 63 characters and it will be truncated if exceeded."
249
+ "Fourth (optional) parameter: a unique name for this index. If the value is NULL then a name will be generated from the table & field name."
239
250
  ]
240
251
  }
241
252
  snippet-*/
242
- CREATE OR REPLACE FUNCTION ax_define.define_index(fieldName text, tableName text, schemaName text) RETURNS void
253
+ CREATE OR REPLACE FUNCTION ax_define.define_index(fieldName text, tableName text, schemaName text, indexName text default NULL) RETURNS void
243
254
  LANGUAGE plpgsql
244
255
  AS $$
245
256
  BEGIN
246
- PERFORM ax_define.drop_index(fieldName, tableName);
247
- EXECUTE 'CREATE INDEX idx_' || tableName || '_' || fieldName || ' ON ' || schemaName || '.' || tableName || ' (' || fieldName || ');';
257
+ SELECT COALESCE(indexName, 'idx_' || tableName || '_' || fieldName) INTO indexName;
258
+ PERFORM ax_utils.validate_identifier_length(indexName, 'If the auto-generated name is too long then an "indexName" argument must be provided.');
259
+ PERFORM ax_define.drop_index(fieldName, tableName, indexName);
260
+ EXECUTE 'CREATE INDEX ' || indexName || ' ON ' || schemaName || '.' || tableName || ' (' || fieldName || ');';
248
261
  END;
249
262
  $$;
250
263
 
@@ -254,17 +267,20 @@ $$;
254
267
  "SELECT ax_define.drop_multiple_field_index('{\"${1:column_name_one}\", \"${2:column_name_two}\"}','${3:table_name}');"
255
268
  ],
256
269
  "description": [
257
- "Drops previously defined multi-field index."
270
+ "Drops previously defined multi-field index.\n",
271
+ "Third (optional) parameter: the unique name for this index. If the value is NULL then the name will be generated from the table & field names."
258
272
  ]
259
273
  }
260
274
  snippet-*/
261
- CREATE OR REPLACE FUNCTION ax_define.drop_multiple_field_index(fieldNames text[], tableName text) RETURNS void
275
+ CREATE OR REPLACE FUNCTION ax_define.drop_multiple_field_index(fieldNames text[], tableName text, indexName text default NULL) RETURNS void
262
276
  LANGUAGE plpgsql
263
277
  AS $$
264
278
  DECLARE
265
279
  fieldNamesConcat text = array_to_string(array_agg(fieldNames), '_');
266
280
  BEGIN
267
- EXECUTE 'DROP INDEX IF EXISTS idx_' || tableName || '_' || fieldNamesConcat || ' cascade;';
281
+ SELECT COALESCE(indexName, 'idx_' || tableName || '_' || fieldNamesConcat) INTO indexName;
282
+ PERFORM ax_utils.validate_identifier_length(indexName, 'If the auto-generated name is too long then an "indexName" argument must be provided.');
283
+ EXECUTE 'DROP INDEX IF EXISTS ' || indexName || ' cascade;';
268
284
  END;
269
285
  $$;
270
286
 
@@ -275,19 +291,21 @@ $$;
275
291
  ],
276
292
  "description": [
277
293
  "Defines a regular index on multiple columns.\n",
278
- "N.B! Keep in mind, that the maximum index name length is 63 characters and it will be truncated if exceeded."
294
+ "Fourth (optional) parameter: a unique name for this index. If the value is NULL then a name will be generated from the table & field names."
279
295
  ]
280
296
  }
281
297
  snippet-*/
282
- CREATE OR REPLACE FUNCTION ax_define.define_multiple_field_index(fieldNames text[], tableName text, schemaName text) RETURNS void
298
+ CREATE OR REPLACE FUNCTION ax_define.define_multiple_field_index(fieldNames text[], tableName text, schemaName text, indexName text default NULL) RETURNS void
283
299
  LANGUAGE plpgsql
284
300
  AS $$
285
301
  DECLARE
286
302
  fieldNamesConcat text = array_to_string(array_agg(fieldNames), '_');
287
303
  fieldList text = array_to_string(array_agg(fieldNames), ', ');
288
304
  BEGIN
289
- PERFORM ax_define.drop_index(fieldNamesConcat, tableName);
290
- EXECUTE 'CREATE INDEX idx_' || tableName || '_' || fieldNamesConcat || ' ON ' || schemaName || '.' || tableName || ' (' || fieldList || ');';
305
+ SELECT COALESCE(indexName, 'idx_' || tableName || '_' || fieldNamesConcat) INTO indexName;
306
+ PERFORM ax_utils.validate_identifier_length(indexName, 'If the auto-generated name is too long then an "indexName" argument must be provided.');
307
+ PERFORM ax_define.drop_multiple_field_index(fieldNames, tableName, indexName);
308
+ EXECUTE 'CREATE INDEX ' || indexName || ' ON ' || schemaName || '.' || tableName || ' (' || fieldList || ');';
291
309
  END;
292
310
  $$;
293
311
 
@@ -298,16 +316,21 @@ $$;
298
316
  "SELECT ax_define.drop_indexes_with_id('${1:column_name}', '${2:table_name}');"
299
317
  ],
300
318
  "description": [
301
- "Drops previously defined ASC/DESC indexes based on id and one other column."
319
+ "Drops previously defined ASC/DESC indexes based on id and one other column.\n",
320
+ "Third & fourth (optional) parameters: unique names for the indexes. If either value is NULL then the name will be generated from the table & field name."
302
321
  ]
303
322
  }
304
323
  snippet-*/
305
- CREATE OR REPLACE FUNCTION ax_define.drop_indexes_with_id(fieldName text, tableName text) RETURNS void
324
+ CREATE OR REPLACE FUNCTION ax_define.drop_indexes_with_id(fieldName text, tableName text, indexNameAsc text default NULL, indexNameDesc text default NULL) RETURNS void
306
325
  LANGUAGE plpgsql
307
326
  AS $$
308
327
  BEGIN
309
- EXECUTE 'DROP INDEX IF EXISTS idx_' || tableName || '_' || fieldName || '_asc_with_id cascade;';
310
- EXECUTE 'DROP INDEX IF EXISTS idx_' || tableName || '_' || fieldName || '_desc_with_id cascade;';
328
+ SELECT COALESCE(indexNameAsc, 'idx_' || tableName || '_' || fieldName || '_asc_with_id') INTO indexNameAsc;
329
+ SELECT COALESCE(indexNameDesc, 'idx_' || tableName || '_' || fieldName || '_desc_with_id') INTO indexNameDesc;
330
+ PERFORM ax_utils.validate_identifier_length(indexNameAsc, 'If the auto-generated name is too long then an "indexNameAsc" argument must be provided.');
331
+ PERFORM ax_utils.validate_identifier_length(indexNameDesc, 'If the auto-generated name is too long then an "indexNameDesc" argument must be provided.');
332
+ EXECUTE 'DROP INDEX IF EXISTS ' || indexNameAsc || ' cascade;';
333
+ EXECUTE 'DROP INDEX IF EXISTS ' || indexNameDesc || ' cascade;';
311
334
  END;
312
335
  $$;
313
336
 
@@ -319,15 +342,19 @@ $$;
319
342
  "description": [
320
343
  "Defines ASC/DESC indexes based on id and one other column.",
321
344
  "Use these indexes if there is a plan to perform sorting by this column.\n",
322
- "N.B! Keep in mind, that the maximum index name length is 63 characters and it will be truncated if exceeded."
345
+ "Fourth & fifth (optional) parameters: unique names for the indexes. If either value is NULL then a name will be generated from the table & field name."
323
346
  ]
324
347
  }
325
348
  snippet-*/
326
- CREATE OR REPLACE FUNCTION ax_define.define_indexes_with_id(fieldName text, tableName text, schemaName text) RETURNS void
349
+ CREATE OR REPLACE FUNCTION ax_define.define_indexes_with_id(fieldName text, tableName text, schemaName text, indexNameAsc text default NULL, indexNameDesc text default NULL) RETURNS void
327
350
  LANGUAGE plpgsql
328
351
  AS $$
329
352
  BEGIN
330
- PERFORM ax_define.drop_indexes_with_id(fieldName, tableName);
353
+ SELECT COALESCE(indexNameAsc, 'idx_' || tableName || '_' || fieldName || '_asc_with_id') INTO indexNameAsc;
354
+ SELECT COALESCE(indexNameDesc, 'idx_' || tableName || '_' || fieldName || '_desc_with_id') INTO indexNameDesc;
355
+ PERFORM ax_utils.validate_identifier_length(indexNameAsc, 'If the auto-generated name is too long then an "indexNameAsc" argument must be provided.');
356
+ PERFORM ax_utils.validate_identifier_length(indexNameDesc, 'If the auto-generated name is too long then an "indexNameDesc" argument must be provided.');
357
+ PERFORM ax_define.drop_indexes_with_id(fieldName, tableName, indexNameAsc, indexNameDesc);
331
358
  EXECUTE 'CREATE INDEX idx_' || tableName || '_' || fieldName || '_asc_with_id ON ' || schemaName || '.' || tableName || ' (' || fieldName || ' ASC, id ASC);';
332
359
  EXECUTE 'CREATE INDEX idx_' || tableName || '_' || fieldName || '_desc_with_id ON ' || schemaName || '.' || tableName || ' (' || fieldName || ' DESC, id ASC);';
333
360
  END;
@@ -341,16 +368,18 @@ $$;
341
368
  "description": [
342
369
  "Defines a UNIQUE index on a column.",
343
370
  "You can drop a unique index by using the normal 'ax_define.drop_index' function.\n",
344
- "N.B! Keep in mind, that the maximum index name length is 63 characters and it will be truncated if exceeded."
371
+ "Fourth (optional) parameter: a unique name for the index. If the value is NULL then a name will be generated from the table & field name."
345
372
  ]
346
373
  }
347
374
  snippet-*/
348
- CREATE OR REPLACE FUNCTION ax_define.define_unique_index(fieldName text, tableName text, schemaName text) RETURNS void
375
+ CREATE OR REPLACE FUNCTION ax_define.define_unique_index(fieldName text, tableName text, schemaName text, indexName text default NULL) RETURNS void
349
376
  LANGUAGE plpgsql
350
377
  AS $$
351
378
  BEGIN
352
- PERFORM ax_define.drop_index(fieldName, tableName);
353
- EXECUTE 'CREATE UNIQUE INDEX idx_' || tableName || '_' || fieldName || ' ON ' || schemaName || '.' || tableName || ' (' || fieldName || ');';
379
+ SELECT COALESCE(indexName, 'idx_' || tableName || '_' || fieldName) INTO indexName;
380
+ PERFORM ax_utils.validate_identifier_length(indexName, 'If the auto-generated name is too long then an "indexName" argument must be provided.');
381
+ PERFORM ax_define.drop_index(fieldName, tableName, indexName);
382
+ EXECUTE 'CREATE UNIQUE INDEX ' || indexName || ' ON ' || schemaName || '.' || tableName || ' (' || fieldName || ');';
354
383
  END;
355
384
  $$;
356
385
 
@@ -360,15 +389,18 @@ $$;
360
389
  "SELECT ax_define.drop_unique_constraint('${1:column_name}', '${2:table_name}', '${3:app_public}');"
361
390
  ],
362
391
  "description": [
363
- "Drops a UNIQUE constraint based on a column."
392
+ "Drops a UNIQUE constraint based on a column.\n",
393
+ "Fourth (optional) parameter: the unique name for the contraint. If the value is NULL then the name will be generated from the table & field name."
364
394
  ]
365
395
  }
366
396
  snippet-*/
367
- CREATE OR REPLACE FUNCTION ax_define.drop_unique_constraint(fieldName text, tableName text, schemaName text) RETURNS void
397
+ CREATE OR REPLACE FUNCTION ax_define.drop_unique_constraint(fieldName text, tableName text, schemaName text, constraintName text default NULL) RETURNS void
368
398
  LANGUAGE plpgsql
369
399
  AS $$
370
400
  BEGIN
371
- EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' DROP CONSTRAINT IF EXISTS ' || tableName || '_' || fieldName || '_is_unique;';
401
+ SELECT COALESCE(constraintName, tableName || '_' || fieldName || '_is_unique') INTO constraintName;
402
+ PERFORM ax_utils.validate_identifier_length(constraintName, 'If the auto-generated name is too long then a "constraintName" argument must be provided.');
403
+ EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' DROP CONSTRAINT IF EXISTS ' || constraintName || ';';
372
404
  END;
373
405
  $$;
374
406
 
@@ -378,16 +410,19 @@ $$;
378
410
  "SELECT ax_define.define_unique_constraint('${1:column_name}', '${2:table_name}', '${3:app_public}');"
379
411
  ],
380
412
  "description": [
381
- "Defines a UNIQUE constraint on a column."
413
+ "Defines a UNIQUE constraint on a column.\n",
414
+ "Fourth (optional) parameter: a unique name for the contraint. If the value is NULL then a name will be generated from the table & field name."
382
415
  ]
383
416
  }
384
417
  snippet-*/
385
- CREATE OR REPLACE FUNCTION ax_define.define_unique_constraint(fieldName text, tableName text, schemaName text) RETURNS void
418
+ CREATE OR REPLACE FUNCTION ax_define.define_unique_constraint(fieldName text, tableName text, schemaName text, constraintName text default NULL) RETURNS void
386
419
  LANGUAGE plpgsql
387
420
  AS $$
388
421
  BEGIN
389
- PERFORM ax_define.drop_unique_constraint(fieldName, tableName, schemaName);
390
- EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' ADD CONSTRAINT ' || tableName || '_' || fieldName || '_is_unique UNIQUE (' || fieldName || ');';
422
+ SELECT COALESCE(constraintName, tableName || '_' || fieldName || '_is_unique') INTO constraintName;
423
+ PERFORM ax_utils.validate_identifier_length(constraintName, 'If the auto-generated name is too long then a "constraintName" argument must be provided.');
424
+ PERFORM ax_define.drop_unique_constraint(fieldName, tableName, schemaName, constraintName);
425
+ EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' ADD CONSTRAINT ' ||constraintName || ' UNIQUE (' || fieldName || ');';
391
426
  END;
392
427
  $$;
393
428
 
@@ -399,16 +434,19 @@ $$;
399
434
  "description": [
400
435
  "Defines a deferred UNIQUE constraint on a column.\n",
401
436
  "Uniqueness is only checked when the transaction is committed.",
402
- "If you want to drop it - use 'ax_define.drop_unique_constraint'."
437
+ "If you want to drop it - use 'ax_define.drop_unique_constraint'.\n",
438
+ "Fourth (optional) parameter: a unique name for the contraint. If the value is NULL then a name will be generated from the table & field name."
403
439
  ]
404
440
  }
405
441
  snippet-*/
406
- CREATE OR REPLACE FUNCTION ax_define.define_deferred_unique_constraint(fieldName text, tableName text, schemaName text) RETURNS void
442
+ CREATE OR REPLACE FUNCTION ax_define.define_deferred_unique_constraint(fieldName text, tableName text, schemaName text, constraintName text default NULL) RETURNS void
407
443
  LANGUAGE plpgsql
408
444
  AS $$
409
445
  BEGIN
410
- PERFORM ax_define.drop_unique_constraint(fieldName, tableName, schemaName);
411
- EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' ADD CONSTRAINT ' || tableName || '_' || fieldName || '_is_unique UNIQUE (' || fieldName || ') DEFERRABLE INITIALLY DEFERRED;';
446
+ SELECT COALESCE(constraintName, tableName || '_' || fieldName || '_is_unique') INTO constraintName;
447
+ PERFORM ax_utils.validate_identifier_length(constraintName, 'If the auto-generated name is too long then a "constraintName" argument must be provided.');
448
+ PERFORM ax_define.drop_unique_constraint(fieldName, tableName, schemaName, constraintName);
449
+ EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' ADD CONSTRAINT ' || constraintName || ' UNIQUE (' || fieldName || ') DEFERRABLE INITIALLY DEFERRED;';
412
450
  END;
413
451
  $$;
414
452
 
@@ -418,15 +456,18 @@ $$;
418
456
  "SELECT ax_define.drop_like_index('${1:column_name}', '${2:table_name}');"
419
457
  ],
420
458
  "description": [
421
- "Drops a gin_trgm_ops index (for LIKE/ILIKE searches) based on a column."
459
+ "Drops a gin_trgm_ops index (for LIKE/ILIKE searches) based on a column.\n",
460
+ "Third (optional) parameter: the unique name for the index. If the value is NULL then the name will be generated from the table & field name."
422
461
  ]
423
462
  }
424
463
  snippet-*/
425
- CREATE OR REPLACE FUNCTION ax_define.drop_like_index(fieldName text, tableName text) RETURNS void
464
+ CREATE OR REPLACE FUNCTION ax_define.drop_like_index(fieldName text, tableName text, indexName text default NULL) RETURNS void
426
465
  LANGUAGE plpgsql
427
466
  AS $$
428
467
  BEGIN
429
- EXECUTE 'DROP INDEX IF EXISTS idx_trgm_' || tableName || '_' || fieldName || ' cascade;';
468
+ SELECT COALESCE(indexName, 'idx_trgm_' || tableName || '_' || fieldName) INTO indexName;
469
+ PERFORM ax_utils.validate_identifier_length(indexName, 'If the auto-generated name is too long then an "indexName" argument must be provided.');
470
+ EXECUTE 'DROP INDEX IF EXISTS ' || indexName || ' cascade;';
430
471
  END;
431
472
  $$;
432
473
 
@@ -438,16 +479,19 @@ $$;
438
479
  "description": [
439
480
  "Defines a gin_trgm_ops index (for LIKE/ILIKE searches) on a column.",
440
481
  "Read more here: https://niallburkley.com/blog/index-columns-for-like-in-postgres/\n",
441
- "N.B! Keep in mind, that the maximum index name length is 63 characters and it will be truncated if exceeded."
482
+ "Fourth (optional) parameter: a unique name for the index. If the value is NULL then a name will be generated from the table & field name."
442
483
  ]
443
484
  }
444
485
  snippet-*/
445
- CREATE OR REPLACE FUNCTION ax_define.define_like_index(fieldName text, tableName text, schemaName text) RETURNS void
486
+
487
+ CREATE OR REPLACE FUNCTION ax_define.define_like_index(fieldName text, tableName text, schemaName text, indexName text default NULL) RETURNS void
446
488
  LANGUAGE plpgsql
447
489
  AS $$
448
490
  BEGIN
449
- PERFORM ax_define.drop_like_index(fieldName, tableName);
450
- EXECUTE 'CREATE INDEX idx_trgm_' || tableName || '_' || fieldName || ' ON ' || schemaName || '.' || tableName || ' USING gin (' || fieldName || ' gin_trgm_ops);';
491
+ SELECT COALESCE(indexName, 'idx_trgm_' || tableName || '_' || fieldName) INTO indexName;
492
+ PERFORM ax_utils.validate_identifier_length(indexName, 'If the auto-generated name is too long then an "indexName" argument must be provided.');
493
+ PERFORM ax_define.drop_like_index(fieldName, tableName, indexName);
494
+ EXECUTE 'CREATE INDEX ' || indexName || ' ON ' || schemaName || '.' || tableName || ' USING gin (' || fieldName || ' gin_trgm_ops);';
451
495
  END;
452
496
  $$;
453
497
 
@@ -576,7 +620,8 @@ $$;
576
620
  "Also defines triggers that populate these columns on row create/update.\n",
577
621
  "The third parameter is a placeholder for the default username value.",
578
622
  "Please make sure that this placeholder is defined in the 'graphile-migrate' settings object,",
579
- "in the placeholders property with the same name e.g. ':DEFAULT_USERNAME'."
623
+ "in the placeholders property with the same name e.g. ':DEFAULT_USERNAME'.\n",
624
+ "N.B! The trigger functions are not compatible with JSON columns. Use JSONB instead."
580
625
  ]
581
626
  }
582
627
  snippet-*/
@@ -863,7 +908,8 @@ $$;
863
908
  "If the column does not exist - it is created with type text.\n",
864
909
  "The sixth parameter is optional. Do not define if the column already exists or if the new column should not have a default value.",
865
910
  "The seventh parameter is optional and decides if the column should be required or optional.",
866
- "The default value is 'NOT NULL'. Use 'NULL' if the new column should be optional."
911
+ "The default value is 'NOT NULL'. Use 'NULL' if the new column should be optional.",
912
+ "The eighth parameter is optional: a unique name for the generated constraint. If the value is NULL then a name will be generated from the table & column name."
867
913
  ]
868
914
  }
869
915
  snippet-*/
@@ -874,12 +920,14 @@ CREATE OR REPLACE FUNCTION ax_define.bind_enum_schema_table_to_table(
874
920
  enumName text, -- 'example_status'
875
921
  enumSchemaName text, -- 'app_public'
876
922
  defaultEnumValue text default '', -- 'DASH'
877
- notNullOptions text default 'NOT NULL' -- 'NOT NULL'
923
+ notNullOptions text default 'NOT NULL', -- 'NOT NULL'
924
+ constraintName text default NULL
878
925
  ) RETURNS void LANGUAGE plpgsql AS $$
879
926
  DECLARE
880
- fk_constraint_name TEXT = tableName || '_' || columnName || '_fkey';
881
927
  default_setting TEXT = '';
882
928
  BEGIN
929
+ SELECT COALESCE(constraintName, tableName || '_' || columnName || '_fkey') INTO constraintName;
930
+ PERFORM ax_utils.validate_identifier_length(constraintName, 'If the auto-generated name is too long then a "constraintName" argument must be provided.');
883
931
  IF NOT coalesce(defaultEnumValue, '') = '' THEN
884
932
  default_setting = 'DEFAULT ''' || defaultEnumValue || '''::text';
885
933
  END IF;
@@ -888,7 +936,7 @@ BEGIN
888
936
  END IF;
889
937
 
890
938
  -- Set the column that uses enum value as a foreign key
891
- EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' ADD CONSTRAINT ' || fk_constraint_name || ' FOREIGN KEY ('|| columnName ||') REFERENCES ' || enumSchemaName || '.' || enumName || '(value);';
939
+ EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' ADD CONSTRAINT ' || constraintName || ' FOREIGN KEY ('|| columnName ||') REFERENCES ' || enumSchemaName || '.' || enumName || '(value);';
892
940
  END;
893
941
  $$;
894
942
 
@@ -1012,3 +1060,117 @@ BEGIN
1012
1060
 
1013
1061
  END;
1014
1062
  $function$;
1063
+
1064
+ /*-snippet
1065
+ {
1066
+ "body": [
1067
+ "SELECT ax_define.drop_timestamp_propagation('${1:fk_column}', '${2:table_name}', '${3:app_public}', '${4:id}', '${5:parent_table}', '${6:app_public}');"
1068
+ ],
1069
+ "description": [
1070
+ "Removes a function and trigger created by 'ax_define.define_timestamp_propagation' in an idempotent way.\n",
1071
+ "The call signature mirrors 'ax_define.define_timestamp_propagation' and should be applied with the same arguments.\n",
1072
+ "Seventh (optional) parameter: the unique name for the generated function. If the value is NULL then the name will be generated from the table names."
1073
+ ]
1074
+ }
1075
+ snippet-*/
1076
+ CREATE OR REPLACE FUNCTION ax_define.drop_timestamp_propagation(
1077
+ idColumnName text,
1078
+ tableName text,
1079
+ schemaName text,
1080
+ foreignIdColumnName text,
1081
+ foreignTableName text,
1082
+ foreignSchemaName text,
1083
+ functionName text default NULL
1084
+ ) RETURNS void
1085
+ LANGUAGE plpgsql
1086
+ AS $$
1087
+ BEGIN
1088
+ SELECT COALESCE(functionName, 'tg_' || tableName || '__' || foreignTableName || '_ts_propagation') INTO functionName;
1089
+ PERFORM ax_utils.validate_identifier_length(functionName, 'If the auto-generated name is too long then a "functionName" argument must be provided.');
1090
+ EXECUTE 'DROP TRIGGER IF EXISTS _200_propogate_timestamps on ' || schemaName || '.' || tableName;
1091
+ EXECUTE 'DROP FUNCTION IF EXISTS ' || schemaName || '.' || functionName || '()';
1092
+ END;
1093
+ $$;
1094
+
1095
+ /*-snippet
1096
+ {
1097
+ "body": [
1098
+ "SELECT ax_define.define_timestamp_propagation('${1:fk_column}', '${2:table_name}', '${3:app_public}', '${4:id}', '${5:parent_table}', '${6:app_public}');"
1099
+ ],
1100
+ "description": [
1101
+ "Defines a function and trigger to propagate 'updated_date' changes to related entities.\n",
1102
+ "Propagation can trigger 'UPDATED' triggers on the target entity including chained timestamp propagation.\n",
1103
+ "First parameter: a column name from the \"table_name\" table that the trigger should be associated with.",
1104
+ "Fourth parameter: a column name from the \"parent_table\" that 'updated_date' should be propagated to.",
1105
+ "Seventh (optional) parameter: a unique name for the generated function. If the value is NULL then a name will be generated from the table names.\n",
1106
+ "NB!. This function uses SECURITY DEFINER permission. It will always be executed as DB_OWNER."
1107
+ ]
1108
+ }
1109
+ snippet-*/
1110
+ CREATE OR REPLACE FUNCTION ax_define.define_timestamp_propagation(
1111
+ idColumnName text,
1112
+ tableName text,
1113
+ schemaName text,
1114
+ foreignIdColumnName text,
1115
+ foreignTableName text,
1116
+ foreignSchemaName text,
1117
+ functionName text default NULL
1118
+ ) RETURNS void
1119
+ LANGUAGE plpgsql
1120
+ AS $a$
1121
+ BEGIN
1122
+ SELECT COALESCE(functionName, 'tg_' || tableName || '__' || foreignTableName || '_ts_propagation') INTO functionName;
1123
+ PERFORM ax_utils.validate_identifier_length(functionName, 'If the auto-generated name is too long then a "functionName" argument must be provided.');
1124
+ -- Set updated_date=now() on the foreign table. This will propogate UPDATE triggers.
1125
+ --
1126
+ -- A new function is created for each table to do this.
1127
+ -- It *may* be possible to use a stock function with trigger arguments but its not easy as NEW and OLD cannot be accessed with dynamic column names. A possible
1128
+ -- solution to that is described here: https://itectec.com/database/postgresql-assignment-of-a-column-with-dynamic-column-name/. But even there the advise is
1129
+ -- to: "Just write a new trigger function for each table. Less hassle, better performance. Byte the bullet on code duplication:"
1130
+ --
1131
+ -- WARNING: This function uses "SECURITY DEFINER". This is required to ensure that update to the target table is allowed. This means that the function is
1132
+ -- executed with role "DB_OWNER". Any propogated trigger functions will also execute with role "DB_OWNER".
1133
+ EXECUTE '
1134
+ CREATE OR REPLACE FUNCTION ' || schemaName || '.' || functionName || '() RETURNS TRIGGER
1135
+ LANGUAGE plpgsql
1136
+ SECURITY DEFINER
1137
+ SET search_path = pg_temp
1138
+ AS $b$
1139
+ BEGIN
1140
+ -- if updated_date exists on source and its not changed, then exit here
1141
+ -- this prevents multiple propogation from different children in one transaction
1142
+ if (to_jsonb(NEW) ? ''updated_date'') THEN
1143
+ -- must be a seperate condition to prevent exception if column does not exist
1144
+ IF (OLD.updated_date = NEW.updated_date) THEN
1145
+ RETURN NULL;
1146
+ END IF;
1147
+ END IF;
1148
+
1149
+ -- UPDATE (where relationship is unchanged, or changed to another entity in which case a change is triggered on both the old and new relation)
1150
+ IF (OLD.' || idColumnName || ' IS NOT NULL AND NEW.' || idColumnName || ' IS NOT NULL) THEN
1151
+ UPDATE ' || foreignSchemaName || '.' || foreignTableName || ' SET updated_date=now()
1152
+ WHERE (' || foreignIdColumnName || ' = OLD.' || idColumnName || ') OR (' || foreignIdColumnName || ' = NEW.' || idColumnName || ');
1153
+
1154
+ -- INSERT (or UPDATE which sets nullable relationship)
1155
+ ELSIF (NEW.' || idColumnName || ' IS NOT NULL) THEN
1156
+ UPDATE ' || foreignSchemaName || '.' || foreignTableName || ' SET updated_date=now()
1157
+ WHERE ' || foreignIdColumnName || ' = NEW.' || idColumnName || ';
1158
+
1159
+ -- DELETE (or UPDATE which removes nullable relationship)
1160
+ ELSIF (OLD.' || idColumnName || ' IS NOT NULL) THEN
1161
+ UPDATE ' || foreignSchemaName || '.' || foreignTableName || ' SET updated_date=now()
1162
+ WHERE ' || foreignIdColumnName || ' = OLD.' || idColumnName || ';
1163
+
1164
+ END IF;
1165
+ RETURN NULL;
1166
+ END $b$;
1167
+ REVOKE EXECUTE ON FUNCTION ' || schemaName || '.' || functionName || '() FROM public;
1168
+ ';
1169
+
1170
+ -- Function runs *AFTER* INSERT, UPDATE, DELETE. Propogated queries can still raise an error and rollback the transaction
1171
+ EXECUTE 'DROP TRIGGER IF EXISTS _200_propogate_timestamps on ' || schemaName || '.' || tableName;
1172
+ EXECUTE 'CREATE trigger _200_propogate_timestamps
1173
+ AFTER INSERT OR UPDATE OR DELETE ON ' || schemaName || '.' || tableName || '
1174
+ FOR EACH ROW EXECUTE PROCEDURE ' || schemaName || '.' || functionName || '();';
1175
+ END;
1176
+ $a$;
@@ -21,9 +21,7 @@ CREATE OR REPLACE FUNCTION ax_define.define_multitenancy_on_table(
21
21
  AS $$
22
22
  BEGIN
23
23
  SELECT COALESCE(constraintName, tableName || '_unique_id_tenant_environment') INTO constraintName;
24
- IF LENGTH(constraintName) > 63 THEN
25
- perform ax_utils.raise_error('Invalid parameters provided to "ax_define.define_multitenancy_on_table". Constraint name "%s" exceeds 63 bytes. If the auto-generated name exceeds 63 bytes then a "constraintName" argument must be provided.', 'SETUP', constraintName);
26
- END IF;
24
+ PERFORM ax_utils.validate_identifier_length(constraintName, 'If the auto-generated name is too long then a "constraintName" argument must be provided.');
27
25
  EXECUTE '
28
26
  DO $do$ BEGIN
29
27
  BEGIN
@@ -85,9 +83,7 @@ CREATE OR REPLACE FUNCTION ax_define.define_multitenancy_relation(
85
83
  AS $$
86
84
  BEGIN
87
85
  SELECT COALESCE(constraintName, 'multitenancy_relation_' || tableName || '_to_' || foreignTableName) INTO constraintName;
88
- IF LENGTH(constraintName) > 63 THEN
89
- perform ax_utils.raise_error('Invalid parameters provided to "ax_define.define_multitenancy_relation". Constraint name "%s" exceeds 63 bytes. If the auto-generated name exceeds 63 bytes then a "constraintName" argument must be provided.', 'SETUP', constraintName);
90
- END IF;
86
+ PERFORM ax_utils.validate_identifier_length(constraintName, 'If the auto-generated name is too long then a "constraintName" argument must be provided.');
91
87
  EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' DROP CONSTRAINT IF EXISTS ' || constraintName || ';';
92
88
  EXECUTE '
93
89
  ALTER TABLE ' || schemaName || '.' || tableName || ' ADD CONSTRAINT ' || constraintName || '
@@ -112,9 +108,7 @@ CREATE OR REPLACE FUNCTION ax_define.define_multitenancy_unique_field_constraint
112
108
  AS $$
113
109
  BEGIN
114
110
  SELECT COALESCE(constraintName, tableName || '_' || fieldName || '_is_unique') INTO constraintName;
115
- IF LENGTH(constraintName) > 63 THEN
116
- perform ax_utils.raise_error('Invalid parameters provided to "ax_define.define_multitenancy_unique_constraint". Constraint name "%s" exceeds 63 bytes. If the auto-generated name exceeds 63 bytes then a "constraintName" argument must be provided.', 'SETUP', constraintName);
117
- END IF;
111
+ PERFORM ax_utils.validate_identifier_length(constraintName, 'If the auto-generated name is too long then a "constraintName" argument must be provided.');
118
112
  EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' DROP CONSTRAINT IF EXISTS ' || constraintName || ';';
119
113
  EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' ADD CONSTRAINT ' || constraintName || ' UNIQUE (' || fieldName || ', tenant_id, environment_id);';
120
114
  END;
@@ -139,9 +133,7 @@ DECLARE
139
133
  fieldList text = array_to_string(array_agg(fieldNames), ', ');
140
134
  BEGIN
141
135
  SELECT COALESCE(constraintName, tableName || '_' || fieldNamesConcat || '_is_unique') INTO constraintName;
142
- IF LENGTH(constraintName) > 63 THEN
143
- perform ax_utils.raise_error('Invalid parameters provided to "ax_define.define_multitenancy_multi_unique_constraint". Constraint name "%s" exceeds 63 bytes. If the auto-generated name exceeds 63 bytes then a "constraintName" argument must be provided.', 'SETUP', constraintName);
144
- END IF;
136
+ PERFORM ax_utils.validate_identifier_length(constraintName, 'If the auto-generated name is too long then a "constraintName" argument must be provided.');
145
137
  EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' DROP CONSTRAINT IF EXISTS ' || constraintName || ';';
146
138
  EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' ADD CONSTRAINT ' || constraintName || ' UNIQUE (' || fieldList || ', tenant_id, environment_id);';
147
139
  END;
@@ -163,9 +155,7 @@ CREATE OR REPLACE FUNCTION ax_define.define_multitenancy_unique_constraint(table
163
155
  AS $$
164
156
  BEGIN
165
157
  SELECT COALESCE(constraintName, tableName || '_multitenancy_entry_is_unique') INTO constraintName;
166
- IF LENGTH(constraintName) > 63 THEN
167
- perform ax_utils.raise_error('Invalid parameters provided to "ax_define.define_multitenancy_unique_constraint". Constraint name "%s" exceeds 63 bytes. If the auto-generated name exceeds 63 bytes then a "constraintName" argument must be provided.', 'SETUP', constraintName);
168
- END IF;
158
+ PERFORM ax_utils.validate_identifier_length(constraintName, 'If the auto-generated name is too long then a "constraintName" argument must be provided.');
169
159
  EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' DROP CONSTRAINT IF EXISTS ' || constraintName || ';';
170
160
  EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' ADD CONSTRAINT ' || constraintName || ' UNIQUE (tenant_id, environment_id);';
171
161
  END;
@@ -188,9 +178,7 @@ CREATE OR REPLACE FUNCTION ax_define.define_multitenancy_primary_key(fieldNames
188
178
  AS $$
189
179
  BEGIN
190
180
  SELECT COALESCE(constraintName, tableName || '_pkey') INTO constraintName;
191
- IF LENGTH(constraintName) > 63 THEN
192
- perform ax_utils.raise_error('Invalid parameters provided to "ax_define.define_multitenancy_primary_key". Constraint name "%s" exceeds 63 bytes. If the auto-generated name exceeds 63 bytes then a "constraintName" argument must be provided.', 'SETUP', constraintName);
193
- END IF;
181
+ PERFORM ax_utils.validate_identifier_length(constraintName, 'If the auto-generated name is too long then a "constraintName" argument must be provided.');
194
182
  EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' DROP CONSTRAINT IF EXISTS ' || constraintName || ';';
195
183
  EXECUTE 'ALTER TABLE ' || schemaName || '.' || tableName || ' ADD CONSTRAINT ' || constraintName || ' PRIMARY KEY (' || fieldNames || ',tenant_id, environment_id);';
196
184
  END;
@@ -212,9 +200,7 @@ CREATE OR REPLACE FUNCTION ax_define.define_multitenancy_index(fieldName text, t
212
200
  AS $$
213
201
  BEGIN
214
202
  SELECT COALESCE(indexName, 'idx_' || tableName || '_' || fieldName || '_mt') INTO indexName;
215
- IF LENGTH(indexName) > 63 THEN
216
- perform ax_utils.raise_error('Invalid parameters provided to "ax_define.define_multitenancy_index". Index name "%s" exceeds 63 bytes. If the auto-generated name exceeds 63 bytes then an "indexName" argument must be provided.', 'SETUP', indexName);
217
- END IF;
203
+ PERFORM ax_utils.validate_identifier_length(indexName, 'If the auto-generated name is too long then an "indexName" argument must be provided.');
218
204
  EXECUTE 'DROP INDEX IF EXISTS ' || indexName || ' cascade;';
219
205
  EXECUTE 'CREATE INDEX ' || indexName || ' ON ' || schemaName || '.' || tableName || ' (' || fieldName || ', tenant_id, environment_id);';
220
206
  END;
@@ -285,9 +271,7 @@ CREATE OR REPLACE FUNCTION ax_define.drop_multitenancy_timestamp_propagation(
285
271
  AS $$
286
272
  BEGIN
287
273
  SELECT COALESCE(functionName, 'tg_' || tableName || '__' || foreignTableName || '_mt_ts_propagation') INTO functionName;
288
- IF LENGTH(functionName) > 63 THEN
289
- perform ax_utils.raise_error('Invalid parameters provided to "ax_define.drop_multitenancy_timestamp_propagation". Function name "%s" exceeds 63 bytes. If the auto-generated name exceeds 63 bytes then a "functionName" argument must be provided.', 'SETUP', functionName);
290
- END IF;
274
+ PERFORM ax_utils.validate_identifier_length(functionName, 'If the auto-generated name is too long then a "functionName" argument must be provided.');
291
275
  EXECUTE 'DROP TRIGGER IF EXISTS _200_propogate_timestamps on ' || schemaName || '.' || tableName;
292
276
  EXECUTE 'DROP FUNCTION IF EXISTS ' || schemaName || '.' || functionName || '()';
293
277
  END;
@@ -304,7 +288,8 @@ $$;
304
288
  "The call signature mirrors ax_define.define_multitenancy_relation and can generally be applied with the same arguments.\n",
305
289
  "First parameter: a column name from the \"table_name\" table that the trigger should be associated with.",
306
290
  "Fourth parameter: a column name from the \"parent_table\" that 'updated_date' should be propagated to.",
307
- "Seventh (optional) parameter: a unique name for the generated function. If the value is NULL then a name will be generated from the table names."
291
+ "Seventh (optional) parameter: a unique name for the generated function. If the value is NULL then a name will be generated from the table names.\n",
292
+ "NB!. This function uses SECURITY DEFINER permission. It will always be executed as DB_OWNER."
308
293
  ]
309
294
  }
310
295
  snippet-*/
@@ -321,9 +306,7 @@ CREATE OR REPLACE FUNCTION ax_define.define_multitenancy_timestamp_propagation(
321
306
  AS $a$
322
307
  BEGIN
323
308
  SELECT COALESCE(functionName, 'tg_' || tableName || '__' || foreignTableName || '_mt_ts_propagation') INTO functionName;
324
- IF LENGTH(functionName) > 63 THEN
325
- perform ax_utils.raise_error('Invalid parameters provided to "ax_define.define_multitenancy_timestamp_propagation". Function name "%s" exceeds 63 bytes. If the auto-generated name exceeds 63 bytes then a "functionName" argument must be provided.', 'SETUP', functionName);
326
- END IF;
309
+ PERFORM ax_utils.validate_identifier_length(functionName, 'If the auto-generated name is too long then a "functionName" argument must be provided.');
327
310
  -- Set updated_date=now() on the foreign table. This will propogate UPDATE triggers.
328
311
  --
329
312
  -- A new function is created for each table to do this.
@@ -341,18 +324,26 @@ BEGIN
341
324
  SET search_path = pg_temp
342
325
  AS $b$
343
326
  BEGIN
344
- if (OLD.updated_date = NEW.updated_date) THEN
345
- RETURN NULL;
327
+ -- if updated_date exists on source and its not changed, then exit here
328
+ -- this prevents multiple propogation from different children in one transaction
329
+ if (to_jsonb(NEW) ? ''updated_date'') THEN
330
+ -- must be a seperate condition to prevent exception if column does not exist
331
+ IF (OLD.updated_date = NEW.updated_date) THEN
332
+ RETURN NULL;
333
+ END IF;
346
334
  END IF;
335
+
347
336
  -- UPDATE (where relationship is unchanged, or changed to another entity in which case a change is triggered on both the old and new relation)
348
337
  IF (OLD.' || idColumnName || ' IS NOT NULL AND NEW.' || idColumnName || ' IS NOT NULL) THEN
349
338
  UPDATE ' || foreignSchemaName || '.' || foreignTableName || ' SET updated_date=now()
350
339
  WHERE (' || foreignIdColumnName || ' = OLD.' || idColumnName || ' AND tenant_id = OLD.tenant_id AND environment_id = OLD.environment_id)
351
340
  OR (' || foreignIdColumnName || ' = NEW.' || idColumnName || ' AND tenant_id = NEW.tenant_id AND environment_id = NEW.environment_id);
341
+
352
342
  -- INSERT (or UPDATE which sets nullable relationship)
353
343
  ELSIF (NEW.' || idColumnName || ' IS NOT NULL) THEN
354
344
  UPDATE ' || foreignSchemaName || '.' || foreignTableName || ' SET updated_date=now()
355
345
  WHERE ' || foreignIdColumnName || ' = NEW.' || idColumnName || ' AND tenant_id = NEW.tenant_id AND environment_id = NEW.environment_id;
346
+
356
347
  -- DELETE (or UPDATE which removes nullable relationship)
357
348
  ELSIF (OLD.' || idColumnName || ' IS NOT NULL) THEN
358
349
  UPDATE ' || foreignSchemaName || '.' || foreignTableName || ' SET updated_date=now()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-db-common",
3
- "version": "0.14.1-rc.6",
3
+ "version": "0.15.0",
4
4
  "description": "This library encapsulates database-related functionality to develop Mosaic based services.",
5
5
  "author": "Axinom",
6
6
  "license": "PROPRIETARY",
@@ -48,5 +48,5 @@
48
48
  "publishConfig": {
49
49
  "access": "public"
50
50
  },
51
- "gitHead": "9fa0c83deed32d7f1581fb976f88c5c73ce37877"
51
+ "gitHead": "2d8cec2958a2cc6f80ee764d21e66b4fe1729bb5"
52
52
  }