turbine-orm 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -16
- package/dist/adapters/cockroachdb.d.ts +40 -0
- package/dist/adapters/cockroachdb.js +172 -0
- package/dist/adapters/index.d.ts +107 -0
- package/dist/adapters/index.js +83 -0
- package/dist/adapters/yugabytedb.d.ts +52 -0
- package/dist/adapters/yugabytedb.js +156 -0
- package/dist/cjs/adapters/cockroachdb.js +174 -0
- package/dist/cjs/adapters/index.js +87 -0
- package/dist/cjs/adapters/yugabytedb.js +158 -0
- package/dist/cjs/cli/index.js +2 -1
- package/dist/cjs/cli/migrate.js +18 -12
- package/dist/cjs/cli/studio.js +5 -4
- package/dist/cjs/client.js +1 -0
- package/dist/cjs/dialect.js +57 -0
- package/dist/cjs/generate.js +8 -1
- package/dist/cjs/index.js +12 -3
- package/dist/cjs/introspect.js +46 -18
- package/dist/cjs/query/builder.js +129 -96
- package/dist/cjs/query/index.js +4 -1
- package/dist/cjs/query/utils.js +18 -0
- package/dist/cjs/schema.js +8 -0
- package/dist/cli/config.d.ts +11 -0
- package/dist/cli/index.js +2 -1
- package/dist/cli/migrate.d.ts +3 -0
- package/dist/cli/migrate.js +16 -10
- package/dist/cli/studio.d.ts +4 -0
- package/dist/cli/studio.js +5 -4
- package/dist/client.d.ts +3 -0
- package/dist/client.js +1 -0
- package/dist/dialect.d.ts +61 -0
- package/dist/dialect.js +55 -0
- package/dist/generate.js +8 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/introspect.js +46 -18
- package/dist/query/builder.d.ts +9 -1
- package/dist/query/builder.js +130 -97
- package/dist/query/index.d.ts +3 -1
- package/dist/query/index.js +2 -1
- package/dist/query/utils.d.ts +8 -0
- package/dist/query/utils.js +17 -0
- package/dist/schema.d.ts +6 -4
- package/dist/schema.js +7 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
# turbine-orm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
110 KB. One runtime dependency. The Postgres ORM that ships light and locks tight.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
npm install turbine-orm
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
**Full docs: [turbineorm.dev](https://turbineorm.dev)** — [Quick Start](https://turbineorm.dev/quickstart) · [API Reference](https://turbineorm.dev/queries) · [Relations](https://turbineorm.dev/relations) · [Transactions & Pipelines](https://turbineorm.dev/transactions) · [Serverless & Edge](https://turbineorm.dev/serverless) · [Typed Errors](https://turbineorm.dev/errors) · [Benchmarks](https://turbineorm.dev/benchmarks)
|
|
10
|
+
|
|
9
11
|
## Why Turbine?
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Prisma ships a 1.6 MB WASM engine. Drizzle ships zero runtime but no Studio, no typed errors, no migration checksums. Turbine ships **one dependency (`pg`), ~110 KB**, and bundles six things no other TS ORM has together:
|
|
12
14
|
|
|
13
|
-
1. **One runtime dependency (`pg`).** No engine binary, no WASM adapter, no
|
|
14
|
-
2. **
|
|
15
|
-
3. **
|
|
16
|
-
4. **
|
|
15
|
+
1. **One runtime dependency (`pg`).** No engine binary, no WASM adapter, no adapter packages to keep in lockstep. ~110 KB on npm. 5 KB on the edge entry. Prisma's WASM engine alone is 1.6 MB.
|
|
16
|
+
2. **Built-in read-only Studio.** `npx turbine studio` spins up a loopback-bound web UI with 192-bit auth tokens, `BEGIN READ ONLY` transactions, and a statement-stacking guard. The only TS ORM Studio that physically cannot mutate your database. DBA-approvable.
|
|
17
|
+
3. **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.
|
|
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.
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
Every ORM claims single-query nested loads now (Prisma 7 and Drizzle v2 both use json_agg). Turbine does too — see [How It Works](#how-it-works). The differentiator isn't the query strategy; it's the 110 KB footprint, the read-only Studio, and the error messages that never leak user data.
|
|
19
23
|
|
|
20
24
|
## Benchmarks
|
|
21
25
|
|
|
@@ -39,7 +43,7 @@ Tested against **Prisma 7.6** (adapter-pg, relationJoins preview on) and **Drizz
|
|
|
39
43
|
- **Streaming 50K rows.** Turbine's optimized streaming (speculative first fetch + batch size 1000) matches Prisma at ~3.1–3.2 s. Drizzle's keyset pagination is 1.49× slower at 4.6 s. Turbine's cursor still gives you correctness on any `orderBy` and clean early-`break` semantics.
|
|
40
44
|
- **Pipeline batching** puts 5 independent queries through a single round-trip using the Postgres extended-query pipeline protocol — all three ORMs are tied here since each runs 5 queries sequentially in a transaction.
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
Performance is at parity with Prisma and Drizzle — the real reasons to choose Turbine are elsewhere: **110 KB install** (vs Prisma's 1.6 MB WASM), the **only read-only 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 — no manual assertion, no helper annotation.
|
|
43
47
|
|
|
44
48
|
> Full analysis with p50/p95/p99 and methodology notes: [`benchmarks/RESULTS.md`](./benchmarks/RESULTS.md).
|
|
45
49
|
> Reproduce: `cd benchmarks && npm install && npx prisma generate && DATABASE_URL=... npx tsx bench.ts`
|
|
@@ -303,6 +307,8 @@ try {
|
|
|
303
307
|
|
|
304
308
|
Error codes: `TURBINE_E001` (NotFound), `TURBINE_E002` (Timeout), `TURBINE_E003` (Validation), `TURBINE_E004` (Connection), `TURBINE_E005` (Relation), `TURBINE_E006` (Migration), `TURBINE_E007` (CircularRelation), `TURBINE_E008` (UniqueConstraint), `TURBINE_E009` (ForeignKey), `TURBINE_E010` (NotNullViolation), `TURBINE_E011` (CheckConstraint), `TURBINE_E012` (Deadlock), `TURBINE_E013` (SerializationFailure), `TURBINE_E014` (Pipeline).
|
|
305
309
|
|
|
310
|
+
Full reference with `wrapPgError()` translation, retry patterns for `DeadlockError` / `SerializationFailureError`, and safe vs verbose message modes: **[turbineorm.dev/errors](https://turbineorm.dev/errors)**.
|
|
311
|
+
|
|
306
312
|
## WHERE Operator Reference
|
|
307
313
|
|
|
308
314
|
Every operator supported by the `where` clause. Operators compose freely with `AND`, `OR`, `NOT`, and the relation filters `some` / `every` / `none`.
|
|
@@ -565,9 +571,9 @@ Priority order: CLI flags > environment variables (`DATABASE_URL`) > config file
|
|
|
565
571
|
|
|
566
572
|
## How It Works
|
|
567
573
|
|
|
568
|
-
Turbine resolves
|
|
574
|
+
Turbine resolves nested relations the same way Prisma 7 and Drizzle v2 do: correlated subqueries with `json_agg` + `json_build_object`, evaluated by PostgreSQL in a single round-trip. No N+1, no client-side stitching, no separate queries per relation. The `with` clause is fully type-inferred end-to-end — write `db.users.findMany({ with: { posts: { with: { comments: { with: { author: true } } } } } })` and `users[0].posts[0].comments[0].author.name` autocompletes with zero manual annotation.
|
|
569
575
|
|
|
570
|
-
|
|
576
|
+
The query strategy is table stakes now. What isn't table stakes: the 110 KB footprint that makes it possible, the read-only Studio your DBA will approve, the error messages that never leak PII, and the SQL-first migrations with SHA-256 drift detection. See [Why Turbine?](#why-turbine) for the full breakdown.
|
|
571
577
|
|
|
572
578
|
## Type Mapping
|
|
573
579
|
|
|
@@ -589,14 +595,18 @@ Turbine maps Postgres types to TypeScript:
|
|
|
589
595
|
|
|
590
596
|
| | **Turbine** | **Prisma** | **Drizzle** | **Kysely** |
|
|
591
597
|
|---|---|---|---|---|
|
|
592
|
-
| **
|
|
593
|
-
| **API style** | `findMany`, `with` | `findMany`, `include` | SQL-like + relational | SQL builder |
|
|
594
|
-
| **Schema** | TypeScript | Custom DSL (`.prisma`) | TypeScript | Manual interfaces |
|
|
598
|
+
| **Install size** | ~110 KB (`pg` only) | ~1.6 MB (WASM engine) | ~0 KB (no runtime) | ~0 KB (no runtime) |
|
|
595
599
|
| **Runtime deps** | 1 (`pg`) | `@prisma/client` + adapter | 0 | 0 |
|
|
600
|
+
| **Studio** | Read-only, 192-bit auth | Full CRUD, cloud-hosted | Paid tier | None |
|
|
601
|
+
| **Error PII safety** | Keys only by default | Values in messages | Raw pg errors | Raw pg errors |
|
|
602
|
+
| **Migrations** | SQL-first, SHA-256 checksums | DSL-generated, shadow DB | SQL or Drizzle Kit | None |
|
|
603
|
+
| **Edge runtime** | One import swap, 5 KB | 1.6 MB WASM adapter | Native | Native |
|
|
604
|
+
| **Pipeline batching** | Parse/Bind/Execute protocol | Sequential in txn | Sequential | Manual |
|
|
605
|
+
| **Typed errors** | `isRetryable` discriminant | Error codes only | None | None |
|
|
606
|
+
| **Nested relations** | 1 query, deep type inference | 1 query, shallow inference | 1 query, `relations()` re-declaration | Manual (`jsonArrayFrom`) |
|
|
596
607
|
| **Multi-DB** | PostgreSQL only | PG, MySQL, SQLite, MSSQL | PG, MySQL, SQLite | PG, MySQL, SQLite |
|
|
597
|
-
| **Code generation** | `turbine generate` | `prisma generate` | Not needed | Not needed |
|
|
598
608
|
|
|
599
|
-
All three ORMs now do single-query nested loads
|
|
609
|
+
All three ORMs now do single-query nested loads — that's table stakes. Turbine's real differentiators: the smallest install of any full-featured ORM (110 KB vs Prisma's 1.6 MB), the only read-only 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.
|
|
600
610
|
|
|
601
611
|
## Limitations
|
|
602
612
|
|
|
@@ -624,7 +634,15 @@ Turbine is focused and opinionated. Here's what it doesn't do:
|
|
|
624
634
|
|
|
625
635
|
## Guides
|
|
626
636
|
|
|
627
|
-
- **[
|
|
637
|
+
- **[Quick Start](https://turbineorm.dev/quickstart)** — zero-to-first-query in five minutes
|
|
638
|
+
- **[API Reference](https://turbineorm.dev/queries)** — every `findMany` / `findUnique` / `create` / `update` / `delete` option, the full operator table, and `pipeline()` semantics
|
|
639
|
+
- **[Relations](https://turbineorm.dev/relations)** — deep `with` clause, nested options, relation filters (`some` / `every` / `none`), payload-size guidance
|
|
640
|
+
- **[Transactions & Pipelines](https://turbineorm.dev/transactions)** — isolation levels, nested SAVEPOINTs, retry loops for `DeadlockError` and `SerializationFailureError`
|
|
641
|
+
- **[Schema & Migrations](https://turbineorm.dev/schema)** — `defineSchema()`, auto-diff migrations, checksum validation
|
|
642
|
+
- **[Serverless & Edge](https://turbineorm.dev/serverless)** — Neon, Vercel Postgres, Cloudflare Hyperdrive, Supabase walkthroughs
|
|
643
|
+
- **[CLI](https://turbineorm.dev/cli)** — every command, flag, and config option
|
|
644
|
+
- **[Typed Errors](https://turbineorm.dev/errors)** — error code reference, `wrapPgError()` translation, retry patterns
|
|
645
|
+
- **[Migrating from Prisma](https://turbineorm.dev/migrate-from-prisma)** — API mapping table, side-by-side `findMany`, and notes on the differences
|
|
628
646
|
|
|
629
647
|
## Requirements
|
|
630
648
|
|
|
@@ -0,0 +1,40 @@
|
|
|
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;
|
|
40
|
+
//# sourceMappingURL=cockroachdb.d.ts.map
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Lock table SQL
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
const LOCK_TABLE = '_turbine_lock';
|
|
42
|
+
/**
|
|
43
|
+
* DDL for the table-based lock mechanism. The table holds a single row per
|
|
44
|
+
* lock ID. `SELECT ... FOR UPDATE NOWAIT` on this row serves as a mutex.
|
|
45
|
+
*/
|
|
46
|
+
const CREATE_LOCK_TABLE_SQL = `
|
|
47
|
+
CREATE TABLE IF NOT EXISTS "${LOCK_TABLE}" (
|
|
48
|
+
lock_id INT PRIMARY KEY,
|
|
49
|
+
acquired_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
50
|
+
acquired_by TEXT
|
|
51
|
+
)
|
|
52
|
+
`;
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Introspection query overrides
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
/**
|
|
57
|
+
* CockroachDB index introspection via SHOW INDEXES.
|
|
58
|
+
* This returns a different shape than pg_indexes, so the consumer
|
|
59
|
+
* needs to handle the transformation. However, since we're providing this
|
|
60
|
+
* as a drop-in SQL string for the existing introspection flow, we use
|
|
61
|
+
* CockroachDB's pg_indexes compatibility view but with a fallback query
|
|
62
|
+
* that produces the same columns.
|
|
63
|
+
*
|
|
64
|
+
* CockroachDB's pg_indexes is compatible since v22.1 — we keep this
|
|
65
|
+
* override for older versions or when indexdef is incomplete.
|
|
66
|
+
*/
|
|
67
|
+
const SQL_INDEXES_CRDB = `
|
|
68
|
+
SELECT
|
|
69
|
+
tablename,
|
|
70
|
+
indexname,
|
|
71
|
+
indexdef
|
|
72
|
+
FROM pg_indexes
|
|
73
|
+
WHERE schemaname = $1
|
|
74
|
+
`;
|
|
75
|
+
/**
|
|
76
|
+
* Row estimates using crdb_internal. Falls back gracefully if the view
|
|
77
|
+
* doesn't exist (permissions issue) — returns 0 rows in that case.
|
|
78
|
+
*/
|
|
79
|
+
const SQL_ROW_ESTIMATES_CRDB = `
|
|
80
|
+
SELECT
|
|
81
|
+
t.name AS relname,
|
|
82
|
+
COALESCE(s.estimated_row_count, 0)::text AS reltuples
|
|
83
|
+
FROM crdb_internal.tables t
|
|
84
|
+
LEFT JOIN crdb_internal.table_row_statistics s
|
|
85
|
+
ON s.table_id = t.table_id
|
|
86
|
+
WHERE t.schema_name = $1
|
|
87
|
+
AND t.database_name = current_database()
|
|
88
|
+
`;
|
|
89
|
+
/**
|
|
90
|
+
* Enum introspection — CockroachDB supports pg_type/pg_enum since v20.2.
|
|
91
|
+
* The standard query works, but we include it explicitly so the override
|
|
92
|
+
* mechanism is complete.
|
|
93
|
+
*/
|
|
94
|
+
const SQL_ENUMS_CRDB = `
|
|
95
|
+
SELECT t.typname, e.enumlabel
|
|
96
|
+
FROM pg_type t
|
|
97
|
+
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
98
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
99
|
+
WHERE n.nspname = $1
|
|
100
|
+
ORDER BY t.typname, e.enumsortorder
|
|
101
|
+
`;
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Adapter implementation
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
export const cockroachdb = {
|
|
106
|
+
name: 'cockroachdb',
|
|
107
|
+
createLockTableSQL() {
|
|
108
|
+
return CREATE_LOCK_TABLE_SQL;
|
|
109
|
+
},
|
|
110
|
+
async acquireLock(client, lockId) {
|
|
111
|
+
// Ensure the lock table exists
|
|
112
|
+
await client.query(CREATE_LOCK_TABLE_SQL);
|
|
113
|
+
// Insert the lock row if it doesn't exist (idempotent)
|
|
114
|
+
await client.query(`INSERT INTO "${LOCK_TABLE}" (lock_id) VALUES ($1) ON CONFLICT (lock_id) DO NOTHING`, [lockId]);
|
|
115
|
+
// Try to acquire the row lock with NOWAIT — fails immediately if held
|
|
116
|
+
try {
|
|
117
|
+
await client.query('BEGIN');
|
|
118
|
+
await client.query(`SELECT lock_id FROM "${LOCK_TABLE}" WHERE lock_id = $1 FOR UPDATE NOWAIT`, [lockId]);
|
|
119
|
+
// Update the acquired metadata
|
|
120
|
+
await client.query(`UPDATE "${LOCK_TABLE}" SET acquired_at = now(), acquired_by = current_user WHERE lock_id = $1`, [lockId]);
|
|
121
|
+
// Note: we leave the transaction OPEN — the lock is held until
|
|
122
|
+
// releaseLock() commits or rolls back.
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
// NOWAIT throws error code 55P03 (lock_not_available) if the row is locked
|
|
127
|
+
const pgErr = err;
|
|
128
|
+
if (pgErr.code === '55P03') {
|
|
129
|
+
try {
|
|
130
|
+
await client.query('ROLLBACK');
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
/* ignore rollback errors */
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
// Any other error — rollback and re-throw
|
|
138
|
+
try {
|
|
139
|
+
await client.query('ROLLBACK');
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
/* ignore */
|
|
143
|
+
}
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
async releaseLock(client, _lockId) {
|
|
148
|
+
// Commit the transaction opened in acquireLock to release the FOR UPDATE lock
|
|
149
|
+
try {
|
|
150
|
+
await client.query('COMMIT');
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// If commit fails, try rollback (either way, lock is released)
|
|
154
|
+
try {
|
|
155
|
+
await client.query('ROLLBACK');
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
/* ignore */
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
introspectionOverrides: {
|
|
163
|
+
indexes: SQL_INDEXES_CRDB,
|
|
164
|
+
enums: SQL_ENUMS_CRDB,
|
|
165
|
+
rowEstimates: SQL_ROW_ESTIMATES_CRDB,
|
|
166
|
+
},
|
|
167
|
+
statementTimeout(seconds) {
|
|
168
|
+
// CockroachDB v23.1+ supports transaction_timeout
|
|
169
|
+
return `SET transaction_timeout = '${seconds}s'`;
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=cockroachdb.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
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 `SET LOCAL statement_timeout = '<n>s'`.
|
|
59
|
+
* CockroachDB uses `SET transaction_timeout = '<n>s'` (v23.1+).
|
|
60
|
+
*
|
|
61
|
+
* @param seconds — timeout in seconds
|
|
62
|
+
* @returns the SQL string to execute
|
|
63
|
+
*/
|
|
64
|
+
statementTimeout?(seconds: number): string;
|
|
65
|
+
/**
|
|
66
|
+
* SQL to create the lock table used by table-based locking adapters.
|
|
67
|
+
* Called during `ensureTrackingTable` when the adapter uses table locks.
|
|
68
|
+
*/
|
|
69
|
+
createLockTableSQL?(): string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The default PostgreSQL adapter. Uses pg_try_advisory_lock, standard
|
|
73
|
+
* pg_catalog queries, and SET LOCAL statement_timeout.
|
|
74
|
+
*/
|
|
75
|
+
export declare const postgresql: DatabaseAdapter;
|
|
76
|
+
/**
|
|
77
|
+
* Google AlloyDB adapter. AlloyDB is PostgreSQL with Google's columnar storage
|
|
78
|
+
* engine. It is wire-protocol and catalog-compatible — no adapter overrides
|
|
79
|
+
* are needed. All Turbine features (json_agg, advisory locks, introspection,
|
|
80
|
+
* migrations) work identically to standard PostgreSQL.
|
|
81
|
+
*
|
|
82
|
+
* This export exists for documentation and explicit configuration:
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* import { alloydb } from 'turbine-orm/adapters';
|
|
86
|
+
* const config = { url: process.env.DATABASE_URL, adapter: alloydb };
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export declare const alloydb: DatabaseAdapter;
|
|
90
|
+
/**
|
|
91
|
+
* TimescaleDB adapter. Timescale is a PostgreSQL extension that adds
|
|
92
|
+
* hypertables, continuous aggregates, and time-series optimizations.
|
|
93
|
+
* Standard tables and hypertables both introspect via information_schema
|
|
94
|
+
* identically. Advisory locks, json_agg, and all other Turbine features
|
|
95
|
+
* work without modification.
|
|
96
|
+
*
|
|
97
|
+
* This export exists for documentation and explicit configuration:
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* import { timescale } from 'turbine-orm/adapters';
|
|
101
|
+
* const config = { url: process.env.DATABASE_URL, adapter: timescale };
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export declare const timescale: DatabaseAdapter;
|
|
105
|
+
export { cockroachdb } from './cockroachdb.js';
|
|
106
|
+
export { yugabytedb } from './yugabytedb.js';
|
|
107
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Default PostgreSQL adapter (no-op — standard behavior)
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* The default PostgreSQL adapter. Uses pg_try_advisory_lock, standard
|
|
22
|
+
* pg_catalog queries, and SET LOCAL statement_timeout.
|
|
23
|
+
*/
|
|
24
|
+
export const postgresql = {
|
|
25
|
+
name: 'postgresql',
|
|
26
|
+
async acquireLock(client, lockId) {
|
|
27
|
+
const result = await client.query(`SELECT pg_try_advisory_lock($1) AS locked`, [lockId]);
|
|
28
|
+
return result.rows[0]?.locked ?? false;
|
|
29
|
+
},
|
|
30
|
+
async releaseLock(client, lockId) {
|
|
31
|
+
await client.query(`SELECT pg_advisory_unlock($1)`, [lockId]);
|
|
32
|
+
},
|
|
33
|
+
statementTimeout(seconds) {
|
|
34
|
+
return `SET LOCAL statement_timeout = '${seconds}s'`;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// AlloyDB — fully PostgreSQL-compatible, no adapter logic needed
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
/**
|
|
41
|
+
* Google AlloyDB adapter. AlloyDB is PostgreSQL with Google's columnar storage
|
|
42
|
+
* engine. It is wire-protocol and catalog-compatible — no adapter overrides
|
|
43
|
+
* are needed. All Turbine features (json_agg, advisory locks, introspection,
|
|
44
|
+
* migrations) work identically to standard PostgreSQL.
|
|
45
|
+
*
|
|
46
|
+
* This export exists for documentation and explicit configuration:
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* import { alloydb } from 'turbine-orm/adapters';
|
|
50
|
+
* const config = { url: process.env.DATABASE_URL, adapter: alloydb };
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export const alloydb = {
|
|
54
|
+
...postgresql,
|
|
55
|
+
name: 'alloydb',
|
|
56
|
+
};
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// TimescaleDB — PostgreSQL extension, fully compatible
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
/**
|
|
61
|
+
* TimescaleDB adapter. Timescale is a PostgreSQL extension that adds
|
|
62
|
+
* hypertables, continuous aggregates, and time-series optimizations.
|
|
63
|
+
* Standard tables and hypertables both introspect via information_schema
|
|
64
|
+
* identically. Advisory locks, json_agg, and all other Turbine features
|
|
65
|
+
* work without modification.
|
|
66
|
+
*
|
|
67
|
+
* This export exists for documentation and explicit configuration:
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { timescale } from 'turbine-orm/adapters';
|
|
71
|
+
* const config = { url: process.env.DATABASE_URL, adapter: timescale };
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export const timescale = {
|
|
75
|
+
...postgresql,
|
|
76
|
+
name: 'timescale',
|
|
77
|
+
};
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Re-exports
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
export { cockroachdb } from './cockroachdb.js';
|
|
82
|
+
export { yugabytedb } from './yugabytedb.js';
|
|
83
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
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;
|
|
52
|
+
//# sourceMappingURL=yugabytedb.d.ts.map
|