Skip to content

Sqlite support: proof of concept#80

Open
ryanrasti wants to merge 33 commits into
mainfrom
ryan_sqlite
Open

Sqlite support: proof of concept#80
ryanrasti wants to merge 33 commits into
mainfrom
ryan_sqlite

Conversation

@ryanrasti

Copy link
Copy Markdown
Owner
  • Shared SqlValue base extracted from PG's Any. Dialect is now { name, root, bool }; DialectName is the string tag.
  • SQLite inference tool (src/types/sqlite/generate.ts) — reads pragma_function_list, probes with a typed input matrix, emits functions.json + class stubs. LLM-over-docs plan in the file header for later cleanup.
  • SqliteDriver — better-sqlite3, row-value normalization at the boundary.
  • Sweep: instanceof Any → SqlValue across builders/util; runtime.ts utilities widened.

ryanrasti and others added 9 commits May 27, 2026 16:29
Phase 0.1 of SQLite generalization. Moves generated/, overrides/,
generate.ts, introspect.ts, and the PG-specific index.test.ts under
src/types/postgres/. runtime.ts, deserialize.ts, cast.test.ts, and
match.test.ts remain at top level as dialect-agnostic.

src/types/index.ts is a transitional re-export shim so external
callers don't have to change in one shot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0.2 of SQLite generalization. Drop the Pg* prefix from runtime
expression builders since they aren't dialect-specific (they just wrap
the dialect-agnostic Func/Op AST nodes in builder/sql.ts).

- runtime.PgFunc → runtime.funcCall  (lowercase factory; Func is taken)
- runtime.PgOp   → runtime.opCall    (lowercase factory; Op is taken)
- runtime.PgSrf  → runtime.Srf       (class — no name collision)

The PgFunc interface in src/types/postgres/introspect.ts is unchanged
— it represents a pg_catalog function row and remains PG-specific.

Codegen template and all 77 generated/ files updated via npm run codegen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0.3 of SQLite generalization. Splits the deserialize registry so
each dialect owns its own typname → TS-type mapping.

- src/types/deserialize.ts: shared TypeDef/Dialect, registerDialect(),
  getTypeDef(dialect, typname). Internal map is a Map (avoids
  proto-pollution lint rule on dynamic object keys).
- src/types/postgres/deserialize.ts: PG registry (bool, int2/4, float
  4/8, oid). Registers itself on import.
- PG Any base class gets `static dialect: Dialect = "postgres"`;
  runtime.match() and Any.{deserialize,serialize} read it for lookup.

SQLite's registry (Phase 1) will register the same way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0.4 of SQLite generalization. Threads a CompileContext (carrying
the dialect) through Sql.bind(ctx) and compile(root, ctx) so future
dialect-aware AST nodes can dispatch without baking dialect into the
node at construction time.

- src/builder/sql.ts:
  - CompileContext { dialect } + pgCtx/sqliteCtx singletons.
  - Sql.bind() → Sql.bind(ctx); subclasses without dispatch can omit.
  - compile(root, "pg"|"sqlite") → compile(root, ctx). Param-placeholder
    style (`$N` vs `?`) derives from ctx.dialect.
  - New Cast(expr, typname) node; TypedParam now delegates to Cast.
- src/driver.ts: Driver gets `readonly dialect: Dialect`. PgDriver and
  PgliteDriver default to "postgres" and build ctx from their own
  dialect on execute().
- src/types/postgres/overrides/any.ts: Any.cast() emits Cast node instead
  of inline `CAST(... AS ...)` templating.
- All 45 compile() call sites updated to pass pgCtx/sqliteCtx instead of
  string args.
- delete/insert/update FinalizedX.bind() take ctx and forward it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0.5 of SQLite generalization. Moves the body of Bool.and/or/not
into dialect-agnostic helpers at src/types/bool-mixin.ts. PG's Bool
calls them with its own __typname so primitive TS booleans coerce to
pg `bool` via the Cast node; SQLite's Bool (Phase 1) will reuse the
same helpers with its own typname.

The previous inline `CAST(... AS bool)` template is gone — Cast is the
single source of truth for cast emission.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hoists the dialect-agnostic behavior (cast, coalesce, from, serialize,
column, deserialize, toSql, [meta], statics) out of PG's `Any` into a
shared `SqlValue<N>` base in src/types/any.ts. Codegen'd PG root `any.ts`
now extends `SqlValue`; every other pg_catalog type inherits transitively.

Also introduces `Dialect` as a rich descriptor `{ name, root, bool }`
(name kept as the compile-ctx tag, aliased to `DialectName` in the
deserialize registry) and a shared `Bool<N>` interface + `isBool`
predicate for future dialect-agnostic detection.

`isNull` / `isNotNull` / `.in()` stay on PG `Any` (not SqlValue): the
shared `Bool<N>.and(Bool<any> | boolean)` vs concrete
`types.Bool.and(types.Bool<any> | boolean)` are contravariantly
incompatible, and duplicating three method bodies per dialect is cheaper
than plumbing around the variance.

