turbine-orm 0.49.0 → 0.50.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 (157) hide show
  1. package/README.md +122 -39
  2. package/dist/cjs/adapters/cockroachdb.d.ts +39 -0
  3. package/dist/cjs/adapters/index.d.ts +110 -0
  4. package/dist/cjs/adapters/yugabytedb.d.ts +51 -0
  5. package/dist/cjs/cli/config.d.ts +181 -0
  6. package/dist/cjs/cli/config.js +32 -6
  7. package/dist/cjs/cli/destructive.d.ts +38 -0
  8. package/dist/cjs/cli/index.d.ts +359 -0
  9. package/dist/cjs/cli/index.js +228 -56
  10. package/dist/cjs/cli/loader.d.ts +61 -0
  11. package/dist/cjs/cli/mcp.d.ts +42 -0
  12. package/dist/cjs/cli/migrate.d.ts +356 -0
  13. package/dist/cjs/cli/migrate.js +131 -40
  14. package/dist/cjs/cli/observe-ui.d.ts +1 -0
  15. package/dist/cjs/cli/observe-ui.js +14 -5
  16. package/dist/cjs/cli/observe.d.ts +25 -0
  17. package/dist/cjs/cli/observe.js +49 -12
  18. package/dist/cjs/cli/pii-tags.d.ts +53 -0
  19. package/dist/cjs/cli/prisma-report.d.ts +33 -0
  20. package/dist/cjs/cli/prisma-report.js +73 -0
  21. package/dist/cjs/cli/prisma-resolve.d.ts +106 -0
  22. package/dist/cjs/cli/prisma-resolve.js +1 -0
  23. package/dist/cjs/cli/prisma-schema.d.ts +176 -0
  24. package/dist/cjs/cli/prisma-schema.js +82 -4
  25. package/dist/cjs/cli/rate-limit.d.ts +32 -0
  26. package/dist/cjs/cli/rate-limit.js +45 -0
  27. package/dist/cjs/cli/studio-demo.d.ts +43 -0
  28. package/dist/cjs/cli/studio-ui.generated.d.ts +1 -0
  29. package/dist/cjs/cli/studio.d.ts +207 -0
  30. package/dist/cjs/cli/studio.js +136 -71
  31. package/dist/cjs/cli/ui.d.ts +73 -0
  32. package/dist/cjs/cli/ui.js +51 -9
  33. package/dist/cjs/client.d.ts +837 -0
  34. package/dist/cjs/client.js +3 -0
  35. package/dist/cjs/dialect.d.ts +516 -0
  36. package/dist/cjs/dialect.js +37 -12
  37. package/dist/cjs/errors.d.ts +370 -0
  38. package/dist/cjs/generate.d.ts +137 -0
  39. package/dist/cjs/generate.js +39 -6
  40. package/dist/cjs/index-advisor.d.ts +153 -0
  41. package/dist/cjs/index-stats.d.ts +384 -0
  42. package/dist/cjs/index.d.ts +55 -0
  43. package/dist/cjs/index.js +7 -2
  44. package/dist/cjs/introspect.d.ts +269 -0
  45. package/dist/cjs/mssql.d.ts +232 -0
  46. package/dist/cjs/mssql.js +6 -0
  47. package/dist/cjs/mysql.d.ts +173 -0
  48. package/dist/cjs/mysql.js +16 -0
  49. package/dist/cjs/nested-write.d.ts +96 -0
  50. package/dist/cjs/nested-write.js +414 -24
  51. package/dist/cjs/observe.d.ts +115 -0
  52. package/dist/cjs/optional-peer-import.d.cts +72 -0
  53. package/dist/cjs/pipeline-submittable.d.ts +93 -0
  54. package/dist/cjs/pipeline.d.ts +71 -0
  55. package/dist/cjs/powdb-introspect.d.ts +84 -0
  56. package/dist/cjs/powdb.d.ts +931 -0
  57. package/dist/cjs/powdb.js +106 -21
  58. package/dist/cjs/powql.d.ts +592 -0
  59. package/dist/cjs/powql.js +42 -6
  60. package/dist/cjs/prisma-compat.d.ts +283 -0
  61. package/dist/cjs/prisma-compat.js +167 -9
  62. package/dist/cjs/query/aggregates.d.ts +92 -0
  63. package/dist/cjs/query/aggregates.js +7 -3
  64. package/dist/cjs/query/batched-loader.d.ts +193 -0
  65. package/dist/cjs/query/builder.d.ts +849 -0
  66. package/dist/cjs/query/builder.js +571 -65
  67. package/dist/cjs/query/compound-unique.d.ts +51 -0
  68. package/dist/cjs/query/deferred.d.ts +223 -0
  69. package/dist/cjs/query/filters.d.ts +201 -0
  70. package/dist/cjs/query/index.d.ts +14 -0
  71. package/dist/cjs/query/index.js +6 -1
  72. package/dist/cjs/query/relations.d.ts +609 -0
  73. package/dist/cjs/query/relations.js +693 -46
  74. package/dist/cjs/query/types.d.ts +1300 -0
  75. package/dist/cjs/query/utils.d.ts +209 -0
  76. package/dist/cjs/query/utils.js +208 -1
  77. package/dist/cjs/query/warn-registry.d.ts +68 -0
  78. package/dist/cjs/query/warn-registry.js +9 -0
  79. package/dist/cjs/query/where-compile.d.ts +139 -0
  80. package/dist/cjs/query/where.d.ts +548 -0
  81. package/dist/cjs/query/where.js +58 -22
  82. package/dist/cjs/query/writes.d.ts +172 -0
  83. package/dist/cjs/query/writes.js +105 -12
  84. package/dist/cjs/realtime.d.ts +70 -0
  85. package/dist/cjs/schema-builder.d.ts +354 -0
  86. package/dist/cjs/schema-metadata.d.ts +83 -0
  87. package/dist/cjs/schema-sql.d.ts +217 -0
  88. package/dist/cjs/schema-sql.js +23 -5
  89. package/dist/cjs/schema.d.ts +356 -0
  90. package/dist/cjs/schema.js +125 -0
  91. package/dist/cjs/seed.d.ts +15 -0
  92. package/dist/cjs/serverless.d.ts +142 -0
  93. package/dist/cjs/sqlite.d.ts +143 -0
  94. package/dist/cjs/sqlite.js +4 -0
  95. package/dist/cjs/typed-sql.d.ts +102 -0
  96. package/dist/cli/config.d.ts +18 -4
  97. package/dist/cli/config.js +31 -6
  98. package/dist/cli/index.d.ts +123 -0
  99. package/dist/cli/index.js +223 -58
  100. package/dist/cli/migrate.d.ts +59 -10
  101. package/dist/cli/migrate.js +128 -41
  102. package/dist/cli/observe-ui.d.ts +1 -1
  103. package/dist/cli/observe-ui.js +14 -5
  104. package/dist/cli/observe.d.ts +7 -1
  105. package/dist/cli/observe.js +48 -12
  106. package/dist/cli/prisma-report.d.ts +14 -0
  107. package/dist/cli/prisma-report.js +72 -0
  108. package/dist/cli/prisma-resolve.d.ts +6 -0
  109. package/dist/cli/prisma-resolve.js +1 -0
  110. package/dist/cli/prisma-schema.d.ts +62 -2
  111. package/dist/cli/prisma-schema.js +81 -4
  112. package/dist/cli/rate-limit.d.ts +32 -0
  113. package/dist/cli/rate-limit.js +40 -0
  114. package/dist/cli/studio.d.ts +5 -5
  115. package/dist/cli/studio.js +135 -70
  116. package/dist/cli/ui.d.ts +1 -1
  117. package/dist/cli/ui.js +51 -9
  118. package/dist/client.d.ts +40 -0
  119. package/dist/client.js +3 -0
  120. package/dist/dialect.d.ts +17 -1
  121. package/dist/dialect.js +37 -12
  122. package/dist/generate.js +40 -7
  123. package/dist/index.d.ts +1 -1
  124. package/dist/index.js +1 -1
  125. package/dist/mssql.js +6 -0
  126. package/dist/mysql.js +16 -0
  127. package/dist/nested-write.d.ts +2 -0
  128. package/dist/nested-write.js +415 -25
  129. package/dist/powdb.d.ts +4 -2
  130. package/dist/powdb.js +106 -21
  131. package/dist/powql.d.ts +5 -0
  132. package/dist/powql.js +42 -6
  133. package/dist/prisma-compat.d.ts +2 -0
  134. package/dist/prisma-compat.js +166 -8
  135. package/dist/query/aggregates.js +7 -3
  136. package/dist/query/builder.d.ts +292 -21
  137. package/dist/query/builder.js +570 -64
  138. package/dist/query/deferred.d.ts +39 -0
  139. package/dist/query/index.d.ts +1 -1
  140. package/dist/query/index.js +1 -1
  141. package/dist/query/relations.d.ts +173 -5
  142. package/dist/query/relations.js +688 -47
  143. package/dist/query/types.d.ts +123 -39
  144. package/dist/query/utils.d.ts +116 -0
  145. package/dist/query/utils.js +198 -0
  146. package/dist/query/warn-registry.d.ts +9 -0
  147. package/dist/query/warn-registry.js +9 -0
  148. package/dist/query/where.d.ts +38 -1
  149. package/dist/query/where.js +58 -23
  150. package/dist/query/writes.d.ts +42 -1
  151. package/dist/query/writes.js +104 -13
  152. package/dist/schema-sql.d.ts +14 -0
  153. package/dist/schema-sql.js +23 -5
  154. package/dist/schema.d.ts +38 -0
  155. package/dist/schema.js +123 -0
  156. package/dist/sqlite.js +4 -0
  157. package/package.json +77 -28
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # turbine-orm
2
2
 
