[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"CEBoB8CjCC":3},"# lean-linq\n\n[![CI](https://github.com/palladin/lean-linq/actions/workflows/ci.yml/badge.svg)](https://github.com/palladin/lean-linq/actions/workflows/ci.yml)\n[![Lean 4](https://img.shields.io/badge/Lean-v4.31.0-blue)](https://leanprover.github.io/)\n[![dialects](https://img.shields.io/badge/SQL-SQLite%20%7C%20PostgreSQL%20%7C%20MySQL%20%7C%20SQL%20Server-informational)](#integration-tests)\n\nA type-safe, deeply-embedded SQL query DSL for Lean 4 — language-integrated queries built\nfrom intrinsically-typed GADTs and PHOAS binders: schemas index the types of queries and rows,\nso only well-formed SQL elaborates.\n\nQueries are staged: bound row variables carry *SQL expressions* (not runtime values) and\ncompose an expression tree. The compiler emits parameterized SQL — literals\nnever appear in the SQL text.\n\n```lean\nimport LeanLinq\nopen LeanLinq\n\nabbrev CustomersS : Schema :=          -- bare = NOT NULL; nullable is explicit\n  [(\"Id\", .int), (\"Name\", .string), (\"Age\", .null .int)]\ndef customers : Table \"Customers\" CustomersS := ⟨⟩\n\nabbrev MyDb : Ctx := { tables := [(\"Customers\", CustomersS)] }\n\ndef adults := Query.from' (ts := MyDb) customers\n  |>.where' (fun c => 18 \u003C. c[\"Age\"])\n  |>.orderBy (fun c => [c[\"Name\"].asc])\n  |>.select (fun c => ![c[\"Id\"].as \"Id\", c[\"Name\"].as \"Name\"])\n\n#eval adults.toSql .sqlite\n-- { sql := \"SELECT \\\"a0\\\".\\\"Id\\\" AS \\\"Id\\\", \\\"a0\\\".\\\"Name\\\" AS \\\"Name\\\"\n--           FROM \\\"Customers\\\" \\\"a0\\\"\n--           WHERE (:p0 \u003C \\\"a0\\\".\\\"Age\\\") ORDER BY \\\"a0\\\".\\\"Name\\\" ASC\",\n--   params := #[(\":p0\", .int 18)] }\n```\n\nThe same query in `query!` comprehension syntax — both surfaces normalize to *identical* SQL:\n\n```lean\ndef adults' := query! MyDb {\n  from c in customers\n  where 18 \u003C. c[\"Age\"]\n  orderBy c[\"Name\"].asc\n  select ![c[\"Id\"].as \"Id\", c[\"Name\"].as \"Name\"]\n}\n\n#eval adults.toSql .sqlite == adults'.toSql .sqlite   -- true\n```\n\nIll-typed queries don't compile: a misspelled column name, comparing an `int` column to a\n`string`, adding two `string` columns, referencing a table outside the declared context\n(`Ctx`), or referencing an unbound named parameter are all elaboration errors. A query's\ntype records the database context it is written against — its tables *and* its named\nparameters — and each reference is resolved by instance search (`HasTable`/`HasParam`) at\nelaboration time, the way columns are resolved by `HasCol`.\n\n**Nullability lives in the universe.** A schema entry is a column type *plus* its\nnullability, and **bare means NOT NULL** — `(\"Age\", .int)` never holds NULL;\nNULL-capable columns say so: `(\"SignupDate\", .null .dateTime)` (a deliberate,\nKotlin/C#-style divergence from SQL's default). The flag flows: expressions carry it\n(the `SqlType` index bundles primitive and nullability), a `leftJoin`'s joined row is\nNULL-lifted *in its type*, aggregates\nare nullable (empty groups), `isNull` never is, and writing NULL into a NOT NULL\ncolumn — `setNull`, or a nullable value in `.value` — is an elaboration error. The\npayoff is at the read side: fetched cells have **honest types** — `row[\"Name\"]` is\na `String` when the schema says NOT NULL, an `Option String` only when it says\n`.null` — and the drivers reject a wire NULL in a NOT NULL column as the protocol\nerror it is.\n\n**Scope**: lean-linq compiles queries to `CompiledSql` (SQL text + parameter bindings) for\nany driver to execute, and ships **native drivers for all four engines** — SQLite\n(C FFI over the system sqlite3), PostgreSQL (libpq), MySQL (libmysqlclient,\n`brew install mysql-client`), and SQL Server (FreeTDS, `sp_executesql` RPC): typed queries in, typed rows\nout, parameters bound natively — see \"Executing for real\" below.\n\n## `db!` — database programs with the round-trip bill in the type\n\n`Db` prices round trips in the type (`fetch` = 1, data-dependent `bind` =\n`+`, per-row `for` = body grade × collection length — a derived bind-chain), and\nexecution demands a budget plus a proof — the philosophy being that everything is\npriced and the *proof* is the gate. A grade **is its collapse** — a function\nfrom table-size valuations to `Nat` — and there is no ⊤ and no ℕ∞ anywhere: the\nunknown is not \"unbounded\", it is a *symbol* (`customers.size` reads the size),\nand evaluating at a valuation σ is application. `customers.size + 1` is a type;\n`q.gcard` prices a query's rows in the same terms (a source is its table's\nsymbol, joins multiply, unions add, `limit` takes the pointwise `min` of the\ninner bound and the limit — tighter than either); the algebra is pointwise, so\ngrade arithmetic is plain `Nat` arithmetic, and the doors discharge their\nobligations by unfolding to it (`omega` after a definitional normalization).\n\nThat gives N+1 four doors, none accidental: `fetchFor` batches a whole key set\ninto one `IN (…)` round (grade 1); `let ys ← for x in xs do body` loops per row\nwith the **exact dynamic grade** `k * xs.length` — for collections already in\nhand, proved at the door (`by decide` for literals, `omega` for computed\nbudgets); over *just-fetched* rows, `fetchLimit q n` returns a length-refined\nlist (`{xs // xs.length ≤ n}`, backed by the first theorem about the executable\nsemantics: `Query.run_limit_length_le`, `LIMIT` really limits), and looping over\nits `.val` fuses into `DbP.forRows`, whose budget proof *is* the refinement\n— grade `m + k * n`, closed, silent; and the plain spelling needs no bound at\nall — `let xs ← q.execQuery` then `for p in xs do body` fuses into `forFetched`,\nbecause `fetch` carries as its postcondition that the rows fit `q.gcard` at every\nsize valuation σ — and the graded spec threads σ, so the loop's budget proof\nconsumes the contract exactly where it was measured and transports it through\nthe evaluation homomorphism, grade `1 + k * q.gcard` in the\ndatabase's own terms. `exec budget` refuses a symbolic grade statically (no\nnumber dominates a table symbol); the sized door `execWithin` collapses the\ngrade against the live database's own sizes and checks *before* interpreting\na single round; `execAll` runs unchecked, visibly. You can write N+1 when you\nmean it — priced by a bounded query or by the database itself — and you cannot\nwrite it by accident: a loop over a collection *derived* from fetched rows\n(`filterMap` ids and the like) has no contract to consume, and no proof exists.\n(Independence — fetches sharing a round — is applicative structure, not\nmonadic; it was deliberately removed from the core and returns with a free\napplicative layered over the monad, where PostgreSQL pipeline batching lives.)\n\nAll of it in one definition, written in `db!` do-sugar:\n\n```lean\ndef topSpendersDetail (n : Nat) :\n    Db ShopDb (Grade.nat n + 1) (List (String × Nat)) := db! {\n  let spenders ← Query.from' (ts := ShopDb) customers\n    |>.where' (fun c => 18 \u003C. c[\"Age\"])\n    |>.orderBy (fun c => [c[\"Name\"].asc])\n    |>.fetchLimit n                           -- LIMIT n, length-refined rows\n  let report ← for s in spenders.val do       -- fuses into forRows: the proof is the refinement\n    Query.from' (ts := ShopDb) orders\n      |>.where' (fun o => o[\"CustomerId\"] ==. s[\"Id\"])\n      |>.execQuery\n      |>.map (fun os => (s[\"Name\"], os.length))\n  return report\n}\n\n#eval (topSpendersDetail 5).exec 6 db      -- 5 + 1 rounds declared; proof by decide, silent\n\n-- and \"all rows, per row\" needs no bound at all — the price is symbolic:\ndef topSpendersAll : Db ShopDb (customers.size + 1) (List (String × Nat)) := db! {\n  let spenders ← Query.from' (ts := ShopDb) customers\n    |>.where' (fun c => 18 \u003C. c[\"Age\"]) |>.execQuery\n  let report ← for s in spenders do           -- fuses into forFetched: the proof is fetch's contract\n    Query.from' (ts := ShopDb) orders\n      |>.where' (fun o => o[\"CustomerId\"] ==. s[\"Id\"])\n      |>.execQuery\n      |>.map (fun os => (s[\"Name\"], os.length))\n  return report\n}\n\n#eval topSpendersAll.execWithin 50 db              -- collapses |customers|+1 at db's sizes, checks, runs\n#check_failure (topSpendersAll.exec 1000 db)       -- no number dominates |customers| + 1\n```\n\nWrites are effects in the same monad — `insert`, `update`, `delete`,\n`INSERT … SELECT`, and batched multi-row `VALUES` each cost one operation and\nreturn their **affected-row count** (exact in the model, engine-reported over the\nwire). The grade is simply the count of database operations, reads and writes\nalike — so the write-side N+1 carries its bill too:\n\n```lean\n-- one SELECT, then one INSERT per row — the type says what it costs:\ndef duplicateAll : Db ShopDb (customers.size + 1) Nat := db! {\n  let rows ← Query.from' (ts := ShopDb) customers |>.execQuery\n  let ks ← for r in rows do\n    customers.insert (ts := ShopDb)\n      |>.value \"Id\" r[\"Id\"] |>.value \"Age\" r[\"Age\"] |>.value \"Name\" r[\"Name\"]\n      |>.execInsert\n  return ks.sum\n}\n\n-- the same copy as INSERT … SELECT: the engine moves the rows, grade 1\ndef duplicateAllFast : Db ShopDb 1 Nat := db! {\n  let k ← customers.insertFrom (Query.from' (ts := ShopDb) customers)\n    |>.execInsertSelect\n  return k\n}\n```\n\nOn a fetched row, `s[\"Id\"]` in an expression position embeds the cell as a typed\nliteral (the inner query's WHERE), and anywhere else reads the honest value —\nthe same brackets both ways. Over the wire the doors are per-driver:\n`f.execIO conn budget` (SQLite), `f.execPg conn budget` (PostgreSQL, pipelined),\n`f.execMs conn budget` (SQL Server) — each with an unchecked `…All` variant.\nAll interpret sequentially, one statement per round.\n\n## Building\n\n```\nlake build                        # library\nlake test                         # golden tests: 404 cases × 4 dialects (exact SQL + parameters)\nlake exe tests --update           # regenerate Tests/golden/{sqlite,sqlserver,postgres}.golden\n\ndocker compose up -d --wait       # PostgreSQL + SQL Server test databases\nlake exe integration              # execute all 358 cases against live SQLite/PostgreSQL/SQL Server\nlake exe integration --update     # regenerate Tests/golden/results-*.golden\n\nlake exe sqlitedriver             # native-driver sweeps: full corpus through each driver,\nlake exe pgdriver                 #   compared against the evaluator at the Values level\nlake exe mssqldriver\n```\n\n## Integration tests\n\nNearly every pipeline query case has a comprehension twin (`C\u003CName>`, Tests/QueriesC.lean;\nthe exceptions are shapes `query!` cannot spell — split limit/offset chains and\nset-operation compositions)\nexpressing the same shape with `query!` clauses, so both surfaces are covered by\nevery layer below. `lake exe integration` executes every registered query and\nstatement against real databases: SQLite (local temp file), PostgreSQL and SQL Server (docker compose\nservices, driven through `psql`/`sqlcmd` inside the containers — no local client\ninstalls needed). The seed dataset mirrors the classic customers/products/orders\nfixture. Parameters are inlined as dialect-escaped literals *for execution only*;\nthe library itself always emits parameterized SQL.\n\n- Row results are normalized (booleans, decimal trailing zeros, datetime\n  precision, guid case, NULL sentinels, row order for unordered queries) and\n  checked three ways: against the **evaluator** (`Query.run` over the seed\n  database — expected rows computed from the same query value that produced\n  the SQL, for every deterministic case), against per-dialect goldens\n  (`Tests/golden/results-{db}.golden`), and across engines.\n- Statements run inside a transaction: execute, verify table state with a\n  SELECT, roll back.\n- A cross-dialect comparison then checks that all engines agree on every case,\n  modulo a small allowlist (AVG division semantics differ by engine: integer on\n  SQL Server, numeric on PostgreSQL, float on SQLite).\n- Unreachable databases are skipped with a warning: `--db sqlite,postgres`\n  selects explicitly.\n- Prerequisites: the `sqlite3` CLI (ships with macOS and GitHub runners;\n  `apt-get install sqlite3` on minimal Linux) and docker compose v2. The SQL\n  Server image is amd64-only: on Apple Silicon it runs via Docker Desktop's\n  Rosetta emulation; on ARM Linux (no Rosetta) skip it with\n  `--db sqlite,postgres`.\n\n## Feature surface\n\n- **Types**: int, long, double, decimal, string, bool, dateTime, guid; NULL via\n  `isNull`/`isNotNull`, `setNull`/`valueNull` in statements.\n- **Expressions**: `+ - * /` (numeric), `++` (concat), `==. !=. \u003C. \u003C=. >. >=.`,\n  `&&. ||. !.`, `like`, `inValues`/`notInValues`, `inQuery`/`notInQuery` (subquery),\n  `exists'`/`notExists` (correlated subqueries), `caseWhen`,\n  context-typed named parameters (`SqlExpr.param \"minAge\"`), `abs`/`round`/`ceiling`/`floor`,\n  `substring`/`upper`/`lower`/`trim`/`length`,\n  `now`/`year`/`month`/`day`/`addDays`/`addMonths`/`addYears`/`diffDays`/`diffMonths`/`diffYears`.\n- **Queries**: `from'`/`where'`/`select`, `innerJoin`/`leftJoin` (fuse into one\n  statement), `orderBy` (multi-key, fuses), `distinct`, `limitOffset`,\n  `groupBy`/`having`/grouped `select` with aggregates (`a.count`, `a.sum`, …),\n  scalar queries (`count`/`sum`/`avg`/`min`/`max`, embeddable via `.embed`),\n  `union`/`intersect`/`except`.\n- **Statements**: `t.insert |>.value …`, `t.update |>.set/.setWith/.setNull |>.where' …`,\n  `t.delete |>.where' …`.\n- **Dialects**: SQLite (`\"x\"`, `:p0`, `LIMIT/OFFSET`), SQL Server (`[x]`, `@p0`,\n  `OFFSET…FETCH`, `GETDATE`/`DATEADD`/`DATEDIFF`/`LEN`, `+` concat), PostgreSQL\n  (`\"x\"`, `:p0`, `EXTRACT`/`INTERVAL`/`NOW()`): `q.toSql .sqlite`,\n  `.toSqlServer`, `.toPostgres`. Every SQLite golden is parse-validated against\n  a real SQLite database.\n\n## The pipeline API\n\nThe primary surface is a fluent pipeline over typed combinators (`from` and `where` are Lean\nkeywords, hence the primes):\n\n```lean\nabbrev OrdersS : Schema := [(\"OrderId\", .int), (\"CustomerId\", .int), (\"Amount\", .int)]\ndef orders : Table \"Orders\" OrdersS := ⟨⟩\nabbrev ShopDb : Ctx := { tables := [(\"Customers\", CustomersS), (\"Orders\", OrdersS)] }\n\ndef report := Query.from' (ts := ShopDb) customers\n  |>.where' (fun c => 18 \u003C. c[\"Age\"])\n  |>.innerJoin orders (fun c o => c[\"Id\"] ==. o[\"CustomerId\"])\n      (fun c o => ![c[\"Id\"].as \"CustId\", c[\"Name\"].as \"Name\", o[\"Amount\"].as \"Amount\"])\n  |>.groupBy (fun r => [r[\"CustId\"].key, r[\"Name\"].key])\n  |>.having (fun _ a => 1 \u003C. a.count)\n  |>.select (fun r a => ![r[\"Name\"].as \"Name\", (a.sum r[\"Amount\"]).as \"Total\"])\n  |>.orderBy (fun r => [r[\"Total\"].desc])\n  |>.limit 10\n```\n\nAvailable: `from'` / `where'` / `select`, `innerJoin` / `leftJoin`, `orderBy` (multi-key),\n`groupBy` / `having` / grouped `select` (the lambda receives an `Agg` token: `a.count`,\n`a.sum e`, …), `distinct`, `limitOffset`/`limit`/`offset`, `union`/`intersect`/`except`,\nand scalar queries (`count`/`sum`/`avg`/`min`/`max`, embeddable in expressions via\n`.embed`, subqueries via `inQuery`).\n\nQueries **normalize at construction time** (`Query.bind`, the comprehension monad):\n`where'` splices a conjunct, `select` replaces the projection, joins extend the FROM clause,\n`orderBy` attaches to the statement, and a query used as a source is inlined — so pipelines\ncompile to a single flat SELECT. Clauses that must not be spliced through — DISTINCT,\nLIMIT/OFFSET, GROUP BY/HAVING, set operations — are *boundary nodes*: binding over them\nwraps the query as a derived table, which is exactly SQL's semantics.\n\n## The query! comprehension syntax\n\nThe same core also has a C#-LINQ-style comprehension surface: `query! { … }` takes newline-\n(or `;`-) separated clauses, desugared right-to-left onto the spine constructors (exactly\nhow C# desugars LINQ into `SelectMany`):\n\n| Clause | Desugars to | SQL |\n|---|---|---|\n| `from x in src` | `QuerySource.bind src (fun x => …)` | a `FROM` source |\n| `join x in t on p` / `leftJoin x in t on p` | `Query.joinOn` | `INNER/LEFT JOIN … ON` |\n| `where p` | `Query.guard p …` | a `WHERE` conjunct |\n| `orderBy k, …` | `Query.orderWith` | `ORDER BY` (may reference aggregates when grouped) |\n| `groupBy k, … into a` + `having p` | grouped terminal (`Query.groupYieldQ`) | `GROUP BY … HAVING` |\n| `select r` | `Query.yield r` | the `SELECT` list |\n| trailing `distinct`, `limit n [offset m]`, `offset n` | `.distinct`/`.limitOffset` | `DISTINCT`, `LIMIT/OFFSET` |\n\nSources are tables *or* queries via the `QuerySource` class (nested `from`s are cross\nproducts). `groupBy … into a` *binds* the aggregate token — like C#'s `group … into g` —\nso `a.count` / `a.sum e` are in scope only in the clauses after the grouping; `where` after\n`groupBy` and `having` without `groupBy` are rejected. Ordering by an aggregate *expression*\nis something only the comprehension can express in one statement:\n\n```lean\nquery! {\n  from c in customers\n  join o in orders on c[\"Id\"] ==. o[\"CustomerId\"]\n  where c[\"Age\"] >=. 18\n  groupBy c[\"Id\"].key, c[\"Name\"].key into a\n  having a.count >. 1\n  orderBy (a.sum o[\"Amount\"]).desc\n  select ![c[\"Id\"].as \"CustomerId\", (a.sum o[\"Amount\"]).as \"TotalSpent\"]\n}\n```\n\nNearly every pipeline test case has a comprehension twin (Tests/QueriesC.lean), so both surfaces\nare exercised by the full test stack.\n\n## Statements\n\nINSERT / UPDATE / DELETE use the same name-checked, typed column machinery:\n\n```lean\ncustomers.insert (ts := MyDb)\n  |>.value \"Id\" 200 |>.value \"Name\" \"John Doe\" |>.valueNull \"Age\"\n-- INSERT INTO \"Customers\" (\"Id\", \"Name\", \"Age\") VALUES (:p0, :p1, NULL)\n\ncustomers.update (ts := MyDb)\n  |>.setWith \"Age\" (fun c => c[\"Age\"] + 1)\n  |>.where' (fun c => c[\"Id\"] ==. 200)\n-- UPDATE \"Customers\" SET \"Age\" = (\"Age\" + :p0) WHERE (\"Id\" = :p1)\n\ncustomers.delete (ts := MyDb) |>.where' (fun c => c[\"Age\"] \u003C. 18)\n-- DELETE FROM \"Customers\" WHERE (\"Age\" \u003C :p0)\n```\n\n## Executing for real: the native drivers\n\n`import LeanLinq.Driver.Sqlite` (a separate lib target — the core library stays\nFFI-free) talks to the system sqlite3 library:\n\n```lean\nopen LeanLinq.Sqlite\n\ndef demo : IO Unit := do\n  let conn ← Sqlite.connect \"app.db\"\n  let rows ← conn.query adults          -- IO (List (Values s)): typed rows out\n  let _ ← conn.execInsert someInsert    -- statements too\n  conn.close\n```\n\n- **Parameters are bound natively**, and there are two kinds. *User parameters*\n  (`Ctx.params`) are the query's typed interface — declared names whose values the\n  caller supplies at execution, read from the same typed `ParamEnv c.params` by the\n  evaluator and the drivers alike. *Auto parameters* (`p0, p1, …`) are a compilation\n  artifact: one per literal, value shipped alongside the SQL — the evaluator never\n  sees them (it evaluates literals directly), and they exist so no value ever appears\n  in the SQL text (injection safety, plan-cache reuse, typed wire transfer). Nothing\n  is ever inlined, at compile time or execution time; user names shaped like `p0` are\n  statically refused to keep the namespaces apart.\n- **Rows decode schema-directed into `Values s`** using the `SqlType.interp`\n  conventions, so driver output is cell-for-cell comparable with `Query.run` — and the\n  test suite does exactly that: `lake exe sqlitedriver` runs all registered cases through the\n  driver and compares against the evaluator **at the `Values` level** (statements\n  verified inside rolled-back transactions).\n- **`Db` programs run over the wire**: `f.execIO conn budget` interprets the same\n  round-budgeted tree `runWith` interprets in memory, with the same proof discipline.\n\n**PostgreSQL** works the same way (`import LeanLinq.Driver.Postgres`, `Pg.connect` with\na conninfo string; requires libpq — `brew install libpq` / `libpq-dev`): the driver\nrewrites the compiled `:name` placeholders to the wire's `$N` form and sends every\nparameter with an explicit type OID, which resolves `EXTRACT(YEAR FROM $1)`-style\ninference properly. `f.execPg conn budget` interprets one statement per round\n(pipeline batching is applicative structure and returns with the free-applicative\nlayer). `lake exe pgdriver` sweeps the full corpus against live PostgreSQL, typed\n`Values`-to-`Values` against the evaluator.\n\n**MySQL** (`import LeanLinq.Driver.Mysql`, `Mysql.connect`; requires\nlibmysqlclient — `brew install mysql-client` / `libmysqlclient-dev`) executes over\nprepared statements: the driver rewrites each `:name` *occurrence* to MySQL's\nunnamed `?` and emits values in occurrence order (a repeated named reference\nrepeats its value — unlike PostgreSQL's `$N`), parameters bind as text (MySQL\ncoerces in typed contexts), and results decode through the same shared cell\nparser as every other driver. `lake exe mysqldriver` sweeps the corpus against\nthe compose service on port 3307.\n\n**SQL Server** (`import LeanLinq.Driver.Mssql`, `Ms.connect`\nwith host/port/credentials; requires FreeTDS — `brew install freetds` /\n`freetds-dev`). It is the one engine that needs **no placeholder rewriting at all**:\nthe sqlServer dialect already compiles `@p0`/`@minAge`, TDS's native named-parameter\nform, so execution is an `sp_executesql` RPC — the compiled SQL travels verbatim as\n`@stmt`, a declaration string types every parameter, and the values ride as text\nwith server-side conversion (the same text-plus-typed-declaration strategy as the\nPostgreSQL driver's OIDs). One DB-Library wrinkle is handled transparently: its API\ncannot express an *empty string* parameter (zero length means NULL at the API\nlayer), so executions carrying one fall back to an equivalent `EXEC sp_executesql`\nbatch with the statement still verbatim. TDS allows one active request per\nconnection — no pipelining — so `Db.execMs` is sequential and the `max` grade\nstays an honest upper bound, as with in-process SQLite. `lake exe mssqldriver`\nsweeps the full corpus against live SQL Server (docker compose, port 14333), typed\n`Values`-to-`Values` against the evaluator — with the native drivers now covering\nall four engines, the string-based CLI integration harness is a retirement\ncandidate once the parallel-run period ends.\n\n## Running queries in memory\n\nQueries are total, deeply-embedded values, so they carry a denotational\nsemantics: `Query.run : Query c s → TableEnv c.tables → ParamEnv c.params →\nExcept EvalError (List (Values s))` evaluates the exact query value that\ncompiles to SQL against a *typed* in-memory database — SQL semantics\nincluded (three-valued NULL logic, LEFT JOIN padding, GROUP BY/HAVING with\naggregates, exact fixed-point decimals, civil-calendar date arithmetic).\nStatements apply as `TableEnv c.tables → Except EvalError (TableEnv\nc.tables)`. For a parameterless context the `ParamEnv` argument defaults\naway.\n\nNULL and errors are separate channels, never conflated: `Nullable`'s `none`\nis SQL NULL and only NULL, while exceptional, statement-aborting conditions\n— division by zero, `now` without a clock — are explicit `EvalError`s\n(`CASE WHEN` stays lazy, as in SQL, so a guarded division doesn't abort).\n\nBecause every table and parameter reference was resolved against the context\nat elaboration time (the `HasTable`/`HasParam` instance stored in the query\n*is* the accessor), evaluation performs no name lookup and has no failure\nmode: running a query against a database that lacks one of its tables — or\nleaves one of its parameters unbound — is not an error case, it is\nuntypeable.\n\n```lean\ndef db : TableEnv MyDb.tables := .cons [/- value rows -/] .nil\n\n#eval adults.run db      -- Except.ok [(2, \"Jane Smith\"), (1, \"John Doe\")]\n```\n\nThis is also how the test suite works: the integration runner computes every\ncase's expected rows with `Query.run` and differential-tests all four\nengines against it — the executable semantics is the oracle, not hand-written\nexpectations. And it is the foundation for stating propositions about result\nsets (rows of `q.where' p` satisfy `p`, `orderBy` results are sorted, …) as\ntheorems about `⟦q⟧ = q.run`.\n\n## Rows and operators\n\nRow access and construction:\n\n- `r[\"Name\"]` — column by name, checked at compile time (typeclass `HasCol` over the schema;\n  a typo fails instance synthesis).\n- `r.nth ⟨0, by decide⟩` — positional.\n- `![e₁.as \"A\", e₂.as \"B\"]` — row literal for projections; `r₁ ++ r₂` splices whole rows.\n  (`![…]` rather than `[…]`: overloading the list brackets would break list *patterns* in any\n  scope with LeanLinq notation open — Lean does not backtrack syntax choice nodes in patterns.)\n\nOperators on `SqlExpr` (scoped in `LeanLinq`): `+` (int), `++` (string concat), and the dotted\ncomparison/logic family `==.` `!=.` `\u003C.` `>.` `&&.` `||.` `!.` (the Prelude's `==`/`\u003C`/`&&`\nreturn `Bool`/`Prop`, so SQL needs its own).\n\n## Rules of the road\n\n- **Schemas and contexts must be `abbrev`**, not `def` — column lookup (`HasCol`) and table\n  lookup (`HasTable`) resolve by instance search over the literal lists.\n- **Pin the context** — at the query head (`Query.from' (ts := MyDb) t`) or on the\n  comprehension (`query! MyDb { … }`): an unannotated standalone definition would leave\n  the context as a metavariable, and instance search cannot run against an undetermined\n  context. (Nested blocks — subqueries, sources — infer it from the enclosing query.)\n- **Literals go on the right** of `==.`/`!=.` (`c[\"Name\"] ==. \"Alice\"`): coercions only fire\n  against a known expected type. For literal-first, use `SqlExpr.str`/`.int`/`.bool`.\n  (Monomorphic operators like `\u003C.`/`>.`/`+` take literals on either side.)\n\n## Design\n\n- `SqlType` universe; `SqlExpr : Ctx → SqlType → Type` GADT — ill-typed SQL is unrepresentable.\n- `SqlType = {ty : SqlPrim, nullable : Bool}` — the SQL type *is* its primitive plus\n  its nullability, one value that travels together (bare = NOT NULL). `Schema := List\n  (String × SqlType)`; `Ctx := { tables, params }` both over `SqlType`;\n  `SqlExpr : Ctx → SqlType → Type` — nullability rides the single index;\n  `Row : Ctx → Schema → Type` heterogeneous tuple of expressions; `Values` cells have\n  per-column honest types (`Option` only where `.null`).\n- Table names live at the type level (`Table (n : String) (s : Schema)`); queries are indexed\n  by their ambient context, and `fromT`/`joinT`/`param` *store* the `HasTable`/`HasParam`\n  membership instance resolved at elaboration — a query carries its referenced tables and\n  parameters as capabilities, so the evaluator reads rows and bindings through them with no\n  run-time resolution. A parameter's type comes from the context, not an annotation\n  (`SqlExpr.param \"minAge\"`).\n- One mutual query algebra (PHOAS): expressions, rows, and the two query levels —\n  `SpineQP` (the comprehension spine: `yield`/`groupYield`/`guard`/`fromT`/`joinT`/`order`/\n  `fromQ`) and `QueryP` (boundary nodes: `distinct`, `limit`, set ops) — form a single\n  inductive family parameterized by the row representation `ρ : Schema → Type`. Binders\n  take the opaque atom `ρ s` (the one slot a `∀ρ`-polymorphic term cannot inspect), which\n  keeps every mutual occurrence positive; the smart constructors re-wrap with\n  `RowP.ofAtom`, so surface lambdas receive rows. The public `Query ts s` is the bundle\n  `∀ ρ, QueryP ρ ts s`: the compiler reads it at `AliasOf`, the evaluator walks the same\n  instantiation, and `card` counts it — one term, every reading, with agreement by\n  parametricity rather than by discipline.\n- Subqueries inside expressions (`inQuery`, `exists'`, `.embed`) are stored\n  *structurally* at the same ρ, so a correlated subquery captures the outer binder like\n  any other Lean value. That capture pins it to the ambient representation: a correlated\n  inner chain spells its head per-ρ (`QueryP.from' … |>.where' (fun o => o[\"CustomerId\"]\n  ==. c[\"Id\"])`), and an inner `query!` block ascribes `: QueryP _ Ctx _`. Uncorrelated\n  subqueries stay ordinary bundles and drop in unchanged.\n- Compiler: a `StateM` (alias counter + parameter accumulator) walk that renders the spine as\n  one flat SELECT, recursing structurally into stored subqueries (inner aliases continue the\n  outer numbering). The evaluator (`Query.run`) walks the same instantiation with scopes\n  flowing down — sources extend each alias→row scope, terminals evaluate where their trees\n  are structural — so correlated references resolve identically in both readings.\n  Everything is total — the family is a reflexive inductive, so structural recursion covers\n  binder continuations applied to any atom —\n  which means the kernel itself can run both (`#guard` tests of generated SQL *and* of\n  evaluated rows at elaboration time).\n\n## Status\n\nCore, full query surface (joins, grouping, aggregates, set ops, subqueries),\nstatements, the four dialects, native drivers for all four engines, and the\nround-budgeted `Db` layer are implemented, with a 404-case × 4-dialect\ngolden suite (both surfaces), an executable in-memory oracle, per-driver\ncorpus sweeps, and live 3-engine integration tests.\n\nKnown limitations: trailing `orderBy` after\n`distinct`/`limit` is pipeline-only (the comprehension fuses ordering before\nthem). Possible next steps: EXISTS/NOT IN, window functions, CTEs, and\ncardinality-indexed queries — row bounds, predicate satisfaction, and\nsortedness as propositions the fetch returns with the rows.\n",1784670017212]