Skip to content

fix(sql-orm-client): scope update()/delete() to a single row#435

Open
SevInf wants to merge 2 commits intomainfrom
fix-update
Open

fix(sql-orm-client): scope update()/delete() to a single row#435
SevInf wants to merge 2 commits intomainfrom
fix-update

Conversation

@SevInf
Copy link
Copy Markdown
Contributor

@SevInf SevInf commented May 7, 2026

Intent

Collection.update() and Collection.delete() returned a single row, but their SQL was unrestricted: UPDATE/DELETE … WHERE <filters> RETURNING * mutated every row matching the user's where() clause and then the JS layer kept only rows[0]. The single-row return value silently masked multi-row writes. This branch makes the runtime behavior match the contract the return type already implies: update()/delete() affect exactly one row. updateAll()/deleteAll() remain the way to mutate every match.

Change map

  • packages/3-extensions/sql-orm-client/src/collection.tsupdate() and delete() now resolve a single matching primary key first and re-issue the mutation scoped to that PK. A small private helper (#findFirstMatchingPkWhere) does the lookup using the existing first() infrastructure. deleteAll()'s body is extracted into #executeDeleteReturning so delete() can call it on a cloned (PK-narrowed) instance without tripping the this: constraint.
  • packages/3-extensions/sql-orm-client/test/integration/update.test.ts, delete.test.ts — new integration tests assert the single-row guarantee: seed three rows where two match where(), confirm exactly one is mutated and the other untouched.

The story

The bug is straightforward once you see it: update() at collection.ts delegated to updateAll() and returned rows[0] ?? null. updateAll() compiled UPDATE … WHERE filters RETURNING * with no LIMIT — Postgres updated every row matching filters and returned them all. The caller saw one row come back and assumed one row was changed. delete() had the same shape via deleteAll().toArray().

The fix mirrors what the existing nested-mutation path (updateFirstGraph in mutation-executor.ts) already does: SELECT the first matching PK, then issue the mutation scoped to that PK. Concretely, #findFirstMatchingPkWhere reuses first() on a clone with selectedFields: [pkColumn], includes: [] to keep the lookup cheap, then calls buildPrimaryKeyFilterFromRow + shorthandToWhereExpr to turn the PK value into a WHERE expression. update() and delete() clone themselves with that PK-only filter and delegate to the existing multi-row path — updateAll() / a private #executeDeleteReturning(). The *All methods are untouched and continue to mutate every match.

The cleanest fix would be to add a LIMIT clause to UpdateAst/DeleteAst and let update() express itself as take(1).updateAll(). That's not a small change: UpdateAst and DeleteAst have no limit slot today, Postgres doesn't accept UPDATE … LIMIT at all (only SQLite does, behind a build flag), and a portable LIMIT requires rewriting WHERE to pk IN (SELECT pk … LIMIT n). That deserves its own branch — see follow-ups.

Behavior changes & evidence

  • update() and delete() now mutate at most one row regardless of how many where() matches.
  • updateAll(), deleteAll(), updateCount(), deleteCount() are unchanged in behavior — they remain the multi-row variants.
  • Mongo ORM is unaffected (its update()/delete() already use findOneAndUpdate/findOneAndDelete, which are atomic and single-row at the storage layer).

New integration tests covering the contract:

  • update.test.tsupdate() affects only one row even when where() matches several
  • delete.test.tsdelete() affects only one row even when where() matches several

Full sql-orm-client suite: 480/480 passing. pnpm typecheck and pnpm lint:deps clean.

Compatibility / migration / risk

  • Behavior change for callers relying on the bug. Anyone calling users.where({ name: 'X' }).update({ … }) expecting all matching rows to update will now only get one. The fix is to switch those call sites to updateAll(…) / deleteAll(…). The return-type signature already implied single-row, so any such usage was incorrect against the documented surface.
  • Non-atomic. The fix issues two statements (SELECT PK, then UPDATE/DELETE WHERE pk = …). Concurrent writes between the two could in principle hit a stale PK. This matches what the existing nested-mutation path (updateFirstGraph) already does, so it's consistent with the rest of the codebase. Mongo's update()/delete() remain atomic via findOneAndUpdate/findOneAndDelete.
  • Determinism. Which row wins when several match is determined by the SELECT's order. #findFirstMatchingPkWhere preserves the collection's orderBy, so callers who care can chain orderBy() to make the choice deterministic.

Follow-ups / open questions

  • Add withLimit to UpdateAst/DeleteAst and a portable UPDATE/DELETE … WHERE pk IN (SELECT pk … LIMIT n) rewrite for Postgres. That would let update()/delete() collapse into take(1).updateAll() and would also enable updateAll(n)/deleteAll(n)-style bounded mutations.
  • Consider an atomic single-statement variant for SQL update()/delete() once the AST supports it, to match Mongo's atomicity.

Non-goals / intentionally out of scope

  • No changes to updateAll/deleteAll/updateCount/deleteCount.
  • No changes to the AST, SQL renderers, or capability gating.
  • No changes to the Mongo ORM.

Summary by CodeRabbit

  • New Features

    • Added a public helper to determine a table's row-identity columns (primary key or first unique).
  • Tests

    • Added integration tests ensuring update() and delete() modify only a single matched row.
    • Added unit tests for the new row-identity resolver covering PKs, unique constraints, composites, and missing tables.
  • Refactor

    • Internal improvements to single-row update/delete behavior and delete-returning execution for more reliable mutations.

update() and delete() were compiling UPDATE/DELETE ... WHERE <filters>
RETURNING * and then returning rows[0]. The single-row return value
masked the fact that every matching row was being mutated.

Resolve a single matching primary key first (via the existing first()),
then narrow the mutation to that PK so update()/delete() match the
single-row contract their return type already implies. The *All variants
remain the way to mutate every matching row.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Collection.update() and Collection.delete() now resolve an identity-scoped single-row where (primary key or first unique) via a new helper, run mutations inside withMutationScope() constrained to that where, return null if no identity can be resolved, and return the single affected row. deleteAll() delegates to a shared delete-returning executor. A contract helper resolveRowIdentityColumns() and tests were added; integration tests verify single-row behavior.

Changes

Single-Row Operations with Identity Constraint

Layer / File(s) Summary
Contract: resolve identity columns
packages/3-extensions/sql-orm-client/src/collection-contract.ts
Adds resolveRowIdentityColumns(contract, tableName) that returns primary key columns when present, otherwise the first unique constraint's columns, otherwise [].
Contract tests
packages/3-extensions/sql-orm-client/test/collection-contract.test.ts
Unit tests covering PK selection, composite PKs, unique-constraint fallback, composite unique columns, unknown table, and empty identity.
Identity where helper
packages/3-extensions/sql-orm-client/src/collection.ts
Adds #findFirstMatchingRowIdentityWhere() which selects the first matching row, extracts identity fields, builds a criterion and shorthand where, or returns null.
Runtime clone helper
packages/3-extensions/sql-orm-client/src/collection.ts
Adds #withRuntime(runtime) to create a cloned Collection bound to a provided runtime while preserving model/table/state and registry.
Refactored mutation methods
packages/3-extensions/sql-orm-client/src/collection.ts
update() and delete() now resolve an identity-scoped single-row where inside withMutationScope(), return null if unresolved, otherwise clone constrained to that where and perform the mutation returning the first affected row. deleteAll() delegates returned-rows execution to new #executeDeleteReturning().
Integration tests
packages/3-extensions/sql-orm-client/test/integration/delete.test.ts, packages/3-extensions/sql-orm-client/test/integration/update.test.ts
New tests verify delete() and update() affect exactly one row and return the affected row when where() matches multiple rows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through contracts, fields, and where,
Found the single row hiding there,
I bounded scope and kept it neat,
Update and delete now skip the fleet,
Tests clap softly — one row, complete.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 directly and clearly describes the main change: fixing update() and delete() to scope mutations to a single row instead of affecting all matches.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-update

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

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 May 7, 2026

Open in StackBlitz

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-telemetry

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: 9b10cef

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

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

Inline comments:
In `@packages/3-extensions/sql-orm-client/src/collection.ts`:
- Around line 1205-1223: `#findFirstMatchingPkWhere`() currently selects only a
single PK column because it uses resolvePrimaryKeyColumn; implement a new helper
resolvePrimaryKeyColumns(contract, tableName) that returns all PK columns
(array) and replace the single-column projection with selectedFields: pkColumns
(e.g., const pkColumns = resolvePrimaryKeyColumns(this.contract,
this.tableName); const firstRow = await this.#clone({ selectedFields: pkColumns,
includes: [] }).first();), so buildPrimaryKeyFilterFromRow receives the full
composite PK values and produces a unique WHERE filter.
🪄 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: 8b5c9493-fca6-4024-95fd-0673298fecbb

