Skip to content

Robust predicate transfer - #92

Draft
leborchuk wants to merge 27 commits into
open-gpdb:mainfrom
leborchuk:rpt
Draft

leborchuk wants to merge 27 commits into
open-gpdb:mainfrom
leborchuk:rpt

Conversation

@leborchuk

Copy link
Copy Markdown

It's MVP for robust predicate transfer

Copilot AI balanced review requested due to automatic review settings October 23, 2026 15:56
@leborchuk
leborchuk marked this pull request as draft October 23, 2026 15:56

Copilot AI 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.

🟡 Changes recommended

Unresolved build propagation, producer handling, memory allocation, regression baseline, and license-audit issues prevent approval.

7 open issues
What changed in this PR

This PR introduces Anser runtime Bloom filters and an analysis-only RPT+ extension for planning predicate-transfer schedules across joins. It also adds supporting dispatcher, serialization, build, and test infrastructure.

Changes:

  • Add Bloom-filter production, consumption, payload handling, and extension hooks.
  • Add RPT transfer-plan analysis, SQL inspection, and regression fixtures.
  • Extend dispatch-side messaging and integrate optional builds and CI coverage.
File Description
src/​test/​modules/​meson.build Register the libpq notification test.
src/​test/​modules/​Makefile Include the notification test in Make builds.
src/​test/​modules/​libpq_isbusy_notify/​t/​001_isbusy_notify.pl Run the standalone notification check.
src/​test/​modules/​libpq_isbusy_notify/​meson.build Define the test executable and TAP integration.
src/​test/​modules/​libpq_isbusy_notify/​Makefile Build the standalone libpq test.
src/​test/​modules/​libpq_isbusy_notify/​libpq_isbusy_notify.c Check queued notifications while PQisBusy remains true.
src/​test/​modules/​libpq_isbusy_notify/​.gitignore Ignore generated test artifacts.
src/​Makefile.global.in Export the Anser build setting.
src/​include/​lib/​bloomfilter.h Declare Bloom bitset access and reconstruction APIs.
src/​include/​cdb/​cdbvars.h Define the sideband protocol message type.
src/​include/​cdb/​cdbdisp.h Expose an extension notification hook.
src/​backend/​tcop/​postgres.c Accept and discard unexpected sideband messages.
src/​backend/​nodes/​gen_node_support.pl Correct CustomScan binary method-name serialization.
src/​backend/​lib/​bloomfilter.c Implement Bloom bitset access and reconstruction.
src/​backend/​commands/​sequence.c Skip sideband messages while awaiting sequence replies.
src/​backend/​cdb/​dispatcher/​cdbdisp_async.c Handle buffered notifications and nonblocking output.
pom.xml Add Anser license-audit exclusions.
licenses/​LICENSE-mit-duckdb-rpt.txt Record the upstream transfer-graph license.
gpcontrib/​robust_predicate_transfer/​src/​rptinit.cpp Register RPT settings, planner hook, and SQL inspection.
gpcontrib/​robust_predicate_transfer/​sql/​rpt_regions.sql Test transfer-region boundaries.
gpcontrib/​robust_predicate_transfer/​sql/​rpt_graph.sql Test graph structure and consumer eligibility.
gpcontrib/​robust_predicate_transfer/​robust_predicate_transfer.control Define RPT extension metadata.
gpcontrib/​robust_predicate_transfer/​robust_predicate_transfer--1.0.sql Expose the transfer-plan SQL function.
gpcontrib/​robust_predicate_transfer/​README.md Describe scope, setup, and testing.
gpcontrib/​robust_predicate_transfer/​Makefile Build RPT and prepare regression runs.
gpcontrib/​robust_predicate_transfer/​include/​rpt.h Define the transfer-analysis data model.
gpcontrib/​robust_predicate_transfer/​expected/​rpt_regions.out Supply region-test expected output.
gpcontrib/​robust_predicate_transfer/​expected/​rpt_graph.out Supply graph-test expected output.
gpcontrib/​robust_predicate_transfer/​docs/​design.md Document architecture and correctness constraints.
gpcontrib/​Makefile Add optional extension build targets.
gpcontrib/​anser/​test/​Makefile Build and run standalone fold tests.
gpcontrib/​anser/​test/​anserfold_test.c Check Bloom unions and header preservation.
gpcontrib/​anser/​test/​.gitignore Ignore the fold-test executable.
gpcontrib/​anser/​src/​anserpayload.c Register payload-specific merge and checksum behavior.
gpcontrib/​anser/​src/​anserinit.c Register Anser settings and lifecycle hooks.
gpcontrib/​anser/​src/​anserfilter.c Implement Bloom sizing, serialization, and merging.
gpcontrib/​anser/​src/​anserbloomproduce.c Build and publish producer filters.
gpcontrib/​anser/​src/​anserbloomconsume.c Receive and reconstruct consumer filters.
gpcontrib/​anser/​sql/​anser_test.sql Exercise payload, protocol, and planning helpers.
gpcontrib/​anser/​sql/​anser_multijoin.sql Compare filtered and unfiltered multi-join results.
gpcontrib/​anser/​sql/​anser_faults.sql Exercise delayed delivery and protocol fault cases.
gpcontrib/​anser/​Makefile Build Anser and prepare its test environment.
gpcontrib/​anser/​include/​ansersideband.h Define transport framing and sideband APIs.
gpcontrib/​anser/​include/​anserplan.h Declare injection and scan-pushdown interfaces.
gpcontrib/​anser/​include/​anserpayload.h Define payload-operation contracts.
gpcontrib/​anser/​include/​anserfilter.h Define Bloom payload framing and thresholds.
gpcontrib/​anser/​include/​anserbloom.h Expose producer and consumer executor helpers.
gpcontrib/​anser/​include/​anser.h Define channel identity and shared settings.
gpcontrib/​anser/​expected/​anser_multijoin.out Supply multi-join expected results.
gpcontrib/​anser/​expected/​anser_faults.out Supply fault-test expected results.
gpcontrib/​anser/​anser_test/​Makefile Build the separate SQL test harness.
gpcontrib/​anser/​anser_test/​anser_test.control Define test-extension metadata.
gpcontrib/​anser/​anser_test/​anser_test--1.0.sql Register SQL-accessible test helpers.
devops/​build/​automation/​cloudberry/​scripts/​configure-cloudberry.sh Support opt-in Anser automation builds.
configure.ac Declare optional Anser and RPT builds.
configure Include generated configure-option handling.
.github/​workflows/​build-cloudberry.yml Add Anser and RPT integration-test jobs.

