Skip to content

Read the variables a CALL imports apart from its aggregate, so sum() and collect() of one aren't 0 and [] on Oracle and count() of one compiles on Postgres - #278

Merged
jasperblues merged 3 commits into
mainfrom
fix/call-aggregate-imports
Oct 29, 2026
Merged

jasperblues merged 3 commits into
mainfrom
fix/call-aggregate-imports

Conversation

@jasperblues

Copy link
Copy Markdown
Member

Fixes #253. Port of liberation-data/drivine4j#406 (liberation-data/drivine4j#368).

The bug. An aggregate inside CALL { } of a variable the call imports went wrong:

  • Oracle: the import read as null, so sum() gave 0 and collect() gave []. Oracle 23 reads a JSON_TABLE outer column as null inside a lateral aggregate that is joined to another source.
  • Postgres: CALL (o) { … count(o) } failed with "aggregate functions are not allowed in FROM clause of their own query level".

The fix. A new _imports_apart, called from _called_part (drivine/cypher/sql/sql_cypher_compiler.py). When a WITH or the RETURN in the call holds an aggregate, the row's imported variables are read first into a query of no table, kept apart by bound_select (NO_MERGE on Oracle, OFFSET 0 on Postgres). That query is the call's first source, and the call binds the variables from it as its own.

Two cases are left out:

  • names the call binds itself: an UNWIND variable, or what a CALL within it returns or yields;
  • a call whose every WITH, and its RETURN, is *.

The code and its doc comment match drivine4j's. As _match does, the columns are written with self._identities, so the UNION recompile without identities applies to them too.

SQL. Every statement of the spec (v0.6.19) was compiled on Postgres and Oracle before and after the change. Only one statement changes on each store: MATCH (p:Person) CALL { WITH p MATCH (p)-[:ACTED_IN]->(m:Movie) RETURN count(m) AS movies } … (queries.yaml, "CALL subqueries", step 1). That is the same statement drivine4j's change touched, and it now reads p from (SELECT /*+ NO_MERGE */ n1.nid AS v2_nid, …) i3 on Oracle and from (SELECT … OFFSET 0) i3 on Postgres. The goldens are unchanged.

Tests. drivine4j's cross-engine "an aggregate in a CALL reads the variables the CALL imports" is ported with Neo4j's rows, in tests/integration/test_cypher_compiled.py (and its unasync copy in tests/sync_integration), test_an_aggregate_in_a_call_reads_the_variables_the_call_imports. It runs on Neo4j, Postgres and Oracle, async and sync and covers:

  • the WITH and CALL (o) forms;
  • sum, collect, count and max;
  • grouping, including by a key that holds a parameter;
  • an imported node property;
  • OPTIONAL CALL;
  • null, map and relationship imports;
  • two imports;
  • a nested importing CALL;
  • a CALL after a write;
  • the two RETURN * cases;
  • a UNION inside the CALL;
  • the UNWIND-shadow exclusion.

Gate. scripts/check_cypher_spec.py (v0.6.19), ruff check, ruff format --check, pyright, scripts/unasync.py --check, and ORACLE_TESTS=true uv run pytest -q (FalkorDB 6.0.3) all pass: 5,161 passed, 298 skipped. After the run, oracledb's pool threads print DPY-4011 tracebacks as the Oracle container stops; they come after the summary and don't fail it.

Drivine for TypeScript's port is for liberation-data/drivine#429.

…and collect() of one aren't 0 and [] on Oracle and count() of one compiles on Postgres

Where a WITH or the RETURN in a CALL { } aggregates, the row's variables are read first into a query of no table,
kept apart by bound_select (NO_MERGE on Oracle, OFFSET 0 on Postgres), made the call's first source, and bound from
there as the call's own. Names the call binds itself (an UNWIND variable, what a CALL within it returns) are left out,
and so is a call whose WITHs and RETURN are all *. Port of liberation-data/drivine4j#406
(liberation-data/drivine4j#368).

Fixes #253
…sion and a CASE, and a renamed, boolean and path import
@jasperblues
jasperblues merged commit 9293c3d into main Oct 29, 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

Development

Successfully merging this pull request may close these issues.

Oracle: an aggregate inside CALL { } reads a variable the CALL imports as null, so sum() gives 0 and collect() gives []; Postgres refuses count() of one

1 participant