turbine-orm 0.65.0 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +34 -32
  2. package/dist/adapters/cockroachdb.js +21 -3
  3. package/dist/adapters/index.d.ts +15 -0
  4. package/dist/adapters/yugabytedb.js +20 -3
  5. package/dist/cjs/adapters/cockroachdb.js +21 -3
  6. package/dist/cjs/adapters/index.d.ts +15 -0
  7. package/dist/cjs/adapters/yugabytedb.js +20 -3
  8. package/dist/cjs/cli/destructive.d.ts +18 -4
  9. package/dist/cjs/cli/destructive.js +230 -122
  10. package/dist/cjs/cli/index.d.ts +21 -4
  11. package/dist/cjs/cli/index.js +119 -22
  12. package/dist/cjs/cli/mcp.d.ts +28 -8
  13. package/dist/cjs/cli/mcp.js +170 -127
  14. package/dist/cjs/cli/migrate.d.ts +134 -13
  15. package/dist/cjs/cli/migrate.js +349 -241
  16. package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
  17. package/dist/cjs/cli/pii-predicate-guard.js +390 -0
  18. package/dist/cjs/cli/prisma-resolve.js +75 -4
  19. package/dist/cjs/cli/prisma-schema.d.ts +17 -1
  20. package/dist/cjs/cli/prisma-schema.js +83 -17
  21. package/dist/cjs/cli/sql-statements.d.ts +125 -0
  22. package/dist/cjs/cli/sql-statements.js +378 -0
  23. package/dist/cjs/cli/studio.js +49 -118
  24. package/dist/cjs/cli/ui.d.ts +1 -1
  25. package/dist/cjs/client.d.ts +43 -0
  26. package/dist/cjs/client.js +125 -6
  27. package/dist/cjs/dialect.d.ts +123 -0
  28. package/dist/cjs/dialect.js +33 -0
  29. package/dist/cjs/errors.d.ts +74 -1
  30. package/dist/cjs/errors.js +239 -25
  31. package/dist/cjs/index-advisor.d.ts +33 -1
  32. package/dist/cjs/index-advisor.js +32 -1
  33. package/dist/cjs/introspect.d.ts +48 -0
  34. package/dist/cjs/introspect.js +222 -91
  35. package/dist/cjs/mssql.js +43 -1
  36. package/dist/cjs/mysql.d.ts +5 -2
  37. package/dist/cjs/mysql.js +202 -17
  38. package/dist/cjs/nested-write.js +6 -1
  39. package/dist/cjs/pipeline-submittable.js +17 -3
  40. package/dist/cjs/pipeline.js +75 -9
  41. package/dist/cjs/powdb.d.ts +23 -0
  42. package/dist/cjs/powdb.js +33 -1
  43. package/dist/cjs/powql.d.ts +61 -9
  44. package/dist/cjs/powql.js +186 -49
  45. package/dist/cjs/prisma-compat.js +160 -41
  46. package/dist/cjs/query/aggregates.d.ts +1 -1
  47. package/dist/cjs/query/aggregates.js +80 -18
  48. package/dist/cjs/query/batched-loader.d.ts +10 -0
  49. package/dist/cjs/query/batched-loader.js +268 -7
  50. package/dist/cjs/query/builder.d.ts +73 -0
  51. package/dist/cjs/query/builder.js +225 -28
  52. package/dist/cjs/query/filters.d.ts +162 -0
  53. package/dist/cjs/query/filters.js +250 -1
  54. package/dist/cjs/query/relations.d.ts +10 -10
  55. package/dist/cjs/query/relations.js +93 -12
  56. package/dist/cjs/query/types.d.ts +14 -1
  57. package/dist/cjs/query/utils.d.ts +146 -2
  58. package/dist/cjs/query/utils.js +210 -4
  59. package/dist/cjs/query/warn-registry.d.ts +10 -0
  60. package/dist/cjs/query/warn-registry.js +10 -0
  61. package/dist/cjs/query/where-compile.d.ts +30 -0
  62. package/dist/cjs/query/where-compile.js +41 -0
  63. package/dist/cjs/query/where.d.ts +128 -13
  64. package/dist/cjs/query/where.js +215 -77
  65. package/dist/cjs/query/writes.d.ts +1 -1
  66. package/dist/cjs/query/writes.js +39 -15
  67. package/dist/cjs/schema-builder.d.ts +2 -1
  68. package/dist/cjs/schema-sql.d.ts +94 -4
  69. package/dist/cjs/schema-sql.js +506 -30
  70. package/dist/cjs/schema.d.ts +3 -1
  71. package/dist/cjs/sqlite.d.ts +6 -0
  72. package/dist/cjs/sqlite.js +151 -10
  73. package/dist/cjs/typed-sql.d.ts +29 -1
  74. package/dist/cjs/typed-sql.js +30 -12
  75. package/dist/cli/destructive.d.ts +18 -4
  76. package/dist/cli/destructive.js +229 -121
  77. package/dist/cli/index.d.ts +21 -4
  78. package/dist/cli/index.js +120 -24
  79. package/dist/cli/mcp.d.ts +28 -8
  80. package/dist/cli/mcp.js +172 -129
  81. package/dist/cli/migrate.d.ts +134 -13
  82. package/dist/cli/migrate.js +347 -238
  83. package/dist/cli/pii-predicate-guard.d.ts +112 -0
  84. package/dist/cli/pii-predicate-guard.js +386 -0
  85. package/dist/cli/prisma-resolve.js +75 -4
  86. package/dist/cli/prisma-schema.d.ts +17 -1
  87. package/dist/cli/prisma-schema.js +83 -17
  88. package/dist/cli/sql-statements.d.ts +125 -0
  89. package/dist/cli/sql-statements.js +373 -0
  90. package/dist/cli/studio.js +49 -118
  91. package/dist/cli/ui.d.ts +1 -1
  92. package/dist/client.d.ts +43 -0
  93. package/dist/client.js +126 -7
  94. package/dist/dialect.d.ts +123 -0
  95. package/dist/dialect.js +33 -0
  96. package/dist/errors.d.ts +74 -1
  97. package/dist/errors.js +228 -19
  98. package/dist/index-advisor.d.ts +33 -1
  99. package/dist/index-advisor.js +31 -1
  100. package/dist/introspect.d.ts +48 -0
  101. package/dist/introspect.js +221 -91
  102. package/dist/mssql.js +44 -2
  103. package/dist/mysql.d.ts +5 -2
  104. package/dist/mysql.js +203 -18
  105. package/dist/nested-write.js +7 -2
  106. package/dist/pipeline-submittable.js +18 -4
  107. package/dist/pipeline.js +76 -10
  108. package/dist/powdb.d.ts +23 -0
  109. package/dist/powdb.js +33 -2
  110. package/dist/powql.d.ts +61 -9
  111. package/dist/powql.js +187 -50
  112. package/dist/prisma-compat.js +160 -41
  113. package/dist/query/aggregates.d.ts +1 -1
  114. package/dist/query/aggregates.js +82 -20
  115. package/dist/query/batched-loader.d.ts +10 -0
  116. package/dist/query/batched-loader.js +270 -9
  117. package/dist/query/builder.d.ts +73 -0
  118. package/dist/query/builder.js +226 -30
  119. package/dist/query/filters.d.ts +162 -0
  120. package/dist/query/filters.js +246 -1
  121. package/dist/query/relations.d.ts +10 -10
  122. package/dist/query/relations.js +94 -14
  123. package/dist/query/types.d.ts +14 -1
  124. package/dist/query/utils.d.ts +146 -2
  125. package/dist/query/utils.js +204 -3
  126. package/dist/query/warn-registry.d.ts +10 -0
  127. package/dist/query/warn-registry.js +10 -0
  128. package/dist/query/where-compile.d.ts +30 -0
  129. package/dist/query/where-compile.js +40 -1
  130. package/dist/query/where.d.ts +128 -13
  131. package/dist/query/where.js +216 -80
  132. package/dist/query/writes.d.ts +1 -1
  133. package/dist/query/writes.js +40 -16
  134. package/dist/schema-builder.d.ts +2 -1
  135. package/dist/schema-sql.d.ts +94 -4
  136. package/dist/schema-sql.js +505 -30
  137. package/dist/schema.d.ts +3 -1
  138. package/dist/sqlite.d.ts +6 -0
  139. package/dist/sqlite.js +151 -10
  140. package/dist/typed-sql.d.ts +29 -1
  141. package/dist/typed-sql.js +30 -12
  142. package/package.json +6 -4
