turbine-orm 0.50.0 → 0.51.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 +66 -66
- package/dist/adapters/cockroachdb.d.ts +5 -5
- package/dist/adapters/cockroachdb.js +10 -10
- package/dist/adapters/index.d.ts +5 -5
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/yugabytedb.d.ts +7 -7
- package/dist/adapters/yugabytedb.js +10 -10
- package/dist/cjs/adapters/cockroachdb.d.ts +5 -5
- package/dist/cjs/adapters/cockroachdb.js +10 -10
- package/dist/cjs/adapters/index.d.ts +5 -5
- package/dist/cjs/adapters/index.js +7 -7
- package/dist/cjs/adapters/yugabytedb.d.ts +7 -7
- package/dist/cjs/adapters/yugabytedb.js +10 -10
- package/dist/cjs/cli/config.d.ts +13 -2
- package/dist/cjs/cli/config.js +3 -2
- package/dist/cjs/cli/destructive.d.ts +1 -1
- package/dist/cjs/cli/destructive.js +1 -1
- package/dist/cjs/cli/index.d.ts +10 -10
- package/dist/cjs/cli/index.js +49 -45
- package/dist/cjs/cli/loader.d.ts +7 -7
- package/dist/cjs/cli/loader.js +9 -9
- package/dist/cjs/cli/mcp.js +4 -4
- package/dist/cjs/cli/migrate.d.ts +5 -5
- package/dist/cjs/cli/migrate.js +11 -11
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/ui.d.ts +2 -2
- package/dist/cjs/cli/ui.js +2 -2
- package/dist/cjs/client.d.ts +49 -38
- package/dist/cjs/client.js +57 -56
- package/dist/cjs/dialect.d.ts +62 -18
- package/dist/cjs/dialect.js +40 -2
- package/dist/cjs/errors.d.ts +5 -5
- package/dist/cjs/errors.js +11 -11
- package/dist/cjs/generate.d.ts +6 -6
- package/dist/cjs/generate.js +31 -29
- package/dist/cjs/index-advisor.d.ts +5 -5
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +7 -7
- package/dist/cjs/introspect.d.ts +35 -9
- package/dist/cjs/introspect.js +83 -32
- package/dist/cjs/mssql.d.ts +11 -11
- package/dist/cjs/mssql.js +64 -29
- package/dist/cjs/mysql.d.ts +8 -8
- package/dist/cjs/mysql.js +61 -23
- package/dist/cjs/nested-write.d.ts +21 -2
- package/dist/cjs/nested-write.js +51 -14
- package/dist/cjs/optional-peer-import.cjs +7 -7
- package/dist/cjs/optional-peer-import.d.cts +7 -7
- package/dist/cjs/pipeline-submittable.d.ts +2 -2
- package/dist/cjs/pipeline-submittable.js +6 -6
- package/dist/cjs/pipeline.d.ts +1 -1
- package/dist/cjs/pipeline.js +4 -4
- package/dist/cjs/powdb-introspect.d.ts +1 -1
- package/dist/cjs/powdb-introspect.js +1 -1
- package/dist/cjs/powdb.d.ts +28 -28
- package/dist/cjs/powdb.js +66 -66
- package/dist/cjs/powql.d.ts +27 -27
- package/dist/cjs/powql.js +73 -52
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +5 -5
- package/dist/cjs/query/batched-loader.d.ts +11 -11
- package/dist/cjs/query/batched-loader.js +24 -24
- package/dist/cjs/query/builder.d.ts +39 -21
- package/dist/cjs/query/builder.js +99 -57
- package/dist/cjs/query/compound-unique.d.ts +1 -1
- package/dist/cjs/query/compound-unique.js +0 -0
- package/dist/cjs/query/deferred.d.ts +12 -6
- package/dist/cjs/query/deferred.js +1 -1
- package/dist/cjs/query/filters.d.ts +31 -11
- package/dist/cjs/query/filters.js +67 -14
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/index.js +1 -1
- package/dist/cjs/query/relations.d.ts +9 -9
- package/dist/cjs/query/relations.js +164 -57
- package/dist/cjs/query/types.d.ts +86 -35
- package/dist/cjs/query/types.js +1 -1
- package/dist/cjs/query/utils.d.ts +27 -10
- package/dist/cjs/query/utils.js +86 -14
- package/dist/cjs/query/where.d.ts +47 -28
- package/dist/cjs/query/where.js +130 -31
- package/dist/cjs/query/writes.d.ts +24 -5
- package/dist/cjs/query/writes.js +102 -13
- package/dist/cjs/realtime.d.ts +7 -7
- package/dist/cjs/realtime.js +9 -9
- package/dist/cjs/schema-builder.d.ts +18 -7
- package/dist/cjs/schema-builder.js +17 -10
- package/dist/cjs/schema-metadata.d.ts +3 -3
- package/dist/cjs/schema-metadata.js +9 -9
- package/dist/cjs/schema-sql.d.ts +9 -9
- package/dist/cjs/schema-sql.js +20 -20
- package/dist/cjs/schema.d.ts +19 -9
- package/dist/cjs/schema.js +6 -6
- package/dist/cjs/serverless.d.ts +15 -15
- package/dist/cjs/serverless.js +16 -16
- package/dist/cjs/sqlite.d.ts +8 -8
- package/dist/cjs/sqlite.js +53 -22
- package/dist/cjs/typed-sql.d.ts +4 -4
- package/dist/cjs/typed-sql.js +5 -5
- package/dist/cli/config.d.ts +13 -2
- package/dist/cli/config.js +3 -2
- package/dist/cli/destructive.d.ts +1 -1
- package/dist/cli/destructive.js +1 -1
- package/dist/cli/index.d.ts +10 -10
- package/dist/cli/index.js +49 -45
- package/dist/cli/loader.d.ts +7 -7
- package/dist/cli/loader.js +9 -9
- package/dist/cli/mcp.js +4 -4
- package/dist/cli/migrate.d.ts +5 -5
- package/dist/cli/migrate.js +11 -11
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/ui.d.ts +2 -2
- package/dist/cli/ui.js +2 -2
- package/dist/client.d.ts +49 -38
- package/dist/client.js +57 -56
- package/dist/dialect.d.ts +62 -18
- package/dist/dialect.js +40 -2
- package/dist/errors.d.ts +5 -5
- package/dist/errors.js +11 -11
- package/dist/generate.d.ts +6 -6
- package/dist/generate.js +31 -29
- package/dist/index-advisor.d.ts +5 -5
- package/dist/index-advisor.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -7
- package/dist/introspect.d.ts +35 -9
- package/dist/introspect.js +82 -32
- package/dist/mssql.d.ts +11 -11
- package/dist/mssql.js +64 -29
- package/dist/mysql.d.ts +8 -8
- package/dist/mysql.js +61 -23
- package/dist/nested-write.d.ts +21 -2
- package/dist/nested-write.js +51 -14
- package/dist/optional-peer-import.cjs +7 -7
- package/dist/optional-peer-import.d.cts +7 -7
- package/dist/pipeline-submittable.d.ts +2 -2
- package/dist/pipeline-submittable.js +6 -6
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +4 -4
- package/dist/powdb-introspect.d.ts +1 -1
- package/dist/powdb-introspect.js +1 -1
- package/dist/powdb.d.ts +28 -28
- package/dist/powdb.js +66 -66
- package/dist/powql.d.ts +27 -27
- package/dist/powql.js +73 -52
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +5 -5
- package/dist/query/batched-loader.d.ts +11 -11
- package/dist/query/batched-loader.js +24 -24
- package/dist/query/builder.d.ts +39 -21
- package/dist/query/builder.js +100 -58
- package/dist/query/compound-unique.d.ts +1 -1
- package/dist/query/compound-unique.js +0 -0
- package/dist/query/deferred.d.ts +12 -6
- package/dist/query/deferred.js +1 -1
- package/dist/query/filters.d.ts +31 -11
- package/dist/query/filters.js +66 -13
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/relations.d.ts +9 -9
- package/dist/query/relations.js +165 -58
- package/dist/query/types.d.ts +86 -35
- package/dist/query/types.js +1 -1
- package/dist/query/utils.d.ts +27 -10
- package/dist/query/utils.js +84 -14
- package/dist/query/where.d.ts +47 -28
- package/dist/query/where.js +129 -32
- package/dist/query/writes.d.ts +24 -5
- package/dist/query/writes.js +101 -13
- package/dist/realtime.d.ts +7 -7
- package/dist/realtime.js +9 -9
- package/dist/schema-builder.d.ts +18 -7
- package/dist/schema-builder.js +17 -10
- package/dist/schema-metadata.d.ts +3 -3
- package/dist/schema-metadata.js +9 -9
- package/dist/schema-sql.d.ts +9 -9
- package/dist/schema-sql.js +20 -20
- package/dist/schema.d.ts +19 -9
- package/dist/schema.js +6 -6
- package/dist/serverless.d.ts +15 -15
- package/dist/serverless.js +16 -16
- package/dist/sqlite.d.ts +8 -8
- package/dist/sqlite.js +53 -22
- package/dist/typed-sql.d.ts +4 -4
- package/dist/typed-sql.js +5 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Most query layers are designed for the shape of a laptop database: empty, dispos
|
|
|
8
8
|
npm install turbine-orm
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
**Full docs: [turbineorm.dev](https://turbineorm.dev)
|
|
11
|
+
**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)
|
|
12
12
|
|
|
13
13
|
## Why Turbine?
|
|
14
14
|
|
|
@@ -151,7 +151,7 @@ const user = await db.users.findUnique({
|
|
|
151
151
|
|
|
152
152
|
### Many-to-many relations
|
|
153
153
|
|
|
154
|
-
Turbine auto-detects pure junction tables during `generate
|
|
154
|
+
Turbine auto-detects pure junction tables during `generate`, a table whose primary key is exactly two single-column foreign keys and which carries no other columns (e.g. `posts_tags(post_id, tag_id)`). Both endpoints gain a many-to-many relation you can load like any other:
|
|
155
155
|
|
|
156
156
|
```typescript
|
|
157
157
|
const posts = await db.posts.findMany({
|
|
@@ -165,7 +165,7 @@ const post = await db.posts.findFirst({
|
|
|
165
165
|
});
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
A junction table that carries extra columns (a "payload") is treated as a first-class entity, so it stays an ordinary `hasMany
|
|
168
|
+
A junction table that carries extra columns (a "payload") is treated as a first-class entity, so it stays an ordinary `hasMany`. That's by design. For those, or for any junction you want to wire up by hand, declare the relation explicitly in your code-first schema:
|
|
169
169
|
|
|
170
170
|
```typescript
|
|
171
171
|
import { defineSchema } from 'turbine-orm';
|
|
@@ -205,7 +205,7 @@ The other nested operations (`create`, `connectOrCreate`, `update`, `upsert`, `d
|
|
|
205
205
|
|
|
206
206
|
### Self-relations
|
|
207
207
|
|
|
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
|
|
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:
|
|
209
209
|
|
|
210
210
|
```typescript
|
|
211
211
|
// A category with its parent and its children
|
|
@@ -267,7 +267,7 @@ const deleted = await db.users.delete({
|
|
|
267
267
|
For race-free counter updates, pass an operator object instead of a literal. Turbine generates `col = col + $n` style SQL so concurrent updates are safe.
|
|
268
268
|
|
|
269
269
|
```typescript
|
|
270
|
-
// Atomic increment
|
|
270
|
+
// Atomic increment, no read-modify-write race
|
|
271
271
|
await db.posts.update({
|
|
272
272
|
where: { id: 1 },
|
|
273
273
|
data: { viewCount: { increment: 1 } },
|
|
@@ -325,7 +325,7 @@ const stats = await db.raw<{ day: Date; count: number }>`
|
|
|
325
325
|
|
|
326
326
|
### Typed raw SQL (`db.sql<T>`)
|
|
327
327
|
|
|
328
|
-
`db.sql<T>` is the typed escape hatch: you supply the row shape and get a thenable query with `.one()` and `.scalar()` helpers. Every `${value}` is bound as a `$N` parameter
|
|
328
|
+
`db.sql<T>` is the typed escape hatch: you supply the row shape and get a thenable query with `.one()` and `.scalar()` helpers. Every `${value}` is bound as a `$N` parameter, never interpolated, so injection isn't possible even with hostile input.
|
|
329
329
|
|
|
330
330
|
```typescript
|
|
331
331
|
// Awaiting the query returns T[]
|
|
@@ -360,7 +360,7 @@ const users = await db.users.findMany({
|
|
|
360
360
|
### Streaming large result sets
|
|
361
361
|
|
|
362
362
|
```typescript
|
|
363
|
-
// Stream rows using PostgreSQL cursors
|
|
363
|
+
// Stream rows using PostgreSQL cursors, constant memory, no matter how many rows
|
|
364
364
|
for await (const user of db.users.findManyStream({
|
|
365
365
|
where: { orgId: 1 },
|
|
366
366
|
batchSize: 500, // internal FETCH batch size (default: 1000)
|
|
@@ -371,7 +371,7 @@ for await (const user of db.users.findManyStream({
|
|
|
371
371
|
}
|
|
372
372
|
```
|
|
373
373
|
|
|
374
|
-
Uses `DECLARE CURSOR` under the hood
|
|
374
|
+
Uses `DECLARE CURSOR` under the hood, rows are fetched in batches on a dedicated connection, parsed individually, and yielded via `AsyncGenerator`. Safe to `break` early; the cursor and connection are cleaned up automatically.
|
|
375
375
|
|
|
376
376
|
### Query timeout
|
|
377
377
|
|
|
@@ -406,17 +406,17 @@ const db = turbine({
|
|
|
406
406
|
// relation). Override per query on findMany/findFirst/findUnique.
|
|
407
407
|
relationLoadStrategy: 'auto',
|
|
408
408
|
// 'positional' (Postgres-only) drops repeated JSON keys from relation
|
|
409
|
-
// subqueries
|
|
409
|
+
// subqueries, ~39% fewer wire bytes on wide relations, byte-identical output.
|
|
410
410
|
// Default 'object'.
|
|
411
411
|
jsonEncoding: 'object',
|
|
412
|
-
// Parse `timestamp` (without time zone) as UTC
|
|
413
|
-
// convention
|
|
412
|
+
// Parse `timestamp` (without time zone) as UTC, the Prisma/Rails/Django
|
|
413
|
+
// convention, so results don't shift with the server's local zone.
|
|
414
414
|
// Default true; set false for the legacy local-time interpretation.
|
|
415
415
|
utcTimestamps: true,
|
|
416
416
|
});
|
|
417
417
|
```
|
|
418
418
|
|
|
419
|
-
Run `npx turbine doctor` to catch relations whose child-side FK lacks a covering index
|
|
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.
|
|
420
420
|
|
|
421
421
|
### Pool and statement configuration
|
|
422
422
|
|
|
@@ -467,7 +467,7 @@ Use `db.pipeline(...)` when the queries are independent and you want one round-t
|
|
|
467
467
|
|
|
468
468
|
### Middleware
|
|
469
469
|
|
|
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()
|
|
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.
|
|
471
471
|
|
|
472
472
|
```typescript
|
|
473
473
|
// Query timing
|
|
@@ -478,7 +478,7 @@ db.$use(async (params, next) => {
|
|
|
478
478
|
return result;
|
|
479
479
|
});
|
|
480
480
|
|
|
481
|
-
// Result transformation
|
|
481
|
+
// Result transformation, redact a field on the way out
|
|
482
482
|
db.$use(async (params, next) => {
|
|
483
483
|
const result = await next(params);
|
|
484
484
|
if (params.model === 'users' && Array.isArray(result)) {
|
|
@@ -488,7 +488,7 @@ db.$use(async (params, next) => {
|
|
|
488
488
|
});
|
|
489
489
|
```
|
|
490
490
|
|
|
491
|
-
> **Warning:** `params.args` is a read-only snapshot
|
|
491
|
+
> **Warning:** `params.args` is a read-only snapshot, mutating it does not change the executed SQL. The query is fully built and parameterized before middleware runs.
|
|
492
492
|
|
|
493
493
|
Middleware can't rewrite queries, so cross-cutting predicates like **soft deletes** and **multi-tenancy** belong to [global filters](https://turbineorm.dev/global-filters) instead. A global filter is a `WhereClause` that Turbine `AND`-merges into the compiled `WHERE` of every query on a table: reads, the relation subqueries that target it, and the predicate of `update` / `delete` / `upsert`. `create` and `createMany` are never filtered, since a new row has nothing to scope.
|
|
494
494
|
|
|
@@ -598,13 +598,13 @@ await db.$notify('order_created', JSON.stringify({ id: 1 }));
|
|
|
598
598
|
await sub.unsubscribe();
|
|
599
599
|
```
|
|
600
600
|
|
|
601
|
-
`$listen` holds a dedicated connection open for the lifetime of the subscription, so it requires a real persistent pool
|
|
601
|
+
`$listen` holds a dedicated connection open for the lifetime of the subscription, so it requires a real persistent pool. It is not available over serverless HTTP drivers. `$notify` is a single round-trip and works everywhere. Channel names are validated as plain identifiers; the payload is always bound as a parameter.
|
|
602
602
|
|
|
603
603
|
## Vector search (pgvector)
|
|
604
604
|
|
|
605
605
|
Query a `vector` column for nearest neighbors. Requires the [pgvector](https://github.com/pgvector/pgvector) extension and a `vector` column on your table.
|
|
606
606
|
|
|
607
|
-
**KNN ranking
|
|
607
|
+
**KNN ranking**, order by distance to a query vector and take the closest rows:
|
|
608
608
|
|
|
609
609
|
```typescript
|
|
610
610
|
const similar = await db.items.findMany({
|
|
@@ -614,7 +614,7 @@ const similar = await db.items.findMany({
|
|
|
614
614
|
// queryVector is a number[]; nearest-first by default (direction: 'desc' to invert)
|
|
615
615
|
```
|
|
616
616
|
|
|
617
|
-
**Distance filter
|
|
617
|
+
**Distance filter**, keep only rows within a distance threshold:
|
|
618
618
|
|
|
619
619
|
```typescript
|
|
620
620
|
const close = await db.items.findMany({
|
|
@@ -622,7 +622,7 @@ const close = await db.items.findMany({
|
|
|
622
622
|
});
|
|
623
623
|
```
|
|
624
624
|
|
|
625
|
-
`metric` selects the pgvector operator: `'l2'` → `<->` (Euclidean), `'cosine'` → `<=>` (cosine distance), `'ip'` → `<#>` (negative inner product). Distance filters accept `lt`, `lte`, `gt`, and `gte`. The query vector is always bound as `$n::vector
|
|
625
|
+
`metric` selects the pgvector operator: `'l2'` → `<->` (Euclidean), `'cosine'` → `<=>` (cosine distance), `'ip'` → `<#>` (negative inner product). Distance filters accept `lt`, `lte`, `gt`, and `gte`. The query vector is always bound as `$n::vector`, never interpolated.
|
|
626
626
|
|
|
627
627
|
> **Note:** pg has no built-in parser for the `vector` type, so a fetched `vector` column comes back as a string literal like `'[1,2,3]'` unless you register a parser (e.g. via pgvector's own client helpers). Querying by distance works regardless.
|
|
628
628
|
|
|
@@ -663,7 +663,7 @@ Every operator supported by the `where` clause. Operators compose freely with `A
|
|
|
663
663
|
| `endsWith` | Suffix match (`LIKE %v`) | `where: { email: { endsWith: '@acme.com' } }` |
|
|
664
664
|
| `mode: 'insensitive'` | Switch any string operator to `ILIKE` | `where: { title: { contains: 'SQL', mode: 'insensitive' } }` |
|
|
665
665
|
|
|
666
|
-
LIKE wildcards in user input are escaped automatically
|
|
666
|
+
LIKE wildcards in user input are escaped automatically, `%`, `_`, and `\` are treated as literals.
|
|
667
667
|
|
|
668
668
|
### Relation filters
|
|
669
669
|
|
|
@@ -780,8 +780,8 @@ npx turbine migrate status
|
|
|
780
780
|
```
|
|
781
781
|
|
|
782
782
|
**Destructive migrations require explicit confirmation.** If a pending migration (or a DOWN
|
|
783
|
-
section being rolled back) contains data-destroying SQL
|
|
784
|
-
`TRUNCATE`, `DELETE FROM`, `UPDATE` without `WHERE`, `ALTER COLUMN … TYPE
|
|
783
|
+
section being rolled back) contains data-destroying SQL, `DROP TABLE`, `DROP COLUMN`,
|
|
784
|
+
`TRUNCATE`, `DELETE FROM`, `UPDATE` without `WHERE`, `ALTER COLUMN … TYPE`, Turbine refuses
|
|
785
785
|
to run it and prints an itemized report. Interactively you must type `destroy my data` and
|
|
786
786
|
then `yes`; in CI you must pass `--allow-destructive`. A refused batch applies nothing.
|
|
787
787
|
|
|
@@ -800,8 +800,8 @@ npx turbine studio --port 5173 --host 127.0.0.1 --no-open
|
|
|
800
800
|
**Features**
|
|
801
801
|
|
|
802
802
|
- **Query / Data / Schema tabs.** Compose queries visually, browse rows, and inspect tables and relations.
|
|
803
|
-
- **ORM-native query composer.** The Query tab builds a real `findMany
|
|
804
|
-
- **Saved queries.** Named builder queries persisted to `.turbine/studio-queries.json
|
|
803
|
+
- **ORM-native query composer.** The Query tab builds a real `findMany`, drill into relations (`with`) to any depth, pick fields (`select`/`omit`), add filters (`where`), `orderBy`, and `limit` at every level, with a live TypeScript preview of the exact call to copy into your codebase.
|
|
804
|
+
- **Saved queries.** Named builder queries persisted to `.turbine/studio-queries.json`, share them across runs without committing them.
|
|
805
805
|
- **Cmd+K command palette.** Jump to any table, tab, or saved query in one keystroke.
|
|
806
806
|
- **Full-text search across rows.** The Data tab supports substring search across every text column of the current table.
|
|
807
807
|
- **PII redaction.** Columns tagged `pii: true` in the schema render as a redaction placeholder in every tab. `--show-pii` reveals them, with a loud startup warning. Tags are a code-first declaration (introspection never infers one), so Studio reads them from the generated metadata in your `out` directory; if it finds none it says so at startup rather than implying a protection it cannot apply.
|
|
@@ -817,22 +817,22 @@ npx turbine studio --port 5173 --host 127.0.0.1 --no-open
|
|
|
817
817
|
|
|
818
818
|
## Observability
|
|
819
819
|
|
|
820
|
-
Built-in query metrics with zero new dependencies. `$observe` buffers per-query timings in memory and flushes **per-minute aggregates
|
|
820
|
+
Built-in query metrics with zero new dependencies. `$observe` buffers per-query timings in memory and flushes **per-minute aggregates**, count, avg, p50, p95, p99, and error count per `model:action`, to a `_turbine_metrics` table in a **separate database**, over its own 1-connection pool so metrics writes never contend with your application pool.
|
|
821
821
|
|
|
822
822
|
```typescript
|
|
823
823
|
const handle = await db.$observe({
|
|
824
824
|
connectionString: process.env.TURBINE_OBSERVE_URL!, // metrics DB (not your app DB)
|
|
825
825
|
flushIntervalMs: 60_000, // default: 60s
|
|
826
|
-
retentionDays: 30, // default: 30
|
|
826
|
+
retentionDays: 30, // default: 30, older buckets are pruned on flush
|
|
827
827
|
});
|
|
828
828
|
|
|
829
829
|
// Later, to flush remaining metrics and close the metrics pool
|
|
830
830
|
await handle.stop();
|
|
831
831
|
```
|
|
832
832
|
|
|
833
|
-
`$observe` creates the `_turbine_metrics` table if it doesn't exist. Flushes are fire-and-forget (`INSERT ... ON CONFLICT` additive merge) and never throw into your application. If the `TURBINE_OBSERVE_URL` environment variable is set, the client starts observing automatically on construction
|
|
833
|
+
`$observe` creates the `_turbine_metrics` table if it doesn't exist. Flushes are fire-and-forget (`INSERT ... ON CONFLICT` additive merge) and never throw into your application. If the `TURBINE_OBSERVE_URL` environment variable is set, the client starts observing automatically on construction, no code needed.
|
|
834
834
|
|
|
835
|
-
For your own instrumentation, subscribe to query events with `$on('query')
|
|
835
|
+
For your own instrumentation, subscribe to query events with `$on('query')`, each event carries `sql`, `params`, `duration` (ms), `model`, `action`, `rows`, `timestamp`, and `error` (if the query failed):
|
|
836
836
|
|
|
837
837
|
```typescript
|
|
838
838
|
db.$on('query', (e) => {
|
|
@@ -853,7 +853,7 @@ Same security model as Studio: loopback by default, non-loopback refused without
|
|
|
853
853
|
|
|
854
854
|
## Serverless / Edge
|
|
855
855
|
|
|
856
|
-
Turbine's core is driver-agnostic: pass any pg-compatible pool to `TurbineConfig.pool` (or use the `turbineHttp()` factory) and Turbine runs on **Vercel Edge**, **Cloudflare Workers**, **Deno Deploy**, **Netlify Edge**, or any other environment where a direct TCP connection is unavailable. No new dependencies
|
|
856
|
+
Turbine's core is driver-agnostic: pass any pg-compatible pool to `TurbineConfig.pool` (or use the `turbineHttp()` factory) and Turbine runs on **Vercel Edge**, **Cloudflare Workers**, **Deno Deploy**, **Netlify Edge**, or any other environment where a direct TCP connection is unavailable. No new dependencies, install whichever driver you already use.
|
|
857
857
|
|
|
858
858
|
### Neon Serverless (HTTP / WebSocket)
|
|
859
859
|
|
|
@@ -888,7 +888,7 @@ const pool = createPool({ connectionString: process.env.POSTGRES_URL });
|
|
|
888
888
|
const db = turbineHttp(pool, SCHEMA);
|
|
889
889
|
```
|
|
890
890
|
|
|
891
|
-
### Supabase (direct Postgres
|
|
891
|
+
### Supabase (direct Postgres, no HTTP proxy needed)
|
|
892
892
|
|
|
893
893
|
```ts
|
|
894
894
|
import { TurbineClient } from 'turbine-orm';
|
|
@@ -921,27 +921,27 @@ export default {
|
|
|
921
921
|
|
|
922
922
|
- **Streaming cursors** (`findManyStream`) require `DECLARE CURSOR`, which most HTTP drivers don't support. Use `findMany` with `limit` + pagination instead.
|
|
923
923
|
- **LISTEN/NOTIFY** is not available over HTTP.
|
|
924
|
-
- Transactions work but hold an HTTP connection for their duration
|
|
924
|
+
- Transactions work but hold an HTTP connection for their duration, keep them short.
|
|
925
925
|
|
|
926
926
|
When Turbine receives an external pool, `db.disconnect()` is a no-op: the caller owns the pool's lifecycle.
|
|
927
927
|
|
|
928
928
|
## Database engines
|
|
929
929
|
|
|
930
|
-
Turbine is **Postgres-first
|
|
930
|
+
Turbine is **Postgres-first**, `import { TurbineClient } from 'turbine-orm'` targets PostgreSQL, and the safety bundle above is built around it. When you need another database, the same typed API runs on **SQLite**, **MySQL 8**, and **SQL Server** through subpath exports, plus **PowDB**, a single-node embedded database with its own query language (PowQL). Multi-engine is *additive*, not a pivot: pick the engine that fits, keep the same `findMany` / `with` / `where` API.
|
|
931
931
|
|
|
932
|
-
Two engines run **in-process** (no server): **SQLite** (always
|
|
932
|
+
Two engines run **in-process** (no server): **SQLite** (always. There is no SQLite wire protocol) and **PowDB**, which uniquely runs *both* in-process (embedded) *and* over a network client against the same data. The root install stays one dependency (`pg`). Each engine's driver is its own concern: SQLite needs nothing (Node's built-in `node:sqlite`), while MySQL, SQL Server, and PowDB use **optional peer dependencies** you install only if you use them.
|
|
933
933
|
|
|
934
934
|
```bash
|
|
935
|
-
# SQLite
|
|
935
|
+
# SQLite, zero extra deps (Node >= 22.5, built-in node:sqlite)
|
|
936
936
|
npm install turbine-orm
|
|
937
937
|
|
|
938
|
-
# MySQL 8
|
|
938
|
+
# MySQL 8, optional peer
|
|
939
939
|
npm install turbine-orm mysql2
|
|
940
940
|
|
|
941
|
-
# SQL Server 2016
|
|
941
|
+
# SQL Server 2016+, optional peer
|
|
942
942
|
npm install turbine-orm mssql
|
|
943
943
|
|
|
944
|
-
# PowDB
|
|
944
|
+
# PowDB, optional peer; embedded (in-process) or networked transport
|
|
945
945
|
npm install turbine-orm @zvndev/powdb-embedded # in-process
|
|
946
946
|
npm install turbine-orm @zvndev/powdb-client # networked
|
|
947
947
|
```
|
|
@@ -949,7 +949,7 @@ npm install turbine-orm @zvndev/powdb-client # networked
|
|
|
949
949
|
Each engine ships a factory that returns the same `TurbineClient`:
|
|
950
950
|
|
|
951
951
|
```ts
|
|
952
|
-
// SQLite
|
|
952
|
+
// SQLite, synchronous; pass a file path, ':memory:', or an open DatabaseSync
|
|
953
953
|
import { turbineSqlite } from 'turbine-orm/sqlite';
|
|
954
954
|
import { SCHEMA } from './generated/turbine/metadata.js';
|
|
955
955
|
|
|
@@ -958,7 +958,7 @@ const users = await db.users.findMany({ with: { posts: true }, limit: 10 });
|
|
|
958
958
|
```
|
|
959
959
|
|
|
960
960
|
```ts
|
|
961
|
-
// MySQL 8
|
|
961
|
+
// MySQL 8, async; connection string, mysql2 config, or an existing mysql2 pool
|
|
962
962
|
import { turbineMysql } from 'turbine-orm/mysql';
|
|
963
963
|
import { SCHEMA } from './generated/turbine/metadata.js';
|
|
964
964
|
|
|
@@ -966,7 +966,7 @@ const db = await turbineMysql('mysql://user:pass@localhost:3306/app', SCHEMA);
|
|
|
966
966
|
```
|
|
967
967
|
|
|
968
968
|
```ts
|
|
969
|
-
// SQL Server 2016
|
|
969
|
+
// SQL Server 2016+, async; connection string, mssql config, or an existing pool
|
|
970
970
|
import { turbineMssql } from 'turbine-orm/mssql';
|
|
971
971
|
import { SCHEMA } from './generated/turbine/metadata.js';
|
|
972
972
|
|
|
@@ -974,7 +974,7 @@ const db = await turbineMssql('mssql://sa:Passw0rd!@localhost:1433/app', SCHEMA)
|
|
|
974
974
|
```
|
|
975
975
|
|
|
976
976
|
```ts
|
|
977
|
-
// PowDB
|
|
977
|
+
// PowDB, async; embedded (in-process) or networked. Schema is code-defined.
|
|
978
978
|
import { turbinePowDB } from 'turbine-orm/powdb';
|
|
979
979
|
import { schemaDefToMetadata } from 'turbine-orm';
|
|
980
980
|
import { schema } from './schema.js'; // defineSchema({...})
|
|
@@ -1021,9 +1021,9 @@ Everything is honest about what ports and what doesn't. Features marked **PG-onl
|
|
|
1021
1021
|
|
|
1022
1022
|
✗ E017 = throws `UnsupportedFeatureError`. The full matrix (atomic updates, introspection, optimistic locking, per-cell mechanics) is on [turbineorm.dev/engines](https://turbineorm.dev/engines).
|
|
1023
1023
|
|
|
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
|
|
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`.
|
|
1025
1025
|
|
|
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
|
|
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)**.
|
|
1027
1027
|
|
|
1028
1028
|
Full setup, signatures, and the complete support matrix: **[turbineorm.dev/engines](https://turbineorm.dev/engines)**.
|
|
1029
1029
|
|
|
@@ -1050,7 +1050,7 @@ Priority order: CLI flags > environment variables (`DATABASE_URL`) > config file
|
|
|
1050
1050
|
|
|
1051
1051
|
## How It Works
|
|
1052
1052
|
|
|
1053
|
-
Turbine resolves nested relations the same way Prisma 7 and Drizzle 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
|
|
1053
|
+
Turbine resolves nested relations the same way Prisma 7 and Drizzle 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.
|
|
1054
1054
|
|
|
1055
1055
|
The query strategy is table stakes now. What isn't table stakes: the one-dependency, no-WASM footprint, 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.
|
|
1056
1056
|
|
|
@@ -1062,7 +1062,7 @@ Turbine maps Postgres types to TypeScript:
|
|
|
1062
1062
|
|---|---|---|
|
|
1063
1063
|
| `int2`, `int4`, `float4`, `float8` | `number` | Standard numeric types |
|
|
1064
1064
|
| `int8` / `bigint` | `number` | Values > `Number.MAX_SAFE_INTEGER` (2^53 - 1) are returned as `string` at runtime to avoid precision loss. This affects < 0.01% of use cases (auto-increment IDs, counts, etc. are all safe). |
|
|
1065
|
-
| `numeric`, `money` | `string` | Arbitrary precision
|
|
1065
|
+
| `numeric`, `money` | `string` | Arbitrary precision, kept as string to avoid JS float issues |
|
|
1066
1066
|
| `text`, `varchar`, `uuid`, `citext` | `string` | |
|
|
1067
1067
|
| `timestamptz`, `timestamp`, `date` | `Date` | `timestamp` (without time zone) is parsed as UTC by default (Prisma/Rails/Django convention), so the same row yields the same instant in every region. Opt out with `utcTimestamps: false`. |
|
|
1068
1068
|
| `boolean` | `boolean` | |
|
|
@@ -1093,43 +1093,43 @@ Reading the table: no engine binary and no WASM, just one runtime dependency (`p
|
|
|
1093
1093
|
|
|
1094
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.*
|
|
1095
1095
|
|
|
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
|
|
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.
|
|
1097
1097
|
|
|
1098
1098
|
## Limitations
|
|
1099
1099
|
|
|
1100
1100
|
Turbine is focused and opinionated. Here's what it doesn't do:
|
|
1101
1101
|
|
|
1102
|
-
- **Postgres-first.** PostgreSQL is the default and primary target
|
|
1103
|
-
- **Full-text search** is available via a `search` filter
|
|
1104
|
-
- **Large nested result sets.** Nested results are materialized server-side in PostgreSQL memory. For relations with 10K+ rows, always use `limit` in your `with` clause
|
|
1102
|
+
- **Postgres-first.** PostgreSQL is the default and primary target, going deep on one database is what enables the safety bundle and the edge-runtime story. SQLite, MySQL 8, and SQL Server engines are available as additive subpath exports (see [Database engines](#database-engines)), but several flagship features (pgvector, LISTEN/NOTIFY, RLS `sessionContext`, full-text `search`, array-column filters, `groupBy({ distinctOn })`) are Postgres-only and throw `UnsupportedFeatureError` elsewhere.
|
|
1103
|
+
- **Full-text search** is available via a `search` filter, `where: { title: { search: 'hello & world', config: 'english' } }` compiles to a parameterized `to_tsvector(...) @@ to_tsquery(...)`. PostgreSQL only: the other engines throw `UnsupportedFeatureError` (`TURBINE_E017`) rather than degrade to a `LIKE`. For advanced ranking (`ts_rank`, weighted vectors) use `db.raw`.
|
|
1104
|
+
- **Large nested result sets.** Nested results are materialized server-side in PostgreSQL memory. For relations with 10K+ rows, always use `limit` in your `with` clause, or stream the parents with `findManyStream` and resolve children per-row.
|
|
1105
1105
|
|
|
1106
1106
|
## Examples
|
|
1107
1107
|
|
|
1108
1108
|
**Feature demos**
|
|
1109
1109
|
|
|
1110
|
-
- **[Thread Machine](./examples/thread-machine/)
|
|
1111
|
-
- **[Streaming CSV](./examples/streaming-csv/)
|
|
1112
|
-
- **[Clickstorm](./examples/clickstorm/)
|
|
1110
|
+
- **[Thread Machine](./examples/thread-machine/)**, HN clone rendered from a single `findMany`. 4-level object graph (stories → comments → replies → author), every property autocompletes through the chain
|
|
1111
|
+
- **[Streaming CSV](./examples/streaming-csv/)**, Export 100K orders + line items to CSV with constant memory. PostgreSQL cursors, live heap meter, nested `with` inside `findManyStream`
|
|
1112
|
+
- **[Clickstorm](./examples/clickstorm/)**, Side-by-side atomic-increment vs read-modify-write load test. 10K concurrent clicks. The atomic path wins every time
|
|
1113
1113
|
|
|
1114
1114
|
**Runtime targets**
|
|
1115
1115
|
|
|
1116
|
-
- **[Next.js](./examples/nextjs/)
|
|
1117
|
-
- **[Neon Edge](./examples/neon-edge/)
|
|
1118
|
-
- **[Vercel Postgres](./examples/vercel-postgres/)
|
|
1119
|
-
- **[Cloudflare Worker](./examples/cloudflare-worker/)
|
|
1120
|
-
- **[Supabase](./examples/supabase/)
|
|
1116
|
+
- **[Next.js](./examples/nextjs/)**, Server-rendered app with nested relations, streaming, and live code demos
|
|
1117
|
+
- **[Neon Edge](./examples/neon-edge/)**, Vercel Edge route handler talking to Neon over HTTP via `@neondatabase/serverless`
|
|
1118
|
+
- **[Vercel Postgres](./examples/vercel-postgres/)**, Next.js app router route handler on `@vercel/postgres`
|
|
1119
|
+
- **[Cloudflare Worker](./examples/cloudflare-worker/)**, Worker `fetch` handler with `pg` over Cloudflare Hyperdrive
|
|
1120
|
+
- **[Supabase](./examples/supabase/)**, Standalone script over the standard `pg` driver against Supabase
|
|
1121
1121
|
|
|
1122
1122
|
## Guides
|
|
1123
1123
|
|
|
1124
|
-
- **[Quick Start](https://turbineorm.dev/quickstart)
|
|
1125
|
-
- **[API Reference](https://turbineorm.dev/queries)
|
|
1126
|
-
- **[Relations](https://turbineorm.dev/relations)
|
|
1127
|
-
- **[Transactions & Pipelines](https://turbineorm.dev/transactions)
|
|
1128
|
-
- **[Schema & Migrations](https://turbineorm.dev/schema)
|
|
1129
|
-
- **[Serverless & Edge](https://turbineorm.dev/serverless)
|
|
1130
|
-
- **[CLI](https://turbineorm.dev/cli)
|
|
1131
|
-
- **[Typed Errors](https://turbineorm.dev/errors)
|
|
1132
|
-
- **[Migrating from Prisma](https://turbineorm.dev/migrate-from-prisma)
|
|
1124
|
+
- **[Quick Start](https://turbineorm.dev/quickstart)**, zero-to-first-query in five minutes
|
|
1125
|
+
- **[API Reference](https://turbineorm.dev/queries)**, every `findMany` / `findUnique` / `create` / `update` / `delete` option, the full operator table, and `pipeline()` semantics
|
|
1126
|
+
- **[Relations](https://turbineorm.dev/relations)**, deep `with` clause, nested options, relation filters (`some` / `every` / `none`), payload-size guidance
|
|
1127
|
+
- **[Transactions & Pipelines](https://turbineorm.dev/transactions)**, isolation levels, nested SAVEPOINTs, retry loops for `DeadlockError` and `SerializationFailureError`
|
|
1128
|
+
- **[Schema & Migrations](https://turbineorm.dev/schema)**, `defineSchema()`, auto-diff migrations, checksum validation
|
|
1129
|
+
- **[Serverless & Edge](https://turbineorm.dev/serverless)**, Neon, Vercel Postgres, Cloudflare Hyperdrive, Supabase walkthroughs
|
|
1130
|
+
- **[CLI](https://turbineorm.dev/cli)**, every command, flag, and config option
|
|
1131
|
+
- **[Typed Errors](https://turbineorm.dev/errors)**, error code reference, `wrapPgError()` translation, retry patterns
|
|
1132
|
+
- **[Migrating from Prisma](https://turbineorm.dev/migrate-from-prisma)**, API mapping table, side-by-side `findMany`, and notes on the differences
|
|
1133
1133
|
|
|
1134
1134
|
## Requirements
|
|
1135
1135
|
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine-orm
|
|
2
|
+
* turbine-orm, CockroachDB adapter
|
|
3
3
|
*
|
|
4
4
|
* CockroachDB speaks the PostgreSQL wire protocol but has key differences:
|
|
5
5
|
*
|
|
6
|
-
* 1. **No advisory locks
|
|
6
|
+
* 1. **No advisory locks**, `pg_try_advisory_lock()` is not supported.
|
|
7
7
|
* This adapter uses a `_turbine_lock` table with `SELECT FOR UPDATE NOWAIT`
|
|
8
8
|
* as a concurrency mechanism for migrations.
|
|
9
9
|
*
|
|
10
|
-
* 2. **No `SET LOCAL statement_timeout
|
|
10
|
+
* 2. **No `SET LOCAL statement_timeout`**, CockroachDB uses
|
|
11
11
|
* `SET transaction_timeout` (v23.1+) for per-transaction time limits.
|
|
12
12
|
*
|
|
13
|
-
* 3. **`pg_indexes` view
|
|
13
|
+
* 3. **`pg_indexes` view**, CockroachDB supports `pg_indexes` since v22.1
|
|
14
14
|
* but the `indexdef` column may not match Postgres exactly. We use
|
|
15
15
|
* `SHOW INDEXES` as a more reliable alternative.
|
|
16
16
|
*
|
|
17
|
-
* 4. **`pg_class.reltuples
|
|
17
|
+
* 4. **`pg_class.reltuples`**, Not reliable in CockroachDB. We use
|
|
18
18
|
* `crdb_internal.table_row_statistics` for row estimates.
|
|
19
19
|
*
|
|
20
20
|
* Known limitations with Turbine on CockroachDB:
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine-orm
|
|
2
|
+
* turbine-orm, CockroachDB adapter
|
|
3
3
|
*
|
|
4
4
|
* CockroachDB speaks the PostgreSQL wire protocol but has key differences:
|
|
5
5
|
*
|
|
6
|
-
* 1. **No advisory locks
|
|
6
|
+
* 1. **No advisory locks**, `pg_try_advisory_lock()` is not supported.
|
|
7
7
|
* This adapter uses a `_turbine_lock` table with `SELECT FOR UPDATE NOWAIT`
|
|
8
8
|
* as a concurrency mechanism for migrations.
|
|
9
9
|
*
|
|
10
|
-
* 2. **No `SET LOCAL statement_timeout
|
|
10
|
+
* 2. **No `SET LOCAL statement_timeout`**, CockroachDB uses
|
|
11
11
|
* `SET transaction_timeout` (v23.1+) for per-transaction time limits.
|
|
12
12
|
*
|
|
13
|
-
* 3. **`pg_indexes` view
|
|
13
|
+
* 3. **`pg_indexes` view**, CockroachDB supports `pg_indexes` since v22.1
|
|
14
14
|
* but the `indexdef` column may not match Postgres exactly. We use
|
|
15
15
|
* `SHOW INDEXES` as a more reliable alternative.
|
|
16
16
|
*
|
|
17
|
-
* 4. **`pg_class.reltuples
|
|
17
|
+
* 4. **`pg_class.reltuples`**, Not reliable in CockroachDB. We use
|
|
18
18
|
* `crdb_internal.table_row_statistics` for row estimates.
|
|
19
19
|
*
|
|
20
20
|
* Known limitations with Turbine on CockroachDB:
|
|
@@ -69,7 +69,7 @@ const SQL_INDEXES_CRDB = `
|
|
|
69
69
|
`;
|
|
70
70
|
/**
|
|
71
71
|
* Row estimates using crdb_internal. Falls back gracefully if the view
|
|
72
|
-
* doesn't exist (permissions issue)
|
|
72
|
+
* doesn't exist (permissions issue), returns 0 rows in that case.
|
|
73
73
|
*/
|
|
74
74
|
const SQL_ROW_ESTIMATES_CRDB = `
|
|
75
75
|
SELECT
|
|
@@ -82,7 +82,7 @@ const SQL_ROW_ESTIMATES_CRDB = `
|
|
|
82
82
|
AND t.database_name = current_database()
|
|
83
83
|
`;
|
|
84
84
|
/**
|
|
85
|
-
* Enum introspection
|
|
85
|
+
* Enum introspection, CockroachDB supports pg_type/pg_enum since v20.2.
|
|
86
86
|
* The standard query works, but we include it explicitly so the override
|
|
87
87
|
* mechanism is complete.
|
|
88
88
|
*/
|
|
@@ -107,13 +107,13 @@ export const cockroachdb = {
|
|
|
107
107
|
await client.query(CREATE_LOCK_TABLE_SQL);
|
|
108
108
|
// Insert the lock row if it doesn't exist (idempotent)
|
|
109
109
|
await client.query(`INSERT INTO "${LOCK_TABLE}" (lock_id) VALUES ($1) ON CONFLICT (lock_id) DO NOTHING`, [lockId]);
|
|
110
|
-
// Try to acquire the row lock with NOWAIT
|
|
110
|
+
// Try to acquire the row lock with NOWAIT, fails immediately if held
|
|
111
111
|
try {
|
|
112
112
|
await client.query('BEGIN');
|
|
113
113
|
await client.query(`SELECT lock_id FROM "${LOCK_TABLE}" WHERE lock_id = $1 FOR UPDATE NOWAIT`, [lockId]);
|
|
114
114
|
// Update the acquired metadata
|
|
115
115
|
await client.query(`UPDATE "${LOCK_TABLE}" SET acquired_at = now(), acquired_by = current_user WHERE lock_id = $1`, [lockId]);
|
|
116
|
-
// Note: we leave the transaction OPEN
|
|
116
|
+
// Note: we leave the transaction OPEN, the lock is held until
|
|
117
117
|
// releaseLock() commits or rolls back.
|
|
118
118
|
return true;
|
|
119
119
|
}
|
|
@@ -129,7 +129,7 @@ export const cockroachdb = {
|
|
|
129
129
|
}
|
|
130
130
|
return false;
|
|
131
131
|
}
|
|
132
|
-
// Any other error
|
|
132
|
+
// Any other error, rollback and re-throw
|
|
133
133
|
try {
|
|
134
134
|
await client.query('ROLLBACK');
|
|
135
135
|
}
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine-orm
|
|
2
|
+
* turbine-orm, Database adapter interface
|
|
3
3
|
*
|
|
4
4
|
* Adapters allow Turbine to work with PostgreSQL-compatible databases that
|
|
5
5
|
* have subtle differences (e.g. CockroachDB, YugabyteDB). The default
|
|
6
|
-
* behavior remains standard PostgreSQL
|
|
6
|
+
* behavior remains standard PostgreSQL, adapters only override specific
|
|
7
7
|
* operations where compatibility gaps exist.
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
@@ -57,9 +57,9 @@ export interface DatabaseAdapter {
|
|
|
57
57
|
* Generate the SQL to set a statement timeout within a transaction.
|
|
58
58
|
* PostgreSQL uses `SELECT set_config('statement_timeout', $1, true)`.
|
|
59
59
|
* CockroachDB uses `SELECT set_config('transaction_timeout', $1, true)` (v23.1+).
|
|
60
|
-
* (`SET LOCAL ... = $1` is a syntax error
|
|
60
|
+
* (`SET LOCAL ... = $1` is a syntax error, SET takes no bind params.)
|
|
61
61
|
*
|
|
62
|
-
* @param seconds
|
|
62
|
+
* @param seconds, timeout in seconds
|
|
63
63
|
* @returns an object with the parameterized SQL and its bound values
|
|
64
64
|
*/
|
|
65
65
|
statementTimeout?(seconds: number): {
|
|
@@ -79,7 +79,7 @@ export interface DatabaseAdapter {
|
|
|
79
79
|
export declare const postgresql: DatabaseAdapter;
|
|
80
80
|
/**
|
|
81
81
|
* Google AlloyDB adapter. AlloyDB is PostgreSQL with Google's columnar storage
|
|
82
|
-
* engine. It is wire-protocol and catalog-compatible
|
|
82
|
+
* engine. It is wire-protocol and catalog-compatible, no adapter overrides
|
|
83
83
|
* are needed. All Turbine features (json_agg, advisory locks, introspection,
|
|
84
84
|
* migrations) work identically to standard PostgreSQL.
|
|
85
85
|
*
|
package/dist/adapters/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine-orm
|
|
2
|
+
* turbine-orm, Database adapter interface
|
|
3
3
|
*
|
|
4
4
|
* Adapters allow Turbine to work with PostgreSQL-compatible databases that
|
|
5
5
|
* have subtle differences (e.g. CockroachDB, YugabyteDB). The default
|
|
6
|
-
* behavior remains standard PostgreSQL
|
|
6
|
+
* behavior remains standard PostgreSQL, adapters only override specific
|
|
7
7
|
* operations where compatibility gaps exist.
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
|
-
// Default PostgreSQL adapter (no-op
|
|
18
|
+
// Default PostgreSQL adapter (no-op, standard behavior)
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
/**
|
|
21
21
|
* The default PostgreSQL adapter. Uses pg_try_advisory_lock, standard
|
|
@@ -31,18 +31,18 @@ export const postgresql = {
|
|
|
31
31
|
await client.query(`SELECT pg_advisory_unlock($1)`, [lockId]);
|
|
32
32
|
},
|
|
33
33
|
statementTimeout(seconds) {
|
|
34
|
-
// `SET LOCAL ... = $1` is a Postgres syntax error
|
|
34
|
+
// `SET LOCAL ... = $1` is a Postgres syntax error, SET does not accept
|
|
35
35
|
// bind parameters. `set_config(name, value, is_local=true)` is the
|
|
36
36
|
// parameterizable, transaction-local equivalent.
|
|
37
37
|
return { sql: `SELECT set_config('statement_timeout', $1, true)`, params: [`${seconds}s`] };
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
40
|
// ---------------------------------------------------------------------------
|
|
41
|
-
// AlloyDB
|
|
41
|
+
// AlloyDB, fully PostgreSQL-compatible, no adapter logic needed
|
|
42
42
|
// ---------------------------------------------------------------------------
|
|
43
43
|
/**
|
|
44
44
|
* Google AlloyDB adapter. AlloyDB is PostgreSQL with Google's columnar storage
|
|
45
|
-
* engine. It is wire-protocol and catalog-compatible
|
|
45
|
+
* engine. It is wire-protocol and catalog-compatible, no adapter overrides
|
|
46
46
|
* are needed. All Turbine features (json_agg, advisory locks, introspection,
|
|
47
47
|
* migrations) work identically to standard PostgreSQL.
|
|
48
48
|
*
|
|
@@ -58,7 +58,7 @@ export const alloydb = {
|
|
|
58
58
|
name: 'alloydb',
|
|
59
59
|
};
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
|
-
// TimescaleDB
|
|
61
|
+
// TimescaleDB, PostgreSQL extension, fully compatible
|
|
62
62
|
// ---------------------------------------------------------------------------
|
|
63
63
|
/**
|
|
64
64
|
* TimescaleDB adapter. Timescale is a PostgreSQL extension that adds
|