Review effort: Balanced

Comment thread pom.xml Outdated
Comment on lines +1431 to +1433
<exclude>gpcontrib/anser/anser_test.control</exclude>
<exclude>gpcontrib/anser/**/*.sql</exclude>
<exclude>gpcontrib/anser/**/*.out</exclude>
AnserBloomFilterProduceState *state;
uint64 seed;

if (channel_key == NULL || total_parts == 0 || part_index >= total_parts)
state->part_index = part_index;
state->total_parts = total_parts;
seed = AnserBloomSeed(channel_key->condition_key);
state->filter = AnserBloomCreate(total_elems, max_payload_bytes, seed);
Comment on lines +165 to +179
/*
* Serialize as a self-contained single part (index 0 of 1). The coordinator
* stores the first part verbatim and OR-folds each later part, bumping the
* merged header's fold count, so the final count reflects how many parts were
* unioned. state->part_index / state->total_parts identify this producer to
* the channel (expected_producers), not the on-wire part layout.
*/
payload_size = AnserBloomSerializedSize(state->filter);
payload = palloc(payload_size);
ok = AnserBloomSerializePart(state->filter,
0,
1,
payload,
payload_size,
&payload_len);
Comment on lines +31 to +35
rpt_transfer_plan
-----------------------
transfer order: 2 1
vertex 1 rpt_b rows=10 unfiltered
vertex 2 rpt_a rows=100 unfiltered

-- FULL JOIN produces no usable edge at all: no region.
SELECT rpt_transfer_plan(
'SELECT * FROM rpt_a a FULL JOIN rpt_b b ON a.a = b.a WHERE a.b > 50')
Comment thread src/Makefile.global.in
have_yaml = @have_yaml@
YAML_LIBS = @YAML_LIBS@
with_zstd = @with_zstd@
with_anser = @with_anser@
@leborchuk
leborchuk force-pushed the rpt branch 2 times, most recently from 5d800d2 to 80b6f2d Compare November 9, 2026 12:56
yjhjstz and others added 10 commits November 9, 2026 05:47
Hash and nested-loop joins create two partition propagation requests
for every combination of their other property requests: request 0
may push DPE predicates to a child, request 1 passes the incoming
requirement through. Parallel hash joins always create both, and so
does every nested-loop join.

When the join has no partition consumers below it and its parent
requires no partition propagation, both requests produce an empty
spec for every child. The second request then optimizes exactly the
same child contexts as the first one, only for its cost context to
be dropped as a duplicate. With parallel plans enabled this repeats
a large share of the optimization jobs of every join group.

Reject such requests in CEngine::FCheckReqdProps, before any child
is optimized, and add CPartitionPropagationSpec::IsEmpty() for the
check.

Measured on a debug build with star, outer, semi, anti, aggregate,
multi-key, union and DPE queries: plans and costs are unchanged,
and ORCA planning time with parallel plans drops by 10-19% on the
join-heavy queries.

Claude-Session: https://claude.ai/code/session_01M1CxTMjApWd2XBG3C4wnyi
Claude-Session: https://claude.ai/code/session_01ALBeQzzkBwDJDY1UJ65JrA
(cherry picked from commit f4d28f67ae59dc572bb78d46b087b7cbc9bbec65)
A request of a group expression is optimized once per context of its
group. Under contexts that differ only in the required distribution, it
usually computes the same child requirements and reaches the same child
contexts again, after computing every child requirement, looking up the
child contexts, waiting for the child jobs and deriving the plan
properties of each optimized child.

Store the child contexts a request reaches on its group expression,
keyed by the request number, the search stage and the parent's required
plan properties apart from distribution. A later request with an equal
key reuses them if it would reach the same contexts:
- Every child requirement except distribution only depends on the
  matching part of the parent's requirements, which the key makes
  equal, and on the plans of the children optimized before, which are
  the best plans of the same contexts. Only Ped() sees the whole parent
  requirement, so the required distribution of each child is computed
  again and compared. The plan properties of earlier children are
  derived again for this, as DerivePrevChildProps() does: some
  PdsRequired() implementations change the derived distribution of the
  first child in place, and the properties cached on a cost context are
  shared.
- A child context must have a plan, must not be the context itself, and
  must be the one a lookup of its requirements finds. A group can hold
  several contexts with equal requirements when a requirement changes
  after its context was inserted.
