-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsql_saga--default_version
More file actions
3646 lines (3276 loc) · 155 KB
/
sql_saga--default_version
File metadata and controls
3646 lines (3276 loc) · 155 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION sql_saga" to load this file. \quit
/* This extension is non-relocatable */
CREATE SCHEMA sql_saga;
GRANT USAGE ON SCHEMA sql_saga TO PUBLIC;
CREATE TYPE sql_saga.drop_behavior AS ENUM ('CASCADE', 'RESTRICT');
CREATE TYPE sql_saga.fk_actions AS ENUM ('CASCADE', 'SET NULL', 'SET DEFAULT', 'RESTRICT', 'NO ACTION');
CREATE TYPE sql_saga.fk_match_types AS ENUM ('FULL', 'PARTIAL', 'SIMPLE');
/*
* All referencing columns must be either name or regsomething in order for
* pg_dump to work properly. Plain OIDs are not allowed but attribute numbers
* are, so that we don't have to track renames.
*
* Anything declared as regsomething and created for the period (such as the
* "__as_of" function), should be UNIQUE. If Postgres already verifies
* uniqueness, such as constraint names on a table, then we don't need to do it
* also.
*/
CREATE TABLE sql_saga.era (
table_schema name NOT NULL,
table_name name NOT NULL,
era_name name NOT NULL DEFAULT 'valid',
valid_from_column_name name NOT NULL,
valid_until_column_name name NOT NULL,
-- active_column_name name NOT NULL,
range_type regtype NOT NULL,
bounds_check_constraint name NOT NULL,
-- infinity_check_constraint name NOT NULL,
-- generated_always_trigger name NOT NULL,
audit_schema_name name,
audit_table_name name,
-- audit_trigger name NOT NULL,
-- delete_trigger name NOT NULL,
--excluded_column_names name[] NOT NULL DEFAULT '{}',
-- UNIQUE(...) for each trigger/function name.
PRIMARY KEY (table_schema, table_name, era_name),
CHECK (valid_from_column_name <> valid_until_column_name)
);
COMMENT ON TABLE sql_saga.era IS 'The main catalog for sql_saga. All "DDL" operations for periods must first take an exclusive lock on this table.';
GRANT SELECT ON TABLE sql_saga.era TO PUBLIC;
SELECT pg_catalog.pg_extension_config_dump('sql_saga.era', '');
CREATE TABLE sql_saga.system_time_era (
table_schema name NOT NULL,
table_name name NOT NULL,
era_name name NOT NULL,
infinity_check_constraint name NOT NULL,
generated_always_trigger name NOT NULL,
write_history_trigger name NOT NULL,
truncate_trigger name NOT NULL,
excluded_column_names name[] NOT NULL DEFAULT '{}',
PRIMARY KEY (table_schema, table_name, era_name),
FOREIGN KEY (table_schema, table_name, era_name) REFERENCES sql_saga.era(table_schema, table_name, era_name),
CHECK (era_name = 'system_time')
);
GRANT SELECT ON TABLE sql_saga.system_time_era TO PUBLIC;
SELECT pg_catalog.pg_extension_config_dump('sql_saga.system_time_era', '');
CREATE TABLE sql_saga.unique_keys (
unique_key_name name NOT NULL,
table_schema name NOT NULL,
table_name name NOT NULL,
column_names name[] NOT NULL,
era_name name NOT NULL,
is_primary boolean NOT NULL DEFAULT false,
unique_constraint name NOT NULL,
exclude_constraint name NOT NULL,
PRIMARY KEY (unique_key_name),
FOREIGN KEY (table_schema, table_name, era_name) REFERENCES sql_saga.era (table_schema, table_name, era_name)
);
CREATE UNIQUE INDEX ON sql_saga.unique_keys (table_schema, table_name, era_name) WHERE is_primary;
GRANT SELECT ON TABLE sql_saga.unique_keys TO PUBLIC;
SELECT pg_catalog.pg_extension_config_dump('sql_saga.unique_keys', '');
COMMENT ON TABLE sql_saga.unique_keys IS 'A registry of UNIQUE/PRIMARY keys using era WITHOUT OVERLAPS';
CREATE TABLE sql_saga.foreign_keys (
foreign_key_name name NOT NULL,
table_schema name NOT NULL,
table_name name NOT NULL,
column_names name[] NOT NULL,
era_name name NOT NULL,
unique_key_name name NOT NULL,
match_type sql_saga.fk_match_types NOT NULL DEFAULT 'SIMPLE',
update_action sql_saga.fk_actions NOT NULL DEFAULT 'NO ACTION',
delete_action sql_saga.fk_actions NOT NULL DEFAULT 'NO ACTION',
fk_insert_trigger name NOT NULL,
fk_update_trigger name NOT NULL,
uk_update_trigger name NOT NULL,
uk_delete_trigger name NOT NULL,
PRIMARY KEY (foreign_key_name),
FOREIGN KEY (table_schema, table_name, era_name) REFERENCES sql_saga.era (table_schema, table_name, era_name),
FOREIGN KEY (unique_key_name) REFERENCES sql_saga.unique_keys,
CHECK (delete_action NOT IN ('CASCADE', 'SET NULL', 'SET DEFAULT')),
CHECK (update_action NOT IN ('CASCADE', 'SET NULL', 'SET DEFAULT'))
);
GRANT SELECT ON TABLE sql_saga.foreign_keys TO PUBLIC;
SELECT pg_catalog.pg_extension_config_dump('sql_saga.foreign_keys', '');
COMMENT ON TABLE sql_saga.foreign_keys IS 'A registry of foreign keys using era WITHOUT OVERLAPS';
CREATE TABLE sql_saga.system_versioning (
table_schema name NOT NULL,
table_name name NOT NULL,
era_name name NOT NULL,
history_schema_name name NOT NULL,
history_table_name name NOT NULL,
view_schema_name name NOT NULL,
view_table_name name NOT NULL,
-- These functions should be of type regprocedure, but that blocks pg_upgrade.
func_as_of text NOT NULL,
func_between text NOT NULL,
func_between_symmetric text NOT NULL,
func_from_to text NOT NULL,
PRIMARY KEY (table_schema, table_name),
FOREIGN KEY (table_schema, table_name, era_name) REFERENCES sql_saga.era(table_schema, table_name, era_name),
CHECK (era_name = 'system_time'),
UNIQUE (history_schema_name, history_table_name),
UNIQUE (view_schema_name, view_table_name),
UNIQUE (func_as_of),
UNIQUE (func_between),
UNIQUE (func_between_symmetric),
UNIQUE (func_from_to)
);
GRANT SELECT ON TABLE sql_saga.system_versioning TO PUBLIC;
SELECT pg_catalog.pg_extension_config_dump('sql_saga.system_versioning', '');
COMMENT ON TABLE sql_saga.system_versioning IS 'A registry of tables with SYSTEM VERSIONING';
CREATE VIEW sql_saga.information_schema__era AS
SELECT current_catalog AS table_catalog,
e.table_schema,
e.table_name,
e.era_name,
e.valid_from_column_name,
e.valid_until_column_name
FROM sql_saga.era AS e;
CREATE TABLE sql_saga.api_view (
table_schema name NOT NULL,
table_name name NOT NULL,
era_name name NOT NULL,
view_schema_name name NOT NULL,
view_table_name name NOT NULL,
trigger_name name NOT NULL,
-- truncate_trigger name NOT NULL,
PRIMARY KEY (table_schema, table_name, era_name),
FOREIGN KEY (table_schema, table_name, era_name) REFERENCES sql_saga.era (table_schema, table_name, era_name),
UNIQUE (view_schema_name, view_table_name)
);
GRANT SELECT ON TABLE sql_saga.api_view TO PUBLIC;
SELECT pg_catalog.pg_extension_config_dump('sql_saga.api_view', '');
CREATE TYPE sql_saga.temporal_merge_mode AS ENUM (
'upsert_patch',
'upsert_replace',
'patch_only',
'replace_only',
'insert_only'
);
-- Use Allen's Interval Relation for covering all possible cases of overlap. Ref. https://ics.uci.edu/~alspaugh/cls/shr/allen.html
CREATE TYPE public.allen_interval_relation AS ENUM (
'precedes', -- X before Y: X.until < Y.from
-- X: [ XXXX )
-- Y: [ YYYY )
'meets', -- X meets Y: X.until = Y.from
-- X: [ XXXX )
-- Y: [ YYYY )
'overlaps', -- X overlaps Y
-- X: [ XXXX----)
-- Y: [----YYYY )
'starts', -- X starts Y
-- X: [ XXXX )
-- Y: [ YYYYYYYY )
'during', -- X during Y (X is contained in Y)
-- X: [ XXXX )
-- Y: [ YYYYYYYY )
'finishes', -- X finishes Y
-- X: [ XXXX )
-- Y: [ YYYYYYYY )
'equals', -- X equals Y
-- X: [ XXXX )
-- Y: [ YYYY )
'overlapped_by', -- X is overlapped by Y (Y overlaps X)
-- X: [----XXXX )
-- Y: [ YYYY----)
'started_by', -- X is started by Y (Y starts X)
-- X: [ XXXXXXX )
-- Y: [ YYYY )
'contains', -- X contains Y (Y is during X)
-- X: [ XXXXXXX )
-- Y: [ YYYY )
'finished_by', -- X is finished by Y (Y finishes X)
-- X: [ XXXXXXX )
-- Y: [ YYYY )
'met_by', -- X is met by Y (Y meets X)
-- X: [ XXXX )
-- Y: [ YYYY )
'preceded_by' -- X is preceded by Y (Y precedes X)
-- X: [ XXXX )
-- Y: [ YYYY )
);
COMMENT ON TYPE public.allen_interval_relation IS
'Allen''s interval algebra relations for two intervals X=[X.from, X.until) and Y=[Y.from, Y.until), using [inclusive_start, exclusive_end) semantics.
The ASCII art illustrates interval X relative to interval Y.';
CREATE FUNCTION public.allen_get_relation(
x_from anyelement, x_until anyelement,
y_from anyelement, y_until anyelement
) RETURNS public.allen_interval_relation
LANGUAGE sql IMMUTABLE PARALLEL SAFE AS
$BODY$
SELECT CASE
-- Cases where start points are the same
WHEN x_from = y_from AND x_until = y_until THEN 'equals'::public.allen_interval_relation
WHEN x_from = y_from AND x_until < y_until THEN 'starts'::public.allen_interval_relation
WHEN x_from = y_from AND x_until > y_until THEN 'started_by'::public.allen_interval_relation
-- Cases where end points are the same
WHEN x_from > y_from AND x_until = y_until THEN 'finishes'::public.allen_interval_relation
WHEN x_from < y_from AND x_until = y_until THEN 'finished_by'::public.allen_interval_relation
-- Case where one interval is during another
WHEN x_from > y_from AND x_until < y_until THEN 'during'::public.allen_interval_relation
WHEN x_from < y_from AND x_until > y_until THEN 'contains'::public.allen_interval_relation
-- Cases where intervals are adjacent
WHEN x_until = y_from THEN 'meets'::public.allen_interval_relation
WHEN y_until = x_from THEN 'met_by'::public.allen_interval_relation
-- Cases where intervals overlap
WHEN x_from < y_from AND x_until > y_from AND x_until < y_until THEN 'overlaps'::public.allen_interval_relation
WHEN y_from < x_from AND y_until > x_from AND y_until < x_until THEN 'overlapped_by'::public.allen_interval_relation
-- Cases where intervals are disjoint
WHEN x_until < y_from THEN 'precedes'::public.allen_interval_relation
WHEN y_until < x_from THEN 'preceded_by'::public.allen_interval_relation
END;
$BODY$;
COMMENT ON FUNCTION public.allen_get_relation IS
'Calculates the Allen Interval Algebra relation between two intervals X and Y,
assuming [inclusive_start, exclusive_end) semantics.';
/*
* C Helper functions
*/
CREATE OR REPLACE FUNCTION sql_saga.covers_without_gaps_transfn(internal, anyrange, anyrange)
RETURNS internal
AS 'sql_saga', 'covers_without_gaps_transfn'
LANGUAGE c;
CREATE OR REPLACE FUNCTION sql_saga.fk_insert_check_c()
RETURNS trigger
AS 'sql_saga', 'fk_insert_check_c'
LANGUAGE c;
CREATE OR REPLACE FUNCTION sql_saga.fk_update_check_c()
RETURNS trigger
AS 'sql_saga', 'fk_update_check_c'
LANGUAGE c;
CREATE OR REPLACE FUNCTION sql_saga.uk_delete_check_c()
RETURNS trigger
AS 'sql_saga', 'uk_delete_check_c'
LANGUAGE c;
CREATE OR REPLACE FUNCTION sql_saga.uk_update_check_c()
RETURNS trigger
AS 'sql_saga', 'uk_update_check_c'
LANGUAGE c;
CREATE OR REPLACE FUNCTION sql_saga.generated_always_as_row_start_end()
RETURNS trigger
AS 'sql_saga', 'generated_always_as_row_start_end'
LANGUAGE c STRICT SECURITY DEFINER;
CREATE OR REPLACE FUNCTION sql_saga.write_history()
RETURNS trigger
AS 'sql_saga', 'write_history'
LANGUAGE c STRICT SECURITY DEFINER;
CREATE OR REPLACE FUNCTION sql_saga.covers_without_gaps_finalfn(internal, anyrange, anyrange)
RETURNS boolean
AS 'sql_saga', 'covers_without_gaps_finalfn'
LANGUAGE c;
/*
* covers_without_gaps(period anyrange, target anyrange) -
* Returns true if the collected `period` ranges are contiguous (have no gaps)
* and completely cover the fixed `target` range.
*/
CREATE AGGREGATE sql_saga.covers_without_gaps(anyrange, anyrange) (
sfunc = sql_saga.covers_without_gaps_transfn,
stype = internal,
finalfunc = sql_saga.covers_without_gaps_finalfn,
finalfunc_extra
);
/*
* These function starting with "_" are private to the periods extension and
* should not be called by outsiders. When all the other functions have been
* translated to C, they will be removed.
*/
CREATE FUNCTION sql_saga.__internal_serialize(table_name regclass)
RETURNS void
LANGUAGE sql
AS
$function$
/* XXX: Is this the best way to do locking? */
SELECT pg_catalog.pg_advisory_xact_lock('sql_saga.era'::regclass::oid::integer, table_name::oid::integer);
$function$;
CREATE FUNCTION sql_saga.__internal_make_name(resizable text[], fixed text DEFAULT NULL, separator text DEFAULT '_', extra integer DEFAULT 2)
RETURNS name
IMMUTABLE
LANGUAGE plpgsql
AS
$function$
#variable_conflict use_variable
DECLARE
max_length integer;
result text;
NAMEDATALEN CONSTANT integer := 64;
BEGIN
/*
* Reduce the resizable texts until they and the fixed text fit in
* NAMEDATALEN. This probably isn't very efficient but it's not on a hot
* code path so we don't care.
*/
SELECT max(length(t))
INTO max_length
FROM unnest(resizable) AS u (t);
LOOP
result := format('%s%s', array_to_string(resizable, separator), separator || fixed);
IF octet_length(result) <= NAMEDATALEN-extra-1 THEN
RETURN result;
END IF;
max_length := max_length - 1;
resizable := ARRAY (
SELECT left(t, max_length)
FROM unnest(resizable) WITH ORDINALITY AS u (t, o)
ORDER BY o
);
END LOOP;
END;
$function$;
CREATE FUNCTION sql_saga.__internal_make_api_view_name(table_name name, era_name name)
RETURNS name
IMMUTABLE
LANGUAGE plpgsql
AS
$function$
#variable_conflict use_variable
DECLARE
max_length integer;
result text;
NAMEDATALEN CONSTANT integer := 64;
BEGIN
/*
* Reduce the table and period names until they fit in NAMEDATALEN. This
* probably isn't very efficient but it's not on a hot code path so we
* don't care.
*/
max_length := greatest(length(table_name), length(era_name));
LOOP
result := format('%s__for_portion_of_%s', table_name, era_name);
IF octet_length(result) <= NAMEDATALEN-1 THEN
RETURN result;
END IF;
max_length := max_length - 1;
table_name := left(table_name, max_length);
era_name := left(era_name, max_length);
END LOOP;
END;
$function$;
/*
* Generic trigger function to synchronize an inclusive end-date ('valid_to')
* with the exclusive end-date ('valid_until') required by sql_saga.
* Ensures valid_until = valid_to + '1 day'.
* This is provided as a convenience for users who prefer to work with
* inclusive end dates.
*/
CREATE FUNCTION sql_saga.synchronize_valid_to_until()
RETURNS TRIGGER LANGUAGE plpgsql AS $synchronize_valid_to_until$
BEGIN
-- This trigger synchronizes an inclusive end column (e.g., 'valid_to')
-- with an exclusive end column ('valid_until') for date-based periods.
-- The relationship is valid_until = valid_to + '1 day'.
-- For INSERT operations
IF TG_OP = 'INSERT' THEN
IF NEW.valid_until IS NOT NULL AND NEW.valid_to IS NULL THEN
NEW.valid_to := NEW.valid_until - INTERVAL '1 day';
ELSIF NEW.valid_to IS NOT NULL AND NEW.valid_until IS NULL THEN
NEW.valid_until := NEW.valid_to + INTERVAL '1 day';
ELSIF NEW.valid_until IS NOT NULL AND NEW.valid_to IS NOT NULL THEN
IF NEW.valid_to != (NEW.valid_until - INTERVAL '1 day') THEN
RAISE EXCEPTION 'On INSERT, valid_to and valid_until are inconsistent. Expected valid_to = valid_until - 1 day. Got valid_to=%, valid_until=%', NEW.valid_to, NEW.valid_until;
END IF;
-- If both are NULL, do nothing, let table constraints handle it.
END IF;
-- For UPDATE operations
ELSIF TG_OP = 'UPDATE' THEN
-- Case 1: Both columns are explicitly changed.
IF NEW.valid_until IS DISTINCT FROM OLD.valid_until AND NEW.valid_to IS DISTINCT FROM OLD.valid_to THEN
IF NEW.valid_until IS NULL OR NEW.valid_to IS NULL THEN
RAISE EXCEPTION 'On UPDATE, when changing both valid_to and valid_until, neither can be set to NULL.';
END IF;
IF NEW.valid_to != (NEW.valid_until - INTERVAL '1 day') THEN
RAISE EXCEPTION 'On UPDATE, conflicting explicit values for valid_to and valid_until. With valid_until=%, expected valid_to=%. Got valid_to=%',
NEW.valid_until, NEW.valid_until - INTERVAL '1 day', NEW.valid_to;
END IF;
-- Case 2: Only valid_until is explicitly changed.
ELSIF NEW.valid_until IS DISTINCT FROM OLD.valid_until THEN
IF NEW.valid_until IS NULL THEN
RAISE EXCEPTION 'On UPDATE, valid_until cannot be set to NULL.';
END IF;
NEW.valid_to := NEW.valid_until - INTERVAL '1 day';
-- Case 3: Only valid_to is explicitly changed.
ELSIF NEW.valid_to IS DISTINCT FROM OLD.valid_to THEN
IF NEW.valid_to IS NULL THEN
RAISE EXCEPTION 'On UPDATE, valid_to cannot be set to NULL.';
END IF;
NEW.valid_until := NEW.valid_to + INTERVAL '1 day';
-- Case 4: Neither is being distinctly changed. Check for consistency if they are not NULL.
ELSE
IF NEW.valid_until IS NOT NULL AND NEW.valid_to IS NOT NULL THEN
IF NEW.valid_to != (NEW.valid_until - INTERVAL '1 day') THEN
RAISE EXCEPTION 'On UPDATE, existing valid_to and valid_until are inconsistent. Got valid_to=%, valid_until=%', NEW.valid_to, NEW.valid_until;
END IF;
END IF;
END IF;
END IF;
RETURN NEW;
END;
$synchronize_valid_to_until$;
CREATE FUNCTION sql_saga.update_portion_of()
RETURNS trigger
LANGUAGE plpgsql
AS
$function$
#variable_conflict use_variable
DECLARE
info record;
view_schema_name name;
view_table_name name;
test boolean;
generated_columns_sql text;
generated_columns text[];
jnew jsonb;
fromval jsonb;
toval jsonb;
jold jsonb;
bstartval jsonb;
bendval jsonb;
identifier_columns name[];
pre_row jsonb;
new_row jsonb;
post_row jsonb;
pre_assigned boolean;
post_assigned boolean;
SERVER_VERSION CONSTANT integer := current_setting('server_version_num')::integer;
TEST_SQL CONSTANT text :=
'VALUES (CAST(%2$L AS %1$s) < CAST(%3$L AS %1$s) AND '
' CAST(%3$L AS %1$s) < CAST(%4$L AS %1$s))';
GENERATED_COLUMNS_SQL_PRE_10 CONSTANT text :=
'SELECT array_agg(a.attname) '
'FROM pg_catalog.pg_attribute AS a '
'LEFT JOIN pg_catalog.pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum '
'WHERE a.attrelid = $1 '
' AND a.attnum > 0 '
' AND NOT a.attisdropped '
' AND (pg_catalog.pg_get_serial_sequence(a.attrelid::regclass::text, a.attname) IS NOT NULL '
' OR (a.atthasdef AND pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) LIKE ''nextval(%)'') '
' OR EXISTS (SELECT 1 FROM sql_saga.era AS _p '
' JOIN pg_catalog.pg_class _c ON _c.relname = _p.table_name '
' JOIN pg_catalog.pg_namespace _n ON _n.oid = _c.relnamespace AND _n.nspname = _p.table_schema '
' WHERE _c.oid = a.attrelid AND _p.era_name = ''system_time'' '
' AND a.attname IN (_p.valid_from_column_name, _p.valid_until_column_name)))';
GENERATED_COLUMNS_SQL_PRE_12 CONSTANT text :=
'SELECT array_agg(a.attname) '
'FROM pg_catalog.pg_attribute AS a '
'LEFT JOIN pg_catalog.pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum '
'WHERE a.attrelid = $1 '
' AND a.attnum > 0 '
' AND NOT a.attisdropped '
' AND (pg_catalog.pg_get_serial_sequence(a.attrelid::regclass::text, a.attname) IS NOT NULL '
' OR (a.atthasdef AND pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) LIKE ''nextval(%)'') '
' OR a.attidentity <> '''' '
' OR EXISTS (SELECT 1 FROM sql_saga.era AS _p '
' JOIN pg_catalog.pg_class _c ON _c.relname = _p.table_name '
' JOIN pg_catalog.pg_namespace _n ON _n.oid = _c.relnamespace AND _n.nspname = _p.table_schema '
' WHERE _c.oid = a.attrelid AND _p.era_name = ''system_time'' '
' AND a.attname IN (_p.valid_from_column_name, _p.valid_until_column_name)))';
GENERATED_COLUMNS_SQL_CURRENT CONSTANT text :=
'SELECT array_agg(a.attname) '
'FROM pg_catalog.pg_attribute AS a '
'LEFT JOIN pg_catalog.pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum '
'WHERE a.attrelid = $1 '
' AND a.attnum > 0 '
' AND NOT a.attisdropped '
' AND (pg_catalog.pg_get_serial_sequence(a.attrelid::regclass::text, a.attname) IS NOT NULL '
' OR (a.atthasdef AND pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) LIKE ''nextval(%)'') '
' OR a.attidentity <> '''' '
' OR a.attgenerated <> '''' '
' OR EXISTS (SELECT 1 FROM sql_saga.era AS _p '
' JOIN pg_catalog.pg_class _c ON _c.relname = _p.table_name '
' JOIN pg_catalog.pg_namespace _n ON _n.oid = _c.relnamespace AND _n.nspname = _p.table_schema '
' WHERE _c.oid = a.attrelid AND _p.era_name = ''system_time'' '
' AND a.attname IN (_p.valid_from_column_name, _p.valid_until_column_name)))';
BEGIN
/*
* REFERENCES:
* SQL:2016 15.13 GR 10
*/
-- The identifier columns are passed as arguments to the trigger.
IF TG_NARGS = 0 THEN
RAISE EXCEPTION 'update_portion_of trigger must be created with identifier columns as arguments';
END IF;
identifier_columns := TG_ARGV;
SELECT n.nspname, c.relname
INTO view_schema_name, view_table_name
FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid
WHERE c.oid = TG_RELID;
/* Get the table information from this view */
SELECT c.oid AS table_oid,
fpv.table_schema, fpv.table_name, fpv.era_name,
e.valid_from_column_name, e.valid_until_column_name,
format_type(a.atttypid, a.atttypmod) AS datatype
INTO info
FROM sql_saga.api_view AS fpv
JOIN sql_saga.era AS e ON (e.table_schema, e.table_name, e.era_name) = (fpv.table_schema, fpv.table_name, fpv.era_name)
JOIN pg_catalog.pg_namespace AS n ON n.nspname = fpv.table_schema
JOIN pg_catalog.pg_class AS c ON (c.relnamespace, c.relname) = (n.oid, fpv.table_name)
JOIN pg_catalog.pg_attribute AS a ON (a.attrelid, a.attname) = (c.oid, e.valid_from_column_name)
WHERE (fpv.view_schema_name, fpv.view_table_name) = (view_schema_name, view_table_name);
IF NOT FOUND THEN
RAISE EXCEPTION 'table and era information not found for view "%"', TG_RELID::regclass;
END IF;
jnew := to_jsonb(NEW);
fromval := jnew->info.valid_from_column_name;
toval := jnew->info.valid_until_column_name;
jold := to_jsonb(OLD);
bstartval := jold->info.valid_from_column_name;
bendval := jold->info.valid_until_column_name;
-- If the new period does not overlap with the old period, do nothing.
EXECUTE format('SELECT NOT (%L::%s >= %L::%s OR %L::%s >= %L::%s)',
fromval, info.datatype, bendval, info.datatype, bstartval, info.datatype, toval, info.datatype)
INTO test;
IF NOT test THEN
RETURN NULL;
END IF;
-- If the new period does not overlap with the old period, do nothing.
EXECUTE format('SELECT NOT (%L::%s >= %L::%s OR %L::%s >= %L::%s)',
fromval, info.datatype, bendval, info.datatype, bstartval, info.datatype, toval, info.datatype)
INTO test;
IF NOT test THEN
RETURN NULL;
END IF;
-- If the new period does not overlap with the old period, do nothing.
EXECUTE format('SELECT %L::%s < %L::%s AND %L::%s < %L::%s',
bstartval, info.datatype, toval, info.datatype, fromval, info.datatype, bendval, info.datatype)
INTO test;
IF NOT test THEN
RETURN NULL;
END IF;
pre_row := jold;
new_row := jnew;
post_row := jold;
/* Reset the period columns */
new_row := jsonb_set(new_row, ARRAY[info.valid_from_column_name], bstartval);
new_row := jsonb_set(new_row, ARRAY[info.valid_until_column_name], bendval);
/* If the period is the only thing changed, do nothing */
IF new_row = jold THEN
RETURN NULL;
END IF;
pre_assigned := false;
EXECUTE format(TEST_SQL, info.datatype, bstartval, fromval, bendval) INTO test;
IF test THEN
pre_assigned := true;
pre_row := jsonb_set(pre_row, ARRAY[info.valid_until_column_name], fromval);
new_row := jsonb_set(new_row, ARRAY[info.valid_from_column_name], fromval);
END IF;
post_assigned := false;
EXECUTE format(TEST_SQL, info.datatype, bstartval, toval, bendval) INTO test;
IF test THEN
post_assigned := true;
new_row := jsonb_set(new_row, ARRAY[info.valid_until_column_name], toval::jsonb);
post_row := jsonb_set(post_row, ARRAY[info.valid_from_column_name], toval::jsonb);
END IF;
IF pre_assigned OR post_assigned THEN
/* Don't validate foreign keys until all this is done */
SET CONSTRAINTS ALL DEFERRED;
/*
* Find and remove all generated columns from pre_row and post_row.
* SQL:2016 15.13 GR 10)b)i)
*
* We also remove columns that own a sequence as those are a form of
* generated column. We do not, however, remove columns that default
* to nextval() without owning the underlying sequence.
*
* Columns belonging to a SYSTEM_TIME period are also removed.
*
* Note: Primary key columns are not implicitly treated as generated.
* Columns are considered generated if they are `serial`, `IDENTITY`, a
* generated expression, part of the `system_time` period, or have a
* `DEFAULT` expression that calls `nextval()`. Other columns, including
* non-generated primary keys that often serve as entity identifiers, are
* preserved. This is crucial for maintaining historical integrity when
* splitting rows.
*/
-- Create a cache table for generated columns if it doesn't exist for this session.
-- Using to_regclass is a clean way to check for a temp table's existence.
IF to_regclass('__sql_saga_generated_columns_cache') IS NULL THEN
CREATE TEMP TABLE __sql_saga_generated_columns_cache (
table_oid oid PRIMARY KEY,
column_names name[]
) ON COMMIT DROP;
END IF;
-- Try to fetch from cache first
SELECT column_names INTO generated_columns FROM __sql_saga_generated_columns_cache WHERE table_oid = info.table_oid;
IF NOT FOUND THEN
-- Not in cache, so query catalogs
IF SERVER_VERSION < 100000 THEN
generated_columns_sql := GENERATED_COLUMNS_SQL_PRE_10;
ELSIF SERVER_VERSION < 120000 THEN
generated_columns_sql := GENERATED_COLUMNS_SQL_PRE_12;
ELSE
generated_columns_sql := GENERATED_COLUMNS_SQL_CURRENT;
END IF;
EXECUTE generated_columns_sql
INTO generated_columns
USING info.table_oid;
-- Store in cache for subsequent calls in this transaction
INSERT INTO __sql_saga_generated_columns_cache (table_oid, column_names) VALUES (info.table_oid, generated_columns);
END IF;
/* There may not be any generated columns. */
IF generated_columns IS NOT NULL THEN
IF SERVER_VERSION < 100000 THEN
SELECT jsonb_object_agg(e.key, e.value)
INTO pre_row
FROM jsonb_each(pre_row) AS e (key, value)
WHERE e.key <> ALL (generated_columns);
SELECT jsonb_object_agg(e.key, e.value)
INTO post_row
FROM jsonb_each(post_row) AS e (key, value)
WHERE e.key <> ALL (generated_columns);
ELSE
pre_row := pre_row - generated_columns;
post_row := post_row - generated_columns;
END IF;
END IF;
END IF;
IF pre_assigned THEN
EXECUTE format('INSERT INTO %I.%I (%s) VALUES (%s)',
info.table_schema,
info.table_name,
(SELECT string_agg(quote_ident(key), ', ' ORDER BY key) FROM jsonb_each_text(pre_row)),
(SELECT string_agg(quote_nullable(value), ', ' ORDER BY key) FROM jsonb_each_text(pre_row)));
END IF;
EXECUTE format('UPDATE %I.%I SET %s WHERE %s AND %I = %s AND %I = %s',
info.table_schema,
info.table_name,
(SELECT string_agg(format('%I = %L', j.key, j.value), ', ')
FROM (SELECT key, value FROM jsonb_each_text(new_row)
EXCEPT ALL
SELECT key, value FROM jsonb_each_text(jold)
) AS j
),
(SELECT string_agg(format('%I = %L', j.key, j.value), ' AND ')
FROM jsonb_each_text(jold) j
WHERE j.key = ANY(identifier_columns)
),
info.valid_from_column_name,
quote_literal(bstartval::text),
info.valid_until_column_name,
quote_literal(bendval::text)
);
IF post_assigned THEN
EXECUTE format('INSERT INTO %I.%I (%s) VALUES (%s)',
info.table_schema,
info.table_name,
(SELECT string_agg(quote_ident(key), ', ' ORDER BY key) FROM jsonb_each_text(post_row)),
(SELECT string_agg(quote_nullable(value), ', ' ORDER BY key) FROM jsonb_each_text(post_row)));
END IF;
IF pre_assigned OR post_assigned THEN
SET CONSTRAINTS ALL IMMEDIATE;
END IF;
RETURN NEW;
END;
$function$;
CREATE FUNCTION sql_saga.truncate_system_versioning()
RETURNS trigger
LANGUAGE plpgsql
STRICT
SECURITY DEFINER
AS
$function$
#variable_conflict use_variable
DECLARE
history_schema name;
history_table name;
table_schema name;
table_name name;
BEGIN
SELECT n.nspname, c.relname
INTO table_schema, table_name
FROM pg_catalog.pg_class AS c
JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.oid = TG_RELID;
SELECT sv.history_schema_name, sv.history_table_name
INTO history_schema, history_table
FROM sql_saga.system_versioning AS sv
WHERE (sv.table_schema, sv.table_name) = (table_schema, table_name);
IF FOUND THEN
EXECUTE format('TRUNCATE %I.%I', history_schema, history_table);
END IF;
RETURN NULL;
END;
$function$;
CREATE FUNCTION sql_saga.drop_foreign_key_by_name(
table_oid regclass,
key_name name)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
AS
$function$
#variable_conflict use_variable
DECLARE
foreign_key_row sql_saga.foreign_keys;
unique_table_oid regclass;
fk_table_oid regclass;
fk_schema_name name;
fk_table_name name;
BEGIN
IF table_oid IS NULL AND key_name IS NULL THEN
RAISE EXCEPTION 'no table or key name specified';
END IF;
IF table_oid IS NOT NULL THEN
SELECT n.nspname, c.relname
INTO fk_schema_name, fk_table_name
FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE c.oid = table_oid;
END IF;
/* Always serialize operations on our catalogs */
PERFORM sql_saga.__internal_serialize(table_oid);
FOR foreign_key_row IN
SELECT fk.*
FROM sql_saga.foreign_keys AS fk
WHERE (table_oid IS NULL OR (fk.table_schema, fk.table_name) = (fk_schema_name, fk_table_name))
AND (fk.foreign_key_name = key_name OR key_name IS NULL)
LOOP
DELETE FROM sql_saga.foreign_keys AS fk
WHERE fk.foreign_key_name = foreign_key_row.foreign_key_name;
/*
* Make sure the table hasn't been dropped and that the triggers exist
* before doing these. We could use the IF EXISTS clause but we don't
* in order to avoid the NOTICE.
*/
fk_table_oid := format('%I.%I', foreign_key_row.table_schema, foreign_key_row.table_name)::regclass;
IF EXISTS (
SELECT FROM pg_catalog.pg_class AS c
WHERE c.oid = fk_table_oid)
AND EXISTS (
SELECT FROM pg_catalog.pg_trigger AS t
WHERE t.tgrelid = fk_table_oid
AND t.tgname IN (foreign_key_row.fk_insert_trigger, foreign_key_row.fk_update_trigger))
THEN
EXECUTE format('DROP TRIGGER %I ON %s', foreign_key_row.fk_insert_trigger, fk_table_oid);
EXECUTE format('DROP TRIGGER %I ON %s', foreign_key_row.fk_update_trigger, fk_table_oid);
END IF;
SELECT to_regclass(format('%I.%I', uk.table_schema, uk.table_name))
INTO unique_table_oid
FROM sql_saga.unique_keys AS uk
WHERE uk.unique_key_name = foreign_key_row.unique_key_name;
/* Ditto for the UNIQUE side. */
IF FOUND
AND EXISTS (
SELECT FROM pg_catalog.pg_class AS c
WHERE c.oid = unique_table_oid)
AND EXISTS (
SELECT FROM pg_catalog.pg_trigger AS t
WHERE t.tgrelid = unique_table_oid
AND t.tgname IN (foreign_key_row.uk_update_trigger, foreign_key_row.uk_delete_trigger))
THEN
EXECUTE format('DROP TRIGGER %I ON %s', foreign_key_row.uk_update_trigger, unique_table_oid);
EXECUTE format('DROP TRIGGER %I ON %s', foreign_key_row.uk_delete_trigger, unique_table_oid);
END IF;
END LOOP;
RETURN true;
END;
$function$;
CREATE FUNCTION sql_saga.drop_foreign_key(
table_oid regclass,
column_names name[],
era_name name,
drop_behavior sql_saga.drop_behavior DEFAULT 'RESTRICT'
)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS
$function$
#variable_conflict use_variable
DECLARE
key_name_found name;
fk_schema_name name;
fk_table_name name;
BEGIN
SELECT n.nspname, c.relname
INTO fk_schema_name, fk_table_name
FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE c.oid = table_oid;
SELECT fk.foreign_key_name INTO key_name_found
FROM sql_saga.foreign_keys AS fk
WHERE (fk.table_schema, fk.table_name) = (fk_schema_name, fk_table_name)
AND fk.column_names = column_names
AND fk.era_name = era_name;
IF NOT FOUND THEN
RAISE EXCEPTION 'foreign key on table % for columns % with era % does not exist', table_oid, column_names, era_name;
END IF;
PERFORM sql_saga.drop_foreign_key_by_name(table_oid, key_name_found);
END;
$function$;
CREATE FUNCTION sql_saga.drop_unique_key_by_name(
table_oid regclass,
key_name name,
drop_behavior sql_saga.drop_behavior DEFAULT 'RESTRICT',
cleanup boolean DEFAULT true
)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS
$function$
#variable_conflict use_variable
DECLARE
table_schema name;
table_name name;
foreign_key_row sql_saga.foreign_keys;
unique_key_row sql_saga.unique_keys;
BEGIN
IF table_oid IS NULL THEN
RAISE EXCEPTION 'no table name specified';
END IF;
SELECT n.nspname, c.relname
INTO table_schema, table_name
FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.oid = table_oid;
/* Always serialize operations on our catalogs */
PERFORM sql_saga.__internal_serialize(table_oid);
FOR unique_key_row IN
SELECT uk.*
FROM sql_saga.unique_keys AS uk
WHERE (uk.table_schema, uk.table_name) = (table_schema, table_name)
AND (uk.unique_key_name = key_name OR key_name IS NULL)
LOOP
/* Cascade to foreign keys, if desired */
FOR foreign_key_row IN
SELECT fk.*
FROM sql_saga.foreign_keys AS fk
WHERE fk.unique_key_name = unique_key_row.unique_key_name
LOOP
IF drop_behavior = 'RESTRICT' THEN
RAISE EXCEPTION 'cannot drop unique key "%" because foreign key "%" on table "%" depends on it',
unique_key_row.unique_key_name, foreign_key_row.foreign_key_name, format('%I.%I', foreign_key_row.table_schema, foreign_key_row.table_name)::regclass;
END IF;
PERFORM sql_saga.drop_foreign_key_by_name(NULL, foreign_key_row.foreign_key_name);
END LOOP;
DELETE FROM sql_saga.unique_keys AS uk
WHERE uk.unique_key_name = unique_key_row.unique_key_name;
/* If purging, drop the underlying constraints unless the table has been dropped */
IF cleanup AND EXISTS (
SELECT FROM pg_catalog.pg_class AS c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE (n.nspname, c.relname) = (unique_key_row.table_schema, unique_key_row.table_name))
THEN
EXECUTE format('ALTER TABLE %I.%I DROP CONSTRAINT %I, DROP CONSTRAINT %I',
unique_key_row.table_schema, unique_key_row.table_name, unique_key_row.unique_constraint, unique_key_row.exclude_constraint);
END IF;
END LOOP;
END;
$function$;
CREATE FUNCTION sql_saga.drop_unique_key(
table_oid regclass,
column_names name[],
era_name name,
drop_behavior sql_saga.drop_behavior DEFAULT 'RESTRICT',
cleanup boolean DEFAULT true
)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS
$function$
#variable_conflict use_variable
DECLARE
table_schema name;
table_name name;
key_name_found name;
BEGIN
SELECT n.nspname, c.relname
INTO table_schema, table_name
FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.oid = table_oid;
SELECT uk.unique_key_name INTO key_name_found
FROM sql_saga.unique_keys AS uk
WHERE (uk.table_schema, uk.table_name) = (table_schema, table_name)
AND uk.column_names = column_names
AND uk.era_name = era_name;
IF NOT FOUND THEN
RAISE EXCEPTION 'unique key on table % for columns % with era % does not exist', table_oid, column_names, era_name;
END IF;