3
- The Postgres ORM your DBA will sign off on. A read-only-by-default Studio, PII-tagged columns that stay out of results until asked for, errors that never leak data, one dependency, and checksummed migrations.
3
+ **The Postgres ORM that assumes your database has real data in it.**
4
+
5
+ Most query layers are designed for the shape of a laptop database: empty, disposable, nobody's. Turbine is designed for the same schema six months later, when it is holding customer records. The database UI you point at it is read-only until you say otherwise. The columns you tagged as personal data stay out of query results, out of logs, and out of aggregates. The operations that can lose data make you say so out loud before they run.
4
6
 
5
7
  ```
6
8
  npm install turbine-orm
@@ -10,16 +12,22 @@ npm install turbine-orm
10
12
 
11
13
  ## Why Turbine?
12
14
 
13
- Every TS ORM now resolves nested relations in a single `json_agg` query Prisma 7 and Drizzle both ship it, and so does Turbine. That part is table stakes. The reason to reach for Turbine is the **safety bundle**: the boxes a DBA ticks before a query layer goes anywhere near production. It's the only TypeScript ORM that ships all six of these together:
15
+ First, what is **not** a reason. Resolving a nested `with` clause in one statement is table stakes in 2026: Drizzle has compiled relational queries to `LEFT JOIN LATERAL` plus JSON aggregation since 0.28, Prisma does the same under its `relationJoins` preview flag, and Kysely ships `jsonArrayFrom` / `jsonObjectFrom` helpers for it. Turbine does it too, it does it well, and it is documented under [How It Works](#how-it-works) as a correctness detail rather than a headline.
16
+
17
+ The reason to reach for Turbine is that every layer between you and a production database is built on one assumption: **the rows are real**. That plays out in five concrete places.
18
+
19
+ 1. **The database UI is read-only, and writes are a per-launch decision.** `npx turbine studio` binds loopback, authenticates with a 192-bit per-process token, and runs every read inside `BEGIN READ ONLY`. In the default mode the write endpoints do not exist in the router at all (they 404), so there is nothing to bypass. `--write` opts a single launch in to edits, each addressed by its full primary key rather than a predicate, compiled by the same validated builder your app uses. There is no raw-SQL surface at all since v0.19.
20
+ 2. **PII is a schema contract, enforced in the SQL.** Tag a column `pii: true` and it is excluded from every default projection on every engine: top-level rows, `with` subqueries, batched loaders, write returns, and the Studio UI. It is also **refused** as a `groupBy` key and as a `_min` / `_max` target, because both hand back a stored cell. `includePii: true` unlocks it explicitly per read. A schema with no tagged column emits byte-identical SQL.
21
+ 3. **Errors carry keys, never values.** A `NotFoundError` says `where: { id, email }`. A `UniqueConstraintError` names the column that conflicted. Neither prints the user's data, so the error is safe to send straight to Sentry with no scrubbing rule in front of it. The full `where` object stays available as `err.where` in code.
22
+ 4. **Data-destroying statements need consent.** `migrate up`, `migrate down` and `push` scan for `DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, unqualified `DELETE` / `UPDATE`, and `ALTER COLUMN … TYPE`, print an itemized report, and refuse to run. Interactively you type `destroy my data` and then `yes`; in CI you pass `--allow-destructive`. A refused batch applies nothing.
23
+ 5. **The review a DBA would have given you, offline.** `npx turbine doctor` derives every column set the ORM's relation subqueries probe and reports the ones with no covering index, with a cost tier per finding. `--fix` writes the migration. No cloud service, no telemetry, no account: it reads your introspected schema.
14
24
 