The cost bound checks made after each child are repeated with the
reused contexts. CTE sequences and multi-stage search are left out.

The developer GUC optimizer_enable_child_opt_memo (default on) turns the
memo off. optimizer_verify_child_opt_memo optimizes the children anyway
and logs where the result differs from the reused contexts.
optimizer_print_optimization_stats prints the memo counters.

In 13 join and aggregation queries the plans are identical with and
without the memo, and 11914 of 16127 lookups reuse child contexts with
parallel plans (5020 of 7870 serial). Planning time drops by 12.6%
geomean (15.4% in total) with parallel plans and by 8.3% (9.4%)
serial. Verify mode finds no difference there or in 25 related
regression tests, whose results do not change apart from run-to-run
noise.

Claude-Session: https://claude.ai/code/session_01M1CxTMjApWd2XBG3C4wnyi
Claude-Session: https://claude.ai/code/session_01ALBeQzzkBwDJDY1UJ65JrA
(cherry picked from commit 9bb35ccdd3c1e13809776bbe76189c1bc34175c4)
Since 37224a3 CXformUtils::PdrgpcrIndexKeys() has been a thin wrapper
around PdrgpcrIndexColumns(), which appends the index's INCLUDE columns
after its key columns. CPredicateUtils::ExtractIndexPredicates() therefore
treated a predicate on an INCLUDE-only column as indexable. Ordinary
comparisons were still rejected later by CMDIndexGPDB::IsCompatible(),
but the shortcuts for boolean column references (col -> col = true,
NOT col -> col = false) and for IS [NOT] NULL bypass that check and went
straight into the index condition list.

The DXL-to-plan translator then maps the column through GetKeyPos(), which
returns ulong_max for a non-key column, so the Var's varattno wrapped to 0
and execution failed with

  ERROR:  bogus index qualification (nodeIndexscan.c:1250)

or, for IS [NOT] NULL,

  ERROR:  btree index keys must be ordered by attribute (nbtutils.c:777)

Restore the key-only semantics: reintroduce the EIndexCols selector
(EicKey / EicKeyAndIncluded) on PdrgpcrIndexColumns()/PcrsIndexColumns()
and make PdrgpcrIndexKeys()/PcrsIndexKeys() request key columns only.
The cost model keeps asking for key + INCLUDE columns so that
ComputeUnusedIndexWeight() costing is unchanged.

Index-only-scan coverage checks are unaffected: they already use
PcrsIndexReturnableColumns().

Add regression coverage to gp_covering_index.

Fixes apache#1948
memoize_optimizer.out expects an Index Scan on expr_key_idx_x_t for the
expr_key join, but ORCA chooses an Index Only Scan whenever the preceding
VACUUM managed to mark expr_key all-visible. Whether it can depends on the
snapshots held by tests running concurrently in the same parallel group,
so the test intermittently fails with

  -         ->  Index Scan using expr_key_idx_x_t on expr_key t2 ...
  +         ->  Index Only Scan using expr_key_idx_x_t on expr_key t2 ...
  +               Heap Fetches: N

The scan type is irrelevant to what the test checks (memoize cache hits
and misses on an expression key), so disable optimizer_enable_indexonlyscan
around that query. The GUC only affects ORCA; the planner output merely
gains the echoed SET/RESET lines.
PexprBitmapSelectBestIndex() breaks ties between equally selective bitmap
index candidates by the number of index columns, as a proxy for how big
the index is on disk. It got that number from PdrgpcrIndexKeys(), which
the previous commit made key-only, so an index with INCLUDE columns now
looked exactly as narrow as one without. Two indexes on the same key then
tie, and the first one in pg_index order wins - which can be the much
wider one:

  CREATE INDEX wide   ON t(a) INCLUDE (payload);  --  12 MB
  CREATE INDEX narrow ON t(a);                    -- 720 kB
  SELECT * FROM t WHERE a IN (1, 100, 1000) OR b = 17;
  ->  Bitmap Index Scan on wide     -- was: narrow

INCLUDE columns are stored in every index tuple, so the width comparison
must count them; only index *conditions* have to stay key-only. Ask for
EicKeyAndIncluded there. Key columns still come first in the array, so
the adjacent first-index-key check is unaffected.

Also fix a typo in the PcrsIndexReturnableColumns comment.

Reported-by: Alena0704 <apache#1957>
EXPLAIN (ANALYZE, BUFFERS) did not include buffer usage collected by
query executors in the instrumentation deposited on the coordinator.

Send per-node buffer usage to the coordinator and accumulate block
counters across all workers within each slice. Node
statistics are inclusive, so a parent also contains buffer usage
accumulated by its descendants within the same slice.
…uted xid

SetupUDPIFCInterconnect_Internal() records one CursorICHistoryEntry per
interconnect instance on the QD and prunes old entries only when the
statement runs in a different transaction than the previous one, using
the distributed xid as the transaction identity.  Distributed xids are
assigned lazily (only for transactions that write or are dispatched
two-phase), so autocommit read-only statements always see
InvalidDistributedTransactionId on both sides of the comparison.  The
table is therefore never pruned on read-only sessions and grows by one
48-byte entry per Motion-containing statement for the life of the
backend (2.8 MB after 60000 statements, plus the heap fragmentation the
steady stream of small long-lived allocations induces).

Use MyProc->lxid instead: it is assigned to every top-level transaction,
including read-only ones, and stays constant within a transaction block,
so entries belonging to cursors that are still open in the current
transaction remain protected exactly as before.

