Skip to content

feat(jdbc): single edge→target JOIN for unbounded g.V().out().has() (stacked on #172) - #173

Open
seanbarzilay wants to merge 1 commit into
feat/unbounded-adjacency-scanfrom
feat/unbounded-adjacency-join
Open

seanbarzilay wants to merge 1 commit into
feat/unbounded-adjacency-scanfrom
feat/unbounded-adjacency-join

Conversation

@seanbarzilay

Copy link
Copy Markdown
Member

Stacked on #172 (feat/unbounded-adjacency-scan) — review/merge that first; this PR's diff is only the delta.

What

#172 eliminates the source-vertex scan by rewriting unbounded g.V().out(L).has(k,v) to g.E().hasLabel(L).inV().has(k,v) — an edge scan plus a separate deferred target fetch (two query phases). This PR collapses that into one SQL JOIN when the backend supports it:

select v.* from edges e join <target> v on e.<endpoint> = v.id where <target has()>

DB-side filtering, a single round-trip, and only matching rows returned/materialized (vs. reading every edge into the app first).

How

  • SearchVertexQuery.allSources — "match all sources", i.e. no source-id bound. Distinct from an empty vertex list, which aborts (= match none).
  • RowEdgeSchema.getJoinSearch / RowController fallback honour allSources (edge-label predicates only, never abort), reusing the perf(jdbc): adjacency join push-down for vertex-return hops (out/in/both + filter/order/limit) #167 join push-down.
  • SupportsUnboundedAdjacency marks RowController as allSources-capable. The strategy swaps in the JOIN start step only when a controller advertises it; every other backend keeps feat(core): skip source-vertex scan for unbounded g.V().out()/in()/both() via edge-scan rewrite #172's backend-agnostic g.E().<adjacency>() rewrite — no ES regression.
  • UniGraphUnboundedAdjacencyStep — a start step implementing Orderable / ReceivesPredicatesHolder / PropertyFetcher, so the order/limit/has()/property strategies fold into it exactly as they do for UniGraphVertexStep. Handles both the JOIN result and a deferred fallback (inner edge schemas, where searchJoin declines) that hydrates+filters neighbours and preserves duplicate ids (both()).

Two latent bugs surfaced + fixed

Both were only reachable once an adjacency query can be the first query in a traversal — which this start step makes possible:

  1. RowController.search(SearchVertexQuery) never flushed the write buffer before searchJoin's direct fetch (only the generic search() flushes). A first-query join read stale data — this caused 56 feature-suite failures until fixed. Now flushes up front.
  2. UniGraphPropertiesStrategy GroupCountStep branch dereferences propertyFetchers without the null-guard its sibling branches have. Worked around by making the step a PropertyFetcher (so the lookup is non-null); the unguarded line is noted for a follow-up hardening.

Testing

  • JdbcUnboundedAdjacencyTest (11): single-join-not-two-queries, multi-hop chaining from the join step, out/in/both, has() push-down, guards, flag gate.
  • JdbcJoinPushdownTest (11), JdbcAdjacencyFilterTest (9): unchanged.
  • Full feature + structure suites run with the flag ON produce a failing set byte-identical to flag-off and to master (65 failures, same names) — verified by diffing failure names. Zero regressions, zero new errors.

Follow-ups

  • Null-guard UniGraphPropertiesStrategy GroupCountStep branch (independent latent bug).
  • outE()/inE()/bothE() and an ES allSources path.

🤖 Generated with Claude Code

…et JOIN

Builds on the unbounded-adjacency rewrite: when a search controller can honour an
unbounded-source query, replace the whole hop with a single start step that issues
one SearchVertexQuery with no source-id bound, so JDBC resolves it as
  select v.* from edges e join <target> v on e.<endpoint> = v.id where <target has()>
instead of an edge scan + a separate deferred target fetch. DB-side filtering, one
round-trip, only matching rows returned.

Mechanism:
- SearchVertexQuery.allSources: "match all sources" (no id bound), distinct from an
  empty vertex list (which aborts = "none").
- RowEdgeSchema.getJoinSearch / RowController fallback honour allSources (edge-label
  predicates only, never abort).
- SupportsUnboundedAdjacency marks RowController as allSources-capable. The strategy
  uses the JOIN start step only when a controller advertises it; every other backend
  keeps the backend-agnostic g.E().<adjacency>() rewrite -- no ES regression.
- UniGraphUnboundedAdjacencyStep: start step, implements Orderable / ReceivesPredicates
  Holder / PropertyFetcher so order/limit/has()/property strategies fold into it exactly
  as they do for UniGraphVertexStep. Handles the JOIN result and a deferred fallback
  (inner edge schemas, where searchJoin declines) that hydrates+filters neighbours,
  preserving duplicate ids (both()).

Two pre-existing latent bugs surfaced and fixed (both only reachable once an adjacency
query can be the FIRST query in a traversal, which this step makes possible):
- RowController.search(SearchVertexQuery) never flushed the pending write buffer before
  searchJoin's direct fetch; a first-query join read stale data. Now flushes like the
  generic search() path.
- (worked around) UniGraphPropertiesStrategy.GroupCountStep branch dereferences
  propertyFetchers without the null-guard the sibling branches have; making the step a
  PropertyFetcher keeps it non-null.

Verified: JdbcUnboundedAdjacencyTest (11) incl. single-join-not-two-queries and
multi-hop chaining; JdbcJoinPushdownTest (11) and JdbcAdjacencyFilterTest (9) unchanged.
Full feature+structure suites run with the flag ON produce a failing set byte-identical
to flag-off and to master (65 failures, same names) -- zero regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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