Table of Contents

How the engine works

A tour of the moving parts — for contributors, extenders, and anyone who wants to trust the machine by understanding it.

The layers

┌─────────────────────────────────────────────────────────────┐
│ Contracts (eQuantic.Core.Data)                              │
│   IRepository / IUnitOfWork / QueryOptions / Migrations     │
│   Modeling annotations · DataConventions · EntityLifecycle  │
├─────────────────────────────────────────────────────────────┤
│ Interpreters (store-agnostic, eQuantic.Core.Data.Query)     │
│   FilterInterpreter  → QueryFilter IR                       │
│   UpdateInterpreter  → UpdateAssignment IR                  │
│   GroupInterpreter   → GroupQuery IR (+ HAVING)             │
│   UnionInterpreter   → UnionQuery                           │
├──────────────┬──────────────┬───────────────┬───────────────┤
│ Relational   │ MongoDB      │ Cosmos DB     │ Cassandra     │
│ engine +     │ (driver      │ (SDK LINQ +   │ (CQL renderer │
│ SqlDialects  │ LINQ + class │ CosmosLinq-   │ + pushdown    │
│ (PG/My/MS)   │ maps)        │ Serializer)   │ planner)      │
└──────────────┴──────────────┴───────────────┴───────────────┘

The interpreters — one IR, many renderers

Every lambda (filters, update factories, group shapes) is interpreted once, store-agnostically into a small IR: comparisons, IN, ranges, string operations, logical nodes; set/increment/ collection assignments; group keys and aggregate bindings. Captured variables are partially evaluated to constants at this stage (built on eQuantic.Linq.Expressions — which also makes filters wire-serializable).

Providers then render the IR: SQL through a SqlDialect, CQL through the Cassandra renderer, Mongo filters/updates through the driver, Cosmos through the SDK's LINQ. What a renderer cannot express falls out as the residual — the raw material of the gates. Because validation happens in the shared interpreter, rejection messages and accepted shapes are identical across providers; because rendering is per-store, each provider does its native best.

The relational engine

One engine (eQuantic.Core.Data.Relational), four thin dialects. The dialect owns exactly what differs between engines: identifier quoting, naming conventions, paging syntax, DDL type names (facets included), generated-key retrieval (RETURNING vs OUTPUT INSERTED vs last-insert-id), index structures (GIN, filtered, trigram), and the rendering of constructs engines disagree on. Everything else — the batch flush, concurrency tokens, includes, unions, group-by, materialization — is written once. A dialect is ~200 lines; that is the extension point.

The write path

Add/Modify/RemoveEntityLifecycle stamps (time, WHO, soft-delete conversion, token bumps) → the provider stages the native write (SQL batch command, Mongo WriteModel, Cassandra bound statement, Cosmos point operation) → Commit flushes the store's best batch → results are checked (affected rows, [applied], matched counts) and ConcurrencyConflictException surfaces lost races. No tracker, no snapshots: the staged write is the state.

Generated accessors — reflection out of the hot paths

The package ships a source generator (beside the analyzer, no extra install) that emits a reflection-free accessor per entity — construction, member reads and member writes as direct code behind a name switch — registered into the engine's EntityAccessors registry by a module initializer. Relational materialization and column reads consult the registry first; reflection remains the fallback contract, so assemblies compiled without the generator behave identically.

Honesty notes: entities the generated code could not honor faithfully (init-only or non-public setters on mapped-shaped members, no accessible parameterless constructor) are skipped entirely — no accessor beats a lossy one — and stay on the reflection path. Measured effect on the benchmarks: within noise (the database dominates); the point of the accessors is eliminating the reflection dependence itself — the groundwork for trimming/NativeAOT support.

The model registry

Each provider builds an immutable model at startup (annotations pre-pass seeds each entity builder; fluent calls override; Build() validates and freezes). Configurations carry both sides of every mapping (member ↔ stored name), so renames flow through queries, writes, DDL, projections and migrations from one source of truth — and Explain() prints it.

Protection of the engine seam

The interpreter/IR namespace (eQuantic.Core.Data.Query) is public — providers in other packages need it — but marked [EditorBrowsable(Never)]: it is SPI, not API. The support policy is explicit: application code targets the repository contracts; the IR may evolve with provider needs.