Skip to content

Pass on a CALL's imports through a leading WITH *, and work out a pattern comprehension that reads only the row per match - #309

Merged
jasperblues merged 2 commits into
mainfrom
call-star-patcomp
Oct 31, 2026
Merged

jasperblues merged 2 commits into
mainfrom
call-star-patcomp

Conversation

@jasperblues

Copy link
Copy Markdown
Member

Ports two merged drivine4j fixes, as Drivine for TypeScript's liberation-data/drivine#490 does, and syncs the Cypher spec from v0.10.0 to v0.12.0 (liberation-data/drivine-cypher-spec#35 and #37).

Fixes #304
Fixes #305

#304: WITH * at the start of a CALL (port of liberation-data/drivine4j#472, merged as liberation-data/drivine4j#484)

  • _called_part now runs a CALL without brackets through _starred_imports. Its leading WITH * (after the per-row UUID UNWIND that _anew_call adds) names each variable the CALL imports in place of the star. The compiler's own _made_anew names are left out. A CALL that imports nothing keeps its clauses as they are.
  • Bracket CALLs (CALL (i), CALL (*)) keep their clauses as they are. Neo4j's star there gives none of the bracket imports, and the query sees them throughout anyway.
  • In _read, a bare WITH * where select.bound holds only _made_anew names returns the rows as they are. If it has a WHERE, that still filters them (_filtered_rows, so a randomUUID() in it is made for each row). With DISTINCT, ORDER BY, SKIP or LIMIT it is still refused ("A projection has at least one item").

#305: a pattern comprehension whose body reads only the row (port of liberation-data/drivine4j#467, merged as liberation-data/drivine4j#488)

  • New variables_read_in: the same as variables_in, but it leaves out names that a comprehension, quantifier or reduce binds itself. It still counts a binder's own list, so [m IN [m.x] | m] reads the outer m. A nested pattern comprehension's or EXISTS's pattern counts in full.
  • _pattern_comprehension: a body that reads none of the pattern's own variables, and reads some variable or holds a subquery, is worked out per match in a derived table: (SELECT agg(pcN.v) FROM (SELECT <body> AS v FROM … WHERE …) pcN). Postgres refused these before ("must appear in the GROUP BY clause"). Literal, parameter and randomUUID() bodies compile as before.
  • As in Drivine for TypeScript, the comprehension's path variable always counts as its own, even where the row holds that name: WITH 1 AS p RETURN [p = ()-->() | length(p)] compiles byte for byte as before. drivine4j takes p there as the row's. The rows are the same either way.

Spec version

Synced to v0.12.0, not v0.13.0. At v0.13.0 the only failures are the refusal cases of liberation-data/drivine-cypher-spec#38, "Aggregate in a WHERE" and "Implicit grouping key in a body or an ORDER BY" (4 refusal tests, on Postgres and on Oracle). Those are drivine4j#489/#482's aggregation refusals, which are #306 and #307 here. v0.13.0 differs from v0.12.0 only by spec #38.

Tests

  • Spec v0.12.0 on main, before the fix: 5 failed. Postgres failed "Pattern comprehension whose body reads only the row" (column "n1.props" must appear in the GROUP BY clause), "CALL imports only what it names" and "RETURN *". Oracle failed the last two of those. Both failed with "A projection has at least one item". With the fix, all 516 spec tests pass on Neo4j, Postgres and Oracle.
  • drivine4j's two CypherQueryCrossEngineTest cases, as Drivine for TypeScript ported them, are added to tests/integration/test_cypher_compiled.py, and to the sync twin through scripts/unasync.py.

SQL comparison

I compiled 4,074 statements on Postgres and on Oracle, before and after (8,148 compilations). The statements were every spec statement at v0.6.19, v0.12.0 and v0.13.0, the golden statements, every Cypher string in this repo's tests, samples and docs, drivine#482's corpus, and every Cypher literal in drivine4j's CypherQueryCrossEngineTest.kt. SQL, prelude and parameters are compared. 8,020 are byte-identical. Each of the other 128 is a targeted shape:

  • 50 (25 statements × 2 stores) were refused and now compile. These are the WITH * shapes.
  • 4 (2 × 2) are still refused, with a different message. Neo4j refuses these too: CALL () { WITH * RETURN i … } and CALL { WITH * RETURN * } RETURN i.
  • 74 (37 × 2) compiled before and now give the derived table. These are pattern comprehensions whose body reads only the row.

tests/golden/statements.json is unchanged, and all 164 golden statements compile byte for byte as before. TCK Create3 #2 and #3 (MATCH () CREATE () WITH * CREATE ()) now compile.

Cross-library: drivine4j has both fixes, and Drivine for TypeScript has them in liberation-data/drivine#490.

…tern comprehension that reads only the row per match, at Cypher spec v0.12.0

Ports drivine4j#472 (#304) and drivine4j#467 (#305), as Drivine for TypeScript's #490 does.

- `_called_part` runs a CALL without brackets through `_starred_imports`: its leading `WITH *` names each variable the
  CALL imports, leaving out the compiler's own per-row UUID names. Bracket CALLs are as they were.
- `_read` leaves out a bare `WITH *` where nothing is bound; its WHERE still filters the rows it reads. DISTINCT,
  ORDER BY, SKIP or LIMIT on it are refused as before.
- `variables_read_in`, and `_pattern_comprehension` works out a body that reads none of the pattern's own variables
  per match in a derived table, so its aggregate is no longer the row query's.
- Cypher spec synced from v0.10.0 to v0.12.0. drivine4j's two cross-engine tests ported, async and sync.
@jasperblues
jasperblues merged commit 1b3ffe7 into main Oct 31, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant