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.
- package/README.md +34 -32
- package/dist/adapters/cockroachdb.js +21 -3
- package/dist/adapters/index.d.ts +15 -0
- package/dist/adapters/yugabytedb.js +20 -3
- package/dist/cjs/adapters/cockroachdb.js +21 -3
- package/dist/cjs/adapters/index.d.ts +15 -0
- package/dist/cjs/adapters/yugabytedb.js +20 -3
- package/dist/cjs/cli/destructive.d.ts +18 -4
- package/dist/cjs/cli/destructive.js +230 -122
- package/dist/cjs/cli/index.d.ts +21 -4
- package/dist/cjs/cli/index.js +119 -22
- package/dist/cjs/cli/mcp.d.ts +28 -8
- package/dist/cjs/cli/mcp.js +170 -127
- package/dist/cjs/cli/migrate.d.ts +134 -13
- package/dist/cjs/cli/migrate.js +349 -241
- package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cjs/cli/pii-predicate-guard.js +390 -0
- package/dist/cjs/cli/prisma-resolve.js +75 -4
- package/dist/cjs/cli/prisma-schema.d.ts +17 -1
- package/dist/cjs/cli/prisma-schema.js +83 -17
- package/dist/cjs/cli/sql-statements.d.ts +125 -0
- package/dist/cjs/cli/sql-statements.js +378 -0
- package/dist/cjs/cli/studio.js +49 -118
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +43 -0
- package/dist/cjs/client.js +125 -6
- package/dist/cjs/dialect.d.ts +123 -0
- package/dist/cjs/dialect.js +33 -0
- package/dist/cjs/errors.d.ts +74 -1
- package/dist/cjs/errors.js +239 -25
- package/dist/cjs/index-advisor.d.ts +33 -1
- package/dist/cjs/index-advisor.js +32 -1
- package/dist/cjs/introspect.d.ts +48 -0
- package/dist/cjs/introspect.js +222 -91
- package/dist/cjs/mssql.js +43 -1
- package/dist/cjs/mysql.d.ts +5 -2
- package/dist/cjs/mysql.js +202 -17
- package/dist/cjs/nested-write.js +6 -1
- package/dist/cjs/pipeline-submittable.js +17 -3
- package/dist/cjs/pipeline.js +75 -9
- package/dist/cjs/powdb.d.ts +23 -0
- package/dist/cjs/powdb.js +33 -1
- package/dist/cjs/powql.d.ts +61 -9
- package/dist/cjs/powql.js +186 -49
- package/dist/cjs/prisma-compat.js +160 -41
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +80 -18
- package/dist/cjs/query/batched-loader.d.ts +10 -0
- package/dist/cjs/query/batched-loader.js +268 -7
- package/dist/cjs/query/builder.d.ts +73 -0
- package/dist/cjs/query/builder.js +225 -28
- package/dist/cjs/query/filters.d.ts +162 -0
- package/dist/cjs/query/filters.js +250 -1
- package/dist/cjs/query/relations.d.ts +10 -10
- package/dist/cjs/query/relations.js +93 -12
- package/dist/cjs/query/types.d.ts +14 -1
- package/dist/cjs/query/utils.d.ts +146 -2
- package/dist/cjs/query/utils.js +210 -4
- package/dist/cjs/query/warn-registry.d.ts +10 -0
- package/dist/cjs/query/warn-registry.js +10 -0
- package/dist/cjs/query/where-compile.d.ts +30 -0
- package/dist/cjs/query/where-compile.js +41 -0
- package/dist/cjs/query/where.d.ts +128 -13
- package/dist/cjs/query/where.js +215 -77
- package/dist/cjs/query/writes.d.ts +1 -1
- package/dist/cjs/query/writes.js +39 -15
- package/dist/cjs/schema-builder.d.ts +2 -1
- package/dist/cjs/schema-sql.d.ts +94 -4
- package/dist/cjs/schema-sql.js +506 -30
- package/dist/cjs/schema.d.ts +3 -1
- package/dist/cjs/sqlite.d.ts +6 -0
- package/dist/cjs/sqlite.js +151 -10
- package/dist/cjs/typed-sql.d.ts +29 -1
- package/dist/cjs/typed-sql.js +30 -12
- package/dist/cli/destructive.d.ts +18 -4
- package/dist/cli/destructive.js +229 -121
- package/dist/cli/index.d.ts +21 -4
- package/dist/cli/index.js +120 -24
- package/dist/cli/mcp.d.ts +28 -8
- package/dist/cli/mcp.js +172 -129
- package/dist/cli/migrate.d.ts +134 -13
- package/dist/cli/migrate.js +347 -238
- package/dist/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cli/pii-predicate-guard.js +386 -0
- package/dist/cli/prisma-resolve.js +75 -4
- package/dist/cli/prisma-schema.d.ts +17 -1
- package/dist/cli/prisma-schema.js +83 -17
- package/dist/cli/sql-statements.d.ts +125 -0
- package/dist/cli/sql-statements.js +373 -0
- package/dist/cli/studio.js +49 -118
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +43 -0
- package/dist/client.js +126 -7
- package/dist/dialect.d.ts +123 -0
- package/dist/dialect.js +33 -0
- package/dist/errors.d.ts +74 -1
- package/dist/errors.js +228 -19
- package/dist/index-advisor.d.ts +33 -1
- package/dist/index-advisor.js +31 -1
- package/dist/introspect.d.ts +48 -0
- package/dist/introspect.js +221 -91
- package/dist/mssql.js +44 -2
- package/dist/mysql.d.ts +5 -2
- package/dist/mysql.js +203 -18
- package/dist/nested-write.js +7 -2
- package/dist/pipeline-submittable.js +18 -4
- package/dist/pipeline.js +76 -10
- package/dist/powdb.d.ts +23 -0
- package/dist/powdb.js +33 -2
- package/dist/powql.d.ts +61 -9
- package/dist/powql.js +187 -50
- package/dist/prisma-compat.js +160 -41
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +82 -20
- package/dist/query/batched-loader.d.ts +10 -0
- package/dist/query/batched-loader.js +270 -9
- package/dist/query/builder.d.ts +73 -0
- package/dist/query/builder.js +226 -30
- package/dist/query/filters.d.ts +162 -0
- package/dist/query/filters.js +246 -1
- package/dist/query/relations.d.ts +10 -10
- package/dist/query/relations.js +94 -14
- package/dist/query/types.d.ts +14 -1
- package/dist/query/utils.d.ts +146 -2
- package/dist/query/utils.js +204 -3
- package/dist/query/warn-registry.d.ts +10 -0
- package/dist/query/warn-registry.js +10 -0
- package/dist/query/where-compile.d.ts +30 -0
- package/dist/query/where-compile.js +40 -1
- package/dist/query/where.d.ts +128 -13
- package/dist/query/where.js +216 -80
- package/dist/query/writes.d.ts +1 -1
- package/dist/query/writes.js +40 -16
- package/dist/schema-builder.d.ts +2 -1
- package/dist/schema-sql.d.ts +94 -4
- package/dist/schema-sql.js +505 -30
- package/dist/schema.d.ts +3 -1
- package/dist/sqlite.d.ts +6 -0
- package/dist/sqlite.js +151 -10
- package/dist/typed-sql.d.ts +29 -1
- package/dist/typed-sql.js +30 -12
- 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 **
|
|
34
|
-
- **Real 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-
|
|
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-
|
|
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
|
+
| Scenario | Turbine 0.66 | Prisma 7.9 | Drizzle 0.45 | raw pg |
|
|
51
51
|
|---|---|---|---|---|
|
|
52
|
-
| findMany, 100 users (flat) | **0.
|
|
53
|
-
| findMany, 50 users + posts (L2) | 2.
|
|
54
|
-
| findMany, 10 users → posts → comments (L3)
|
|
55
|
-
| findUnique, single user by PK | **0.
|
|
56
|
-
| findUnique, user + posts + comments (L3) | **0.
|
|
57
|
-
| count, all users | **0.
|
|
58
|
-
| stream, iterate 50K rows (batch 1000) |
|
|
59
|
-
| atomic increment, `view_count + 1` *(
|
|
60
|
-
| pipeline, 5-query batch | **0.
|
|
61
|
-
| hot findUnique, 500x same shape | **0.029 ms** | 0.
|
|
62
|
-
|
|
63
|
-
**The number worth quoting from that run: Turbine ran at 1.
|
|
64
|
-
|
|
65
|
-
- **Turbine takes seven scenarios**, Drizzle three, Prisma none. Prisma is behind Turbine on all ten.
|
|
66
|
-
- **
|
|
67
|
-
- **Drizzle
|
|
68
|
-
- **
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
139
|
+
// Contention (see LOCK_CONTENTION_CODES) means somebody else is migrating.
|
|
122
140
|
const pgErr = err;
|
|
123
|
-
if (pgErr.code
|
|
141
|
+
if (LOCK_CONTENTION_CODES.has(String(pgErr.code))) {
|
|
124
142
|
try {
|
|
125
143
|
await client.query('ROLLBACK');
|
|
126
144
|
}
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -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
|
-
//
|
|
114
|
-
if (pgErr.code
|
|
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
|
-
//
|
|
142
|
+
// Contention (see LOCK_CONTENTION_CODES) means somebody else is migrating.
|
|
125
143
|
const pgErr = err;
|
|
126
|
-
if (pgErr.code
|
|
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
|
-
//
|
|
117
|
-
if (pgErr.code
|
|
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,
|
|
22
|
-
*
|
|
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
|
-
/**
|
|
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>";
|