📥 Commits

Reviewing files that changed from the base of the PR and between 91a1853 and 9e3f525.

📒 Files selected for processing (3)
  • packages/3-extensions/sql-orm-client/src/collection.ts
  • packages/3-extensions/sql-orm-client/test/integration/delete.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/update.test.ts

Comment thread packages/3-extensions/sql-orm-client/src/collection.ts Outdated
Comment thread packages/3-extensions/sql-orm-client/src/collection.ts Outdated
The previous fix scoped update()/delete() by `resolvePrimaryKeyColumn`,
which only returns the first PK column. Composite primary keys would
collapse to a single column and fail to identify a row uniquely, and
PK-less tables (with a unique constraint instead) had no path at all.

Introduce `resolveRowIdentityColumns`: PK columns if present, else the
first unique constraint, else throw. The lookup helper now projects all
identity columns and builds a multi-column WHERE criterion via
`shorthandToWhereExpr`, which produces the AND-of-equalities filter that
uniquely identifies the row.

Also wrap the SELECT-then-mutate pair in `withMutationScope` so both
queries run against the same transaction-bound scope, matching what
`executeNestedUpdateMutation` already does for the nested-callback path.
A new `#withRuntime` helper clones the collection bound to the scoped
runtime so the existing Collection-level helpers (`first`, `updateAll`,
`#executeDeleteReturning`) continue to work inside the callback.
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.

🧹 Nitpick comments (5)
packages/3-extensions/sql-orm-client/test/collection-contract.test.ts (2)

240-240: 💤 Low value

Nit: nested under an unrelated outer describe.

The new resolveRowIdentityColumns() suite is nested inside describe('collection-contract capability detection', ...), which is about capability detection. Consider lifting it to a sibling top-level describe for clearer grouping (mirroring the resolvePolymorphismInfo() block below).

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

In `@packages/3-extensions/sql-orm-client/test/collection-contract.test.ts` at
line 240, The test suite for resolveRowIdentityColumns() is nested inside the
describe('collection-contract capability detection', ...) block; lift it out to
a sibling top-level describe to match the structure used for
resolvePolymorphismInfo(). Move the entire
describe('resolveRowIdentityColumns()', ...) block so it is not inside the
collection-contract capability detection describe, placing it at the same
indentation/level as the resolvePolymorphismInfo() describe to keep grouping
consistent and tests isolated.

240-295: 💤 Low value

Consider adding a test for the empty-PK-columns fallback path.

resolveRowIdentityColumns explicitly checks table.primaryKey && table.primaryKey.columns.length > 0 before returning PK columns, so a table with primaryKey: { columns: [] } and a non-empty unique should fall through to the unique. That branch is currently uncovered by these unit tests.