package/README.md CHANGED
@@ -30,8 +30,8 @@ The reason to reach for Turbine is that every layer between you and a production
30
30
 
31
31
  Two more things worth knowing, which are about cost rather than safety:
32
32
 
33
- - **One runtime dependency (`pg`).** No engine binary, no WASM, no adapter packages in lockstep. The main entry's **import graph** is held under **77 kB brotli** (edge under 61 kB) with `pg` external. That is the ceiling `size-limit` enforces in CI from `.size-limit.js`, not a figure typed into this file: a measurement quoted in prose goes stale silently, and this one did, drifting ~12% low over ten releases before a review caught it. Run `npm run size` for the current number. That is the client footprint your bundler sees, not the size of the dual ESM+CJS build on disk, which is larger. Prisma 7 dropped its Rust query engine but its client still ships a TypeScript/WASM query compiler, a ~1.6 MB bundle, down from the ~14 MB Rust-era client.
34
- - **Real pipelining, not a batch transaction.** `db.pipeline(...)` uses the Postgres extended-query protocol (Parse/Bind/Execute/Sync) to put N independent queries in one TCP flush. node-postgres does not expose pipelining in its pure-JS core ([brianc/node-postgres#2646](https://github.com/brianc/node-postgres/issues/2646) was still open as of July 2026), and Drizzle's `db.batch()` is an implicit transaction on specific drivers rather than independent-query pipelining.
33
+ - **One runtime dependency (`pg`).** No engine binary, no WASM, no adapter packages in lockstep. The main entry's **import graph** is held under **85 kB brotli** (edge under 68 kB) with `pg` external. That is the ceiling `size-limit` enforces in CI from `.size-limit.js`, not a figure typed into this file: a measurement quoted in prose goes stale silently, and this one did, drifting ~12% low over ten releases before a review caught it, and again in 0.66.0 when the budget was re-baselined. Run `npm run size` for the current number. That is the client footprint your bundler sees, not the size of the dual ESM+CJS build on disk, which is larger. Prisma 7 dropped its Rust query engine but its client still ships a TypeScript/WASM query compiler, a ~1.6 MB bundle, down from the ~14 MB Rust-era client.
34
+ - **Real pipelining at the protocol level.** `db.pipeline(...)` uses the Postgres extended-query protocol (Parse/Bind/Execute/Sync) to put N queries in one TCP flush, so the win is round-trips, not batching semantics. node-postgres does not expose pipelining in its pure-JS core ([brianc/node-postgres#2646](https://github.com/brianc/node-postgres/issues/2646) was still open as of July 2026), and Drizzle's `db.batch()` is a driver-specific implicit transaction rather than wire pipelining. The batch is atomic by default; `{ transactional: false }` makes the queries independent instead.
35
35
 
36
36
  **Beyond the safety bundle, what ships today:** [global filters](https://turbineorm.dev/global-filters) for soft-delete and multi-tenancy · [read replicas](https://turbineorm.dev/read-replicas) with a `$primary()` escape hatch · a read-only [MCP server](https://turbineorm.dev/mcp) for AI agents · [seed-as-code](https://turbineorm.dev/seeding) and a non-interactive `migrate deploy` for CI · [Zod generation](https://turbineorm.dev/zod) · read-only [views & generated columns](https://turbineorm.dev/views) · [optional SQLite / MySQL / SQL Server / PowDB engines](https://turbineorm.dev/engines) behind subpath exports · a [Prisma migration toolkit](https://turbineorm.dev/migrate-from-prisma) (schema mapper plus a runtime compat adapter) · a cost-aware index advisor in [`turbine doctor`](https://turbineorm.dev/cli#turbine-doctor).
37
37
 
@@ -39,39 +39,41 @@ Per-release detail lives in the [CHANGELOG](https://github.com/zvndev/turbine-or
39
39
 
40
40
  ## Benchmarks
41
41
 
42
- > **These are a dated snapshot, not a live claim.** Every figure below comes from **one measurement run on 2026-07-25 against turbine-orm 0.50.0**. It has not been re-run since, and the package you are installing is several minor versions ahead of it. Nothing here has been adjusted to match a later release, because inventing numbers is worse than quoting old ones. Read the table as *the shape of the result* (who leads which scenario, and by roughly how much) rather than as the latency your deployment will see, and reproduce it with the command at the end of this section if the absolute values matter to you. The same reasoning applies to the bundle-size figure above: a precise number typed into prose goes stale silently, so prefer the claim that cannot rot.
42
+ > **These are a dated snapshot, not a live claim.** Every figure below comes from **one measurement run on 2026-08-09 against turbine-orm 0.66.0**, the release you are installing. Nothing here has been adjusted to match anything, because inventing numbers is worse than quoting old ones. Read the table as *the shape of the result* (who leads which scenario, and by roughly how much) rather than as the latency your deployment will see, and reproduce it with the command at the end of this section if the absolute values matter to you. The same reasoning applies to the bundle-size figure above: a precise number typed into prose goes stale silently, so prefer the claim that cannot rot.
43
43
 
44
- Measured **2026-07-25 against turbine-orm 0.50.0** (commit `f8fec86`), tested against **Prisma 7.9.0** (`@prisma/adapter-pg`, `relationJoins` preview on) and **Drizzle 0.45.2** (relational queries) on a **local PostgreSQL 17.9** database over a Unix socket, with a **hand-written `pg` control arm**. Node v24.18.0, Apple Silicon MacBook Pro (M5 Max). Same schema, same data (1K users, 10K posts, 50K comments), same pool config.
44
+ Measured **2026-08-09 against turbine-orm 0.66.0**, tested against **Prisma 7.9.0** (`@prisma/adapter-pg`, `relationJoins` preview on) and **Drizzle 0.45.2** (relational queries) on a **local PostgreSQL 17.9** database over a Unix socket, with a **hand-written `pg` control arm**. Node v24.18.0, Apple Silicon MacBook Pro (M5 Max). Same schema, same data (1K users, 10K posts, 50K comments), same pool config.
45
45
 
46
46
  Every arm runs once per round, the arm order rotates every round, and each figure is the median over 200 rounds, taken as the median of three full runs. A local socket has no network round-trip, so these numbers are mostly sub-millisecond and isolate per-query overhead instead of hiding it behind network latency.
47
47
 
48
48
  Prisma's nested scenarios run on its **`join`** load strategy, which is its favorable configuration and is chosen deliberately.
49
49
 
50
- | Scenario | Turbine 0.50 | Prisma 7.9 | Drizzle 0.45 | raw pg |
50
+ | Scenario | Turbine 0.66 | Prisma 7.9 | Drizzle 0.45 | raw pg |
51
51
  |---|---|---|---|---|
52
- | findMany, 100 users (flat) | **0.256 ms** | 0.358 ms | 0.330 ms | 0.210 ms |
53
- | findMany, 50 users + posts (L2) | 2.421 ms | 4.603 ms | **2.001 ms** | 1.953 ms |
54
- | findMany, 10 users → posts → comments (L3) *(near-tie)* | 1.255 ms | 3.735 ms | **1.214 ms** | n/a |
55
- | findUnique, single user by PK | **0.051 ms** | 0.105 ms | 0.108 ms | 0.060 ms |
56
- | findUnique, user + posts + comments (L3) | **0.217 ms** | 0.469 ms | 0.357 ms | n/a |
57
- | count, all users | **0.044 ms** | 0.081 ms | 0.061 ms | 0.045 ms |
58
- | stream, iterate 50K rows (batch 1000) | 63.87 ms | 71.08 ms | **50.18 ms** | 50.97 ms |
59
- | atomic increment, `view_count + 1` *(near-tie)* | **0.115 ms** | 0.174 ms | 0.123 ms | 0.095 ms |
60
- | pipeline, 5-query batch | **0.206 ms** | 0.431 ms | 0.402 ms | 0.205 ms |
61
- | hot findUnique, 500x same shape | **0.029 ms** | 0.065 ms | 0.075 ms | 0.033 ms |
62
-
63
- **The number worth quoting from that run: Turbine ran at 1.07x hand-written `pg`, where Drizzle ran at 1.47x and Prisma at 1.84x** (geometric mean over the eight scenarios with a raw control). Across all ten scenarios Turbine was **1.87x faster than Prisma 7.9** and **1.36x faster than Drizzle 0.45** by geometric mean, against those two competitor versions.
64
-
65
- - **Turbine takes seven scenarios**, Drizzle three, Prisma none. Prisma is behind Turbine on all ten. Two of the ten (L3 nested, atomic increment) are genuine near-ties and are not leads for either side.
66
- - **Drizzle wins streaming outright, by 27%** (50.18 ms vs Turbine's 63.87 ms), reproduced in every run. Drizzle sits exactly on the raw `pg` keyset control, which is where a thin builder should sit; Turbine's cursor carries about 25% overhead above hand-written keyset pagination on a full-table drain. That overhead buys cursor semantics keyset cannot offer (any `orderBy`, deterministic early break, nested `with` per batch), but on this shape it is a loss and it is an open optimization target. A previous version of this table showed Turbine fastest here and called it a near-tie; that rested on a Drizzle figure we could not reproduce, and it was wrong.
67
- - **Drizzle also leads nested reads (L2).** Turbine's `json_agg` nesting is close behind and **1.9x to 3.0x ahead of Prisma** on the same L2/L3 shapes.
68
- - **Pipeline batching is Turbine's clearest win**: one TCP flush for 5 queries runs the dashboard batch 2.09x faster than Prisma's and 1.95x faster than Drizzle's sequential transaction, level with the raw `pg` control.
69
-
70
- > **Read the drift floor before quoting a sub-millisecond figure.** The identical raw control arm drifts 1% to 14% between runs on the multi-millisecond scenarios but **21% to 47%** on the sub-0.15 ms ones (findUnique by PK, count, atomic increment, hot findUnique, pipeline). Those orderings are stable across all five runs; their absolute values carry roughly one third uncertainty. Full per-run drift tables in [`benchmarks/RESULTS-0.50.0.md`](https://github.com/zvndev/turbine-orm/blob/main/benchmarks/RESULTS-0.50.0.md).
71
-
72
- Net, as of that run: Turbine was competitive-to-ahead across the board rather than a clean sweep, and the takeaway is the part that does not go stale: performance is close enough that the real reasons to choose Turbine are elsewhere. **One dependency and no WASM** (vs Prisma 7's ~1.6 MB TypeScript/WASM query compiler), the **only read-only-by-default Studio** in the TS ORM ecosystem, **PII-safe error messages** that never leak user data, and **SQL-first migrations** with SHA-256 drift detection. Deep type inference through `with` clauses works end-to-end: write `db.users.findMany({ with: { posts: { with: { comments: true } } } })` and `users[0].posts[0].comments[0].body` autocompletes, with no manual assertion and no helper annotation.
73
-
74
- > Full analysis, methodology and the drift floor: [`benchmarks/RESULTS-0.50.0.md`](https://github.com/zvndev/turbine-orm/blob/main/benchmarks/RESULTS-0.50.0.md). Historical runs: [`benchmarks/RESULTS.md`](https://github.com/zvndev/turbine-orm/blob/main/benchmarks/RESULTS.md).
52
+ | findMany, 100 users (flat) | **0.189 ms** | 0.254 ms | 0.248 ms | 0.164 ms |
53
+ | findMany, 50 users + posts (L2) *(contested, see below)* | 2.379 ms | 4.289 ms | **1.804 ms** | 1.779 ms |
54
+ | findMany, 10 users → posts → comments (L3) | 1.436 ms | 3.990 ms | **1.223 ms** | n/a |
55
+ | findUnique, single user by PK | **0.038 ms** | 0.083 ms | 0.085 ms | 0.037 ms |
56
+ | findUnique, user + posts + comments (L3) | **0.197 ms** | 0.397 ms | 0.303 ms | n/a |
57
+ | count, all users | **0.043 ms** | 0.076 ms | 0.058 ms | 0.044 ms |
58
+ | stream, iterate 50K rows (batch 1000) | 54.01 ms | 57.93 ms | **40.81 ms** | 42.77 ms |
59
+ | atomic increment, `view_count + 1` *(contested, see below)* | **0.073 ms** | 0.114 ms | 0.079 ms | 0.060 ms |
60
+ | pipeline, 5-query batch | **0.183 ms** | 0.386 ms | 0.366 ms | 0.194 ms |
61
+ | hot findUnique, 500x same shape | **0.029 ms** | 0.064 ms | 0.073 ms | 0.033 ms |
62
+
63
+ **The number worth quoting from that run: Turbine ran at 1.09x hand-written `pg`, where Drizzle ran at 1.49x and Prisma at 1.86x** (geometric mean over the eight scenarios with a raw control). Across all ten scenarios Turbine was **1.82x faster than Prisma 7.9** and **1.32x faster than Drizzle 0.45** by geometric mean, against those two competitor versions.
64
+
65
+ - **Turbine takes seven scenarios**, Drizzle three, Prisma none. Prisma is behind Turbine on all ten.
66
+ - **Two scenarios are contested and are not claimed by either side.** This run used two independent harnesses (one interleaves the arms and rotates their order every round, one measures each ORM in a contiguous block), and they disagree about **L2 nested reads** and **atomic increment**. The table above reports the interleaved harness, which is the more rigorous of the two because drift over the life of the process is shared across arms rather than landing on whichever arm held that slice of wall clock. Where the two disagree, the honest reading is that measurement design decides the winner, so the scenario is a tie.
67
+ - **Drizzle wins streaming outright, by 24%** (40.81 ms vs Turbine's 54.01 ms), reproduced in every run. Drizzle sits on the raw `pg` keyset control, which is where a thin builder should sit; Turbine's cursor carries about 26% overhead above hand-written keyset pagination on a full-table drain. That overhead buys cursor semantics keyset cannot offer (any `orderBy`, deterministic early break, nested `with` per batch), but on this shape it is a loss and it is an open optimization target.
68
+ - **Drizzle leads L3 nested reads**, and L2 on the primary harness. Turbine's `json_agg` nesting is close behind and **1.8x to 2.8x ahead of Prisma** on the same L2/L3 shapes.
69
+ - **The nested-relation gap to Drizzle widened since the 0.50.0 run** (L2 published at 1.21x, now 1.32x on the primary harness), and we have not yet found why. It is **not** this release: a direct interleaved A/B of 0.65.0 against 0.66.0 on the same database found 0.66.0 equal or marginally faster on every shape. Turbine's absolute L2 number barely moved between the two runs; the raw control and Drizzle both got about 10% faster and Turbine did not. Treat L2 as an open investigation rather than a result in either direction.
70
+ - **Pipeline batching is Turbine's clearest win**: one TCP flush for 5 queries runs the dashboard batch 2.11x faster than Prisma's and 2.00x faster than Drizzle's sequential transaction, level with the raw `pg` control.
71
+
72
+ > **Read the drift floor before quoting a sub-millisecond figure.** A `SELECT 1` probe at the head, middle and tail of each suite measured 0.0266 / 0.0107 / 0.0105 ms, a **153.8% spread**, nearly all of it process warmup between the head probe and the rest. The multi-millisecond scenarios (L2, L3, stream) are stable and their orderings are trustworthy. The sub-0.15 ms scenarios carry roughly one third uncertainty in their absolute values; their orderings held across all three runs but their **margins** should not be quoted. Full method, the 0.65-vs-0.66 A/B, and the harness-disagreement table in [`benchmarks/RESULTS-0.66.0.md`](https://github.com/zvndev/turbine-orm/blob/main/benchmarks/RESULTS-0.66.0.md).
73
+
74
+ Net, as of that run: Turbine is competitive-to-ahead across the board rather than a clean sweep, and the takeaway is the part that does not go stale: performance is close enough that the real reasons to choose Turbine are elsewhere. **One dependency and no WASM** (vs Prisma 7's ~1.6 MB TypeScript/WASM query compiler), the **only read-only-by-default Studio** in the TS ORM ecosystem, **PII-safe error messages** that never leak user data, and **SQL-first migrations** with SHA-256 drift detection. Deep type inference through `with` clauses works end-to-end: write `db.users.findMany({ with: { posts: { with: { comments: true } } } })` and `users[0].posts[0].comments[0].body` autocompletes, with no manual assertion and no helper annotation.
75
+
76
+ > Full analysis, methodology and the drift floor: [`benchmarks/RESULTS-0.66.0.md`](https://github.com/zvndev/turbine-orm/blob/main/benchmarks/RESULTS-0.66.0.md). Previous run: [`benchmarks/RESULTS-0.50.0.md`](https://github.com/zvndev/turbine-orm/blob/main/benchmarks/RESULTS-0.50.0.md). Historical runs: [`benchmarks/RESULTS.md`](https://github.com/zvndev/turbine-orm/blob/main/benchmarks/RESULTS.md).
75
77
  > Reproduce: `cd benchmarks && npm install && npx prisma generate && DATABASE_URL=... npx tsx bench-interleaved.ts`
76
78
 
77
79
  ## Quick Start
@@ -491,7 +493,7 @@ const [order, _items, updated] = await db.$transaction([
491
493
 
492
494
  The full set: `buildFindMany`, `buildFindUnique`, `buildFindFirst`, `buildFindUniqueOrThrow`, `buildFindFirstOrThrow`, `buildCount`, `buildAggregate`, `buildGroupBy`, `buildCreate`, `buildCreateMany`, `buildUpdate`, `buildUpdateMany`, `buildUpsert`, `buildDelete`, `buildDeleteMany`.
493
495
 
494
- Use `db.pipeline(...)` when the queries are independent and you want one round-trip with no transaction; use `db.$transaction([...])` when you want all-or-nothing. Nested writes (relation operations inside `data`) open their own transaction, so those methods stay `async` and have no `build*` twin.
496
+ Use `db.pipeline(...)` when the queries are independent and you want them in one round-trip; use `db.$transaction([...])` when you also need interactive control or SAVEPOINT nesting. Both are atomic by default: a pipeline wraps the batch in one `BEGIN`/`COMMIT` unless you pass `{ transactional: false }`, and only that opt-out path can produce a `PipelineError` (TURBINE_E014) with per-slot results. Nested writes (relation operations inside `data`) open their own transaction, so those methods stay `async` and have no `build*` twin.
495
497
 
496
498
  ### Middleware
497
499
 
@@ -1177,11 +1179,11 @@ Turbine maps Postgres types to TypeScript:
1177
1179
  |---|---|---|---|---|
1178
1180
  | **Engine / runtime** | No engine binary (`pg` only) | Client + TS/WASM query compiler | No engine | No engine |
1179
1181
  | **Runtime deps** | 1 (`pg`) | `@prisma/client` + required driver adapter | 0 | 0 |
1180
- | **Main bundle (brotli)** | under 77 kB import graph (CI-enforced), `pg` external | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small |
1182
+ | **Main bundle (brotli)** | under 85 kB import graph (CI-enforced), `pg` external | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small |
1181
1183
  | **Studio** | Read-only, 192-bit auth | Full CRUD, cloud-hosted | Free; hosted Gateway paid | None |
1182
1184
  | **Error PII safety** | Keys only by default | Values in messages | Raw pg errors | Raw pg errors |
1183
1185
  | **Migrations** | SQL-first, SHA-256 checksums | DSL-generated, shadow DB | SQL or Drizzle Kit | None |
1184
- | **Edge runtime** | One import swap, under 61 kB brotli (CI-enforced) | Driver adapter + WASM compiler | Native | Native |
1186
+ | **Edge runtime** | One import swap, under 68 kB brotli (CI-enforced) | Driver adapter + WASM compiler | Native | Native |
1185
1187
  | **Pipeline batching** | Parse/Bind/Execute protocol | Sequential in txn | Sequential | Manual |
1186
1188
  | **Typed errors** | `isRetryable` discriminant | Error codes only | None | None |
1187
1189
  | **Nested relations** | 1 query, deep type inference | 1 query per relation by default; single-query `relationJoins` is still Preview and whole-query only | 1 query, `relations()` re-declaration | Manual (`jsonArrayFrom`) |
@@ -1200,7 +1202,7 @@ Reading the table: no engine binary and no WASM, just one runtime dependency (`p
1200
1202
 
1201
1203
  Turbine is focused and opinionated. Here's what it doesn't do:
1202
1204
 
1203
- - **Postgres-first.** PostgreSQL is the default and primary target, going deep on one database is what enables the safety bundle and the edge-runtime story. SQLite, MySQL 8, and SQL Server engines are available as additive subpath exports (see [Database engines](#database-engines)), but several flagship features (pgvector, LISTEN/NOTIFY, RLS `sessionContext`, full-text `search`, array-column filters, `groupBy({ distinctOn })`) are Postgres-only and throw `UnsupportedFeatureError` elsewhere.
1205
+ - **Postgres-first.** PostgreSQL is the default and primary target, going deep on one database is what enables the safety bundle and the edge-runtime story. SQLite, MySQL 8, and SQL Server engines are available as additive subpath exports (see [Database engines](#database-engines)), but several flagship features (pgvector, LISTEN/NOTIFY, RLS `sessionContext`, full-text `search`, array-column filters, `findMany({ distinct })` and `groupBy({ distinctOn })`) are Postgres-only and throw `UnsupportedFeatureError` elsewhere.
1204
1206
  - **Full-text search** is available via a `search` filter, `where: { title: { search: 'hello & world', config: 'english' } }` compiles to a parameterized `to_tsvector(...) @@ to_tsquery(...)`. PostgreSQL only: the other engines throw `UnsupportedFeatureError` (`TURBINE_E017`) rather than degrade to a `LIKE`. For advanced ranking (`ts_rank`, weighted vectors) use `db.raw`.
1205
1207
  - **Large nested result sets.** Nested results are materialized server-side in PostgreSQL memory. For relations with 10K+ rows, always use `limit` in your `with` clause, or stream the parents with `findManyStream` and resolve children per-row.
1206
1208
 
@@ -97,8 +97,24 @@ const SQL_ENUMS_CRDB = `
97
97
  // ---------------------------------------------------------------------------
98
98
  // Adapter implementation
99
99
  // ---------------------------------------------------------------------------
100
+ /**
101
+ * Error codes that mean "another session already holds the migration lock".
102
+ *
103
+ * `55P03` (lock_not_available) is what `NOWAIT` raises, and was the only code
104
+ * treated as contention. CockroachDB runs SERIALIZABLE by default, so the very
105
+ * same contention can instead surface as `40001` (serialization_failure) when
106
+ * the conflicting transactions are ordered rather than blocked. That crashed
107
+ * `turbine migrate` with a raw driver error in place of the clean "another
108
+ * migration is already running" message, for exactly the situation the lock
109
+ * exists to describe.
110
+ */
111
+ const LOCK_CONTENTION_CODES = new Set(['55P03', '40001']);
100
112
  export const cockroachdb = {
101
113
  name: 'cockroachdb',
114
+ // The lock is a row lock, so it lives and dies with the transaction
115
+ // acquireLock leaves open. The migration runner gives it a dedicated
116
+ // connection on the strength of this flag.
117
+ lockHoldsOpenTransaction: true,
102
118
  createLockTableSQL() {
103
119
  return CREATE_LOCK_TABLE_SQL;
104
120
  },
@@ -114,13 +130,15 @@ export const cockroachdb = {
114
130
  // Update the acquired metadata
115
131
  await client.query(`UPDATE "${LOCK_TABLE}" SET acquired_at = now(), acquired_by = current_user WHERE lock_id = $1`, [lockId]);
116
132
  // Note: we leave the transaction OPEN, the lock is held until
117
- // releaseLock() commits or rolls back.
133
+ // releaseLock() commits or rolls back. That is why this adapter sets
134
+ // `lockHoldsOpenTransaction` and the runner hands it its own connection:
135
+ // on a shared one, the first migration's COMMIT would end this.
118
136
  return true;
119
137
  }
120
138
  catch (err) {
121
- // NOWAIT throws error code 55P03 (lock_not_available) if the row is locked
139
+ // Contention (see LOCK_CONTENTION_CODES) means somebody else is migrating.
122
140
  const pgErr = err;
123
- if (pgErr.code === '55P03') {
141
+ if (LOCK_CONTENTION_CODES.has(String(pgErr.code))) {
124
142
  try {
125
143
  await client.query('ROLLBACK');
126
144
  }
@@ -37,6 +37,21 @@ export interface IntrospectionOverrides {
37
37
  export interface DatabaseAdapter {
38
38
  /** Identifier for the adapter (e.g. 'postgresql', 'cockroachdb') */
39
39
  readonly name: string;
40
+ /**
41
+ * True when this adapter's lock lives in a transaction that `acquireLock`
42
+ * deliberately leaves OPEN on the connection it was given (the
43
+ * `SELECT ... FOR UPDATE NOWAIT` table-lock strategy: a row lock exists only
44
+ * for as long as its transaction does).
45
+ *
46
+ * The migration runner reads this to decide whether the lock needs its own
47
+ * connection. It does: the runner wraps every migration in BEGIN/COMMIT, so
48
+ * sharing one connection means the first migration's COMMIT ends the LOCK's
49
+ * transaction and every later migration runs unprotected, silently, because
50
+ * the eventual `releaseLock` COMMIT only warns. Leave it unset for a
51
+ * session-scoped lock (`pg_try_advisory_lock`), which survives COMMIT on its
52
+ * own and must keep using the runner's connection unchanged.
53
+ */
54
+ readonly lockHoldsOpenTransaction?: boolean;
40
55
  /**
41
56
  * Acquire a concurrency lock for migrations.
42
57
  * PostgreSQL uses `pg_try_advisory_lock`. CockroachDB uses a lock table.
@@ -88,8 +88,22 @@ const SQL_ROW_ESTIMATES_YBDB = `
88
88
  // ---------------------------------------------------------------------------
89
89
  // Adapter implementation
90
90
  // ---------------------------------------------------------------------------
91
+ /**
92
+ * Error codes that mean "another session already holds the migration lock".
93
+ *
94
+ * `55P03` (lock_not_available) is what `NOWAIT` raises. `40001`
95
+ * (serialization_failure) is the same contention reported the other way round:
96
+ * YugabyteDB's distributed transaction layer can abort the conflicting
97
+ * transaction instead of refusing the lock, and treating that as a hard error
98
+ * crashed `turbine migrate` with a raw driver message in place of the clean
99
+ * "another migration is already running" one.
100
+ */
101
+ const LOCK_CONTENTION_CODES = new Set(['55P03', '40001']);
91
102
  export const yugabytedb = {
92
103
  name: 'yugabytedb',
104
+ // Row lock, so it lives and dies with the transaction acquireLock leaves
105
+ // open, and the migration runner must give it a dedicated connection.
106
+ lockHoldsOpenTransaction: true,
93
107
  createLockTableSQL() {
94
108
  return CREATE_LOCK_TABLE_SQL;
95
109
  },
@@ -105,13 +119,16 @@ export const yugabytedb = {
105
119
  await client.query(`SELECT lock_id FROM "${LOCK_TABLE}" WHERE lock_id = $1 FOR UPDATE NOWAIT`, [lockId]);
106
120
  // Update acquisition metadata for observability
107
121
  await client.query(`UPDATE "${LOCK_TABLE}" SET acquired_at = now(), acquired_by = current_user WHERE lock_id = $1`, [lockId]);
108
- // Leave the transaction open, lock is held until releaseLock()
122
+ // Leave the transaction open, lock is held until releaseLock(). That is
123
+ // why this adapter sets `lockHoldsOpenTransaction`: on the runner's own
124
+ // connection, the first migration's COMMIT would end this transaction and
125
+ // release the lock with every later migration still to run.
109
126
  return true;
110
127
  }
111
128
  catch (err) {
112
129
  const pgErr = err;
113
- // 55P03 = lock_not_available (NOWAIT couldn't acquire)
114
- if (pgErr.code === '55P03') {
130
+ // Contention (see LOCK_CONTENTION_CODES) means somebody else is migrating.
131
+ if (LOCK_CONTENTION_CODES.has(String(pgErr.code))) {
115
132
  try {
116
133
  await client.query('ROLLBACK');
117
134
  }
@@ -100,8 +100,24 @@ const SQL_ENUMS_CRDB = `
100
100
  // ---------------------------------------------------------------------------
101
101
  // Adapter implementation
102
102
  // ---------------------------------------------------------------------------
103
+ /**
104
+ * Error codes that mean "another session already holds the migration lock".
105
+ *
106
+ * `55P03` (lock_not_available) is what `NOWAIT` raises, and was the only code
107
+ * treated as contention. CockroachDB runs SERIALIZABLE by default, so the very
108
+ * same contention can instead surface as `40001` (serialization_failure) when
109
+ * the conflicting transactions are ordered rather than blocked. That crashed
110
+ * `turbine migrate` with a raw driver error in place of the clean "another
111
+ * migration is already running" message, for exactly the situation the lock
112
+ * exists to describe.
113
+ */
114
+ const LOCK_CONTENTION_CODES = new Set(['55P03', '40001']);
103
115
  exports.cockroachdb = {
104
116
  name: 'cockroachdb',
117
+ // The lock is a row lock, so it lives and dies with the transaction
118
+ // acquireLock leaves open. The migration runner gives it a dedicated
119
+ // connection on the strength of this flag.
120
+ lockHoldsOpenTransaction: true,
105
121
  createLockTableSQL() {
106
122
  return CREATE_LOCK_TABLE_SQL;
107
123
  },
@@ -117,13 +133,15 @@ exports.cockroachdb = {
117
133
  // Update the acquired metadata
118
134
  await client.query(`UPDATE "${LOCK_TABLE}" SET acquired_at = now(), acquired_by = current_user WHERE lock_id = $1`, [lockId]);
119
135
  // Note: we leave the transaction OPEN, the lock is held until
120
- // releaseLock() commits or rolls back.
136
+ // releaseLock() commits or rolls back. That is why this adapter sets
137
+ // `lockHoldsOpenTransaction` and the runner hands it its own connection:
138
+ // on a shared one, the first migration's COMMIT would end this.
121
139
  return true;
122
140
  }
123
141
  catch (err) {
124
- // NOWAIT throws error code 55P03 (lock_not_available) if the row is locked
142
+ // Contention (see LOCK_CONTENTION_CODES) means somebody else is migrating.
125
143
  const pgErr = err;
126
- if (pgErr.code === '55P03') {
144
+ if (LOCK_CONTENTION_CODES.has(String(pgErr.code))) {
127
145
  try {
128
146
  await client.query('ROLLBACK');
129
147
  }
@@ -37,6 +37,21 @@ export interface IntrospectionOverrides {
37
37
  export interface DatabaseAdapter {
38
38
  /** Identifier for the adapter (e.g. 'postgresql', 'cockroachdb') */
39
39
  readonly name: string;
40
+ /**
41
+ * True when this adapter's lock lives in a transaction that `acquireLock`
42
+ * deliberately leaves OPEN on the connection it was given (the
43
+ * `SELECT ... FOR UPDATE NOWAIT` table-lock strategy: a row lock exists only
44
+ * for as long as its transaction does).
45
+ *
46
+ * The migration runner reads this to decide whether the lock needs its own
47
+ * connection. It does: the runner wraps every migration in BEGIN/COMMIT, so
48
+ * sharing one connection means the first migration's COMMIT ends the LOCK's
49
+ * transaction and every later migration runs unprotected, silently, because
50
+ * the eventual `releaseLock` COMMIT only warns. Leave it unset for a
51
+ * session-scoped lock (`pg_try_advisory_lock`), which survives COMMIT on its
52
+ * own and must keep using the runner's connection unchanged.
53
+ */
54
+ readonly lockHoldsOpenTransaction?: boolean;
40
55
  /**
41
56
  * Acquire a concurrency lock for migrations.
42
57
  * PostgreSQL uses `pg_try_advisory_lock`. CockroachDB uses a lock table.
@@ -91,8 +91,22 @@ const SQL_ROW_ESTIMATES_YBDB = `
91
91
  // ---------------------------------------------------------------------------
92
92
  // Adapter implementation
93
93
  // ---------------------------------------------------------------------------
94
+ /**
95
+ * Error codes that mean "another session already holds the migration lock".
96
+ *
97
+ * `55P03` (lock_not_available) is what `NOWAIT` raises. `40001`
98
+ * (serialization_failure) is the same contention reported the other way round:
99
+ * YugabyteDB's distributed transaction layer can abort the conflicting
100
+ * transaction instead of refusing the lock, and treating that as a hard error
101
+ * crashed `turbine migrate` with a raw driver message in place of the clean
102
+ * "another migration is already running" one.
103
+ */
104
+ const LOCK_CONTENTION_CODES = new Set(['55P03', '40001']);
94
105
  exports.yugabytedb = {
95
106
  name: 'yugabytedb',
107
+ // Row lock, so it lives and dies with the transaction acquireLock leaves
108
+ // open, and the migration runner must give it a dedicated connection.
109
+ lockHoldsOpenTransaction: true,
96
110
  createLockTableSQL() {
97
111
  return CREATE_LOCK_TABLE_SQL;
98
112
  },
@@ -108,13 +122,16 @@ exports.yugabytedb = {
108
122
  await client.query(`SELECT lock_id FROM "${LOCK_TABLE}" WHERE lock_id = $1 FOR UPDATE NOWAIT`, [lockId]);
109
123
  // Update acquisition metadata for observability
110
124
  await client.query(`UPDATE "${LOCK_TABLE}" SET acquired_at = now(), acquired_by = current_user WHERE lock_id = $1`, [lockId]);
111
- // Leave the transaction open, lock is held until releaseLock()
125
+ // Leave the transaction open, lock is held until releaseLock(). That is
126
+ // why this adapter sets `lockHoldsOpenTransaction`: on the runner's own
127
+ // connection, the first migration's COMMIT would end this transaction and
128
+ // release the lock with every later migration still to run.
112
129
  return true;
113
130
  }
114
131
  catch (err) {
115
132
  const pgErr = err;
116
- // 55P03 = lock_not_available (NOWAIT couldn't acquire)
117
- if (pgErr.code === '55P03') {
133
+ // Contention (see LOCK_CONTENTION_CODES) means somebody else is migrating.
134
+ if (LOCK_CONTENTION_CODES.has(String(pgErr.code))) {
118
135
  try {
119
136
  await client.query('ROLLBACK');
120
137
  }
@@ -18,15 +18,27 @@
18
18
  * Row removal hides in more than a leading `DELETE`, so the scan also covers:
19
19
  * the optional-`COLUMN` shorthand (`ALTER TABLE t DROP email`), data-modifying
20
20
  * CTEs (`WITH d AS (DELETE ...) SELECT ...`), `MERGE ... THEN DELETE`, dynamic
21
- * SQL inside a `DO`/function body, and an `UPDATE` whose only WHERE sits inside
22
- * a subquery (which restricts nothing).
21
+ * SQL inside a `DO`/function body, an `UPDATE` whose only WHERE sits inside a
22
+ * subquery (which restricts nothing), a `DROP ... CASCADE` of a non-table
23
+ * object (which takes dependent COLUMNS with it), a detached partition, and an
24
+ * `EXPLAIN ANALYZE` of any of the above (which really executes it).
25
+ *
26
+ * The lexing, "where does a statement end" and "which characters are code",
27
+ * is NOT done here: it lives in `sql-statements.ts` and is shared with the
28
+ * migration runner. Two lexers is how this guard came to disagree with the
29
+ * executor about what a file contained (see that module's header).
23
30
  */
24
- export type DestructiveKind = 'drop-table' | 'drop-schema' | 'drop-database' | 'drop-owned' | 'drop-matview' | 'drop-column' | 'truncate' | 'delete' | 'update-without-where' | 'alter-column-type' | 'merge-delete';
31
+ export type DestructiveKind = 'drop-table' | 'drop-schema' | 'drop-database' | 'drop-owned' | 'drop-matview' | 'drop-column' | 'drop-cascade' | 'detach-partition' | 'truncate' | 'delete' | 'update-without-where' | 'alter-column-type' | 'merge-delete' | 'rename' | 'dynamic-destructive';
25
32
  export interface DestructiveStatement {
26
33
  /** The offending SQL statement (trimmed, possibly long, display truncated) */
27
34
  statement: string;
28
35
  kind: DestructiveKind;
29
- /** Best-effort extracted object name (table, schema, or table.column) */
36
+ /**
37
+ * Best-effort extracted object name (table, schema, or table.column). For
38
+ * `drop-cascade` it is prefixed with the object kind (`TYPE order_status`),
39
+ * because the same rule covers eight different kinds of object and the bare
40
+ * name would not tell the operator what they are about to lose.
41
+ */
30
42
  target: string;
31
43
  }
32
44
  /** Human explanation per kind, used in CLI output. */
@@ -36,3 +48,5 @@ export declare const DESTRUCTIVE_KIND_LABEL: Record<DestructiveKind, string>;
36
48
  * return every statement that can destroy data.
37
49
  */
38
50
  export declare function scanDestructiveSql(sql: string): DestructiveStatement[];
51
+ /** Shown in place of an object name that does not exist until the block runs. */
52
+ export declare const DYNAMIC_TARGET = "<name assembled at run time>";