Verified on a single long-lived session running the queries from apache#1947:
UdpInterconnectMemContext grew 0 -> 2814 KB over 60000 statements before
the change and stays at 12 KB after it; RssAnon went 12.7 -> 24.6 MB
before and is flat at 14.4 MB after.  Pruning still does not happen
inside an explicit transaction block or while a cursor is open, and
FETCH from an open cursor after 300 intervening statements works.

Related: apache#1947
paramValues is declared and never used. The GNUmakefile builds
extended_protocol_resqueue without the usual warning flags, so nothing
has reported it; with -Wall the compiler warns about it.

Assisted-by: Claude Code
MERGE into a DISTRIBUTED REPLICATED table did not broadcast the source
relation, so each segment merged only the source rows stored locally.
The replicas diverged and the reported row count was wrong.

Treat MERGE like UPDATE/DELETE when flagging a replicated result
relation (upd_del_replicated_table), so the join broadcasts the source
to all segments.

Also reject volatile functions in MERGE actions on replicated tables.
The existing check only looked at the subplan targetlist, but MERGE
action expressions are kept in mergeActionLists, so e.g.
"UPDATE SET v = random()" was accepted and the replicas diverged.
src/common is compiled twice, for the frontend programs and for the
backend, but two of its files include backend headers in both:

- percentrepl.c includes cdb/cdbvars.h, postmaster/postmaster.h and
  utils/builtins.h for GpIdentity, terminal_fd and pg_ltoa(), which it
  uses only under #ifndef FRONTEND.
- kmgr_utils.c includes postgres.h, which no frontend file may include,
  and utils/builtins.h after choosing between postgres.h and
  postgres_fe.h, and uses neither.

Through them, the frontend copies of both files depended on
utils/fmgrprotos.h, which the backend build generates from its function
catalog, and on postgres.h itself. A frontend compile has no business
with either, and it could not run until the backend's headers had been
generated; a top-level make generates every header before it builds
src/common, which is why this went unnoticed. After this change neither
file's frontend compile includes a generated header at all.

Move percentrepl.c's three includes under #ifndef FRONTEND, and drop
kmgr_utils.c's two.

Assisted-by: Claude Code
leborchuk and others added 6 commits November 10, 2026 14:44
Anser needs to move a small piece of information -- today a bloom
filter over a join key -- from the segments to the coordinator and
back, over the dispatch connection that is already open.  Four things
in the backend stand in the way, and none of them mentions Anser.

A QE can already send a NOTIFY to the coordinator, but the dispatcher
dropped any it did not recognise.  Add cdbdisp_notify_hook so an
extension can claim one, and keep the ones that arrive in the same
read as a command completion instead of losing them.

The other direction had no message type at all.  Add
GP_SIDEBAND_MESSAGE, which the QE command loop accepts and discards
when nobody is waiting -- a waiter can be cancelled after the
coordinator has written, and an unknown type would otherwise be fatal.

A CustomScan travelled through the binary plan writer as the text
" :methods " plus a token, and the reader took the first four bytes of
that as a length and asked for 1.7 GB.  Any plan with a CustomScan
failed to deserialize on the segments; write the name as a counted
string instead.  catversion is bumped accordingly.

Finally, a bloom filter cannot be shipped without reaching its bitset,
so expose it along with the sizing its holder must agree on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Anser is a publish/subscribe facility for MPP execution.  Producers on
the segments publish something about a running query, the coordinator
combines the per-segment parts into one, and consumers on the segments
receive it and prune work with it.  Today that is a bloom filter over
a hash join's build key, used to skip probe rows that cannot join.

A channel is the rendezvous for one such piece of information in one
query, keyed by session, command, and the condition it describes.  It
lives only in the coordinator backend running the query, for exactly
as long as that query runs: no shared memory, no background worker, no
second connection to authenticate.  Parts travel up as a NOTIFY and
the merged payload comes back as a sideband message.

Both nodes wrap a base scan rather than an intermediate node, which
makes the filter a superset of the keys the joins below would have
left -- less selective, never wrong.  The direction is fixed to build
-> probe, because a consumer on the inner side would wait on a
producer the join is not yet reading, which is a deadlock and not a
slow path.

Every failure ends in unfiltered execution: a filter too small to
reject anything, a squelched producer, a malformed part, a missed
deadline.  The planner gate is the one that costs nothing, and the
later ones exist because a row estimate can be wrong.

Built only with --with-anser and off by default; the subsystem creates
no catalog objects and is configured entirely by GUCs.  Includes the
regression suites and the CI job that runs them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Anser builds as a MODULE_big with no SHLIB_EXPORTS, so the module is
compiled -fvisibility=hidden and every extern in its headers lands in
.symtab but not .dynsym.  A second extension cannot reach any of it:
RTLD_GLOBAL does not help, because a hidden symbol never reaches the
dynamic symbol table, and load_external_function() fails for the same
reason.  Mark the public declarations PGDLLEXPORT, which is the
convention the tree already uses for a cross-module C API -- see
contrib/hstore and src/pl/plpython.

Two plan-resolution helpers are promoted into that API rather than
left for a caller to reimplement.  Both encode which plan shapes are
safe to inject into, so a second copy would be a second place to get
that wrong.

