diff --git a/docs/testing/semantic-coverage-matrix.md b/docs/testing/semantic-coverage-matrix.md index 41f3169d..aad5250c 100644 --- a/docs/testing/semantic-coverage-matrix.md +++ b/docs/testing/semantic-coverage-matrix.md @@ -624,3 +624,35 @@ transition elapsed time), Temporal3 [10] / Temporal2 [6] (offset resolution on a DST-transition date). `named_tz_offset`'s month approximation is load-bearing (an accurate last-Sunday-rule swap regressed -29), so DST needs a careful, empirical, per-zone effort + across-transition interval math. Deferred. + +## Coverage update (2026-06-01) — MATERIALIZE non-deterministic WITH/pre CTEs (Quantifier +18) + +`sql_builder.c`. Verified via the TCK harness (3758 -> 3776, rigorous full +pass-set diff: zero regressions, +18; unit 944/944; functional clean): + +- **A WITH-projected value built with `rand()` is now stable across references.** + SQLite treats a multiply-referenced CTE as a view and re-evaluates its body per + reference, so a `rand()`-derived list got a DIFFERENT value at each use — e.g. + `none(x IN list WHERE p)` and `any(x IN list WHERE p)` over the same `list` saw + different random lists, breaking algebraic identities and yielding inconsistent + rows. `sql_cte`/`sql_pre_cte` now emit `AS MATERIALIZED (...)` when the CTE body + calls a non-deterministic function (detected by `RANDOM(`), forcing single + evaluation. Gated on `RANDOM(` and non-recursive only, so recursive/varlen and + ordinary CTEs are untouched. Fixes Quantifier9/11/12 algebraic-identity cluster + and related (+18 across Quantifier1-12). + +## Coverage update (2026-06-01) — property access on an entity inside a list (groundwork) + +`transform_expr_ops.c`. Verified via the TCK harness (rigorous full pass-set diff: +zero regressions; unit 944/944; functional clean): + +- **`r.name` on a node/relationship that is a list element now reads `.properties`.** + A projected/list-element JSON value (e.g. the quantifier variable in + `any(r IN relationships(p) WHERE r.name = 'a')`) used top-level + `json_extract($.name)`, which is null for an entity (its props live under + `.properties`). The projected-JSON property branch now routes through + `_gql_dyn_prop`, which picks `$.properties.` for entity-shaped objects and + `$.` for plain maps. Correct standalone (the entity-in-list quantifier now + evaluates), though the Quantifier1-4 [8]/[9] scenarios additionally need varlen + `relationships(p)`/`nodes(p)` through an aggregating WITH + GROUP-BY-on-list + (deeper, deferred). diff --git a/src/backend/transform/sql_builder.c b/src/backend/transform/sql_builder.c index ff639800..11088548 100644 --- a/src/backend/transform/sql_builder.c +++ b/src/backend/transform/sql_builder.c @@ -537,7 +537,15 @@ void sql_cte(sql_builder *b, const char *name, const char *query, bool recursive dbuf_append(&b->cte, ", "); } - dbuf_appendf(&b->cte, "%s AS (%s)", name, query); + /* A CTE whose body calls a non-deterministic function (rand() -> + * RANDOM()) must be MATERIALIZED: SQLite otherwise treats a multiply- + * referenced CTE as a view and re-evaluates RANDOM() per reference, so a + * WITH-projected random list yields a DIFFERENT value at each use (e.g. + * none()/any() over it see different lists — Quantifier9/11/12). Gating on + * RANDOM() keeps recursive/UNWIND CTEs untouched. Non-recursive only + * (MATERIALIZED is invalid on a recursive CTE). */ + bool materialize = !recursive && strstr(query, "RANDOM(") != NULL; + dbuf_appendf(&b->cte, "%s AS %s(%s)", name, materialize ? "MATERIALIZED " : "", query); b->cte_count++; } @@ -555,7 +563,9 @@ void sql_pre_cte(sql_builder *b, const char *name, const char *query, bool recur if (b->pre_cte_count > 0) { dbuf_append(&b->pre_cte, ", "); } - dbuf_appendf(&b->pre_cte, "%s AS (%s)", name, query); + /* MATERIALIZED when the body is non-deterministic — see sql_cte(). */ + bool materialize = !recursive && strstr(query, "RANDOM(") != NULL; + dbuf_appendf(&b->pre_cte, "%s AS %s(%s)", name, materialize ? "MATERIALIZED " : "", query); b->pre_cte_count++; if (recursive) b->pre_cte_recursive = true; } diff --git a/src/backend/transform/transform_expr_ops.c b/src/backend/transform/transform_expr_ops.c index dbcc97fc..b4f56201 100644 --- a/src/backend/transform/transform_expr_ops.c +++ b/src/backend/transform/transform_expr_ops.c @@ -803,7 +803,7 @@ int transform_property_access(cypher_transform_context *ctx, cypher_property *pr * parser. (Temporal5 [7].) */ append_sql(ctx, "CASE WHEN json_valid(%s) AND instr(%s, '\"_iso8601\"') > 0 THEN _gql_duration_field(%s, '%s')" - " WHEN json_valid(%s) THEN json_extract(%s, '$.%s')" + " WHEN json_valid(%s) THEN _gql_dyn_prop(%s, '%s')" " ELSE _gql_temporal_field(%s, '%s') END", alias, alias, alias, pn, alias, alias, pn,