Repository navigation
Refuse an aggregate in a WHERE, and one beside a variable no item groups by in a body or an ORDER BY, as drivine4j does, at Cypher spec v0.13.0 - #310
Merged
Conversation
…ups by in a body or an ORDER BY, as drivine4j does, at Cypher spec v0.13.0 Ports drivine4j's #489 and #482 (merged as liberation-data/drivine4j#490), with the spec's liberation-data/drivine-cypher-spec#38. - New check_aggregates (drivine/cypher/sql/aggregates.py), run before compiling beside check_variable_kinds: an aggregate in the WHERE of a MATCH, a WITH, a list or pattern comprehension, a quantifier, a quantified path pattern, EXISTS { } or COUNT { } is refused (Neo4j's 42I24). It reads every clause, UNION branches, UNWIND, SET, MERGE's ON CREATE and ON MATCH, CREATE, FOREACH and nested CALLs and subqueries among them, but not a subquery's own projection. - _require_explicit_grouping also reads an aggregating item's comprehension, quantifier, reduce and pattern bodies, and, after items that aggregate, an aggregating ORDER BY term (Neo4j's 42I18). It matches whole key expressions, so a body naming a grouping key or a key property compiles; a map projection of a renamed key reads the key, and a renamed key in an order term's pattern is refused. - drivine4j's CompilerRefusalTest and CypherQueryContract cases are ported. Fixes #306 Fixes #307
This was referenced Oct 31, 2026
…he clauses the aggregate check walks
This was referenced Oct 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ports drivine4j's aggregation refusals, #489 and #482, merged together as liberation-data/drivine4j#490. Also syncs the Cypher spec from v0.12.0 to v0.13.0 (liberation-data/drivine-cypher-spec#38).
Fixes #306
Fixes #307
#306: an aggregate in a WHERE (Neo4j's 42I24)
New
check_aggregatesindrivine/cypher/sql/aggregates.py, a port of drivine4j'sAggregates. It runs before compiling, besidecheck_variable_kinds, and reads the statement alone, so a statement that compiled before compiles to the same SQL. It refuses an aggregate in the WHERE of:EXISTS { … }orCOUNT { … }.It looks in every clause: each UNION branch, UNWIND, SET, MERGE's ON CREATE and ON MATCH, CREATE's properties, FOREACH, and nested CALLs and subqueries. It doesn't read a subquery's own projection, so all of these still compile:
COUNT { MATCH … WITH count(m) AS c WHERE c > 1 RETURN c }CALL (n) { … WITH count(m) AS c WHERE c > 0 RETURN c }WITH count(*) AS c WHERE c > 1is_aggregateandchildrenare passed in, as in drivine4j, so neither module imports the other.#307: an implicit grouping key in a body or an ORDER BY (Neo4j's 42I18)
_require_explicit_grouping(items, order_by, scope)keeps its old check. It now also reads:reduce()and pattern bodies (_named_in_bodies);_named_outside).It matches whole key expressions, not just variables:
RETURN x, [y IN collect(x) | y + x]compiles, because x is a key.RETURN p.name, size([x IN collect(r) WHERE x.role <> p.name])compiles.RETURN x.a AS a, count(*) ORDER BY x.a + count(*)compiles.After a rename (
RETURN p AS q, count(*) …):p {.born}.bornreads the key, and compiles;pinside a pattern (size([(p)-->(k) | k]),EXISTS { (p)-->() }) is refused. Neo4j reads thatpas a new variable that matches any node, and the spec refuses it on SQL too.Tests
tests/unit/test_cypher_refusals.pygets drivine4j's twoCompilerRefusalTestcases on Postgres and Oracle. One has 21 refusals and 6 statements that must compile; the other has 17 refusals and 16 that must compile. Before the fix, all 76 refusal tests (38 × 2 stores) failed, and the 44 compile tests passed.tests/integration/test_cypher_compiled.pygets drivine4j'sCypherQueryContractcase "an aggregate beside a WHERE, a grouping key's body or an order term that Neo4j takes gives Neo4j's rows".scripts/unasync.pycopies it to the sync tests.scripts/unasync.py --check, and the full pytest withORACLE_TESTS=true: 5840 passed, 298 skipped.SQL comparison
I compiled 4,179 statements on Postgres and on Oracle, before (main at 1b3ffe7) and after. The statements were:
CompilerRefusalTest.kt.SQL, prelude and parameters are compared. No statement that compiled before gives different SQL. 42 distinct statements changed, on both stores:
Each is a statement from refusals.yaml at v0.13.0 or from drivine4j's refusal lists, which Neo4j refuses. There are three exceptions, all forms of
RETURN p AS q, count(*) … ORDER BYwithpin a pattern. Neo4j runs those, readingpas a new variable. drivine4j and the spec'sonly: [sql]step refuse them on purpose.tests/golden/statements.jsonis unchanged, and all 164 golden compilations (82 statements × 2 stores) are the same before and after.Differences from drivine4j
Cross-library: drivine4j has the fix. Drivine for TypeScript tracks it as liberation-data/drivine#487 and #488, which are being ported now.