SqlValue is *sibling* to Sql (not subclass) — matches the pre-existing
design and avoids inheriting Sql's abstract `bind()` down the whole
concrete-type chain. Values are duck-type-spliced into sql\`...\`
templates via `.toSql()`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 foundation. Introduces:

- `SqliteValue<N>` (src/types/sqlite/base.ts) — dialect root, extends
  the shared `SqlValue<N>`. Carries the SQLite `Dialect` object
  (barrel-getter root/bool) plus `isNull`/`isNotNull`. `.in()` is
  deferred: its `<T, Vs>(this: T, ...)` shape triggers TS2589 at
  SQLite's flatter class hierarchy (see file comment).

- `src/types/sqlite/deserialize.ts` — dialect entries for the
  registry. Registers on module load, same side-effect pattern PG
  uses. Bool is a convention (INTEGER 0/1); blob repr TBD (better-sqlite3
  → Buffer, Cloudflare DO → ArrayBuffer — reconcile at driver layer).

- `src/types/sqlite/generate.ts` — runtime introspection tool that
  reads `pragma_function_list`, probes each function through a typed
  input matrix, and emits both a JSON snapshot (`functions.json`,
  ~20k lines) and first-pass class stubs (`generated/{integer,real,
  text,blob}.ts`). Bool is hand-written (via boolAnd/boolOr/boolNot
  helpers from Phase 0.5).

Known limits — runtime probing conflates null-propagation with
error-signal nulls, admits nonsensical coercions (Blob.acos exists),
misbuckets aggregates onto whichever leaf type they were probed with,
and collapses multi-arity overloads. Detailed follow-up plan in the
generate.ts header comment: LLM-over-docs with this file as a
deterministic sanity check.

better-sqlite3 added as a devDependency (codegen only; peer-dep
plumbing lands with the driver in Phase 4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`SqliteDriver` wraps better-sqlite3 to satisfy the async `Driver`
contract. Single in-process db, so `runInSingleConnection` is a
pass-through. `.execute()` dispatches SELECT-shaped statements via
`.all()` and non-reader statements via `.run()`.

Row values from better-sqlite3 come back as typed JS (number, string,
Buffer, bigint, null). Normalized to strings at the driver boundary so
downstream `deserialize()` code keeps working against the PG-style
`{[key: string]: string}` contract. Buffer → \\x-prefixed hex to match
PG's bytea repr.

Smoke tests exercise the full pipeline end-to-end: typed-value
construction → composed method calls (abs, upper, lower, length,
isNull/isNotNull) → compile with sqlite ctx (validates `?` placeholder
emission, no `$N`) → better-sqlite3 execute → normalized rows.

8 tests, all passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Widens the query/mutation builders from PG-only (`instanceof Any`) to
dialect-agnostic (`instanceof SqlValue`) across ~15 runtime dispatch
points + ~20 type refs. PG callers unchanged — Any still is-a SqlValue.

Changes by file:

- src/table.ts, src/util.ts, src/builder/{values,insert,update,query}.ts:
  Any → SqlValue at instanceof checks + type positions. `isColumn` /
  `getColumn` imports moved from postgres/overrides/any (re-export)
  to src/types/any (source).

- src/types/runtime.ts: `NullOf`, `TsTypeOf`, `Aggregate`, `AggregateRow`
  widened from `T extends Any<...>` to `T extends SqlValue<...>` so the
  utilities work uniformly over PG's Any and SQLite's SqliteValue.

- src/types/bool.ts: the shared `Bool<N>` interface loses `and`/`or`/`not`
  and becomes a nominal marker. Concrete Bool classes' `.and(other: X
  <any> | boolean)` are contravariantly incompatible with a shared
  method signature — trying to require the ops here breaks assignment
  from concrete to shared. Chaining callers cast through `any` (see
  `combinePredicates`); runtime validation still uses the `isBool`
  identity predicate.

- src/types/meta.ts (new): extracted the `meta` symbol into its own
  tiny module. `types/any.ts` used to import `meta` from `runtime.ts`,
  but `runtime.ts` also does `import * as types from "./index"` which
  triggers the PG barrel chain — extending SqlValue via PG's generated
  any before any.ts had defined it. Splitting `meta` out breaks the
  cycle.

- src/driver.ts (SqliteDriver): unwraps a single matched outer paren
  pair from compiled SQL. `QueryBuilder.FinalizedQuery.bind()` wraps
  its output in `(...)` for subquery-splicing; PG tolerates a
  parenthesized top-level, SQLite does not.

- src/builder/{delete,update,query}.ts: `@expose` validators swapped
  from `z.instanceof(Bool)` (PG-only) to `z.custom<SharedBool<any>>
  (isBool)` (dialect-agnostic via the identity predicate). Note: the
  broader "same-database provenance" concern is filed as ISSUES.md #16;
  this changes the check from PG-nominal to dialect-nominal (catches
  cross-dialect but not cross-tenant).

- src/types/sqlite/table.test.ts (new): 7 end-to-end tests through the
  full Table → QueryBuilder → SqliteDriver stack — INSERT, SELECT with
  method composition (.upper()), .where predicates, INSERT/UPDATE/
  DELETE ... RETURNING.

Deferred (documented as comments in place, not blocking Phase 1):

- QueryBuilder.scalar() stays PG-only — emits ROW() + array_agg +
  COALESCE(..., '{}'). Phase 2.1 Scalar AST node lands the SQLite
  dispatch (json_object + json_group_array).
- `.where(true)` shorthand in delete/update still constructs a PG
  `Bool` — sqlite-only apps that need this will hit a nominal mismatch
  until the shorthand becomes dialect-aware.

Test totals: 28 files, 550 passed, 1 skipped (was 27/544). PG suite
green throughout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a proof-of-concept SQLite dialect alongside Postgres, extracting a shared SqlValue<N> base for typed SQL expressions and making compilation/deserialization dialect-aware.

Changes:

  • Added SQLite dialect scaffolding (base type, deserialize registry, generated type stubs) plus end-to-end smoke tests using better-sqlite3.
  • Refactored core type system to use shared SqlValue + a dialect-aware type registry and compile context (pgCtx/sqliteCtx).
  • Reorganized Postgres type generation into src/types/postgres/*, updating generated/override imports and runtime helpers (funcCall/opCall/Srf).

Reviewed changes

Copilot reviewed 24 out of 124 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/util.ts Updates runtime typed-expression checks from Any to SqlValue.
src/types/sqlite/table.test.ts Adds SQLite mutation + Table/QueryBuilder smoke tests.
src/types/sqlite/smoke.test.ts Adds SQLite end-to-end expression/compile/execute smoke tests.
src/types/sqlite/index.ts SQLite dialect barrel export for generated + hand-written entries.
src/types/sqlite/generated/text.ts Generated SQLite Text typed value methods.
src/types/sqlite/generated/bool.ts Temporary SQLite Bool stub (Phase 1 placeholder).
src/types/sqlite/deserialize.ts Registers SQLite typname→TS-type/deserializer mapping.
src/types/sqlite/base.ts Defines SqliteValue root and dialect descriptor wiring.
src/types/runtime.ts Generalizes nullability/type utilities to SqlValue and adds generic runtime call helpers.
src/types/postgres/overrides/record.ts Fixes import paths after Postgres codegen rehome.
src/types/postgres/overrides/bool.ts Refactors Bool ops to use shared bool helpers and updated runtime import paths.
src/types/postgres/overrides/anycompatiblearray.ts Updates runtime/sql imports for new Postgres module layout.
src/types/postgres/overrides/anyarray.ts Updates runtime/sql/exoeval imports for new Postgres module layout.
src/types/postgres/overrides/any.ts Introduces PG root Any override carrying dialect metadata + pg-specific bool-returning methods.
src/types/postgres/introspect.ts Adds Postgres introspection utilities for codegen.
src/types/postgres/index.ts New Postgres barrel exporting generated + overrides.
src/types/postgres/index.test.ts Updates tests to new compile context API and import paths.
src/types/postgres/generated/xml.ts Regenerated PG type bindings to use runtime.funcCall/opCall and new import paths.
src/types/postgres/generated/xid8.ts Regenerated PG type bindings to use runtime.funcCall/opCall and new import paths.
src/types/postgres/generated/xid.ts Regenerated PG type bindings to use runtime.funcCall/opCall and new import paths.
src/types/postgres/generated/varchar.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/varbit.ts Regenerated PG type bindings to use runtime.funcCall/opCall and new import paths.
src/types/postgres/generated/uuid.ts Regenerated PG type bindings to use runtime.funcCall/opCall and new import paths.
src/types/postgres/generated/unknown.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/txid_snapshot.ts Regenerated PG type bindings to use runtime.Srf and new runtime call helpers.
src/types/postgres/generated/tid.ts Regenerated PG type bindings to use runtime.funcCall/opCall and new import paths.
src/types/postgres/generated/regtype.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/regrole.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/regprocedure.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/regproc.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/regoperator.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/regoper.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/regnamespace.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/regdictionary.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/regconfig.ts Regenerated PG type bindings to use runtime.funcCall/opCall and new import paths.
src/types/postgres/generated/regcollation.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/regclass.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/refcursor.ts Regenerated PG type bindings import paths.
src/types/postgres/generated/pg_snapshot.ts Regenerated PG type bindings to use runtime.Srf and new runtime call helpers.
src/types/postgres/generated/pg_node_tree.ts Regenerated PG type bindings import paths.
src/types/postgres/generated/pg_ndistinct.ts Regenerated PG type bindings import paths.
src/types/postgres/generated/pg_mcv_list.ts Regenerated PG type bindings import paths.
src/types/postgres/generated/pg_dependencies.ts Regenerated PG type bindings import paths.
src/types/postgres/generated/pg_brin_minmax_multi_summary.ts Regenerated PG type bindings import paths.
src/types/postgres/generated/pg_brin_bloom_summary.ts Regenerated PG type bindings import paths.
src/types/postgres/generated/jsonpath.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/json.ts Regenerated PG type bindings to use runtime.funcCall/opCall/Srf and new import paths.
src/types/postgres/generated/gtsvector.ts Regenerated PG type bindings import paths.
src/types/postgres/generated/cidr.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/cid.ts Regenerated PG type bindings to use runtime.funcCall/opCall and new import paths.
src/types/postgres/generated/char.ts Regenerated PG type bindings to use runtime.funcCall/opCall and new import paths.
src/types/postgres/generated/bool.ts Regenerated PG type bindings to use runtime.funcCall/opCall and new import paths.
src/types/postgres/generated/anynonarray.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/anyenum.ts Regenerated PG type bindings to use runtime.funcCall/opCall and new import paths.
src/types/postgres/generated/anyelement.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/anycompatiblerange.ts Regenerated PG type bindings import paths.
src/types/postgres/generated/anycompatiblenonarray.ts Regenerated PG type bindings import paths.
src/types/postgres/generated/anycompatiblemultirange.ts Regenerated PG type bindings import paths.
src/types/postgres/generated/anycompatiblearray.ts Regenerated PG type bindings to use runtime.funcCall/opCall and new import paths.
src/types/postgres/generated/anycompatible.ts Regenerated PG type bindings to use runtime.funcCall and new import paths.
src/types/postgres/generated/any.ts Makes generated PG Any extend shared SqlValue and updates runtime call helpers.
src/types/postgres/generated/aclitem.ts Regenerated PG type bindings to use runtime.opCall and new import paths.
src/types/postgres/generate.ts Updates codegen to new Postgres module layout + new runtime helper names + dialect-aware typedef lookup.
src/types/postgres/deserialize.ts Adds Postgres deserialize/type registry registration.
src/types/meta.ts Introduces standalone meta symbol module to avoid load-order cycles.
src/types/match.test.ts Updates tests to new compile context API.
src/types/index.ts Transitional re-export to new Postgres barrel (./postgres).
src/types/deserialize.ts Replaces single registry with dialect-aware registry + registration API.
src/types/bool.ts Adds dialect-agnostic Bool marker interface + isBool helper.
src/types/bool-mixin.ts Adds shared bool operator helpers for dialect-specific Bool classes.
src/types/any.ts Extracts shared SqlValue base with dialect metadata and common typed-value behaviors.
src/tables/generate.ts Updates introspection import path to new Postgres location.
src/table.ts Switches column detection to dialect-agnostic isColumn from types/any.
src/driver.ts Adds dialect to Driver, introduces SqliteDriver adapter, and updates compile to dialect context.
src/builder/values.ts Replaces Any checks with SqlValue for VALUES builder row typing/coercion.
src/builder/update.ts Makes UPDATE builder dialect-aware (ctx threading) and uses shared Bool validation.
src/builder/sql.ts Introduces CompileContext, dialect-aware param emission, and a Cast node.
src/builder/sql.test.ts Updates compile tests to use pgCtx/sqliteCtx.
src/builder/query.test.ts Updates compile usage to CompileContext and adjusts validation expectations.
src/builder/insert.ts Threads compile context through bind/debug and updates getColumn import.
src/builder/delete.ts Threads compile context through bind/debug and uses shared Bool validation.
package.json Moves codegen script to Postgres module path and adds better-sqlite3 deps/types.
eslint.config.js Updates ignore path to new Postgres generated directory.
docs/ISSUES.md Documents RPC security considerations for cross-dialect/tenant typed value provenance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/driver.ts
Comment on lines +196 to +211
const normalizeValue = (v: unknown): string => {
if (v === null || v === undefined) {
// Preserved as null via cast; downstream checks for null-ness.
return v as unknown as string;
}
if (typeof v === "string") {return v;}
if (typeof v === "number" || typeof v === "bigint" || typeof v === "boolean") {return String(v);}
if (v instanceof Uint8Array) {
// Match PG bytea repr: \x-prefixed lowercase hex.
return "\\x" + Buffer.from(v).toString("hex");
}
return String(v);
};

const normalizeRow = (row: { [key: string]: unknown }): { [key: string]: string } =>
Object.fromEntries(Object.entries(row).map(([k, v]) => [k, normalizeValue(v)]));
Comment thread src/types/sqlite/deserialize.ts Outdated
Comment on lines +22 to +26
// Blob deserialize is a placeholder — the driver returns Buffer
// directly, so this path is only used if a raw string sneaks in.
// Real repr TBD (Buffer? Uint8Array? hex? see plan.md A1).
blob: { tsType: "object", deserialize: identity },
bool: { tsType: "boolean", deserialize: (raw) => raw === "1" },
Comment thread src/types/sqlite/table.test.ts Outdated
Comment on lines +42 to +46
const rows = await Users.from().execute(db);
expect(rows.length).toBe(2);
// Rows come out in insertion order (SQLite rowid ordering).
expect(rows[0]!.name).toBe("alice");
expect(rows[1]!.name).toBe("bob");
Comment thread src/types/sqlite/table.test.ts Outdated
Comment on lines +57 to +60
const rows = await Users.from()
.select((u) => ({ upperName: u.users.name.upper() }))
.execute(db);
expect(rows.map((r) => r.upperName)).toEqual(["ALICE", "BOB", "CAROL"]);
Comment thread src/driver.ts Outdated
@@ -9,6 +11,7 @@ export type QueryResult = { rows: { [key: string]: string }[] };
export type ExecuteFn = (query: Sql) => Promise<QueryResult>;
Comment thread src/builder/update.ts Outdated
Comment on lines 11 to 14
import { Database } from "../database";
import { Any, getColumn } from "../types/overrides/any";
import { SqlValue, getColumn } from "../types/any";
import { fn, expose } from "../exoeval/tool";
import z from "zod";
Comment thread src/builder/update.ts Outdated
Comment on lines +82 to +83
@expose(z.union([z.literal(true), fn.returns(z.custom<SharedBool<any>>(isBool))]))
where(fn: ((ns: Namespace<Name, T>) => SharedBool<any>) | true): UpdateBuilder<Name, T, R> {
ryanrasti and others added 18 commits July 1, 2026 15:34
…+values

Ground work for the provenance/tagging refactor (ISSUES.md #16).

- `CompileContext = { database: DatabaseRef }` (was `{ dialect }`).
  DatabaseRef is a minimal structural view (id: symbol, dialect) declared
  in sql.ts to sidestep a value-import cycle; the concrete `Database`
  class implements it.
- `Driver.execute(text, values)` — drivers no longer see the Sql tree.
  Compilation happens in `Database.#exec`, which supplies its own
  DatabaseRef as the ctx. `runInSingleConnection`'s callback signature
  updated in lockstep.
- `Ident` grows an optional `database` slot + provenance check in
  `bind(ctx)`. Untagged Idents (server-authored raw templates, escape
  hatch) still pass through; tagged Idents that mismatch throw.
  Table classes and their emitted Idents remain untagged in this
  commit; the sweep to route them through `db.scopedIdent` comes next.
- `Database` implements DatabaseRef (id, dialect, name?). Txn-bound
  Databases inherit their parent's id so a Table built outside the txn
  still compiles inside it. Adds `db.scopedIdent(name)` factory.
- `Bus` takes a `Database` (not `Driver`) so it can compile via db.execute.
- Test-helpers, live/db-live, bus tests, postgres/index.test all
  updated to use `db.execute(sql)` instead of `driver.execute(sql)`.

All 550 tests green. Placeholder emission now reads
`ctx.database.dialect`; sentinel pgCtx/sqliteCtx still work for
compile-only paths (SQL emission tests, .debug()).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `Table(name, opts, database?)` factory takes an optional DatabaseRef;
  `db.Table(name)` supplies `this`. Static `Table("users")` leaves the
  new `database` static undefined (escape hatch, existing callers
  unchanged).
- `TableBase.bind()` emits `new Ident(tableName, this.database)`.
  `TableBase.ident(name)` convenience for column-name idents from
  a Table class.
