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.
- package/README.md +122 -39
- package/dist/cjs/adapters/cockroachdb.d.ts +39 -0
- package/dist/cjs/adapters/index.d.ts +110 -0
- package/dist/cjs/adapters/yugabytedb.d.ts +51 -0
- package/dist/cjs/cli/config.d.ts +181 -0
- package/dist/cjs/cli/config.js +32 -6
- package/dist/cjs/cli/destructive.d.ts +38 -0
- package/dist/cjs/cli/index.d.ts +359 -0
- package/dist/cjs/cli/index.js +228 -56
- package/dist/cjs/cli/loader.d.ts +61 -0
- package/dist/cjs/cli/mcp.d.ts +42 -0
- package/dist/cjs/cli/migrate.d.ts +356 -0
- package/dist/cjs/cli/migrate.js +131 -40
- package/dist/cjs/cli/observe-ui.d.ts +1 -0
- package/dist/cjs/cli/observe-ui.js +14 -5
- package/dist/cjs/cli/observe.d.ts +25 -0
- package/dist/cjs/cli/observe.js +49 -12
- package/dist/cjs/cli/pii-tags.d.ts +53 -0
- package/dist/cjs/cli/prisma-report.d.ts +33 -0
- package/dist/cjs/cli/prisma-report.js +73 -0
- package/dist/cjs/cli/prisma-resolve.d.ts +106 -0
- package/dist/cjs/cli/prisma-resolve.js +1 -0
- package/dist/cjs/cli/prisma-schema.d.ts +176 -0
- package/dist/cjs/cli/prisma-schema.js +82 -4
- package/dist/cjs/cli/rate-limit.d.ts +32 -0
- package/dist/cjs/cli/rate-limit.js +45 -0
- package/dist/cjs/cli/studio-demo.d.ts +43 -0
- package/dist/cjs/cli/studio-ui.generated.d.ts +1 -0
- package/dist/cjs/cli/studio.d.ts +207 -0
- package/dist/cjs/cli/studio.js +136 -71
- package/dist/cjs/cli/ui.d.ts +73 -0
- package/dist/cjs/cli/ui.js +51 -9
- package/dist/cjs/client.d.ts +837 -0
- package/dist/cjs/client.js +3 -0
- package/dist/cjs/dialect.d.ts +516 -0
- package/dist/cjs/dialect.js +37 -12
- package/dist/cjs/errors.d.ts +370 -0
- package/dist/cjs/generate.d.ts +137 -0
- package/dist/cjs/generate.js +39 -6
- package/dist/cjs/index-advisor.d.ts +153 -0
- package/dist/cjs/index-stats.d.ts +384 -0
- package/dist/cjs/index.d.ts +55 -0
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/introspect.d.ts +269 -0
- package/dist/cjs/mssql.d.ts +232 -0
- package/dist/cjs/mssql.js +6 -0
- package/dist/cjs/mysql.d.ts +173 -0
- package/dist/cjs/mysql.js +16 -0
- package/dist/cjs/nested-write.d.ts +96 -0
- package/dist/cjs/nested-write.js +414 -24
- package/dist/cjs/observe.d.ts +115 -0
- package/dist/cjs/optional-peer-import.d.cts +72 -0
- package/dist/cjs/pipeline-submittable.d.ts +93 -0
- package/dist/cjs/pipeline.d.ts +71 -0
- package/dist/cjs/powdb-introspect.d.ts +84 -0
- package/dist/cjs/powdb.d.ts +931 -0
- package/dist/cjs/powdb.js +106 -21
- package/dist/cjs/powql.d.ts +592 -0
- package/dist/cjs/powql.js +42 -6
- package/dist/cjs/prisma-compat.d.ts +283 -0
- package/dist/cjs/prisma-compat.js +167 -9
- package/dist/cjs/query/aggregates.d.ts +92 -0
- package/dist/cjs/query/aggregates.js +7 -3
- package/dist/cjs/query/batched-loader.d.ts +193 -0
- package/dist/cjs/query/builder.d.ts +849 -0
- package/dist/cjs/query/builder.js +571 -65
- package/dist/cjs/query/compound-unique.d.ts +51 -0
- package/dist/cjs/query/deferred.d.ts +223 -0
- package/dist/cjs/query/filters.d.ts +201 -0
- package/dist/cjs/query/index.d.ts +14 -0
- package/dist/cjs/query/index.js +6 -1
- package/dist/cjs/query/relations.d.ts +609 -0
- package/dist/cjs/query/relations.js +693 -46
- package/dist/cjs/query/types.d.ts +1300 -0
- package/dist/cjs/query/utils.d.ts +209 -0
- package/dist/cjs/query/utils.js +208 -1
- package/dist/cjs/query/warn-registry.d.ts +68 -0
- package/dist/cjs/query/warn-registry.js +9 -0
- package/dist/cjs/query/where-compile.d.ts +139 -0
- package/dist/cjs/query/where.d.ts +548 -0
- package/dist/cjs/query/where.js +58 -22
- package/dist/cjs/query/writes.d.ts +172 -0
- package/dist/cjs/query/writes.js +105 -12
- package/dist/cjs/realtime.d.ts +70 -0
- package/dist/cjs/schema-builder.d.ts +354 -0
- package/dist/cjs/schema-metadata.d.ts +83 -0
- package/dist/cjs/schema-sql.d.ts +217 -0
- package/dist/cjs/schema-sql.js +23 -5
- package/dist/cjs/schema.d.ts +356 -0
- package/dist/cjs/schema.js +125 -0
- package/dist/cjs/seed.d.ts +15 -0
- package/dist/cjs/serverless.d.ts +142 -0
- package/dist/cjs/sqlite.d.ts +143 -0
- package/dist/cjs/sqlite.js +4 -0
- package/dist/cjs/typed-sql.d.ts +102 -0
- package/dist/cli/config.d.ts +18 -4
- package/dist/cli/config.js +31 -6
- package/dist/cli/index.d.ts +123 -0
- package/dist/cli/index.js +223 -58
- package/dist/cli/migrate.d.ts +59 -10
- package/dist/cli/migrate.js +128 -41
- package/dist/cli/observe-ui.d.ts +1 -1
- package/dist/cli/observe-ui.js +14 -5
- package/dist/cli/observe.d.ts +7 -1
- package/dist/cli/observe.js +48 -12
- package/dist/cli/prisma-report.d.ts +14 -0
- package/dist/cli/prisma-report.js +72 -0
- package/dist/cli/prisma-resolve.d.ts +6 -0
- package/dist/cli/prisma-resolve.js +1 -0
- package/dist/cli/prisma-schema.d.ts +62 -2
- package/dist/cli/prisma-schema.js +81 -4
- package/dist/cli/rate-limit.d.ts +32 -0
- package/dist/cli/rate-limit.js +40 -0
- package/dist/cli/studio.d.ts +5 -5
- package/dist/cli/studio.js +135 -70
- package/dist/cli/ui.d.ts +1 -1
- package/dist/cli/ui.js +51 -9
- package/dist/client.d.ts +40 -0
- package/dist/client.js +3 -0
- package/dist/dialect.d.ts +17 -1
- package/dist/dialect.js +37 -12
- package/dist/generate.js +40 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.js +6 -0
- package/dist/mysql.js +16 -0
- package/dist/nested-write.d.ts +2 -0
- package/dist/nested-write.js +415 -25
- package/dist/powdb.d.ts +4 -2
- package/dist/powdb.js +106 -21
- package/dist/powql.d.ts +5 -0
- package/dist/powql.js +42 -6
- package/dist/prisma-compat.d.ts +2 -0
- package/dist/prisma-compat.js +166 -8
- package/dist/query/aggregates.js +7 -3
- package/dist/query/builder.d.ts +292 -21
- package/dist/query/builder.js +570 -64
- package/dist/query/deferred.d.ts +39 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/relations.d.ts +173 -5
- package/dist/query/relations.js +688 -47
- package/dist/query/types.d.ts +123 -39
- package/dist/query/utils.d.ts +116 -0
- package/dist/query/utils.js +198 -0
- package/dist/query/warn-registry.d.ts +9 -0
- package/dist/query/warn-registry.js +9 -0
- package/dist/query/where.d.ts +38 -1
- package/dist/query/where.js +58 -23
- package/dist/query/writes.d.ts +42 -1
- package/dist/query/writes.js +104 -13
- package/dist/schema-sql.d.ts +14 -0
- package/dist/schema-sql.js +23 -5
- package/dist/schema.d.ts +38 -0
- package/dist/schema.js +123 -0
- package/dist/sqlite.js +4 -0
- package/package.json +77 -28
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# turbine-orm
|
|
2
2
|
|
|
3
|
-
The Postgres ORM
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
37
|
-
| findMany, 50 users + posts (L2) | 2.
|
|
38
|
-
| findMany, 10 users → posts → comments (L3) *(near-tie)* | 1.
|
|
39
|
-
| findUnique, single user by PK | **0.
|
|
40
|
-
| findUnique, user + posts + comments (L3) | **0.
|
|
41
|
-
| count, all users
|
|
42
|
-
| stream, iterate 50K rows (batch 1000)
|
|
43
|
-
| atomic increment, `view_count + 1` *(near-tie)* | **0.
|
|
44
|
-
| pipeline, 5-query batch | **0.
|
|
45
|
-
| hot findUnique, 500x same shape | **0.
|
|
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
|
-
**
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 {
|
|
91
|
+
import { TurbineClient, defineSchema } from 'turbine-orm';
|
|
80
92
|
|
|
81
93
|
// CommonJS
|
|
82
|
-
const {
|
|
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
|
-
|
|
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
|
|
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)** | ~
|
|
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, ~
|
|
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
|
-
|
|
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;
|