mirror of
https://github.com/postgres/postgres.git
synced 2026-10-23 13:29:09 +03:00
Remove batching from RI fast-path checks
Commitb7b27eb41aadded batching to the direct-index fast path for foreign key checks introduced by2da86c1ef9. Instead of probing the referenced index once per row, it accumulated referencing rows and checked them in groups, using SK_SEARCHARRAY for single-column foreign keys. The batching requires state to survive across trigger invocations and to be flushed at the end of each trigger-firing cycle. Follow-up work has had to define how that state interacts with nested trigger firing, subtransactions, deferred constraints, and SET CONSTRAINTS. Failure to handle one of those cases can leave a buffered check unperformed, allowing a transaction to commit a permanent foreign key violation without reporting an error. With PostgreSQL 19 close to release, there is not enough time to gain confidence that all relevant trigger and transaction states have been covered. Remove the batching and its after-trigger callback infrastructure, including the per-batch RI cache and associated subtransaction cleanup. Restore the early exit in AfterTriggerFireDeferred(), since batch callbacks can no longer queue additional deferred triggers after afterTriggerInvokeEvents() returns. Remove tests that exercise only the batching implementation and its callback and cache lifetime machinery. Retain tests that continue to exercise the underlying per-row fast path, including validation, scan-key construction, cross-type rechecks, deferred checks, and metadata invalidation. Keep the underlying per-row fast path. It performs each check synchronously and retains no pending checks across trigger invocations. Also retain the fixes made to the per-row probe path, including support for domain-typed referencing columns, restriction to btree referenced indexes, concurrent index replacement, metadata invalidation, and nullable referenced keys. This removal applies only to REL_19_STABLE. Discussion: https://postgr.es/m/CA+HiwqEDcCSDoYTtoXH4Ta_BpGN+revMZSLLj6GRz2_5ybwE1g@mail.gmail.com
This commit is contained in:
@@ -17,9 +17,6 @@
|
||||
3d2f2eb1664e5c823b66a063d2de9a9787970d42 # 2026-08-15 23:16:15 +0900
|
||||
# pgindent fix for commit 7b7c4a8dcc9
|
||||
|
||||
52d87b42d9bef6a2ca66572fc39e5c1b0f61f5dd # 2026-08-07 17:38:13 +0900
|
||||
# Fix indentation issue introduced by commit 291a4bd2ca
|
||||
|
||||
b1aeda3ec939c2867e5c3eb4ee7e1bae4768503e # 2026-07-30 09:26:07 +0200
|
||||
# pgindent fix for 4ee0ccfd
|
||||
|
||||
|
||||
@@ -698,10 +698,6 @@ Author: Amit Langote <amitlan@postgresql.org>
|
||||
2026-03-31 [2da86c1ef] Add fast path for foreign key constraint checks
|
||||
Author: Amit Langote <amitlan@postgresql.org>
|
||||
2026-04-01 [e484b0eea] Fix two issues in fast-path FK check introduced by commi
|
||||
Author: Amit Langote <amitlan@postgresql.org>
|
||||
2026-04-03 [b7b27eb41] Optimize fast-path FK checks with batched index probes
|
||||
Author: Amit Langote <amitlan@postgresql.org>
|
||||
2026-04-07 [5c54c3ed1] Fix deferred FK check batching introduced by commit b7b2
|
||||
-->
|
||||
|
||||
<listitem>
|
||||
|
||||
@@ -5245,7 +5245,6 @@ CommitSubTransaction(void)
|
||||
s->parent->subTransactionId);
|
||||
AtEOSubXact_HashTables(true, s->nestingLevel);
|
||||
AtEOSubXact_PgStat(true, s->nestingLevel);
|
||||
AtEOSubXact_RI(true, s->subTransactionId, s->parent->subTransactionId);
|
||||
AtSubCommit_Snapshot(s->nestingLevel);
|
||||
|
||||
/*
|
||||
@@ -5420,7 +5419,6 @@ AbortSubTransaction(void)
|
||||
s->parent->subTransactionId);
|
||||
AtEOSubXact_HashTables(false, s->nestingLevel);
|
||||
AtEOSubXact_PgStat(false, s->nestingLevel);
|
||||
AtEOSubXact_RI(false, s->subTransactionId, s->parent->subTransactionId);
|
||||
AtSubAbort_Snapshot(s->nestingLevel);
|
||||
}
|
||||
|
||||
|
||||
@@ -3907,18 +3907,6 @@ typedef struct AfterTriggersData
|
||||
/* per-subtransaction-level data: */
|
||||
AfterTriggersTransData *trans_stack; /* array of structs shown below */
|
||||
int maxtransdepth; /* allocated len of above array */
|
||||
|
||||
List *batch_callbacks; /* List of AfterTriggerCallbackItem; for
|
||||
* deferred constraints */
|
||||
bool firing_batch_callbacks; /* true when in
|
||||
* FireAfterTriggerBatchCallbacks() */
|
||||
|
||||
/*
|
||||
* Incremented around the trigger-firing loops in AfterTriggerEndQuery,
|
||||
* AfterTriggerFireDeferred, and AfterTriggerSetState. Used by
|
||||
* AfterTriggerIsActive() to signal that after-trigger firing is active.
|
||||
*/
|
||||
int firing_depth;
|
||||
} AfterTriggersData;
|
||||
|
||||
struct AfterTriggersQueryData
|
||||
@@ -3926,7 +3914,6 @@ struct AfterTriggersQueryData
|
||||
AfterTriggerEventList events; /* events pending from this query */
|
||||
Tuplestorestate *fdw_tuplestore; /* foreign tuples for said events */
|
||||
List *tables; /* list of AfterTriggersTableData, see below */
|
||||
List *batch_callbacks; /* List of AfterTriggerCallbackItem */
|
||||
};
|
||||
|
||||
struct AfterTriggersTransData
|
||||
@@ -3935,8 +3922,6 @@ struct AfterTriggersTransData
|
||||
SetConstraintState state; /* saved S C state, or NULL if not yet saved */
|
||||
AfterTriggerEventList events; /* saved list pointer */
|
||||
int query_depth; /* saved query_depth */
|
||||
int firing_depth; /* saved firing_depth */
|
||||
bool firing_batch_callbacks; /* saved firing_batch_callbacks */
|
||||
CommandId firing_counter; /* saved firing_counter */
|
||||
};
|
||||
|
||||
@@ -3958,13 +3943,6 @@ struct AfterTriggersTableData
|
||||
TupleTableSlot *storeslot; /* for converting to tuplestore's format */
|
||||
};
|
||||
|
||||
/* Entry in afterTriggers.batch_callbacks */
|
||||
typedef struct AfterTriggerCallbackItem
|
||||
{
|
||||
AfterTriggerBatchCallback callback;
|
||||
void *arg;
|
||||
} AfterTriggerCallbackItem;
|
||||
|
||||
static AfterTriggersData afterTriggers;
|
||||
|
||||
static void AfterTriggerExecute(EState *estate,
|
||||
@@ -4000,7 +3978,6 @@ static SetConstraintState SetConstraintStateAddItem(SetConstraintState state,
|
||||
Oid tgoid, bool tgisdeferred);
|
||||
static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);
|
||||
|
||||
static void FireAfterTriggerBatchCallbacks(List *callbacks);
|
||||
|
||||
/*
|
||||
* Get the FDW tuplestore for the current trigger query level, creating it
|
||||
@@ -5126,9 +5103,6 @@ AfterTriggerBeginXact(void)
|
||||
*/
|
||||
afterTriggers.firing_counter = (CommandId) 1; /* mustn't be 0 */
|
||||
afterTriggers.query_depth = -1;
|
||||
afterTriggers.firing_depth = 0;
|
||||
afterTriggers.batch_callbacks = NIL;
|
||||
afterTriggers.firing_batch_callbacks = false;
|
||||
|
||||
/*
|
||||
* Verify that there is no leftover state remaining. If these assertions
|
||||
@@ -5213,7 +5187,6 @@ AfterTriggerEndQuery(EState *estate)
|
||||
*/
|
||||
qs = &afterTriggers.query_stack[afterTriggers.query_depth];
|
||||
|
||||
afterTriggers.firing_depth++;
|
||||
for (;;)
|
||||
{
|
||||
if (afterTriggerMarkEvents(&qs->events, &afterTriggers.events, true))
|
||||
@@ -5251,23 +5224,10 @@ AfterTriggerEndQuery(EState *estate)
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* Fire batch callbacks before releasing query-level storage and before
|
||||
* decrementing query_depth. Callbacks may do real work (index probes,
|
||||
* error reporting).
|
||||
*
|
||||
* Recompute qs first: the loop above refreshes it after each
|
||||
* afterTriggerInvokeEvents() call (see comment there), but the "all
|
||||
* fired" break exits without doing so, leaving qs potentially stale here.
|
||||
*/
|
||||
qs = &afterTriggers.query_stack[afterTriggers.query_depth];
|
||||
FireAfterTriggerBatchCallbacks(qs->batch_callbacks);
|
||||
|
||||
/* Release query-level-local storage, including tuplestores if any */
|
||||
AfterTriggerFreeQuery(&afterTriggers.query_stack[afterTriggers.query_depth]);
|
||||
|
||||
afterTriggers.query_depth--;
|
||||
afterTriggers.firing_depth--;
|
||||
}
|
||||
|
||||
|
||||
@@ -5324,9 +5284,6 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
|
||||
*/
|
||||
qs->tables = NIL;
|
||||
list_free_deep(tables);
|
||||
|
||||
list_free_deep(qs->batch_callbacks);
|
||||
qs->batch_callbacks = NIL;
|
||||
}
|
||||
|
||||
|
||||
@@ -5366,34 +5323,17 @@ AfterTriggerFireDeferred(void)
|
||||
* Run all the remaining triggers. Loop until they are all gone, in case
|
||||
* some trigger queues more for us to do.
|
||||
*/
|
||||
afterTriggers.firing_depth++;
|
||||
while (afterTriggerMarkEvents(events, NULL, false))
|
||||
{
|
||||
CommandId firing_id = afterTriggers.firing_counter++;
|
||||
|
||||
(void) afterTriggerInvokeEvents(events, firing_id, NULL, true);
|
||||
|
||||
/*
|
||||
* Flush any fast-path FK-check batches accumulated by the triggers
|
||||
* just fired. A batch callback runs user-supplied cast or equality
|
||||
* functions, whose DML can queue further deferred trigger events.
|
||||
* Flush inside the loop so afterTriggerMarkEvents() sees any such
|
||||
* events on the next iteration and fires them; flushing after the
|
||||
* loop would leave them unfired, silently skipping e.g. a deferred FK
|
||||
* check and letting a violating row commit. (The former "all fired"
|
||||
* break is therefore gone: the loop now terminates only when
|
||||
* afterTriggerMarkEvents() finds nothing left, including events
|
||||
* queued by the flush.)
|
||||
*/
|
||||
FireAfterTriggerBatchCallbacks(afterTriggers.batch_callbacks);
|
||||
if (afterTriggerInvokeEvents(events, firing_id, NULL, true))
|
||||
break; /* all fired */
|
||||
}
|
||||
|
||||
afterTriggers.firing_depth--;
|
||||
|
||||
/*
|
||||
* We don't bother freeing the event list or batch_callbacks, since they
|
||||
* will go away anyway (and more efficiently than via pfree) in
|
||||
* AfterTriggerEndXact.
|
||||
* We don't bother freeing the event list, since it will go away anyway
|
||||
* (and more efficiently than via pfree) in AfterTriggerEndXact.
|
||||
*/
|
||||
|
||||
if (snap_pushed)
|
||||
@@ -5455,12 +5395,6 @@ AfterTriggerEndXact(bool isCommit)
|
||||
|
||||
/* No more afterTriggers manipulation until next transaction starts. */
|
||||
afterTriggers.query_depth = -1;
|
||||
|
||||
afterTriggers.firing_depth = 0;
|
||||
|
||||
list_free_deep(afterTriggers.batch_callbacks);
|
||||
afterTriggers.batch_callbacks = NIL;
|
||||
afterTriggers.firing_batch_callbacks = false;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -5508,9 +5442,6 @@ AfterTriggerBeginSubXact(void)
|
||||
afterTriggers.trans_stack[my_level].state = NULL;
|
||||
afterTriggers.trans_stack[my_level].events = afterTriggers.events;
|
||||
afterTriggers.trans_stack[my_level].query_depth = afterTriggers.query_depth;
|
||||
afterTriggers.trans_stack[my_level].firing_depth = afterTriggers.firing_depth;
|
||||
afterTriggers.trans_stack[my_level].firing_batch_callbacks =
|
||||
afterTriggers.firing_batch_callbacks;
|
||||
afterTriggers.trans_stack[my_level].firing_counter = afterTriggers.firing_counter;
|
||||
}
|
||||
|
||||
@@ -5610,29 +5541,6 @@ AfterTriggerEndSubXact(bool isCommit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Restore firing_depth and firing_batch_callbacks to their values at
|
||||
* subtransaction start. The matching decrement of firing_depth in
|
||||
* AfterTriggerEndQuery()/AfterTriggerFireDeferred(), and the clearing of
|
||||
* firing_batch_callbacks in FireAfterTriggerBatchCallbacks(), run after
|
||||
* their loops and are not protected by PG_FINALLY. A trigger or batch
|
||||
* callback error caught by this subtransaction can therefore leave either
|
||||
* one set; restoring the saved values unwinds only this subtransaction's
|
||||
* firing.
|
||||
*
|
||||
* Restoring (rather than zeroing/clearing) matters because a
|
||||
* subtransaction can begin and end while an outer query's triggers are
|
||||
* firing -- for instance a batch callback whose user-supplied cast or
|
||||
* equality function runs DML in a BEGIN ... EXCEPTION block. There
|
||||
* firing_depth is positive and firing_batch_callbacks is true; forcing
|
||||
* them to 0/false would corrupt the outer firing
|
||||
* (FireAfterTriggerBatchCallbacks() asserts firing_depth > 0, and
|
||||
* clearing the guard would defeat its re-entrancy check).
|
||||
*/
|
||||
afterTriggers.firing_depth = afterTriggers.trans_stack[my_level].firing_depth;
|
||||
afterTriggers.firing_batch_callbacks =
|
||||
afterTriggers.trans_stack[my_level].firing_batch_callbacks;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -5787,7 +5695,6 @@ AfterTriggerEnlargeQueryState(void)
|
||||
qs->events.tailfree = NULL;
|
||||
qs->fdw_tuplestore = NULL;
|
||||
qs->tables = NIL;
|
||||
qs->batch_callbacks = NIL;
|
||||
|
||||
++init_depth;
|
||||
}
|
||||
@@ -6137,7 +6044,6 @@ AfterTriggerSetState(ConstraintsSetStmt *stmt)
|
||||
AfterTriggerEventList *events = &afterTriggers.events;
|
||||
bool snapshot_set = false;
|
||||
|
||||
afterTriggers.firing_depth++;
|
||||
while (afterTriggerMarkEvents(events, NULL, true))
|
||||
{
|
||||
CommandId firing_id = afterTriggers.firing_counter++;
|
||||
@@ -6167,14 +6073,6 @@ AfterTriggerSetState(ConstraintsSetStmt *stmt)
|
||||
break; /* all fired */
|
||||
}
|
||||
|
||||
/*
|
||||
* Flush any fast-path batches accumulated by the triggers just fired.
|
||||
*/
|
||||
FireAfterTriggerBatchCallbacks(afterTriggers.batch_callbacks);
|
||||
afterTriggers.firing_depth--;
|
||||
list_free_deep(afterTriggers.batch_callbacks);
|
||||
afterTriggers.batch_callbacks = NIL;
|
||||
|
||||
if (snapshot_set)
|
||||
PopActiveSnapshot();
|
||||
}
|
||||
@@ -6871,99 +6769,3 @@ check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple tuple)
|
||||
|
||||
return tuple;
|
||||
}
|
||||
|
||||
/*
|
||||
* RegisterAfterTriggerBatchCallback
|
||||
* Register a function to be called when the current trigger-firing
|
||||
* batch completes.
|
||||
*
|
||||
* Must be called from within a trigger function's execution context
|
||||
* (i.e., while afterTriggers state is active).
|
||||
*
|
||||
* The callback list is cleared after invocation, so the caller must
|
||||
* re-register for each new batch if needed.
|
||||
*/
|
||||
void
|
||||
RegisterAfterTriggerBatchCallback(AfterTriggerBatchCallback callback,
|
||||
void *arg)
|
||||
{
|
||||
AfterTriggerCallbackItem *item;
|
||||
MemoryContext oldcxt;
|
||||
|
||||
/*
|
||||
* Allocate in TopTransactionContext so the item survives for the duration
|
||||
* of the batch, which may span multiple trigger invocations.
|
||||
*
|
||||
* Must be called while afterTriggers is active; callbacks registered
|
||||
* outside a trigger-firing context would never fire.
|
||||
*/
|
||||
Assert(afterTriggers.firing_depth > 0);
|
||||
Assert(!afterTriggers.firing_batch_callbacks);
|
||||
oldcxt = MemoryContextSwitchTo(TopTransactionContext);
|
||||
item = palloc(sizeof(AfterTriggerCallbackItem));
|
||||
item->callback = callback;
|
||||
item->arg = arg;
|
||||
if (afterTriggers.query_depth >= 0)
|
||||
{
|
||||
AfterTriggersQueryData *qs =
|
||||
&afterTriggers.query_stack[afterTriggers.query_depth];
|
||||
|
||||
qs->batch_callbacks = lappend(qs->batch_callbacks, item);
|
||||
}
|
||||
else
|
||||
afterTriggers.batch_callbacks =
|
||||
lappend(afterTriggers.batch_callbacks, item);
|
||||
MemoryContextSwitchTo(oldcxt);
|
||||
}
|
||||
|
||||
/*
|
||||
* FireAfterTriggerBatchCallbacks
|
||||
* Invoke all callbacks in the given list.
|
||||
*
|
||||
* Memory cleanup of the list and its items is handled by the caller
|
||||
* (AfterTriggerFreeQuery for query-level callbacks, AfterTriggerEndXact
|
||||
* for top-level deferred callbacks).
|
||||
*/
|
||||
static void
|
||||
FireAfterTriggerBatchCallbacks(List *callbacks)
|
||||
{
|
||||
ListCell *lc;
|
||||
|
||||
Assert(afterTriggers.firing_depth > 0);
|
||||
afterTriggers.firing_batch_callbacks = true;
|
||||
foreach(lc, callbacks)
|
||||
{
|
||||
AfterTriggerCallbackItem *item = lfirst(lc);
|
||||
|
||||
item->callback(item->arg);
|
||||
}
|
||||
afterTriggers.firing_batch_callbacks = false;
|
||||
}
|
||||
|
||||
/*
|
||||
* AfterTriggerIsActive
|
||||
* Returns true if we're inside the after-trigger framework where
|
||||
* registered batch callbacks will actually be invoked.
|
||||
*
|
||||
* This is false during validateForeignKeyConstraint(), which calls
|
||||
* RI trigger functions directly outside the after-trigger framework.
|
||||
*/
|
||||
bool
|
||||
AfterTriggerIsActive(void)
|
||||
{
|
||||
return afterTriggers.firing_depth > 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* AfterTriggerCurrentQueryDepth
|
||||
* Return the current after-trigger query nesting depth.
|
||||
*
|
||||
* Lets a batch-callback registrant (e.g. the RI fast path) associate cached
|
||||
* state with the firing cycle that created it, so a nested cycle's callback
|
||||
* acts only on its own entries. Returns -1 outside any query level.
|
||||
*/
|
||||
int
|
||||
AfterTriggerCurrentQueryDepth(void)
|
||||
{
|
||||
return afterTriggers.query_depth;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -289,30 +289,6 @@ extern void RI_PartitionRemove_Check(Trigger *trigger, Relation fk_rel,
|
||||
|
||||
extern int RI_FKey_trigger_type(Oid tgfoid);
|
||||
|
||||
/*
|
||||
* Callback type for end-of-trigger-batch callbacks.
|
||||
*
|
||||
* Currently used by ri_triggers.c to flush fast-path FK batches and
|
||||
* clean up associated resources.
|
||||
*
|
||||
* Registered via RegisterAfterTriggerBatchCallback(). Invoked when
|
||||
* the current trigger-firing batch completes:
|
||||
* - AfterTriggerEndQuery() (immediate constraints)
|
||||
* - AfterTriggerFireDeferred() (deferred constraints at COMMIT)
|
||||
* - AfterTriggerSetState() (SET CONSTRAINTS IMMEDIATE)
|
||||
*
|
||||
* The callback list is cleared after each batch. Callers must
|
||||
* re-register if they need to be called again in a subsequent batch.
|
||||
*/
|
||||
typedef void (*AfterTriggerBatchCallback) (void *arg);
|
||||
|
||||
extern void RegisterAfterTriggerBatchCallback(AfterTriggerBatchCallback callback,
|
||||
void *arg);
|
||||
extern bool AfterTriggerIsActive(void);
|
||||
extern int AfterTriggerCurrentQueryDepth(void);
|
||||
|
||||
extern void AtEOXact_RI(bool isCommit);
|
||||
extern void AtEOSubXact_RI(bool isCommit, SubTransactionId mySubid,
|
||||
SubTransactionId parentSubid);
|
||||
|
||||
#endif /* TRIGGER_H */
|
||||
|
||||
@@ -30,7 +30,6 @@ test: detach-partition-concurrently-2
|
||||
test: detach-partition-concurrently-3
|
||||
test: detach-partition-concurrently-4
|
||||
test: fk-contention
|
||||
test: fk-crosstype-recheck
|
||||
test: fk-deadlock
|
||||
test: fk-deadlock2
|
||||
test: fk-partitioned-1
|
||||
@@ -39,6 +38,7 @@ test: fk-snapshot
|
||||
test: fk-snapshot-2
|
||||
test: fk-snapshot-3
|
||||
test: fk-concurrent-pk-upd
|
||||
test: fk-crosstype-recheck
|
||||
test: fk-fastpath-null-key
|
||||
test: subxid-overflow
|
||||
test: eval-plan-qual
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# A foreign key check racing a rebuild of the index it resolves through.
|
||||
#
|
||||
# The RI fast path reads conindid before it locks the referenced table, so a
|
||||
# concurrent REINDEX CONCURRENTLY can repoint the constraint and drop that
|
||||
# index in between. Cover both outcomes: an index that is already gone, and
|
||||
# one that is only marked dead and so no longer receives new rows.
|
||||
# Pause the RI fast path before it locks the referenced table, allowing
|
||||
# REINDEX CONCURRENTLY to repoint the constraint. Verify that the check
|
||||
# uses the replacement index whether the old index has been dropped or
|
||||
# has only been marked dead and no longer receives new rows.
|
||||
|
||||
setup
|
||||
{
|
||||
@@ -38,7 +38,7 @@ step reindex { REINDEX INDEX CONCURRENTLY ri_pk_pkey; }
|
||||
# Forces the rebuild to have finished before anything else runs.
|
||||
step reindexed { }
|
||||
|
||||
# The writer, stopped after it read conindid and before it locks ri_pk.
|
||||
# The writer, stopped before it locks ri_pk and reloads the constraint.
|
||||
session s2
|
||||
setup
|
||||
{
|
||||
@@ -102,7 +102,7 @@ step rows { SELECT id, pid FROM ri_fk WHERE id IN (1, 2) ORDER BY id; }
|
||||
# The constraint must still be enforced, not merely not crashing.
|
||||
step orphan { INSERT INTO ri_fk VALUES (999, 12345); }
|
||||
|
||||
# Batched call site.
|
||||
# Old index dropped before the check resumes.
|
||||
permutation reindex upd wake_reindex reindexed swapped wake_check checked rows orphan
|
||||
# Dead index rather than dropped one.
|
||||
permutation park_drop reindex upd_new wake_reindex await_drop dead wake_check checked wake_drop reindexed rows orphan
|
||||
|
||||
@@ -3557,27 +3557,45 @@ DETAIL: drop cascades to table fkpart13_t1
|
||||
drop cascades to table fkpart13_t2
|
||||
drop cascades to table fkpart13_t3
|
||||
RESET search_path;
|
||||
-- Tests foreign key check fast-path no-cache path.
|
||||
-- Test the fast-path ALTER TABLE validation path. RLS on the referenced
|
||||
-- table makes RI_Initial_Check() decline the set-based check, so validation
|
||||
-- invokes the RI trigger once per referencing row.
|
||||
CREATE ROLE regress_fk_fastpath_role;
|
||||
CREATE TABLE fp_pk_alter (a int PRIMARY KEY);
|
||||
INSERT INTO fp_pk_alter SELECT generate_series(1, 100);
|
||||
ALTER TABLE fp_pk_alter ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY fp_pk_alter_all ON fp_pk_alter
|
||||
FOR ALL USING (true) WITH CHECK (true);
|
||||
GRANT REFERENCES, SELECT ON fp_pk_alter TO regress_fk_fastpath_role;
|
||||
CREATE TABLE fp_fk_alter (a int);
|
||||
INSERT INTO fp_fk_alter SELECT generate_series(1, 100);
|
||||
ALTER TABLE fp_fk_alter OWNER TO regress_fk_fastpath_role;
|
||||
-- Validation path: should succeed
|
||||
SET ROLE regress_fk_fastpath_role;
|
||||
ALTER TABLE fp_fk_alter ADD FOREIGN KEY (a) REFERENCES fp_pk_alter;
|
||||
INSERT INTO fp_fk_alter VALUES (101); -- should fail (constraint active)
|
||||
ERROR: insert or update on table "fp_fk_alter" violates foreign key constraint "fp_fk_alter_a_fkey"
|
||||
DETAIL: Key (a)=(101) is not present in table "fp_pk_alter".
|
||||
RESET ROLE;
|
||||
DROP TABLE fp_fk_alter, fp_pk_alter;
|
||||
-- Separate test: validation catches existing violation
|
||||
CREATE TABLE fp_pk_alter2 (a int PRIMARY KEY);
|
||||
INSERT INTO fp_pk_alter2 VALUES (1);
|
||||
ALTER TABLE fp_pk_alter2 ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY fp_pk_alter2_all ON fp_pk_alter2
|
||||
FOR ALL USING (true) WITH CHECK (true);
|
||||
GRANT REFERENCES, SELECT ON fp_pk_alter2 TO regress_fk_fastpath_role;
|
||||
CREATE TABLE fp_fk_alter2 (a int);
|
||||
INSERT INTO fp_fk_alter2 VALUES (1), (200); -- 200 has no PK match
|
||||
ALTER TABLE fp_fk_alter2 OWNER TO regress_fk_fastpath_role;
|
||||
SET ROLE regress_fk_fastpath_role;
|
||||
ALTER TABLE fp_fk_alter2 ADD FOREIGN KEY (a) REFERENCES fp_pk_alter2; -- should fail
|
||||
ERROR: insert or update on table "fp_fk_alter2" violates foreign key constraint "fp_fk_alter2_a_fkey"
|
||||
DETAIL: Key (a)=(200) is not present in table "fp_pk_alter2".
|
||||
RESET ROLE;
|
||||
DROP TABLE fp_fk_alter2, fp_pk_alter2;
|
||||
-- Tests that the fast-path handles caching for multiple constraints
|
||||
DROP ROLE regress_fk_fastpath_role;
|
||||
-- Tests that the fast-path handles multiple constraints
|
||||
CREATE TABLE fp_pk1 (a int PRIMARY KEY);
|
||||
CREATE TABLE fp_pk2 (b int PRIMARY KEY);
|
||||
INSERT INTO fp_pk1 VALUES (1);
|
||||
@@ -3586,26 +3604,26 @@ CREATE TABLE fp_multi_fk (
|
||||
a int REFERENCES fp_pk1,
|
||||
b int REFERENCES fp_pk2
|
||||
);
|
||||
INSERT INTO fp_multi_fk VALUES (1, 1); -- two constraints, one batch
|
||||
INSERT INTO fp_multi_fk VALUES (1, 1); -- two constraints
|
||||
INSERT INTO fp_multi_fk VALUES (1, 2); -- second constraint fails
|
||||
ERROR: insert or update on table "fp_multi_fk" violates foreign key constraint "fp_multi_fk_b_fkey"
|
||||
DETAIL: Key (b)=(2) is not present in table "fp_pk2".
|
||||
DROP TABLE fp_multi_fk, fp_pk1, fp_pk2;
|
||||
-- Test that fast-path cache handles deferred constraints and SET CONSTRAINTS IMMEDIATE
|
||||
-- Test that fast-path handles deferred constraints and SET CONSTRAINTS IMMEDIATE
|
||||
CREATE TABLE fp_pk_defer (a int PRIMARY KEY);
|
||||
CREATE TABLE fp_fk_defer (a int REFERENCES fp_pk_defer DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO fp_pk_defer VALUES (1), (2);
|
||||
BEGIN;
|
||||
INSERT INTO fp_fk_defer VALUES (1);
|
||||
INSERT INTO fp_fk_defer VALUES (2);
|
||||
SET CONSTRAINTS ALL IMMEDIATE; -- fires batch callback here
|
||||
INSERT INTO fp_fk_defer VALUES (3); -- should fail, also tests that cache was cleaned up
|
||||
SET CONSTRAINTS ALL IMMEDIATE; -- fires deferred checks here
|
||||
INSERT INTO fp_fk_defer VALUES (3); -- should fail
|
||||
ERROR: insert or update on table "fp_fk_defer" violates foreign key constraint "fp_fk_defer_a_fkey"
|
||||
DETAIL: Key (a)=(3) is not present in table "fp_pk_defer".
|
||||
COMMIT;
|
||||
DROP TABLE fp_pk_defer, fp_fk_defer;
|
||||
-- A deferred FK check queued while firing deferred triggers at commit must
|
||||
-- not be lost. The RI fast-path flush runs during the deferred firing loop
|
||||
-- not be lost. The RI fast-path runs during the deferred firing loop
|
||||
-- and can run user cast/equality code whose DML queues a further deferred
|
||||
-- check; that check must still fire. fk_defer_main's deferred fast-path check
|
||||
-- runs a cast whose function inserts a violating row into fk_defer_t2,
|
||||
@@ -3626,8 +3644,7 @@ CREATE TABLE fk_defer_main_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fk_defer_main_pk VALUES (1);
|
||||
CREATE TABLE fk_defer_main (a fk_defer_vch REFERENCES fk_defer_main_pk(id)
|
||||
DEFERRABLE INITIALLY DEFERRED);
|
||||
-- At COMMIT the queued fk_defer_t2 check must fire and report the violation,
|
||||
-- rather than being dropped (which would let the dangling row commit).
|
||||
-- At COMMIT the queued fk_defer_t2 check must fire and report the violation
|
||||
BEGIN;
|
||||
INSERT INTO fk_defer_main VALUES (row(1)::fk_defer_vch);
|
||||
COMMIT; -- expected: ERROR on fk_defer_t2_a_fkey
|
||||
@@ -3643,25 +3660,6 @@ DROP TABLE fk_defer_main, fk_defer_main_pk, fk_defer_t2, fk_defer_t2_pk;
|
||||
DROP CAST (fk_defer_vch AS int);
|
||||
DROP FUNCTION fk_defer_cast(fk_defer_vch);
|
||||
DROP TYPE fk_defer_vch;
|
||||
-- Subtransaction abort: cached state must be invalidated on ROLLBACK TO
|
||||
CREATE TABLE fp_pk_subxact (a int PRIMARY KEY);
|
||||
CREATE TABLE fp_fk_subxact (a int REFERENCES fp_pk_subxact);
|
||||
INSERT INTO fp_pk_subxact VALUES (1), (2);
|
||||
BEGIN;
|
||||
INSERT INTO fp_fk_subxact VALUES (1);
|
||||
SAVEPOINT sp1;
|
||||
INSERT INTO fp_fk_subxact VALUES (2);
|
||||
ROLLBACK TO sp1;
|
||||
INSERT INTO fp_fk_subxact VALUES (1);
|
||||
COMMIT;
|
||||
SELECT * FROM fp_fk_subxact;
|
||||
a
|
||||
---
|
||||
1
|
||||
1
|
||||
(2 rows)
|
||||
|
||||
DROP TABLE fp_fk_subxact, fp_pk_subxact;
|
||||
-- FK check must see PK rows inserted by earlier AFTER triggers
|
||||
-- firing on the same statement
|
||||
CREATE TABLE fp_pk_cci (a int PRIMARY KEY);
|
||||
@@ -3751,7 +3749,7 @@ SELECT count(*) FROM fk_spi;
|
||||
(1 row)
|
||||
|
||||
ROLLBACK;
|
||||
-- Multi-column FK: exercises batched per-row probing with composite keys
|
||||
-- Multi-column FK: exercises direct per-row probing with composite keys
|
||||
CREATE TABLE fp_pk_multi (a int, b int, PRIMARY KEY (a, b));
|
||||
INSERT INTO fp_pk_multi SELECT i, i FROM generate_series(1, 100) i;
|
||||
CREATE TABLE fp_fk_multi (x int, a int, b int,
|
||||
@@ -3790,7 +3788,7 @@ INSERT INTO fp_fk_same VALUES (9, 9); -- should fail
|
||||
ERROR: insert or update on table "fp_fk_same" violates foreign key constraint "fp_fk_same_c2_c1_fkey"
|
||||
DETAIL: Key (c2, c1)=(9, 9) is not present in table "fp_pk_same".
|
||||
DROP TABLE fp_fk_same, fp_pk_same;
|
||||
-- Deferred constraint: batch flushed at COMMIT, not at statement end
|
||||
-- Deferred constraint: deferred check fired at COMMIT, not at statement end
|
||||
CREATE TABLE fp_pk_commit (a int PRIMARY KEY);
|
||||
CREATE TABLE fp_fk_commit (a int REFERENCES fp_pk_commit
|
||||
DEFERRABLE INITIALLY DEFERRED);
|
||||
@@ -3828,15 +3826,7 @@ DETAIL: Key (a)=(999) is not present in table "fp_pk_dom".
|
||||
INSERT INTO fp_fk_dom VALUES (NULL);
|
||||
DROP TABLE fp_fk_dom, fp_pk_dom;
|
||||
DROP DOMAIN fp_int8dom;
|
||||
-- Duplicate FK values: when using the batched SAOP path, every
|
||||
-- row must be recognized as satisfied, not just the first match
|
||||
CREATE TABLE fp_pk_dup (a int PRIMARY KEY);
|
||||
INSERT INTO fp_pk_dup VALUES (1);
|
||||
CREATE TABLE fp_fk_dup (a int REFERENCES fp_pk_dup);
|
||||
INSERT INTO fp_fk_dup SELECT 1 FROM generate_series(1, 100);
|
||||
DROP TABLE fp_fk_dup, fp_pk_dup;
|
||||
-- Re-entrant FK fast-path: DML on the same FK table from a cast function
|
||||
-- during a full-batch flush must not corrupt the batch array.
|
||||
CREATE TABLE fp_reentry_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fp_reentry_pk VALUES (1), (2);
|
||||
CREATE TYPE fp_vch AS (v int);
|
||||
@@ -3850,78 +3840,26 @@ END$$;
|
||||
CREATE CAST (fp_vch AS int) WITH FUNCTION fp_vcast(fp_vch) AS IMPLICIT;
|
||||
CREATE TABLE fp_reentry_fk (a fp_vch
|
||||
REFERENCES fp_reentry_pk (id));
|
||||
-- Fill exactly one batch so the flush fires; the cast re-enters with DML
|
||||
-- on the same FK and must take the per-row path.
|
||||
INSERT INTO fp_reentry_fk SELECT row(1)::fp_vch FROM generate_series(1, 64);
|
||||
-- cast re-enters with DML on the same FK
|
||||
INSERT INTO fp_reentry_fk SELECT row(1)::fp_vch FROM generate_series(1, 2);
|
||||
SELECT a, count(*) FROM fp_reentry_fk GROUP BY a ORDER BY a;
|
||||
a | count
|
||||
-----+-------
|
||||
(1) | 64
|
||||
(2) | 64
|
||||
(1) | 2
|
||||
(2) | 2
|
||||
(2 rows)
|
||||
|
||||
DROP TABLE fp_reentry_fk, fp_reentry_pk;
|
||||
DROP CAST (fp_vch AS int);
|
||||
DROP FUNCTION fp_vcast(fp_vch);
|
||||
DROP TYPE fp_vch;
|
||||
-- Flush error caught by a savepoint must leave the entry empty and reusable.
|
||||
CREATE TABLE fp_reentry_pk2 (id int PRIMARY KEY);
|
||||
INSERT INTO fp_reentry_pk2 VALUES (1);
|
||||
CREATE TABLE fp_reentry_fk2 (a int REFERENCES fp_reentry_pk2 (id));
|
||||
DO $$
|
||||
BEGIN
|
||||
-- A batch containing a violating row; the flush reports the violation.
|
||||
BEGIN
|
||||
INSERT INTO fp_reentry_fk2 SELECT CASE WHEN g = 32 THEN 999 ELSE 1 END
|
||||
FROM generate_series(1, 64) g;
|
||||
EXCEPTION WHEN foreign_key_violation THEN
|
||||
RAISE NOTICE 'caught fk violation';
|
||||
END;
|
||||
|
||||
-- Reuse the same FK with a full batch in the same transaction. The
|
||||
-- entry must be empty after the caught violation: no stale rows from the
|
||||
-- rolled-back batch (in particular no 999), and no array overflow.
|
||||
INSERT INTO fp_reentry_fk2 SELECT 1 FROM generate_series(1, 64);
|
||||
END$$;
|
||||
NOTICE: caught fk violation
|
||||
SELECT count(*), max(a) FROM fp_reentry_fk2; -- 64 rows, max 1
|
||||
count | max
|
||||
-------+-----
|
||||
64 | 1
|
||||
(1 row)
|
||||
|
||||
DROP TABLE fp_reentry_fk2, fp_reentry_pk2;
|
||||
-- Subtransaction abort during after-trigger firing must not drop FK checks
|
||||
-- for rows buffered earlier in the same statement. Batching is confined to
|
||||
-- the top transaction level and the buffered batch is no longer discarded on
|
||||
-- subxact abort, so the violating rows are detected.
|
||||
CREATE TABLE fp_subxact_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fp_subxact_pk SELECT g FROM generate_series(1, 10) g;
|
||||
CREATE TABLE fp_subxact_fk (a int, tag text);
|
||||
ALTER TABLE fp_subxact_fk ADD CONSTRAINT fp_subxact_fk_fkey
|
||||
FOREIGN KEY (a) REFERENCES fp_subxact_pk (id);
|
||||
CREATE FUNCTION fp_abort_subxact() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NEW.tag = 'boom' THEN
|
||||
BEGIN PERFORM 1/0; EXCEPTION WHEN division_by_zero THEN NULL; END;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END$$;
|
||||
CREATE TRIGGER fp_subxact_trg AFTER INSERT ON fp_subxact_fk
|
||||
FOR EACH ROW EXECUTE FUNCTION fp_abort_subxact();
|
||||
INSERT INTO fp_subxact_fk VALUES (999, 'bad'), (0, 'boom'), (1, 'ok');
|
||||
ERROR: insert or update on table "fp_subxact_fk" violates foreign key constraint "fp_subxact_fk_fkey"
|
||||
DETAIL: Key (a)=(999) is not present in table "fp_subxact_pk".
|
||||
DROP TRIGGER fp_subxact_trg ON fp_subxact_fk;
|
||||
DROP FUNCTION fp_abort_subxact();
|
||||
DROP TABLE fp_subxact_fk, fp_subxact_pk;
|
||||
--
|
||||
-- Cache invalidation arriving in the middle of a fast-path batch flush.
|
||||
-- Cache invalidation arriving during a fast-path check.
|
||||
--
|
||||
-- A cross-type foreign key runs the user's cast function once per key per
|
||||
-- buffered row, inside ri_FastPathFlushLoop(). A cast that performs DDL
|
||||
-- raises an invalidation there, which detaches the constraint's fast-path
|
||||
-- metadata while the flush is still using it.
|
||||
-- A cross-type foreign key runs the user's cast function while building its
|
||||
-- index scan keys. A cast that performs DDL raises an invalidation there,
|
||||
-- detaching the constraint's fast-path metadata while the check is still
|
||||
-- using it.
|
||||
--
|
||||
CREATE TYPE fkint;
|
||||
CREATE FUNCTION fkint_in(cstring) RETURNS fkint
|
||||
@@ -3934,7 +3872,7 @@ LINE 1: CREATE FUNCTION fkint_out(fkint) RETURNS cstring
|
||||
^
|
||||
CREATE TYPE fkint (INPUT = fkint_in, OUTPUT = fkint_out, LIKE = int4);
|
||||
-- Renames the constraint the first time it is called, and so raises an
|
||||
-- invalidation partway through the flush. Guarded on the catalog so the
|
||||
-- invalidation partway through the check. Guarded on the catalog so the
|
||||
-- second and later calls are no-ops.
|
||||
CREATE FUNCTION fkint_to_int4(fkint) RETURNS int4 AS $$
|
||||
BEGIN
|
||||
@@ -3949,12 +3887,12 @@ END $$ LANGUAGE plpgsql;
|
||||
CREATE CAST (fkint AS int4) WITH FUNCTION fkint_to_int4(fkint) AS IMPLICIT;
|
||||
CREATE TABLE pktable_inval (a int4, b int4, PRIMARY KEY (a, b));
|
||||
INSERT INTO pktable_inval VALUES (1, 1), (2, 2);
|
||||
-- Multi-column FK, so the flush takes the per-row loop rather than the
|
||||
-- array path; cross-type on column a, so the cast above is invoked.
|
||||
-- Cross-type on column a, so the cast above is invoked.
|
||||
CREATE TABLE fktable_inval (a fkint, b int4,
|
||||
CONSTRAINT fktable_inval_fk FOREIGN KEY (a, b)
|
||||
REFERENCES pktable_inval (a, b));
|
||||
-- More than one row, so the flush is still running after the invalidation.
|
||||
-- The first row invalidates the metadata; the second check must repopulate
|
||||
-- and use it safely.
|
||||
INSERT INTO fktable_inval VALUES ('1', 1), ('2', 2);
|
||||
-- Confirms the cast actually ran and raised the invalidation. Without this
|
||||
-- the insert above could pass merely by not exercising the path at all.
|
||||
@@ -3979,206 +3917,3 @@ DROP TYPE fkint CASCADE;
|
||||
NOTICE: drop cascades to 2 other objects
|
||||
DETAIL: drop cascades to function fkint_in(cstring)
|
||||
drop cascades to function fkint_out(fkint)
|
||||
-- Stranded firing state must not misroute ALTER TABLE ... ADD FOREIGN KEY
|
||||
-- validation into the batched fast path. A caught FK-check error inside a
|
||||
-- subtransaction leaves firing_depth set (its decrement is skipped); a
|
||||
-- following ALTER whose validation runs per-row (forced here by RLS on the
|
||||
-- referenced table, so RI_Initial_Check() bails) would then be wrongly treated
|
||||
-- as running inside trigger firing, batched, and never flushed (a utility
|
||||
-- command has no AfterTriggerEndQuery), silently validating a violating row.
|
||||
CREATE ROLE regress_fpav_role;
|
||||
CREATE TABLE fpav_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fpav_pk VALUES (1);
|
||||
ALTER TABLE fpav_pk ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY fpav_pk_all ON fpav_pk FOR ALL USING (true) WITH CHECK (true);
|
||||
GRANT REFERENCES, SELECT ON fpav_pk TO regress_fpav_role;
|
||||
CREATE TABLE fpav_fk (a int);
|
||||
INSERT INTO fpav_fk VALUES (1), (99);
|
||||
ALTER TABLE fpav_fk OWNER TO regress_fpav_role;
|
||||
CREATE TABLE fpav_cv_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fpav_cv_pk VALUES (1);
|
||||
CREATE TABLE fpav_cv_fk (a int REFERENCES fpav_cv_pk(id));
|
||||
GRANT INSERT ON fpav_cv_fk TO regress_fpav_role;
|
||||
GRANT SELECT, INSERT ON fpav_cv_pk TO regress_fpav_role;
|
||||
SET ROLE regress_fpav_role;
|
||||
BEGIN;
|
||||
-- Caught FK violation: leaves firing_depth set if it is not restored.
|
||||
DO $$
|
||||
BEGIN
|
||||
BEGIN
|
||||
INSERT INTO fpav_cv_fk VALUES (999);
|
||||
EXCEPTION WHEN foreign_key_violation THEN
|
||||
NULL;
|
||||
END;
|
||||
END$$;
|
||||
-- Must ERROR on the violating row (99), not silently validate it.
|
||||
ALTER TABLE fpav_fk ADD CONSTRAINT fpav_fk_fkey
|
||||
FOREIGN KEY (a) REFERENCES fpav_pk (id);
|
||||
ERROR: insert or update on table "fpav_fk" violates foreign key constraint "fpav_fk_fkey"
|
||||
DETAIL: Key (a)=(99) is not present in table "fpav_pk".
|
||||
ROLLBACK;
|
||||
RESET ROLE;
|
||||
DROP TABLE fpav_fk, fpav_pk, fpav_cv_fk, fpav_cv_pk;
|
||||
DROP ROLE regress_fpav_role;
|
||||
-- Re-entrant fast-path check inside a committing subtransaction. An AFTER
|
||||
-- trigger on one FK table runs FK DML on a second FK table inside a PL/pgSQL
|
||||
-- BEGIN ... EXCEPTION block, so the inner check batches in its own
|
||||
-- trigger-firing cycle nested in the outer check's. The inner cycle must
|
||||
-- register its own end-of-batch callback and flush -- otherwise its FK check
|
||||
-- is skipped (an orphan commits) and its relations leak.
|
||||
CREATE TABLE fp_inner_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fp_inner_pk VALUES (1);
|
||||
CREATE TABLE fp_inner_fk (a int REFERENCES fp_inner_pk (id));
|
||||
CREATE TABLE fp_outer_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fp_outer_pk SELECT g FROM generate_series(1, 64) g;
|
||||
CREATE FUNCTION fp_reentry_subxact() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NEW.a = 32 THEN
|
||||
BEGIN
|
||||
INSERT INTO fp_inner_fk VALUES (999); -- violates; must be caught
|
||||
EXCEPTION WHEN foreign_key_violation THEN
|
||||
NULL;
|
||||
END;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END$$;
|
||||
CREATE TABLE fp_outer_fk (a int REFERENCES fp_outer_pk (id));
|
||||
CREATE TRIGGER fp_reentry_subxact_trg AFTER INSERT ON fp_outer_fk
|
||||
FOR EACH ROW EXECUTE FUNCTION fp_reentry_subxact();
|
||||
INSERT INTO fp_outer_fk SELECT g FROM generate_series(1, 64) g;
|
||||
SELECT count(*) AS outer_rows FROM fp_outer_fk; -- 64, outer batch intact
|
||||
outer_rows
|
||||
------------
|
||||
64
|
||||
(1 row)
|
||||
|
||||
SELECT count(*) AS inner_rows FROM fp_inner_fk; -- 0, inner check caught
|
||||
inner_rows
|
||||
------------
|
||||
0
|
||||
(1 row)
|
||||
|
||||
DROP TRIGGER fp_reentry_subxact_trg ON fp_outer_fk;
|
||||
DROP FUNCTION fp_reentry_subxact();
|
||||
DROP TABLE fp_outer_fk, fp_outer_pk, fp_inner_fk, fp_inner_pk;
|
||||
-- A nested trigger-firing cycle that checks the same constraint must use a
|
||||
-- separate cache entry. The inner violation is caught by its subtransaction,
|
||||
-- while the valid outer row remains buffered and is checked normally.
|
||||
CREATE TABLE fp_same_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fp_same_pk VALUES (1);
|
||||
CREATE TABLE fp_same_fk (a int REFERENCES fp_same_pk (id));
|
||||
CREATE FUNCTION fp_reentry_same_constraint() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NEW.a = 1 THEN
|
||||
BEGIN
|
||||
INSERT INTO fp_same_fk VALUES (999);
|
||||
EXCEPTION WHEN foreign_key_violation THEN
|
||||
NULL;
|
||||
END;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END$$;
|
||||
CREATE TRIGGER fp_reentry_same_constraint_trg AFTER INSERT ON fp_same_fk
|
||||
FOR EACH ROW EXECUTE FUNCTION fp_reentry_same_constraint();
|
||||
INSERT INTO fp_same_fk VALUES (1);
|
||||
SELECT * FROM fp_same_fk;
|
||||
a
|
||||
---
|
||||
1
|
||||
(1 row)
|
||||
|
||||
DROP TRIGGER fp_reentry_same_constraint_trg ON fp_same_fk;
|
||||
DROP FUNCTION fp_reentry_same_constraint();
|
||||
DROP TABLE fp_same_fk, fp_same_pk;
|
||||
-- An AFTER trigger runs a query of its own, and that query inserts into a
|
||||
-- second table with a fast-path foreign key. The entry the nested INSERT
|
||||
-- creates belongs to the cursor's portal, which is gone by the time the
|
||||
-- entry is torn down at the end of the outer statement. Every key stored
|
||||
-- below is present in its referenced table, so the INSERT must just succeed.
|
||||
CREATE TABLE fp_customer (id int PRIMARY KEY);
|
||||
INSERT INTO fp_customer VALUES (1);
|
||||
CREATE TABLE fp_product (id int PRIMARY KEY);
|
||||
INSERT INTO fp_product SELECT generate_series(1, 4);
|
||||
CREATE TABLE fp_kit_component (kit_product_id int, component_product_id int);
|
||||
INSERT INTO fp_kit_component VALUES (1, 2), (1, 3), (1, 4);
|
||||
CREATE TABLE fp_order (id int, customer_id int REFERENCES fp_customer,
|
||||
product_id int);
|
||||
CREATE TABLE fp_order_item (order_id int, product_id int
|
||||
REFERENCES fp_product);
|
||||
CREATE FUNCTION fp_add_order_item(order_id int, product_id int) RETURNS int
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
INSERT INTO fp_order_item VALUES (order_id, product_id);
|
||||
RETURN product_id;
|
||||
END$$;
|
||||
CREATE FUNCTION fp_expand_kit() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
component_id int;
|
||||
ncomponents int := 0;
|
||||
BEGIN
|
||||
FOR component_id IN
|
||||
SELECT fp_add_order_item(NEW.id, component_product_id)
|
||||
FROM fp_kit_component WHERE kit_product_id = NEW.product_id
|
||||
LOOP
|
||||
ncomponents := ncomponents + 1;
|
||||
END LOOP;
|
||||
RAISE NOTICE 'order % expanded into % order items', NEW.id, ncomponents;
|
||||
RETURN NULL;
|
||||
END$$;
|
||||
CREATE TRIGGER fp_expand_kit_trg AFTER INSERT ON fp_order
|
||||
FOR EACH ROW EXECUTE FUNCTION fp_expand_kit();
|
||||
INSERT INTO fp_order VALUES (1, 1, 1);
|
||||
NOTICE: order 1 expanded into 3 order items
|
||||
SELECT count(*) FROM fp_order_item;
|
||||
count
|
||||
-------
|
||||
3
|
||||
(1 row)
|
||||
|
||||
DROP TABLE fp_order, fp_order_item, fp_kit_component, fp_product, fp_customer;
|
||||
DROP FUNCTION fp_expand_kit(), fp_add_order_item(int, int);
|
||||
-- Nested firing of the same constraint must use an entry for its own query
|
||||
-- depth. The RAISE is reached if the nested violation remains buffered for
|
||||
-- the outer cycle's callback.
|
||||
CREATE TABLE fp_depth_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fp_depth_pk VALUES (1);
|
||||
CREATE TABLE fp_depth_fk (a int REFERENCES fp_depth_pk);
|
||||
CREATE FUNCTION fp_depth_reentry() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NEW.a = 1 THEN
|
||||
INSERT INTO fp_depth_fk VALUES (999);
|
||||
RAISE EXCEPTION 'nested FK check was not flushed';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END$$;
|
||||
-- Sort after the RI trigger, so the outer row has already been batched.
|
||||
CREATE TRIGGER zz_fp_depth_reentry AFTER INSERT ON fp_depth_fk
|
||||
FOR EACH ROW EXECUTE FUNCTION fp_depth_reentry();
|
||||
INSERT INTO fp_depth_fk VALUES (1);
|
||||
ERROR: insert or update on table "fp_depth_fk" violates foreign key constraint "fp_depth_fk_a_fkey"
|
||||
DETAIL: Key (a)=(999) is not present in table "fp_depth_pk".
|
||||
CONTEXT: SQL statement "INSERT INTO fp_depth_fk VALUES (999)"
|
||||
PL/pgSQL function fp_depth_reentry() line 4 at SQL statement
|
||||
SELECT * FROM fp_depth_fk;
|
||||
a
|
||||
---
|
||||
(0 rows)
|
||||
|
||||
DROP TABLE fp_depth_fk, fp_depth_pk;
|
||||
DROP FUNCTION fp_depth_reentry();
|
||||
-- Deferred FK check fires at commit (query depth -1); its batch must still get
|
||||
-- a callback registered and flushed.
|
||||
CREATE TABLE fp_deferred_pk (id int PRIMARY KEY);
|
||||
CREATE TABLE fp_deferred_fk (a int REFERENCES fp_deferred_pk (id)
|
||||
DEFERRABLE INITIALLY DEFERRED);
|
||||
BEGIN;
|
||||
INSERT INTO fp_deferred_fk VALUES (1);
|
||||
INSERT INTO fp_deferred_pk VALUES (1);
|
||||
COMMIT;
|
||||
SELECT count(*) AS deferred_rows FROM fp_deferred_fk; -- 1, check passed at commit
|
||||
deferred_rows
|
||||
---------------
|
||||
1
|
||||
(1 row)
|
||||
|
||||
DROP TABLE fp_deferred_fk, fp_deferred_pk;
|
||||
|
||||
@@ -3644,27 +3644,3 @@ drop table defer_trig;
|
||||
drop function whoami();
|
||||
drop role regress_fn_owner;
|
||||
drop role regress_caller;
|
||||
--
|
||||
-- Test a recursive AFTER ROW trigger that nests after-trigger query levels
|
||||
-- deeply enough to grow query_stack mid-fire. Outer levels then resume their
|
||||
-- post-loop cleanup against the relocated stack.
|
||||
--
|
||||
create table trigger_recursive (id int);
|
||||
create function trigger_recursive_fn() returns trigger language plpgsql as $$
|
||||
begin
|
||||
if new.id < 10 then
|
||||
insert into trigger_recursive values (new.id + 1);
|
||||
end if;
|
||||
return new;
|
||||
end$$;
|
||||
create trigger trigger_recursive after insert on trigger_recursive
|
||||
for each row execute function trigger_recursive_fn();
|
||||
insert into trigger_recursive values (1);
|
||||
select count(*) from trigger_recursive;
|
||||
count
|
||||
-------
|
||||
10
|
||||
(1 row)
|
||||
|
||||
drop table trigger_recursive;
|
||||
drop function trigger_recursive_fn();
|
||||
|
||||
@@ -2536,25 +2536,43 @@ WITH cte AS (
|
||||
DROP SCHEMA fkpart13 CASCADE;
|
||||
RESET search_path;
|
||||
|
||||
-- Tests foreign key check fast-path no-cache path.
|
||||
-- Test the fast-path ALTER TABLE validation path. RLS on the referenced
|
||||
-- table makes RI_Initial_Check() decline the set-based check, so validation
|
||||
-- invokes the RI trigger once per referencing row.
|
||||
CREATE ROLE regress_fk_fastpath_role;
|
||||
CREATE TABLE fp_pk_alter (a int PRIMARY KEY);
|
||||
INSERT INTO fp_pk_alter SELECT generate_series(1, 100);
|
||||
ALTER TABLE fp_pk_alter ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY fp_pk_alter_all ON fp_pk_alter
|
||||
FOR ALL USING (true) WITH CHECK (true);
|
||||
GRANT REFERENCES, SELECT ON fp_pk_alter TO regress_fk_fastpath_role;
|
||||
CREATE TABLE fp_fk_alter (a int);
|
||||
INSERT INTO fp_fk_alter SELECT generate_series(1, 100);
|
||||
ALTER TABLE fp_fk_alter OWNER TO regress_fk_fastpath_role;
|
||||
-- Validation path: should succeed
|
||||
SET ROLE regress_fk_fastpath_role;
|
||||
ALTER TABLE fp_fk_alter ADD FOREIGN KEY (a) REFERENCES fp_pk_alter;
|
||||
INSERT INTO fp_fk_alter VALUES (101); -- should fail (constraint active)
|
||||
RESET ROLE;
|
||||
DROP TABLE fp_fk_alter, fp_pk_alter;
|
||||
|
||||
-- Separate test: validation catches existing violation
|
||||
CREATE TABLE fp_pk_alter2 (a int PRIMARY KEY);
|
||||
INSERT INTO fp_pk_alter2 VALUES (1);
|
||||
ALTER TABLE fp_pk_alter2 ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY fp_pk_alter2_all ON fp_pk_alter2
|
||||
FOR ALL USING (true) WITH CHECK (true);
|
||||
GRANT REFERENCES, SELECT ON fp_pk_alter2 TO regress_fk_fastpath_role;
|
||||
CREATE TABLE fp_fk_alter2 (a int);
|
||||
INSERT INTO fp_fk_alter2 VALUES (1), (200); -- 200 has no PK match
|
||||
ALTER TABLE fp_fk_alter2 OWNER TO regress_fk_fastpath_role;
|
||||
SET ROLE regress_fk_fastpath_role;
|
||||
ALTER TABLE fp_fk_alter2 ADD FOREIGN KEY (a) REFERENCES fp_pk_alter2; -- should fail
|
||||
RESET ROLE;
|
||||
DROP TABLE fp_fk_alter2, fp_pk_alter2;
|
||||
DROP ROLE regress_fk_fastpath_role;
|
||||
|
||||
-- Tests that the fast-path handles caching for multiple constraints
|
||||
-- Tests that the fast-path handles multiple constraints
|
||||
CREATE TABLE fp_pk1 (a int PRIMARY KEY);
|
||||
CREATE TABLE fp_pk2 (b int PRIMARY KEY);
|
||||
INSERT INTO fp_pk1 VALUES (1);
|
||||
@@ -2563,11 +2581,11 @@ CREATE TABLE fp_multi_fk (
|
||||
a int REFERENCES fp_pk1,
|
||||
b int REFERENCES fp_pk2
|
||||
);
|
||||
INSERT INTO fp_multi_fk VALUES (1, 1); -- two constraints, one batch
|
||||
INSERT INTO fp_multi_fk VALUES (1, 1); -- two constraints
|
||||
INSERT INTO fp_multi_fk VALUES (1, 2); -- second constraint fails
|
||||
DROP TABLE fp_multi_fk, fp_pk1, fp_pk2;
|
||||
|
||||
-- Test that fast-path cache handles deferred constraints and SET CONSTRAINTS IMMEDIATE
|
||||
-- Test that fast-path handles deferred constraints and SET CONSTRAINTS IMMEDIATE
|
||||
CREATE TABLE fp_pk_defer (a int PRIMARY KEY);
|
||||
CREATE TABLE fp_fk_defer (a int REFERENCES fp_pk_defer DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO fp_pk_defer VALUES (1), (2);
|
||||
@@ -2575,13 +2593,13 @@ INSERT INTO fp_pk_defer VALUES (1), (2);
|
||||
BEGIN;
|
||||
INSERT INTO fp_fk_defer VALUES (1);
|
||||
INSERT INTO fp_fk_defer VALUES (2);
|
||||
SET CONSTRAINTS ALL IMMEDIATE; -- fires batch callback here
|
||||
INSERT INTO fp_fk_defer VALUES (3); -- should fail, also tests that cache was cleaned up
|
||||
SET CONSTRAINTS ALL IMMEDIATE; -- fires deferred checks here
|
||||
INSERT INTO fp_fk_defer VALUES (3); -- should fail
|
||||
COMMIT;
|
||||
DROP TABLE fp_pk_defer, fp_fk_defer;
|
||||
|
||||
-- A deferred FK check queued while firing deferred triggers at commit must
|
||||
-- not be lost. The RI fast-path flush runs during the deferred firing loop
|
||||
-- not be lost. The RI fast-path runs during the deferred firing loop
|
||||
-- and can run user cast/equality code whose DML queues a further deferred
|
||||
-- check; that check must still fire. fk_defer_main's deferred fast-path check
|
||||
-- runs a cast whose function inserts a violating row into fk_defer_t2,
|
||||
@@ -2602,8 +2620,7 @@ CREATE TABLE fk_defer_main_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fk_defer_main_pk VALUES (1);
|
||||
CREATE TABLE fk_defer_main (a fk_defer_vch REFERENCES fk_defer_main_pk(id)
|
||||
DEFERRABLE INITIALLY DEFERRED);
|
||||
-- At COMMIT the queued fk_defer_t2 check must fire and report the violation,
|
||||
-- rather than being dropped (which would let the dangling row commit).
|
||||
-- At COMMIT the queued fk_defer_t2 check must fire and report the violation
|
||||
BEGIN;
|
||||
INSERT INTO fk_defer_main VALUES (row(1)::fk_defer_vch);
|
||||
COMMIT; -- expected: ERROR on fk_defer_t2_a_fkey
|
||||
@@ -2618,20 +2635,6 @@ DROP CAST (fk_defer_vch AS int);
|
||||
DROP FUNCTION fk_defer_cast(fk_defer_vch);
|
||||
DROP TYPE fk_defer_vch;
|
||||
|
||||
-- Subtransaction abort: cached state must be invalidated on ROLLBACK TO
|
||||
CREATE TABLE fp_pk_subxact (a int PRIMARY KEY);
|
||||
CREATE TABLE fp_fk_subxact (a int REFERENCES fp_pk_subxact);
|
||||
INSERT INTO fp_pk_subxact VALUES (1), (2);
|
||||
BEGIN;
|
||||
INSERT INTO fp_fk_subxact VALUES (1);
|
||||
SAVEPOINT sp1;
|
||||
INSERT INTO fp_fk_subxact VALUES (2);
|
||||
ROLLBACK TO sp1;
|
||||
INSERT INTO fp_fk_subxact VALUES (1);
|
||||
COMMIT;
|
||||
SELECT * FROM fp_fk_subxact;
|
||||
DROP TABLE fp_fk_subxact, fp_pk_subxact;
|
||||
|
||||
-- FK check must see PK rows inserted by earlier AFTER triggers
|
||||
-- firing on the same statement
|
||||
CREATE TABLE fp_pk_cci (a int PRIMARY KEY);
|
||||
@@ -2719,7 +2722,7 @@ INSERT INTO fk_spi VALUES (ROW(1)::lookup_key);
|
||||
SELECT count(*) FROM fk_spi;
|
||||
ROLLBACK;
|
||||
|
||||
-- Multi-column FK: exercises batched per-row probing with composite keys
|
||||
-- Multi-column FK: exercises direct per-row probing with composite keys
|
||||
CREATE TABLE fp_pk_multi (a int, b int, PRIMARY KEY (a, b));
|
||||
INSERT INTO fp_pk_multi SELECT i, i FROM generate_series(1, 100) i;
|
||||
CREATE TABLE fp_fk_multi (x int, a int, b int,
|
||||
@@ -2755,7 +2758,7 @@ INSERT INTO fp_fk_same VALUES (1, 2); -- should succeed
|
||||
INSERT INTO fp_fk_same VALUES (9, 9); -- should fail
|
||||
DROP TABLE fp_fk_same, fp_pk_same;
|
||||
|
||||
-- Deferred constraint: batch flushed at COMMIT, not at statement end
|
||||
-- Deferred constraint: deferred check fired at COMMIT, not at statement end
|
||||
CREATE TABLE fp_pk_commit (a int PRIMARY KEY);
|
||||
CREATE TABLE fp_fk_commit (a int REFERENCES fp_pk_commit
|
||||
DEFERRABLE INITIALLY DEFERRED);
|
||||
@@ -2790,16 +2793,7 @@ INSERT INTO fp_fk_dom VALUES (NULL);
|
||||
DROP TABLE fp_fk_dom, fp_pk_dom;
|
||||
DROP DOMAIN fp_int8dom;
|
||||
|
||||
-- Duplicate FK values: when using the batched SAOP path, every
|
||||
-- row must be recognized as satisfied, not just the first match
|
||||
CREATE TABLE fp_pk_dup (a int PRIMARY KEY);
|
||||
INSERT INTO fp_pk_dup VALUES (1);
|
||||
CREATE TABLE fp_fk_dup (a int REFERENCES fp_pk_dup);
|
||||
INSERT INTO fp_fk_dup SELECT 1 FROM generate_series(1, 100);
|
||||
DROP TABLE fp_fk_dup, fp_pk_dup;
|
||||
|
||||
-- Re-entrant FK fast-path: DML on the same FK table from a cast function
|
||||
-- during a full-batch flush must not corrupt the batch array.
|
||||
CREATE TABLE fp_reentry_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fp_reentry_pk VALUES (1), (2);
|
||||
CREATE TYPE fp_vch AS (v int);
|
||||
@@ -2813,67 +2807,21 @@ END$$;
|
||||
CREATE CAST (fp_vch AS int) WITH FUNCTION fp_vcast(fp_vch) AS IMPLICIT;
|
||||
CREATE TABLE fp_reentry_fk (a fp_vch
|
||||
REFERENCES fp_reentry_pk (id));
|
||||
-- Fill exactly one batch so the flush fires; the cast re-enters with DML
|
||||
-- on the same FK and must take the per-row path.
|
||||
INSERT INTO fp_reentry_fk SELECT row(1)::fp_vch FROM generate_series(1, 64);
|
||||
-- cast re-enters with DML on the same FK
|
||||
INSERT INTO fp_reentry_fk SELECT row(1)::fp_vch FROM generate_series(1, 2);
|
||||
SELECT a, count(*) FROM fp_reentry_fk GROUP BY a ORDER BY a;
|
||||
DROP TABLE fp_reentry_fk, fp_reentry_pk;
|
||||
DROP CAST (fp_vch AS int);
|
||||
DROP FUNCTION fp_vcast(fp_vch);
|
||||
DROP TYPE fp_vch;
|
||||
|
||||
-- Flush error caught by a savepoint must leave the entry empty and reusable.
|
||||
CREATE TABLE fp_reentry_pk2 (id int PRIMARY KEY);
|
||||
INSERT INTO fp_reentry_pk2 VALUES (1);
|
||||
CREATE TABLE fp_reentry_fk2 (a int REFERENCES fp_reentry_pk2 (id));
|
||||
DO $$
|
||||
BEGIN
|
||||
-- A batch containing a violating row; the flush reports the violation.
|
||||
BEGIN
|
||||
INSERT INTO fp_reentry_fk2 SELECT CASE WHEN g = 32 THEN 999 ELSE 1 END
|
||||
FROM generate_series(1, 64) g;
|
||||
EXCEPTION WHEN foreign_key_violation THEN
|
||||
RAISE NOTICE 'caught fk violation';
|
||||
END;
|
||||
|
||||
-- Reuse the same FK with a full batch in the same transaction. The
|
||||
-- entry must be empty after the caught violation: no stale rows from the
|
||||
-- rolled-back batch (in particular no 999), and no array overflow.
|
||||
INSERT INTO fp_reentry_fk2 SELECT 1 FROM generate_series(1, 64);
|
||||
END$$;
|
||||
SELECT count(*), max(a) FROM fp_reentry_fk2; -- 64 rows, max 1
|
||||
DROP TABLE fp_reentry_fk2, fp_reentry_pk2;
|
||||
|
||||
-- Subtransaction abort during after-trigger firing must not drop FK checks
|
||||
-- for rows buffered earlier in the same statement. Batching is confined to
|
||||
-- the top transaction level and the buffered batch is no longer discarded on
|
||||
-- subxact abort, so the violating rows are detected.
|
||||
CREATE TABLE fp_subxact_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fp_subxact_pk SELECT g FROM generate_series(1, 10) g;
|
||||
CREATE TABLE fp_subxact_fk (a int, tag text);
|
||||
ALTER TABLE fp_subxact_fk ADD CONSTRAINT fp_subxact_fk_fkey
|
||||
FOREIGN KEY (a) REFERENCES fp_subxact_pk (id);
|
||||
CREATE FUNCTION fp_abort_subxact() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NEW.tag = 'boom' THEN
|
||||
BEGIN PERFORM 1/0; EXCEPTION WHEN division_by_zero THEN NULL; END;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END$$;
|
||||
CREATE TRIGGER fp_subxact_trg AFTER INSERT ON fp_subxact_fk
|
||||
FOR EACH ROW EXECUTE FUNCTION fp_abort_subxact();
|
||||
INSERT INTO fp_subxact_fk VALUES (999, 'bad'), (0, 'boom'), (1, 'ok');
|
||||
DROP TRIGGER fp_subxact_trg ON fp_subxact_fk;
|
||||
DROP FUNCTION fp_abort_subxact();
|
||||
DROP TABLE fp_subxact_fk, fp_subxact_pk;
|
||||
|
||||
--
|
||||
-- Cache invalidation arriving in the middle of a fast-path batch flush.
|
||||
-- Cache invalidation arriving during a fast-path check.
|
||||
--
|
||||
-- A cross-type foreign key runs the user's cast function once per key per
|
||||
-- buffered row, inside ri_FastPathFlushLoop(). A cast that performs DDL
|
||||
-- raises an invalidation there, which detaches the constraint's fast-path
|
||||
-- metadata while the flush is still using it.
|
||||
-- A cross-type foreign key runs the user's cast function while building its
|
||||
-- index scan keys. A cast that performs DDL raises an invalidation there,
|
||||
-- detaching the constraint's fast-path metadata while the check is still
|
||||
-- using it.
|
||||
--
|
||||
CREATE TYPE fkint;
|
||||
CREATE FUNCTION fkint_in(cstring) RETURNS fkint
|
||||
@@ -2883,7 +2831,7 @@ CREATE FUNCTION fkint_out(fkint) RETURNS cstring
|
||||
CREATE TYPE fkint (INPUT = fkint_in, OUTPUT = fkint_out, LIKE = int4);
|
||||
|
||||
-- Renames the constraint the first time it is called, and so raises an
|
||||
-- invalidation partway through the flush. Guarded on the catalog so the
|
||||
-- invalidation partway through the check. Guarded on the catalog so the
|
||||
-- second and later calls are no-ops.
|
||||
CREATE FUNCTION fkint_to_int4(fkint) RETURNS int4 AS $$
|
||||
BEGIN
|
||||
@@ -2901,13 +2849,13 @@ CREATE CAST (fkint AS int4) WITH FUNCTION fkint_to_int4(fkint) AS IMPLICIT;
|
||||
CREATE TABLE pktable_inval (a int4, b int4, PRIMARY KEY (a, b));
|
||||
INSERT INTO pktable_inval VALUES (1, 1), (2, 2);
|
||||
|
||||
-- Multi-column FK, so the flush takes the per-row loop rather than the
|
||||
-- array path; cross-type on column a, so the cast above is invoked.
|
||||
-- Cross-type on column a, so the cast above is invoked.
|
||||
CREATE TABLE fktable_inval (a fkint, b int4,
|
||||
CONSTRAINT fktable_inval_fk FOREIGN KEY (a, b)
|
||||
REFERENCES pktable_inval (a, b));
|
||||
|
||||
-- More than one row, so the flush is still running after the invalidation.
|
||||
-- The first row invalidates the metadata; the second check must repopulate
|
||||
-- and use it safely.
|
||||
INSERT INTO fktable_inval VALUES ('1', 1), ('2', 2);
|
||||
|
||||
-- Confirms the cast actually ran and raised the invalidation. Without this
|
||||
@@ -2922,178 +2870,3 @@ DROP TABLE pktable_inval;
|
||||
DROP CAST (fkint AS int4);
|
||||
DROP FUNCTION fkint_to_int4(fkint);
|
||||
DROP TYPE fkint CASCADE;
|
||||
|
||||
-- Stranded firing state must not misroute ALTER TABLE ... ADD FOREIGN KEY
|
||||
-- validation into the batched fast path. A caught FK-check error inside a
|
||||
-- subtransaction leaves firing_depth set (its decrement is skipped); a
|
||||
-- following ALTER whose validation runs per-row (forced here by RLS on the
|
||||
-- referenced table, so RI_Initial_Check() bails) would then be wrongly treated
|
||||
-- as running inside trigger firing, batched, and never flushed (a utility
|
||||
-- command has no AfterTriggerEndQuery), silently validating a violating row.
|
||||
CREATE ROLE regress_fpav_role;
|
||||
CREATE TABLE fpav_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fpav_pk VALUES (1);
|
||||
ALTER TABLE fpav_pk ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY fpav_pk_all ON fpav_pk FOR ALL USING (true) WITH CHECK (true);
|
||||
GRANT REFERENCES, SELECT ON fpav_pk TO regress_fpav_role;
|
||||
CREATE TABLE fpav_fk (a int);
|
||||
INSERT INTO fpav_fk VALUES (1), (99);
|
||||
ALTER TABLE fpav_fk OWNER TO regress_fpav_role;
|
||||
CREATE TABLE fpav_cv_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fpav_cv_pk VALUES (1);
|
||||
CREATE TABLE fpav_cv_fk (a int REFERENCES fpav_cv_pk(id));
|
||||
GRANT INSERT ON fpav_cv_fk TO regress_fpav_role;
|
||||
GRANT SELECT, INSERT ON fpav_cv_pk TO regress_fpav_role;
|
||||
SET ROLE regress_fpav_role;
|
||||
BEGIN;
|
||||
-- Caught FK violation: leaves firing_depth set if it is not restored.
|
||||
DO $$
|
||||
BEGIN
|
||||
BEGIN
|
||||
INSERT INTO fpav_cv_fk VALUES (999);
|
||||
EXCEPTION WHEN foreign_key_violation THEN
|
||||
NULL;
|
||||
END;
|
||||
END$$;
|
||||
-- Must ERROR on the violating row (99), not silently validate it.
|
||||
ALTER TABLE fpav_fk ADD CONSTRAINT fpav_fk_fkey
|
||||
FOREIGN KEY (a) REFERENCES fpav_pk (id);
|
||||
ROLLBACK;
|
||||
RESET ROLE;
|
||||
DROP TABLE fpav_fk, fpav_pk, fpav_cv_fk, fpav_cv_pk;
|
||||
DROP ROLE regress_fpav_role;
|
||||
|
||||
-- Re-entrant fast-path check inside a committing subtransaction. An AFTER
|
||||
-- trigger on one FK table runs FK DML on a second FK table inside a PL/pgSQL
|
||||
-- BEGIN ... EXCEPTION block, so the inner check batches in its own
|
||||
-- trigger-firing cycle nested in the outer check's. The inner cycle must
|
||||
-- register its own end-of-batch callback and flush -- otherwise its FK check
|
||||
-- is skipped (an orphan commits) and its relations leak.
|
||||
CREATE TABLE fp_inner_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fp_inner_pk VALUES (1);
|
||||
CREATE TABLE fp_inner_fk (a int REFERENCES fp_inner_pk (id));
|
||||
CREATE TABLE fp_outer_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fp_outer_pk SELECT g FROM generate_series(1, 64) g;
|
||||
CREATE FUNCTION fp_reentry_subxact() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NEW.a = 32 THEN
|
||||
BEGIN
|
||||
INSERT INTO fp_inner_fk VALUES (999); -- violates; must be caught
|
||||
EXCEPTION WHEN foreign_key_violation THEN
|
||||
NULL;
|
||||
END;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END$$;
|
||||
CREATE TABLE fp_outer_fk (a int REFERENCES fp_outer_pk (id));
|
||||
CREATE TRIGGER fp_reentry_subxact_trg AFTER INSERT ON fp_outer_fk
|
||||
FOR EACH ROW EXECUTE FUNCTION fp_reentry_subxact();
|
||||
INSERT INTO fp_outer_fk SELECT g FROM generate_series(1, 64) g;
|
||||
SELECT count(*) AS outer_rows FROM fp_outer_fk; -- 64, outer batch intact
|
||||
SELECT count(*) AS inner_rows FROM fp_inner_fk; -- 0, inner check caught
|
||||
DROP TRIGGER fp_reentry_subxact_trg ON fp_outer_fk;
|
||||
DROP FUNCTION fp_reentry_subxact();
|
||||
DROP TABLE fp_outer_fk, fp_outer_pk, fp_inner_fk, fp_inner_pk;
|
||||
|
||||
-- A nested trigger-firing cycle that checks the same constraint must use a
|
||||
-- separate cache entry. The inner violation is caught by its subtransaction,
|
||||
-- while the valid outer row remains buffered and is checked normally.
|
||||
CREATE TABLE fp_same_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fp_same_pk VALUES (1);
|
||||
CREATE TABLE fp_same_fk (a int REFERENCES fp_same_pk (id));
|
||||
CREATE FUNCTION fp_reentry_same_constraint() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NEW.a = 1 THEN
|
||||
BEGIN
|
||||
INSERT INTO fp_same_fk VALUES (999);
|
||||
EXCEPTION WHEN foreign_key_violation THEN
|
||||
NULL;
|
||||
END;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END$$;
|
||||
CREATE TRIGGER fp_reentry_same_constraint_trg AFTER INSERT ON fp_same_fk
|
||||
FOR EACH ROW EXECUTE FUNCTION fp_reentry_same_constraint();
|
||||
INSERT INTO fp_same_fk VALUES (1);
|
||||
SELECT * FROM fp_same_fk;
|
||||
DROP TRIGGER fp_reentry_same_constraint_trg ON fp_same_fk;
|
||||
DROP FUNCTION fp_reentry_same_constraint();
|
||||
DROP TABLE fp_same_fk, fp_same_pk;
|
||||
|
||||
-- An AFTER trigger runs a query of its own, and that query inserts into a
|
||||
-- second table with a fast-path foreign key. The entry the nested INSERT
|
||||
-- creates belongs to the cursor's portal, which is gone by the time the
|
||||
-- entry is torn down at the end of the outer statement. Every key stored
|
||||
-- below is present in its referenced table, so the INSERT must just succeed.
|
||||
CREATE TABLE fp_customer (id int PRIMARY KEY);
|
||||
INSERT INTO fp_customer VALUES (1);
|
||||
CREATE TABLE fp_product (id int PRIMARY KEY);
|
||||
INSERT INTO fp_product SELECT generate_series(1, 4);
|
||||
CREATE TABLE fp_kit_component (kit_product_id int, component_product_id int);
|
||||
INSERT INTO fp_kit_component VALUES (1, 2), (1, 3), (1, 4);
|
||||
CREATE TABLE fp_order (id int, customer_id int REFERENCES fp_customer,
|
||||
product_id int);
|
||||
CREATE TABLE fp_order_item (order_id int, product_id int
|
||||
REFERENCES fp_product);
|
||||
CREATE FUNCTION fp_add_order_item(order_id int, product_id int) RETURNS int
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
INSERT INTO fp_order_item VALUES (order_id, product_id);
|
||||
RETURN product_id;
|
||||
END$$;
|
||||
CREATE FUNCTION fp_expand_kit() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
component_id int;
|
||||
ncomponents int := 0;
|
||||
BEGIN
|
||||
FOR component_id IN
|
||||
SELECT fp_add_order_item(NEW.id, component_product_id)
|
||||
FROM fp_kit_component WHERE kit_product_id = NEW.product_id
|
||||
LOOP
|
||||
ncomponents := ncomponents + 1;
|
||||
END LOOP;
|
||||
RAISE NOTICE 'order % expanded into % order items', NEW.id, ncomponents;
|
||||
RETURN NULL;
|
||||
END$$;
|
||||
CREATE TRIGGER fp_expand_kit_trg AFTER INSERT ON fp_order
|
||||
FOR EACH ROW EXECUTE FUNCTION fp_expand_kit();
|
||||
INSERT INTO fp_order VALUES (1, 1, 1);
|
||||
SELECT count(*) FROM fp_order_item;
|
||||
DROP TABLE fp_order, fp_order_item, fp_kit_component, fp_product, fp_customer;
|
||||
DROP FUNCTION fp_expand_kit(), fp_add_order_item(int, int);
|
||||
|
||||
-- Nested firing of the same constraint must use an entry for its own query
|
||||
-- depth. The RAISE is reached if the nested violation remains buffered for
|
||||
-- the outer cycle's callback.
|
||||
CREATE TABLE fp_depth_pk (id int PRIMARY KEY);
|
||||
INSERT INTO fp_depth_pk VALUES (1);
|
||||
CREATE TABLE fp_depth_fk (a int REFERENCES fp_depth_pk);
|
||||
CREATE FUNCTION fp_depth_reentry() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NEW.a = 1 THEN
|
||||
INSERT INTO fp_depth_fk VALUES (999);
|
||||
RAISE EXCEPTION 'nested FK check was not flushed';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END$$;
|
||||
-- Sort after the RI trigger, so the outer row has already been batched.
|
||||
CREATE TRIGGER zz_fp_depth_reentry AFTER INSERT ON fp_depth_fk
|
||||
FOR EACH ROW EXECUTE FUNCTION fp_depth_reentry();
|
||||
|
||||
INSERT INTO fp_depth_fk VALUES (1);
|
||||
SELECT * FROM fp_depth_fk;
|
||||
|
||||
DROP TABLE fp_depth_fk, fp_depth_pk;
|
||||
DROP FUNCTION fp_depth_reentry();
|
||||
|
||||
-- Deferred FK check fires at commit (query depth -1); its batch must still get
|
||||
-- a callback registered and flushed.
|
||||
CREATE TABLE fp_deferred_pk (id int PRIMARY KEY);
|
||||
CREATE TABLE fp_deferred_fk (a int REFERENCES fp_deferred_pk (id)
|
||||
DEFERRABLE INITIALLY DEFERRED);
|
||||
BEGIN;
|
||||
INSERT INTO fp_deferred_fk VALUES (1);
|
||||
INSERT INTO fp_deferred_pk VALUES (1);
|
||||
COMMIT;
|
||||
SELECT count(*) AS deferred_rows FROM fp_deferred_fk; -- 1, check passed at commit
|
||||
DROP TABLE fp_deferred_fk, fp_deferred_pk;
|
||||
|
||||
@@ -2788,26 +2788,3 @@ drop table defer_trig;
|
||||
drop function whoami();
|
||||
drop role regress_fn_owner;
|
||||
drop role regress_caller;
|
||||
|
||||
--
|
||||
-- Test a recursive AFTER ROW trigger that nests after-trigger query levels
|
||||
-- deeply enough to grow query_stack mid-fire. Outer levels then resume their
|
||||
-- post-loop cleanup against the relocated stack.
|
||||
--
|
||||
create table trigger_recursive (id int);
|
||||
create function trigger_recursive_fn() returns trigger language plpgsql as $$
|
||||
begin
|
||||
if new.id < 10 then
|
||||
insert into trigger_recursive values (new.id + 1);
|
||||
end if;
|
||||
return new;
|
||||
end$$;
|
||||
|
||||
create trigger trigger_recursive after insert on trigger_recursive
|
||||
for each row execute function trigger_recursive_fn();
|
||||
|
||||
insert into trigger_recursive values (1);
|
||||
select count(*) from trigger_recursive;
|
||||
|
||||
drop table trigger_recursive;
|
||||
drop function trigger_recursive_fn();
|
||||
|
||||
@@ -30,8 +30,6 @@ AddForeignUpdateTargets_function
|
||||
AddrInfo
|
||||
AffixNode
|
||||
AffixNodeData
|
||||
AfterTriggerBatchCallback
|
||||
AfterTriggerCallbackItem
|
||||
AfterTriggerEvent
|
||||
AfterTriggerEventChunk
|
||||
AfterTriggerEventData
|
||||
@@ -2495,8 +2493,6 @@ RIX
|
||||
RI_CompareHashEntry
|
||||
RI_CompareKey
|
||||
RI_ConstraintInfo
|
||||
RI_FastPathEntry
|
||||
RI_FastPathKey
|
||||
RI_QueryHashEntry
|
||||
RI_QueryKey
|
||||
RTEKind
|
||||
|
||||
Reference in New Issue
Block a user