15
- 1. **Read-only-by-default Studio your DBA will approve.** `npx turbine studio` spins up a loopback-bound web UI with 192-bit auth tokens, `BEGIN READ ONLY` transactions, and (since v0.19) no raw-SQL surface at all: queries are composed in the ORM's own validated builder. In the default mode the write endpoints do not exist and every transaction is read-only at the database level; edits require an explicit `--write` opt-in per launch, and every edit is addressed by its full primary key (never a predicate).
16
- 2. **PII-safe error messages.** Turbine errors show WHERE keys, not values. A `UniqueConstraintError` says which column violated the constraint — never the actual user data. Safe to log, safe to surface to monitoring, no scrubbing needed.
17
- 3. **One runtime dependency (`pg`).** No engine binary, no WASM, no adapter packages to keep in lockstep. The main entry's **import graph** is ~59 kB brotli (edge ~44 kB) with `pg` external (that is the client footprint your bundler sees, not the dual ESM+CJS install size on disk, ~3 MB). 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.
18
- 4. **SQL-first migrations with drift detection.** Write real SQL. SHA-256 checksums catch modified migration files. `pg_try_advisory_lock()` prevents concurrent runs. Each migration in its own transaction. No shadow database, no magic DSL.
19
- 5. **Edge-native — one import swap.** `turbineHttp(pool, SCHEMA)` — same API on Neon, Vercel Postgres, Cloudflare Hyperdrive, Supabase. No WASM bundle, no adapter package, no separate serverless build.
20
- 6. **Pipeline batching via wire protocol.** Real Parse/Bind/Execute pipeline — not queries wrapped in a transaction. N independent queries in one round-trip.
25
+ **On "only".** Each of these is checkable, so here is the checkable version, current as of July 2026: no other TypeScript ORM ships a studio that is read-only by default or that redacts PII (Prisma Studio is proprietary and its read-only request has been open since February 2021; Drizzle Studio is not open source and self-hosting runs through the paid Drizzle Gateway; TypeORM, MikroORM, Kysely and Sequelize have no studio). No TypeScript ORM CLI offers missing-index advice: not the Prisma CLI, drizzle-kit, kysely-ctl, or the MikroORM / TypeORM / Sequelize CLIs, and Prisma Optimize was retired in March 2026 in favour of cloud-only Query Insights. Prior art exists outside TypeScript, notably Ruby's `active_record_doctor`, so the honest claim is "no TypeScript ORM", not "no ORM". Turbine is the only TypeScript ORM that ships all five of the above together.
21
26
 
22
- See [How It Works](#how-it-works) for the `json_agg` query strategy itself, but the query strategy isn't why you'd pick Turbine. The safety bundle above is: a Studio that is read-only unless you explicitly opt in to writes, PII columns that stay out of results until asked for, errors that never leak data, one dependency, and checksummed migrations.
27
+ Two more things worth knowing, which are about cost rather than safety:
28
+
29
+ - **One runtime dependency (`pg`).** No engine binary, no WASM, no adapter packages in lockstep. The main entry's **import graph** is ~65 kB brotli (edge ~50 kB) with `pg` external (measured 2026-07-25 on 0.50.0: 65.16 kB and 50.18 kB, budgeted in `.size-limit.js`). 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.
30
+ - **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.
23
31
 
24
32
  **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).
25
33
 
@@ -27,34 +35,38 @@ Per-release detail lives in the [CHANGELOG](./CHANGELOG.md) and at [turbineorm.d
27
35
 
28
36
  ## Benchmarks
29
37
 
30
- Tested against **Prisma 7.9.0** (`@prisma/adapter-pg`) and **Drizzle 0.45.2** (relational queries) on a **local PostgreSQL 17.9** database over a Unix socket. 200 iterations, 20 warmup, Node v24.18.0. Same schema, same data (1K users, 10K posts, 50K comments), same connection pool config. _Measured 2026-07-21 on turbine-orm 0.39.0 (Apple Silicon MacBook Pro, macOS); the harness has not been re-run since, so these are not 0.48.0 numbers. A local socket has no network round-trip, so these numbers are sub-millisecond and are **not** comparable to the earlier pooled-Neon table: they isolate per-query overhead instead of hiding it behind ~35 ms of network latency. See [`benchmarks/RESULTS.md`](./benchmarks/RESULTS.md) to reproduce._
38
+ 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.
39
+
40
+ 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.
31
41
 
32
- Prisma's nested scenarios run on its **`join`** load strategy: `benchmarks/prisma/schema.prisma` enables the `relationJoins` preview feature, which (per Prisma's docs) makes `join` the client-wide default, and the harness never overrides `relationLoadStrategy` on a query. Without that flag Prisma would fall back to its per-relation `query` strategy and lose the nested scenarios by a wider margin.
42
+ Prisma's nested scenarios run on its **`join`** load strategy, which is its favorable configuration and is chosen deliberately.
33
43
 
34
- | Scenario | Turbine | Prisma 7.9 | Drizzle 0.45 |
35
- |---|---|---|---|
36
- | findMany, 100 users (flat) | **0.29 ms** | 0.37 ms | 0.39 ms |
37
- | findMany, 50 users + posts (L2) | 2.86 ms | 4.64 ms | **2.39 ms** |
38
- | findMany, 10 users → posts → comments (L3) *(near-tie)* | 1.55 ms | 4.04 ms | **1.32 ms** |
39
- | findUnique, single user by PK | **0.06 ms** | 0.12 ms | 0.10 ms |
40
- | findUnique, user + posts + comments (L3) | **0.18 ms** | 0.45 ms | 0.31 ms |
41
- | count, all users *(near-tie)* | **0.05 ms** | 0.08 ms | 0.06 ms |
42
- | stream, iterate 50K rows (batch 1000) *(near-tie)* | **60.7 ms** | 68.8 ms | 65.8 ms |
43
- | atomic increment, `view_count + 1` *(near-tie)* | **0.14 ms** | 0.19 ms | 0.19 ms |
44
- | pipeline, 5-query batch | **0.20 ms** | 0.45 ms | 0.41 ms |
45
- | hot findUnique, 500x same shape | **0.03 ms** | 0.06 ms | 0.08 ms |
44
+ | Scenario | Turbine 0.50 | Prisma 7.9 | Drizzle 0.45 | raw pg |
45
+ |---|---|---|---|---|
46
+ | findMany, 100 users (flat) | **0.256 ms** | 0.358 ms | 0.330 ms | 0.210 ms |
47
+ | findMany, 50 users + posts (L2) | 2.421 ms | 4.603 ms | **2.001 ms** | 1.953 ms |
48
+ | findMany, 10 users → posts → comments (L3) *(near-tie)* | 1.255 ms | 3.735 ms | **1.214 ms** | n/a |
49
+ | findUnique, single user by PK | **0.051 ms** | 0.105 ms | 0.108 ms | 0.060 ms |
50
+ | findUnique, user + posts + comments (L3) | **0.217 ms** | 0.469 ms | 0.357 ms | n/a |
51
+ | count, all users | **0.044 ms** | 0.081 ms | 0.061 ms | 0.045 ms |
52
+ | stream, iterate 50K rows (batch 1000) | 63.87 ms | 71.08 ms | **50.18 ms** | 50.97 ms |
53
+ | atomic increment, `view_count + 1` *(near-tie)* | **0.115 ms** | 0.174 ms | 0.123 ms | 0.095 ms |
54
+ | pipeline, 5-query batch | **0.206 ms** | 0.431 ms | 0.402 ms | 0.205 ms |
55
+ | hot findUnique, 500x same shape | **0.029 ms** | 0.065 ms | 0.075 ms | 0.033 ms |
56
+
57
+ **The number worth quoting: Turbine runs at 1.07x hand-written `pg`, where Drizzle runs at 1.47x and Prisma at 1.84x** (geometric mean over the eight scenarios with a raw control). Across all ten scenarios Turbine is **1.87x faster than Prisma 7.9** and **1.36x faster than Drizzle 0.45** by geometric mean.
46
58
 
47
- **Over a local socket the network floor disappears, so per-query overhead becomes the whole signal.** The picture that emerges across two full runs:
59
+ - **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.
60
+ - **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.
61
+ - **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.
62
+ - **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.
48
63
 
49
- - **Turbine leads flat reads, both findUnique shapes, pipeline, and the hot path.** SQL template caching and prepared statements keep its per-call overhead lowest on simple and repeated-shape queries, and its real Postgres pipeline protocol (one TCP flush for 5 queries) runs the dashboard batch ~2x faster than Prisma's or Drizzle's sequential transaction.
50
- - **Drizzle leads nested reads (L2).** Its relational query builder emits tighter SQL for the posts/comments joins. Turbine's `json_agg` nesting is close behind and still 1.6x to 2.6x ahead of Prisma on the same L2/L3 shapes on this run.
51
- - **Four scenarios are near-ties.** L3 nested, count, streaming, and atomic increment each flipped winner between the two runs. Treat them as within noise on this host rather than a lead for either side.
52
- - **Prisma trails Turbine on every scenario here.** It edges out Drizzle on the flat read and ties it on the atomic increment, but is behind Turbine on all ten. Its engine-less client's per-query work is no longer masked by network latency; on a pooled remote database (the regime we measured previously) these same deltas compress back into the noise floor. Prisma 7.9 is a real improvement on 7.6 (flat reads ~30% faster, the pipeline batch ~26%).
64
+ > **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`](./benchmarks/RESULTS-0.50.0.md).
53
65
 
54
- Net: on a local socket Turbine takes five scenarios outright, loses L2 to Drizzle, and trades four more run-to-run. It is competitive-to-ahead across the board rather than a clean sweep, and the honest takeaway is unchanged: 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.
66
+ Net: Turbine is competitive-to-ahead across the board rather than a clean sweep, and the honest takeaway is unchanged: 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.
55
67
 
56
- > Full analysis with p50/p95/p99 and methodology notes: [`benchmarks/RESULTS.md`](./benchmarks/RESULTS.md).
57
- > Reproduce: `cd benchmarks && npm install && npx prisma generate && DATABASE_URL=... npx tsx bench.ts`
68
+ > Full analysis, methodology and the drift floor: [`benchmarks/RESULTS-0.50.0.md`](./benchmarks/RESULTS-0.50.0.md). Historical runs: [`benchmarks/RESULTS.md`](./benchmarks/RESULTS.md).
69
+ > Reproduce: `cd benchmarks && npm install && npx prisma generate && DATABASE_URL=... npx tsx bench-interleaved.ts`
58
70
 
59
71
  ## Quick Start
60
72
 
@@ -76,12 +88,14 @@ The `turbine-orm` package ships real dual builds, so importing the package works
76
88
 
77
89
  ```typescript
78
90
  // ESM
79
- import { turbine } from 'turbine-orm';
91
+ import { TurbineClient, defineSchema } from 'turbine-orm';
80
92
 
81
93
  // CommonJS
82
- const { turbine } = require('turbine-orm');
94
+ const { TurbineClient, defineSchema } = require('turbine-orm');
83
95
  ```
84
96
 
97
+ > **`turbine` is not exported by the package.** The `turbine()` factory is emitted into the *generated* client, because it is typed against your schema. Import it from your output directory (`./generated/turbine`), never from `'turbine-orm'`. From the package itself you get the untyped-but-generic `TurbineClient`, which takes a `SchemaMetadata` as its second argument.
98
+
85
99
  The generated client (`./generated/turbine/`) is TypeScript source: it re-exports across files with ESM-style `./metadata.js` specifiers, so you consume it through your bundler, `tsx`, or `tsc` like the rest of your app:
86
100
 
87
101
  ```typescript