♻️ Suggested addition
+    it('falls back to uniques when primary key has empty columns', () => {
+      expect(
+        resolveRowIdentityColumns(
+          buildContract({
+            primaryKey: { columns: [] },
+            uniques: [{ columns: ['email'] }],
+          }),
+          't',
+        ),
+      ).toEqual(['email']);
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/3-extensions/sql-orm-client/test/collection-contract.test.ts` around
lines 240 - 295, Add a unit test in collection-contract.test.ts that covers the
code path where a table has primaryKey: { columns: [] } and a non-empty uniques
array; specifically, use the existing buildContract helper and call
resolveRowIdentityColumns with table 't' having primaryKey with empty columns
and uniques like [{ columns: ['email'] }], and assert the result equals
['email'] to verify resolveRowIdentityColumns falls back to uniques when
primaryKey.columns is empty.
packages/3-extensions/sql-orm-client/src/collection.ts (2)

1213-1219: 💤 Low value

Throwing on missing PK/unique is a behavior change worth documenting at the API.

Pre-PR, update()/delete() on a table without a primary key or unique constraint would (incorrectly) mutate every matching row. After this change they throw. That is the right call, but it's a runtime-visible behavior change for a niche case (PK-less tables). A brief note in the update() / delete() jsdoc about this precondition would help users diagnose the error fast.

The error message itself is good, just consider mentioning that updateAll() / deleteAll() remain available as an explicit "I really mean every match" escape hatch.

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

In `@packages/3-extensions/sql-orm-client/src/collection.ts` around lines 1213 -
1219, Add a jsdoc note to the public update() and delete() methods explaining
that they now require the table to have a primary key or unique constraint and
will throw (as implemented by `#findFirstMatchingRowIdentityWhere`()) when none
exist; mention that updateAll() and deleteAll() remain available as explicit
escape hatches to mutate every matching row and include a brief sentence
pointing to the thrown error message for diagnosis.

1213-1246: 💤 Low value

Identity-lookup clone inherits cursor / distinct / distinctOn / offset.

this.#clone({ selectedFields: [...identityColumns], includes: [] }) only overrides selectedFields and includes; cursor, distinct, distinctOn, and offset from the outer collection still flow into the SELECT used to pick the identity row. Preserving orderBy is intentional (determinism), and preserving filters/offset is generally what the user wants, but distinct / distinctOn combined with a forced selectedFields = identityColumns can produce a SELECT whose "first row" semantics differ from what the user constructed.

Consider being explicit about which state fields are preserved vs. reset for the identity probe (e.g., reset distinct / distinctOn since the PK/unique projection makes them moot, and keep filters / orderBy / offset / cursor). Low-priority since the typical call shape on a where().update() won't hit this.

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

In `@packages/3-extensions/sql-orm-client/src/collection.ts` around lines 1213 -
1246, The identity probe in `#findFirstMatchingRowIdentityWhere` currently clones
the collection but only overrides selectedFields and includes, letting
distinct/distinctOn (and others) leak into the identity SELECT; update the clone
call to explicitly reset distinct and distinctOn (e.g., pass distinct: undefined
and distinctOn: undefined) so the PK/unique projection isn't affected, keep
preserving filters and orderBy as before (and keep offset/cursor if you want
their semantics), and ensure the call remains this.#clone({ selectedFields:
[...identityColumns], includes: [], distinct: undefined, distinctOn: undefined
}) so identity resolution uses a correct projection.
packages/3-extensions/sql-orm-client/src/collection-contract.ts (1)

325-342: 💤 Low value

Consider throwing here rather than returning [].