The pushdown itself is split so the decision can be tested on its own.
A scan key is numbered by the table's attributes while the consumer is
numbered by the scan's output targetlist, and the two coincide only
when the scan emits every column in order -- so the filter was handed
a position where an attribute number was wanted, and a projecting scan
tested the wrong column and discarded every row.  The decision now
maps one to the other, and refuses the shapes that have no mapping: a
computed column, a whole-row or system Var, a Var of another relation.

A second function walks from the consumer to the deepest node that
accepts the key, so stacked consumers over one relation all push into
the same scan instead of probing tuple by tuple.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A filter that is a subset of the build side's keys rejects rows that
join, so every one of these is a wrong answer rather than a slow one.

A build side under a nested loop yields different keys per outer tuple,
but neither node rebuilds on rescan: the producer stays published and
the consumer keeps the filter it has, so the second iteration is probed
against the first iteration's keys.  Refuse such a join at injection
time, by intersecting the loop's nestParams with the build subtree's
extParam -- the same test UpdateChangedParamSet makes at runtime.

Cleanup was scoped by counting ExecutorEnd, which only ever measures
recursion into ExecutorEnd itself.  A statement run through SPI from a
function body goes through its whole Start/Run/End inside the outer
query's Run, so its End looked outermost and took the running query's
channels with it.  Count over execution instead, as auto_explain does.

Two readers share the QD->QE connection, and the nextval one died on a
sideband message: it took the type byte, then errored without the body,
leaving the stream desynchronised and the session unusable.  It now
skips one whole and waits on, and Anser no longer subscribes when it
has no deadline to wait out.

The coordinator could also leave a QE's notification unread for the
whole lifetime of the slice that sent it, because PQisBusy() reports
the command rather than the queued message, and the bytes may already
have been absorbed by a flush.  Drain before deciding to wait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A blocking push stranded the dispatcher uncancellably when a consumer
had timed out: deliver without blocking, and let a consumer withdraw.

AnserSliceProducers counted a singleton reader and direct dispatch as
the whole cluster, so the channel never completed.

A producer whose slice ran but whose node never did stayed silent,
leaving consumers to wait out the deadline.

A consumer ignored QueryFinishPending, so a query the coordinator had
already finished -- a LIMIT satisfied early -- still sat out the full
deadline.  CHECK_FOR_INTERRUPTS does not cover that flag.

Injection is now refused when the build and probe sides share a slice.
Slices are divided by Motions only, so a co-located join offers no
synchronisation point to hang the exchange on and no network cost to
save against the filter's own.  probe_slice already carried the answer
but was never read, and was left uninitialized on one path.

Channel cleanup is wholesale, and inside SPI the executor nesting level
never returns to zero, so a PL/pgSQL body held one merged filter per
statement until the outermost one ended -- about 1 MB a query, and up to
the full anser.max_info_size.  A query now takes its own channels with
it, keyed by the command count the channel already carries.  Only
finished ones: gp_command_count names the newest query descriptor rather
than the one that is ending, and the two differ whenever a portal has
outlived the statements that ran after it.

Planning joins that nesting level too.  Constant folding can run a query
through SPI, and that query reached ExecutorEnd believing it was the
outermost one -- the split counter PostgreSQL merged in 76db9cb.

The anser.timeout_ms default drops from 100 s to 10 s.  Its rationale
had outlived itself: delivery no longer blocks, so a wide slice no
longer needs the headroom it was sized for.  The help text said
otherwise, in four places.

Hash keys are now checked on the base type and against the type's own
equality operator.  A domain inherits typlen and typbyval, so a domain
over float8 passed both the by-value test and a float exclusion that
names only the built-in OIDs -- and -0.0 would have stopped matching
0.0, dropping rows that join.

The anser_test harness moves to a library of its own.  It was linked
into anser.so, which every user preloads, and its extension shipped with
the subsystem; it now builds only for installcheck.  It resolves the
C API against the preloaded library at load time, which is what the
PGDLLEXPORT markers on the public headers were for.

Tests move to redistributed joins to match the Motion rule.  The
parallel section had also been running on the 1 ms deadline left over
from the preceding block, which let it pass without a filter ever
arriving; that block has since moved out of the file altogether.  It
keeps its coverage under GPORCA only, since the Postgres planner marks
Motion paths parallel-unsafe.

The catversion bump is withdrawn.  The CustomScan serialization fix
reaches only outfast/readfast, the binary format the dispatcher uses;
the text branch that nodeToString writes into catalogs is untouched, and
a CustomScan is a Plan node, which catalogs never store anyway.

configure-cloudberry.sh no longer passes --with-anser unconditionally,
which had put the extension into every CI and release build despite the
configure default of no.  The ic-anser job does not need it: the
gpcontrib/anser target builds, installs and preloads the extension
itself, so the matrix entry no longer arms the cluster either -- and the
postgres_conf_addons hook added for that is gone with it.

anser_test carries its own PG_MODULE_MAGIC.  It had been relying on the
one in anserinit.c, which stopped covering it when it became a library
of its own.

anser.max_info_size is synced to the segments.  Both ends read it, and
a segment hands it to pq_getmessage() as the cap on the message it is
about to read, so a disagreement does not merely size things
differently -- it frames the wire differently.  Without the flag a gang
created after the SET fell back to the compiled-in default.  Syncing
narrows that window rather than closing it, since a gang busy with an
open cursor is never sent the SET, so a read that overruns the cap now
terminates the connection instead of going on: pq_getmessage takes the
length word and leaves the body behind, and a message boundary cannot
be recovered from there.  The README said POSTMASTER where the code
said USERSET, and now says what the trade-off is.