- Mutation builders (insert/update/delete) route their tableName +
  column-name idents through `instance.constructor.ident(...)`, so a
  db-scoped Table propagates its provenance to every emitted Ident.
- `reAlias` picks up the source Table's database when rewriting column
  refs into alias-qualified `Column(alias, Ident(name, db))`.
- `QB.debug()` / mutation `.debug()` derive ctx from the first
  Table's database instead of hardcoding `pgCtx`. Falls back to
  sentinel for non-Table sources (db.values etc.).

550 tests green. Provenance enforcement is now active for any query
whose Idents come from `db.Table(name)` — cross-db mismatches throw at
compile with a clear message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Constructors take an optional `dialect` slot; bind(ctx) throws if it
  disagrees with `ctx.database.dialect`.
- `runtime.funcCall` / `runtime.opCall` pass `type.dialect.name`, so
  every codegen'd PG method emits a properly-tagged Func/Op node
  automatically (no codegen output changes needed — the helper does it).
- `SqlValue.cast()` / `.from()` (via TypedParam) also pass dialect so
  CAST expressions carry provenance.
- `Srf` tracks dialect from its first column's class; its bind() now
  takes ctx and emits `new Ident(name, ctx.database)` for the
  function-name identifier.
- `Fromable.bind` widened to `(ctx?)`; `FinalizedQuery.bind` threads
  ctx to `t.source.bind(ctx)` so Srf sources see the compile ctx.

