Skip to content

Commit 08ab271

Browse files
committed
Merge branch 'feature/unique-keys-with-predicate'
2 parents 7fbe21c + 174a31a commit 08ab271

26 files changed

+458
-172
lines changed

README.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ In the context of this extension, a **Saga** represents the complete history of
2424
## Temporal Tables with Foreign Keys example
2525

2626
A simplified example to illustrate the concept.
27-
A temporal table has `valid_from` and `valid_until` columns, which define a `[)` period (inclusive start, exclusive end), aligning with PostgreSQL's native range types.
27+
A temporal table has `valid_from` and `valid_until` columns, which define a `[)` period (inclusive start, exclusive end), aligning with PostgreSQL's native range types. While `DATE` is used in these examples for simplicity, any data type that can form a range is supported, including `TIMESTAMPTZ`, `TIMESTAMP`, `INTEGER`, `BIGINT`, and `NUMERIC`.
2828

2929
### Entity Identifiers
3030

@@ -178,8 +178,9 @@ CREATE TABLE legal_unit (
178178
id SERIAL NOT NULL,
179179
legal_ident VARCHAR NOT NULL,
180180
name VARCHAR NOT NULL,
181-
valid_from TIMESTAMPTZ,
182-
valid_until TIMESTAMPTZ,
181+
status TEXT, -- e.g., 'active', 'inactive'
182+
valid_from DATE,
183+
valid_until DATE,
183184
valid_to DATE -- Optional: for human-readable inclusive end dates
184185
-- Note: A primary key on temporal tables is often not on the temporal columns
185186
);
@@ -193,17 +194,23 @@ CREATE TRIGGER legal_unit_synchronize_validity
193194
SELECT sql_saga.add_era(table_oid => 'legal_unit', valid_from_column_name => 'valid_from', valid_until_column_name => 'valid_until');
194195
-- Add temporal unique keys. A name is generated if the last argument is omitted.
195196
SELECT sql_saga.add_unique_key(table_oid => 'legal_unit', column_names => ARRAY['id'], unique_key_name => 'legal_unit_id_valid');
196-
SELECT sql_saga.add_unique_key(table_oid => 'legal_unit', column_names => ARRAY['name'], unique_key_name => 'legal_unit_name_valid');
197197
SELECT sql_saga.add_unique_key(table_oid => 'legal_unit', column_names => ARRAY['legal_ident'], unique_key_name => 'legal_unit_legal_ident_valid');
198+
-- Add a predicated unique key (e.g., only active units must have a unique name).
199+
SELECT sql_saga.add_unique_key(
200+
table_oid => 'legal_unit',
201+
column_names => ARRAY['name'],
202+
predicate => 'status = ''active''',
203+
unique_key_name => 'legal_unit_active_name_valid'
204+
);
198205
199206
200207
CREATE TABLE establishment (
201208
id SERIAL NOT NULL,
202209
name VARCHAR NOT NULL,
203210
address TEXT NOT NULL,
204211
legal_unit_id INTEGER NOT NULL,
205-
valid_from TIMESTAMPTZ,
206-
valid_until TIMESTAMPTZ
212+
valid_from DATE,
213+
valid_until DATE
207214
);
208215
209216
SELECT sql_saga.add_era(table_oid => 'establishment', valid_from_column_name => 'valid_from', valid_until_column_name => 'valid_until');
@@ -237,14 +244,14 @@ SELECT sql_saga.drop_foreign_key(
237244
era_name => 'valid'
238245
);
239246
240-
SELECT sql_saga.drop_unique_key(table_oid => 'establishment', key_name => 'establishment_id_valid');
241-
SELECT sql_saga.drop_unique_key(table_oid => 'establishment', key_name => 'establishment_name_valid');
247+
SELECT sql_saga.drop_unique_key_by_name(table_oid => 'establishment', key_name => 'establishment_id_valid');
248+
SELECT sql_saga.drop_unique_key_by_name(table_oid => 'establishment', key_name => 'establishment_name_valid');
242249
SELECT sql_saga.drop_era('establishment');
243250
244251
245-
SELECT sql_saga.drop_unique_key(table_oid => 'legal_unit', key_name => 'legal_unit_id_valid');
246-
SELECT sql_saga.drop_unique_key(table_oid => 'legal_unit', key_name => 'legal_unit_name_valid');
247-
SELECT sql_saga.drop_unique_key(table_oid => 'legal_unit', key_name => 'legal_unit_legal_ident_valid');
252+
SELECT sql_saga.drop_unique_key_by_name(table_oid => 'legal_unit', key_name => 'legal_unit_id_valid');
253+
SELECT sql_saga.drop_unique_key_by_name(table_oid => 'legal_unit', key_name => 'legal_unit_active_name_valid');
254+
SELECT sql_saga.drop_unique_key_by_name(table_oid => 'legal_unit', key_name => 'legal_unit_legal_ident_valid');
248255
SELECT sql_saga.drop_era('legal_unit');
249256
```
250257

@@ -279,7 +286,7 @@ The test suite uses `pg_regress` and is designed to be fully idempotent, creatin
279286
- `drop_era(table_oid regclass, era_name name DEFAULT 'valid', drop_behavior sql_saga.drop_behavior DEFAULT 'RESTRICT', cleanup boolean DEFAULT false) RETURNS boolean`
280287

281288
### Unique Keys
282-
- `add_unique_key(table_oid regclass, column_names name[], era_name name DEFAULT 'valid', unique_key_name name DEFAULT NULL, unique_constraint name DEFAULT NULL, exclude_constraint name DEFAULT NULL) RETURNS name`
289+
- `add_unique_key(table_oid regclass, column_names name[], era_name name DEFAULT 'valid', unique_key_name name DEFAULT NULL, unique_constraint name DEFAULT NULL, exclude_constraint name DEFAULT NULL, predicate text DEFAULT NULL) RETURNS name`
283290
- `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`
284291
- `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`
285292

expected/03_api_symmetry_test.out

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,9 @@ TABLE sql_saga.foreign_keys;
122122

123123
-- Test overloaded drop_unique_key
124124
TABLE sql_saga.unique_keys;
125-
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint
126-
-----------------+--------------+------------+--------------+----------+--------------------------------------+--------------------------
127-
parent_id_p | public | parent | {id} | p | parent_id_valid_from_valid_until_key | parent_id_int4range_excl
125+
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint | predicate
126+
-----------------+--------------+------------+--------------+----------+--------------------------------------+--------------------+-----------
127+
parent_id_p | public | parent | {id} | p | parent_id_valid_from_valid_until_key | parent_id_p_excl |
128128
(1 row)
129129

130130
SELECT sql_saga.drop_unique_key('parent', ARRAY['id'], 'p');
@@ -134,8 +134,8 @@ SELECT sql_saga.drop_unique_key('parent', ARRAY['id'], 'p');
134134
(1 row)
135135

136136
TABLE sql_saga.unique_keys;
137-
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint
138-
-----------------+--------------+------------+--------------+----------+-------------------+--------------------
137+
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint | predicate
138+
-----------------+--------------+------------+--------------+----------+-------------------+--------------------+-----------
139139
(0 rows)
140140

141141
-- Test original drop_unique_key (with positional and named arguments)
@@ -146,9 +146,9 @@ SELECT sql_saga.add_unique_key('parent', ARRAY['id'], 'p');
146146
(1 row)
147147

148148
TABLE sql_saga.unique_keys;
149-
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint
150-
-----------------+--------------+------------+--------------+----------+--------------------------------------+--------------------------
151-
parent_id_p | public | parent | {id} | p | parent_id_valid_from_valid_until_key | parent_id_int4range_excl
149+
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint | predicate
150+
-----------------+--------------+------------+--------------+----------+--------------------------------------+--------------------+-----------
151+
parent_id_p | public | parent | {id} | p | parent_id_valid_from_valid_until_key | parent_id_p_excl |
152152
(1 row)
153153

154154
-- by name (positional)
@@ -159,8 +159,8 @@ SELECT sql_saga.drop_unique_key_by_name('parent', 'parent_id_p');
159159
(1 row)
160160

161161
TABLE sql_saga.unique_keys;
162-
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint
163-
-----------------+--------------+------------+--------------+----------+-------------------+--------------------
162+
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint | predicate
163+
-----------------+--------------+------------+--------------+----------+-------------------+--------------------+-----------
164164
(0 rows)
165165

166166
-- Re-create to test named arguments
@@ -171,9 +171,9 @@ SELECT sql_saga.add_unique_key('parent', ARRAY['id'], 'p');
171171
(1 row)
172172

173173
TABLE sql_saga.unique_keys;
174-
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint
175-
-----------------+--------------+------------+--------------+----------+--------------------------------------+--------------------------
176-
parent_id_p | public | parent | {id} | p | parent_id_valid_from_valid_until_key | parent_id_int4range_excl
174+
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint | predicate
175+
-----------------+--------------+------------+--------------+----------+--------------------------------------+--------------------+-----------
176+
parent_id_p | public | parent | {id} | p | parent_id_valid_from_valid_until_key | parent_id_p_excl |
177177
(1 row)
178178

179179
-- by name (named)
@@ -184,8 +184,8 @@ SELECT sql_saga.drop_unique_key_by_name(table_oid => 'parent', key_name => 'pare
184184
(1 row)
185185

186186
TABLE sql_saga.unique_keys;
187-
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint
188-
-----------------+--------------+------------+--------------+----------+-------------------+--------------------
187+
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint | predicate
188+
-----------------+--------------+------------+--------------+----------+-------------------+--------------------+-----------
189189
(0 rows)
190190

191191
-- Test symmetrical add_era/drop_era with column management

expected/06_unique_foreign.out

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,16 @@ SELECT sql_saga.add_unique_key('uk'::regclass, ARRAY['id'], 'p', unique_key_name
4242
(1 row)
4343

4444
TABLE sql_saga.unique_keys;
45-
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint
46-
-----------------+--------------+------------+--------------+----------+-------------------+----------------------
47-
uk_id_p | public | uk | {id} | p | uk_pkey | uk_id_int4range_excl
45+
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint | predicate
46+
-----------------+--------------+------------+--------------+----------+-------------------+--------------------+-----------
47+
uk_id_p | public | uk | {id} | p | uk_pkey | uk_id_p_excl |
4848
(1 row)
4949

5050
INSERT INTO uk (id, valid_from, valid_until) VALUES (100, 2, 4), (100, 4, 5), (100, 5, 11); -- success
5151
INSERT INTO uk (id, valid_from, valid_until) VALUES (200, 2, 4), (200, 4, 5), (200, 6, 11); -- success
5252
SAVEPOINT pristine;
5353
INSERT INTO uk (id, valid_from, valid_until) VALUES (300, 2, 4), (300, 4, 6), (300, 5, 11); -- fail
54-
ERROR: conflicting key value violates exclusion constraint "uk_id_int4range_excl"
54+
ERROR: conflicting key value violates exclusion constraint "uk_id_p_excl"
5555
DETAIL: Key (id, int4range(valid_from, valid_until))=(300, [5,11)) conflicts with existing key (id, int4range(valid_from, valid_until))=(300, [4,6)).
5656
ROLLBACK TO SAVEPOINT pristine;
5757
CREATE TABLE fk (id integer, uk_id integer, valid_from integer, valid_until integer, PRIMARY KEY (id));

expected/09_drop_protection.out

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,12 @@ SELECT sql_saga.add_unique_key('dp', ARRAY['id'], 'p', 'k', 'u', 'x');
102102
SAVEPOINT s7;
103103
ALTER TABLE dp DROP CONSTRAINT u; -- fails
104104
ERROR: cannot drop constraint "u" on table "dp" because it is used in era unique key "k"
105-
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 219 at RAISE
105+
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 232 at RAISE
106106
ROLLBACK TO SAVEPOINT s7;
107107
SAVEPOINT s8;
108108
ALTER TABLE dp DROP CONSTRAINT x; -- fails
109109
ERROR: cannot drop constraint "x" on table "dp" because it is used in era unique key "k"
110-
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 231 at RAISE
110+
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 248 at RAISE
111111
ROLLBACK TO SAVEPOINT s8;
112112
SAVEPOINT s9;
113113
ALTER TABLE dp DROP CONSTRAINT dp_p_check; -- fails
@@ -129,22 +129,22 @@ SELECT sql_saga.add_foreign_key('dp_ref', ARRAY['id'], 'p', 'k', foreign_key_nam
129129
SAVEPOINT s10;
130130
DROP TRIGGER f_fk_insert ON dp_ref; -- fails
131131
ERROR: cannot drop trigger "f_fk_insert" on table "dp_ref" because it is used in era foreign key "f"
132-
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 247 at RAISE
132+
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 265 at RAISE
133133
ROLLBACK TO SAVEPOINT s10;
134134
SAVEPOINT s11;
135135
DROP TRIGGER f_fk_update ON dp_ref; -- fails
136136
ERROR: cannot drop trigger "f_fk_update" on table "dp_ref" because it is used in era foreign key "f"
137-
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 258 at RAISE
137+
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 276 at RAISE
138138
ROLLBACK TO SAVEPOINT s11;
139139
SAVEPOINT s12;
140140
DROP TRIGGER f_uk_update ON dp; -- fails
141141
ERROR: cannot drop trigger "f_uk_update" on table "dp" because it is used in era foreign key "f"
142-
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 270 at RAISE
142+
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 288 at RAISE
143143
ROLLBACK TO SAVEPOINT s12;
144144
SAVEPOINT s13;
145145
DROP TRIGGER f_uk_delete ON dp; -- fails
146146
ERROR: cannot drop trigger "f_uk_delete" on table "dp" because it is used in era foreign key "f"
147-
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 282 at RAISE
147+
CONTEXT: PL/pgSQL function sql_saga.drop_protection() line 300 at RAISE
148148
ROLLBACK TO SAVEPOINT s13;
149149
SELECT sql_saga.drop_foreign_key('dp_ref', ARRAY['id'], 'p');
150150
drop_foreign_key

expected/10_rename_following.out

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,18 @@ SELECT sql_saga.add_unique_key('rename_test', ARRAY['col2', 'col1', 'col3'], 'p'
113113
(1 row)
114114

115115
TABLE sql_saga.unique_keys;
116-
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint
117-
------------------------------+--------------+-------------+------------------+----------+---------------------------------------------------------+-------------------------------------------
118-
rename_test_col2_col1_col3_p | public | rename_test | {col2,col1,col3} | p | rename_test_col2_col1_col3_s < e_embedded " symbols_key | rename_test_col2_col1_col3_int4range_excl
116+
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint | predicate
117+
------------------------------+--------------+-------------+------------------+----------+---------------------------------------------------------+-----------------------------------+-----------
118+
rename_test_col2_col1_col3_p | public | rename_test | {col2,col1,col3} | p | rename_test_col2_col1_col3_s < e_embedded " symbols_key | rename_test_col2_col1_col3_p_excl |
119119
(1 row)
120120

121121
ALTER TABLE rename_test RENAME COLUMN col1 TO "COLUMN1";
122122
ALTER TABLE rename_test RENAME CONSTRAINT "rename_test_col2_col1_col3_s < e_embedded "" symbols_key" TO unconst;
123-
ALTER TABLE rename_test RENAME CONSTRAINT rename_test_col2_col1_col3_int4range_excl TO exconst;
123+
ALTER TABLE rename_test RENAME CONSTRAINT rename_test_col2_col1_col3_p_excl TO exconst;
124124
TABLE sql_saga.unique_keys;
125-
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint
126-
------------------------------+--------------+-------------+---------------------+----------+-------------------+--------------------
127-
rename_test_col2_col1_col3_p | public | rename_test | {col2,COLUMN1,col3} | p | unconst | exconst
125+
unique_key_name | table_schema | table_name | column_names | era_name | unique_constraint | exclude_constraint | predicate
126+
------------------------------+--------------+-------------+---------------------+----------+-------------------+--------------------+-----------
127+
rename_test_col2_col1_col3_p | public | rename_test | {col2,COLUMN1,col3} | p | unconst | exconst |
128128
(1 row)
129129

130130
/* foreign_keys */

expected/13_issues.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ ROLLBACK TO SAVEPOINT s4;
166166
SAVEPOINT s5;
167167
INSERT INTO uk(id, f, u) VALUES (4, 1, 4),
168168
(4, 3, 5);
169-
ERROR: conflicting key value violates exclusion constraint "uk_id_int4range_excl"
169+
ERROR: conflicting key value violates exclusion constraint "uk_id_p_excl"
170170
DETAIL: Key (id, int4range(f, u))=(4, [3,5)) conflicts with existing key (id, int4range(f, u))=(4, [1,4)).
171171
ROLLBACK TO SAVEPOINT s5;
172172
SELECT sql_saga.drop_foreign_key('fk', ARRAY['uk_id'], 'q');

0 commit comments

Comments
 (0)