Skip to content

Cypher/GFQL: introduce a first-class row-carrier IR for multi-stage vectorized row semantics #989

Description

@lmeyerov

Problem

The current local Cypher/GFQL execution model does not have a first-class row-carrier IR.

As a result, row-seeded semantics are handled in feature-specific ways instead of through one reusable contract. The bounded reentry work in PR #975 is the clearest example, but the same architectural pressure will recur for:

  • multi-stage MATCH ... WITH ... MATCH ...
  • multi-alias WITH / RETURN / ORDER BY
  • OPTIONAL MATCH null extension
  • grouped row-preserving aggregation
  • future vectorized GPU/backend audits

Today those semantics are spread across compiler rewrites, projection metadata, hidden columns, and runtime stitching. That is workable for a bounded slice, but it is not a good long-term model for a graph query compiler/runtime.

Why This Matters

  • makes row semantics harder to extend cleanly
  • encourages feature-by-feature protocol glue instead of a reusable IR
  • complicates vectorization reasoning and GPU/backend parity review
  • increases the chance that future Cypher support growth expands lowering.py and gfql_unified.py in ad hoc ways

Proposed Direction

Introduce a first-class row-carrier / seeded-row IR for local Cypher/GFQL execution.

Core ideas:

  1. Represent carried row state explicitly.

    • row ids / seed ids
    • bound aliases
    • carried scalar columns
    • ordering contract
    • null-extension contract where applicable
  2. Lower row-seeded features into that IR instead of feature-specific side channels.

  3. Keep the implementation columnar/vectorized.

    • pandas/cudf-friendly
    • no generic Python row-loop fallback
  4. Let specific features become clients of the same row model.

    • bounded reentry
    • later multi-alias WITH
    • later OPTIONAL MATCH
    • later multiplicity-preserving grouped aggregation

Relationship To Other Issues

Non-Goals

Success Criteria

  • bounded reentry can be expressed as a normal client of the row IR
  • future row-seeded Cypher features stop requiring bespoke hidden-column / metadata handshakes
  • vectorization/backend expectations are clearer to audit
  • lowering.py and runtime orchestration can shrink over time instead of accumulating one-off row mechanics

Context

PR #975 is landing the bounded-reentry feature/hardening slice.
Issue #987 tracks the narrower follow-on cleanup for that implementation.
This issue tracks the broader architectural direction beyond that one feature.