Untagged nodes (Func/Op without dialect — hand-authored `sql`
templates, raw fragments) still pass through unchecked. This is the
escape hatch for server-authored SQL that doesn't route through a
typed class.

550 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Widen `runtime.funcCall` / `runtime.opCall` `type` arg to
  `typeof SqlValue` so both PG's Any-typed classes and SQLite's
  SqliteValue-typed classes call sites typecheck.
- SQLite codegen switches from raw `sql\`abs(\${this.toSql()})\``
  templates to `funcCall("abs", [this], types.Integer)`. Every method
  now emits a Func node carrying `dialect: "sqlite"`, checked at
  compile.
- `argToSql` widened to duck-typed check (`.toSql()` presence) so it
  handles both dialects uniformly.
- Regenerated all four SQLite class files (integer, real, text, blob).

550 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fic Bool

- Update/DeleteBuilder track `matchAll: boolean` alongside `where`. The
  two are independent — a real predicate wins at compile if both are
  set. Bind emits `WHERE <pred>` when a predicate exists, no WHERE
  clause when only matchAll is set, and finalize throws when neither.
- Drops the `Bool.from(sql\`TRUE\`)` construction in the shorthand.
  Removes the last PG-nominal `Bool` residue from delete/update.
- Live-events update transformer emits `WHERE TRUE` explicitly for the
  before-CTE snapshot when the outer update used matchAll (its
  `FOR UPDATE` snapshot always needs a WHERE clause).

Semantics for `.where(pred).where(true)` and `.where(true).where(pred)`
are identical — the pred is honored. matchAll is only the "user
acknowledged unrestricted mutation" flag.