@@ -171,6 +185,24 @@ export default defineSchema({
171
185
 
172
186
  `sourceKey`/`targetKey` are the junction columns referencing each side's primary key; add `references` if the source side is keyed on something other than `id`.
173
187
 
188
+ Since 0.50, a many-to-many relation also takes **`connect`, `disconnect` and `set`** as nested writes, so link rows are written for you inside the write's transaction:
189
+
190
+ ```typescript
191
+ // link (idempotent: an existing link is left alone, never duplicated)
192
+ await db.posts.update({ where: { id: 1 }, data: { tags: { connect: [{ id: 7 }, { id: 9 }] } } });
193
+
194
+ // unlink exactly those two, and nothing else
195
+ await db.posts.update({ where: { id: 1 }, data: { tags: { disconnect: [{ id: 7 }] } } });
196
+
197
+ // replace the whole link set (`set: []` clears it)
198
+ await db.posts.update({ where: { id: 1 }, data: { tags: { set: [{ id: 3 }] } } });
199
+
200
+ // on create, `connect` links after the parent row exists
201
+ await db.posts.create({ data: { title: 'hi', tags: { connect: [{ id: 3 }] } } });
202
+ ```
203
+
204
+ The other nested operations (`create`, `connectOrCreate`, `update`, `upsert`, `delete`) would have to write the target row too, and there is no safe default for a junction's own extra columns, so they throw `ValidationError` (`TURBINE_E003`) naming the supported set. For those, write the target row on its own table and link it with `connect`, or write junction rows directly through the junction accessor, inside the same `$transaction`. Composite junction keys are refused for the same reason (nothing partially-keyed is emitted).
205
+
174
206
  ### Self-relations
175
207
 
176
208
  A self-referencing foreign key (e.g. `categories.parent_id → categories.id`) introspects to a `belongsTo` *and* a `hasMany` on the same table, so parent and child queries just work — including nested trees:
@@ -268,6 +300,8 @@ await db.$transaction(async (tx) => {
268
300
  // Fully typed -- tx.users and tx.posts have the same API as db.users and db.posts
269
301
  ```
270
302
 
303
+ `tx` is a `TransactionClient`, which is deliberately a **smaller** object than `db`. It has table accessors (`tx.users`, or `tx.table('users')`), `tx.$transaction(fn)` for SAVEPOINT-nested blocks, `tx.raw` for tagged-template raw SQL, and `tx.schema`. It does **not** have `tx.sql`, `tx.pipeline`, `tx.$use`, `tx.$listen` / `tx.$notify`, `tx.$observe`, `tx.$on` / `tx.$off`, `tx.$retry`, `tx.$primary`, `tx.$withSession`, `tx.pipelineSupported()`, `tx.transaction()`, `tx.connect()`, `tx.pool`, `tx.stats`, `tx.disconnect()` or `tx.end()`. `TransactionClient` declares exactly `table()`, `$transaction()`, `raw` and `schema` (plus the generated per-table accessors) and has no index signature, so reaching for any of the others is a **compile error** (`TS2339`), caught in your editor rather than in production. See [Transactions & Pipelines](https://turbineorm.dev/transactions#transactionclient-reference) for the full surface and the reason each omission exists.
304
+
271
305
  ### Pipeline (batch queries in one round-trip)
272
306
 
273
307
  ```typescript
@@ -384,6 +418,53 @@ const db = turbine({
384
418
 
385
419
  Run `npx turbine doctor` to catch relations whose child-side FK lacks a covering index — the correlated-subquery strategy probes the child once per parent row, so a missing FK index costs a full scan per parent.
386
420
 
421
+ ### Pool and statement configuration
422
+
423
+ ```typescript
424
+ const db = turbine({
425
+ connectionString: process.env.DATABASE_URL,
426
+ poolSize: 10, // max pooled connections (default 10; pg alias: max)
427
+ idleTimeoutMs: 30_000, // close an idle connection after this (pg alias: idleTimeoutMillis)
428
+ connectionTimeoutMs: 5_000, // give up acquiring a connection after this (pg alias: connectionTimeoutMillis)
429
+ preparedStatements: true, // see the warning below
430
+ sqlCache: true, // SQL template cache (default true)
431
+ sqlCacheSize: 1000, // distinct query SHAPES retained per table (default 1000)
432
+ });
433
+ ```
434
+
435
+ Where a pg-style alias exists (`max`, `idleTimeoutMillis`, `connectionTimeoutMillis`), the explicit Turbine field wins when both are set.
436
+
437
+ > **`preparedStatements` and connection poolers.** With prepared statements on, Turbine submits queries as `{ name, text, values }` so Postgres caches the parse and plan **per backend connection**. That is a real win against a database you connect to directly, and a hazard behind a transaction-pooling proxy (PgBouncer in `transaction` mode, Supabase's pooler port, some serverless poolers): the named statement is prepared on one backend and your next query may land on another, which fails with `prepared statement "..." does not exist`. Turbine defaults it to `true` only for pools it creates itself and `false` for external pools passed via `pool` / `turbineHttp()`, because serverless drivers are the common case there. If you are pointing a Turbine-owned pool at a transaction pooler, set `preparedStatements: false`. The environment variable `TURBINE_DISABLE_PREPARED=1` turns it off globally without a code change.
438
+
439
+ ### Client escape hatches
440
+
441
+ | Member | Type | What it is for |
442
+ |---|---|---|
443
+ | `db.table<T>(name)` | `QueryInterface<T>` | Query a table by string name. This is the escape hatch for tables that are absent from your generated types: a table created after the last `generate`, a table reached over `turbineHttp(pool, SCHEMA)` where there is no generated subclass, or a name that is not a valid identifier. Pass `T` yourself to get typing back. The name is still validated against the schema metadata. |
444
+ | `db.pool` | `pg.Pool` | The underlying pool, for anything Turbine does not wrap. |
445
+ | `db.schema` | `SchemaMetadata` | The metadata the client was built from. |
446
+ | `db.stats` | `{ totalCount, idleCount, waitingCount }` | Pool gauges for a health endpoint. Returns zeros on drivers that do not expose counts (Neon HTTP). |
447
+ | `db.transaction(fn)` | raw `pg.PoolClient` | The pre-typed transaction API. Prefer `$transaction`; this exists for hand-written SQL that needs the same connection. |
448
+ | `db.end()` | `Promise<void>` | Alias for `disconnect()`. Both are a no-op for an external pool, because the caller owns its lifecycle. |
449
+ | `QueryInterface.cacheStats()` | `{ size, hits, misses, hitRate }` | Per-table SQL-template cache counters, e.g. `db.users.cacheStats()`. Useful to confirm a hot path is actually reusing a cached template rather than re-fingerprinting a new shape every call. |
450
+
451
+ ### Building queries without running them
452
+
453
+ Every query method has a `build*` twin that returns a `DeferredQuery` (`{ sql, params, transform, tag }`) instead of executing. That is what `pipeline()` and the array form of `$transaction()` consume, and it is not read-only: the write builders are batchable too.
454
+
455
+ ```typescript
456
+ // Reads and writes in ONE atomic batch, one connection, one BEGIN/COMMIT
457
+ const [order, _items, updated] = await db.$transaction([
458
+ db.orders.buildCreate({ data: { userId: 1, total: 4200 } }),
459
+ db.orderItems.buildCreateMany({ data: [{ orderId: 1, sku: 'A' }, { orderId: 1, sku: 'B' }] }),
460
+ db.users.buildUpdate({ where: { id: 1 }, data: { orderCount: { increment: 1 } } }),
461
+ ]);
462
+ ```
463
+
464
+ The full set: `buildFindMany`, `buildFindUnique`, `buildFindFirst`, `buildFindUniqueOrThrow`, `buildFindFirstOrThrow`, `buildCount`, `buildAggregate`, `buildGroupBy`, `buildCreate`, `buildCreateMany`, `buildUpdate`, `buildUpdateMany`, `buildUpsert`, `buildDelete`, `buildDeleteMany`.
465
+
466
+ 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.
467
+
387
468
  ### Middleware
388
469
 
389
470
  Middleware wraps every query. It runs **after SQL generation**, so it can observe what's about to execute (`params.model`, `params.action`, `params.args`), measure timing, and transform the result returned by `next()` — but it cannot change the query itself.
@@ -706,7 +787,7 @@ then `yes`; in CI you must pass `--allow-destructive`. A refused batch applies n
706
787
 
707
788
  ## Studio
708
789
 
709
- The only Postgres ORM with a Studio your DBA will approve. `turbine studio` launches a local web UI for exploring your database. It is **read-only by default** (no mutations, no writes, every transaction `BEGIN READ ONLY`) and since v0.19 has **no raw-SQL surface at all**: every query is composed visually in the ORM and compiled by the same validated query builder your application uses. Since v0.36, `--write` opts a launch in to primary-key-addressed insert/update/delete through that same validated builder (single rows, or a capped multi-select batch run in one all-or-nothing transaction since v0.38); without the flag the write endpoints do not exist.
790
+ `turbine studio` launches a local web UI for exploring your database. It is **read-only by default** (no mutations, no writes, every transaction `BEGIN READ ONLY`) and since v0.19 has **no raw-SQL surface at all**: every query is composed visually in the ORM and compiled by the same validated query builder your application uses. Since v0.36, `--write` opts a launch in to primary-key-addressed insert/update/delete through that same validated builder (single rows, or a capped multi-select batch run in one all-or-nothing transaction since v0.38); without the flag the write endpoints do not exist.
710
791
 
711
792
  ```bash
712
793
  DATABASE_URL=postgres://user:pass@localhost:5432/mydb npx turbine studio
@@ -942,7 +1023,7 @@ Everything is honest about what ports and what doesn't. Features marked **PG-onl
942
1023
 
943
1024
  **Engine notes:** SQLite uses `RETURNING` (≥ 3.35) just like Postgres. MySQL has no `RETURNING`, so writes re-`SELECT` the affected row and **`createMany` returns `[]`** (the rows ARE inserted — re-query if you need them). SQL Server returns rows via `OUTPUT`/`MERGE`; `DISTINCT ON` is Postgres-only. Only Postgres streams via a true cursor (constant memory); the other engines' `findManyStream` materializes the result then yields it in batches. Optimistic locking throws `OptimisticLockError` on all engines (on MySQL the conflict is detected from the version-checked UPDATE's affected-row count). The `turbine` CLI (`generate`, `migrate`) is currently PostgreSQL-only — point the engine factories at a hand-written or programmatically introspected `SCHEMA`.
944
1025
 
945
- **PowDB** speaks its own non-SQL query language (PowQL), so it sits outside the SQL matrix above. Writes use a trailing **`returning`** keyword (upsert reselects by PK). PKs are server-assigned `auto` ints **or** client UUIDs. Nested relations run as **one statement** on engine 0.18+ (PowQL nested projections — per-parent order/limit, childless parents kept, the same single-query shape as Postgres `json_agg`); older engines and ineligible shapes (many-to-many via the junction) load client-side with identical output. Nested writes cover hasMany/hasOne/belongsTo; many-to-many nested writes are not supported. Transactions are single-writer: concurrent `$transaction` calls queue FIFO (bounded by `transactionQueueTimeoutMs`); nested/re-entrant transactions throw typed errors (no savepoints). Schema is code-first via `defineSchema` — `schemaDefToMetadata()` bridges it to any engine that needs runtime metadata, and a programmatic `describe`-based introspector exists since 0.34 (relations excluded). JSON documents are first-class on engine 0.12+: `JsonFilter` where-filters, JSON-path `orderBy`/`groupBy`, doc-field expression indexes, and a lossless native wire (0.13+) that keeps JSON `null`, missing fields, and the string `"null"` distinct. Embedded `syncMode: 'normal'` moves fsync off the commit path; the networked transport runs the same data over a socket. Cursor streaming and the Postgres-only trio (pgvector / LISTEN/NOTIFY / RLS session GUCs) throw `UnsupportedFeatureError`. Full details: **[turbineorm.dev/engines#powdb](https://turbineorm.dev/engines#powdb)**.
1026
+ **PowDB** speaks its own non-SQL query language (PowQL), so it sits outside the SQL matrix above. Writes use a trailing **`returning`** keyword (upsert reselects by PK). PKs are server-assigned `auto` ints **or** client UUIDs. Nested relations run as **one statement** on engine 0.18+ (PowQL nested projections — per-parent order/limit, childless parents kept, the same single-query shape as Postgres `json_agg`); older engines and ineligible shapes (many-to-many via the junction) load client-side with identical output. Nested writes cover hasMany/hasOne/belongsTo, and route through the same shared nested-write engine as the SQL engines, so the many-to-many `connect` / `disconnect` / `set` junction writes added in 0.50 apply here too (they are ordinary reads and writes on the junction table); the remaining many-to-many operations throw `ValidationError`. Transactions are single-writer: concurrent `$transaction` calls queue FIFO (bounded by `transactionQueueTimeoutMs`); nested/re-entrant transactions throw typed errors (no savepoints). Schema is code-first via `defineSchema` — `schemaDefToMetadata()` bridges it to any engine that needs runtime metadata, and a programmatic `describe`-based introspector exists since 0.34 (relations excluded). JSON documents are first-class on engine 0.12+: `JsonFilter` where-filters, JSON-path `orderBy`/`groupBy`, doc-field expression indexes, and a lossless native wire (0.13+) that keeps JSON `null`, missing fields, and the string `"null"` distinct. Embedded `syncMode: 'normal'` moves fsync off the commit path; the networked transport runs the same data over a socket. Cursor streaming and the Postgres-only trio (pgvector / LISTEN/NOTIFY / RLS session GUCs) throw `UnsupportedFeatureError`. Full details: **[turbineorm.dev/engines#powdb](https://turbineorm.dev/engines#powdb)**.
946
1027
 
947
1028
  Full setup, signatures, and the complete support matrix: **[turbineorm.dev/engines](https://turbineorm.dev/engines)**.
948
1029
 
@@ -995,11 +1076,11 @@ Turbine maps Postgres types to TypeScript:
995
1076
  |---|---|---|---|---|
996
1077
  | **Engine / runtime** | No engine binary (`pg` only) | Client + TS/WASM query compiler | No engine | No engine |
997
1078
  | **Runtime deps** | 1 (`pg`) | `@prisma/client` + required driver adapter | 0 | 0 |
998
- | **Main bundle (brotli)** | ~59 kB | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small |
1079
+ | **Main bundle (brotli)** | ~65 kB import graph, `pg` external | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small |
999
1080
  | **Studio** | Read-only, 192-bit auth | Full CRUD, cloud-hosted | Free; hosted Gateway paid | None |
1000
1081
  | **Error PII safety** | Keys only by default | Values in messages | Raw pg errors | Raw pg errors |
1001
1082
  | **Migrations** | SQL-first, SHA-256 checksums | DSL-generated, shadow DB | SQL or Drizzle Kit | None |
1002
- | **Edge runtime** | One import swap, ~44 kB brotli | Driver adapter + WASM compiler | Native | Native |
1083
+ | **Edge runtime** | One import swap, ~50 kB brotli | Driver adapter + WASM compiler | Native | Native |
1003
1084
  | **Pipeline batching** | Parse/Bind/Execute protocol | Sequential in txn | Sequential | Manual |
1004
1085
  | **Typed errors** | `isRetryable` discriminant | Error codes only | None | None |
1005
1086
  | **Nested relations** | 1 query, deep type inference | 1 query, shallow inference | 1 query, `relations()` re-declaration | Manual (`jsonArrayFrom`) |
@@ -1008,7 +1089,9 @@ Turbine maps Postgres types to TypeScript:
1008
1089
  | **LISTEN/NOTIFY** | `$listen` / `$notify` | None | None | None |
1009
1090
  | **Multi-DB** | Postgres-first (+ SQLite/MySQL/MSSQL engines) | PG, MySQL, SQLite, MSSQL | PG, MySQL, SQLite | PG, MySQL, SQLite |
1010
1091
 
1011
- All three ORMs now do single-query nested loads — that's table stakes. Turbine's real differentiators: no engine binary or WASM just one dependency (`pg`), vs Prisma 7's ~1.6 MB TypeScript/WASM query compiler and required driver adapter; the only read-only-by-default Studio in the ecosystem; error messages that never leak PII; and SQL-first migrations with SHA-256 drift detection. See [Benchmarks](#benchmarks) for performance numbers most scenarios are within noise over a real pooled database.
1092
+ Reading the table: no engine binary and no WASM, just one runtime dependency (`pg`), against Prisma 7's ~1.6 MB TypeScript/WASM query compiler plus a required driver adapter; a Studio that is read-only by default, which as of July 2026 no other TypeScript ORM ships; error messages that never leak PII; and SQL-first migrations with SHA-256 drift detection. See [Benchmarks](#benchmarks) for performance numbers: most scenarios are within noise over a real pooled database.
1093
+
1094
+ *Competitor columns are re-verified against competitor releases on a fixed schedule. Last checked July 2026, against Prisma 7 and Drizzle 0.45. Features marked Preview or beta may change, and bundle sizes move release to release.*
1012
1095
 
1013
1096
  **A note on Kysely.** Kysely's [`jsonArrayFrom` / `jsonObjectFrom`](https://kysely.dev/docs/recipes/relations) relations recipe builds nested results with the same correlated-subquery-plus-JSON approach Turbine uses — good evidence the pattern is the right one. The gap is in what the driver can no longer see once rows are aggregated into JSON: nested fields lose their column types, so a `Date` inside a `jsonArrayFrom` result is typed `Date` but arrives as a **string** at runtime ([kysely-org/kysely#482](https://github.com/kysely-org/kysely/issues/482)), and the nesting isn't type-checked at depth. Turbine's `WithResult` inference types the whole tree, and `parseNestedRow` re-applies date coercion (and snake→camel mapping) to every nested row — so `users[0].posts[0].createdAt` is an actual `Date`, at any depth, with no plugin to wire up.
1014
1097
 
@@ -1051,7 +1134,7 @@ Turbine is focused and opinionated. Here's what it doesn't do:
1051
1134
  ## Requirements
1052
1135
 
1053
1136
  - Node.js >= 20.0.0
1054
- - PostgreSQL >= 14
1137
+ - PostgreSQL >= 14 (tested). CI runs the integration suite against PostgreSQL 14, 15, 16 and 17 on every change; 14 is the oldest version anything is verified on. Nothing in Turbine's introspection or query generation is known to require a feature newer than PostgreSQL 12, so older servers may well work, but they are untested and unsupported.
1055
1138
  - Works with both ESM (`import`) and CommonJS (`require`)
1056
1139
 
1057
1140
  ## Contributing
@@ -0,0 +1,39 @@
1
+ /**
2
+ * turbine-orm — CockroachDB adapter
3
+ *
4
+ * CockroachDB speaks the PostgreSQL wire protocol but has key differences:
5
+ *
6
+ * 1. **No advisory locks** — `pg_try_advisory_lock()` is not supported.
7
+ * This adapter uses a `_turbine_lock` table with `SELECT FOR UPDATE NOWAIT`
8
+ * as a concurrency mechanism for migrations.
9
+ *
10
+ * 2. **No `SET LOCAL statement_timeout`** — CockroachDB uses
11
+ * `SET transaction_timeout` (v23.1+) for per-transaction time limits.
12
+ *
13
+ * 3. **`pg_indexes` view** — CockroachDB supports `pg_indexes` since v22.1
14
+ * but the `indexdef` column may not match Postgres exactly. We use
15
+ * `SHOW INDEXES` as a more reliable alternative.
16
+ *
17
+ * 4. **`pg_class.reltuples`** — Not reliable in CockroachDB. We use
18
+ * `crdb_internal.table_row_statistics` for row estimates.
19
+ *
20
+ * Known limitations with Turbine on CockroachDB:
21
+ * - `json_agg` works but NULL ordering within aggregates may differ
22
+ * - `SERIAL` columns use `unique_rowid()` instead of sequences
23
+ * - Schema introspection via information_schema works for tables, columns,
24
+ * constraints; pg_catalog has gaps for some metadata
25
+ * - Pipeline batching works (extended query protocol is supported)
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { cockroachdb } from 'turbine-orm/adapters';
30
+ *
31
+ * // In turbine.config.ts:
32
+ * export default {
33
+ * url: process.env.DATABASE_URL,
34
+ * adapter: cockroachdb,
35
+ * };
36
+ * ```
37
+ */
38
+ import type { DatabaseAdapter } from './index.js';
39
+ export declare const cockroachdb: DatabaseAdapter;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * turbine-orm — Database adapter interface
3
+ *
4
+ * Adapters allow Turbine to work with PostgreSQL-compatible databases that
5
+ * have subtle differences (e.g. CockroachDB, YugabyteDB). The default
6
+ * behavior remains standard PostgreSQL — adapters only override specific
7
+ * operations where compatibility gaps exist.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { cockroachdb } from 'turbine-orm/adapters';
12
+ *
13
+ * // Pass to TurbineCliConfig or migration functions
14
+ * const config = { url: process.env.DATABASE_URL, adapter: cockroachdb };
15
+ * ```
16
+ */
17
+ import type { PgCompatPoolClient } from '../client.js';
18
+ /**
19
+ * Override individual introspection SQL queries for databases where
20
+ * pg_catalog or information_schema behaves differently.
21
+ */
22
+ export interface IntrospectionOverrides {
23
+ /** Override the pg_indexes query (CockroachDB uses crdb_internal or show indexes) */
24
+ indexes: string;
25
+ /** Override the pg_enum query */
26
+ enums: string;
27
+ /** Override the row count estimation query (pg_class reltuples) */
28
+ rowEstimates: string;
29
+ }
30
+ /**
31
+ * A DatabaseAdapter encapsulates dialect-specific behavior for databases
32
+ * that speak the PostgreSQL wire protocol but differ in implementation.
33
+ *
34
+ * All methods are optional except `name`. Turbine falls through to standard
35
+ * PostgreSQL behavior for any method not provided by the adapter.
36
+ */
37
+ export interface DatabaseAdapter {
38
+ /** Identifier for the adapter (e.g. 'postgresql', 'cockroachdb') */
39
+ readonly name: string;
40
+ /**
41
+ * Acquire a concurrency lock for migrations.
42
+ * PostgreSQL uses `pg_try_advisory_lock`. CockroachDB uses a lock table.
43
+ *
44
+ * @returns `true` if the lock was successfully acquired.
45
+ */
46
+ acquireLock(client: PgCompatPoolClient, lockId: number): Promise<boolean>;
47
+ /**
48
+ * Release the concurrency lock acquired by `acquireLock`.
49
+ */
50
+ releaseLock(client: PgCompatPoolClient, lockId: number): Promise<void>;
51
+ /**
52
+ * Optional overrides for introspection queries where the default
53
+ * information_schema/pg_catalog queries don't work.
54
+ */
55
+ introspectionOverrides?: Partial<IntrospectionOverrides>;
56
+ /**
57
+ * Generate the SQL to set a statement timeout within a transaction.
58
+ * PostgreSQL uses `SELECT set_config('statement_timeout', $1, true)`.
59
+ * CockroachDB uses `SELECT set_config('transaction_timeout', $1, true)` (v23.1+).
60
+ * (`SET LOCAL ... = $1` is a syntax error — SET takes no bind params.)
61
+ *
62
+ * @param seconds — timeout in seconds
63
+ * @returns an object with the parameterized SQL and its bound values
64
+ */
65
+ statementTimeout?(seconds: number): {
66
+ sql: string;
67
+ params: unknown[];
68
+ };
69
+ /**
70
+ * SQL to create the lock table used by table-based locking adapters.
71
+ * Called during `ensureTrackingTable` when the adapter uses table locks.
72
+ */
73
+ createLockTableSQL?(): string;
74
+ }
75
+ /**
76
+ * The default PostgreSQL adapter. Uses pg_try_advisory_lock, standard
77
+ * pg_catalog queries, and SET LOCAL statement_timeout.
78
+ */
79
+ export declare const postgresql: DatabaseAdapter;
80
+ /**
81
+ * Google AlloyDB adapter. AlloyDB is PostgreSQL with Google's columnar storage
82
+ * engine. It is wire-protocol and catalog-compatible — no adapter overrides
83
+ * are needed. All Turbine features (json_agg, advisory locks, introspection,
84
+ * migrations) work identically to standard PostgreSQL.
85
+ *
86
+ * This export exists for documentation and explicit configuration:
87
+ *
88
+ * ```ts
89
+ * import { alloydb } from 'turbine-orm/adapters';
90
+ * const config = { url: process.env.DATABASE_URL, adapter: alloydb };
91
+ * ```
92
+ */
93
+ export declare const alloydb: DatabaseAdapter;
94
+ /**
95
+ * TimescaleDB adapter. Timescale is a PostgreSQL extension that adds
96
+ * hypertables, continuous aggregates, and time-series optimizations.
97
+ * Standard tables and hypertables both introspect via information_schema
98
+ * identically. Advisory locks, json_agg, and all other Turbine features
99
+ * work without modification.
100
+ *
101
+ * This export exists for documentation and explicit configuration:
102
+ *
103
+ * ```ts
104
+ * import { timescale } from 'turbine-orm/adapters';
105
+ * const config = { url: process.env.DATABASE_URL, adapter: timescale };
106
+ * ```
107
+ */
108
+ export declare const timescale: DatabaseAdapter;
109
+ export { cockroachdb } from './cockroachdb.js';
110
+ export { yugabytedb } from './yugabytedb.js';
@@ -0,0 +1,51 @@
1
+ /**
2
+ * turbine-orm — YugabyteDB adapter
3
+ *
4
+ * YugabyteDB is a distributed SQL database that speaks the PostgreSQL wire
5
+ * protocol. It supports most PostgreSQL features including json_agg,
6
+ * subqueries, CTEs, and the standard information_schema.
7
+ *
8
+ * Key differences from PostgreSQL that this adapter addresses:
9
+ *
10
+ * 1. **Advisory locks are per-node** — `pg_try_advisory_lock()` is supported
11
+ * but only scoped to the tserver node handling the connection. In a
12
+ * multi-node cluster, two concurrent `turbine migrate` runs routed to
13
+ * different nodes would both acquire the "same" advisory lock. This adapter
14
+ * provides a table-based distributed lock using `SELECT FOR UPDATE NOWAIT`
15
+ * which is cluster-wide via YugabyteDB's distributed transactions.
16
+ *
17
+ * 2. **Sequences may have gaps** — YugabyteDB uses distributed sequences.
18
+ * SERIAL/BIGSERIAL columns work correctly but may produce non-contiguous
19
+ * IDs under concurrent inserts. This is purely cosmetic and does not affect
20
+ * Turbine's behavior.
21
+ *
22
+ * 3. **pg_catalog** — Mostly complete. `pg_indexes`, `pg_type`, `pg_enum`,
23
+ * `information_schema.columns` all work. Row estimate via `pg_class.reltuples`
24
+ * may be stale or zero on recently created tables (YugabyteDB's stats
25
+ * collection is asynchronous). This adapter provides an override that
26
+ * falls back to `yb_table_properties` when available.
27
+ *
28
+ * Features that work identically to PostgreSQL (no adapter override needed):
29
+ * - `json_agg` / `json_build_object` — fully supported
30
+ * - Correlated subqueries — fully supported
31
+ * - `COALESCE`, `LIMIT`, `OFFSET`, `ORDER BY` — fully supported
32
+ * - `information_schema` for table/column/constraint introspection
33
+ * - Extended query protocol (parameterized queries, pipeline batching)
34
+ * - Transactions with `SAVEPOINT` (nested transactions)
35
+ * - `FOR UPDATE` / `FOR SHARE` row-level locking
36
+ * - All WHERE operators (LIKE, ILIKE, IN, etc.)
37
+ * - Array and JSON column types
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import { yugabytedb } from 'turbine-orm/adapters';
42
+ *
43
+ * // In turbine.config.ts:
44
+ * export default {
45
+ * url: process.env.DATABASE_URL,
46
+ * adapter: yugabytedb,
47
+ * };
48
+ * ```
49
+ */
50
+ import type { DatabaseAdapter } from './index.js';
51
+ export declare const yugabytedb: DatabaseAdapter;