Skip to content

Import batch-callback API from upstream (partial) - #22

Open
chahatagrawal117 wants to merge 5 commits into
yb-pg15from
2026_pg_batch_callback_api
Open

Import batch-callback API from upstream (partial)#22
chahatagrawal117 wants to merge 5 commits into
yb-pg15from
2026_pg_batch_callback_api

Conversation

@chahatagrawal117

@chahatagrawal117 chahatagrawal117 commented Aug 18, 2026

Copy link
Copy Markdown

Partial point-import of the upstream postgres batch-callback API chain, needed by yugabyte-db D55985 for cascade DELETE batching.

Each upstream commit is a separate git cherry-pick -x on top of yb-pg15.

Upstream commits:

Commit Title
b7b27eb41a5c Optimize fast-path FK checks with batched index probes
5c54c3ed1b93 Fix deferred FK check batching introduced by commit b7b27eb
34a307862930 Fix RI fast-path crash under nested C-level SPI
03029409b409 Fix typo left by 34a3078
d6e96bacd3c0 Move afterTriggerFiringDepth into AfterTriggersData

Imported

Batch-callback API infrastructure. Three files:

  • src/backend/commands/trigger.c
  • src/include/commands/trigger.h
  • src/tools/pgindent/typedefs.list

Skipped and why

  • src/backend/utils/adt/ri_triggers.c — upstream's own use of the API for fast-path FK check batching (RI_FastPathEntry cache, ri_FastPathBatchAdd, ri_FastPathCheck, and associated helpers). Not used by D55985.
  • src/test/regress/sql/foreign_key.sql, src/test/regress/expected/foreign_key.out — regression tests for the fast-path FK check.
  • RI_FastPathEntry typedef in src/tools/pgindent/typedefs.list.

D55985 uses this API for cascade DELETE batching only, not for FK insert-check batching. A follow-up point-import can bring in the fast-path FK check code if YB later wants that optimization. Each of the 5 commit messages documents the same skip list.

Conflict resolution

Only the first cherry-pick (b7b27eb41a5c) had conflicts. Resolved by dropping the skipped files (ri_triggers.c, foreign_key.sql/.out) and the RI_FastPathEntry typedef entry. Cherry-picks 2–5 applied cleanly.

Testing

./configure --enable-cassert --enable-debug --enable-tap-tests
make -j 8
make check

@chahatagrawal117 chahatagrawal117 changed the title Import batch-callback API from upstream (b7b27eb, 5c54c3ed, 34a30786, 03029409, d6e96bacd3c0) — partial Import batch-callback API from upstream (partial) Aug 18, 2026
@chahatagrawal117
chahatagrawal117 force-pushed the 2026_pg_batch_callback_api branch from 819b263 to 942cf74 Compare August 18, 2026 14:40
@jasonyb
jasonyb requested review from jasonyb and removed request for jason-yb August 18, 2026 20:05

@jasonyb jasonyb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only have a single commit. This is not in the format requested by docs.yugabyte.com/stable/contribute/core-database/merge-with-upstream-repositories#squash-point-imports.

amitlan and others added 5 commits August 19, 2026 19:07
Instead of probing the PK index on each trigger invocation, buffer
FK rows in a new per-constraint cache entry (RI_FastPathEntry) and
flush them as a batch.

On each trigger invocation, the new ri_FastPathBatchAdd() buffers
the FK row in RI_FastPathEntry.  When the buffer fills (64 rows)
or the trigger-firing cycle ends, the new ri_FastPathBatchFlush()
probes the index for all buffered rows, sharing a single
CommandCounterIncrement, snapshot, permission check, and security
context switch across the batch, rather than repeating each per row
as the SPI path does.  Per-flush CCI is safe because all AFTER
triggers for the buffered rows have already fired by flush time.

For single-column foreign keys, the new ri_FastPathFlushArray()
builds an ArrayType from the buffered FK values (casting to the
PK-side type if needed) and constructs a scan key with the
SK_SEARCHARRAY flag.  The index AM sorts and deduplicates the array
internally, then walks matching leaf pages in one ordered traversal
instead of descending from the root once per row.  A matched[] bitmap
tracks which batch items were satisfied; the first unmatched item is
reported as a violation.  Multi-column foreign keys fall back to
per-row probing via the new ri_FastPathFlushLoop().

The fast path introduced in the previous commit (2da86c1) yields
~1.8x speedup.  This commit adds ~1.6x on top of that, for a combined
~2.9x speedup over the unpatched code (int PK / int FK, 1M rows, PK
table and index cached in memory).

