Skip to content

Commit faa2e8f

Browse files
dylanbstoreyDylan Bobby Storey
andauthored
quantifier: MATERIALIZE non-deterministic CTEs + entity-in-list property access (+18) (#90)
* sql: MATERIALIZE non-deterministic CTEs so rand() WITH values are stable (+18 TCK) A WITH-projected value computed with rand() (e.g. `WITH CASE WHEN rand()<0.5 THEN reverse(list) ELSE list END + x AS list`) changed on every reference. SQLite treats a multiply-referenced CTE as a view and re-evaluates its body (re-running RANDOM()) per reference, so in a chain of WITH CTEs the same `list` yielded a DIFFERENT random value at each use. The quantifier algebraic-identity scenarios then broke: `none(x IN list WHERE p)`, `any(x IN list WHERE p)`, and `NOT any(...)` over the SAME list each saw a different random list, so e.g. `none(...) = (NOT any(...))` was false. sql_cte() and sql_pre_cte() now emit `AS MATERIALIZED (...)` when the CTE body contains a non-deterministic function (gated on the substring `RANDOM(`), forcing SQLite to compute the CTE once. Gated to non-recursive CTEs only (MATERIALIZED is invalid on a recursive CTE) and only when RANDOM() is present, so recursive/varlen and ordinary deterministic CTEs are unchanged. Rigorous full pass-set diff: zero regressions, +18 (Quantifier9/11/12 identity cluster + Quantifier1-4 related). 3758 -> 3776. Unit 944/944, functional clean. * expr: property access on an entity inside a list reads .properties (groundwork) `r.name` where r is a node/relationship that is a LIST ELEMENT (e.g. the quantifier variable in `any(r IN relationships(p) WHERE r.name = 'a')`) returned null: the projected-JSON property branch used top-level `json_extract(value, '$.name')`, but an entity stores props under `.properties`. It now routes through `_gql_dyn_prop`, which picks `$.properties.<key>` for entity-shaped objects and `$.<key>` for plain maps (same UDF already used for subscript bases). Correct + regression-free (rigorous full pass-set diff: zero regressions, zero newly-passing). Groundwork: the Quantifier1-4 [8]/[9] scenarios additionally require varlen relationships(p)/nodes(p) through an aggregating WITH and GROUP-BY-on-list, deferred. --------- Co-authored-by: Dylan Bobby Storey <dstorey@dstorey-personal-m3.local>
1 parent fca8931 commit faa2e8f

3 files changed

Lines changed: 45 additions & 3 deletions

File tree

docs/testing/semantic-coverage-matrix.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,3 +624,35 @@ transition elapsed time), Temporal3 [10] / Temporal2 [6] (offset resolution on a
624624
DST-transition date). `named_tz_offset`'s month approximation is load-bearing
625625
(an accurate last-Sunday-rule swap regressed -29), so DST needs a careful,
626626
empirical, per-zone effort + across-transition interval math. Deferred.
627+
628+
## Coverage update (2026-06-01) — MATERIALIZE non-deterministic WITH/pre CTEs (Quantifier +18)
629+
630+
`sql_builder.c`. Verified via the TCK harness (3758 -> 3776, rigorous full
631+
pass-set diff: zero regressions, +18; unit 944/944; functional clean):
632+
633+
- **A WITH-projected value built with `rand()` is now stable across references.**
634+
SQLite treats a multiply-referenced CTE as a view and re-evaluates its body per
635+
reference, so a `rand()`-derived list got a DIFFERENT value at each use — e.g.
636+
`none(x IN list WHERE p)` and `any(x IN list WHERE p)` over the same `list` saw
637+
different random lists, breaking algebraic identities and yielding inconsistent
638+
rows. `sql_cte`/`sql_pre_cte` now emit `AS MATERIALIZED (...)` when the CTE body
639+
calls a non-deterministic function (detected by `RANDOM(`), forcing single
640+
evaluation. Gated on `RANDOM(` and non-recursive only, so recursive/varlen and
641+
ordinary CTEs are untouched. Fixes Quantifier9/11/12 algebraic-identity cluster
642+
and related (+18 across Quantifier1-12).
643+
644+
## Coverage update (2026-06-01) — property access on an entity inside a list (groundwork)
645+
646+
`transform_expr_ops.c`. Verified via the TCK harness (rigorous full pass-set diff:
647+
zero regressions; unit 944/944; functional clean):
648+
649+
- **`r.name` on a node/relationship that is a list element now reads `.properties`.**
650+
A projected/list-element JSON value (e.g. the quantifier variable in
651+
`any(r IN relationships(p) WHERE r.name = 'a')`) used top-level
652+
`json_extract($.name)`, which is null for an entity (its props live under
653+
`.properties`). The projected-JSON property branch now routes through
654+
`_gql_dyn_prop`, which picks `$.properties.<key>` for entity-shaped objects and
655+
`$.<key>` for plain maps. Correct standalone (the entity-in-list quantifier now
656+
evaluates), though the Quantifier1-4 [8]/[9] scenarios additionally need varlen
657+
`relationships(p)`/`nodes(p)` through an aggregating WITH + GROUP-BY-on-list
658+
(deeper, deferred).

