Skip to content

TCK batch: exists + SET-entity + NaN + ORDER BY comparator + Any-type funcs (+18) - #87

Merged
dylanbstorey merged 9 commits into
mainfrom
i0339-next6
May 30, 2026
Merged

TCK batch: exists + SET-entity + NaN + ORDER BY comparator + Any-type funcs (+18)#87
dylanbstorey merged 9 commits into
mainfrom
i0339-next6

Conversation

@dylanbstorey

@dylanbstorey dylanbstorey commented May 29, 2026

Copy link
Copy Markdown
Contributor

A batch of independent openCypher TCK conformance fixes, stacked on one branch for a single squash-merge. 3710 → 3728 (+18 net), zero regressions — every fix verified with a rigorous full pass-set diff.

Fixes (TCK delta)

  1. +2 exists: existential subquery brace form with inner WHERE (ExistentialSubquery1 [2]/[4])
  2. +2 set: bulk SET r = a copies entity properties (Merge6 [6], Merge7 [4])
  3. +7 expr: NaN constant comparison semantics for 0.0/0.0 (Comparison1 [8], Comparison2 [5])
  4. +0 unwind: splice MATCH FROM into UNWIND of an entity-containing list (crash → correct; prereq for gqlite bug? #5)
  5. Cross-type total-ordering ORDER BY comparator (GQLITE-T-0340), +4:
    • +0 A orderability type-rank: _gql_order_rank UDF + two-key ORDER BY → map<node<rel<list<path<string<bool<number<NaN<null
    • +1 B renderable NaN value (sentinel string; float-NaN agtype; bare NaN) — WithOrderBy1 [22]
    • +3 C path-as-list-element hydration (emit_hydrated_path flag) — ReturnOrderBy1 [11]/[12], WithOrderBy1 [21]
  6. +3 func: labels()/type()/keys() accept type Any via _gql_labels/_gql_type UDFs that raise a runtime TypeError on invalid values (Graph3 [6], Graph4 [5], Map3 [2]; Graph3 [9] still correctly errors)

Verification

Unit 944/944, functional clean throughout. Two struct fields added → angreal dev clean. No bison conflicts. Each fix has a rigorous per-fix pass-set diff showing zero regressions.

Design + findings for the comparator stack recorded in Metis GQLITE-T-0340.

Deferred (each a larger feature, surveyed): smallest-int literal (-INT64_MIN, needs scanner/token/AST flag), boolean type preservation through params/lists (List1 [9], TypeConversion4 [5]), varlen path-length tracking (Path3 [1]), full/nested existential subqueries, multi-row MATCH+MERGE, WITH-WHERE input-scope (Comparison2 [3]).

Dylan Bobby Storey added 2 commits May 29, 2026 07:46
`MATCH (n) WHERE exists { (n)-->(m) WHERE n.prop = m.prop } RETURN n`
(ExistentialSubquery1 [2]/[4]) needs the brace form to (a) parse an inner
WHERE and (b) allow fresh inner pattern variables (`m`, `r`).

- cypher_exists_expr gains `where_clause` (inner predicate) and
  `is_subquery` (brace vs paren pattern-predicate). Grammar rule
  `EXISTS '{' pattern_list WHERE expr '}'` populates both.
- The EXISTS_TYPE_PATTERN emitter now registers the inner pattern's NEW
  node/rel variables against their subquery aliases (n%d / e%d) before
  transforming the inner WHERE, folds it in as ` AND (<expr>)`, then
  transform_var_truncate_to restores the outer scope (mirrors the
  pattern-comprehension save/restore).
- The WHERE fresh-variable validator skips is_subquery EXISTS nodes: the
  brace form legitimately scopes fresh vars; the paren pattern-predicate
  keeps the stricter rule.

Struct field added -> angreal dev clean. No bison conflicts (still
`%expect 15` / `%expect-rr 3`).

Fixes ExistentialSubquery1 [2] and [4] (all of ExistentialSubquery1 now
green). Zero TCK regressions. 3710 -> 3712. Unit 944/944, functional clean.
`MERGE (a)-[r:TYPE]->(b) ON CREATE SET r = a` (Merge6 [6]) and the ON MATCH
variant (Merge7 [4]) copy all of node `a`'s properties onto rel `r`. The
bulk-SET handler only accepted a map literal or JSON parameter as the RHS and
errored ("Bulk SET value must be a map literal or parameter") on an entity
identifier, so the copy silently produced no properties.

Added `copy_entity_properties()`: reads the source entity's five
property-type tables (text/int/real/bool/json) joined to property_keys and
re-sets each on the destination via cypher_schema_set_{node,edge}_property,
incrementing result->properties_set. Wired into the bulk-SET path as a new
RHS case (AST_NODE_IDENTIFIER); replace mode (`=`) reuses the existing
delete-all-first step, `+=` merges.

Fixes Merge6 [6] and Merge7 [4]. Merge8 [1] / Merge9 [3] still fail on the
separate multi-row MATCH+MERGE cartesian-iteration gap (deferred). Zero TCK
regressions. 3712 -> 3714. Unit 944/944, functional clean.
@dylanbstorey dylanbstorey changed the title exists: existential subquery brace form with inner WHERE (+2 TCK) exists: brace-form existential subqueries (inner WHERE) + set: bulk SET from entity (+4 TCK) May 29, 2026
SQLite collapses float division-by-zero to NULL at the operator level, so a
NaN value can neither survive as a native double nor be distinguished from
null at runtime. Every NaN TCK scenario produces NaN via the literal constant
`0.0 / 0.0`, so detect that shape at compile time and emit the correct
Cypher comparison result directly.

is_nan_const() recognizes `DIV(0-lit, 0-lit)`. classify_nan_other() buckets
the other operand (number / string / null / non-null / unknown). For a NaN
operand the comparison emits a raw SQL truth value (1/0/NULL — the same shape
a native comparison yields, so an enclosing _gql_bool_str(CASE WHEN ...) or
WHERE filter evaluates it correctly; a tagged 'true'/'false' text would be
re-read as falsy):
  NaN = x   -> false   (x non-null; vs null -> null)
  NaN <> x  -> true    (x non-null; vs null -> null)
  NaN </<=/>/>= number-or-NaN -> false; vs other type -> null

Falls through untouched when the other operand is not a compile-time literal
(so non-NaN comparisons are unaffected — verified by a rigorous full pass-set
diff: zero regressions, exactly 7 newly passing).

Fixes Comparison1 [8] (4 examples) and Comparison2 [5] (3 examples). NaN via
a variable (ReturnOrderBy1 [11]/[12], Comparison2 [3]) needs the cross-type
total-ordering comparator and is deferred. 3714 -> 3721. Unit 944/944,
functional clean.
@dylanbstorey dylanbstorey changed the title exists: brace-form existential subqueries (inner WHERE) + set: bulk SET from entity (+4 TCK) TCK batch: exists inner-WHERE subqueries + SET-from-entity + NaN comparisons (+11) May 29, 2026
Dylan Bobby Storey added 5 commits May 29, 2026 14:18
…ectness, +0 TCK)

`MATCH p = (n)-[r]->() UNWIND [n, r, p, ...] AS x` crashed with
`no such column: _gql_default_alias_0.id`. The UNWIND LIST branch emitted a
per-UNION-arm FROM clause only when a WITH projection was being carried
(has_carry); pre-WITH MATCH node/edge variables are deliberately excluded from
carry (their alias is a table alias, not an id column ref), so a list whose
elements reference bound entities produced arms like
`SELECT json_object('id', _gql_default_alias_0.id, ...) AS value` with no FROM
— the aliases were unbound.

The LIST branch now splices the prior MATCH's FROM tables (and re-attaches its
WHERE) into each UNION arm when inner_sql is a splicable `SELECT * FROM ...`,
mirroring the existing function-call/subscript/binary-op branch. Element
expressions keep referencing the original aliases, which are now in scope, and
cardinality correctly tracks the surrounding MATCH.

Prerequisite for the ORDER-BY total-ordering scenarios (ReturnOrderBy1
[11]/[12], WithOrderBy1 [21]/[22]): they no longer crash (error -> fail) but
still need a Cypher orderability key in _gql_order_key, a distinguishable NaN
value, and path-through-UNWIND hydration — all deferred.

Rigorous full pass-set diff vs prior HEAD: zero regressions, zero newly
passing (the 4 scenarios move error -> fail). 3721 pass unchanged. Unit
944/944, functional clean.
…ness, +0 TCK)

ORDER BY over heterogeneous values used SQLite's native storage-class order
(null < number < text < blob), which is wrong for Cypher. Cypher orderability
is map < node < rel < list < path < string < bool < number < NaN < null.

New `_gql_order_rank(value)` UDF returns the integer type rank 0..9 (JSON
entity/map/path shapes distinguished by their distinctive keys: nodes&rels ->
path, labels -> node, startNode* -> rel, else map). `sql_order_by`
(sql_builder.c) and the WITH ORDER-BY path (transform_with.c) now emit
`_gql_order_rank(e) <dir>, _gql_order_key(e) <dir>`: the rank groups by type
for the correct cross-type order, and the existing `_gql_order_key` sorts
within each (now homogeneous) rank. test_sql_builder.c assertions updated to
the two-key form.

This is the GQLITE-T-0340 comparator. It is standalone correctness groundwork:
the mixed-type ORDER-BY TCK scenarios (ReturnOrderBy1 [11]/[12], WithOrderBy1
[21]/[22]) additionally require a renderable NaN value and path-through-UNWIND
hydration, both deferred (tracked in GQLITE-T-0340). The rank UDF already
detects the planned NaN sentinel (rank 8) for forward-compat.

Rigorous full pass-set diff: zero regressions, zero newly passing. Mixed-type
ORDER BY now verifiably sorts map<node<rel<list<string<bool<number. Unit
944/944, functional clean. 3721 pass unchanged.
…ure B)

SQLite collapses float division-by-zero to NULL and drops result subtypes
across CTE boundaries, so a runtime NaN can neither survive as a native double
nor be distinguished from null. Carry NaN as the private string
GQL_NAN_SENTINEL (0x01 'N' 'a' 'N') — recognized by content, collision-proof
(the leading control byte can't begin a real Cypher string).

- transform_expr_ops.c: standalone `0.0/0.0` emits `(CHAR(1) || 'NaN')`
  (the comparison-operand case is still folded at compile time by the earlier
  is_cmp NaN block, so this only fires for non-comparison NaN constants).
- executor_match.c create_property_agtype_value: map the sentinel to a float
  NaN agtype so entity/agtype result rendering emits the bare token.
- agtype.c AGTV_FLOAT serializer: render isnan() as `NaN` (not "nan").
- extension.c plain formatter: print the sentinel as `NaN`.

Combined with the orderability rank (sub-feature A), this fixes WithOrderBy1
[22]. ReturnOrderBy1 [11]/[12] and WithOrderBy1 [21] now order correctly and
fail only on path-as-list-element rendering (sub-feature C, deferred).

Rigorous full pass-set diff: zero regressions, +1 (WithOrderBy1 [22]).
3721 -> 3722. Unit 944/944, functional clean.
…340 sub-feature C)

A path variable used as a list element (`UNWIND [n, r, p, ...]`) rendered as
the raw elem_ids array `[1,1,2]` instead of the `{nodes,rels}` path object,
because the executor's elem_ids post-hydration (build_path_from_ids) only
reaches top-level RETURN columns — not a value buried inside an UNWIND row.

Added a transform-context flag `emit_hydrated_path`. When set, the path
projection in transform_expression emits the self-contained fully-hydrated path
JSON (reusing the pattern-comprehension builder: json_object('nodes',
json_array(...), 'rels', json_array(...))) for non-single-varlen paths instead
of elem_ids. transform_unwind sets the flag around each list-element expression
transform and restores it after.

Completes the GQLITE-T-0340 type-ordering stack:
  A (orderability rank, 22b00a7) + B (renderable NaN, d74210e) + C (this).
Mixed-type ORDER BY now fully follows Cypher orderability
map<node<rel<list<path<string<bool<number<NaN<null.

Fixes ReturnOrderBy1 [11]/[12] and WithOrderBy1 [21] (WithOrderBy1 [22] landed
with B). Struct field added -> angreal dev clean. 3722 -> 3725. Unit 944/944,
functional clean.
@dylanbstorey dylanbstorey changed the title TCK batch: exists inner-WHERE subqueries + SET-from-entity + NaN comparisons (+11) TCK batch: exists subqueries + SET-from-entity + NaN + cross-type ORDER BY comparator (+15) May 30, 2026
`labels(list[0])`, `type(list[0])`, `keys($param)` were rejected at
compile time because these functions only accepted a bare node/rel/map
identifier argument. openCypher types a list/subscript element (and a
parameter) as Any, resolved at runtime.

- labels()/type() on a non-identifier now emit new `_gql_labels` / `_gql_type`
  UDFs. They inspect the runtime value: a node/relationship JSON object yields
  its labels/type; null yields null; anything else raises
  `TypeError: InvalidArgumentValue` (sqlite3_result_error, which the harness
  classifies as TypeError). This satisfies both the accept-Any scenarios
  (Graph3 [6], Graph4 [5]) and the fail-on-invalid scenario (Graph3 [9]),
  which a naive json_extract would have regressed.
- keys() on a parameter/expression emits a single-eval subquery over json_each,
  using the value's `properties` object when present (node/rel) else the
  value's own keys (map). Fixes Map3 [2].

Rigorous full pass-set diff: zero regressions, +3. 3725 -> 3728. Unit
944/944, functional clean.
@dylanbstorey dylanbstorey changed the title TCK batch: exists subqueries + SET-from-entity + NaN + cross-type ORDER BY comparator (+15) TCK batch: exists + SET-entity + NaN + ORDER BY comparator + Any-type funcs (+18) May 30, 2026
@dylanbstorey
dylanbstorey merged commit ef26bbd into main May 30, 2026
17 checks passed
@dylanbstorey
dylanbstorey deleted the i0339-next6 branch May 30, 2026 14:12
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.

1 participant