550 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `.in()` back to SqliteValue with a non-generic signature that
sidesteps the TS2589 that killed the PG-style `<T, Vs>(this: T, ...)`
version at SQLite's shallow class hierarchy. Trade-off: users don't
get "same-typed args required" narrowing at TS compile time; runtime
enforcement via `serialize()` still applies.

Two smoke tests validate the runtime path (member/non-member cases).
552 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace open-design entry #16 with a description of the shipped
  solution: DatabaseRef in CompileContext, Ident carries database via
  Table, dialect tags on Func/Op/Cast/Srf, matchAll flag on .where(true).
  Follow-ups (db.Int4-style factories, Database/Connection split) noted.
- Consolidate inline-import type refs (CompileContext) in query.ts.
- Drop unused sql import from table.ts.
- Curly braces around `if (v instanceof SqliteValue) return v;` per
  eslint rule.

`npm run check` green: 28 files / 552 tests + examples/basic 6/6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the spec: Database owns dialect + provenance identity + Table factory +
scopedIdent; Connection has the driver + execute/hydrate/transaction/close/
live. Database.attach(driver) returns a fresh Connection (multiple allowed).

- `new Database({ dialect })` constructs a metadata handle (module-load
  safe; no driver required).
- `const conn = db.attach(driver)` wires up a runtime Connection.
- Sql.execute(conn), builder .execute(conn) — explicit connection
  passing throughout; supports txn nesting via the tx passed to the
  callback.
- Driver.execute now takes `(text, values)` — Database compiles first
  with its own ctx (`{ database: this }`) then hands to driver. Drivers
  never see Sql nodes anymore.
- Connection ergonomically forwards `Table` / `from` / `values` /
  `scopedIdent` to its database (no need to thread `db` through code
  that already holds a `conn`).
- `typegres({ ... })` returns `{ db, conn }` for scripts / playground.
- Bus + runLiveIteration take Connection (they need `.execute`).

Test migration sweep: `db.execute(qb)` → `conn.execute(qb)`,
`.execute(db)` → `.execute(conn)`, `db.transaction` → `conn.transaction`,
etc. Across bus.test, db-live.test, database.test, hydrate.test,
events.test, postgres/index.test, rpc.test, query.test, sqlite tests,
examples/basic tests, demo.ts, and the README snippet.

`npm run check` green: 28 files / 552 tests + examples/basic 6/6.
Provenance still active — Idents carry Database id, dialects tagged on
Func/Op/Cast/Srf.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes it from the "still open" list; keeps only the instance-scoped
typed-value factory (`db.Int4.from(5)`) as the remaining provenance
follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up on the escape-hatch discussion.

Test sweep — no more untagged Tables in test files:
- Every `class X extends Table("name")` in the PG test suite becomes
  `class X extends db.Table("name")`, with `db` imported from
  test-helpers (or a locally-constructed `Database` for standalone
  unit tests like extractor.test).
- `db.Table<Name, LocalC>(name)` gets an optional per-class LocalC
  override so scope tests (each defining a distinct Principal shape)
  can override the app-wide `C` per class.
- `expectSqlEqual` auto-detects the compile ctx by walking the sql
  tree for the first tagged Ident, falls back to the suite's `db`,
  falls back to pgCtx.

Provenance-enforcement suite (src/provenance.test.ts, 18 tests):
- Ident from dbA rejected when compiled against dbB.
- Ident from dbA accepted against dbA.
- Untagged `sql.ident(...)` passes any ctx (escape hatch preserved).
- Column via db.Table carries the Table's database.
- PG Func in SQLite ctx → throws (`upper()`).
- SQLite Func in PG ctx → throws (`abs()`).
- PG Op in SQLite ctx → throws (`+`).
- Untagged Func/Op/UnaryOp/Cast pass any ctx.
- Tagged UnaryOp/Cast reject cross-dialect.
- Cross-dialect Ident smuggling caught (PG-tagged Ident inside a
  SQLite compile).

Also: Bus's `private db` field renamed to `private conn` (was a
Connection since the last commit; field name kept for smaller diff
previously — cleaned up now).

`npm run check` green: 29 files / 570 tests + examples/basic 6/6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
External code can no longer construct untagged Idents via
\`sql.ident("x")\`. The only public path is \`db.scopedIdent(name)\`,
which yields a provenance-checked Ident.

Internal library callers (live/events, live/extractor, live/bus,
live/events-ddl, builder/query output labels, builder/query AS clause
column names) continue to use \`new Ident(name)\` directly — the class
constructor still allows a \`database?\` optional for these
schema-agnostic local-scope uses (CTE aliases, output labels,
hardcoded event-table columns).

The library-internal path is unreachable from external code:
- \`sql\` is not exposed to exoeval callbacks (attackers over RPC can't
  even reference \`sql.ident\`).
- \`Ident\` is exported from builder/sql but requires import; external
  users don't touch it.

Callsite updates:
- test-helpers: \`sql.ident(schema)\` → \`db.scopedIdent(schema)\`
- postgres/index.test: \`sql.ident("result")\` → \`exec.database.scopedIdent(...)\`
- builder/sql.test: unit test shim that constructs \`new Ident(name)\`
  directly — kept for SQL emission testing without a real Database.
- provenance.test: added test that \`sql.ident\` is undefined on the
  public helper; kept the library-internal \`new Ident(name)\` test
  as documentation.

571 tests + 6 examples green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove stale isBool doc comment in query.ts.
- Strip narrative comments from types/meta.ts.
- eslint ignore: src/types/postgres/generated → src/types/*/generated
  (also covers SQLite codegen output).
- package.json codegen + codegen:check scripts run both PG and SQLite
  generators; codegen:check diffs both trees.
- Delete src/types/index.ts (transitional PG re-export). All callers
  updated to import from src/types/postgres directly.
- SQLite deserialize: blob tsType flipped from "object" to a sentinel
  string so the overload matcher can't wrongly accept arbitrary
  objects (Date, plain records) as blob primitives.
- isSetRow widened from `instanceof types.Any` (PG-only) to
  `instanceof SqlValue` — SQLite SqlValue instances now pass through
  the mutation SET-row validator.
- QueryResult row values typed `string | null` (SQL NULL is real; the
  previous `string` typing was unsound).
- QueryBuilder.scalar() detects a SQLite source Table via
  first-source.database.dialect and throws with a clear Phase 2.1
  pointer, replacing the "defer to bind" TODO.

29 files / 571 tests + examples/basic 6/6 all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d-type declares

- Each test in sqlite/table.test wrapped in a rollback transaction
  (same pattern as PG's withinTransaction) so tests are independent
  and idempotent — no id-numbering assumptions carry across.
- Ordering assertions explicitly ORDER BY id — no more assumption
  that SQLite returns rows in insertion order.
- expectTypeOf assertions on every test's row shape.
- SQLite codegen now emits `declare deserialize: (raw: string) => <ts>`
  per storage class (integer→number, real→number, text→string,
  blob→Uint8Array). Without it TsTypeOf<T> collapses to unknown and
  SQLite row assertions can't be typed.
- Regenerated the four SQLite class files.

Also tightens bool-mixin: boolAnd/boolOr drop the primitive-boolean
coercion path (no callers) and take a Bool value directly. PG + SQLite
Bool signatures narrow from `Bool | boolean` to `Bool` accordingly.

Runtime.ts: removed unused `import * as types from "./postgres"`.

29 files / 571 tests + examples 6/6 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
compile(root) walks the tree for a tagged Ident (yielding its
database) or a dialect-tagged Func/Op/Cast/Srf/TypedParam (yielding a
synthetic DatabaseRef with the matching dialect). Falls back to the
pgCtx sentinel only when the tree is fully untagged.

The second arg stays optional so callers that want to force a
particular ctx — e.g., sqlite/smoke.test's placeholder-emission
check — still work. Most `compile(qb, pgCtx)` call sites could now
drop the second arg without behavior change, but I've left them
untouched to keep this diff small.

571 tests + 6 examples green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parity with PG codegen shape:
- Every method with args gets `<M0 extends types.X<any> | number>` (or
  `| string` for text, plain type for blob) — accepts the typed value
  or a JS-native primitive.
- Runtime dispatch through runtime.match([args], [[[matchers], retType]])
  validates arg types + serializes primitives, then funcCall constructs
  a dialect-tagged Func node.
- Blob-typed args reject primitives (registry sentinel from prior
  commit; no meaningful JS blob primitive yet).
- Return type nullability tracked via `StrictNull<N | NullOf<M0> | ...>`
  from the actual method type params rather than opaque `typeof arg0`.

Runtime widening: match's ArgMatcher.type + MatchCase.retType + Srf's
column-type slot go from `typeof Any` (PG-only) to `typeof SqlValue`
(dialect-agnostic). Both PG and SQLite codegen output typechecks.

Regenerated all four SQLite class files.

Two smoke tests added: Integer.atan2(1) works with a JS number primitive;
passing a string throws with `No matching overload` at runtime.

573 tests + 6 examples green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cross-database provenance concern is now live in the code
(Ident/Func/Op/Cast/Srf tagging, sql.ident removed from public helper,
provenance-enforcement test suite). No open follow-up worth carrying
in the issues doc — instance-scoped typed-value factories are a
speculative nice-to-have, not an active gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `Bool as SharedBool` rename in delete/update/query was needed
back when both PG's concrete `Bool` class and the shared `Bool`
interface were imported side by side. Since the `.where(true)` refactor
removed the PG Bool import from the builders, the alias is redundant.

573 tests + 6 examples green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QueryBuilder.debug() had a bespoke "first table's database, else pgCtx"
probe — now it just calls compile(this), since FinalizedQuery.children()
already surfaces TableRefs and walkForDatabase can pick up the tagged
Ident from there.

Insert/Update/DeleteBuilder.debug() can't rely on the tree walk (their
Idents are created lazily inside Finalized*.bind() and aren't exposed via
children()), so they pass ctx explicitly from
instance.constructor.database. That field is always set for
db.Table-defined tables; the only untagged tables (TypegresLiveEvents)
are library-internal and never reach debug().

bind() on Insert/Update/DeleteBuilder still takes ctx and threads it to
the transformer's returned Sql — Sql.bind's abstract signature requires
it, and dropping it would break the ternary's inferred type.
ryanrasti added 6 commits July 2, 2026 16:45
Tables are always scoped to a Database — a table without one is
nonsensical. Enforce it:

- `TableBase.database` is now a required `DatabaseRef` (was optional).
- `Table(name, opts, database)` factory requires `database`.
- `TypegresLiveEvents` no longer extends `Table` — it doesn't use the
  builder API (no .from() / .insert() / etc.), only static utilities
  (`tableName`, `createTableSql`, `makeTransformer`). Rewriting it as
  a plain class lets us tighten TableBase without inventing a fake
  Database for the shadow-events table.

Sentinel DatabaseRefs are gone:

- `pgCtx` / `sqliteCtx` exports and the `walkForDatabase` auto-detect
  fallback in `compile()` are removed. `compile(root, ctx)` now
  requires ctx.
- `QueryBuilder` carries `database` in its options, threaded from
  `Table.from` / `db.from` / `db.values` at construction (spread
  through every chained builder). `QueryBuilder.debug()` uses it
  directly.
- Insert/Update/DeleteBuilder `debug()` methods drop the `!` on
  `constructor.database` since the field is now required.
- Test files construct a real `Database` for their compile ctx
  instead of importing sentinels; test-helpers throws when a ctx
  can't be resolved rather than falling back to pgCtx.

bind(ctx) on the builders is unchanged: ctx comes from the OUTER
compile — deriving it from the builder's own table would defeat
the provenance check for cross-database mixing.
- Extract `paramForDialect(dialect, position)` from compile()'s inline
  ternary. Exhaustive switch on DialectName forces a compile-time
  update when a new dialect is added.
- Rewrite the sql.ident-absence comment as a positive statement about
  the current design (why schema idents must carry a DatabaseRef)
  instead of referencing the removed helper.
- Add AGENTS.md rule: comments describe present code, not deleted code.
Bus's #poll query referenced the events table via an untagged
Ident — the last remaining place a schema-referencing Ident was
constructed without provenance in the query path. Store the
Connection's Database on the Bus and thread it through the Ident,
so the provenance check covers this path too.

DDL emission in events-ddl.ts still uses untagged Idents; that's
schema-setup code that runs once at boot and never mixes across
databases, but can be tightened next if we want zero untagged
schema idents anywhere.
The last untagged schema Idents in the codebase — the events-table
DDL and the live-mutation transformer's INSERT-into-events wrap —
now carry the emitting Database as provenance.

- `eventsTableSqlStatements(database)` and
  `TypegresLiveEvents.createTable{Sql,SqlStatements}(database)` take
  the target DatabaseRef; callers (`Connection.installLiveEvents`,
  live test helper) already have one at the call site.
- Insert/Update/DeleteBuilder expose a `database` getter so the
  live-events transformer can pull the target Database off the
  builder it wraps.
- events.ts `wrapInsertOrDelete` / `wrapUpdate` / `eventsInsertCte`
  thread `database` and tag every schema-referencing Ident (events
  shadow table, user table, columns, ctid). CTE/RETURNING alias
  Idents stay untagged — they're query-local names, not schema
  references.
Add `scopedIdent(name)` to the DatabaseRef interface (Database already
implements it). Sweep library-internal `new Ident(name, database)`
call sites — events-ddl, bus, events transformer, Table.bind/ident,
Func.bind, Srf.bind — to go through the interface method instead.

Untagged Idents (CTE aliases, output column labels, reAlias for non-
Table rows via db.values) still use `new Ident(name)` — there's no
database to route through.
Origin/main's `typeRegistry` (later split into per-dialect registries)
was a legacy of pre-per-class codegen — a central map of typname →
{tsType, deserialize} that the base SqlValue looked up at runtime.
Now that codegen emits real classes and there's already an
`overrides/` hook next to `generated/`, the registry is unnecessary
indirection. Move deserialize onto the class where it belongs.

Changes:

- `SqlValue.deserialize`: identity default typed as `unknown` so
  overrides can freely narrow the return type.
- `SqlValue.primitiveTs`: static string ("string" default) — the JS
  `typeof` accepted by serialize/match. Was `getTypeDef(...).tsType`.
- Concrete overrides for the non-string types:
    PG:     Bool, Int2, Int4, Float4, Float8, Oid
    SQLite: Bool, Integer, Real
  Each sets `static primitiveTs` + `override deserialize(...)` in
  one place — parser + return type + primitive check colocated.
- Codegen still emits `declare deserialize: (raw) => ${tsType}` on
  concrete classes WITHOUT an override, so `TsTypeOf<Text<1>>`
  narrows to `string`. Classes with overrides get no `declare` —
  the override's real return type is the source of truth.
- `runtime.match` reads `m.type.primitiveTs` directly.
- `DialectName` moves from the deleted `deserialize.ts` to a
  new one-line `src/types/dialect.ts`. All consumers (`driver.ts`,
  `database.ts`, `builder/sql.ts`, `types/any.ts`) rebind their imports.
- SQLite codegen: `updateBarrel` re-exports from `overrides/<name>.ts`
  when the file exists (matches how PG's `generateIndex` does it);
  `emitClassFile` receives the overrides root so the "declare or not"
  decision uses the same source of truth.

Deletions: three deserialize.ts files and every side-effect import
of them (`overrides/any.ts`, `sqlite/base.ts`).
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.

2 participants