SHLIB_PREREQS is dropped.  Anser links nothing and only reads libpq's
headers, and under PGXS the prerequisite expands to "make -C $(libdir)
all", for which there is no such target.

preload-anser no longer mistakes tr's exit status for psql's: a psql
that failed left the list empty, and the recipe then wrote
shared_preload_libraries back as just anser, dropping every other
library.  It also leaves the cluster alone when it is already armed
instead of restarting on every installcheck.

The cases that need gp_inject_fault move to anser_faults.  The
regression driver already excludes a file that calls one when the debug
extensions are off -- faultinjector_prep in src/Makefile.global.in --
so sharing a file with them took the rest of anser_runtime_filter down
as well.

The TAP helper uses pg_hton32 rather than htonl and so no longer needs
<arpa/inet.h>, which MSVC does not have.

The fold no longer touches the part header.  It incremented total_parts
once per fold, but a producer already writes the channel's producer
count there, so a merged header carried 2N-1 -- which is N only when N
is 1, and that is the shape the one test covering it pinned.  The number
is what a consumer reports and what the log prints when a filter goes
missing, so it was wrong exactly where someone would be reading it.  A
unit test now folds three parts, where N and 2N-1 differ; it needs no
cluster and runs from installcheck.

anser.max_info_size stops at what the protocol can carry.  The bound was
INT_MAX, so a large setting overflowed the pq_getmessage() cap computed
from it into a negative int and every delivery was then refused -- and
anything past PQ_LARGE_MESSAGE_LIMIT could never have been read in the
first place.  The frame around the payload has a name now rather than
being spelled out at the single place that needed it.

The outer-join case in anser_multijoin produces NULL keys at last.  Its
right side held every key the left could offer, and the join above it is
strict in that key, so reduce_outer_joins() made the LEFT JOIN an inner
one before any path was considered.  It now joins a table with gaps,
through a subquery that keeps the reduction from applying.  The tables
the chains rest on are left alone: thinning one of those would have cost
eighteen assertions to fix one.

No filter reaches that join, and none can.  Keeping the outer join costs
a SubqueryScan, which the key walk does not descend through, and there
is no way around it: a NULL-keyed row survives only under an outer join,
an outer join survives only behind a subquery boundary.  The block says
so now, and checks what it can -- that such a plan returns the same rows
with the filter on and off.

The pushdown cases say whether anything was injected.  They compared a
filtered count against an unfiltered one, which also agree when no
filter was ever built -- and the computed-key case turns out to be one
of those: its subquery is pulled up, the hash clause becomes an
expression, and anser_hashjoin_keys takes plain Vars only.  The comment
claiming otherwise is corrected rather than the behaviour.

Every GUC a test pins is reset at the end of it.  Three files out of
four left one behind, and anser_faults all five.

Smaller things: send_fake_sideband_message() checks what pqPutnchar()
returns; the nextval reply no longer serializes uninitialized locals on
its error path, where a QE reads overflow before error and would report
reaching a sequence's maximum rather than the real failure; the
condition key's zero-padding is written down beside the struct that
requires it; and the comments naming AnserBloomShapeFor,
AnserChannelLock and anser_rf_size now name things that exist.

configure is regenerated for --with-anser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A QE connection whose output buffer still holds bytes at cleanup
time carries a partially delivered asynchronous payload (an Anser
sideband message; normal command traffic is fully flushed before
cleanup).  The peer may already have read a prefix of it, and
nobody flushes the rest once the command is over --
checkDispatchResult only flushes connections whose command is
still running.  Reusing such a connection makes the next dispatch
overwrite the remainder of the buffer (PQsendGpQuery_shared), and
the peer then misreads its stream.

Have cleanupQE decline to recycle any connection with pending
output, so the next dispatch builds a fresh one.
Squashes the review-round commits: the delivery-protocol rework, the
debug tracing used to diagnose it, and the single-slot QE assembly.

A whole-filter push left tens of MB queued in the QD's output buffer,
and on the UDP interconnect it was flushed only while a dispatch
socket was readable.  Once parts and subscribes were consumed, inbound
traffic stopped, flushing stopped with it, and the consumer sat in
pq_getmessage() with no deadline of its own -- an uncancellable
deadlock, seen as a cursor's FETCH 1 never returning.

Deliver the merged payload in chunks instead, one GP_SIDEBAND_MESSAGE
each, sending the next only when the consumer asks with a NEXT (kind
'N', its part field carrying the offset wanted).  At most one chunk --
sized to fit the connection's send buffer -- is ever in flight, so a
consumer walking away leaves nothing torn behind, and every NEXT keeps
waking the coordinator: the delivery drives its own servicing.  Chunk
coordinates (total_len, offset) join the CRC, the QE assembles into
one slot and drops chunks for keys it has unsubscribed from, and the
wait deadline runs from the last progress, with a hard cap from the
subscribe.  All waits use the monotonic clock.  Wire tag is anser4.

Also zero-initialises the dispatcher's buffered[] array (defensive).

Track delivery connections with libpq connection-destroy/reset events so
InitPlan and error teardown cannot leave dangling PGconn pointers. Register
through the existing internal libpq ABI; no new core hook or exported
symbol is required. Refuse subscriptions if registration cannot allocate.

Make torn-delivery cleanup allocation-free and nonblocking, poisoning
connections instead of waiting during error handling. Add a standalone
connection-event test covering destruction, reset, registration failure,
other event users, and preservation of merged payloads.

