docs(projects): shape affected-row-counts (spec + plan) (TML-3166) - #29895
docs(projects): shape affected-row-counts (spec + plan) (TML-3166)#29895StevenMcClankerton wants to merge 1 commit into
Conversation
Shaping artifacts for the **affected-row-counts** project — spec and three-slice plan, per the project lifecycle in `projects/README.md`. ## What this project does `updateAndCount` and `deleteAndCount` run two statements: a `SELECT` of every matching primary key, then the write — returning the *read's* row count and discarding whatever the write reported. That is not atomic (outside a transaction a concurrent insert is updated but not counted), it evaluates the filter twice and materialises every matching key in JS purely to call `.length` on it, and it builds the two `WHERE` clauses through different code paths that already drifted once for MTI variants (#940). The count already exists and is thrown away — Postgres reports it in the `CommandComplete` tag, SQLite in `sqlite3_changes64()` via `StatementSync.run()`. The gap is structural: `RuntimeScope.execute()` returns a row stream with nowhere for statement metadata to live. After this project the driver SPI splits along the question being asked, named the way every prior art names it (JDBC, ADO.NET, Go): query<Row>(req): AsyncIterable<Row> // rows execute(req): Promise<SqlStatementStats> // { affectedRows: number } `affectedRows` is not optional — absence is not a state either engine has for the statements `execute()` exists to serve. Statistics never travel through a row stream, so the seven `for await` re-wrap sites between driver and caller stop being a hazard. Prepared-ness rides on the request rather than doubling the method surface, so four driver methods become two. ## Scope boundaries Streaming write terminals, `createAndCount`, and new targets are out. Count semantics are deliberately *not* unified across targets — Postgres's command tag, SQLite's `sqlite3_changes64()`, and Mongo's `modifiedCount` each mean something different, and the project documents the difference rather than reconciling it. ## Decision provenance Three questions were settled with the operator at spec time and moved into the spec body: the execution shape (an earlier single-`execute()` frame-yielding design was considered and reversed), the naming falling out of that shape, and per-driver count semantics. Spec § Open Questions records the reversal. The project amends ADR 210 — Prepared Statements rather than adding a new ADR: every principle it states survives, only the shape they were expressed through changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Steven McClankerton <tatarintsev@prisma.io>
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (2)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including ⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
|
Closing — opened in error. The spec + plan commit rides slice 1's PR (TML-3167) instead; no separate planning PR. |
Shaping artifacts for the affected-row-counts project — spec and three-slice plan, per the project lifecycle in
projects/README.md. No code changes.What this project does
updateAndCountanddeleteAndCountrun two statements: aSELECTof every matching primary key, then the write — returning the read's row count and discarding whatever the write reported. Three consequences:.lengthon it.WHEREbuilders that can drift. The countSELECTgoes throughcompileSelect, the write throughbuildCountMutationWhere. They already diverged once for MTI variants (CLI: Rename Prisma 2 to Prisma Framework #940).Meanwhile the count already exists and is discarded — Postgres reports it in the
CommandCompletetag, SQLite insqlite3_changes64()viaStatementSync.run(). The gap is structural:RuntimeScope.execute()returns a row stream, which has nowhere for statement metadata to live, and every layer between driver and caller consumes its source withfor await.After this project the driver SPI splits along the question being asked, named the way every prior art names it (JDBC
executeQuery/executeUpdate, ADO.NETExecuteReader/ExecuteNonQuery, GoQuery/Exec):affectedRowsis not optional — absence is not a state either engine has for the statementsexecute()exists to serve, so nothing downstream branches onundefined. Because statistics never travel through a row stream, the sevenfor awaitre-wrap sites stop being a hazard: there is nothing in flight for them to drop. Prepared-ness rides on the request rather than doubling the method surface, so four driver methods become two.Plan shape
Three slices — a stack of two plus an independent docs slice:
query-execute-split(TML-3167) — reshape the driver SPI and the runtime's execution paths. No ORM-visible behaviour change; the pre-SELECTis still in place at the end of this slice.count-terminals(TML-3168) — the terminals consumeexecute(); the pre-SELECTis deleted rather than left dormant.count-semantics(TML-3169, parallel) — amend ADR 210; document each target's definition of "affected".Scope boundaries
Streaming write terminals,
createAndCount, and new targets are out of scope. Count semantics are deliberately not unified across targets — Postgres's command tag counts matched rows, SQLite'ssqlite3_changes64()counts modified rows, Mongo'smodifiedCountexcludes no-op writes. The project documents the difference rather than reconciling it; no translation layer, no normalized definition.Decision provenance
Three questions were settled with the operator at spec time and moved into the spec body: the execution shape (an earlier single-
execute()design yielding row/metadata frames was considered and reversed — it gave statistics a home inside the stream at the cost of making every layer demux), the naming falling out of that shape, and per-driver count semantics.The project amends ADR 210 — Prepared Statements rather than adding a new ADR: every principle it states survives (opaque slot, lazy synchronous allocation, a driver may ignore the slot,
preparedStatements: falseleaves it unset) — only the shape they were expressed through changes. ADR 210's stale-retry contract requiresADAPTER.PREPARE_FAILED, currently emitted nowhere in the codebase; closing that is in scope for slice 1 rather than deferred, since rewriting the surrounding code while leaving a known violation of the ADR being amended would be incoherent.Refs: TML-3166
🤖 Generated with Claude Code