Skip to content

TML-2816: namespace-aware DSL/ORM surface — additive slice#720

Open
SevInf wants to merge 36 commits into
mainfrom
explicit-namespace
Open

TML-2816: namespace-aware DSL/ORM surface — additive slice#720
SevInf wants to merge 36 commits into
mainfrom
explicit-namespace

Conversation

@SevInf
Copy link
Copy Markdown
Contributor

@SevInf SevInf commented Jun 4, 2026

Overview

Slice 01 of TML-2816 (always-qualified, namespace-aware DSL/ORM surface). Adds the explicit per-namespace accessors — sql.<ns>.<table> and orm.<ns>.<Model>additively: the existing flat surface is retained, and its removal + the facade projection are slice 02. Branch is rebased onto origin/main (incl. TML-2807 — SqlModelStorage.namespaceId + kind-agnostic storage hash).

Draft / WIP. See Remaining for slice 01 and Deferred to slice 02 below.

What's in this slice

  • SQL builder: Db<C> gains per-namespace facets (sql.<ns>.<table>) alongside the flat surface; a two-level proxy delegates to coordinate-aware resolvers.
  • ORM client: orm.<ns>.<Model> facets; per-namespace model resolution; ORM keys aligned to SQL storage-namespace keys.
  • ORM execution made namespace-aware end-to-end: metadata resolution → select + count CRUD → returning-row mutations → cross-namespace relation reads. Each resolves per-namespace; single-namespace paths byte-identical.
  • Coordinate-aware core resolvers + fail-fast on ambiguity: resolveStorageTable / codecRefForStorageColumn / resolveTableColumns take a namespace coordinate; threaded through every column/codec call site so the same bare table name in two namespaces with differing columns resolves correctly through read / write / aggregate paths (discriminating tests included).
  • Facade reachability: db.sql.<ns>.<table> / db.orm.<ns>.<Model> reach through the postgres + sqlite facades (incl. transaction / prepare), type-locked.
  • Same-bare-table-name pipeline: authoring (TS + PSL) now allows two same-bare-named tables across namespaces + a cross-namespace FK, emits, and validates; the execution-context codec registry is coordinate-keyed so such a contract loads and queries per-namespace via the coordinate accessors.

Why

A multi-namespace contract must be navigable by explicit namespace (auth.users vs public.users), including the case where the same bare table name appears in more than one namespace. This slice adds that surface additively and makes the SQL/ORM execution pipeline namespace-aware, building on TML-2807 (storage namespaceId) and TML-2605 (runtime qualification).

Deferred to slice 02 (the breaking cut + facade projection)

  • Facade projection (AC4/AC5): db aliased to the connector's default-namespace facet on single-namespace targets vs. the qualified surface on multi-namespace targets, driven solely by defaultNamespaceId. This is the "bare = default namespace" ergonomic, realized at the facade as the ADR-223 "caller that supplies the default" (runtime stays target-agnostic).
  • Removal of the flat builder-layer fallback (FR6 — the deliberate breaking change) + negative type tests (AC3).
  • ADR capturing the always-qualified surface + facade-aliasing + Db<C> facet construction (AC7).

Remaining for slice 01 (to finish on this draft)

  • Multi-namespace PGlite end-to-end proof on the coordinate path (now unblocked by the authoring + codec-registry work) — AC1 / AC2 / AC6.
  • Single-namespace regression + contract.json snapshot-unchanged (FR7).
  • Cross-namespace nested-relation writes remain an explicit non-goal of this slice.

Validation

  • pnpm build, pnpm typecheck (135/135), pnpm fixtures:check (no drift), pnpm lint:deps — green.
  • Package tests green except the mongodb-memory-server suites, which can't run on the dev host (NixOS) — must be confirmed in CI; they're outside this slice's changed surface.

Scope

Additive only — no flat-surface removal, no facade projection in this PR. Single-namespace behaviour is byte-identical throughout.

Refs: TML-2816

Summary by CodeRabbit

Release Notes

  • New Features

    • Added namespace support for SQL storage, contracts, and ORM operations, enabling same table/model names across multiple namespaces.
    • Tables, models, and relations can now be explicitly qualified by namespace for disambiguation.
    • ORM and SQL builder now support namespace-scoped access (e.g., db.public.users, orm.public.User).
    • Cross-namespace relations are now properly resolved and type-safe.
  • Bug Fixes

    • Improved error handling for ambiguous table names across namespaces.

SevInf added 30 commits June 4, 2026 13:43
Type-level test asserts the per-namespace facet and flat keys coexist on
Db<C> and that an undeclared namespace id is not a key. Runtime test uses a
two-namespace contract that declares the same bare table name in both
namespaces, asserting resolution discriminates by namespace coordinate and
does not fall back to a cross-namespace scan.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Db<C> becomes the intersection of the existing flat by-bare-name map and a
new per-namespace facet keyed by namespace id, exposed via the exported
Namespace<C, NsId> type. sql() resolves a declared namespace id to a
facet proxy whose table lookup is scoped to that namespace coordinate and
delegates qualification to TableProxyImpl's namespaceId argument; unknown
props fall through to the existing flat resolution path unchanged.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Type-level test asserts the per-namespace facet and flat keys coexist on the
orm client, that an undeclared namespace id is not a key, and that domain
namespace ids stay in lock-step with storage namespace ids. Runtime test uses
a two-namespace contract declaring the same bare model name in both
namespaces backed by different tables, asserting resolution discriminates by
namespace coordinate and does not fall back to the flat model scan.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
The orm client type becomes the intersection of the existing flat model
collection map and a new per-namespace facet keyed by domain namespace id,
exposed via the exported OrmNamespace type. orm() resolves a declared domain
namespace id to a facet proxy whose model lookup is scoped to that namespace
and threads the namespace-scoped table into the collection so the same bare
model name resolves to the correct table per namespace; unknown props fall
through to the existing flat model-resolution path unchanged.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
On a two-namespace contract declaring the same bare model name in both
namespaces with distinct field maps and tables, the metadata resolvers
(field->column, column->field, storage table) resolve within the named
namespace and discriminate per namespace. Flat bare-name metadata access on a
multi-namespace contract still throws (ambiguous). A collection constructed
with a namespace resolves its own table within that namespace.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
The Collection now carries its namespace coordinate (threaded from the
namespace facet via collection options), and the collection-contract metadata
resolvers (modelsOf and its consumers: field/column maps, relations,
polymorphism, storage table) accept an optional namespace and resolve within
contract.domain.namespaces[nsId].models when given one. Without a namespace
they keep the existing sole-namespace resolution, so single-namespace
execution is unchanged and flat bare-name access on a multi-namespace contract
still throws (ambiguous). Metadata caches are keyed by namespace so the same
bare model name resolves to distinct per-namespace metadata. No
contract-foundation change.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
On a two-namespace contract declaring the same bare model name in both
namespaces (distinct field maps and tables), select / insert / update / delete
execute against a mock runtime and produce per-namespace-correct query plans
(target table + namespace coordinate) and column mappings (returned rows
mapped with the namespace column map). Discriminating: fails if execution fell
back to default/first-match metadata.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…diaries

The Collection's namespaceId now flows into the execution-runtime helpers the
select/insert/update/delete paths traverse — row shaping (collection-runtime),
field/column mapping (collection-column-mapping), shorthand where + field-type
resolution (filters), the base model accessor (model-accessor), select dispatch
+ polymorphism resolution (collection-dispatch, query-plan-select), and modelOf
(collection-contract) — so each resolves model metadata within the collection's
namespace. The namespaceId-absent / sole-namespace path is unchanged, so
single-namespace execution is byte-identical and flat bare-name access on a
multi-namespace contract still throws. Cross-namespace relation-include targets
and returning-row/nested mutation paths are intentionally not threaded here.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
On a two-namespace contract declaring the same bare model name in both
namespaces (no relations, distinct field maps and tables), returning create and
deleteAll via orm.<ns>.<Model> execute against a mock runtime and return rows
mapped with the namespace's column map, with plans targeting the namespace's
table. Discriminating: fails under default/first-match metadata fallback.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
… path

The Collection's namespaceId now flows into the returning-row mutation
dispatch (collection-mutation-dispatch row shaping + the identity reload in
collection-dispatch) and the base-model metadata mutation-executor calls
(hasNestedMutationCallbacks / getRelationDefinitions base relations,
buildPrimaryKeyFilterFromRow, toFieldName), so create/createAll/update/
updateAll/delete/deleteAll resolve metadata within the collection's namespace.
Relation targets and the nested-mutation execution path keep default
resolution (cross-namespace nested-relation writes are out of slice scope).
Single-namespace execution is byte-identical.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
On a two-namespace contract where the same bare model name is declared in both
namespaces, a base model (public.Profile) declaring a cross-namespace relation
to auth.User: base CRUD on the relation-declaring model executes per-namespace,
and a cross-namespace include read resolves the target row within the target's
namespace (child rows mapped with the target namespace's column map).
Discriminating: fails if target resolution fell back to default/first-match.
Updates the resolveIncludeRelation shape assertion for the new
relatedNamespaceId field.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…space

ResolvedRelation now carries the relation target's namespace (relation.to's
cross-reference namespace), and resolveIncludeRelation / getRelationDefinitions
resolve the target model's table and field->column map within that namespace
instead of the default/first-match path that throws on a multi-namespace
contract. IncludeExpr carries the target namespace so cross-namespace include
reads shape child rows with the target namespace's column map. Base CRUD on a
relation-declaring model no longer eagerly resolves relation targets:
hasNestedMutationCallbacks enumerates relation names only. Single-namespace
behaviour is unchanged. Cross-namespace nested-relation writes remain out of
scope.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
For resolveStorageTable, codecRefForStorageColumn, and the sql-orm-client
storage-resolution wrappers, on a same-bare-table-name two-namespace contract
with differing columns: a namespace coordinate resolves strictly within that
namespace (discriminating per-namespace columns/codecs), an ambiguous bare name
without a coordinate throws a diagnostic naming the candidate namespaces, and a
unique bare name / single namespace resolves unchanged.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…aware

resolveStorageTable, codecRefForStorageColumn, and the sql-orm-client
storage-resolution wrappers gain an optional trailing namespaceId. When
supplied, the table is resolved strictly within that namespace (no scan).
When omitted, a bare name unique across namespaces resolves as before, but a
bare name declared in more than one namespace now throws a fail-fast
diagnostic naming the candidate namespaces instead of silently first-matching.
codecRefForStorageColumn reuses resolveStorageTable for the table lookup so the
strict/fail-fast behaviour is shared. Additive: every existing caller compiles
unchanged and single-namespace contracts have no ambiguity, so the fail-fast
never fires for them.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…are table names

A bare table name declared in two namespaces with differing columns must resolve the correct per-namespace columns and codecs through both sql.<ns>.<table> and orm.<ns>.<Model>, while bare flat access to the shared name fails fast as ambiguous.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…c resolution

Pass the namespace coordinate each call site already holds into the coordinate-aware storage resolvers so a bare table name declared in multiple namespaces resolves its own columns and codecs through sql.<ns>.<table>, orm.<ns>.<Model>, and the cross-namespace include child SELECT, rather than silently first-matching.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…heck

Pre-existing break in the postgres facade test's orm mock predating this slice; the intermediate unknown cast restores the workspace typecheck gate. Surfaced and fixed separately from the namespace-threading change.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…ble names

A bare table name declared in two namespaces with differing columns must resolve its own columns and codecs through the write paths too: orm.<ns>.<Model> create/delete returning projections and sql.<ns>.<table>.insert() param values. Without the coordinate these throw ambiguous, so the assertions discriminate per namespace.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…l-builder insert

Pass the namespace coordinate each write call site already holds into the coordinate-aware storage resolvers: query-plan-mutations (insert/create/update/delete/upsert and their count/split variants), the collection write call sites, primary-key resolution and the where-binding entry, plus InsertQueryImpl param-value resolution from its table source. A bare table name declared in multiple namespaces now resolves its own columns and codecs through orm.<ns>.<Model> create/createAll/update/updateAll/delete/deleteAll/upsert and sql.<ns>.<table>.insert(), instead of throwing ambiguous.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…ed bare table names

orm.<ns>.<Model>.aggregate() and grouped aggregate on a same-bare-table-name multi-namespace contract must resolve per-namespace columns and codecs. Without the coordinate the builder and compile path throw ambiguous / mis-resolve the default namespace, so the assertions discriminate per namespace.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…te read path

Pass the namespace coordinate each aggregate call site already holds into the coordinate-aware resolvers: query-plan-aggregate (compileAggregate / compileGroupedAggregate and their codecRefForStorageColumn / tableSourceForContract calls), the aggregate and having builders' field-to-column maps, and the grouped result row mapping. A bare table/model name declared in multiple namespaces now resolves its own columns and codecs through orm.<ns>.<Model>.aggregate() and grouped aggregates instead of throwing ambiguous or mis-resolving the default namespace.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…lar update/delete/upsert

Singular orm.<ns>.<Model>.where(...).update()/.delete() and PK-default upsert() resolve identity/conflict columns within the namespace on a same-bare-table-name multi-namespace contract. Without the coordinate the identity resolver swallows the ambiguous throw into a misleading no-primary-key error and the upsert PK-fallback throws ambiguous, so the assertions discriminate per namespace.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
… resolvers

Thread the optional namespace coordinate into resolveRowIdentityColumns and the resolveUpsertConflictColumns PK fallback, and pass it from the singular update/delete and include-on-mutation call sites. A bare table name declared in multiple namespaces now resolves its own primary-key/unique identity through orm.<ns>.<Model>.where(...).update()/.delete() and PK-default upsert(), instead of throwing ambiguous or masking it as a misleading no-primary-key error. resolveRowIdentityColumns now re-throws the ambiguous diagnostic rather than swallowing it to an empty identity set.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…e client

Lock that the additive namespaced surface reaches through PostgresClient: db.sql.<ns>.<table> and db.orm.<ns>.<Model> resolve alongside the flat surface, including inside transaction(...) and the prepare(...) callback, and an undeclared namespace id is a type error.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…client

Lock that the additive namespaced surface reaches through SqliteClient: db.sql.<ns>.<table> and db.orm.<ns>.<Model> resolve alongside the flat surface, including the prepare(...) callback, and an undeclared namespace id is a type error.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…og, learnings

Orchestrator project bookkeeping for the additive namespaced-DSL slice (TML-2816). Project artifacts only; no source changes.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Two models in different namespaces may now map to the same bare storage
table name (e.g. public.users + auth.users). Remove the flat-storage-era
cross-namespace dup-table guard in buildSqlContractFromDefinition: storage
tables are keyed per-namespace under storage.namespaces[ns].tables, so the
two tables no longer collide in the data structure.

Resolve foreign-key and relation targets by explicit namespace coordinate
when one is supplied (FK references.namespaceId / new RelationNode.toNamespaceId)
rather than by bare model name, so a relation to a model whose bare name also
exists in another namespace resolves to the correct coordinate.

Key aggregate roots by a namespace-qualified key only when two models share a
bare table name; single-namespace contracts keep their bare-tableName keys
unchanged.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
The PSL interpreter keyed models, mappings, resolved-field sets, and the
patch/polymorphism record by bare model name, so two models sharing a bare
name across namespaces (e.g. public.User + auth.User) collided: the resolved
namespace was stamped last-wins (both lowered into one namespace) and the
flatten step hard-threw on the duplicate bare name.

Key model mappings, resolved-field sets, the patch record, and the
re-partition lookup by (namespaceId, modelName) coordinate. Resolve a
namespace-qualified relation target by its explicit coordinate rather than by
bare name, and carry the resolved target namespace onto the lowered relation
so a cross-namespace relation resolves to the correct coordinate.

Existing single-namespace and unique-name cross-namespace behaviour is
unchanged: bare relation targets and polymorphism still resolve by bare name.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Thread a namespace coordinate through codecRefForColumn (CodecDescriptorRegistry)
and forColumn (ContractCodecRegistry), delegating to the already
coordinate-aware codecRefForStorageColumn. The execution-context
pre-population walk and the column-codec integrity check now pass the
namespace they are iterating, so two tables sharing a bare name across
namespaces (e.g. public.users + auth.users) resolve to their own
per-namespace columns/codecs instead of throwing an ambiguity error at
createExecutionContext.

The coordinate is an optional parameter, so single-namespace contracts
resolve identically to before.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…bare=default design)

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Adds namespace-aware resolution across contract authoring, storage lookups, codecs, SQL builder, and ORM. Functions gain namespaceId parameters, ambiguity checks added, and proxies expose db..

and orm... Tests and docs updated, including cross-namespace FK/relation handling.

Changes

Namespace-scoped SQL/ORM surface

Layer / File(s) Summary
Core storage resolution and ambiguity packages/2-sql/1-core/contract/src/resolve-storage-table.ts, tests
Authoring and contracts become coordinate-based packages/2-sql/2-authoring/... (interpreter, field/relation resolution, TS builder), tests
Codec registry and relational core namespace support packages/2-sql/4-lanes/relational-core/...
SQL builder namespace facets and planning packages/2-sql/4-lanes/sql-builder/...
ORM client namespace facet and mapping packages/3-extensions/sql-orm-client/...
Runtime/context and end-to-end tests packages/2-sql/5-runtime/..., test/integration/...

Sequence Diagram(s)

sequenceDiagram
  rect rgba(66, 135, 245, 0.5)
  participant Dev as Client Code
  participant ORM as ORM Client
  participant Builder as SQL Builder
  participant Runtime as Execution Runtime
  participant DB as Database
  end
  Dev->>ORM: orm.public.User.all()
  ORM->>Builder: compileSelect(ns="public", table="users")
  Builder->>Runtime: codecRefForColumn("public","users","id")
  Runtime->>DB: SELECT ... FROM "public"."users"
  DB-->>Runtime: rows
  Runtime-->>ORM: rows with codecs
  ORM-->>Dev: mapped fields per namespace
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • prisma/prisma-next#534 — Introduced storage namespaces and FK endpoint coordinates that this PR’s namespace-aware resolution builds upon.

Suggested reviewers

  • wmadden
  • aqrln

Poem

A rabbit hops through schemas twain,
Public fields and auth’s domain.
It sniffs the path, resolves by name—
“But namespace first,” becomes the game.
With codecs tuned and plans in sight,
It joins across the moonlit night.
Hop, commit—all tables right. 🐇✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch explicit-namespace

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

size-limit report 📦

Path Size
postgres / no-emit 146.12 KB (+0.62% 🔺)
postgres / emit 117.85 KB (+0.71% 🔺)
mongo / no-emit 75.97 KB (0%)
mongo / emit 70.78 KB (0%)
cf-worker / no-emit 175.31 KB (+0.23% 🔺)
cf-worker / emit 143.94 KB (+0.19% 🔺)

Author a same-bare-table-name (public.users + auth.users with differing columns) + cross-namespace-FK contract through the TS builder, emit it to contract.json, load it via the postgres facade, and drive it against PGlite through the explicit coordinate accessors only:

- sql.<ns>.<table> select/insert/update/delete on both namespaces, asserting per-schema qualification ("public"."users" vs "auth"."users") and per-namespace-correct columns (email vs token).

- orm.<ns>.<Model> create/find/update/delete on both namespaces returning per-namespace-correct rows.

- the cross-namespace public.Profile.user relation read returns the auth.User row (distinct token column).

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 4, 2026

Open in StackBlitz

@prisma-next/extension-author-tools

npm i https://pkg.pr.new/@prisma-next/extension-author-tools@720

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/@prisma-next/mongo-runtime@720

@prisma-next/family-mongo

npm i https://pkg.pr.new/@prisma-next/family-mongo@720

@prisma-next/sql-runtime

npm i https://pkg.pr.new/@prisma-next/sql-runtime@720

@prisma-next/family-sql

npm i https://pkg.pr.new/@prisma-next/family-sql@720

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/@prisma-next/extension-arktype-json@720

@prisma-next/middleware-cache

npm i https://pkg.pr.new/@prisma-next/middleware-cache@720

@prisma-next/mongo

npm i https://pkg.pr.new/@prisma-next/mongo@720

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/@prisma-next/extension-paradedb@720

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/@prisma-next/extension-pgvector@720

@prisma-next/extension-postgis

npm i https://pkg.pr.new/@prisma-next/extension-postgis@720

@prisma-next/postgres

npm i https://pkg.pr.new/@prisma-next/postgres@720

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/@prisma-next/sql-orm-client@720

@prisma-next/sqlite

npm i https://pkg.pr.new/@prisma-next/sqlite@720

@prisma-next/target-mongo

npm i https://pkg.pr.new/@prisma-next/target-mongo@720

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/@prisma-next/adapter-mongo@720

@prisma-next/driver-mongo

npm i https://pkg.pr.new/@prisma-next/driver-mongo@720

@prisma-next/contract

npm i https://pkg.pr.new/@prisma-next/contract@720

@prisma-next/utils

npm i https://pkg.pr.new/@prisma-next/utils@720

@prisma-next/config

npm i https://pkg.pr.new/@prisma-next/config@720

@prisma-next/errors

npm i https://pkg.pr.new/@prisma-next/errors@720

@prisma-next/framework-components

npm i https://pkg.pr.new/@prisma-next/framework-components@720

@prisma-next/operations

npm i https://pkg.pr.new/@prisma-next/operations@720

@prisma-next/ts-render

npm i https://pkg.pr.new/@prisma-next/ts-render@720

@prisma-next/contract-authoring

npm i https://pkg.pr.new/@prisma-next/contract-authoring@720

@prisma-next/ids

npm i https://pkg.pr.new/@prisma-next/ids@720

@prisma-next/psl-parser

npm i https://pkg.pr.new/@prisma-next/psl-parser@720

@prisma-next/psl-printer

npm i https://pkg.pr.new/@prisma-next/psl-printer@720

@prisma-next/cli

npm i https://pkg.pr.new/@prisma-next/cli@720

@prisma-next/cli-telemetry

npm i https://pkg.pr.new/@prisma-next/cli-telemetry@720

@prisma-next/emitter

npm i https://pkg.pr.new/@prisma-next/emitter@720

@prisma-next/migration-tools

npm i https://pkg.pr.new/@prisma-next/migration-tools@720

prisma-next

npm i https://pkg.pr.new/prisma-next@720

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/@prisma-next/vite-plugin-contract-emit@720

@prisma-next/mongo-codec

npm i https://pkg.pr.new/@prisma-next/mongo-codec@720

@prisma-next/mongo-contract

npm i https://pkg.pr.new/@prisma-next/mongo-contract@720

@prisma-next/mongo-value

npm i https://pkg.pr.new/@prisma-next/mongo-value@720

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/@prisma-next/mongo-contract-psl@720

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/@prisma-next/mongo-contract-ts@720

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/@prisma-next/mongo-emitter@720

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/@prisma-next/mongo-schema-ir@720

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/@prisma-next/mongo-query-ast@720

@prisma-next/mongo-orm

npm i https://pkg.pr.new/@prisma-next/mongo-orm@720

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/@prisma-next/mongo-query-builder@720

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/@prisma-next/mongo-lowering@720

@prisma-next/mongo-wire

npm i https://pkg.pr.new/@prisma-next/mongo-wire@720

@prisma-next/sql-contract

npm i https://pkg.pr.new/@prisma-next/sql-contract@720

@prisma-next/sql-errors

npm i https://pkg.pr.new/@prisma-next/sql-errors@720

@prisma-next/sql-operations

npm i https://pkg.pr.new/@prisma-next/sql-operations@720

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/@prisma-next/sql-schema-ir@720

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/@prisma-next/sql-contract-psl@720

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/@prisma-next/sql-contract-ts@720

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/@prisma-next/sql-contract-emitter@720

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/@prisma-next/sql-lane-query-builder@720

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/@prisma-next/sql-relational-core@720

@prisma-next/sql-builder

npm i https://pkg.pr.new/@prisma-next/sql-builder@720

@prisma-next/target-postgres

npm i https://pkg.pr.new/@prisma-next/target-postgres@720

@prisma-next/target-sqlite

npm i https://pkg.pr.new/@prisma-next/target-sqlite@720

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/@prisma-next/adapter-postgres@720

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/@prisma-next/adapter-sqlite@720

@prisma-next/driver-postgres

npm i https://pkg.pr.new/@prisma-next/driver-postgres@720

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/@prisma-next/driver-sqlite@720

commit: 94b3518

if (ormCallCount === 1) return { lane: 'orm' };
return txOrmProxy;
}) as typeof ormMock);
}) as unknown as typeof ormMock);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Why is this cast now necessary?

SevInf added 5 commits June 4, 2026 17:02
…mn resolution

Reorder codecRefForColumn and ContractCodecRegistry.forColumn from
(table, column, namespaceId?) to (namespace, table, column). The namespace
coordinate now leads and is always supplied: a leading optional is not valid
TS, and the always-qualified model wants the coordinate threaded explicitly at
every call site rather than relying on a sole-namespace fallback.

Updates the CodecDescriptorRegistry interface + builder, the
ContractCodecRegistry interface + runtime impl, every internal caller in the
execution-context codec walk, and the tests/docs for the new order. The
storage-first helpers (codecRefForStorageColumn, resolveStorageTable) keep
their shape and are untouched.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Remove six section-banner comments that merely restated the operation
performed by the block that follows (insert/update/delete/create per
namespace), per the repo rule preferring code that expresses its intent.
The why-comments (distinct columns prove qualification; the included row
is the auth.User row) are retained. Comment-only change.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…uilder mutation resolvers

codecRefForStorageColumn, codecRefFor, tableToScope, buildParamValues and buildSetExpressions now take a required namespace ahead of the table/column coordinate. The sql-builder mutation path threads the proxy's resolved namespace into InsertQueryImpl instead of reading the optional TableSource.namespaceId, so single-namespace behaviour is unchanged. The bare-scan resolveStorageTable/resolveTableForFlatName exception is untouched.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…table resolvers

Complete the "namespaceId required + leading" refactor in sql-orm-client so
every resolver that resolves a specific model/table takes a required,
namespace-leading coordinate (namespace before model/table/column). This
clears the silent swap left by relational-core's codecRefForStorageColumn
flip: the in-package callers (query-plan-select/mutations/aggregate,
where-binding, model-accessor) now pass (storage, namespaceId, table, column)
in the new order rather than the stale (storage, table, column, namespaceId).

- Tighten Collection.namespaceId, CollectionInit.namespaceId,
  ResolvedRelation.toNamespace, ResolvedIncludeRelation.relatedNamespaceId and
  IncludeExpr.relatedNamespaceId to required string; CrossReference.namespace
  is a required NamespaceId so the | undefined fallbacks were dead.
- Flip every resolver in collection-contract, storage-resolution,
  collection-runtime, collection-column-mapping, model-accessor, filters,
  query-plan-*, aggregate-builder, grouped-collection, collection-dispatch,
  collection-mutation-dispatch and mutation-executor to namespace-leading,
  threading the parent namespace and relation.toNamespace through the nested
  relation accessors and the create/update mutation graph.
- Drive the flat-ORM surface through soleDomainNamespaceId(contract.domain):
  single-namespace resolves; multi-namespace flat access throws (bare=default
  is deferred). Namespace-qualified access (orm.<ns>.<Model>) is unchanged.
- Remove the now-dead bare-name fallbacks (domainModelsAtDefaultNamespace in
  modelsOf, resolveDomainModel* in resolveModelTableName/modelOf, the
  metadataCacheKey undefined arm) and the unused resolveDomainModelForContract.
- where-binding stays a contract-free AST binder: its nested-subquery
  recursion has no namespace, so it keeps an optional coordinate and resolves
  the column's owning namespace before stamping the codec.
- Move the test surface (package + integration sql-orm-client suites) onto the
  required coordinate; obsolete bare-resolution-throws cases are retargeted to
  the new absent-namespace and flat-access guards.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…are=default, PK-fallback discrimination)

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
@SevInf SevInf marked this pull request as ready for review June 4, 2026 19:05
@SevInf SevInf requested a review from a team as a code owner June 4, 2026 19:05
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
packages/3-extensions/sql-orm-client/src/orm.ts (1)

204-217: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Custom collection key validation is still default-namespace-only.

Line 204 validates collections keys against domainModelsAtDefaultNamespace(contract.domain), so a valid model that exists only in another namespace (for example, auth.Session) is rejected in multi-namespace contracts. This breaks custom collection registration for the new namespaced surface.

💡 Suggested fix
 import {
   type Contract,
-  domainModelsAtDefaultNamespace,
   soleDomainNamespaceId,
 } from '`@prisma-next/contract/types`';
@@
 function createCollectionRegistry<
   TContract extends Contract<SqlStorage>,
   Collections extends Partial<Record<string, AnyCollectionClass>>,
 >(contract: TContract, collections: Collections | undefined): Map<string, AnyCollectionClass> {
   const registry = new Map<string, AnyCollectionClass>();
   if (!collections) {
     return registry;
   }
 
-  const models = domainModelsAtDefaultNamespace(contract.domain);
+  const modelNames = new Set(domainModelNames(contract));
   for (const [key, collectionClass] of Object.entries(collections)) {
@@
-    if (!Object.hasOwn(models, key)) {
+    if (!modelNames.has(key)) {
       throw new Error(
-        `No model found for custom collection '${key}'. Available models: ${Object.keys(models).join(', ')}`,
+        `No model found for custom collection '${key}'. Available models: ${[...modelNames].join(', ')}`,
       );
     }
     registry.set(key, collectionClass);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/3-extensions/sql-orm-client/src/orm.ts` around lines 204 - 217, The
validation currently uses domainModelsAtDefaultNamespace(contract.domain) which
only returns default-namespace models so namespaced models (e.g.,
"auth.Session") get rejected; replace that lookup with the namespace-aware model
map (use the function that returns all domain models including namespaces —
e.g., domainModels(contract.domain) or the equivalent namespaced lookup) when
building the models variable and then keep the existing isCollectionClass and
Object.hasOwn(models, key) checks so custom collection keys validate against the
full, namespaced model set; update the reference in the block where models is
defined and used (the models variable, domainModelsAtDefaultNamespace call, and
the loop over collections) to use the namespace-aware lookup.
packages/2-sql/2-authoring/contract-psl/src/interpreter.ts (2)

1517-1528: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Bare-name namespace maps still collide same-named models.

Both modelNamespaceIds and the fallback modelMappings overwrite earlier entries by bare model.name. That means auth.User and public.User cannot coexist safely for any unqualified lookup: relation targets and resolvePolymorphism() will pick whichever namespace was inserted last, not the declaring model’s coordinate. In practice, auth.Session.user User @relation(...) can end up targeting public.User, and discriminator/base patches can land on the wrong model.

Keep these lookups coordinate-aware end-to-end, or resolve unqualified names relative to the declaring model’s namespace and emit an ambiguity diagnostic when multiple coordinates still match.

Also applies to: 1626-1639

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts` around lines 1517
- 1528, The bug is that modelNamespaceIds and fallback modelMappings use bare
model.name keys causing collisions between same-named models across namespaces
(e.g., auth.User vs public.User); update lookups to be coordinate-aware: change
where entries are set (in the loop that builds modelNamespaceIds/modelMappings
and where resolvePolymorphism() or relation resolution uses them) to use a
fully-qualified key (e.g., `${namespace.name}.${model.name}` or the model
coordinate from modelEntries) instead of model.name, and when code paths accept
unqualified names keep resolving them relative to the declaring model’s
namespace (using namespaceId from modelEntries) and emit an ambiguity diagnostic
if multiple qualified matches exist so relation targets and polymorphism patches
always bind to the correct model.

1118-1124: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Relation metadata is only half-qualified here.

This threads targetNamespaceId, but the declaring side still stays bare. When two namespaces both contain User, the later indexFkRelations / model-relation assembly path cannot distinguish auth.User from public.User, so relations/backrelations from one namespace can be attached to the other.

Please carry the declaring model’s namespace (and the same coordinate on backrelation candidates) through this metadata so the later indexing step can stay namespace-stable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts` around lines 1118
- 1124, The relation metadata being pushed via resultFkRelationMetadata
currently includes targetNamespaceId but omits the declaring model’s namespace,
causing ambiguity between same-named models in different namespaces; update the
object pushed in the resultFkRelationMetadata (the block that sets
declaringModelName, declaringFieldName, declaringTableName, targetModelName,
targetTableName and ...ifDefined('targetNamespaceId', targetNamespaceId)) to
also include the declaringNamespaceId (e.g.,
...ifDefined('declaringNamespaceId', declaringNamespaceId) or the appropriate
variable that holds the declaring model’s namespace), and mirror this change
wherever backrelation candidate metadata is assembled so both declaring and
target sides carry their namespace coordinates for use by indexFkRelations and
model-relation assembly.
packages/3-extensions/sql-orm-client/src/collection-contract.ts (1)

318-331: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate relation.to.namespace before accepting a relation entry.

At Line 330, toNamespace is assigned without checking that rel.to.namespace is a string. A malformed relation can pass this guard and fail later with a misleading namespace-resolution error instead of being treated as malformed.

Suggested fix
     if (
       !rel.to ||
       typeof rel.to !== 'object' ||
       typeof rel.to.model !== 'string' ||
+      typeof rel.to.namespace !== 'string' ||
       !Array.isArray(localFields) ||
       !Array.isArray(targetFields)
     ) {
       continue;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/3-extensions/sql-orm-client/src/collection-contract.ts` around lines
318 - 331, The relation entry accepts rel.to.namespace without validating its
type; update the guard that checks rel and its fields (the if that currently
validates rel.to, rel.to.model, localFields, targetFields) to also require
typeof rel.to.namespace === 'string' (or allow undefined explicitly if
intended), and if that check fails continue skipping the relation; then assign
toNamespace from rel.to.namespace in the resolved[name] object as before so
malformed namespace values are treated as invalid relations rather than causing
later namespace-resolution errors.
packages/2-sql/4-lanes/relational-core/src/codec-ref-for-column.ts (1)

39-48: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope enum typeRef fallback to the requested namespace.

After namespaceId became required, this fallback still scans all namespaces and can return the wrong enum entry when two namespaces define the same typeRef. Keep this lookup namespace-qualified.

🔧 Proposed fix
-    if (!instance) {
-      for (const ns of Object.values(storage.namespaces)) {
-        const nsEnums = (ns as { enum?: Record<string, unknown> }).enum;
-        if (nsEnums) {
-          const nsEntry = nsEnums[columnDef.typeRef];
-          if (nsEntry !== undefined) {
-            instance = nsEntry;
-            break;
-          }
-        }
-      }
-    }
+    if (!instance) {
+      const nsEnums = (
+        storage.namespaces[namespaceId] as { enum?: Record<string, unknown> } | undefined
+      )?.enum;
+      if (nsEnums) {
+        instance = nsEnums[columnDef.typeRef];
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/2-sql/4-lanes/relational-core/src/codec-ref-for-column.ts` around
lines 39 - 48, The code currently scans all storage.namespaces and can pick the
wrong enum when multiple namespaces define the same columnDef.typeRef; instead,
restrict lookup to the requested namespace using the required namespace
identifier on the column definition (e.g. columnDef.namespaceId). Change the
logic that iterates Object.values(storage.namespaces) to directly index
storage.namespaces[columnDef.namespaceId] (or the corresponding namespaceId
variable), verify that namespace exists, then read its enum map and pick
nsEnums[columnDef.typeRef] into instance; remove the cross-namespace loop so the
lookup is namespace-qualified.
packages/2-sql/5-runtime/src/sql-context.ts (1)

550-567: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Include namespace in codec instance site identity.

The registry lookup is now namespace-scoped, but usedAt entries and inline instance names are still keyed only by (table, column). Same bare table names across namespaces can collapse context for stateful codecs and leak cross-namespace instance metadata.

🔧 Suggested direction
-        const site = { table: tableName, column: columnName };
+        const site = { namespaceId, table: tableName, column: columnName };

           const name =
             ref.typeParams !== undefined
-              ? `<col:${tableName}.${columnName}>`
+              ? `<col:${namespaceId}.${tableName}.${columnName}>`
               : `<codec:${ref.codecId}>`;

Also thread namespaceId through collectTypeRefSites(...) and extend SqlCodecInstanceContext['usedAt'] to carry namespace so contexts remain unambiguous end-to-end.

Based on learnings, buildContractCodecRegistry must avoid leaking shared codec context across columns so column-aware dispatch remains correct.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/2-sql/5-runtime/src/sql-context.ts` around lines 550 - 567, The
usedAt/site identity must include namespaceId to prevent cross-namespace
collisions: change the site object created in the loop to include namespaceId
(e.g., { namespace: namespaceId, table: tableName, column: columnName }), update
the type of SqlCodecInstanceContext['usedAt'] (and any usedAtByKey maps) to
store sites with namespace, and adjust nameByKey generation to incorporate
namespace when producing inline names for per-column codecs; then thread
namespaceId through collectTypeRefSites(...) and into buildContractCodecRegistry
so every place that records or reads usedAt or builds instance names uses the
namespace-aware site shape.
packages/3-extensions/sql-orm-client/src/collection-dispatch.ts (1)

208-216: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Thread namespaceId into identity-filter binding.

Line 215 still builds identity filters without namespace context, and Lines 257/272 bind that filter via bindWhereExpr(...) without namespaceId. In multi-namespace contracts with duplicate table names, mutation read-back can fail on ambiguous table resolution (or bind against the wrong table context).

🔧 Proposed fix
 export function reloadMutationRowsByIdentities<Row>(options: {
@@
-  const identityFilter = buildIdentityInFilter(contract, tableName, identityColumns, identityRows);
+  const identityFilter = buildIdentityInFilter(
+    contract,
+    namespaceId,
+    tableName,
+    identityColumns,
+    identityRows,
+  );
@@
 function buildIdentityInFilter(
   contract: Contract<SqlStorage>,
+  namespaceId: string,
   tableName: string,
   identityColumns: readonly string[],
   identityRows: readonly Record<string, unknown>[],
 ): AnyExpression | undefined {
@@
-    return bindWhereExpr(
-      contract,
-      BinaryExpr.in(ColumnRef.of(tableName, singleColumn), ListExpression.fromValues(values)),
-    );
+    return bindWhereExpr(
+      contract,
+      BinaryExpr.in(ColumnRef.of(tableName, singleColumn), ListExpression.fromValues(values)),
+      namespaceId,
+    );
@@
-  return bindWhereExpr(contract, OrExpr.of(tuples));
+  return bindWhereExpr(contract, OrExpr.of(tuples), namespaceId);
 }

Also applies to: 242-273

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/3-extensions/sql-orm-client/src/collection-dispatch.ts` around lines
208 - 216, The identity filter is being built and later bound without the
namespace context, causing ambiguous table resolution in multi-namespace
contracts; update the call sites so namespaceId is threaded into the filter
creation and binding: modify buildIdentityInFilter(...) invocation to accept
namespaceId (e.g. buildIdentityInFilter(contract, namespaceId, tableName,
identityColumns, identityRows) or add namespaceId parameter) and ensure
subsequent uses (where bindWhereExpr(...) is called) pass namespaceId when
binding the identityFilter; update the relevant functions (buildIdentityInFilter
and bindWhereExpr call-sites) and their signatures to accept and propagate
namespaceId while preserving existing parameters like contract, tableName,
identityColumns, and identityRows.
🧹 Nitpick comments (1)
test/integration/test/namespaced-accessors-e2e.integration.test.ts (1)

1-28: ⚡ Quick win

Trim long narrative comments and link to canonical docs instead.

The top-level and mid-file prose blocks are overly verbose for test code; keep a short intent comment and link to the canonical design/ADR doc for details.

As per coding guidelines: **/*.{ts,tsx,js,jsx}: "Don't add comments if avoidable, prefer code that expresses its intent. Prefer links to canonical docs over long comments."

Also applies to: 136-142

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/integration/test/namespaced-accessors-e2e.integration.test.ts` around
lines 1 - 28, Replace the long top-level and mid-file narrative comments in
namespaced-accessors-e2e.integration.test.ts with a single short intent line
describing the test (e.g., "E2E test for explicit namespaced accessors across
public/auth schemas") and add a single link to the canonical design/ADR (replace
with the project ADR URL), removing the verbose prose blocks at the top and
around lines 136-142; keep the test behavior and fixtures unchanged (target the
long block comment and the subsequent mid-file comment blocks that describe the
same intent).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/2-sql/4-lanes/sql-builder/src/runtime/sql.ts`:
- Around line 33-49: The current proxy resolution gives namespace ids priority
via Object.hasOwn(storage.namespaces, prop) which hides a flat table when a
namespace id equals a flat table name; update the branch that checks
Object.hasOwn(storage.namespaces, prop) to also call
resolveTableForFlatName(storage, prop) (or otherwise check for a flat table) and
if both a namespace and a flat table are present, throw a clear collision error
instead of silently choosing the namespace; otherwise proceed to create the
namespace proxy (using resolveTableInNamespace and TableProxyImpl) as
before—this makes db.<name> fail fast on namespace/table key collisions.

In `@packages/3-extensions/sql-orm-client/src/query-plan-meta.ts`:
- Around line 24-30: The catch block in query-plan-meta.ts currently checks
error.message.includes('ambiguous') case-sensitively; change that check to a
case-insensitive match (e.g. use a case-insensitive regex like /\bambiguous\b/i
or compare error.message.toLowerCase().includes('ambiguous')) so any
capitalization or minor wording changes still rethrow the ambiguity error; keep
the existing Error instance guard (error instanceof Error) and rethrow the
original error when the match succeeds, otherwise continue to throw the Unknown
table error for tableName.

In `@packages/3-extensions/sql-orm-client/src/where-binding.ts`:
- Around line 153-170: The current lookup for resolvedNamespaceId picks the
first namespace with the table and only afterward checks the column, causing
wrong binds or false "Unknown column" errors when bare table names appear in
multiple namespaces; change the lookup to find namespaces where both table and
column exist by searching Object.keys(namespaces).filter(ns =>
namespaces[ns]?.tables[columnRef.table]?.columns?.[columnRef.column] !==
undefined) and then: if zero candidates throw unknown column, if multiple
candidates throw an ambiguity error listing the candidate namespace ids,
otherwise set resolvedNamespaceId to the single candidate and proceed to call
codecRefForStorageColumn(contract.storage, resolvedNamespaceId, columnRef.table,
columnRef.column). Ensure you still handle an explicitly provided namespaceId by
validating that the column exists in that namespace and throwing if not.

---

Outside diff comments:
In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts`:
- Around line 1517-1528: The bug is that modelNamespaceIds and fallback
modelMappings use bare model.name keys causing collisions between same-named
models across namespaces (e.g., auth.User vs public.User); update lookups to be
coordinate-aware: change where entries are set (in the loop that builds
modelNamespaceIds/modelMappings and where resolvePolymorphism() or relation
resolution uses them) to use a fully-qualified key (e.g.,
`${namespace.name}.${model.name}` or the model coordinate from modelEntries)
instead of model.name, and when code paths accept unqualified names keep
resolving them relative to the declaring model’s namespace (using namespaceId
from modelEntries) and emit an ambiguity diagnostic if multiple qualified
matches exist so relation targets and polymorphism patches always bind to the
correct model.
- Around line 1118-1124: The relation metadata being pushed via
resultFkRelationMetadata currently includes targetNamespaceId but omits the
declaring model’s namespace, causing ambiguity between same-named models in
different namespaces; update the object pushed in the resultFkRelationMetadata
(the block that sets declaringModelName, declaringFieldName, declaringTableName,
targetModelName, targetTableName and ...ifDefined('targetNamespaceId',
targetNamespaceId)) to also include the declaringNamespaceId (e.g.,
...ifDefined('declaringNamespaceId', declaringNamespaceId) or the appropriate
variable that holds the declaring model’s namespace), and mirror this change
wherever backrelation candidate metadata is assembled so both declaring and
target sides carry their namespace coordinates for use by indexFkRelations and
model-relation assembly.

In `@packages/2-sql/4-lanes/relational-core/src/codec-ref-for-column.ts`:
- Around line 39-48: The code currently scans all storage.namespaces and can
pick the wrong enum when multiple namespaces define the same columnDef.typeRef;
instead, restrict lookup to the requested namespace using the required namespace
identifier on the column definition (e.g. columnDef.namespaceId). Change the
logic that iterates Object.values(storage.namespaces) to directly index
storage.namespaces[columnDef.namespaceId] (or the corresponding namespaceId
variable), verify that namespace exists, then read its enum map and pick
nsEnums[columnDef.typeRef] into instance; remove the cross-namespace loop so the
lookup is namespace-qualified.

In `@packages/2-sql/5-runtime/src/sql-context.ts`:
- Around line 550-567: The usedAt/site identity must include namespaceId to
prevent cross-namespace collisions: change the site object created in the loop
to include namespaceId (e.g., { namespace: namespaceId, table: tableName,
column: columnName }), update the type of SqlCodecInstanceContext['usedAt'] (and
any usedAtByKey maps) to store sites with namespace, and adjust nameByKey
generation to incorporate namespace when producing inline names for per-column
codecs; then thread namespaceId through collectTypeRefSites(...) and into
buildContractCodecRegistry so every place that records or reads usedAt or builds
instance names uses the namespace-aware site shape.

In `@packages/3-extensions/sql-orm-client/src/collection-contract.ts`:
- Around line 318-331: The relation entry accepts rel.to.namespace without
validating its type; update the guard that checks rel and its fields (the if
that currently validates rel.to, rel.to.model, localFields, targetFields) to
also require typeof rel.to.namespace === 'string' (or allow undefined explicitly
if intended), and if that check fails continue skipping the relation; then
assign toNamespace from rel.to.namespace in the resolved[name] object as before
so malformed namespace values are treated as invalid relations rather than
causing later namespace-resolution errors.

In `@packages/3-extensions/sql-orm-client/src/collection-dispatch.ts`:
- Around line 208-216: The identity filter is being built and later bound
without the namespace context, causing ambiguous table resolution in
multi-namespace contracts; update the call sites so namespaceId is threaded into
the filter creation and binding: modify buildIdentityInFilter(...) invocation to
accept namespaceId (e.g. buildIdentityInFilter(contract, namespaceId, tableName,
identityColumns, identityRows) or add namespaceId parameter) and ensure
subsequent uses (where bindWhereExpr(...) is called) pass namespaceId when
binding the identityFilter; update the relevant functions (buildIdentityInFilter
and bindWhereExpr call-sites) and their signatures to accept and propagate
namespaceId while preserving existing parameters like contract, tableName,
identityColumns, and identityRows.

In `@packages/3-extensions/sql-orm-client/src/orm.ts`:
- Around line 204-217: The validation currently uses
domainModelsAtDefaultNamespace(contract.domain) which only returns
default-namespace models so namespaced models (e.g., "auth.Session") get
rejected; replace that lookup with the namespace-aware model map (use the
function that returns all domain models including namespaces — e.g.,
domainModels(contract.domain) or the equivalent namespaced lookup) when building
the models variable and then keep the existing isCollectionClass and
Object.hasOwn(models, key) checks so custom collection keys validate against the
full, namespaced model set; update the reference in the block where models is
defined and used (the models variable, domainModelsAtDefaultNamespace call, and
the loop over collections) to use the namespace-aware lookup.

---

Nitpick comments:
In `@test/integration/test/namespaced-accessors-e2e.integration.test.ts`:
- Around line 1-28: Replace the long top-level and mid-file narrative comments
in namespaced-accessors-e2e.integration.test.ts with a single short intent line
describing the test (e.g., "E2E test for explicit namespaced accessors across
public/auth schemas") and add a single link to the canonical design/ADR (replace
with the project ADR URL), removing the verbose prose blocks at the top and
around lines 136-142; keep the test behavior and fixtures unchanged (target the
long block comment and the subsequent mid-file comment blocks that describe the
same intent).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 18c34605-af39-4e06-8ef0-dfd0714e84ac

📥 Commits

Reviewing files that changed from the base of the PR and between c159cd4 and 94b3518.

⛔ Files ignored due to path filters (7)
  • projects/explicit-namespace-dsl/learnings.md is excluded by !projects/**
  • projects/explicit-namespace-dsl/plan.md is excluded by !projects/**
  • projects/explicit-namespace-dsl/plans/plan.md is excluded by !projects/**
  • projects/explicit-namespace-dsl/slices/01-additive-namespaced-surface/plan.md is excluded by !projects/**
  • projects/explicit-namespace-dsl/slices/01-additive-namespaced-surface/spec.md is excluded by !projects/**
  • projects/explicit-namespace-dsl/slices/02-remove-flat-fallback/spec.md is excluded by !projects/**
  • projects/explicit-namespace-dsl/spec.md is excluded by !projects/**
📒 Files selected for processing (98)
  • packages/2-sql/1-core/contract/src/resolve-storage-table.ts
  • packages/2-sql/1-core/contract/test/resolve-storage-table.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.namespaces.test.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-sql/2-authoring/contract-ts/src/contract-definition.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.cross-namespace-same-table.test.ts
  • packages/2-sql/4-lanes/relational-core/DEVELOPING.md
  • packages/2-sql/4-lanes/relational-core/src/ast/codec-types.ts
  • packages/2-sql/4-lanes/relational-core/src/codec-descriptor-registry.ts
  • packages/2-sql/4-lanes/relational-core/src/codec-ref-for-column.ts
  • packages/2-sql/4-lanes/relational-core/src/query-lane-context.ts
  • packages/2-sql/4-lanes/relational-core/test/codec-descriptor-registry.test.ts
  • packages/2-sql/4-lanes/relational-core/test/codec-ref-for-column.test.ts
  • packages/2-sql/4-lanes/sql-builder/src/exports/types.ts
  • packages/2-sql/4-lanes/sql-builder/src/runtime/builder-base.ts
  • packages/2-sql/4-lanes/sql-builder/src/runtime/mutation-impl.ts
  • packages/2-sql/4-lanes/sql-builder/src/runtime/resolve-table.ts
  • packages/2-sql/4-lanes/sql-builder/src/runtime/sql.ts
  • packages/2-sql/4-lanes/sql-builder/src/runtime/table-proxy-impl.ts
  • packages/2-sql/4-lanes/sql-builder/src/types/db.ts
  • packages/2-sql/4-lanes/sql-builder/test/runtime/field-proxy.test.ts
  • packages/2-sql/4-lanes/sql-builder/test/runtime/namespaced-resolution.test.ts
  • packages/2-sql/4-lanes/sql-builder/test/runtime/same-bare-table-name.test.ts
  • packages/2-sql/4-lanes/sql-builder/test/types/namespaced-db.types.test-d.ts
  • packages/2-sql/5-runtime/src/sql-context.ts
  • packages/2-sql/5-runtime/test/contract-codec-registry.test.ts
  • packages/2-sql/5-runtime/test/same-bare-table-name.test.ts
  • packages/2-sql/5-runtime/test/sql-context.codec-context.test.ts
  • packages/3-extensions/postgres/test/fixtures/namespaced-contract.ts
  • packages/3-extensions/postgres/test/namespaced-facade.types.test-d.ts
  • packages/3-extensions/postgres/test/postgres.test.ts
  • packages/3-extensions/sql-orm-client/src/aggregate-builder.ts
  • packages/3-extensions/sql-orm-client/src/collection-column-mapping.ts
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • packages/3-extensions/sql-orm-client/src/collection-dispatch.ts
  • packages/3-extensions/sql-orm-client/src/collection-internal-types.ts
  • packages/3-extensions/sql-orm-client/src/collection-mutation-dispatch.ts
  • packages/3-extensions/sql-orm-client/src/collection-runtime.ts
  • packages/3-extensions/sql-orm-client/src/collection.ts
  • packages/3-extensions/sql-orm-client/src/filters.ts
  • packages/3-extensions/sql-orm-client/src/grouped-collection.ts
  • packages/3-extensions/sql-orm-client/src/model-accessor.ts
  • packages/3-extensions/sql-orm-client/src/mutation-executor.ts
  • packages/3-extensions/sql-orm-client/src/orm.ts
  • packages/3-extensions/sql-orm-client/src/query-plan-aggregate.ts
  • packages/3-extensions/sql-orm-client/src/query-plan-meta.ts
  • packages/3-extensions/sql-orm-client/src/query-plan-mutations.ts
  • packages/3-extensions/sql-orm-client/src/query-plan-select.ts
  • packages/3-extensions/sql-orm-client/src/storage-resolution.ts
  • packages/3-extensions/sql-orm-client/src/types.ts
  • packages/3-extensions/sql-orm-client/src/where-binding.ts
  • packages/3-extensions/sql-orm-client/src/where-interop.ts
  • packages/3-extensions/sql-orm-client/test/aggregate-builder.test.ts
  • packages/3-extensions/sql-orm-client/test/collection-column-mapping.test.ts
  • packages/3-extensions/sql-orm-client/test/collection-contract.test.ts
  • packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts
  • packages/3-extensions/sql-orm-client/test/collection-fixtures.ts
  • packages/3-extensions/sql-orm-client/test/collection-mutation-dispatch.test.ts
  • packages/3-extensions/sql-orm-client/test/collection-runtime.test.ts
  • packages/3-extensions/sql-orm-client/test/collection-variant.test.ts
  • packages/3-extensions/sql-orm-client/test/filters.test.ts
  • packages/3-extensions/sql-orm-client/test/generated-contract-types.test-d.ts
  • packages/3-extensions/sql-orm-client/test/include-cardinality.test-d.ts
  • packages/3-extensions/sql-orm-client/test/model-accessor.test.ts
  • packages/3-extensions/sql-orm-client/test/mutation-executor.test.ts
  • packages/3-extensions/sql-orm-client/test/namespace-qualification.test.ts
  • packages/3-extensions/sql-orm-client/test/orm-namespace-crud.test.ts
  • packages/3-extensions/sql-orm-client/test/orm-namespace-relation.test.ts
  • packages/3-extensions/sql-orm-client/test/orm-namespace-resolution.test.ts
  • packages/3-extensions/sql-orm-client/test/orm-namespace-returning-crud.test.ts
  • packages/3-extensions/sql-orm-client/test/orm-namespaced.test.ts
  • packages/3-extensions/sql-orm-client/test/orm-namespaced.types.test-d.ts
  • packages/3-extensions/sql-orm-client/test/orm-same-bare-table-name.test.ts
  • packages/3-extensions/sql-orm-client/test/orm.test.ts
  • packages/3-extensions/sql-orm-client/test/orm.types.test-d.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-aggregate.test.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-meta.test.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-mutations.test.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-select.test.ts
  • packages/3-extensions/sql-orm-client/test/repository.test.ts
  • packages/3-extensions/sql-orm-client/test/rich-filters-and-where.test.ts
  • packages/3-extensions/sql-orm-client/test/rich-query-plans.test.ts
  • packages/3-extensions/sql-orm-client/test/simplify-deep.test-d.ts
  • packages/3-extensions/sql-orm-client/test/storage-resolution.test.ts
  • packages/3-extensions/sqlite/test/fixtures/namespaced-contract.ts
  • packages/3-extensions/sqlite/test/namespaced-facade.types.test-d.ts
  • test/integration/test/namespaced-accessors-e2e.integration.test.ts
  • test/integration/test/sql-orm-client/collection-fixtures.ts
  • test/integration/test/sql-orm-client/collection-mutation-defaults.test.ts
  • test/integration/test/sql-orm-client/include.test.ts
  • test/integration/test/sql-orm-client/integration-helpers.ts
  • test/integration/test/sql-orm-client/model-accessor.pgvector.test.ts
  • test/integration/test/sql-orm-client/nested-includes-helpers.ts
  • test/integration/test/sql-orm-client/polymorphism.test.ts
  • test/integration/test/sql-orm-client/upsert.test.ts

Comment on lines +33 to 49
if (Object.hasOwn(storage.namespaces, prop)) {
const namespaceId = prop;
return new Proxy(
{},
{
get(_facetTarget, tableName: string) {
const table = resolveTableInNamespace(storage, namespaceId, tableName);
if (table) {
return new TableProxyImpl(tableName, table, tableName, ctx, namespaceId);
}
return undefined;
},
},
);
}
const resolved = resolveTableForFlatName(storage, prop);
if (resolved) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast on namespace/table key collisions in proxy resolution.

Line [33] gives namespace ids priority over flat table names. When a namespace id and a table name are identical, db.<name> can no longer reach the flat table path, so the “flat + namespaced additive surface” is no longer true for that key.

💡 Suggested fix
   return new Proxy({} as Db<C>, {
     get(_target, prop: string) {
-      if (Object.hasOwn(storage.namespaces, prop)) {
+      const isNamespaceKey = Object.hasOwn(storage.namespaces, prop);
+      const resolved = resolveTableForFlatName(storage, prop);
+
+      if (isNamespaceKey && resolved !== undefined) {
+        throw new Error(
+          `Identifier "${prop}" is both a namespace id and a table name; use namespace-qualified access.`,
+        );
+      }
+
+      if (isNamespaceKey) {
         const namespaceId = prop;
         return new Proxy(
           {},
           {
             get(_facetTarget, tableName: string) {
               const table = resolveTableInNamespace(storage, namespaceId, tableName);
               if (table) {
                 return new TableProxyImpl(tableName, table, tableName, ctx, namespaceId);
               }
               return undefined;
             },
           },
         );
       }
-      const resolved = resolveTableForFlatName(storage, prop);
       if (resolved) {
         return new TableProxyImpl(prop, resolved.table, prop, ctx, resolved.namespaceId);
       }
       return undefined;
     },
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (Object.hasOwn(storage.namespaces, prop)) {
const namespaceId = prop;
return new Proxy(
{},
{
get(_facetTarget, tableName: string) {
const table = resolveTableInNamespace(storage, namespaceId, tableName);
if (table) {
return new TableProxyImpl(tableName, table, tableName, ctx, namespaceId);
}
return undefined;
},
},
);
}
const resolved = resolveTableForFlatName(storage, prop);
if (resolved) {
const isNamespaceKey = Object.hasOwn(storage.namespaces, prop);
const resolved = resolveTableForFlatName(storage, prop);
if (isNamespaceKey && resolved !== undefined) {
throw new Error(
`Identifier "${prop}" is both a namespace id and a table name; use namespace-qualified access.`,
);
}
if (isNamespaceKey) {
const namespaceId = prop;
return new Proxy(
{},
{
get(_facetTarget, tableName: string) {
const table = resolveTableInNamespace(storage, namespaceId, tableName);
if (table) {
return new TableProxyImpl(tableName, table, tableName, ctx, namespaceId);
}
return undefined;
},
},
);
}
if (resolved) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/2-sql/4-lanes/sql-builder/src/runtime/sql.ts` around lines 33 - 49,
The current proxy resolution gives namespace ids priority via
Object.hasOwn(storage.namespaces, prop) which hides a flat table when a
namespace id equals a flat table name; update the branch that checks
Object.hasOwn(storage.namespaces, prop) to also call
resolveTableForFlatName(storage, prop) (or otherwise check for a flat table) and
if both a namespace and a flat table are present, throw a clear collision error
instead of silently choosing the namespace; otherwise proceed to create the
namespace proxy (using resolveTableInNamespace and TableProxyImpl) as
before—this makes db.<name> fail fast on namespace/table key collisions.

Comment on lines +24 to 30
} catch (error) {
// Surface the ambiguous-bare-name fail-fast rather than masking it as an
// unknown table.
if (error instanceof Error && error.message.includes('ambiguous')) {
throw error;
}
throw new Error(`Unknown table "${tableName}" in SQL ORM query planner`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make ambiguity rethrow matching robust.

Line 27 uses a case-sensitive substring check; "Ambiguous ..." (or wording changes) can be misclassified and surfaced as an unknown-table error instead of the intended ambiguity diagnostic.

Proposed fix
-    if (error instanceof Error && error.message.includes('ambiguous')) {
+    if (error instanceof Error && /ambiguous/i.test(error.message)) {
       throw error;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
// Surface the ambiguous-bare-name fail-fast rather than masking it as an
// unknown table.
if (error instanceof Error && error.message.includes('ambiguous')) {
throw error;
}
throw new Error(`Unknown table "${tableName}" in SQL ORM query planner`);
} catch (error) {
// Surface the ambiguous-bare-name fail-fast rather than masking it as an
// unknown table.
if (error instanceof Error && /ambiguous/i.test(error.message)) {
throw error;
}
throw new Error(`Unknown table "${tableName}" in SQL ORM query planner`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/3-extensions/sql-orm-client/src/query-plan-meta.ts` around lines 24
- 30, The catch block in query-plan-meta.ts currently checks
error.message.includes('ambiguous') case-sensitively; change that check to a
case-insensitive match (e.g. use a case-insensitive regex like /\bambiguous\b/i
or compare error.message.toLowerCase().includes('ambiguous')) so any
capitalization or minor wording changes still rethrow the ambiguity error; keep
the existing Error instance guard (error instanceof Error) and rethrow the
original error when the match succeeds, otherwise continue to throw the Unknown
table error for tableName.

Comment on lines +153 to +170
const resolvedNamespaceId =
namespaceId ??
Object.keys(namespaces).find((ns) => namespaces[ns]?.tables[columnRef.table] !== undefined);
if (resolvedNamespaceId === undefined) {
throw new Error(`Unknown column "${columnRef.column}" in table "${columnRef.table}"`);
}
const tableInAnyNs = namespaces[resolvedNamespaceId]?.tables[columnRef.table] as
| StorageTable
| undefined;
if (!tableInAnyNs?.columns[columnRef.column]) {
throw new Error(`Unknown column "${columnRef.column}" in table "${columnRef.table}"`);
}
const codec = codecRefForStorageColumn(contract.storage, columnRef.table, columnRef.column);
const codec = codecRefForStorageColumn(
contract.storage,
resolvedNamespaceId,
columnRef.table,
columnRef.column,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make namespace fallback column-aware and ambiguity-safe.

When namespaceId is missing, lookup currently picks the first namespace containing the table, then checks the column. With duplicate bare table names, this can throw for valid columns in another namespace (or bind against the wrong namespace). Resolve by {table,column} candidates and fail fast on ambiguity.

🔧 Suggested fix
   const namespaces = contract.storage.namespaces;
-  const resolvedNamespaceId =
-    namespaceId ??
-    Object.keys(namespaces).find((ns) => namespaces[ns]?.tables[columnRef.table] !== undefined);
+  const resolvedNamespaceId =
+    namespaceId ??
+    (() => {
+      const matches = Object.entries(namespaces)
+        .filter(
+          ([, ns]) => ns?.tables[columnRef.table]?.columns[columnRef.column] !== undefined,
+        )
+        .map(([nsId]) => nsId);
+      if (matches.length === 1) return matches[0];
+      if (matches.length === 0) return undefined;
+      throw new Error(
+        `Ambiguous column "${columnRef.column}" in table "${columnRef.table}" across namespaces: ${matches.join(', ')}`,
+      );
+    })();
   if (resolvedNamespaceId === undefined) {
     throw new Error(`Unknown column "${columnRef.column}" in table "${columnRef.table}"`);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const resolvedNamespaceId =
namespaceId ??
Object.keys(namespaces).find((ns) => namespaces[ns]?.tables[columnRef.table] !== undefined);
if (resolvedNamespaceId === undefined) {
throw new Error(`Unknown column "${columnRef.column}" in table "${columnRef.table}"`);
}
const tableInAnyNs = namespaces[resolvedNamespaceId]?.tables[columnRef.table] as
| StorageTable
| undefined;
if (!tableInAnyNs?.columns[columnRef.column]) {
throw new Error(`Unknown column "${columnRef.column}" in table "${columnRef.table}"`);
}
const codec = codecRefForStorageColumn(contract.storage, columnRef.table, columnRef.column);
const codec = codecRefForStorageColumn(
contract.storage,
resolvedNamespaceId,
columnRef.table,
columnRef.column,
);
const resolvedNamespaceId =
namespaceId ??
(() => {
const matches = Object.entries(namespaces)
.filter(
([, ns]) => ns?.tables[columnRef.table]?.columns[columnRef.column] !== undefined,
)
.map(([nsId]) => nsId);
if (matches.length === 1) return matches[0];
if (matches.length === 0) return undefined;
throw new Error(
`Ambiguous column "${columnRef.column}" in table "${columnRef.table}" across namespaces: ${matches.join(', ')}`,
);
})();
if (resolvedNamespaceId === undefined) {
throw new Error(`Unknown column "${columnRef.column}" in table "${columnRef.table}"`);
}
const tableInAnyNs = namespaces[resolvedNamespaceId]?.tables[columnRef.table] as
| StorageTable
| undefined;
if (!tableInAnyNs?.columns[columnRef.column]) {
throw new Error(`Unknown column "${columnRef.column}" in table "${columnRef.table}"`);
}
const codec = codecRefForStorageColumn(
contract.storage,
resolvedNamespaceId,
columnRef.table,
columnRef.column,
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/3-extensions/sql-orm-client/src/where-binding.ts` around lines 153 -
170, The current lookup for resolvedNamespaceId picks the first namespace with
the table and only afterward checks the column, causing wrong binds or false
"Unknown column" errors when bare table names appear in multiple namespaces;
change the lookup to find namespaces where both table and column exist by
searching Object.keys(namespaces).filter(ns =>
namespaces[ns]?.tables[columnRef.table]?.columns?.[columnRef.column] !==
undefined) and then: if zero candidates throw unknown column, if multiple
candidates throw an ambiguity error listing the candidate namespace ids,
otherwise set resolvedNamespaceId to the single candidate and proceed to call
codecRefForStorageColumn(contract.storage, resolvedNamespaceId, columnRef.table,
columnRef.column). Ensure you still handle an explicitly provided namespaceId by
validating that the column exists in that namespace and throwing if not.

@wmadden-electric
Copy link
Copy Markdown
Contributor

System-design review — PR #720 (architect pass)

Resolved review range: origin/main...HEAD = ae242d9c994b3518568 (three-dot diff; 110 files).
PR: TML-2816 — namespace-aware DSL/ORM surface, additive slice (slice 01 of explicit-namespace-dsl).
Persona: architect — system shape, naming/typology, bounded contexts, dependency direction, conceptual integrity. Implementation correctness, blast-radius, adopter learnability, and scope/value are out of scope and referred where noted.

This is the additive half of an additive-then-cut split. Slice 02 removes the flat surface, adds the facade projection, and writes the ADR (AC7). I review slice 01 as an additive slice — i.e. against the question "does this set slice 02 up cleanly and tell a fresh contributor the truth about the system's shape?" — and I treat the team's recorded deferrals as known debt unless the slice's own choices make that debt worse.

Summary verdict

The "namespace coordinate" concept is real, coherent, and applied with admirable consistency at the behavioural level: every resolver that used to assume one namespace now takes a namespace id, the tests discriminate the same-bare-name case truthfully, and the additive intersection on Db<C> is the right typology for an additive slice. The shape the code presents is, in the large, true.

The findings are about typology consistency of the coordinate's surface, not about whether it works:

  • Major (1): the same coordinate is threaded through sibling resolvers with three different parameter conventions (optional-trailing, required-leading, and a separately-keyed cache string). A fresh contributor cannot infer the calling convention of one coordinate-aware resolver from another. This is a real conceptual-integrity defect in an additive slice whose entire job is to introduce this one concept cleanly.
  • Minor (3): the optional-trailing namespaceId on resolveStorageTable encodes two resolution policies in one parameter slot; the pre-investigated namespace-id/flat-table-name collision case has no type-level test pinning the documented disposition; __unbound__ normalisation logic is duplicated across authoring sites.
  • Known debt, correctly framed (1): two resolution paths (flat-fallback vs coordinate) coexist, and the flat path throws on multi-namespace contracts. The team has recorded this as deferred to slice 02 (bare = default namespace). I agree it is debt, not a new defect, and the slice's framing is honest — with one caveat about a name that currently overstates what it does.

No layering / dependency-direction violations found at the conceptual level. The coordinate-aware signatures land at the right layer.


1. Is "namespace coordinate" a coherent, well-named concept, applied symmetrically?

The concept itself: yes. "Namespace coordinate" names a real, singular, structural distinction — which of the contract's declared namespaces does this table/model live in. It maps directly onto contract IR (storage.namespaces[id], domain.namespaces[id]) and onto the cross-reference shape (CrossReference.namespace). The probe "what does the namespaceId parameter distinguish from?" has a concrete, stable answer: the other declared namespaces of the same contract. This is not a consumer-encoded name and not a mechanism dressed as a concept. Good.

The threading is asymmetric, and that is the major finding. The same coordinate is passed to sibling resolvers under three different calling conventions:

  • Optional trailing, with two meanings folded into presence/absence:
    resolveStorageTable(storage, tableName, namespaceId?)packages/2-sql/1-core/contract/src/resolve-storage-table.ts:32. Coordinate present ⇒ strict within-namespace; coordinate absent ⇒ scan-all-namespaces-and-throw-on-ambiguity.
  • Required leading:
    codecRefForStorageColumn(storage, namespaceId, tableName, columnName)packages/2-sql/4-lanes/relational-core/src/codec-ref-for-column.ts:25; mirrored on the registry as codecRefForColumn(namespaceId, table, column) (codec-descriptor-registry.ts:48) and forColumn(namespaceId, table, column) (sql-context.ts), and on the builder helper codecRefFor(ctx, namespaceId, tableName, columnName) (builder-base.ts:108).
  • Required leading on every ORM metadata resolver:
    modelOf(contract, namespaceId, name), resolveFieldToColumn(contract, namespaceId, modelName, fieldName), getFieldToColumnMap, getColumnToFieldMap, getCompleteColumnToFieldMap, resolvePolymorphismInfopackages/3-extensions/sql-orm-client/src/collection-contract.ts. Plus resolveTableForContract(contract, namespaceId, tableName), storageTableForContract, tableSourceForContract in storage-resolution.ts.

So we have: in the SQL core the coordinate is the optional last argument; one layer up in codecRefForStorageColumn (same package family) it is the required first argument after storage; in the ORM client it is the required argument right after contract. A reader who has learned the convention from any one of these cannot predict the next. The fresh-contributor probe fails: "where does the namespace go in this call?" has no single answer.

The doc comment on codecRefForStorageColumn even narrates the inconsistency as if it were a decision — "namespaceId leads the coordinate args and is always supplied" — directly contradicting the core resolver it delegates to, where the same id trails and is optional. Two functions, one delegating to the other, disagree in prose about where the same concept belongs in the signature.

Why this matters at the architect altitude (not bikeshedding): this slice's one job is to introduce the coordinate as a first-class concept threaded through the system. The signature is the concept's public face at every call site. When the same concept appears in three conventions across sibling resolvers, the surface tells a fresh contributor that these are three loosely-related parameters that happen to share a name — not one concept with one meaning. That is exactly the "homonym / asymmetric sibling" failure the typology probes are meant to catch. It is cheap to fix now (the slice is additive and the call sites are all new or freshly touched) and expensive to fix after slice 02 hardens around it.

Recommendation (for this slice or a fast follow): pick one convention for the coordinate and apply it to every coordinate-aware resolver. Leading-required reads best, because the coordinate is logically part of the identity being resolved, not an option on it — resolve(namespace, table, column) mirrors how the IR is keyed (namespaces[id].tables[name].columns[col]). That also resolves finding #3 below. If resolveStorageTable must keep the bare-scan fallback for the still-present flat path, that is the one defensible place for optional-trailing, and it should be commented as the deliberate exception, not silently disagreed-with by its own caller's doc.


2. The additive-intersection Db<C> — right typology for an additive slice?

Yes, and it sets slice 02 up cleanly. Db<C> = { flat-by-bare-name } & { per-namespace facet } (packages/2-sql/4-lanes/sql-builder/src/types/db.ts:50), with Namespace<C, NsId> (db.ts:41) and the ORM mirror OrmNamespace / NamespacedClientMap (packages/3-extensions/sql-orm-client/src/orm.ts:69–89). The intersection is the correct shape for "add the new surface without removing the old," and slice 02's own spec confirms the cut is mechanical: drop the left intersand, keep the mapped half. The two-level proxy (runtime/sql.ts:31–55) checks prop in storage.namespaces first, then falls back to the flat path — symmetric with the type. The ORM proxy (orm.ts:174–192) does the same against contract.domain.namespaces. Good structural symmetry between the SQL and ORM facets, and between type and runtime.

Namespace<C, NsId> — naming. Fires the discriminator-completeness probe: what does the Namespace type distinguish from? Answer: it is "the tables of one storage namespace, projected as a table-proxy map." The name is slightly too broad — it reads like it models the namespace itself (id, metadata, the lot), when it is specifically the table-proxy facet of a namespace. The ORM side already names its equivalent OrmNamespace, which is more honest (it is the ORM-facet view). Consider NamespaceTables<C, NsId> / NamespaceFacet for the SQL side so the name states it is the table projection, not the namespace. Minor; the JSDoc does say the right thing. Referral: whether Namespace collides confusingly with adopter mental models is a devrel call.

The namespace-id / flat-table-name collision (spec pre-investigated this). The chosen disposition — "namespace id wins on the namespaced path; flat path unchanged; document, no normalization magic" (slice spec, edge-case table) — is architecturally the right call: it avoids constructor/name magic (failure-mode F2) and the precedence is unambiguous because the proxy checks storage.namespaces before the flat path. But the disposition is not pinned by a test. namespaced-db.types.test-d.ts covers facet presence, flat retention, and unknown-ns rejection (:7–21), and namespaced-resolution.test.ts covers same-bare-table-name. Neither exercises a contract where a namespace id equals a flat table name. For a pre-investigated edge case with a deliberate precedence rule, the architectural property "namespace id wins, deterministically" should have a test that would fail if the proxy's check order were swapped. As written, the documented disposition is an assertion no test defends. Minor finding; recommend a type-level + runtime test on a { namespaces: { users: … }, tables: { users: … } }-style fixture. (This is an architectural test-adequacy point — what property must be proven — not a correctness adjudication; the actual ordering looks correct.)


3. Layer purity / bounded contexts

No dependency-direction violations found at the conceptual level. The coordinate-aware signatures land where the SPI-at-lowest-consuming-layer pattern wants them:

  • The bare resolver lives in 2-sql/1-core/contract (resolveStorageTable) — lowest layer, correct.
  • codecRefForStorageColumn and the descriptor registry live in 2-sql/4-lanes/relational-core — the lane that owns codec resolution; it imports the core resolver downward. Correct direction.
  • The ORM metadata/storage resolvers live in 3-extensions/sql-orm-client — they consume the SQL contract types and the core resolver; they do not reach sideways into the builder. Correct.
  • The execution-awareness retrofit (D3–D6) is contained entirely within sql-orm-client; no contract-foundation change leaked out, matching the slice's "halt-and-surface if foundation must change" rule. The one foundation-adjacent helper that was removed (resolveDomainModelForContract / domainModelsAtDefaultNamespace usage in collection-contract.ts) was replaced by direct contract.domain.namespaces[ns].models indexing — which keeps the namespace-scoping decision in the consuming layer rather than pushing a new variant into foundation. Architecturally clean.

lint:deps could not be run here (the worktree's depcruise binary is not installed); NFR3 is a CI-verified property and I did not find a structural violation by inspection. Referral: confirm pnpm lint:deps green in CI (principal-engineer / CI gate).

One coordinate-keying mechanism is now reimplemented three times as the concept threads across contexts:

  • metadataCacheKey(namespaceId, modelName)`${ns}�${model}`collection-contract.ts:52.
  • modelCoordinateKey(namespaceId, modelName)ns + '�' + modelpsl-field-resolution.ts:59.
  • the -joined candidate key inside mutation-executor / relation resolution.

Three private helpers, same NUL-delimited (namespace, name) key, same purpose (key a cache/map by model coordinate), in three packages. This is the conceptual-minimality probe firing softly: the "model coordinate key" is a real shared concept that has been independently rediscovered three times. Crossing a bounded-context boundary is a legitimate reason not to share a helper (authoring vs runtime), so this is not a hard finding — but it is worth a one-line note that "model coordinate = (namespace, name)" is now an implicit shared vocabulary term with three private encodings. If a fourth appears, promote it. Minor.


4. The optional-trailing namespaceId and the deferred "bare = default" tension

This is the question the project most needs framed precisely, so I am explicit.

What exists today (slice 01): two resolution paths coexist.

  1. Coordinate pathsql.<ns>.<table> / orm.<ns>.<Model> resolve strictly within the named namespace.
  2. Flat pathsql.<table> / orm.<Model> route through resolveStorageTable(storage, name) with no coordinate. On a single-namespace contract this resolves the sole namespace. On a multi-namespace contract with a shared bare name, it throws (resolve-storage-table.ts:50–58; pinned by namespaced-resolution.test.ts:82 "throws on flat access to a bare table name shared across namespaces"; the ORM flat path throws via soleDomainNamespaceId).

Is the optional-trailing namespaceId a sound typology choice, or does it smuggle the ambiguity back in? It encodes two distinct policies in one parameter slot: "resolve in this namespace" (coordinate present) vs "scan all and fail if ambiguous" (coordinate absent). That is an overloaded discriminator — the absence of an argument selects a different algorithm, not merely a default value. The optional parameter does not itself reintroduce "bare = first match" (the scan throws on ambiguity rather than silently picking the first — that is the correct interim behaviour, and a genuine improvement over the old silent first-match). But it does carry two policies behind one optional slot, which is the typology smell. Once slice 02 lands "bare = default namespace," the absent-coordinate branch should collapse into "resolve against the connector's defaultNamespaceId," at which point the core resolver arguably should always receive a coordinate and the optional slot disappears. Framing: the optional-trailing shape is acceptable interim debt precisely because it is scheduled to collapse; it is not a new defect. But it is the same overloaded-parameter smell as finding #1, and unifying the calling convention (#1) should keep the bare-scan branch quarantined to the single core resolver, clearly labelled as the interim flat-path exception, rather than letting "optional namespace" spread as a general convention. The learnings doc records bare=default as the deferred unification and the team is carrying this knowingly — correctly framed there.

One naming caveat on the deferred debt. domainModelsAtDefaultNamespace (foundation) does not resolve "the default namespace" — it resolves the sole namespace and throws when there is more than one. Its sibling soleDomainNamespaceId is named honestly ("sole"); domainModelsAtDefaultNamespace is named for a behaviour (pick the default) the function does not yet have. Reads-cold probe: a contributor seeing …AtDefaultNamespace will reasonably expect default-namespace resolution and be surprised by the throw. This is a pre-existing name (not introduced by this PR — it is referenced, and partly removed from collection-contract.ts), so it is not a slice-01 defect, but slice 02 is the natural place to either rename it to …AtSoleNamespace or give it the default-resolution behaviour its name promises. Flagging so it lands on slice 02's radar with the bare=default work. Minor / carry-forward.


5. Test naming as evidence of conceptual partitioning

Strong. The test names encode the namespace concept truthfully and, importantly, encode the discrimination property rather than mere presence:

  • same bare table name across namespaces › resolves the column codec within the proxy namespace, discriminating per namespace (sql-builder + orm suites).
  • namespaced table resolution › resolves the same bare name to the distinct table in each namespace and › scopes table lookup to the named namespace rather than scanning across namespaces and › throws on flat access to a bare table name shared across namespaces (namespaced-resolution.test.ts:64–82).

These names state the architectural property under test (the coordinate discriminates; the flat path throws on ambiguity), which is exactly what makes the same-bare-name fixture meaningful per the slice's F13 calibration ("a regression test must fail under ¬P"). The partitioning — facet-presence (type tests) vs discrimination (runtime tests) vs flat-retention — matches the conceptual structure of the change. This is the test suite doing its job as documentation of the concept.

Gaps (architectural test-adequacy, not correctness):

  • No test pins the namespace-id == flat-table-name collision precedence (finding in §2).
  • The slice-02 spec already records that the same-bare-name discrimination suite cannot catch a PK-path coordinate miswire because every fixture uses PK id — correctly deferred to slice 02 with a differing-PK-name fixture. Noted as known, correctly-framed debt; no action this slice.

Findings ledger

# Severity Finding Where
1 Major One coordinate, three calling conventions across sibling resolvers (optional-trailing in core; required-leading in codec + ORM layers); a delegating pair disagree in prose about where the coordinate belongs. Conceptual-integrity defect in the slice that introduces the concept. resolve-storage-table.ts:32, codec-ref-for-column.ts:25, collection-contract.ts, storage-resolution.ts, builder-base.ts:108
2 Minor Optional-trailing namespaceId overloads one slot with two resolution policies (within-namespace vs scan-and-throw). Acceptable interim debt scheduled to collapse in slice 02; keep the scan branch quarantined to the core resolver when unifying #1. resolve-storage-table.ts:36–60
3 Minor Documented namespace-id/flat-table-name collision precedence ("namespace id wins") has no test that would fail if proxy check-order were swapped. runtime/sql.ts:33, orm.ts:180; missing in namespaced-db.types.test-d.ts
4 Minor "Model coordinate = (namespace, name)" NUL-keyed helper independently reimplemented three times across packages. Cross-context boundary justifies non-sharing for now; promote if a fourth appears. collection-contract.ts:52, psl-field-resolution.ts:59, mutation-executor relation path
5 Minor / carry-forward domainModelsAtDefaultNamespace is named for default-namespace resolution but resolves the sole namespace and throws otherwise. Pre-existing; slice 02 (bare=default) is the place to rename or give it the promised behaviour. foundation default-namespace-adjacent; usage removed in collection-contract.ts
6 Naming nit Namespace<C, NsId> (SQL) reads broader than "the table-proxy facet of a namespace"; ORM side's OrmNamespace is more honest. Consider NamespaceTables/NamespaceFacet. db.types.ts:41

Known debt, correctly carried (no action this slice)

  • Two resolution paths (flat-fallback vs coordinate); flat path throws on multi-namespace shared bare names. Recorded in learnings.md as deferred to slice 02 (bare = default namespace via connector defaultNamespaceId). Honest framing; the throw is a correct interim improvement over silent first-match.
  • ADR (AC7) deferred to slice 02 — expected for an additive slice.
  • PK-path discrimination needs a differing-PK-name fixture — already captured in slice-02 spec's edge-case table.

Out-of-scope referrals (one line each)

  • Principal-engineer: verify the flat-path throw on multi-namespace contracts does not break any existing internal caller in packages/3-extensions/* / examples/ before slice 02 lands bare=default; confirm pnpm lint:deps / workspace typecheck green in CI (could not run depcruise locally).
  • DevRel: whether Namespace / OrmNamespace and the db.<ns>.<table> shape read clearly for adopters, and whether the interim "flat access throws on multi-namespace" is a learnability cliff worth a doc note before slice 02.
  • PM: the four-dispatch growth of the folded-in ORM execution-awareness work (recorded in learnings) is a scope-sizing observation, not an architectural one.

@wmadden-electric
Copy link
Copy Markdown
Contributor

Code review — PR #720 (TML-2816: namespace-aware DSL/ORM surface, additive slice 01)

Persona: principal engineer. Resolved range: origin/main...HEAD (branch explicit-namespace).

Summary

The additive namespaced accessor surface (sql.<ns>.<table>, orm.<ns>.<Model>) is built correctly, threads the namespace coordinate through the SQL and ORM column/codec resolution layers, and is proven end-to-end on a genuine same-bare-table-name + cross-namespace-FK PGlite fixture. Build, the touched package tests, the namespaced e2e integration test, and lint:deps all pass on a clean build; the findings below are mostly narrow correctness edges and a couple of in-scope coverage gaps.

What looks solid

  • The e2e proof is real, not structural. test/integration/test/namespaced-accessors-e2e.integration.test.ts authors the contract through the real TS builder, serializes/deserializes it via the Postgres serializer, loads it through the postgres({ contractJson }) facade, and drives select/insert/update/delete on both namespaces plus a cross-namespace include read against a live PGlite database. The two users tables carry different columns (email vs token), so a mis-qualified resolve would read the wrong columns or fail — the discrimination is genuine (satisfies failure-mode F13).
  • FR11 fail-fast is correct and tested. resolveStorageTable (packages/2-sql/1-core/contract/src/resolve-storage-table.ts) throws naming the candidate namespaces on an ambiguous bare name, asserted at the resolver layer (resolve-storage-table.test.ts:107-113, asserts both auth and public appear) and at the sql-builder layer (namespaced-resolution.test.ts:82-83).
  • Coordinate threading is consistent. Every non-test caller of codecRefForStorageColumn / codecRefForColumn / forColumn passes a namespaceId. The breaking signature change (namespace leads, always supplied) is applied uniformly across sql-builder, sql-orm-client, and the runtime codec registry.
  • Additive safety on the flat path. Single-namespace flat access is unchanged; flat access on a multi-namespace contract fails fast (soleDomainNamespaceId throws, resolveStorageTable ambiguity throws) rather than silently resolving the wrong table. That is the correct interim behaviour for an additive slice; slice 02 replaces the throw with bare=default.
  • Type-level negative tests exist for unknown namespace ids on both sql and orm and on the postgres/sqlite facades (@ts-expect-error on db.auth), and AC2's key-alignment (keyof orm namespaces === keyof sql namespaces) is type-asserted.

Findings

F01 — Nested subquery / join where-bindings drop the namespace coordinate (silent wrong-codec on same-bare-name tables)

Location: packages/3-extensions/sql-orm-client/src/where-binding.ts:141-172, 213-234
Issue: bindWhereExpr accepts and propagates a namespaceId, but the recursive bindSelectAst path (subqueries via DerivedTableSource, joins via bindJoin at line 196, nested where/having at lines 225/230) calls bindWhereExprNode(contract, ...) without a namespaceId. When the coordinate is absent, createParamRef (line 153-155) resolves the column's owning namespace by a first-match scan over storage.namespaces, not by the fail-fast ambiguity path. On a multi-namespace contract where the same bare table name exists in two namespaces with different column codecs, a parameter bound inside a nested subquery/join filter on that table can be stamped with the wrong namespace's codec. The coordinate threaded everywhere else exists precisely to avoid this; the nested binder is the one site that still scans. Blast radius is bounded: the codec only affects param encoding (table qualification rides on TableSource, which is correct), and it is a no-op when the two same-named tables share column codecs (the common case, and what the e2e fixture happens to use — both id are pg/int4@1). The e2e test does not exercise a nested cross-namespace subquery filter, so this path is unverified.
Suggestion: thread the resolved namespace coordinate into the bindSelectAst/bindJoin/bindFromSource recursion (each derived/subquery source already carries a TableSource with a namespaceId that can seed the binder), or — if a coordinate genuinely cannot be recovered at a given recursion point — replace the first-match .find(...) in createParamRef with the same fail-fast-on-ambiguity behaviour resolveStorageTable uses, so a missing coordinate throws rather than silently picking the first namespace. At minimum, add a test that binds a param inside a nested subquery filter against a same-bare-name table with differing codecs and asserts the per-namespace codec.

F02 — No regression proof that single-namespace contract.json is byte-identical (FR7)

Location: packages/2-sql/2-authoring/contract-ts/src/build-contract.ts (root-key assembly, the rootEntries refactor)
Issue: FR7 / plan dispatch D11 call for a single-namespace contract.json snapshot proving the additive change is non-breaking on emitted output. The PR removed the authoring dup-table guard and rewrote root assembly from a roots map to rootEntries + collision-aware keying. The new keying preserves bare keys for single-namespace and non-colliding contracts (only collisions get a qualified key), so the emitted shape is argued unchanged — but there is no committed snapshot/byte-identical assertion to lock it. The existing fixtures passing unedited is indirect evidence only.
Suggestion: add (or point to) a single-namespace contract.json snapshot regression, or run pnpm fixtures:check in CI for this slice and record it in the slice DoD. If the team accepts the structural argument for slice 01 and defers the explicit snapshot to slice 02's regression baseline, state that explicitly in the slice spec rather than leaving D11 implied-done.

F03 — Mongo facade reachability claimed in slice scope but no Mongo change landed

Location: projects/explicit-namespace-dsl/slices/01-additive-namespaced-surface/spec.md (Scope "In": "postgres / sqlite / mongo facade db"); plan.md D9
Issue: The slice's In-scope list and dispatch D9 name the Mongo facade alongside postgres/sqlite, but git diff --name-only origin/main...HEAD touches no *mongo* file. Postgres and SQLite both got namespaced-facade type tests; Mongo got none. The Mongo ORM/builder lives in a separate package (packages/2-mongo-family/...), so the SQL Db<C> additive intersection does not automatically flow there — the claim that the namespaced surface is "reachable through the mongo facade" is unverified for this slice.
Suggestion: either confirm Mongo reachability is genuinely deferred (the exclusive facade projection AC4/AC5 is slice-02, and Mongo's surface is a distinct package) and amend the slice spec's In-scope list to drop Mongo from slice 01, or add the Mongo facade type test now. As written, the spec and the diff disagree.

F04 — createParamRef first-match scan is the same anti-pattern FR11 removed elsewhere

Location: packages/3-extensions/sql-orm-client/src/where-binding.ts:153-155
Issue: This is the root cause under F01, called out separately because it is a latent correctness hazard independent of how callers thread the coordinate: Object.keys(namespaces).find((ns) => namespaces[ns]?.tables[columnRef.table] !== undefined) silently selects the first namespace that contains a table of that bare name. The rest of the codebase deliberately replaced exactly this scan with fail-fast-on-ambiguity (resolveStorageTable). Leaving one silent first-match scan in a binder reachable from nested ORM queries undermines the "no silent first-match" invariant the slice established.
Suggestion: route this resolution through resolveStorageTable(contract.storage, columnRef.table) (no coordinate) so it throws on ambiguity, falling to the strict-coordinate form whenever the caller supplies one. Pairs with the F01 fix.

F05 — Postgres facade test cast widened to as unknown as (test-only, minor)

Location: packages/3-extensions/postgres/test/postgres.test.ts:488
Issue: The namespaced index signature on the ORM client type invalidated the prior as typeof ormMock cast, so the mock was widened to as unknown as typeof ormMock. This is test-only (exempt from no-bare-casts) and was a deliberate scope-noted fix in D8, but as unknown as is the loosest possible cast and erases any structural check on the mock against the real client shape.
Suggestion: leave as-is for this slice if the team accepts it; if tightening is cheap, give the mock an explicit minimal interface so the cast narrows. Not worth expanding scope over.

Deferred (out of scope)

  • AC3 (flat-accessor removal + negative type tests), AC4/AC5 (defaultNamespaceId-keyed facade projection), AC7 (ADR), FR6/FR8/FR9/FR13-facade-alias — all explicitly slice-02 per both the project and slice specs. Confirmed genuinely deferred: the flat surface is still present (Db is the additive intersection; orm() retains the flat map), no projection helper exists, no ADR is in the diff. Not silently half-done.
  • Cross-namespace nested-relation writes — slice spec Out-scope; the e2e exercises a cross-namespace include read only, which matches the stated scope.
  • PK-path discrimination when both same-named tables share PK column id — recorded as a slice-02 carry-in (differing-PK-name fixture). The current fixtures use id in both namespaces, so a coordinate miswire on the PK path would not be caught yet; legitimately deferred and documented in the slice-02 spec.

Already addressed

  • The D2-era postgres facade mock-cast break (the namespaced ORM index signature invalidating the old cast) was fixed in this PR (F05), keeping the workspace typecheck green.
  • The same-bare-table-name case being a full-pipeline property (not provable by hand-built unit contracts) was caught mid-slice and addressed by the real author->serialize->load->query e2e test; the learnings ledger records the false-confidence risk from pipeline-bypassing unit tests.

Acceptance-criteria verification

AC Verdict Detail
AC1: sql.<ns>.<table> incl. same bare table name across namespaces PASS resolve-storage-table.ts resolves strictly within the coordinate; sql-builder two-level proxy (runtime/sql.ts) checks storage.namespaces first; e2e drives sql.public.users vs sql.auth.users with differing columns on PGlite and asserts qualified SQL ("public"."users" / "auth"."users"). Unit + e2e both discriminate. (F01 is a narrow nested-subquery edge, not the headline path.)
AC2: orm.<ns>.<Model>, keys aligned to SQL PASS orm.ts builds the per-namespace facet keyed on domain.namespaces; orm-namespaced.types.test-d.ts asserts keyof domain namespaces === keyof storage namespaces; e2e drives orm.public.User / orm.auth.User create/find/update/delete on PGlite with per-namespace-correct rows.
AC6: multi-namespace fixture authorable→emittable→queryable e2e (PGlite) PASS namespaced-accessors-e2e.integration.test.ts: full author→serialize→deserialize→facade→PGlite path, both accessor surfaces, cross-namespace FK include read. Ran green (2 tests passed).
AC8: test:packages + integration green; lint:deps passes PASS After a clean pnpm build: all touched package tests pass (sql-builder 138, sql-orm-client 521+3 skipped, contract 132, relational-core 335, contract-ts 283, contract-psl 209, sql-runtime, postgres 93, sqlite 40), namespaced e2e green, lint:deps reports no violations. (Note: an initial run failed in relational-core only because my worktree's dist was stale — a fresh build cleared it; this is a build-ordering artifact, not a PR defect.)
FR1-FR5, FR7, FR10, FR11 (in-scope FRs) MOSTLY PASS; FR7 WEAK FR1/FR3 (sql accessor + ops, qualified SQL): PASS via e2e. FR2 (unknown ns compile error): PASS via negative type tests. FR4/FR5 (orm accessor + key alignment): PASS. FR10 (reuse TML-2605 qualification, parameterized by coordinate): PASS — TableProxyImpl(namespaceId) and resolveStorageTable(coordinate) are the single path, no parallel pipeline. FR11 (fail-fast naming the namespace): PASS, tested. FR7 (no contract.json/d.ts shape change): WEAK — argued correct by the collision-only root-keying (build-contract.ts) but no committed byte-identical snapshot (see F02).

Summary counts

Verdict Count ACs
PASS 4 AC1, AC2, AC6, AC8
WEAK 1 FR7 (within the FR group)
FAIL 0
NOT VERIFIED 0 (Mongo facade reachability is flagged as F03; it sits under deferred AC4/AC5 scope but the slice spec's In-list names it — spec/diff disagreement rather than a failed in-scope AC)

In-scope ACs (AC1, AC2, AC6, AC8) all PASS. The in-scope FR set passes except FR7, which is sound by construction but lacks an explicit regression lock.

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