Validation: connection-event tests pass with -O3 -DNDEBUG and ASan/UBSan;
changed C sources pass syntax checks. Cluster regressions remain pending.
Add gpcontrib/robust_predicate_transfer, Phase I of robust
predicate transfer over Anser: a pure analysis pass over a
finished PlannedStmt that discovers transfer regions (connected
components of the join graph over base scans), builds equivalence
classes of join keys by union-find, and computes the asymmetric
transfer plan (chaining forward stage, leader-broadcast backward
stage) ported from the RPT+ artifact (MIT, Stichting DuckDB
Foundation; see licenses/LICENSE-mit-duckdb-rpt.txt).  Adds a
plan-order consumer-eligibility check for the MPP deadlock rule.
SQL-visible via rpt_transfer_plan(text); planner hook logs the
dump at DEBUG1 under rpt.debug.  Nothing is injected or executed.
Tests are EXPLAIN-free and property-based where estimates or
tie-breaks could vary.
Preserve protected join directions when bypassing unfiltered tables,
terminate rewiring on actual changes, and choose direction-aware parents
with separate forward/backward depth tie-breaks. Enforce the chain cap
and identify reducer scans and bridge tables.

Classify both stages from distribution policies, including implied
edges and replicated sources. Keep local backward filters non-blocking
and require cross-slice safety for global consumers. Document producer
placement and parallel-worker completeness requirements.

Unwind STL owners before reporting PostgreSQL errors, fail open in the
diagnostic planner hook, and build dumps before one PostgreSQL append.
Wire --with-rpt into normal builds, require Anser, restrict diagnostic
access, and resolve IndexOnlyScan keys through the index target list.

Add synthetic tests with checks active under NDEBUG and strengthen SQL
assertions for both optimizers using relation names where needed.

Validation: C++ compilation, unitcheck with -O3 -DNDEBUG, an intentional
CHECK failure, ASan/UBSan, configure checks, and dump comparison passed.
SQL installcheck could not start: linking pg_regress requires libpq,
which is unavailable in the local build.

Assisted-by: ChatGPT
Prepare the Phase II go/no-go survey without injecting executor nodes.
Extract backward eligibility into a shared analysis pass and record
per-class decisions, refusal reasons, relink parents, tuple-output key
positions, slice IDs, and bounded-prefetch requirements.

Require a component-root producer, a global transfer, and a hash-build
ordering proof. Reject nullable-side crossings, multi-tuple prefetch
paths, all SubPlan placements even below Motions, and any non-empty
build-side extParam. Apply a per-class consumer cap and report rough NDV
benefit estimates, preserving unknown statistics explicitly.

Add a planning-only, read-only survey runner comparing RPT eligibility
with Anser's injected producer count under both optimizer settings.
Count SELECT statements rather than files, split multi-SELECT templates,
and require TPC-H Q15's CTE form. Expect 103 TPC-DS statements from 99
templates. Record per-statement results, failures, and raw plan
artifacts.

Add reserved QD-only backward settings, document diagnostic EXECUTE
privileges, and update the runtime design to use one statement without a
session cache. Runtime injection remains gated on benchmark results.

Add native safety tests and production-walker/resolver SQL regressions,
including a correlated SubPlan with an internal Motion. SQL assertions
identify tables by name and account for retained optimizer plan shapes.

Validation: C++ compilation, native tests with -O3 -DNDEBUG, ASan/UBSan,
all 10 Python survey tests, and git diff --check passed. The new SQL
regressions and full benchmark survey have not run on a cluster.

Assisted-by: ChatGPT
Build a separate forward schedule rooted at the largest relation,
including unfiltered fact tables, without changing backward survey
leaders or eligibility decisions.

Add a finished-plan builder for the distributed reducer InitPlan:
append coordinator and non-parallel reader slices, Gather and Sequence
nodes, an empty Result with an implicit-AND false qual, and a boolean
PARAM_EXEC. Continue node IDs above both existing plans and reducer
children, preserving existing InitPlans and slice mappings.

Add standalone layout tests for fresh and existing InitPlans, checking
slice indices and parents, parameter numbering, node IDs and qual shape.
Run these tests through unitcheck and ignore generated test binaries.

This is preparatory Phase III work. The builder is not called by the
planner hook yet; reducer scans, filter wrappers, cost gating and runtime
injection remain unimplemented. SQL execution and cross-dispatch filter
delivery have not been validated on a cluster.

Validation: native unitcheck and 10 survey tests pass with -O3 -DNDEBUG;
InitPlan layout tests pass AddressSanitizer and UndefinedBehaviorSanitizer.

Assisted-by: OpenAI Codex
Build the forward reducer on a private copy of the finished statement and
attach it as an uncorrelated distributed InitPlan. Apply completed filters
to main-plan roots, or also to chain scans with rpt.forward_apply=all.
Keep Anser per-join injection exclusive with RPT and fail open on planner
construction errors while preserving query cancellation.

Copy key-only SeqScans using the statement's existing range table and
snapshot. Retain quals only when every operation belongs to an explicit
built-in comparison/hash allowlist; otherwise drop the whole qual list.
Reflect dropped quals in filter sizing and selectivity estimates. Gate
components by root rows, estimated selectivity and payload exchange cost.

Hand intermediate co-distributed filters between reducer scans in-process,
without coordinator traffic or blocking waits. Use statement-scoped Anser
channels for global hops and filters consumed by the main dispatch. Phase
II backward runtime injection remains deferred.

