TML-2816: namespace-aware DSL/ORM surface — additive slice#720
Conversation
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>
📝 WalkthroughWalkthroughAdds 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.ChangesNamespace-scoped SQL/ORM surface
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
size-limit report 📦
|
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>
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/emitter
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
| if (ormCallCount === 1) return { lane: 'orm' }; | ||
| return txOrmProxy; | ||
| }) as typeof ormMock); | ||
| }) as unknown as typeof ormMock); |
There was a problem hiding this comment.
Why is this cast now necessary?
…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>
There was a problem hiding this comment.
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 winCustom collection key validation is still default-namespace-only.
Line 204 validates
collectionskeys againstdomainModelsAtDefaultNamespace(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 liftBare-name namespace maps still collide same-named models.
Both
modelNamespaceIdsand the fallbackmodelMappingsoverwrite earlier entries by baremodel.name. That meansauth.Userandpublic.Usercannot coexist safely for any unqualified lookup: relation targets andresolvePolymorphism()will pick whichever namespace was inserted last, not the declaring model’s coordinate. In practice,auth.Session.user User@relation(...)can end up targetingpublic.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 liftRelation metadata is only half-qualified here.
This threads
targetNamespaceId, but the declaring side still stays bare. When two namespaces both containUser, the laterindexFkRelations/ model-relation assembly path cannot distinguishauth.Userfrompublic.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 winValidate
relation.to.namespacebefore accepting a relation entry.At Line 330,
toNamespaceis assigned without checking thatrel.to.namespaceis 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 winScope enum
typeReffallback to the requested namespace.After
namespaceIdbecame required, this fallback still scans all namespaces and can return the wrong enum entry when two namespaces define the sametypeRef. 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 liftInclude namespace in codec instance site identity.
The registry lookup is now namespace-scoped, but
usedAtentries 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
namespaceIdthroughcollectTypeRefSites(...)and extendSqlCodecInstanceContext['usedAt']to carry namespace so contexts remain unambiguous end-to-end.Based on learnings,
buildContractCodecRegistrymust 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 winThread
namespaceIdinto identity-filter binding.Line 215 still builds identity filters without namespace context, and Lines 257/272 bind that filter via
bindWhereExpr(...)withoutnamespaceId. 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 winTrim 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
⛔ Files ignored due to path filters (7)
projects/explicit-namespace-dsl/learnings.mdis excluded by!projects/**projects/explicit-namespace-dsl/plan.mdis excluded by!projects/**projects/explicit-namespace-dsl/plans/plan.mdis excluded by!projects/**projects/explicit-namespace-dsl/slices/01-additive-namespaced-surface/plan.mdis excluded by!projects/**projects/explicit-namespace-dsl/slices/01-additive-namespaced-surface/spec.mdis excluded by!projects/**projects/explicit-namespace-dsl/slices/02-remove-flat-fallback/spec.mdis excluded by!projects/**projects/explicit-namespace-dsl/spec.mdis excluded by!projects/**
📒 Files selected for processing (98)
packages/2-sql/1-core/contract/src/resolve-storage-table.tspackages/2-sql/1-core/contract/test/resolve-storage-table.test.tspackages/2-sql/2-authoring/contract-psl/src/interpreter.tspackages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.tspackages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.namespaces.test.tspackages/2-sql/2-authoring/contract-ts/src/build-contract.tspackages/2-sql/2-authoring/contract-ts/src/contract-definition.tspackages/2-sql/2-authoring/contract-ts/test/contract-builder.cross-namespace-same-table.test.tspackages/2-sql/4-lanes/relational-core/DEVELOPING.mdpackages/2-sql/4-lanes/relational-core/src/ast/codec-types.tspackages/2-sql/4-lanes/relational-core/src/codec-descriptor-registry.tspackages/2-sql/4-lanes/relational-core/src/codec-ref-for-column.tspackages/2-sql/4-lanes/relational-core/src/query-lane-context.tspackages/2-sql/4-lanes/relational-core/test/codec-descriptor-registry.test.tspackages/2-sql/4-lanes/relational-core/test/codec-ref-for-column.test.tspackages/2-sql/4-lanes/sql-builder/src/exports/types.tspackages/2-sql/4-lanes/sql-builder/src/runtime/builder-base.tspackages/2-sql/4-lanes/sql-builder/src/runtime/mutation-impl.tspackages/2-sql/4-lanes/sql-builder/src/runtime/resolve-table.tspackages/2-sql/4-lanes/sql-builder/src/runtime/sql.tspackages/2-sql/4-lanes/sql-builder/src/runtime/table-proxy-impl.tspackages/2-sql/4-lanes/sql-builder/src/types/db.tspackages/2-sql/4-lanes/sql-builder/test/runtime/field-proxy.test.tspackages/2-sql/4-lanes/sql-builder/test/runtime/namespaced-resolution.test.tspackages/2-sql/4-lanes/sql-builder/test/runtime/same-bare-table-name.test.tspackages/2-sql/4-lanes/sql-builder/test/types/namespaced-db.types.test-d.tspackages/2-sql/5-runtime/src/sql-context.tspackages/2-sql/5-runtime/test/contract-codec-registry.test.tspackages/2-sql/5-runtime/test/same-bare-table-name.test.tspackages/2-sql/5-runtime/test/sql-context.codec-context.test.tspackages/3-extensions/postgres/test/fixtures/namespaced-contract.tspackages/3-extensions/postgres/test/namespaced-facade.types.test-d.tspackages/3-extensions/postgres/test/postgres.test.tspackages/3-extensions/sql-orm-client/src/aggregate-builder.tspackages/3-extensions/sql-orm-client/src/collection-column-mapping.tspackages/3-extensions/sql-orm-client/src/collection-contract.tspackages/3-extensions/sql-orm-client/src/collection-dispatch.tspackages/3-extensions/sql-orm-client/src/collection-internal-types.tspackages/3-extensions/sql-orm-client/src/collection-mutation-dispatch.tspackages/3-extensions/sql-orm-client/src/collection-runtime.tspackages/3-extensions/sql-orm-client/src/collection.tspackages/3-extensions/sql-orm-client/src/filters.tspackages/3-extensions/sql-orm-client/src/grouped-collection.tspackages/3-extensions/sql-orm-client/src/model-accessor.tspackages/3-extensions/sql-orm-client/src/mutation-executor.tspackages/3-extensions/sql-orm-client/src/orm.tspackages/3-extensions/sql-orm-client/src/query-plan-aggregate.tspackages/3-extensions/sql-orm-client/src/query-plan-meta.tspackages/3-extensions/sql-orm-client/src/query-plan-mutations.tspackages/3-extensions/sql-orm-client/src/query-plan-select.tspackages/3-extensions/sql-orm-client/src/storage-resolution.tspackages/3-extensions/sql-orm-client/src/types.tspackages/3-extensions/sql-orm-client/src/where-binding.tspackages/3-extensions/sql-orm-client/src/where-interop.tspackages/3-extensions/sql-orm-client/test/aggregate-builder.test.tspackages/3-extensions/sql-orm-client/test/collection-column-mapping.test.tspackages/3-extensions/sql-orm-client/test/collection-contract.test.tspackages/3-extensions/sql-orm-client/test/collection-dispatch.test.tspackages/3-extensions/sql-orm-client/test/collection-fixtures.tspackages/3-extensions/sql-orm-client/test/collection-mutation-dispatch.test.tspackages/3-extensions/sql-orm-client/test/collection-runtime.test.tspackages/3-extensions/sql-orm-client/test/collection-variant.test.tspackages/3-extensions/sql-orm-client/test/filters.test.tspackages/3-extensions/sql-orm-client/test/generated-contract-types.test-d.tspackages/3-extensions/sql-orm-client/test/include-cardinality.test-d.tspackages/3-extensions/sql-orm-client/test/model-accessor.test.tspackages/3-extensions/sql-orm-client/test/mutation-executor.test.tspackages/3-extensions/sql-orm-client/test/namespace-qualification.test.tspackages/3-extensions/sql-orm-client/test/orm-namespace-crud.test.tspackages/3-extensions/sql-orm-client/test/orm-namespace-relation.test.tspackages/3-extensions/sql-orm-client/test/orm-namespace-resolution.test.tspackages/3-extensions/sql-orm-client/test/orm-namespace-returning-crud.test.tspackages/3-extensions/sql-orm-client/test/orm-namespaced.test.tspackages/3-extensions/sql-orm-client/test/orm-namespaced.types.test-d.tspackages/3-extensions/sql-orm-client/test/orm-same-bare-table-name.test.tspackages/3-extensions/sql-orm-client/test/orm.test.tspackages/3-extensions/sql-orm-client/test/orm.types.test-d.tspackages/3-extensions/sql-orm-client/test/query-plan-aggregate.test.tspackages/3-extensions/sql-orm-client/test/query-plan-meta.test.tspackages/3-extensions/sql-orm-client/test/query-plan-mutations.test.tspackages/3-extensions/sql-orm-client/test/query-plan-select.test.tspackages/3-extensions/sql-orm-client/test/repository.test.tspackages/3-extensions/sql-orm-client/test/rich-filters-and-where.test.tspackages/3-extensions/sql-orm-client/test/rich-query-plans.test.tspackages/3-extensions/sql-orm-client/test/simplify-deep.test-d.tspackages/3-extensions/sql-orm-client/test/storage-resolution.test.tspackages/3-extensions/sqlite/test/fixtures/namespaced-contract.tspackages/3-extensions/sqlite/test/namespaced-facade.types.test-d.tstest/integration/test/namespaced-accessors-e2e.integration.test.tstest/integration/test/sql-orm-client/collection-fixtures.tstest/integration/test/sql-orm-client/collection-mutation-defaults.test.tstest/integration/test/sql-orm-client/include.test.tstest/integration/test/sql-orm-client/integration-helpers.tstest/integration/test/sql-orm-client/model-accessor.pgvector.test.tstest/integration/test/sql-orm-client/nested-includes-helpers.tstest/integration/test/sql-orm-client/polymorphism.test.tstest/integration/test/sql-orm-client/upsert.test.ts
| 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) { |
There was a problem hiding this comment.
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.
| 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.
| } 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`); |
There was a problem hiding this comment.
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.
| } 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.
| 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, | ||
| ); |
There was a problem hiding this comment.
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.
| 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.
Overview
Slice 01 of TML-2816 (always-qualified, namespace-aware DSL/ORM surface). Adds the explicit per-namespace accessors —
sql.<ns>.<table>andorm.<ns>.<Model>— additively: the existing flat surface is retained, and its removal + the facade projection are slice 02. Branch is rebased ontoorigin/main(incl. TML-2807 —SqlModelStorage.namespaceId+ kind-agnostic storage hash).What's in this slice
Db<C>gains per-namespace facets (sql.<ns>.<table>) alongside the flat surface; a two-level proxy delegates to coordinate-aware resolvers.orm.<ns>.<Model>facets; per-namespace model resolution; ORM keys aligned to SQL storage-namespace keys.resolveStorageTable/codecRefForStorageColumn/resolveTableColumnstake 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).db.sql.<ns>.<table>/db.orm.<ns>.<Model>reach through the postgres + sqlite facades (incl.transaction/prepare), type-locked.Why
A multi-namespace contract must be navigable by explicit namespace (
auth.usersvspublic.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 (storagenamespaceId) and TML-2605 (runtime qualification).Deferred to slice 02 (the breaking cut + facade projection)
dbaliased to the connector's default-namespace facet on single-namespace targets vs. the qualified surface on multi-namespace targets, driven solely bydefaultNamespaceId. This is the "bare = default namespace" ergonomic, realized at the facade as the ADR-223 "caller that supplies the default" (runtime stays target-agnostic).Db<C>facet construction (AC7).Remaining for slice 01 (to finish on this draft)
contract.jsonsnapshot-unchanged (FR7).Validation
pnpm build,pnpm typecheck(135/135),pnpm fixtures:check(no drift),pnpm lint:deps— green.mongodb-memory-serversuites, 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
db.public.users,orm.public.User).Bug Fixes