+- **`sum()`/`avg()` over a non-numeric column now raise the same typed error on every engine — polars no longer answers `avg(<string>)` with a silent `null`, and pandas no longer answers `sum(<string>)` with the string CONCATENATION**: the aggregate kernels are written four times (pandas/cuDF row pipeline, native polars row pipeline, the OLAP single-hop grouped fast path, and the fused lazy lane #1823 added in front of it) and each had inherited its host library's opinion about non-numeric input, so the SAME query answered differently depending on the engine: `avg(n.name)` raised on pandas but returned `null` on polars, while `sum(n.name)` returned `'abac'` on pandas and leaked a raw `polars.exceptions.InvalidOperationError` through the GFQL surface. "Match the other engine" was not available, because the two engines were wrong in OPPOSITE directions — so the contract is pinned to Cypher instead. openCypher/Neo4j declares `avg(input)` and `sum(input)` over `INTEGER | FLOAT | DURATION` only (neo4j/docs-cypher `functions/aggregating.adoc`) and enforces it: Neo4j 5.26.26 answers `RETURN avg(r.s)` over strings with *"AVG(...) can only handle numerical values, duration, or null."* and `sum(date(...))` with *"Type mismatch: expected Float, Integer or Duration but was Date"*; Kuzu 0.11.3 rejects both at bind time. A non-numeric input is therefore a **`GFQLTypeError` (E302) naming the aggregate and the user's output alias**, on every engine and on both the row pipeline and the fast path. **This is a deliberate contract change on the pandas engine**: `sum(<string column>)` used to return the concatenation, which is a silent wrong answer with no meaning in Cypher, and it was already inconsistent with `avg()`, which raised. Also brought to the same contract, all found by a full aggregate × dtype differential sweep rather than one at a time: `sum` over temporal and categorical columns (polars returned `null`, pandas raised), `sum`/`avg` over an all-null column (now `0` / `null` per Neo4j's *"`sum(null)` returns `0`"*, substituted rather than delegated — the host kernels answer that case `0`/`''`/`NaT`/`TypeError` depending on dtype, and polars raised), and `min`/`max`/`collect`/`count(DISTINCT)` over a **categorical** column, which Cypher accepts as `ANY` but which pandas rejected outright and cuDF answered with a *category label instead of a count*. A 180-cell polars × cuDF × polars-gpu matrix over 6 aggregates × 10 dtype columns (`_MATRIX_AGGS` × `_MATRIX_COLS`, each compared against the pandas oracle on BOTH the value and the error class) now shows **zero divergences**. Error-path only: the guards read a dtype and do not touch value computation on any served shape.
0 commit comments