Add multi-hop, local/global, apply-all, early-termination, prepared and SPI
SQL coverage plus forced transport-failure cases. Correct backward survey
expectations for simplified outer joins whose root is on the probe side.
Provide an empty-InitPlan measurement switch, workload generator, recorded
exchange measurements and a staged cluster experiment plan.

Validation: native unitcheck and 10 survey tests pass with -O3 -DNDEBUG;
graph, InitPlan and Anser connection-event tests pass ASan/UBSan. Changed
C/C++ sources pass syntax checks. The new SQL suites and cross-dispatch
runtime behavior still require validation on a running cluster.

Assisted-by: OpenAI Codex
Restrict raw-Datum Bloom keys to canonical built-in by-value types, so
custom equality operators cannot introduce false negatives. Add the
modulo-equality reproducer as a regression under both optimizers.

Supply a PlannedStmt-backed context when checking existing InitPlans and
expression SubPlans. Test this path with the real backend plan walker.
Parse C++ locale headers before PostgreSQL gettext macros to avoid clean
non-NLS build failures. Rename the reserved ANALYZE helper parameter and
correct fault-test expected output. Replan fault queries when toggling RPT
and assert that injected faults actually fire.

Add step-marked forward diagnostics with an independent watchdog that
captures activity before cancellation and leaves server backends intact.
The reported forward-test hang remains unlocalized; no hang fix is claimed.

Remove implementation-phase references and detailed design documents from
version control while retaining local copies. Keep a general README with
paper and artifact links, and record artifact attribution in NOTICE and
LICENSE alongside the existing MIT license text.

Validation: native graph, injection and backend-walker tests pass with
-O3 -DNDEBUG; 10 survey and four diagnostic tests pass. Graph/walker tests
pass ASan/UBSan. GCC 15 compiles all extension sources without the forced
libintl include; local linking stops at the absent postgres executable.
Cluster SQL regressions and the original GCC 13 environment remain unrun.

Assisted-by: OpenAI Codex
Use the largest-root forest for forward and backward transfers. Build
one backward producer per root-led equivalence class above the root's
forward consumers, and place member consumers only where the existing
eligibility proof permits waiting. Share channel and plan-node IDs and
track scan stacks so both passes relink the original parent safely.

Add backward sizing, selectivity and exchange-cost gates, including a
per-consumer delivery term shared with forward selection. Support
backward-only injection and retain fail-open planning and cancellation.
No core or Anser changes are required.

Make range-table identities explicit throughout RPT and resolve relation
aliases on demand. Document the call chain and runtime structures, and
extend the survey and diagnostic runner with largest-root decisions.

Add native placement and selection coverage plus SQL and transport-fault
regressions under both optimizers. Allow 120 seconds for assertion blocks,
verify oversized producers do not complete filter deliveries, and ignore
native-test dSYM bundles.

Validation: native tests with -DNDEBUG and ASan/UBSan; compiler checks;
11 survey tests and 6 diagnostic tests; git diff --check. Cluster
regressions and benchmark experiments remain pending on the dev cluster.

Assisted-by: ChatGPT
Install anser_test through Anser's install-anser-test target before
running RPT regressions. Normal Anser installation omits this extension,
but backward fault tests require its delivery counter. The prerequisite
also covers installcheck-with-install through its installcheck call.

Validated the recursive installation target with make -n and checked
whitespace with git diff --check. Cluster regressions were not run.

Assisted-by: ChatGPT
Keep the logical join graph before SkipUnfilteredTable rewrites it for
forward transfers. Grow one forest over both views and choose each
stage's parent from its own edges, so unfiltered roots retain backward
consumers without re-enabling prohibited outer-join directions.

Use Anser's resolved targetlist helper for local filter stacks and add
native coverage for deep stacks, reordered outputs and unfiltered
backward edges. Exercise stacked consumers and producer/consumer stacks
with EXPLAIN VERBOSE in the SQL regressions.

Stop treating InitPlan presence alone as evidence of external producer
parameters. Make backward fixtures use incompressible padding and the
hash-size heuristic, and verify a cached injected plan before arming the
cancellation timeout. The reported ORCA cancellation cause remains
unconfirmed pending a new cluster run.

Validation: compiler checks; native tests with -DNDEBUG; graph and
injection tests with ASan/UBSan; 11 survey and 6 diagnostic tests;
SQL/expected consistency and git diff --check. Cluster SQL regressions
remain pending.

Assisted-by: ChatGPT
Always apply forward filters only to main-plan roots. Remove the
forward_apply switch, its extra consumer placement and cost branches,
and the corresponding SQL and native test variants. Keep intermediate
local transfers inside the reducer and retain the real reducer InitPlan.

Remove debug_noop_initplan and its measurement script. Mark forward and
backward as session-settable developer controls with GUC_NOT_IN_SAMPLE;
rpt.enable remains the user-facing master switch.

Refresh the README with the current planner and executor call chains,
graph views, structures, injection order and diagnostic workflow. Add a
four-table star collector and a step-by-step guide for gathering graph
decisions, results, actual text/JSON EXPLAIN ANALYZE plans and rotating
baseline/forward/backward/full timing samples under both optimizers.

Validation: compiler checks; native tests with -DNDEBUG; 11 survey and
6 diagnostic tests; SQL/expected and README link checks; git diff --check.
The collector was statically checked. Cluster regressions and example
measurements remain pending on the development cluster.

Assisted-by: ChatGPT
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.

6 participants