src/backend/transform/sql_builder.c

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,15 @@ void sql_cte(sql_builder *b, const char *name, const char *query, bool recursive
537537
dbuf_append(&b->cte, ", ");
538538
}
539539

540-
dbuf_appendf(&b->cte, "%s AS (%s)", name, query);
540+
/* A CTE whose body calls a non-deterministic function (rand() ->
541+
* RANDOM()) must be MATERIALIZED: SQLite otherwise treats a multiply-
542+
* referenced CTE as a view and re-evaluates RANDOM() per reference, so a
543+
* WITH-projected random list yields a DIFFERENT value at each use (e.g.
544+
* none()/any() over it see different lists — Quantifier9/11/12). Gating on
545+
* RANDOM() keeps recursive/UNWIND CTEs untouched. Non-recursive only
546+
* (MATERIALIZED is invalid on a recursive CTE). */
547+
bool materialize = !recursive && strstr(query, "RANDOM(") != NULL;
548+
dbuf_appendf(&b->cte, "%s AS %s(%s)", name, materialize ? "MATERIALIZED " : "", query);
541549
b->cte_count++;
542550
}
543551

@@ -555,7 +563,9 @@ void sql_pre_cte(sql_builder *b, const char *name, const char *query, bool recur
555563
if (b->pre_cte_count > 0) {
556564
dbuf_append(&b->pre_cte, ", ");
557565
}
558-
dbuf_appendf(&b->pre_cte, "%s AS (%s)", name, query);
566+
/* MATERIALIZED when the body is non-deterministic — see sql_cte(). */
567+
bool materialize = !recursive && strstr(query, "RANDOM(") != NULL;
568+
dbuf_appendf(&b->pre_cte, "%s AS %s(%s)", name, materialize ? "MATERIALIZED " : "", query);
559569
b->pre_cte_count++;
560570
if (recursive) b->pre_cte_recursive = true;
561571
}

src/backend/transform/transform_expr_ops.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,7 @@ int transform_property_access(cypher_transform_context *ctx, cypher_property *pr
803803
* parser. (Temporal5 [7].) */
804804
append_sql(ctx,
805805
"CASE WHEN json_valid(%s) AND instr(%s, '\"_iso8601\"') > 0 THEN _gql_duration_field(%s, '%s')"
806-
" WHEN json_valid(%s) THEN json_extract(%s, '$.%s')"
806+
" WHEN json_valid(%s) THEN _gql_dyn_prop(%s, '%s')"
807807
" ELSE _gql_temporal_field(%s, '%s') END",
808808
alias, alias, alias, pn,
809809
alias, alias, pn,

0 commit comments

Comments
 (0)