FK tuples are materialized via ExecCopySlotHeapTuple() into a new
purpose-specific memory context (flush_cxt), child of
TopTransactionContext, which is also used for per-flush transient
work: cast results, the search array, and index scan allocations.
It is reset after each flush and deleted in teardown.

The PK relation, index, tuple slots, and fast-path metadata are
cached in RI_FastPathEntry across trigger invocations within a
trigger-firing batch, avoiding repeated open/close overhead.  The
snapshot and IndexScanDesc are taken fresh per flush.  The entry is
not subject to cache invalidation: cached relations are held with
locks for the transaction duration, and the entry's lifetime is
bounded by the trigger-firing cycle.

Lifecycle management for RI_FastPathEntry relies on three new
mechanisms:

  - AfterTriggerBatchCallback: A new general-purpose callback
    mechanism in trigger.c.  Callbacks registered via
    RegisterAfterTriggerBatchCallback() fire at the end of each
    trigger-firing batch (AfterTriggerEndQuery for immediate
    constraints, AfterTriggerFireDeferred at COMMIT, and
    AfterTriggerSetState for SET CONSTRAINTS IMMEDIATE).  The RI
    code registers ri_FastPathEndBatch as a batch callback.

  - Batch callbacks only fire at the outermost query level
    (checked inside FireAfterTriggerBatchCallbacks), so nested
    queries from SPI inside other AFTER triggers do not tear down
    the cache mid-batch.

  - XactCallback: ri_FastPathXactCallback NULLs the static cache
    pointer at transaction end, handling the abort path where the
    batch callback never fired.

  - SubXactCallback: ri_FastPathSubXactCallback NULLs the static
    cache pointer on subtransaction abort, preventing the batch
    callback from accessing already-released resources.

  - AfterTriggerBatchIsActive(): A new exported accessor that
    returns true when afterTriggers.query_depth >= 0.  During
    ALTER TABLE ... ADD FOREIGN KEY validation, RI triggers are
    called directly outside the after-trigger framework, so batch
    callbacks would never fire.  The fast-path code uses this to
    fall back to the non-cached per-invocation path in that
    context.

ri_FastPathEndBatch() flushes any partial batch before tearing
down cached resources.  Since the FK relation may already be
closed by flush time (e.g. for deferred constraints at COMMIT),
it reopens the relation using entry->fk_relid if needed.

The existing ALTER TABLE validation path bypasses batching and
continues to call ri_FastPathCheck() directly per row, because
RI triggers are called outside the after-trigger framework there
and batch callbacks would never fire to flush the buffer.

Suggested-by: David Rowley <dgrowleyml@gmail.com>
Author: Amit Langote <amitlangote09@gmail.com>
Co-authored-by: Junwang Zhao <zhjwpku@gmail.com>
Reviewed-by: Haibo Yan <tristan.yim@gmail.com>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Tested-by: Tomas Vondra <tomas@vondra.me>
Discussion: https://postgr.es/m/CA+HiwqF4C0ws3cO+z5cLkPuvwnAwkSp7sfvgGj3yQ=Li6KNMqA@mail.gmail.com
(cherry picked from commit b7b27eb)

Partial import: skipped src/backend/utils/adt/ri_triggers.c changes (upstream's
fast-path FK check code, ~600 lines) and the accompanying regression tests in
src/test/regress/{sql,expected}/foreign_key.{sql,out}. Also skipped the
RI_FastPathEntry typedef. yugabyte-db D55985 doesn't use those; they can be
a follow-up point-import if YB later wants the fast-path FK check optimization.
That commit introduced AfterTriggerIsActive() to detect whether
we are inside the after-trigger firing machinery, so that RI trigger
functions can take the batched fast path.  It was implemented using
query_depth >= 0, which correctly identified immediate trigger firing
but missed the deferred case where query_depth is -1 at COMMIT via
AfterTriggerFireDeferred().  This caused deferred FK checks to fall
back to the per-row fast path instead of the batched path.

The correct check is whether we are inside an after-trigger firing
loop specifically.  Introduce afterTriggerFiringDepth, a counter
incremented around the trigger-firing loops in AfterTriggerEndQuery,
AfterTriggerFireDeferred, and AfterTriggerSetState, and decremented
after FireAfterTriggerBatchCallbacks() returns.  AfterTriggerIsActive()
now returns afterTriggerFiringDepth > 0.