Activity

  1. lmeyerov commented on Mar 31, 2026

    @lmeyerov
    ContributorAuthor

    Related narrower follow-on: #987 tracks the bounded-reentry-specific cleanup that can land independently of the broader row-carrier IR direction in this issue.

  2. lmeyerov commented on May 2, 2026

    @lmeyerov
    ContributorAuthor

    Concrete row-carrier invariants blocking LDBC SNB IC3 / IC5

    Cross-link from #999. While reproducing the multi-stage MATCH ... WITH ... MATCH ... re-entry gaps on master@6891d39e4, the reduced repros did not stop at the documented parser surface gates — they fell through to two row-lowering invariants that sit at the heart of the row-carrier IR design this issue is scoped to:

    1. graphistry/compute/gfql/cypher/lowering.py:7622 — "Cypher MATCH after WITH currently requires the trailing MATCH to start from the same carried node alias." Today's bounded-reentry contract assumes every re-entry MATCH starts from the carried alias. LDBC IC3 explicitly carries (a, x, y) across an intermediate MATCH (city:City)-[:IS_PART_OF]->(country:Country) whose source alias city is not carried — the row carrier needs to forward unrelated bound aliases through stages where they aren't the MATCH source.

    2. graphistry/compute/gfql/cypher/lowering.py:1914 (via _validate_row_expr_scope → _validate_aggregate_expr_scope → _lower_general_row_projection) — "Cypher row lowering currently supports one MATCH source alias at a time." RETURNing from two distinct MATCH source aliases in the same projection is rejected. LDBC IC5 hits this directly: RETURN forum.id, post.id after MATCH chains rooted at different sources.

    These are not parser surface concerns — they are the exact "feature-specific protocol glue" this issue calls out. If ReentryPlan (#987) and the broader row-carrier IR (this issue) get an explicit multi-alias carry contract, both invariants relax naturally and the LDBC SNB IC3/IC5/IC10 family unlocks together.

    #999 is being scoped narrowly to the parser-surface lift (Path A in that thread) so it doesn't drift into a row-carrier rewrite. The architectural exit gate for the SNB multi-stage benchmark family is here.

  3. lmeyerov commented on May 2, 2026

    @lmeyerov
    ContributorAuthor

    Picking this up — Slice 1 scope

    Following up on the earlier comment about LDBC IC3/IC5 invariants landing here: starting active work on this issue under branch feat/989-reentryplan-multi-alias-carry (cut from master@6891d39e4). Plan file under plans/989-reentryplan-multi-alias-carry/.

    Slice 1 scope (this PR)

    • Introduce explicit ReentryPlan dataclass per #987 Step 1 — replaces the tuple returns and side-channel metadata that the bounded-reentry path threads today.
    • Replace the hidden-column handshake (__cypher_reentry_*) with an explicit carried-column contract on the plan (#987 Step 2).
    • Move bounded-reentry runtime stitching out of gfql_unified.py into a focused module (#987 Step 3).
    • Extend the carrier to N whole-row aliases — the IC3 unblock from #999. When prefix WITH projects multiple whole rows (e.g. WITH a, x, y), one becomes the trailing-MATCH source and the rest are carried as scalar bundles addressable downstream as x.id, x.name, etc.
    • IC3-shaped pandas + cuDF tests against an LDBC-mini Person/Country/City fixture.
    • Failfasts for shapes still out of scope.

    Out of scope (Slice 2, follow-on PR)

    • Multi-MATCH-source row projection (lowering.py:1914) — the IC5 unblock.
    • Variable-length relationship reentry under multi-alias carry (overlaps #1009).
    • Generic WITH semantic broadening — preserving the non-goal called out in this issue.

    Coordination note

    If anyone is touching lowering.py _compile_bounded_reentry_query / _bounded_reentry_carry_columns or gfql_unified.py reentry stitching, please ping — that's the surface area being rewritten. Other parts of lowering.py (general row projection, optional match, varlen) are untouched.

  4. lmeyerov commented on May 3, 2026

    @lmeyerov
    ContributorAuthor

    PR #1248 merged — slices 4.1 / 4.3a / 4.3b / 4.3c landed

    Squashed to master as 80d80849c via #1248.

    What landed

    • Slice 4.1 — ReentryPlan IR. New ReentryPlan + CarriedAlias dataclass at graphistry/compute/gfql/cypher/reentry_plan.py, exposed via compiled_query.reentry_plan. Begins replacing the implicit handshake spread across tuple returns from _bounded_reentry_carry_columns, scalar_reentry_alias / scalar_reentry_columns on CompiledCypherExecutionExtras, and _compiled_query_reentry_contract runtime re-extraction. The legacy fields still co-exist — they will be retired in follow-up work under Cypher/GFQL: replace bounded reentry hidden-column handshake with an explicit ReentryPlan #987 (steps 2–5).
    • Slice 4.3a — multi-whole-row admit gate. Lifted the single-whole-row constraint at lowering.py and the matching runtime contract in gfql_unified.py; WITH a, x now admits whenever only the trailing-MATCH source alias is referenced downstream.
    • Slice 4.3b — property carry rewrite. Compile-time prefix rewrite (_rewrite_multi_whole_row_prefix) turns WITH a, x into WITH a, x.id AS __carry_x__id__ for every x.<prop> referenced in trailing clauses; AST-rewrites those references to property access on the reentry-alias's hidden column. Closes the multi-alias case of the #1026 regression-lock as a positive row assertion.
    • Slice 4.3c — bare-item drop. Downstream WITH a, x, y, collect(...) re-projecting carried aliases now drops the bare items at compile time so the bare-ref failfast does not false-positive on forwarding patterns.

    Still open under this issue

    Validation

    • 1097 cypher tests pass (graphistry/tests/compute/gfql/cypher/).
    • 5 review waves converged (0 BLOCKER / 0 IMPORTANT at HEAD).
    • All hosted CI green at merge (tck-gfql pass without override).
  5. lmeyerov commented on May 3, 2026

    @lmeyerov
    ContributorAuthor

    Slice 4.3d (cross-reentry-boundary carry forwarding — the IC3 unblock called out in the post-merge comment for #1248) is now tracked dedicated as #1256 and claimed. This umbrella issue stays open; #1256 is the actionable next slice.

  6. 16 remaining items

  7. lmeyerov commented on May 6, 2026

    @lmeyerov
    ContributorAuthor

    Update: Worker D follow-through cleanup slice is merged via PR #1314 (commit 23e9b5f).

    What landed:

    • free-form reentry plan contract fix (free-form no longer scalar-only tagged)
    • compile-shape regression lock (test_compile_cypher_records_freeform_reentry_plan_contract)
    • changelog entries moved to active Development section during latest rebase

    Validation/quality:

    • review-skill flow converged (waves 1-4)
    • CI green including tck-gfql before merge

    This closes the scoped Worker D cleanup slice for #989; umbrella architecture scope remains tracked here.

  8. lmeyerov commented on May 7, 2026

    @lmeyerov
    ContributorAuthor

    Status update (May 7, 2026):

    Next step on this thread:

    1. Remove temporary docs override once GitHub exposes pull//head consistently and RTD runs normally again.
    2. Re-run RTD to restore native docs status provenance.
  9. added a commit that references this issue on May 7, 2026
  10. lmeyerov commented on May 7, 2026

    @lmeyerov
    ContributorAuthor

    Meta progress update (May 7, 2026):

    Follow-up tracking:

    1. Remove temporary docs status override once GitHub hidden pull refs are healthy.
    2. Re-run RTD build to restore native docs status provenance.
    3. Continue remaining row-carrier IR consolidation work items under this metaissue.
  11. lmeyerov commented on May 7, 2026

    @lmeyerov
    ContributorAuthor

    Child cleanup #1333 has landed via #1335.

    Follow-through slice completed:

    • clarified reentry module boundaries (compile-time vs runtime naming)
    • retained import compatibility shim for reentry.runtime
    • no semantic behavior change intended

    This keeps the post-#1260/#987 reentry split easier to reason about while preserving downstream imports.

  12. added a commit that references this issue on May 7, 2026
  13. lmeyerov commented on May 7, 2026

    @lmeyerov
    ContributorAuthor

    Merged: #1344 (commit a8c9397) landed on master and closes this issue.\n\nLanded scope:\n- explicit row-carrier plan metadata for non-source aliases via \n- compile-time wiring moved on top of master's source-of-truth split\n- compile-contract coverage for carried-property metadata in standard + free-form lanes\n\nValidation receipts in PR #1344: CI green, including .

  14. lmeyerov commented on May 7, 2026

    @lmeyerov
    ContributorAuthor

    Correction/update (clean formatting):

    Merged: PR #1344 (commit a8c9397) landed on master and closes this issue.

    Landed scope:

    • explicit row-carrier plan metadata for non-source aliases via ReentryPlan alias carried_properties
    • compile-time wiring aligned with the reentry/compiletime.py source-of-truth split on master
    • compile-contract coverage for carried-property metadata in both standard and free-form lanes

    Validation receipts in PR #1344: CI green, including tck-gfql.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions