You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TCK batch: exists + SET-entity + NaN + ORDER BY comparator + Any-type funcs (+18) (#87)
* exists: existential subquery brace form with inner WHERE (+2 TCK)
`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.
* set: bulk SET from an entity copies its properties (SET r = a) (+2 TCK)
`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.
* expr: NaN constant comparison semantics for 0.0/0.0 (+7 TCK)
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.
* unwind: splice MATCH FROM into UNWIND of entity-containing list (correctness, +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.
* order: Cypher orderability type-rank as primary ORDER BY key (correctness, +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.
* nan: renderable NaN value for 0.0/0.0 (+1 TCK, GQLITE-T-0340 sub-feature 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.
* path: hydrate path as a list element under UNWIND (+3 TCK, GQLITE-T-0340 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.
* metis: GQLITE-T-0340 type-ordering stack complete (A+B+C, +4 TCK)
* func: labels()/type()/keys() accept type Any (+3 TCK)
`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.
---------
Co-authored-by: Dylan Bobby Storey <dstorey@dstorey-personal-m3.local>
0 commit comments