Returning [] for "no identity available" or "unknown table" pushes the contract-violation detection to every caller. The single caller (Collection.#findFirstMatchingRowIdentityWhere) immediately re-throws on length === 0, but with a less precise message (it can't tell "no PK/unique" apart from "table doesn't exist"). Throwing distinct errors here would localize the diagnosis and make it harder for future callers to silently misuse the helper. Optional given the current single-caller scope.

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

In `@packages/3-extensions/sql-orm-client/src/collection-contract.ts` around lines
325 - 342, resolveRowIdentityColumns currently returns [] for both "table not
found" and "no PK/unique", which defers precise error handling to callers;
modify resolveRowIdentityColumns to throw distinct errors instead: throw a
TableNotFound (or a clear Error) when contract.storage.tables[tableName] is
missing, and throw a NoIdentityColumnsError (or descriptive Error) when the
table exists but has no primary key or unique columns. Update any callers (e.g.,
Collection.#findFirstMatchingRowIdentityWhere) to remove the length===0 check
and allow these specific errors to surface or catch them to produce
context-specific messages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/3-extensions/sql-orm-client/src/collection-contract.ts`:
- Around line 325-342: resolveRowIdentityColumns currently returns [] for both
"table not found" and "no PK/unique", which defers precise error handling to
callers; modify resolveRowIdentityColumns to throw distinct errors instead:
throw a TableNotFound (or a clear Error) when contract.storage.tables[tableName]
is missing, and throw a NoIdentityColumnsError (or descriptive Error) when the
table exists but has no primary key or unique columns. Update any callers (e.g.,
Collection.#findFirstMatchingRowIdentityWhere) to remove the length===0 check
and allow these specific errors to surface or catch them to produce
context-specific messages.

In `@packages/3-extensions/sql-orm-client/src/collection.ts`:
- Around line 1213-1219: Add a jsdoc note to the public update() and delete()
methods explaining that they now require the table to have a primary key or
unique constraint and will throw (as implemented by
`#findFirstMatchingRowIdentityWhere`()) when none exist; mention that updateAll()
and deleteAll() remain available as explicit escape hatches to mutate every
matching row and include a brief sentence pointing to the thrown error message
for diagnosis.
- Around line 1213-1246: The identity probe in
`#findFirstMatchingRowIdentityWhere` currently clones the collection but only
overrides selectedFields and includes, letting distinct/distinctOn (and others)
leak into the identity SELECT; update the clone call to explicitly reset
distinct and distinctOn (e.g., pass distinct: undefined and distinctOn:
undefined) so the PK/unique projection isn't affected, keep preserving filters
and orderBy as before (and keep offset/cursor if you want their semantics), and
ensure the call remains this.#clone({ selectedFields: [...identityColumns],
includes: [], distinct: undefined, distinctOn: undefined }) so identity
resolution uses a correct projection.

In `@packages/3-extensions/sql-orm-client/test/collection-contract.test.ts`:
- Line 240: The test suite for resolveRowIdentityColumns() is nested inside the
describe('collection-contract capability detection', ...) block; lift it out to
a sibling top-level describe to match the structure used for
resolvePolymorphismInfo(). Move the entire
describe('resolveRowIdentityColumns()', ...) block so it is not inside the
collection-contract capability detection describe, placing it at the same
indentation/level as the resolvePolymorphismInfo() describe to keep grouping
consistent and tests isolated.
- Around line 240-295: Add a unit test in collection-contract.test.ts that
covers the code path where a table has primaryKey: { columns: [] } and a
non-empty uniques array; specifically, use the existing buildContract helper and
call resolveRowIdentityColumns with table 't' having primaryKey with empty
columns and uniques like [{ columns: ['email'] }], and assert the result equals
['email'] to verify resolveRowIdentityColumns falls back to uniques when
primaryKey.columns is empty.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: a6931cd9-80c8-485d-9670-becc7f6538c4

📥 Commits

Reviewing files that changed from the base of the PR and between 9e3f525 and 9b10cef.

📒 Files selected for processing (3)
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • packages/3-extensions/sql-orm-client/src/collection.ts
  • packages/3-extensions/sql-orm-client/test/collection-contract.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants