ADR 0041: Explicit native Cypher clauses beside EF LINQ¶
Date: 2026-10-12. Decision and FGQ-0 through FGQ-3 accepted within the read-only experimental contract.
The user promotes FUT-23 to implementation. GraphBridge's ordinary LINQ renderer deliberately implements CLR equality and ordering semantics. Reusing that renderer for a surface advertised as Cypher would produce surprising optional-match, null and row-multiplicity behavior.
Decision¶
Add context.Cypher() with opaque typed node/relationship symbols, immutable patterns and an ordered clause representation. This is an explicit native query builder inside the provider package, not another IQueryable provider or entity tracker. The finalized EF model supplies storage labels, properties, converters and endpoint roles. Existing native execution owns sessions, transactions, cancellation, diagnostics and buffering.
Choose explicit predicates, for example actor.Property(p => p.Name).Eq(name), over a Boolean C# lambda pretending to have CLR semantics. The lambda form is shorter but its equality, optional values and short-circuit expectations conflict with native three-valued logic. Return(() => new Dto(actor.Value.Name)) uses expressions only to describe scalar DTO construction; alias Value throws if evaluated on the client. No arbitrary predicate/client method evaluation is accepted.
using Graph = GraphBridge.EntityFrameworkCore.FluentCypher.Cypher;
var actor = Graph.Node<Person>();
var film = Graph.Node<Movie>();
var credit = Graph.Edge<Credit>();
var query = db.Cypher()
.Match(actor.Out(credit, film))
.Where(actor.Property(p => p.Name).Eq(name))
.Return(() => new { film.Value.Title, credit.Value.Role })
.OrderBy(film.Property(p => p.Title))
.Limit(100);
Console.WriteLine(query.ToQueryString());
var rows = await query.ToListAsync(cancellationToken);
Semantic contract¶
- Keep MATCH clauses and patterns exactly where the caller puts them. One MATCH's relationship uniqueness and separate MATCH reuse are server semantics; the compiler does not merge or split patterns.
Whereattaches to the preceding MATCH, OPTIONAL MATCH or WITH. Multiple calls combine with native AND. There is no implicit post-optional filter.- Native null comparisons remain unknown. Use
IsNull/IsNotNullintentionally. Native ascending order puts null last. Comparisons/order operate on mapped store values, including converted properties; no CLR ordering equivalence is promised. Different converted properties cannot be compared unless they are the same EF property binding. - WITH forwards only named symbols and can introduce explicit counts. Grouping uses the forwarded native values. Count rows, count non-null values and count distinct entities/values remain different operations. Dropped outer aliases cannot be rebound. EXISTS has lexical local bindings: predicates can be reused, local aliases cannot be projected or forwarded outside, and an explicit later MATCH can introduce a fresh outer binding for the same API symbol.
- Each alias has an identity independent of CLR type; repeated Person/edge symbols are distinct. Relationship roles must match metadata. Identified-edge payload is stored on the edge. Endpoint foreign keys use the mapped principal key on
startNode(edge)/endNode(edge), with the FK converter. An absent optional edge returns null for both endpoints, even when its source node exists; only the edge needs to remain in scope. - Return only scalar/DTO values. Do not construct mapped entities, attach results, perform identity resolution or add EF uniqueness guards. Native physical row multiplicity and explicit RETURN DISTINCT are preserved. This is not an EF entity-materialization integrity contract.
- OPTIONAL MATCH can yield null even for a required EF property. Nullable value projections must say
(int?)alias.Value.Year; projecting null into a non-nullable value throws. Reference values can be null at runtime; C# nullable annotations are not rewritten. DTO constructors/assignments execute after server filtering, and may fail like native projectors. - Reject mapped EF query filters rather than silently bypassing them. No claim of AutoInclude, tracking or implicit tenant filtering. Use the existing EF LINQ surface when those semantics are needed.
- Ordering initially requires returned scalar expressions, avoiding hidden DISTINCT/grouping changes. Paging order is ORDER BY, SKIP, LIMIT. Execution requires an explicit limit of 1–10,000; row limits do not bound database traversal cost. Query inspection is allowed before choosing the limit.
- Parameters are command snapshots; supported scalar converters run at compilation.
ToQueryString()reveals query structure only.ToCommand().Parametersis deliberately explicit and can contain sensitive values. No fluent compiled-query cache is introduced. - Unsupported shapes fail before driver I/O. No arbitrary raw clause, implicit synchronous execution, write DSL or general full-language promise.
Acceptance and consequences¶
Runtime commit 7bebda6 passes hosted run 38181225124: 746 provider units (28 new fluent cases), 20 live tests (9 fluent plus 11 native/path regressions), and fresh package-only offline/live consumers. C1-C6 pass with native controls; the compiled sample and resource/lifecycle/error gates are accepted. The owned fixture was removed. A subsequent scalar-reader correction at dde143e preserves converter-produced nulls and passes 747 units in unit-only run 38181777149; the earlier live/package checkpoint was not rerun for that correction. See the acceptance map for exact scope and deliberate example-domain adaptations.
This surface is more verbose than Cypher, especially when declaring aliases, but the emitted clauses are predictable and endpoint/property names remain mapped and typed. Keep the initial surface experimental. Shorter predicate syntax, broader projection expressions and nullable symbol types are future ergonomic work only if pilot evidence justifies them; never change native semantics merely to shorten an example.
Review correction, 12 October 2026¶
The architecture review (maintainer-local record) found optional endpoint reads incorrectly depended on node aliases, and EXISTS leaked local bindings into compiler state. Immutable node/edge/value/path bindings now permit safe lexical scope snapshots; generated variable names remain unique across expansions. Clause orchestration, metadata/pattern binding, native expression rendering and projection shaping have separate compiler files. These are responsibilities of one compiler, not new runtime service abstractions. The existing native executor and finalized EF model remain authoritative. Unsupported captured receivers now produce the provider's supported-shape diagnostic without invoking user methods or leaking reflection errors.