Skip to content

scan: init external rel table scan state before first scan()#734

Merged
adsharma merged 1 commit into
mainfrom
pg_client
Jul 25, 2026
Merged

scan: init external rel table scan state before first scan()#734
adsharma merged 1 commit into
mainfrom
pg_client

Conversation

@adsharma

Copy link
Copy Markdown
Contributor

Fixes the regression introduced by the pg_client extension moving rel table registration to RelGroupCatalogEntry + ForeignRelTable: MATCH (a:node_person)-[k:rel_knows]->(b:node_person) was crashing with tableFunc: sharedState is null on the first scan.

Root cause

ScanRelTable::initLocalStateInternal creates the per-thread scan state but only calls table->initScanState in fetchNextBoundNodeBatch() and after pulling a new child tuple. The first scan() in getNextTuplesInternal therefore runs against an uninitialized scan state.

The native RelTable::initScanState reads from the bound-node nodeIDVector to pick a node group, so it must run after a child tuple is in flight. External rel table backends (ForeignRelTable, ArrowRelTable, IceDiskRelTable) only use initScanState to set up scan-function / shared / local state and source/nodeGroupIdx — they don't need a bound node — but the first scan() still fires before any re-init call site, so we'd dereference uninitialized state and crash.

Fix

Detect "external" via typeid(*tableInfo.table) != typeid(RelTable) and call table->initScanState once after creating the per-thread scan state, so the first scan() is well-defined. This scales to any future external backend (XxxRelTable : RelTable) without re-enumerating.

Native RelTable behavior is unchanged: first scan() still returns false (source == NONE), then fetchNextBoundNodeBatch re-inits with the child tuple in flight.

Companion change

The pg_client extension submodule was updated to restore RelGroupCatalogEntry registration for rel_* tables (so the planner has a real rel entry to anchor MATCH on) and to add a mutex around the morsel currentOffset in tableFunc (so parallel HashJoinBuild workers atomically claim rows). That lives in the extensions repo on the pg_client branch.

Tests

Verified with the pg_client extension's Python test suite (13/13 passing, including new test_06b_match_rel_table / test_06c_match_rel_count_parallel) and the e2e runner (new MatchOnRelTable case). The push-down optimizer (ForeignJoinPushDownOptimizer) correctly lowers MATCH ... RETURN count(*) to a single COUNT_REL_TABLE operator on rel_knows.

ScanRelTable::initLocalStateInternal creates the per-thread
ForeignRelTableScanState (or ArrowRelTableScanState /
IceDiskRelTableScanState) but only calls table->initScanState in
fetchNextBoundNodeBatch() and after pulling a new child tuple. The
first scan() in getNextTuplesInternal therefore runs against an
uninitialized scan state.

The native RelTable::initScanState reads from the bound-node
nodeIDVector to pick a node group, so it must run after a child tuple
is in flight. External rel table backends, however, only use
initScanState to set up scan-function / shared / local state and
source/nodeGroupIdx — they don't need a bound node — but the first
scan() still fires before any re-init call site, so we'd dereference
uninitialized state and crash. ForeignRelTable::scanInternal calls
tableFunc immediately, which on a null shared state throws or, on
the old code, segfaults.

This manifested as a regression introduced by the pg_client extension
moving rel table registration to RelGroupCatalogEntry + ForeignRelTable:
MATCH (a:node_person)-[k:rel_knows]->(b:node_person) crashed with
'tableFunc: sharedState is null' on the first scan.

Detect 'external' by checking the dynamic type isn't the native
RelTable via typeid, so any future external backend (XxxRelTable :
RelTable) is handled automatically without re-enumerating here.

Native RelTable behavior is unchanged: first scan() still returns
false (source == NONE), then fetchNextBoundNodeBatch re-inits with
the child tuple in flight.
@adsharma
adsharma merged commit c1dcef7 into main Jul 25, 2026
4 checks passed
@adsharma
adsharma deleted the pg_client branch July 25, 2026 03:19
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.

1 participant