Reported-by: Chao Li <li.evan.chao@gmail.com>
Author: Chao Li <li.evan.chao@gmail.com>
Co-authored-by: Amit Langote <amitlangote09@gmail.com>
Discussion: https://postgr.es/m/C2133B47-79CD-40FF-B088-02D20D654806@gmail.com
(cherry picked from commit 5c54c3e)
When a C-language function uses SPI_connect/SPI_execute/SPI_finish to
INSERT into a table with FK constraints, the FK AFTER triggers fire
and schedule ri_FastPathEndBatch via
RegisterAfterTriggerBatchCallback(), opening PK relations under
CurrentResourceOwner at the time of the SPI call.  The query_depth > 0
guard in FireAfterTriggerBatchCallbacks suppresses the callback at
that nesting level, deferring teardown to the outer query's
AfterTriggerEndQuery. By then the resource owner active during the SPI
call may have been released, decrementing the cached relations'
refcounts to zero. ri_FastPathTeardown, running under the outer
query's resource owner, then crashes in assert builds when it attempts
to close relations whose refcounts are already zero:

  TRAP: failed Assert("rel->rd_refcnt > 0")

Fix by storing batch callbacks at the level where they should fire:
in AfterTriggersQueryData.batch_callbacks for immediate constraints
(fired by AfterTriggerEndQuery) and in AfterTriggersData.batch_callbacks
for deferred constraints (fired by AfterTriggerFireDeferred and
AfterTriggerSetState).  RegisterAfterTriggerBatchCallback() routes the
callback to the current query-level list when query_depth >= 0, and to
the top-level list otherwise.  FireAfterTriggerBatchCallbacks() takes a
list parameter and simply iterates and invokes it; memory cleanup is
handled by the caller.  This replaces the query_depth > 0 guard with
list-level scoping.  Note that deferred constraints are unaffected by
this bug: their callbacks fire at commit via AfterTriggerFireDeferred,
under the outer transaction's resource owner, which remains valid
throughout.

Also add firing_batch_callbacks to AfterTriggersData to enforce that
callbacks do not register new callbacks during
FireAfterTriggerBatchCallbacks(), which would be unsafe as it could
modify the list being iterated.  An Assert in
RegisterAfterTriggerBatchCallback() enforces this discipline for
future callers.  The flag is reset at transaction and subtransaction
boundaries to handle cases where an error thrown by a callback is
caught and the subtransaction is rolled back.

While at it, ensure callbacks are properly accounted for at all
transaction boundaries, as cleanup of b7b27eb: discard any
remaining top-level callbacks on both commit and abort in
AfterTriggerEndXact(), and clean up query-level callbacks in
AfterTriggerFreeQuery().

Note that ri_PerformCheck() calls SPI with fire_triggers=false, which
skips AfterTriggerBeginQuery/EndQuery for that SPI command.  Any
triggers queued during that SPI command are not fired immediately but
deferred to the outer query level.  Since the fast-path check for
those triggers runs under the outer query's resource owner rather than
a nested SPI resource owner, and ri_PerformCheck() does not create
a dedicated child resource owner, the bug described above does not
apply.

Reported-by: Evan Montgomery-Recht <montge@mianetworks.net>
Reported-by: Sandro Santilli <strk@kbt.io>
Analyzed-by: Evan Montgomery-Recht <montge@mianetworks.net>
Author: Amit Langote <amitlangote09@gmail.com>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Discussion: https://postgr.es/m/CAEg7pwcKf01FmDqFAf-Hzu_pYnMYScY_Otid-pe9uw3BJ6gq9g@mail.gmail.com
(cherry picked from commit 34a3078)
Reported-by: jie wang <jugierwang@gmail.com>
Reported-by: Chao Li <li.evan.chao@gmail.com>
Discussion: https://postgr.es/m/CAJnZyeDyaS=X-eYN=9rDYqK=6ma1gMLa0qDgfNbZKK0e0+q99Q@mail.gmail.com
(cherry picked from commit 0302940)
The static variable afterTriggerFiringDepth introduced by commit
5c54c3e is logically part of the after-trigger state and in
hindsight should have been a field in AfterTriggersData alongside
query_depth and the other per-transaction after-trigger state.
Move it there as firing_depth.  Also update its comment to
accurately reflect its sole remaining purpose: signaling to
AfterTriggerIsActive() that after-trigger firing is active.

Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Discussion: https://postgr.es/m/CA+HiwqFt4NGTNk7BinOsHHM48E9zGAa852vCfGoSe1bbL=JNFQ@mail.gmail.com
(cherry picked from commit d6e96ba)
@chahatagrawal117
chahatagrawal117 force-pushed the 2026_pg_batch_callback_api branch from 942cf74 to a0cee04 Compare August 19, 2026 13:47
@chahatagrawal117

Copy link
Copy Markdown
Author

Split into 5 cherry-pick commits as per the docs. Each commit has the skipped-files note in its message. PTAL.

@jasonyb jasonyb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. Please create the dedicated phorge revision for these imports with upstream_repositories.csv referencing a0cee04, and that should land first, then come back here and push a0cee04 directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants