Skip to content

feat(operations): author sql operations as typescript functions#374

Merged
SevInf merged 4 commits intomainfrom
worktree/op-registry-ts
Apr 29, 2026
Merged

feat(operations): author sql operations as typescript functions#374
SevInf merged 4 commits intomainfrom
worktree/op-registry-ts

Conversation

@SevInf
Copy link
Copy Markdown
Contributor

@SevInf SevInf commented Apr 23, 2026

closes TML-2307

Intent

Replace the closed declarative operation spec ({args, returns, lowering}) with a TypeScript function that is the operation: its signature is the type-level surface and its body builds the AST. A tiny SqlOperationDescriptor{method, self?, impl} — carries only what the framework actually needs to dispatch, so authors get TypeScript's full expressivity (generics, conditional returns, non-DB argument types, overloads) on the authoring surface and the single surface cannot drift out of step with itself. ADR 204 records the decision.

Change map

Tests (evidence):

The story

  1. Introduce the expression primitives. A new module in relational-core defines Expression<T> (structural: {returnType, buildAst}), CodecExpression<CodecId, Nullable, CT> (exact codec), and TraitExpression<Traits, Nullable, CT> (codec gated by capability traits). toExpr(value, codecId?) wraps a raw JS value as a ParamRef, or threads through an existing Expression. buildOperation({method, args, returns, lowering}) constructs the OperationExpr AST node and returns it as an Expression<Returns>. These are the building blocks operations use.

  2. Shrink the operation descriptor. OperationEntry drops args and returns and becomes {self?, impl}. The framework-level registry validates only that self (when present) names either a codec identity or a trait set, not both — not the old per-argument check. Return codec and lowering live on the AST node the impl constructs; the registry no longer needs to see them.

  3. Flow authored impls through the SQL builder. The builder's fns surface used to wrap each registered op in a generated function that coerced arguments by reading the declarative argument specs. That wrapper is deleted: fns.someOp now returns the authored impl directly. Any generics, conditional returns, or overloads the author wrote survive to the call site unchanged. Aggregate functions (count, sum, avg, …) drop the ExpressionImpl.field field and read expr.returnType structurally.

  4. Make columns Expressions. The ORM field accessor used to be a bag of comparison/extension-method functions. It now is an Expression<{codecId, nullable}> — carrying returnType and buildAst() on the same object — with comparison and extension methods spread on top. Extension-method factories receive the column as a self Expression and forward it to impl. The new ExpressionImpl(column, {codecId: '', nullable: false}) hack is gone. Cross-column composition — db.a.embedding.cosineDistance(db.b.embedding) — now works because the second argument is an Expression, which the CodecExpression union accepts.

  5. Tighten ORM accessor edges. The Proxy used to synthesise a fail-closed accessor for unknown fields; it now returns undefined like any plain JS object would. The relation-shorthand iterator, which used to silently skip unknown predicate keys, now throws — because a typo'd filter key that silently matches every row is a sharp-edged footgun.

  6. Rewire contributors as factories. postgres/ilike, pgvector/{cosineDistance,cosineSimilarity}, and mongo-adapter/near are ported: each adapter/extension exports a QueryOperations<CT>() factory generic over the contract's codec-types map and a matching QueryOperationTypes<CT> at the type level. The runtime-descriptor slots (queryOperations: () => …) call each factory at assembly time.

  7. Emit the intersection. The contract emitter used to reach for a shared generateCodecTypeIntersection helper; it now inlines a per-import QueryOperationTypes<CodecTypes> alias list, producing A<CodecTypes> & B<CodecTypes> — the shape that reaches the SQL builder and ORM client is already specialised to the contract's concrete codec-types map.

Behavior changes & evidence

Compatibility / migration / risk

  • Breaking change for extensions that shipped declarative operation records. Rewrite to a CT-generic factory returning {method, self?, impl}; return codec and lowering move inside the impl's buildOperation({ returns, lowering }) call. ADR 017 (Extension Compatibility Policy) is honoured — no legacy-shape retention.
  • ORM column method signatures widen. .cosineDistance(v: number[] | null) becomes .cosineDistance(CodecExpression<'pg/vector@1', Nullable, CT>), which is a superset (raw value, null, or another Expression of the same codec). Callers that previously passed only raw JS values continue to compile.
  • Unknown accessor keys behave differently. Programmatic code that relied on the old fail-closed Proxy accessor for unknown fields (e.g. catching a thrown error) must now detect undefined; relation-shorthand predicates must avoid typo'd keys or be prepared to catch the new error.

Follow-ups / open questions

  • ADR 204 leaves one design question open: the adapter runtime descriptor's queryOperations slot has the shape () => readonly SqlOperationDescriptor[], so each adapter's thunk currently calls its QueryOperations<CT>() factory internally with an unconstrained CT. Threading the contract's concrete codec-types map through the SPI slot is a separate change.

Non-goals / intentionally out of scope

  • Not touching the built-in comparison methods (eq, gt, like, isNull, …) — they remain codec-trait-gated via COMPARISON_METHODS_META rather than registered operations.
  • Not deriving the self dispatch hint from the function's first parameter. The explicit self field keeps the dispatch key obvious to readers and keeps the type-level matcher identical to the runtime walk.

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced a typed expression system enabling operations to be authored as TypeScript functions with improved type safety and codec awareness.
    • Operations now support flexible dispatch via concrete codec targeting or trait-based capability selection.
  • Refactor

    • Simplified operation registration API to use minimal dispatch metadata and implementation functions instead of parameter/return specifications.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@SevInf has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 53 minutes and 54 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: cda7026c-5f05-408f-8e36-3226653b72d4

📥 Commits

Reviewing files that changed from the base of the PR and between b93ba3b and afef53b.

⛔ Files ignored due to path filters (5)
  • packages/2-sql/4-lanes/sql-builder/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • test/e2e/framework/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • test/integration/test/sql-builder/fixtures/generated/contract.d.ts is excluded by !**/generated/**
📒 Files selected for processing (42)
  • docs/architecture docs/ADR-INDEX.md
  • docs/architecture docs/adrs/ADR 206 - Operations as TypeScript functions.md
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • packages/1-framework/1-core/operations/src/index.ts
  • packages/1-framework/1-core/operations/test/operations-registry.test.ts
  • packages/2-sql/1-core/contract/src/exports/types.ts
  • packages/2-sql/1-core/contract/src/types.ts
  • packages/2-sql/1-core/operations/package.json
  • packages/2-sql/1-core/operations/src/index.ts
  • packages/2-sql/1-core/operations/test/operations-registry.test.ts
  • packages/2-sql/3-tooling/emitter/src/index.ts
  • packages/2-sql/4-lanes/relational-core/package.json
  • packages/2-sql/4-lanes/relational-core/src/exports/expression.ts
  • packages/2-sql/4-lanes/relational-core/src/expression.ts
  • packages/2-sql/4-lanes/relational-core/src/index.ts
  • packages/2-sql/4-lanes/relational-core/test/expression.test.ts
  • packages/2-sql/4-lanes/relational-core/tsdown.config.ts
  • packages/2-sql/4-lanes/sql-builder/src/expression.ts
  • packages/2-sql/4-lanes/sql-builder/src/runtime/builder-base.ts
  • packages/2-sql/4-lanes/sql-builder/src/runtime/expression-impl.ts
  • packages/2-sql/4-lanes/sql-builder/src/runtime/functions.ts
  • packages/2-sql/4-lanes/sql-builder/src/scope.ts
  • packages/2-sql/4-lanes/sql-builder/test/runtime/expression-impl.test.ts
  • packages/2-sql/4-lanes/sql-builder/test/runtime/field-proxy.test.ts
  • packages/2-sql/4-lanes/sql-builder/test/runtime/functions.test.ts
  • packages/2-sql/5-runtime/test/execution-stack.test.ts
  • packages/2-sql/5-runtime/test/sql-context.test.ts
  • packages/3-extensions/pgvector/src/core/descriptor-meta.ts
  • packages/3-extensions/pgvector/src/exports/control.ts
  • packages/3-extensions/pgvector/src/exports/runtime.ts
  • packages/3-extensions/pgvector/src/types/operation-types.ts
  • packages/3-extensions/pgvector/test/operations.test.ts
  • packages/3-extensions/sql-orm-client/src/model-accessor.ts
  • packages/3-extensions/sql-orm-client/src/types.ts
  • packages/3-extensions/sql-orm-client/test/extension-operations.test-d.ts
  • packages/3-extensions/sql-orm-client/test/model-accessor.test.ts
  • packages/3-mongo-target/2-mongo-adapter/src/core/operations.ts
  • packages/3-mongo-target/2-mongo-adapter/test/codecs.test.ts
  • packages/3-targets/6-adapters/postgres/src/core/descriptor-meta.ts
  • packages/3-targets/6-adapters/postgres/src/exports/runtime.ts
  • packages/3-targets/6-adapters/postgres/src/types/operation-types.ts
  • test/integration/test/fixtures/contract.d.ts
📝 Walkthrough

Walkthrough

This pull request refactors the operation specification model across the framework, moving from parameter/return metadata (args/returns) to a minimal self dispatch hint plus an impl function. The change spans operation registration APIs, SQL contract types, a new expression abstraction system, SQL builder runtime, and all extension operation implementations.

Changes

Cohort / File(s) Summary
Documentation & Design
docs/architecture/docs/ADR-INDEX.md, docs/architecture/docs/adrs/ADR 206 - Operations as TypeScript functions.md
Added ADR entries for ADR 203 and 206; new ADR 206 documents the authoring model where operations are TypeScript functions with self dispatch hint and impl body.
Core Framework - Operation Registration
packages/1-framework/1-core/operations/src/index.ts, packages/1-framework/1-core/operations/test/operations-registry.test.ts
Replaced per-parameter args/returns metadata with optional self?: SelfSpec and required impl function. Registry validation now checks self for valid codecId XOR traits[] pattern.
SQL Contract Types
packages/2-sql/1-core/contract/src/types.ts, packages/2-sql/1-core/contract/src/exports/types.ts
Removed QueryOperationArgSpec, added QueryOperationSelfSpec and QueryOperationReturn types. Updated QueryOperationTypeEntry to use self?: SelfSpec and impl instead of args/returns. Extended SqlQueryOperationTypes with codec-types generic parameter.
SQL Operations Registry
packages/2-sql/1-core/operations/src/index.ts, packages/2-sql/1-core/operations/test/operations-registry.test.ts, packages/2-sql/1-core/operations/package.json
Changed SqlOperationEntry to alias QueryOperationTypeEntry. Added dependency on @prisma-next/sql-contract. Updated tests to validate self/impl shape.
Expression Abstraction System
packages/2-sql/4-lanes/relational-core/src/expression.ts, packages/2-sql/4-lanes/relational-core/src/exports/expression.ts, packages/2-sql/4-lanes/relational-core/package.json, packages/2-sql/4-lanes/relational-core/tsdown.config.ts, packages/2-sql/4-lanes/relational-core/test/expression.test.ts, packages/2-sql/4-lanes/relational-core/src/index.ts
New module introducing Expression<T> type with returnType metadata and buildAst() method. Added toExpr runtime helper and buildOperation factory. Included CodecExpression and TraitExpression type aliases for dispatch-aware expressions.
SQL Builder - Expression & Function Handling
packages/2-sql/4-lanes/sql-builder/src/expression.ts, packages/2-sql/4-lanes/sql-builder/src/runtime/expression-impl.ts, packages/2-sql/4-lanes/sql-builder/src/runtime/functions.ts, packages/2-sql/4-lanes/sql-builder/src/runtime/builder-base.ts, packages/2-sql/4-lanes/sql-builder/src/scope.ts, packages/2-sql/4-lanes/sql-builder/test/runtime/*
Refactored to use Expression from relational-core instead of local ExpressionType. Updated ExpressionImpl to carry returnType property. Rewrote function creation to accept operations map and invoke op.impl directly. Removed ExpressionOrValue mechanism and updated operator signatures.
SQL Emitter Tooling
packages/2-sql/3-tooling/emitter/src/index.ts
Modified getFamilyTypeAliases to derive QueryOperationTypes from queryOperationTypeImports with codec-typing instead of using generateCodecTypeIntersection.
PgVector Extension Operations
packages/3-extensions/pgvector/src/core/descriptor-meta.ts, packages/3-extensions/pgvector/src/types/operation-types.ts, packages/3-extensions/pgvector/src/exports/control.ts, packages/3-extensions/pgvector/src/exports/runtime.ts, packages/3-extensions/pgvector/test/operations.test.ts
Changed pgvectorQueryOperations from constant array to generic factory function. Updated operation definitions to use self with buildOperation and toExpr. Updated type signature to accept codec-types parameter. Modified tests to validate AST lowering from impl invocation.
Postgres Target Operations
packages/3-targets/6-adapters/postgres/src/core/descriptor-meta.ts, packages/3-targets/6-adapters/postgres/src/types/operation-types.ts, packages/3-targets/6-adapters/postgres/src/exports/runtime.ts
Changed postgresQueryOperations from constant array to generic factory. Updated ilike operation to use self-based and buildOperation. Made QueryOperationTypes generic over codec types.
MongoDB Operations
packages/3-mongo-target/2-mongo-adapter/src/core/operations.ts, packages/3-mongo-target/2-mongo-adapter/test/codecs.test.ts
Updated mongoVectorNearOperation descriptor to use self codec and impl stub instead of args/returns. Adjusted test assertions to check self field.
ORM Client Implementation
packages/3-extensions/sql-orm-client/src/model-accessor.ts, packages/3-extensions/sql-orm-client/src/types.ts, packages/3-extensions/sql-orm-client/test/extension-operations.test-d.ts, packages/3-extensions/sql-orm-client/test/model-accessor.test.ts
Refactored query operation registration to use entry.self dispatch. Updated type machinery to extract return specs from Expression return types. Modified accessor generation to construct expression-shaped accessors. Improved error handling for unknown fields.
Type Fixtures & Integration Tests
examples/prisma-next-demo/src/prisma/contract.d.ts, test/integration/test/fixtures/contract.d.ts, packages/2-sql/5-runtime/test/execution-stack.test.ts, packages/2-sql/5-runtime/test/sql-context.test.ts
Updated QueryOperationTypes definitions to be parameterized with CodecTypes. Modified test fixtures to use self/impl descriptor shape instead of args/returns/lowering.

Sequence Diagram(s)

sequenceDiagram
    participant Author as Operation Author
    participant Builder as SQL Builder
    participant Expr as Expression System
    participant Runtime as Runtime Engine
    
    Author->>Builder: Define operation as TypeScript function<br/>(self: TraitExpression, arg: CodecExpression)
    
    Builder->>Expr: Call buildOperation(spec)<br/>method, self, args, returns, lowering
    Expr->>Expr: Convert args via toExpr<br/>wrap in ParamRef/ColumnRef
    Expr->>Expr: Create OperationExpr AST<br/>with method, self, args, lowering
    Expr->>Expr: Wrap AST in Expression<T><br/>attach returnType metadata
    Expr->>Builder: Return Expression<T>
    
    Builder->>Runtime: Use expression in query<br/>call buildAst() for AST
    Runtime->>Runtime: Execute operation<br/>apply lowering, return result
    Runtime->>Builder: Result with codec identity
    Builder->>Author: Typed query result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • wmadden
  • aqrln

Poem

🐰 Operations bloom as TypeScript's dance,
Where self and impl lead the prance,
Expressions wrapped with returnType bright,
From trait-based hints to runtime's flight,
A framework reborn, more nimble and right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(operations): author sql operations as typescript functions' clearly and specifically describes the primary architectural change: operations are now authored as TypeScript functions rather than declarative specs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree/op-registry-ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 53 minutes and 54 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 23, 2026

Open in StackBlitz

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/middleware-telemetry

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/middleware-telemetry@374

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: afef53b

@SevInf SevInf force-pushed the worktree/op-registry-ts branch from 95aac77 to 05ae897 Compare April 23, 2026 16:52
Copy link
Copy Markdown

@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: 6

🧹 Nitpick comments (5)
packages/3-extensions/pgvector/test/operations.test.ts (1)

29-55: Prefer non-null assertion after toBeDefined().

cosineDistanceOp and cosineSimilarityOp are asserted defined one line above, so the optional chaining on ?.impl(...) is defensive-check noise. Per the repo guideline "Prefer assertions over defensive checks when data is guaranteed to be valid", use !:

Proposed fix
-    const distExpr = cosineDistanceOp?.impl(
+    const distExpr = cosineDistanceOp!.impl(
       ParamRef.of([1, 2], { codecId: 'pg/vector@1' }) as never,
       [3, 4] as never,
     ) as unknown as { buildAst(): OperationExpr };
...
-    const simExpr = cosineSimilarityOp?.impl(
+    const simExpr = cosineSimilarityOp!.impl(
       ParamRef.of([1, 2], { codecId: 'pg/vector@1' }) as never,
       [3, 4] as never,
     ) as unknown as { buildAst(): OperationExpr };

As per coding guidelines: "Prefer assertions over defensive checks when data is guaranteed to be valid - use non-null assertions (e.g., columnMeta!) after validation checks instead of optional chaining".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-extensions/pgvector/test/operations.test.ts` around lines 29 - 55,
The test currently uses optional chaining on cosineDistanceOp?.impl(...) and
cosineSimilarityOp?.impl(...) immediately after expect(...).toBeDefined();
update both to use non-null assertions instead (cosineDistanceOp!.impl(...) and
cosineSimilarityOp!.impl(...)) so the code reflects the prior assertion; keep
the rest of the invocation and casts (ParamRef.of, as never, buildAst) unchanged
and still assert the resulting distAst/simAst are OperationExpr instances and
their lowering values.
packages/3-extensions/sql-orm-client/test/extension-operations.test-d.ts (1)

40-62: These tests only verify that calls compile, not the parameter type.

Replacing the previous toEqualTypeOf<[number[] | null]>() assertion with plain fn(...) calls loses the type-level assertion: any widening of the argument type (e.g. accidentally becoming unknown) would still pass these tests. If the intent is to document accepted call sites, consider pinning the parameter type with expectTypeOf alongside the exercise, e.g.:

expectTypeOf<Parameters<Fn>[0]>().toExtend<number[] | null | { buildAst(): unknown }>();

so regressions in the impl signature are actually caught. Otherwise these tests mostly just assert toBeFunction().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-extensions/sql-orm-client/test/extension-operations.test-d.ts`
around lines 40 - 62, The tests for PostAccessor['embedding']['cosineDistance']
and ['cosineSimilarity'] currently only assert the calls compile; pin the
parameter type to catch regressions by adding a type-level assertion for
Parameters<Fn>[0] for each test (e.g., use
expectTypeOf<Parameters<Fn>[0]>().toExtend<number[] | null | { buildAst():
unknown }>()), keeping the existing runtime call checks; reference the Fn type
alias, PostAccessor, and the cosineDistance/cosineSimilarity functions when you
update each test.
packages/3-mongo-target/2-mongo-adapter/test/codecs.test.ts (1)

149-153: Tautological placeholder test.

This test only verifies the placeholder behavior of the current stub (() => undefined as never). If/when Mongo actually lowers near, this test will start failing for the right reason — but it provides no behavioral value today and encodes the stub into the spec. Consider removing it and instead asserting only the descriptor shape (method/self), or replace with a TODO-skipped test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-mongo-target/2-mongo-adapter/test/codecs.test.ts` around lines 149
- 153, The test asserting that mongoVectorNearOperation.impl() returns undefined
is tautological and encodes the current stub; remove or replace it so the spec
doesn't lock in the stub. Update the test for mongoVectorNearOperation to either
(a) assert only the descriptor shape (e.g., that mongoVectorNearOperation has
the expected properties such as .impl being a function, and descriptor fields
like .method/.self where applicable) or (b) convert the test to a skipped/TODO
test noting that behavior will change when Mongo lowers `near`; reference
mongoVectorNearOperation.impl and the descriptor name in the replacement.
packages/3-extensions/sql-orm-client/test/model-accessor.test.ts (1)

103-123: Cross-column test doesn't cross columns.

The test name and comment emphasize "cross-column composition", but both post and otherPost resolve to ColumnRef.of('posts', 'embedding') (same model, same field). The assertion passes, but it would equally pass if toExpr collapsed the second argument to the same ColumnRef as self — the test doesn't distinguish.

Consider threading the argument through a distinct field/model to make the "column-handle, not raw value" branch observable:

💡 Suggested tightening
-    const post = createModelAccessor(context, 'Post');
-    const otherPost = createModelAccessor(context, 'Post');
-
-    const result = post['embedding']!.cosineDistance(otherPost['embedding']!) as unknown as Record<
-      string,
-      unknown
-    >;
+    const post = createModelAccessor(context, 'Post');
+    // Use a different vector-typed field (or a different aliased model) so
+    // `self` and `arg0` resolve to distinct ColumnRefs.
+    const result = post['embedding']!.cosineDistance(post['otherEmbedding']!) as unknown as Record<
+      string,
+      unknown
+    >;
     ...
-    expect(opExpr.args[0]).toEqual(ColumnRef.of('posts', 'embedding'));
+    expect(opExpr.args[0]).toEqual(ColumnRef.of('posts', 'other_embedding'));

Skip if the fixture doesn't have a second vector column handy — the current test still exercises the isExpressionLike branch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-extensions/sql-orm-client/test/model-accessor.test.ts` around
lines 103 - 123, The test claims to verify cross-column composition but uses two
identical column handles, so change the second accessor to point to a different
model/field so the produced ColumnRef is distinct and the test actually verifies
that cosineDistance treats the second arg as an Expression (not a ParamRef);
specifically modify the second accessor created by createModelAccessor
(otherPost) to reference a different model or vector field (e.g., use
createModelAccessor(context, 'Comment') or a different field name instead of
'embedding') and keep assertions on OperationExpr.method === 'cosineDistance'
and that opExpr.args[0] is a ColumnRef unequal to opExpr.self.
packages/1-framework/1-core/operations/src/index.ts (1)

12-14: Consider non-empty tuple for traits to mirror runtime check.

readonly string[] allows { traits: [] } at compile time, but the runtime validator at line 42 rejects it. Tightening the type closes the gap and surfaces the error at authoring time:

♻️ Suggested tightening
 export type SelfSpec =
   | { readonly codecId: string; readonly traits?: never }
-  | { readonly traits: readonly string[]; readonly codecId?: never };
+  | { readonly traits: readonly [string, ...string[]]; readonly codecId?: never };

Not a functional bug — the runtime guard already catches it — but it keeps the self vocabulary consistent across compile/runtime.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/1-core/operations/src/index.ts` around lines 12 - 14,
The SelfSpec union allows traits to be an empty array at compile time but the
runtime validator rejects empty traits; update the SelfSpec type so traits is a
non-empty tuple (i.e., require a first string element then zero-or-more strings)
instead of plain readonly string[] to reflect the runtime check and surface
empty-traits errors at compile time; adjust the existing union branches (the
variant with readonly traits and the codecId-exclusive variant) accordingly
while preserving the readonly and mutual-exclusion semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/2-sql/4-lanes/relational-core/src/expression.ts`:
- Around line 71-85: The current isExpressionLike type guard only checks for a
buildAst function and allows arbitrary objects to be treated as
Expression<ScopeField>, letting raw codec inputs bypass ParamRef; update
isExpressionLike (used by toExpr) to also verify the presence and shape of a
returnType property (e.g., ensure value.returnType is an object and has codecId
of type string and nullable of type boolean) so that only true Expression-like
objects with both buildAst() and a valid returnType pass the guard and other
values fall through to ParamRef.of.
- Around line 20-29: The conditional in the CodecIdsWithTrait type is comparing
RequiredTraits[number] to the inferred traits tuple T itself, causing mismatches
when traits are arrays/tuples; change the check to compare against the element
union of the tuple by using T[number] (keep the tuple-wrapping trick to avoid
distributive conditionals). Concretely, in CodecIdsWithTrait update the
conditional from checking [RequiredTraits[number]] extends [T] to
[RequiredTraits[number]] extends [T[number]] so the type extracts the trait
element types correctly (refer to CodecIdsWithTrait, CT, RequiredTraits, and the
inferred T).

In `@packages/3-extensions/pgvector/src/core/descriptor-meta.ts`:
- Around line 23-30: The impls for cosineDistance and cosineSimilarity declare
operands as CodecExpression<'pg/vector@1', boolean, CT> but always return
Expression with returns.nullable: false, causing nullable vector inputs to be
incorrectly typed as non-null; fix by either changing the operand nullability
parameter from boolean to false (i.e., CodecExpression<'pg/vector@1', false,
CT>) to forbid nullable operands, or propagate operand nullability into the
return metadata (compute returns.nullable = self.nullable || other.nullable) by
reading the boolean nullability of the incoming CodecExpression and using that
value when building the return Expression in the buildOperation call (update the
impls for both cosineDistance and cosineSimilarity and ensure toExpr usage
remains correct).

In `@packages/3-extensions/pgvector/src/types/operation-types.ts`:
- Around line 30-40: The exported operation types currently accept nullable
vector operands (CodecExpression<'pg/vector@1', boolean, CT>) but return a
non-null float result; mirror the pgvector nullability contract by making the
operand nullability explicit—change the operand types in the impl signature(s)
to non-null (CodecExpression<'pg/vector@1', false, CT>) or alternatively make
the returned Expression nullable when operands can be nullable; specifically
update the impl signatures for cosineSimilarity (and any sibling impl shown) to
use false for the operand nullability so the public types match the descriptor
contract.

In `@packages/3-mongo-target/2-mongo-adapter/src/core/operations.ts`:
- Around line 4-8: The placeholder impl on mongoVectorNearOperation currently
returns undefined and can silently propagate errors; change the impl (on
mongoVectorNearOperation) to immediately throw a clear runtime Error (e.g.,
"mongo vector 'near' operation not implemented/registered") instead of returning
undefined so any premature dispatch fails loudly, remove the unnecessary "as
never" cast, and update the placeholder test in codecs.test.ts to expect/verify
that calling the impl throws.

In `@packages/3-targets/6-adapters/postgres/src/core/descriptor-meta.ts`:
- Around line 147-155: The ilike operation's pattern parameter is incorrectly
typed as CodecExpression<'pg/text@1', false, CT> which prevents
cross-textual-codec use; change the pattern parameter type in both the ilike
signatures (the one in operation-types and the impl in descriptor-meta for
ilike) to TraitExpression<readonly ['textual'], false, CT> so it matches self;
keep the existing toExpr(pattern, PG_TEXT_CODEC_ID) normalization call and
returns/lowering unchanged (symbols to update: ilike, TraitExpression,
CodecExpression, toExpr, PG_TEXT_CODEC_ID).

---

Nitpick comments:
In `@packages/1-framework/1-core/operations/src/index.ts`:
- Around line 12-14: The SelfSpec union allows traits to be an empty array at
compile time but the runtime validator rejects empty traits; update the SelfSpec
type so traits is a non-empty tuple (i.e., require a first string element then
zero-or-more strings) instead of plain readonly string[] to reflect the runtime
check and surface empty-traits errors at compile time; adjust the existing union
branches (the variant with readonly traits and the codecId-exclusive variant)
accordingly while preserving the readonly and mutual-exclusion semantics.

In `@packages/3-extensions/pgvector/test/operations.test.ts`:
- Around line 29-55: The test currently uses optional chaining on
cosineDistanceOp?.impl(...) and cosineSimilarityOp?.impl(...) immediately after
expect(...).toBeDefined(); update both to use non-null assertions instead
(cosineDistanceOp!.impl(...) and cosineSimilarityOp!.impl(...)) so the code
reflects the prior assertion; keep the rest of the invocation and casts
(ParamRef.of, as never, buildAst) unchanged and still assert the resulting
distAst/simAst are OperationExpr instances and their lowering values.

In `@packages/3-extensions/sql-orm-client/test/extension-operations.test-d.ts`:
- Around line 40-62: The tests for PostAccessor['embedding']['cosineDistance']
and ['cosineSimilarity'] currently only assert the calls compile; pin the
parameter type to catch regressions by adding a type-level assertion for
Parameters<Fn>[0] for each test (e.g., use
expectTypeOf<Parameters<Fn>[0]>().toExtend<number[] | null | { buildAst():
unknown }>()), keeping the existing runtime call checks; reference the Fn type
alias, PostAccessor, and the cosineDistance/cosineSimilarity functions when you
update each test.

In `@packages/3-extensions/sql-orm-client/test/model-accessor.test.ts`:
- Around line 103-123: The test claims to verify cross-column composition but
uses two identical column handles, so change the second accessor to point to a
different model/field so the produced ColumnRef is distinct and the test
actually verifies that cosineDistance treats the second arg as an Expression
(not a ParamRef); specifically modify the second accessor created by
createModelAccessor (otherPost) to reference a different model or vector field
(e.g., use createModelAccessor(context, 'Comment') or a different field name
instead of 'embedding') and keep assertions on OperationExpr.method ===
'cosineDistance' and that opExpr.args[0] is a ColumnRef unequal to opExpr.self.

In `@packages/3-mongo-target/2-mongo-adapter/test/codecs.test.ts`:
- Around line 149-153: The test asserting that mongoVectorNearOperation.impl()
returns undefined is tautological and encodes the current stub; remove or
replace it so the spec doesn't lock in the stub. Update the test for
mongoVectorNearOperation to either (a) assert only the descriptor shape (e.g.,
that mongoVectorNearOperation has the expected properties such as .impl being a
function, and descriptor fields like .method/.self where applicable) or (b)
convert the test to a skipped/TODO test noting that behavior will change when
Mongo lowers `near`; reference mongoVectorNearOperation.impl and the descriptor
name in the replacement.
🪄 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: 3b77b6d9-0fb7-4cb2-ac27-1db8bfcea834

📥 Commits

Reviewing files that changed from the base of the PR and between 749d9f9 and 05ae897.

⛔ Files ignored due to path filters (5)
  • packages/2-sql/4-lanes/sql-builder/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • test/e2e/framework/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • test/integration/test/sql-builder/fixtures/generated/contract.d.ts is excluded by !**/generated/**
📒 Files selected for processing (42)
  • docs/architecture docs/ADR-INDEX.md
  • docs/architecture docs/adrs/ADR 204 - Operations as TypeScript functions.md
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • packages/1-framework/1-core/operations/src/index.ts
  • packages/1-framework/1-core/operations/test/operations-registry.test.ts
  • packages/2-sql/1-core/contract/src/exports/types.ts
  • packages/2-sql/1-core/contract/src/types.ts
  • packages/2-sql/1-core/operations/package.json
  • packages/2-sql/1-core/operations/src/index.ts
  • packages/2-sql/1-core/operations/test/operations-registry.test.ts
  • packages/2-sql/3-tooling/emitter/src/index.ts
  • packages/2-sql/4-lanes/relational-core/package.json
  • packages/2-sql/4-lanes/relational-core/src/exports/expression.ts
  • packages/2-sql/4-lanes/relational-core/src/expression.ts
  • packages/2-sql/4-lanes/relational-core/src/index.ts
  • packages/2-sql/4-lanes/relational-core/test/expression.test.ts
  • packages/2-sql/4-lanes/relational-core/tsdown.config.ts
  • packages/2-sql/4-lanes/sql-builder/src/expression.ts
  • packages/2-sql/4-lanes/sql-builder/src/runtime/builder-base.ts
  • packages/2-sql/4-lanes/sql-builder/src/runtime/expression-impl.ts
  • packages/2-sql/4-lanes/sql-builder/src/runtime/functions.ts
  • packages/2-sql/4-lanes/sql-builder/src/scope.ts
  • packages/2-sql/4-lanes/sql-builder/test/runtime/expression-impl.test.ts
  • packages/2-sql/4-lanes/sql-builder/test/runtime/field-proxy.test.ts
  • packages/2-sql/4-lanes/sql-builder/test/runtime/functions.test.ts
  • packages/2-sql/5-runtime/test/execution-stack.test.ts
  • packages/2-sql/5-runtime/test/sql-context.test.ts
  • packages/3-extensions/pgvector/src/core/descriptor-meta.ts
  • packages/3-extensions/pgvector/src/exports/control.ts
  • packages/3-extensions/pgvector/src/exports/runtime.ts
  • packages/3-extensions/pgvector/src/types/operation-types.ts
  • packages/3-extensions/pgvector/test/operations.test.ts
  • packages/3-extensions/sql-orm-client/src/model-accessor.ts
  • packages/3-extensions/sql-orm-client/src/types.ts
  • packages/3-extensions/sql-orm-client/test/extension-operations.test-d.ts
  • packages/3-extensions/sql-orm-client/test/model-accessor.test.ts
  • packages/3-mongo-target/2-mongo-adapter/src/core/operations.ts
  • packages/3-mongo-target/2-mongo-adapter/test/codecs.test.ts
  • packages/3-targets/6-adapters/postgres/src/core/descriptor-meta.ts
  • packages/3-targets/6-adapters/postgres/src/exports/runtime.ts
  • packages/3-targets/6-adapters/postgres/src/types/operation-types.ts
  • test/integration/test/fixtures/contract.d.ts

Comment on lines +20 to +29
type CodecIdsWithTrait<
CT extends Record<string, { readonly input: unknown }>,
RequiredTraits extends readonly string[],
> = {
[K in keyof CT & string]: CT[K] extends { readonly traits: infer T }
? [RequiredTraits[number]] extends [T]
? K
: never
: never;
}[keyof CT & string];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect codec type maps to confirm whether `traits` is represented as an array/tuple.
# Expected: If matches show `traits: readonly [...]` or `traits: [...]`, the current conditional should use `traits[number]`.

rg -n -C3 "readonly traits|traits:" --glob '*.ts' packages test examples

Repository: prisma/prisma-next

Length of output: 50374


🏁 Script executed:

sed -n '20,29p' packages/2-sql/4-lanes/relational-core/src/expression.ts

Repository: prisma/prisma-next

Length of output: 366


🏁 Script executed:

rg -A5 "CodecIdsWithTrait" packages/2-sql/4-lanes/relational-core/src/expression.ts

Repository: prisma/prisma-next

Length of output: 765


🏁 Script executed:

rg "TraitExpression" packages/2-sql/4-lanes/relational-core/src/ --type ts -A2 | head -50

Repository: prisma/prisma-next

Length of output: 325


🏁 Script executed:

rg "TraitExpression<" packages/2-sql/ packages/3-targets/ test/ --type ts -B2 -A3 | head -100

Repository: prisma/prisma-next

Length of output: 1997


🏁 Script executed:

find packages/2-sql/4-lanes/relational-core -name "*.test-d.ts" -o -name "*.test.ts" | head -5

Repository: prisma/prisma-next

Length of output: 389


🏁 Script executed:

find packages/2-sql/4-lanes/relational-core -name "*expression*" -type f

Repository: prisma/prisma-next

Length of output: 246


🏁 Script executed:

cat packages/2-sql/4-lanes/relational-core/test/expression.test.ts

Repository: prisma/prisma-next

Length of output: 4495


🏁 Script executed:

find packages/2-sql/4-lanes/relational-core -name "*.test-d.ts"

Repository: prisma/prisma-next

Length of output: 138


🏁 Script executed:

cat packages/2-sql/4-lanes/relational-core/test/ast/driver-types.types.test-d.ts

Repository: prisma/prisma-next

Length of output: 1776


🏁 Script executed:

rg -B5 -A10 "TraitExpression.*readonly.*\[" packages/3-targets/6-adapters/postgres/src/types/operation-types.ts | head -30

Repository: prisma/prisma-next

Length of output: 382


🏁 Script executed:

rg -B5 "readonly traits.*readonly \[" packages/2-sql/4-lanes/relational-core/src/ast/codec-types.ts -A3

Repository: prisma/prisma-next

Length of output: 44


🏁 Script executed:

sed -n '240,250p' packages/2-sql/4-lanes/relational-core/src/ast/codec-types.ts

Repository: prisma/prisma-next

Length of output: 452


🏁 Script executed:

rg "type CodecTraits" packages/2-sql/4-lanes/relational-core/src/ast/codec-types.ts -A5

Repository: prisma/prisma-next

Length of output: 241


🏁 Script executed:

rg "type CodecTraits" packages/2-sql/4-lanes/relational-core/src/ast/codec-types.ts -B2 -A8

Repository: prisma/prisma-next

Length of output: 490


🏁 Script executed:

rg "CodecTypes\[" packages/2-sql/4-lanes/relational-core/test/ -B2 -A2 | head -40

Repository: prisma/prisma-next

Length of output: 44


🏁 Script executed:

rg "CodecIdsWithTrait" packages/ test/ -B2 -A2

Repository: prisma/prisma-next

Length of output: 1394


Fix trait matching in CodecIdsWithTrait to extract array element types.

Line 25 compares required trait literals to the inferred traits type. When codec traits are defined as arrays (e.g., traits: ['equality', 'order']), the inferred type T is the tuple itself, not the element union. The check [RequiredTraits[number]] extends [T] then fails because it compares a string literal against a tuple type instead of the tuple's elements.

The fix extracts the element type using [number] indexing and checks for subset membership:

Proposed fix
 type CodecIdsWithTrait<
   CT extends Record<string, { readonly input: unknown }>,
   RequiredTraits extends readonly string[],
 > = {
-  [K in keyof CT & string]: CT[K] extends { readonly traits: infer T }
-    ? [RequiredTraits[number]] extends [T]
+  [K in keyof CT & string]: CT[K] extends { readonly traits: readonly string[] }
+    ? Exclude<RequiredTraits[number], CT[K]['traits'][number]> extends never
       ? K
       : never
     : never;
 }[keyof CT & string];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/4-lanes/relational-core/src/expression.ts` around lines 20 -
29, The conditional in the CodecIdsWithTrait type is comparing
RequiredTraits[number] to the inferred traits tuple T itself, causing mismatches
when traits are arrays/tuples; change the check to compare against the element
union of the tuple by using T[number] (keep the tuple-wrapping trick to avoid
distributive conditionals). Concretely, in CodecIdsWithTrait update the
conditional from checking [RequiredTraits[number]] extends [T] to
[RequiredTraits[number]] extends [T[number]] so the type extracts the trait
element types correctly (refer to CodecIdsWithTrait, CT, RequiredTraits, and the
inferred T).

Comment thread packages/2-sql/4-lanes/relational-core/src/expression.ts
Comment on lines +23 to +30
impl: (
self: CodecExpression<'pg/vector@1', boolean, CT>,
other: CodecExpression<'pg/vector@1', boolean, CT>,
): Expression<{ codecId: 'pg/float8@1'; nullable: false }> =>
buildOperation({
method: 'cosineDistance',
args: [toExpr(self, pgvectorTypeId), toExpr(other, pgvectorTypeId)],
returns: { codecId: 'pg/float8@1', nullable: false },
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 23, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Align operand nullability with the advertised return type.

These impls accept nullable vector expressions via boolean, but always return nullable: false. Either restrict operands to non-null expressions (false) or propagate operand nullability into the returned Expression metadata; otherwise nullable vector columns are typed as producing non-null distances.

Possible narrow fix if nullable operands are not supported
       impl: (
-        self: CodecExpression<'pg/vector@1', boolean, CT>,
-        other: CodecExpression<'pg/vector@1', boolean, CT>,
+        self: CodecExpression<'pg/vector@1', false, CT>,
+        other: CodecExpression<'pg/vector@1', false, CT>,
       ): Expression<{ codecId: 'pg/float8@1'; nullable: false }> =>

Apply the same change to cosineSimilarity.

Also applies to: 41-48

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-extensions/pgvector/src/core/descriptor-meta.ts` around lines 23 -
30, The impls for cosineDistance and cosineSimilarity declare operands as
CodecExpression<'pg/vector@1', boolean, CT> but always return Expression with
returns.nullable: false, causing nullable vector inputs to be incorrectly typed
as non-null; fix by either changing the operand nullability parameter from
boolean to false (i.e., CodecExpression<'pg/vector@1', false, CT>) to forbid
nullable operands, or propagate operand nullability into the return metadata
(compute returns.nullable = self.nullable || other.nullable) by reading the
boolean nullability of the incoming CodecExpression and using that value when
building the return Expression in the buildOperation call (update the impls for
both cosineDistance and cosineSimilarity and ensure toExpr usage remains
correct).

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.

Pre-existing issue, out of scope for this PR

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@SevInf, understood — I'll drop this for the current PR. Would you like me to open a follow-up issue to track the operand nullability alignment for cosineDistance and cosineSimilarity so it isn't lost?


🧠 Learnings used
Learnt from: wmadden
Repo: prisma/prisma-next PR: 38
File: packages/2-sql/3-tooling/family/src/core/migrations/plan-helpers.ts:40-47
Timestamp: 2025-12-26T23:02:40.679Z
Learning: In Prisma-next's TypeScript codebase, prefer using well-established utility libraries (e.g., Lodash) rather than building custom utilities for complex object operations such as deep freezing or deep cloning. Favor reliable, battle-tested functions and ensure dependencies are reviewed for tree-shaking and bundle size. If a lightweight utility suffices, consider modern native approaches or smaller libs.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 191
File: packages/1-framework/3-tooling/cli/src/commands/contract-emit.ts:5-5
Timestamp: 2026-03-01T13:54:21.863Z
Learning: In the Prisma-next repository, prefer importing and using 'pathe' over the built-in 'node:path' module for path operations across the codebase (including CLI commands and tooling files). Apply this in TypeScript files, replacing imports from 'node:path' with 'pathe' and adjusting API usage accordingly. Ensure consistent usage across all modules and update any affected tests or tooling imports when refactoring.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 234
File: packages/2-sql/4-lanes/sql-lane/test/rich-mutation.test.ts:2-2
Timestamp: 2026-03-23T10:01:15.075Z
Learning: In the prisma/prisma-next repo, prefer using `pathe` over Node’s built-in `node:path`. For any TypeScript file (including tests, test helpers, and integration tooling) replace imports like `import { dirname, join } from 'node:path'` with `import { dirname, join } from 'pathe'`—especially when code reads files/fixtures from disk.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 294
File: packages/2-mongo-family/2-query/query-ast/src/read-plan.ts:4-10
Timestamp: 2026-04-04T10:08:34.220Z
Learning: In prisma/prisma-next, don’t flag declaration-emit/typecheck warnings/errors when a file uses an intentional nominal/branding pattern with a non-exported `declare const <brand>: unique symbol` as a computed property key on an exported interface (e.g., `readonly [__mongoReadPlanRow]: ...` / `readonly [aggregateResultBrand]: ...`). The `unique symbol` must remain unexported to prevent external forging, while the branded property key staying on the exported interface provides compile-time discrimination. If the build correctly emits declaration files for this pattern, treat it as intentional rather than a declaration-emit error.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 293
File: packages/1-framework/1-core/shared/contract/src/canonicalization.ts:203-216
Timestamp: 2026-04-04T12:19:05.250Z
Learning: In the prisma/prisma-next repository, do not treat `Array.prototype.sort` comparators as “potentially non-deterministic” solely because the comparator does not include explicit tie-breaking keys. This is because the repo’s required runtime is Node >= 24, where `Array.prototype.sort` is stable (equal elements retain their original relative order via a stable sort such as TimSort). Therefore, equal-comparing elements should remain deterministic without additional tie-break logic.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 334
File: packages/2-mongo-family/9-family/src/core/control-instance.ts:204-228
Timestamp: 2026-04-13T15:54:52.337Z
Learning: When reviewing TypeScript code in this repo, do not assume a property is potentially `undefined` at a call site just because an arktype schema definition uses the DSL syntax `'key?': 'type'` (a quoted string key with a trailing `?`). This DSL notation is not the same as TypeScript optional properties (`key?: type`). To determine whether `key` is truly optional/undefined-capable, verify the actual exported TypeScript type declaration (e.g., `contract-types.ts`) and the runtime validation schema (e.g., `validate-contract.ts`) for that property; only then should the review flag call-site handling for potential `undefined`.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 339
File: test/integration/test/cli.init-templates.e2e.test.ts:96-96
Timestamp: 2026-04-15T19:33:48.733Z
Learning: In prisma/prisma-next, when writing tests that run TypeScript compilation (e.g., via `tsc --noEmit`), avoid hardcoded timeout numbers (such as `30_000`). Instead, use `prisma-next/test-utils` timeout helpers—specifically `timeouts.typeScriptCompilation` (implemented in `test/utils/src/timeouts.ts`) for `tsc` compilation scenarios. Use `timeouts.spinUpPpgDev` for PostgreSQL server startup and `timeouts.databaseOperation` for database operation scenarios.

Comment on lines +30 to +40
readonly impl: (
self: CodecExpression<'pg/vector@1', boolean, CT>,
other: CodecExpression<'pg/vector@1', boolean, CT>,
) => Expression<{ codecId: 'pg/float8@1'; nullable: false }>;
};
readonly cosineSimilarity: {
readonly self: { readonly codecId: 'pg/vector@1' };
readonly impl: (
self: CodecExpression<'pg/vector@1', boolean, CT>,
other: CodecExpression<'pg/vector@1', boolean, CT>,
) => Expression<{ codecId: 'pg/float8@1'; nullable: false }>;
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 23, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Mirror the pgvector nullability contract in the exported types.

The public signatures accept nullable vector expressions (boolean) but return a non-null float expression. Keep this in sync with the descriptor fix: either make operands non-null (false) or return an expression whose nullable metadata reflects nullable operands.

Possible narrow fix if nullable operands are not supported
       readonly impl: (
-        self: CodecExpression<'pg/vector@1', boolean, CT>,
-        other: CodecExpression<'pg/vector@1', boolean, CT>,
+        self: CodecExpression<'pg/vector@1', false, CT>,
+        other: CodecExpression<'pg/vector@1', false, CT>,
       ) => Expression<{ codecId: 'pg/float8@1'; nullable: false }>;

Apply the same change to cosineSimilarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-extensions/pgvector/src/types/operation-types.ts` around lines 30
- 40, The exported operation types currently accept nullable vector operands
(CodecExpression<'pg/vector@1', boolean, CT>) but return a non-null float
result; mirror the pgvector nullability contract by making the operand
nullability explicit—change the operand types in the impl signature(s) to
non-null (CodecExpression<'pg/vector@1', false, CT>) or alternatively make the
returned Expression nullable when operands can be nullable; specifically update
the impl signatures for cosineSimilarity (and any sibling impl shown) to use
false for the operand nullability so the public types match the descriptor
contract.

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.

Pre-existing issue, out of scope

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@SevInf, understood — I'll drop this concern for the current PR. Would you like me to open a follow-up issue to track the nullability alignment for the pgvector operation types?


🧠 Learnings used
Learnt from: wmadden
Repo: prisma/prisma-next PR: 38
File: packages/2-sql/3-tooling/family/src/core/migrations/plan-helpers.ts:40-47
Timestamp: 2025-12-26T23:02:40.679Z
Learning: In Prisma-next's TypeScript codebase, prefer using well-established utility libraries (e.g., Lodash) rather than building custom utilities for complex object operations such as deep freezing or deep cloning. Favor reliable, battle-tested functions and ensure dependencies are reviewed for tree-shaking and bundle size. If a lightweight utility suffices, consider modern native approaches or smaller libs.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 191
File: packages/1-framework/3-tooling/cli/src/commands/contract-emit.ts:5-5
Timestamp: 2026-03-01T13:54:21.863Z
Learning: In the Prisma-next repository, prefer importing and using 'pathe' over the built-in 'node:path' module for path operations across the codebase (including CLI commands and tooling files). Apply this in TypeScript files, replacing imports from 'node:path' with 'pathe' and adjusting API usage accordingly. Ensure consistent usage across all modules and update any affected tests or tooling imports when refactoring.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 234
File: packages/2-sql/4-lanes/sql-lane/test/rich-mutation.test.ts:2-2
Timestamp: 2026-03-23T10:01:15.075Z
Learning: In the prisma/prisma-next repo, prefer using `pathe` over Node’s built-in `node:path`. For any TypeScript file (including tests, test helpers, and integration tooling) replace imports like `import { dirname, join } from 'node:path'` with `import { dirname, join } from 'pathe'`—especially when code reads files/fixtures from disk.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 294
File: packages/2-mongo-family/2-query/query-ast/src/read-plan.ts:4-10
Timestamp: 2026-04-04T10:08:34.220Z
Learning: In prisma/prisma-next, don’t flag declaration-emit/typecheck warnings/errors when a file uses an intentional nominal/branding pattern with a non-exported `declare const <brand>: unique symbol` as a computed property key on an exported interface (e.g., `readonly [__mongoReadPlanRow]: ...` / `readonly [aggregateResultBrand]: ...`). The `unique symbol` must remain unexported to prevent external forging, while the branded property key staying on the exported interface provides compile-time discrimination. If the build correctly emits declaration files for this pattern, treat it as intentional rather than a declaration-emit error.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 293
File: packages/1-framework/1-core/shared/contract/src/canonicalization.ts:203-216
Timestamp: 2026-04-04T12:19:05.250Z
Learning: In the prisma/prisma-next repository, do not treat `Array.prototype.sort` comparators as “potentially non-deterministic” solely because the comparator does not include explicit tie-breaking keys. This is because the repo’s required runtime is Node >= 24, where `Array.prototype.sort` is stable (equal elements retain their original relative order via a stable sort such as TimSort). Therefore, equal-comparing elements should remain deterministic without additional tie-break logic.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 334
File: packages/2-mongo-family/9-family/src/core/control-instance.ts:204-228
Timestamp: 2026-04-13T15:54:52.337Z
Learning: When reviewing TypeScript code in this repo, do not assume a property is potentially `undefined` at a call site just because an arktype schema definition uses the DSL syntax `'key?': 'type'` (a quoted string key with a trailing `?`). This DSL notation is not the same as TypeScript optional properties (`key?: type`). To determine whether `key` is truly optional/undefined-capable, verify the actual exported TypeScript type declaration (e.g., `contract-types.ts`) and the runtime validation schema (e.g., `validate-contract.ts`) for that property; only then should the review flag call-site handling for potential `undefined`.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 339
File: test/integration/test/cli.init-templates.e2e.test.ts:96-96
Timestamp: 2026-04-15T19:33:48.733Z
Learning: In prisma/prisma-next, when writing tests that run TypeScript compilation (e.g., via `tsc --noEmit`), avoid hardcoded timeout numbers (such as `30_000`). Instead, use `prisma-next/test-utils` timeout helpers—specifically `timeouts.typeScriptCompilation` (implemented in `test/utils/src/timeouts.ts`) for `tsc` compilation scenarios. Use `timeouts.spinUpPpgDev` for PostgreSQL server startup and `timeouts.databaseOperation` for database operation scenarios.

Comment on lines 4 to 8
export const mongoVectorNearOperation = Object.freeze({
method: 'near',
args: [{ codecId: MONGO_VECTOR_CODEC_ID, nullable: false }],
returns: { codecId: MONGO_INT32_CODEC_ID, nullable: false },
self: { codecId: MONGO_VECTOR_CODEC_ID },
impl: () => undefined as never,
}) satisfies OperationDescriptor;
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 23, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Placeholder impl will silently return undefined if ever dispatched.

impl: () => undefined as never satisfies the type but has no runtime guard. If anything in the query pipeline ever calls this impl (directly or via the registry) before Mongo wires up near, it will produce undefined and propagate through AST construction / buildOperation paths with a very confusing downstream failure.

Prefer a loud failure so the gap is obvious:

Proposed fix
-  impl: () => undefined as never,
+  impl: () => {
+    throw new Error('mongoVectorNearOperation: not yet implemented for the Mongo adapter');
+  },

This also lets you drop the as never cast (see repo guideline "Minimize type casts"). The placeholder test in codecs.test.ts would need a corresponding toThrow update.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-mongo-target/2-mongo-adapter/src/core/operations.ts` around lines
4 - 8, The placeholder impl on mongoVectorNearOperation currently returns
undefined and can silently propagate errors; change the impl (on
mongoVectorNearOperation) to immediately throw a clear runtime Error (e.g.,
"mongo vector 'near' operation not implemented/registered") instead of returning
undefined so any premature dispatch fails loudly, remove the unnecessary "as
never" cast, and update the placeholder test in codecs.test.ts to expect/verify
that calling the impl throws.

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.

It will never be dispatched before getting replaced with an actual operation

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@SevInf, understood — thanks for the clarification. If the registry/dispatch lifecycle guarantees the placeholder is always replaced with a concrete implementation before any call site can invoke it, the undefined as never stub is safe as a structural placeholder to satisfy OperationDescriptor.


✏️ Learnings added
Learnt from: SevInf
Repo: prisma/prisma-next PR: 374
File: packages/3-mongo-target/2-mongo-adapter/src/core/operations.ts:4-8
Timestamp: 2026-04-23T17:08:52.146Z
Learning: In prisma/prisma-next, the Mongo adapter's `mongoVectorNearOperation` uses `impl: () => undefined as never` as an intentional structural placeholder to satisfy the `OperationDescriptor` type shape at registration time. This placeholder is guaranteed to be replaced with a real implementation before any dispatch occurs, so it will never actually be called. Do not flag this as a missing runtime guard or suggest converting it to a throwing stub.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: wmadden
Repo: prisma/prisma-next PR: 38
File: packages/2-sql/3-tooling/family/src/core/migrations/plan-helpers.ts:40-47
Timestamp: 2025-12-26T23:02:40.679Z
Learning: In Prisma-next's TypeScript codebase, prefer using well-established utility libraries (e.g., Lodash) rather than building custom utilities for complex object operations such as deep freezing or deep cloning. Favor reliable, battle-tested functions and ensure dependencies are reviewed for tree-shaking and bundle size. If a lightweight utility suffices, consider modern native approaches or smaller libs.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 191
File: packages/1-framework/3-tooling/cli/src/commands/contract-emit.ts:5-5
Timestamp: 2026-03-01T13:54:21.863Z
Learning: In the Prisma-next repository, prefer importing and using 'pathe' over the built-in 'node:path' module for path operations across the codebase (including CLI commands and tooling files). Apply this in TypeScript files, replacing imports from 'node:path' with 'pathe' and adjusting API usage accordingly. Ensure consistent usage across all modules and update any affected tests or tooling imports when refactoring.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 234
File: packages/2-sql/4-lanes/sql-lane/test/rich-mutation.test.ts:2-2
Timestamp: 2026-03-23T10:01:15.075Z
Learning: In the prisma/prisma-next repo, prefer using `pathe` over Node’s built-in `node:path`. For any TypeScript file (including tests, test helpers, and integration tooling) replace imports like `import { dirname, join } from 'node:path'` with `import { dirname, join } from 'pathe'`—especially when code reads files/fixtures from disk.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 294
File: packages/2-mongo-family/2-query/query-ast/src/read-plan.ts:4-10
Timestamp: 2026-04-04T10:08:34.220Z
Learning: In prisma/prisma-next, don’t flag declaration-emit/typecheck warnings/errors when a file uses an intentional nominal/branding pattern with a non-exported `declare const <brand>: unique symbol` as a computed property key on an exported interface (e.g., `readonly [__mongoReadPlanRow]: ...` / `readonly [aggregateResultBrand]: ...`). The `unique symbol` must remain unexported to prevent external forging, while the branded property key staying on the exported interface provides compile-time discrimination. If the build correctly emits declaration files for this pattern, treat it as intentional rather than a declaration-emit error.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 293
File: packages/1-framework/1-core/shared/contract/src/canonicalization.ts:203-216
Timestamp: 2026-04-04T12:19:05.250Z
Learning: In the prisma/prisma-next repository, do not treat `Array.prototype.sort` comparators as “potentially non-deterministic” solely because the comparator does not include explicit tie-breaking keys. This is because the repo’s required runtime is Node >= 24, where `Array.prototype.sort` is stable (equal elements retain their original relative order via a stable sort such as TimSort). Therefore, equal-comparing elements should remain deterministic without additional tie-break logic.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 334
File: packages/2-mongo-family/9-family/src/core/control-instance.ts:204-228
Timestamp: 2026-04-13T15:54:52.337Z
Learning: When reviewing TypeScript code in this repo, do not assume a property is potentially `undefined` at a call site just because an arktype schema definition uses the DSL syntax `'key?': 'type'` (a quoted string key with a trailing `?`). This DSL notation is not the same as TypeScript optional properties (`key?: type`). To determine whether `key` is truly optional/undefined-capable, verify the actual exported TypeScript type declaration (e.g., `contract-types.ts`) and the runtime validation schema (e.g., `validate-contract.ts`) for that property; only then should the review flag call-site handling for potential `undefined`.

Learnt from: wmadden
Repo: prisma/prisma-next PR: 339
File: test/integration/test/cli.init-templates.e2e.test.ts:96-96
Timestamp: 2026-04-15T19:33:48.733Z
Learning: In prisma/prisma-next, when writing tests that run TypeScript compilation (e.g., via `tsc --noEmit`), avoid hardcoded timeout numbers (such as `30_000`). Instead, use `prisma-next/test-utils` timeout helpers—specifically `timeouts.typeScriptCompilation` (implemented in `test/utils/src/timeouts.ts`) for `tsc` compilation scenarios. Use `timeouts.spinUpPpgDev` for PostgreSQL server startup and `timeouts.databaseOperation` for database operation scenarios.

Comment on lines +147 to +155
impl: (
self: TraitExpression<readonly ['textual'], false, CT>,
pattern: CodecExpression<'pg/text@1', false, CT>,
): Expression<{ codecId: 'pg/bool@1'; nullable: false }> =>
buildOperation({
method: 'ilike',
args: [toExpr(self), toExpr(pattern, PG_TEXT_CODEC_ID)],
returns: { codecId: PG_BOOL_CODEC_ID, nullable: false },
lowering: { targetFamily: 'sql', strategy: 'infix', template: '{{self}} ILIKE {{arg0}}' },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the Postgres ilike operation type surface matches the descriptor signature.
# Expect: both descriptor-meta.ts and any operation-types.ts ilike signatures use TraitExpression
# for non-raw textual expression operands, not only CodecExpression<'pg/text@1'>.
rg -n -C4 --type=ts "ilike|CodecExpression<'pg/text@1'|TraitExpression<readonly \\['textual'\\]" packages/3-targets/6-adapters/postgres

Repository: prisma/prisma-next

Length of output: 3177


Change ilike pattern parameter to accept any textual codec.

The pattern parameter is narrowed to CodecExpression<'pg/text@1', false, CT> in both operation-types.ts (line 17) and descriptor-meta.ts (line 149), while self is trait-typed as TraitExpression<readonly ['textual'], false, CT>. This inconsistency blocks cross-textual-codec composition (e.g., varchar ILIKE varchar).

Update both signatures to use TraitExpression<readonly ['textual'], false, CT> for the pattern parameter. The toExpr(pattern, PG_TEXT_CODEC_ID) call will continue to normalize the pattern at the semantic level.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-targets/6-adapters/postgres/src/core/descriptor-meta.ts` around
lines 147 - 155, The ilike operation's pattern parameter is incorrectly typed as
CodecExpression<'pg/text@1', false, CT> which prevents cross-textual-codec use;
change the pattern parameter type in both the ilike signatures (the one in
operation-types and the impl in descriptor-meta for ilike) to
TraitExpression<readonly ['textual'], false, CT> so it matches self; keep the
existing toExpr(pattern, PG_TEXT_CODEC_ID) normalization call and
returns/lowering unchanged (symbols to update: ilike, TraitExpression,
CodecExpression, toExpr, PG_TEXT_CODEC_ID).

Copy link
Copy Markdown
Contributor

@wmadden wmadden left a comment

Choose a reason for hiding this comment

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

Review (comment, not blocking automation)

One substantive concern.

The change is structurally complete for SQL but not for Mongo, and the framework permits this asymmetry. SQL ships the full new authoring model — SqlOperationEntry tightening, QueryOperations<CT>() factory shape, QueryOperationTypes<CT> type-only mirror, contract-emitter intersection, accessor dispatch in createExtensionMethodFactory. Mongo ships one entry (mongoVectorNearOperation) using the framework's OperationDescriptor type and nothing else: no Mongo-shaped entry tightening, no factory, no type-only mirror, no Mongo emitter support, no Mongo accessor dispatch site. The descriptor is exported and consumed only by test/codecs.test.ts.

SQL and Mongo can't dispatch into each other (separate contracts, builders, lowering paths), so this is not a runtime hazard. It is a framework-level structural problem: @prisma-next/operations defines the descriptor and a self-validating registry, but every higher-level concern (return tightening, factory shape, type mirror, emitter integration, accessor dispatch) was built ad-hoc inside the SQL packages. The framework neither provides those primitives nor structurally requires that targets supply them. SQL built it well; Mongo skipped it; the type system noticed nothing. The same drift will be available to the next target.

Preferred resolution. Lift the cross-cutting infrastructure into @prisma-next/operations as a target-parametric contract — OperationTarget<Return, ContractCT>, with OperationEntryFor<T>, OperationFactoryFor<T>, OperationTypeMirrorFor<T>, plus framework-provided primitives for registry validation, contract-emitter intersection, and accessor-dispatch resolution that are generic over OperationTarget. SQL becomes SqlOperationTarget extends OperationTarget<QueryOperationReturn, SqlCodecTypesBase>. Mongo becomes MongoOperationTarget extends OperationTarget<MongoOperationReturn, MongoCodecTypesBase>. Adding a target then means instantiating the contract; the framework refuses to wire up a half-built one. Most of D1–D6 stays where it is; the cross-cutting infrastructure moves up a layer. This pre-empts the next drift, not just this one.

Acceptable fallbacks for this PR.

  1. Apply the SQL-shaped infrastructure to Mongo by hand here.
  2. Cut the Mongo stub from this PR (delete mongoVectorNearOperation, mongoVectorOperationDescriptors, the export, and the corresponding codecs.test.ts cases) and add a "Future work — Mongo" section to ADR 204 explicitly scoping the revamp to SQL with Mongo named as the next target.

As shipped, the Mongo entry asserts more than it delivers. SQL-side work is solid and ready otherwise.

args: [{ codecId: MONGO_VECTOR_CODEC_ID, nullable: false }],
returns: { codecId: MONGO_INT32_CODEC_ID, nullable: false },
self: { codecId: MONGO_VECTOR_CODEC_ID },
impl: () => undefined as never,
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.

This file is the symptom of the framework-level structural issue called out in the top-level review.

The SQL side of the PR ships a full operation-authoring system (entry tightening, factory shape, type-only mirror, contract-emitter intersection, accessor dispatch). The Mongo side ships this — an OperationDescriptor re-using the framework's type, a placeholder impl: () => undefined as never, and a single-element descriptors array — exported from src/exports/index.ts and consumed only by test/codecs.test.ts. There is no Mongo QueryOperations<CT>() analogue, no type-only mirror, no Mongo contract-emitter integration, no Mongo accessor dispatch site. Nothing in the codebase actually uses this entry.

The framework permits this because @prisma-next/operations defines OperationDescriptor + a self-validating registry and stops there. Everything else SQL needed was built inside sql-operations / sql-emitter / sql-orm-client ad-hoc.

Preferred fix (in the top-level review): introduce a target-parametric OperationTarget<Return, ContractCT> contract in @prisma-next/operations with framework-provided primitives that SQL and Mongo both instantiate.

Fallback for this PR: either build the SQL-shaped infrastructure for Mongo by hand, or delete this file (and mongoVectorOperationDescriptors, the export, and the test cases) and explicitly scope ADR 204 to SQL with Mongo as a named follow-up. Shipping as-is asserts a Mongo migration that hasn't actually happened.

Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (1)
docs/architecture docs/adrs/ADR 206 - Operations as TypeScript functions.md (1)

13-35: Trim implementation mechanics from the ADR.

This section goes beyond the architectural decision and into step-by-step runtime/type-matching behavior. Keep the ADR at the decision/constraint level, and move the toExpr / buildOperation / registry-walk details to subsystem docs or implementation notes.

As per coding guidelines, ADRs should document architectural decisions and key constraints only, not implementation algorithms, emitter derivation logic, or step-by-step derivation/migration rules.

♻️ Suggested rewrite direction
-Operations are authored as TypeScript functions. The function's signature is the type-level surface. The function's body is the runtime — it receives user arguments ..., wraps them into parameter references ..., and returns an AST expression node ...
+Operations are authored as TypeScript functions, with the signature defining the type-level surface and the descriptor carrying only minimal dispatch metadata.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/architecture` docs/adrs/ADR 206 - Operations as TypeScript functions.md
around lines 13 - 35, The ADR currently mixes architectural decisions with
low-level implementation mechanics; remove or relocate the procedural details
about toExpr, buildOperation, SqlOperationDescriptor internals, registry-walk
behavior, OperationExpr shape, and factory wiring
(QueryOperationTypes/ModelAccessor indexing) from the ADR body into
implementation/subsystem docs. Keep only the decision-level statements: that
operations are authored as TypeScript functions, the descriptor carries minimal
dispatch metadata (method/self/impl), the codec-types map is bound at
factory-call time, and that predicate vs non-predicate is determined by the
return codec's boolean trait; move the step-by-step wrapping, AST construction,
descriptor indexing, and runtime/type-matching algorithms into a new
implementation note or subsystem document.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/1-framework/1-core/operations/test/operations-registry.test.ts`:
- Around line 58-65: Remove the compile-time suppression and instead cast the
invalid test payload at the call boundary so the runtime test still passes
TypeScript checks; replace the `// `@ts-expect-error`` usage before the
registry.register(...) call with an explicit narrow cast like `...register({
method: 'bad', self: {} } as unknown as any)` (or `as unknown as
SelfSpec`/`Partial<SelfSpec>` as appropriate) so the code compiles while
exercising the runtime validation in register; apply the same pattern to the
other negative case that checks the `codecId + traits` combination (the second
failing register call around lines 83-90).

---

Nitpick comments:
In `@docs/architecture` docs/adrs/ADR 206 - Operations as TypeScript functions.md:
- Around line 13-35: The ADR currently mixes architectural decisions with
low-level implementation mechanics; remove or relocate the procedural details
about toExpr, buildOperation, SqlOperationDescriptor internals, registry-walk
behavior, OperationExpr shape, and factory wiring
(QueryOperationTypes/ModelAccessor indexing) from the ADR body into
implementation/subsystem docs. Keep only the decision-level statements: that
operations are authored as TypeScript functions, the descriptor carries minimal
dispatch metadata (method/self/impl), the codec-types map is bound at
factory-call time, and that predicate vs non-predicate is determined by the
return codec's boolean trait; move the step-by-step wrapping, AST construction,
descriptor indexing, and runtime/type-matching algorithms into a new
implementation note or subsystem document.
🪄 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: 2969b48b-ae8e-460a-86cd-f89b1a6f7645

📥 Commits

Reviewing files that changed from the base of the PR and between 05ae897 and b93ba3b.

📒 Files selected for processing (5)
  • docs/architecture docs/ADR-INDEX.md
  • docs/architecture docs/adrs/ADR 206 - Operations as TypeScript functions.md
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • packages/1-framework/1-core/operations/src/index.ts
  • packages/1-framework/1-core/operations/test/operations-registry.test.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/architecture docs/ADR-INDEX.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/1-framework/1-core/operations/src/index.ts

Comment on lines 58 to +65
expect(() =>
registry.register(
descriptor('bad', {
args: [{ nullable: false }],
}),
),
).toThrow('Operation "bad" arg[0] has neither codecId nor traits');
registry.register({
method: 'bad',
// @ts-expect-error — SelfSpec requires codecId or traits
self: {},
impl: noopImpl,
}),
).toThrow('Operation "bad" self has neither codecId nor traits');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid @ts-expect-error in this runtime test.

These cases are validating runtime behavior, so the compile-time suppression is out of place here. Use a narrow cast at the call boundary instead, or move the invalid-shape checks into a dedicated negative type test.

♻️ Suggested adjustment
       registry.register({
         method: 'bad',
-        // `@ts-expect-error` — SelfSpec requires codecId or traits
-        self: {},
+        self: {} as unknown as OperationEntry['self'],
         impl: noopImpl,
       }),

Apply the same pattern to the codecId + traits case as well.

Also applies to: 83-90

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/1-core/operations/test/operations-registry.test.ts`
around lines 58 - 65, Remove the compile-time suppression and instead cast the
invalid test payload at the call boundary so the runtime test still passes
TypeScript checks; replace the `// `@ts-expect-error`` usage before the
registry.register(...) call with an explicit narrow cast like `...register({
method: 'bad', self: {} } as unknown as any)` (or `as unknown as
SelfSpec`/`Partial<SelfSpec>` as appropriate) so the code compiles while
exercising the runtime validation in register; apply the same pattern to the
other negative case that checks the `codecId + traits` combination (the second
failing register call around lines 83-90).

@SevInf SevInf enabled auto-merge (squash) April 29, 2026 10:20
SevInf added 4 commits April 29, 2026 12:21
Replaces the declarative `{args, returns, lowering}` operation record with a
TypeScript function `impl` paired with a minimal `self` dispatch hint. The
function's signature is the type-level surface; the function's body builds
the AST expression node via `toExpr` + `buildOperation`. Adapters and
extensions ship operation contributions as CT-generic factories whose impls
use `CodecExpression<CodecId, N, CT>` or `TraitExpression<Traits, N, CT>`
for argument types, letting authors use TypeScript's full expressivity
(generics, conditional returns, non-DB argument types).

Column handles on the ORM model accessor now implement `Expression<T>`
directly — carrying `returnType` and `buildAst()` on a single plain-object
literal — unlocking cross-column composition like
`db.a.embedding.cosineDistance(db.b.embedding)`. The old
`new ExpressionImpl(column, { codecId: '', nullable: false })` hack in the
extension-method factory is gone; `numericAgg`'s downcast
`(expr as ExpressionImpl).field.codecId` reads `expr.returnType.codecId`
off the Expression interface.

The ORM Proxy returns `undefined` for unknown fields (plain JS object
semantics). The relation-shorthand iterator throws explicitly on unknown
predicate keys so typos surface loudly instead of silently filtering
nothing.

Ported ops: `postgres/ilike`, `pgvector/cosineDistance`,
`pgvector/cosineSimilarity`, and `mongo-adapter/near`.

ADR 204 documents the decision.

Breaking change for extensions that shipped declarative operation records —
rewrite to a CT-generic factory returning `{method, self?, impl}`.
Replaces biome-ignore + `as any` casts in the two negative tests with
`@ts-expect-error`. The assertion under test is specifically that
`SelfSpec` rejects these shapes at the type level — @ts-expect-error
makes that a compile-time check instead of a runtime-only check.
Add unit coverage for the new toExpr / buildOperation helpers in
relational-core and invoke the mongoVectorNearOperation placeholder
impl so both files reach their coverage thresholds.
@SevInf SevInf force-pushed the worktree/op-registry-ts branch from 46e7d79 to afef53b Compare April 29, 2026 10:21
@SevInf SevInf disabled auto-merge April 29, 2026 10:33
@SevInf SevInf merged commit 7f7f64a into main Apr 29, 2026
16 of 17 checks passed
@SevInf SevInf deleted the worktree/op-registry-ts branch April 29, 2026 10:34
wmadden pushed a commit that referenced this pull request May 4, 2026
closes
[TML-2307](https://linear.app/prisma-company/issue/TML-2307/author-sql-operations-as-typescript-functions)

## Intent

Replace the closed declarative operation spec (`{args, returns,
lowering}`) with a TypeScript function that *is* the operation: its
signature is the type-level surface and its body builds the AST. A tiny
`SqlOperationDescriptor` — `{method, self?, impl}` — carries only what
the framework actually needs to dispatch, so authors get TypeScript's
full expressivity (generics, conditional returns, non-DB argument types,
overloads) on the authoring surface and the single surface cannot drift
out of step with itself. ADR 204 records the decision.

## Change map

- **Primitive** — `Expression<T>`, `CodecExpression`, `TraitExpression`,
`toExpr`, `buildOperation` in relational-core:
- [packages/2-sql/4-lanes/relational-core/src/expression.ts
(L1–L117)](packages/2-sql/4-lanes/relational-core/src/expression.ts)
- **Registry reshape** — descriptor is now `{method, self?, impl}`;
registry validates only the `self` hint:
- [packages/1-framework/1-core/operations/src/index.ts
(L1–L60)](packages/1-framework/1-core/operations/src/index.ts)
- [packages/2-sql/1-core/operations/src/index.ts
(L1–L30)](packages/2-sql/1-core/operations/src/index.ts)
- [packages/2-sql/1-core/contract/src/types.ts
(L167–L200)](packages/2-sql/1-core/contract/src/types.ts)
- **SQL builder `fns`** — flows authored `impl`s through unchanged (no
wrapper); `numericAgg` reads `expr.returnType.codecId` structurally:
- [packages/2-sql/4-lanes/sql-builder/src/expression.ts
(L1–L125)](packages/2-sql/4-lanes/sql-builder/src/expression.ts)
- [packages/2-sql/4-lanes/sql-builder/src/runtime/functions.ts
(L1–L170)](packages/2-sql/4-lanes/sql-builder/src/runtime/functions.ts)
- [packages/2-sql/4-lanes/sql-builder/src/runtime/expression-impl.ts
(L1–L25)](packages/2-sql/4-lanes/sql-builder/src/runtime/expression-impl.ts)
- **ORM column handles implement `Expression<T>`** — unlocks
cross-column composition; Proxy returns `undefined` for unknown fields;
shorthand predicate iterator throws on unknown keys:
- [packages/3-extensions/sql-orm-client/src/model-accessor.ts
(L60–L200)](packages/3-extensions/sql-orm-client/src/model-accessor.ts)
- [packages/3-extensions/sql-orm-client/src/types.ts
(L200–L420)](packages/3-extensions/sql-orm-client/src/types.ts)
- **CT-generic factories** — adapters/extensions expose contributions as
`QueryOperations<CT>()`:
- [packages/3-targets/6-adapters/postgres/src/core/descriptor-meta.ts
(L130–L160)](packages/3-targets/6-adapters/postgres/src/core/descriptor-meta.ts)
- [packages/3-targets/6-adapters/postgres/src/types/operation-types.ts
(L1–L22)](packages/3-targets/6-adapters/postgres/src/types/operation-types.ts)
- [packages/3-extensions/pgvector/src/core/descriptor-meta.ts
(L10–L58)](packages/3-extensions/pgvector/src/core/descriptor-meta.ts)
- [packages/3-extensions/pgvector/src/types/operation-types.ts
(L1–L45)](packages/3-extensions/pgvector/src/types/operation-types.ts)
- [packages/3-mongo-target/2-mongo-adapter/src/core/operations.ts
(L1–L12)](packages/3-mongo-target/2-mongo-adapter/src/core/operations.ts)
- **Emitter** — emits a per-import intersection of
`QueryOperationTypes<CodecTypes>`:
- [packages/2-sql/3-tooling/emitter/src/index.ts
(L305–L320)](packages/2-sql/3-tooling/emitter/src/index.ts)
- **ADR 204**:
- [docs/architecture docs/adrs/ADR 204 - Operations as TypeScript
functions.md](docs/architecture%20docs/adrs/ADR%20204%20-%20Operations%20as%20TypeScript%20functions.md)

**Tests (evidence):**

- [packages/2-sql/4-lanes/sql-builder/test/runtime/functions.test.ts
(L256–L320)](packages/2-sql/4-lanes/sql-builder/test/runtime/functions.test.ts)
— extension function dispatch and `returnType` propagation
- [packages/3-extensions/sql-orm-client/test/model-accessor.test.ts
(L80–L135, L205–L230,
L345–L390)](packages/3-extensions/sql-orm-client/test/model-accessor.test.ts)
— raw value path, cross-column composition, unknown-field semantics,
shorthand typo raises
-
[packages/3-extensions/sql-orm-client/test/extension-operations.test-d.ts
(L40–L70)](packages/3-extensions/sql-orm-client/test/extension-operations.test-d.ts)
— type-level assertions that `cosineDistance` accepts raw, `null`, and
another column expression
- [packages/3-extensions/pgvector/test/operations.test.ts
(L24–L60)](packages/3-extensions/pgvector/test/operations.test.ts) —
calling an op's `impl` yields an AST node carrying the lowering template
- [packages/3-mongo-target/2-mongo-adapter/test/codecs.test.ts
(L140–L170)](packages/3-mongo-target/2-mongo-adapter/test/codecs.test.ts)
— `self` hint replaces `args[0]` on the descriptor
- [packages/2-sql/5-runtime/test/execution-stack.test.ts,
packages/2-sql/5-runtime/test/sql-context.test.ts](packages/2-sql/5-runtime/test)
— registry fixtures updated to new shape

## The story

1. **Introduce the expression primitives.** A new module in
relational-core defines `Expression<T>` (structural: `{returnType,
buildAst}`), `CodecExpression<CodecId, Nullable, CT>` (exact codec), and
`TraitExpression<Traits, Nullable, CT>` (codec gated by capability
traits). `toExpr(value, codecId?)` wraps a raw JS value as a `ParamRef`,
or threads through an existing Expression. `buildOperation({method,
args, returns, lowering})` constructs the `OperationExpr` AST node and
returns it as an `Expression<Returns>`. These are the building blocks
operations use.

2. **Shrink the operation descriptor.** `OperationEntry` drops `args`
and `returns` and becomes `{self?, impl}`. The framework-level registry
validates only that `self` (when present) names either a codec identity
*or* a trait set, not both — not the old per-argument check. Return
codec and lowering live on the AST node the `impl` constructs; the
registry no longer needs to see them.

3. **Flow authored impls through the SQL builder.** The builder's `fns`
surface used to wrap each registered op in a generated function that
coerced arguments by reading the declarative argument specs. That
wrapper is deleted: `fns.someOp` now returns the authored `impl`
directly. Any generics, conditional returns, or overloads the author
wrote survive to the call site unchanged. Aggregate functions (`count`,
`sum`, `avg`, …) drop the `ExpressionImpl.field` field and read
`expr.returnType` structurally.

4. **Make columns Expressions.** The ORM field accessor used to be a bag
of comparison/extension-method functions. It now *is* an
`Expression<{codecId, nullable}>` — carrying `returnType` and
`buildAst()` on the same object — with comparison and extension methods
spread on top. Extension-method factories receive the column as a self
Expression and forward it to `impl`. The `new ExpressionImpl(column,
{codecId: '', nullable: false})` hack is gone. Cross-column composition
— `db.a.embedding.cosineDistance(db.b.embedding)` — now works because
the second argument *is* an `Expression`, which the `CodecExpression`
union accepts.

5. **Tighten ORM accessor edges.** The Proxy used to synthesise a
fail-closed accessor for unknown fields; it now returns `undefined` like
any plain JS object would. The relation-shorthand iterator, which used
to silently skip unknown predicate keys, now throws — because a typo'd
filter key that silently matches every row is a sharp-edged footgun.

6. **Rewire contributors as factories.** `postgres/ilike`,
`pgvector/{cosineDistance,cosineSimilarity}`, and `mongo-adapter/near`
are ported: each adapter/extension exports a `QueryOperations<CT>()`
factory generic over the contract's codec-types map and a matching
`QueryOperationTypes<CT>` at the type level. The runtime-descriptor
slots (`queryOperations: () => …`) call each factory at assembly time.

7. **Emit the intersection.** The contract emitter used to reach for a
shared `generateCodecTypeIntersection` helper; it now inlines a
per-import `QueryOperationTypes<CodecTypes>` alias list, producing
`A<CodecTypes> & B<CodecTypes>` — the shape that reaches the SQL builder
and ORM client is already specialised to the contract's concrete
codec-types map.

## Behavior changes & evidence

- **Operations are authored as TypeScript functions.** The descriptor is
`{method, self?, impl}`; the impl's signature is the type surface; its
body builds the AST via `toExpr` + `buildOperation`. Return codec,
lowering template, and argument wrapping live inside the body.
- **Why**: closed records cannot express generics, conditional return
codecs, non-DB argument types, or method overloads; collapsing to a
single authoring surface means the type-level view and the runtime view
cannot drift.
- **Implementation**:
[packages/2-sql/4-lanes/relational-core/src/expression.ts](packages/2-sql/4-lanes/relational-core/src/expression.ts),
[packages/1-framework/1-core/operations/src/index.ts](packages/1-framework/1-core/operations/src/index.ts),
[packages/2-sql/1-core/contract/src/types.ts](packages/2-sql/1-core/contract/src/types.ts)
- **Tests**:
[packages/2-sql/4-lanes/sql-builder/test/runtime/functions.test.ts](packages/2-sql/4-lanes/sql-builder/test/runtime/functions.test.ts)
— `extension functions produces OperationExpr from queryOperationTypes`;
[packages/3-extensions/pgvector/test/operations.test.ts](packages/3-extensions/pgvector/test/operations.test.ts)
— `descriptor provides query operations whose impls build AST with
lowering`

- **ORM column handles implement `Expression<T>` directly, enabling
cross-column composition.**
`db.a.embedding.cosineDistance(db.b.embedding)` compiles to a ColumnRef
on `arg0`, not a ParamRef wrapping the accessor object.
- **Why**: the authoring model treats a column as just another
expression of the column's codec; the runtime must agree.
- **Implementation**:
[packages/3-extensions/sql-orm-client/src/model-accessor.ts](packages/3-extensions/sql-orm-client/src/model-accessor.ts)
- **Tests**:
[packages/3-extensions/sql-orm-client/test/model-accessor.test.ts](packages/3-extensions/sql-orm-client/test/model-accessor.test.ts)
— `cosineDistance accepts another vector column and produces a ColumnRef
on arg0 (cross-column composition)`;
[packages/3-extensions/sql-orm-client/test/extension-operations.test-d.ts](packages/3-extensions/sql-orm-client/test/extension-operations.test-d.ts)
— type-level union covers raw, `null`, and column

- **Unknown fields on a model accessor return `undefined` (plain JS
object semantics).** Before, the Proxy synthesised a fail-closed
accessor whose comparison methods threw “does not support equality
comparisons”.
- **Why**: the `ModelAccessor<TContract, ModelName>` type already
rejects typos at compile time for TS consumers, and programmatic
consumers can detect missing fields with a plain `undefined` check.
- **Implementation**:
[packages/3-extensions/sql-orm-client/src/model-accessor.ts](packages/3-extensions/sql-orm-client/src/model-accessor.ts)
- **Tests**:
[packages/3-extensions/sql-orm-client/test/model-accessor.test.ts](packages/3-extensions/sql-orm-client/test/model-accessor.test.ts)
— `returns undefined for fields whose storage table is not declared`

- **Relation-shorthand predicates throw on unknown field keys.** Before,
unknown keys were silently skipped.
- **Why**: silently dropping a filter clause means a typo like `nmae:
'Alice'` matches every row instead of failing loudly. Prefer a sharp
error at call time.
- **Implementation**:
[packages/3-extensions/sql-orm-client/src/model-accessor.ts](packages/3-extensions/sql-orm-client/src/model-accessor.ts)
— `toRelationWhereExpr`
- **Tests**:
[packages/3-extensions/sql-orm-client/test/model-accessor.test.ts](packages/3-extensions/sql-orm-client/test/model-accessor.test.ts)
— `Unknown fields in a shorthand predicate are surfaced loudly`

- **Adapter / extension contributions ship as CT-generic factories.**
Both the runtime descriptor (`() => readonly SqlOperationDescriptor[]`)
and the type-level surface (`QueryOperationTypes<CT>`) are generic over
the contract's codec-types map; the emitter instantiates
`QueryOperationTypes<CodecTypes>` per import and intersects them.
- **Why**: each `CodecExpression<CodecId, N, CT>` needs the concrete
codec-types map to produce raw JS value unions on call sites; binding
`CT` at factory-call time avoids a lane-level signature projection step.
- **Implementation**:
[packages/3-targets/6-adapters/postgres/src/core/descriptor-meta.ts](packages/3-targets/6-adapters/postgres/src/core/descriptor-meta.ts),
[packages/3-extensions/pgvector/src/core/descriptor-meta.ts](packages/3-extensions/pgvector/src/core/descriptor-meta.ts),
[packages/2-sql/3-tooling/emitter/src/index.ts](packages/2-sql/3-tooling/emitter/src/index.ts)
- **Tests**:
[packages/3-extensions/pgvector/test/operations.test.ts](packages/3-extensions/pgvector/test/operations.test.ts),
[packages/3-mongo-target/2-mongo-adapter/test/codecs.test.ts](packages/3-mongo-target/2-mongo-adapter/test/codecs.test.ts)

## Compatibility / migration / risk

- **Breaking change for extensions that shipped declarative operation
records.** Rewrite to a CT-generic factory returning `{method, self?,
impl}`; return codec and lowering move inside the `impl`'s
`buildOperation({ returns, lowering })` call. ADR 017 (Extension
Compatibility Policy) is honoured — no legacy-shape retention.
- **ORM column method signatures widen.** `.cosineDistance(v: number[] |
null)` becomes `.cosineDistance(CodecExpression<'pg/vector@1', Nullable,
CT>)`, which is a superset (raw value, `null`, or another Expression of
the same codec). Callers that previously passed only raw JS values
continue to compile.
- **Unknown accessor keys behave differently.** Programmatic code that
relied on the old fail-closed Proxy accessor for unknown fields (e.g.
catching a thrown error) must now detect `undefined`; relation-shorthand
predicates must avoid typo'd keys or be prepared to catch the new error.

## Follow-ups / open questions

- ADR 204 leaves one design question open: the adapter runtime
descriptor's `queryOperations` slot has the shape `() => readonly
SqlOperationDescriptor[]`, so each adapter's thunk currently calls its
`QueryOperations<CT>()` factory *internally* with an unconstrained `CT`.
Threading the contract's concrete codec-types map through the SPI slot
is a separate change.

## Non-goals / intentionally out of scope

- Not touching the built-in comparison methods (`eq`, `gt`, `like`,
`isNull`, …) — they remain codec-trait-gated via
`COMPARISON_METHODS_META` rather than registered operations.
- Not deriving the `self` dispatch hint from the function's first
parameter. The explicit `self` field keeps the dispatch key obvious to
readers and keeps the type-level matcher identical to the runtime walk.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **New Features**
* Introduced a typed expression system enabling operations to be
authored as TypeScript functions with improved type safety and codec
awareness.
* Operations now support flexible dispatch via concrete codec targeting
or trait-based capability selection.

* **Refactor**
* Simplified operation registration API to use minimal dispatch metadata
and implementation functions instead of parameter/return specifications.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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