turbine-orm 0.16.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +180 -12
  2. package/dist/adapters/cockroachdb.js +4 -2
  3. package/dist/adapters/index.js +4 -1
  4. package/dist/adapters/yugabytedb.js +4 -2
  5. package/dist/cjs/adapters/cockroachdb.js +4 -2
  6. package/dist/cjs/adapters/index.js +4 -1
  7. package/dist/cjs/adapters/yugabytedb.js +4 -2
  8. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  9. package/dist/cjs/cli/studio.js +35 -73
  10. package/dist/cjs/client.js +164 -0
  11. package/dist/cjs/errors.js +35 -5
  12. package/dist/cjs/generate.js +14 -3
  13. package/dist/cjs/index.js +10 -2
  14. package/dist/cjs/introspect.js +81 -0
  15. package/dist/cjs/nested-write.js +70 -6
  16. package/dist/cjs/query/builder.js +581 -17
  17. package/dist/cjs/realtime.js +147 -0
  18. package/dist/cjs/schema-builder.js +86 -0
  19. package/dist/cjs/schema.js +10 -0
  20. package/dist/cjs/typed-sql.js +149 -0
  21. package/dist/cli/studio-ui.generated.js +1 -1
  22. package/dist/cli/studio.js +35 -73
  23. package/dist/client.d.ts +120 -0
  24. package/dist/client.js +165 -1
  25. package/dist/errors.js +35 -5
  26. package/dist/generate.js +14 -3
  27. package/dist/index.d.ts +4 -2
  28. package/dist/index.js +5 -1
  29. package/dist/introspect.js +81 -0
  30. package/dist/nested-write.js +70 -6
  31. package/dist/query/builder.d.ts +104 -1
  32. package/dist/query/builder.js +582 -18
  33. package/dist/query/index.d.ts +1 -1
  34. package/dist/query/types.d.ts +126 -2
  35. package/dist/realtime.d.ts +71 -0
  36. package/dist/realtime.js +144 -0
  37. package/dist/schema-builder.d.ts +68 -1
  38. package/dist/schema-builder.js +85 -0
  39. package/dist/schema.d.ts +18 -1
  40. package/dist/schema.js +10 -0
  41. package/dist/typed-sql.d.ts +101 -0
  42. package/dist/typed-sql.js +145 -0
  43. package/package.json +17 -15
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # turbine-orm
2
2
 
3
- 110 KB. One runtime dependency. The Postgres ORM that ships light and locks tight.
3
+ One dependency. No WASM engine. The Postgres ORM that ships light and locks tight.
4
4
 
5
5
  ```
6
6
  npm install turbine-orm
@@ -10,20 +10,20 @@ npm install turbine-orm
10
10
 
11
11
  ## Why Turbine?
12
12
 
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:
13
+ Prisma ships a 1.6 MB WASM query engine. Drizzle ships zero runtime but no Studio, no typed errors, no migration checksums. Turbine ships **one dependency (`pg`) and no engine binary**, and bundles six things no other TS ORM has together:
14
14
 
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.
15
+ 1. **One runtime dependency (`pg`).** No engine binary, no WASM adapter, no adapter packages to keep in lockstep. The main entry bundles to ~30 KB gzipped (~109 KB minified); the edge entry to ~21 KB gzipped. Prisma's WASM query engine alone is 1.6 MB.
16
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
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
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
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
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.
21
21
 
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.
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 one-dependency, no-WASM footprint, the read-only Studio, and the error messages that never leak user data.
23
23
 
24
24
  ## Benchmarks
25
25
 
26
- Tested against **Prisma 7.6** (adapter-pg, relationJoins preview on) and **Drizzle 0.45** (relational queries) on a **Neon** PostgreSQL database (pooled endpoint, US-East, PostgreSQL 17.8). 100 iterations, 20 warmup, Node v22. Same schema, same data (1K users, 10K posts, 50K comments), same connection pool config.
26
+ Tested against **Prisma 7.6** (adapter-pg, relationJoins preview on) and **Drizzle 0.45** (relational queries) on a **Neon** PostgreSQL database (pooled endpoint, US-East, PostgreSQL 17.8). 100 iterations, 20 warmup, Node v22. Same schema, same data (1K users, 10K posts, 50K comments), same connection pool config. _Measured April 2026 on turbine-orm 0.7.1; the core read path these scenarios exercise is unchanged through 0.17.0 — see [`benchmarks/RESULTS.md`](./benchmarks/RESULTS.md) to reproduce._
27
27
 
28
28
  | Scenario | Turbine | Prisma 7 | Drizzle v2 |
29
29
  |---|---|---|---|
@@ -43,7 +43,7 @@ Tested against **Prisma 7.6** (adapter-pg, relationJoins preview on) and **Drizz
43
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.
44
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.
45
45
 
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.
46
+ Performance is at parity with Prisma and Drizzle — the real reasons to choose Turbine are elsewhere: **one dependency and no WASM engine** (vs Prisma's 1.6 MB WASM query engine), 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.
47
47
 
48
48
  > Full analysis with p50/p95/p99 and methodology notes: [`benchmarks/RESULTS.md`](./benchmarks/RESULTS.md).
49
49
  > Reproduce: `cd benchmarks && npm install && npx prisma generate && DATABASE_URL=... npx tsx bench.ts`
@@ -118,6 +118,62 @@ const user = await db.users.findUnique({
118
118
  // user.posts is Post[] -- resolved in the same query
119
119
  ```
120
120
 
121
+ ### Many-to-many relations
122
+
123
+ 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:
124
+
125
+ ```typescript
126
+ const posts = await db.posts.findMany({
127
+ with: { tags: true }, // each post comes back with its tags array
128
+ });
129
+
130
+ // Nested where / orderBy / limit work on the m2m target too
131
+ const post = await db.posts.findFirst({
132
+ where: { id: 1 },
133
+ with: { tags: { where: { name: 'sql' }, orderBy: { name: 'asc' }, limit: 5 } },
134
+ });
135
+ ```
136
+
137
+ 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:
138
+
139
+ ```typescript
140
+ import { defineSchema } from 'turbine-orm';
141
+
142
+ export default defineSchema({
143
+ posts: {
144
+ id: { type: 'serial', primaryKey: true },
145
+ title: { type: 'text', notNull: true },
146
+ manyToMany: [
147
+ { name: 'tags', target: 'tags', through: 'postsTags',
148
+ sourceKey: 'postId', targetKey: 'tagId' },
149
+ ],
150
+ },
151
+ // ...tags and postsTags table definitions
152
+ });
153
+ ```
154
+
155
+ `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`.
156
+
157
+ ### Self-relations
158
+
159
+ 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:
160
+
161
+ ```typescript
162
+ // A category with its parent and its children
163
+ const category = await db.categories.findFirst({
164
+ where: { id: 2 },
165
+ with: { parent: true, children: true },
166
+ });
167
+
168
+ // Walk a level deeper
169
+ const tree = await db.categories.findFirst({
170
+ where: { id: 1 },
171
+ with: { children: { with: { children: true } } },
172
+ });
173
+ ```
174
+
175
+ When a table has a single self-referencing FK, Turbine auto-names the relations after the table: the `belongsTo` is named for the singular (`category`) and the `hasMany` for the table (`categories`). Rename them in your code-first schema if you prefer `parent`/`children`.
176
+
121
177
  ### create
122
178
 
123
179
  ```typescript
@@ -216,6 +272,29 @@ const stats = await db.raw<{ day: Date; count: number }>`
216
272
  `;
217
273
  ```
218
274
 
275
+ ### Typed raw SQL (`db.sql<T>`)
276
+
277
+ `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.
278
+
279
+ ```typescript
280
+ // Awaiting the query returns T[]
281
+ const users = await db.sql<{ id: number; name: string }>`
282
+ SELECT id, name FROM users WHERE org_id = ${orgId}
283
+ `;
284
+
285
+ // .one() returns T | null
286
+ const user = await db.sql<{ id: number; name: string }>`
287
+ SELECT id, name FROM users WHERE id = ${42}
288
+ `.one();
289
+
290
+ // .scalar() returns the first column of the first row, or null
291
+ const total = await db.sql<{ count: number }>`
292
+ SELECT COUNT(*)::int AS count FROM users
293
+ `.scalar();
294
+ ```
295
+
296
+ Reach for `db.sql<T>` when you want a hand-written query with a known return type; use `db.raw` when you don't need the typing or the helpers.
297
+
219
298
  ### Case-insensitive search
220
299
 
221
300
  ```typescript
@@ -305,10 +384,95 @@ try {
305
384
  }
306
385
  ```
307
386
 
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).
387
+ 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), `TURBINE_E015` (OptimisticLock), `TURBINE_E016` (ExclusionConstraint).
309
388
 
310
389
  Full reference with `wrapPgError()` translation, retry patterns for `DeadlockError` / `SerializationFailureError`, and safe vs verbose message modes: **[turbineorm.dev/errors](https://turbineorm.dev/errors)**.
311
390
 
391
+ ### groupBy with HAVING
392
+
393
+ `groupBy` aggregates rows by one or more columns. Add a `having` clause to filter the resulting groups by their aggregates. Every comparison value is parameterized.
394
+
395
+ ```typescript
396
+ // Users with more than one post
397
+ const prolific = await db.posts.groupBy({
398
+ by: ['userId'],
399
+ _count: true,
400
+ having: { _count: { gt: 1 } },
401
+ });
402
+
403
+ // Groups whose summed view count clears a threshold
404
+ const popular = await db.posts.groupBy({
405
+ by: ['published'],
406
+ _sum: { viewCount: true },
407
+ having: { viewCount: { _sum: { gte: 100 } } },
408
+ });
409
+ ```
410
+
411
+ Filter on the group count with `_count`, or on a column aggregate with `{ column: { _sum | _avg | _min | _max: { ... } } }`. Operators are `gt`, `gte`, `lt`, `lte`, `in`, and `notIn` (a bare number is shorthand for equality). `having` predicates combine with `AND`, and `where` filters rows *before* grouping while `having` filters groups *after*.
412
+
413
+ ### Multi-tenant queries with RLS session context
414
+
415
+ Set transaction-local Postgres settings (GUCs) so PostgreSQL Row-Level Security policies that call `current_setting()` filter rows for you. Pass `sessionContext` to `$transaction`, or use the `$withSession` shorthand.
416
+
417
+ ```typescript
418
+ // Postgres policy: USING (tenant_id = current_setting('app.current_tenant')::int)
419
+ const rows = await db.$transaction(
420
+ async (tx) => tx.documents.findMany(),
421
+ { sessionContext: { 'app.current_tenant': tenantId } },
422
+ );
423
+
424
+ // Shorthand for a single-purpose session
425
+ const rows2 = await db.$withSession(
426
+ { 'app.current_tenant': tenantId },
427
+ async (tx) => tx.documents.findMany(),
428
+ );
429
+ ```
430
+
431
+ Each entry is applied as `SELECT set_config(name, value, true)` right after `BEGIN`, so the setting is scoped to the transaction and resets automatically on commit. Values may be strings, numbers, or booleans (coerced to strings). Invalid setting names throw `ValidationError` and roll the transaction back before any query runs.
432
+
433
+ ### Realtime with LISTEN/NOTIFY
434
+
435
+ Subscribe to a Postgres channel with `$listen` and publish to it with `$notify`. The handler receives the notification payload as a string.
436
+
437
+ ```typescript
438
+ const sub = await db.$listen('order_created', (payload) => {
439
+ console.log('new order:', payload);
440
+ });
441
+
442
+ await db.$notify('order_created', JSON.stringify({ id: 1 }));
443
+
444
+ // Later, when you're done
445
+ await sub.unsubscribe();
446
+ ```
447
+
448
+ `$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.
449
+
450
+ ## Vector search (pgvector)
451
+
452
+ Query a `vector` column for nearest neighbors. Requires the [pgvector](https://github.com/pgvector/pgvector) extension and a `vector` column on your table.
453
+
454
+ **KNN ranking** — order by distance to a query vector and take the closest rows:
455
+
456
+ ```typescript
457
+ const similar = await db.items.findMany({
458
+ orderBy: { embedding: { distance: { to: queryVector, metric: 'cosine' } } },
459
+ limit: 5,
460
+ });
461
+ // queryVector is a number[]; nearest-first by default (direction: 'desc' to invert)
462
+ ```
463
+
464
+ **Distance filter** — keep only rows within a distance threshold:
465
+
466
+ ```typescript
467
+ const close = await db.items.findMany({
468
+ where: { embedding: { distance: { to: queryVector, metric: 'l2', lt: 0.3 } } },
469
+ });
470
+ ```
471
+
472
+ `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.
473
+
474
+ > **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.
475
+
312
476
  ## WHERE Operator Reference
313
477
 
314
478
  Every operator supported by the `where` clause. Operators compose freely with `AND`, `OR`, `NOT`, and the relation filters `some` / `every` / `none`.
@@ -573,7 +737,7 @@ Priority order: CLI flags > environment variables (`DATABASE_URL`) > config file
573
737
 
574
738
  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.
575
739
 
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.
740
+ 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.
577
741
 
578
742
  ## Type Mapping
579
743
 
@@ -595,25 +759,29 @@ Turbine maps Postgres types to TypeScript:
595
759
 
596
760
  | | **Turbine** | **Prisma** | **Drizzle** | **Kysely** |
597
761
  |---|---|---|---|---|
598
- | **Install size** | ~110 KB (`pg` only) | ~1.6 MB (WASM engine) | ~0 KB (no runtime) | ~0 KB (no runtime) |
762
+ | **Engine / runtime** | No engine binary (`pg` only) | Client + 1.6 MB WASM engine | No engine | No engine |
599
763
  | **Runtime deps** | 1 (`pg`) | `@prisma/client` + adapter | 0 | 0 |
764
+ | **Main bundle (gzip)** | ~30 KB | dominated by 1.6 MB WASM | ~7 KB core | small |
600
765
  | **Studio** | Read-only, 192-bit auth | Full CRUD, cloud-hosted | Paid tier | None |
601
766
  | **Error PII safety** | Keys only by default | Values in messages | Raw pg errors | Raw pg errors |
602
767
  | **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 |
768
+ | **Edge runtime** | One import swap, ~21 KB gzip | 1.6 MB WASM adapter | Native | Native |
604
769
  | **Pipeline batching** | Parse/Bind/Execute protocol | Sequential in txn | Sequential | Manual |
605
770
  | **Typed errors** | `isRetryable` discriminant | Error codes only | None | None |
606
771
  | **Nested relations** | 1 query, deep type inference | 1 query, shallow inference | 1 query, `relations()` re-declaration | Manual (`jsonArrayFrom`) |
772
+ | **Many-to-many** | Auto-detected from junctions | Implicit/explicit | Explicit `relations()` | Manual joins |
773
+ | **Vector search** | Built-in `distance` / KNN | Preview / raw | Extension API | Manual |
774
+ | **LISTEN/NOTIFY** | `$listen` / `$notify` | None | None | None |
607
775
  | **Multi-DB** | PostgreSQL only | PG, MySQL, SQLite, MSSQL | PG, MySQL, SQLite | PG, MySQL, SQLite |
608
776
 
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.
777
+ All three ORMs now do single-query nested loads — that's table stakes. Turbine's real differentiators: no engine binary or WASM just one dependency (`pg`), vs Prisma's 1.6 MB WASM query engine; 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.
610
778
 
611
779
  ## Limitations
612
780
 
613
781
  Turbine is focused and opinionated. Here's what it doesn't do:
614
782
 
615
783
  - **PostgreSQL only.** No MySQL, SQLite, or MSSQL. By design — going deep on one database enables the performance advantage and the edge-runtime story.
616
- - **No full-text search operators.** TSVECTOR/TSQUERY are not exposed in the query builder. Use `db.raw` for full-text queries.
784
+ - **Full-text search** is available via a `search` filter `where: { title: { search: 'hello & world', config: 'english' } }` compiles to a parameterized `to_tsvector(...) @@ to_tsquery(...)`. For advanced ranking (`ts_rank`, weighted vectors) use `db.raw`.
617
785
  - **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.
618
786
 
619
787
  ## Examples
@@ -165,8 +165,10 @@ export const cockroachdb = {
165
165
  rowEstimates: SQL_ROW_ESTIMATES_CRDB,
166
166
  },
167
167
  statementTimeout(seconds) {
168
- // CockroachDB v23.1+ supports transaction_timeout
169
- return { sql: `SET transaction_timeout = $1`, params: [`${seconds}s`] };
168
+ // CockroachDB v23.1+ supports transaction_timeout. `SET ... = $1` cannot
169
+ // take a bind parameter, so use the parameterizable set_config() form
170
+ // (is_local=true scopes it to the current transaction).
171
+ return { sql: `SELECT set_config('transaction_timeout', $1, true)`, params: [`${seconds}s`] };
170
172
  },
171
173
  };
172
174
  //# sourceMappingURL=cockroachdb.js.map
@@ -31,7 +31,10 @@ export const postgresql = {
31
31
  await client.query(`SELECT pg_advisory_unlock($1)`, [lockId]);
32
32
  },
33
33
  statementTimeout(seconds) {
34
- return { sql: `SET LOCAL statement_timeout = $1`, params: [`${seconds}s`] };
34
+ // `SET LOCAL ... = $1` is a Postgres syntax error — SET does not accept
35
+ // bind parameters. `set_config(name, value, is_local=true)` is the
36
+ // parameterizable, transaction-local equivalent.
37
+ return { sql: `SELECT set_config('statement_timeout', $1, true)`, params: [`${seconds}s`] };
35
38
  },
36
39
  };
37
40
  // ---------------------------------------------------------------------------
@@ -149,8 +149,10 @@ export const yugabytedb = {
149
149
  rowEstimates: SQL_ROW_ESTIMATES_YBDB,
150
150
  },
151
151
  statementTimeout(seconds) {
152
- // YugabyteDB supports standard PostgreSQL statement_timeout
153
- return { sql: `SET LOCAL statement_timeout = $1`, params: [`${seconds}s`] };
152
+ // YugabyteDB supports standard PostgreSQL statement_timeout. `SET LOCAL`
153
+ // cannot take a bind parameter, so use the parameterizable, transaction-
154
+ // local set_config() form.
155
+ return { sql: `SELECT set_config('statement_timeout', $1, true)`, params: [`${seconds}s`] };
154
156
  },
155
157
  };
156
158
  //# sourceMappingURL=yugabytedb.js.map
@@ -168,7 +168,9 @@ exports.cockroachdb = {
168
168
  rowEstimates: SQL_ROW_ESTIMATES_CRDB,
169
169
  },
170
170
  statementTimeout(seconds) {
171
- // CockroachDB v23.1+ supports transaction_timeout
172
- return { sql: `SET transaction_timeout = $1`, params: [`${seconds}s`] };
171
+ // CockroachDB v23.1+ supports transaction_timeout. `SET ... = $1` cannot
172
+ // take a bind parameter, so use the parameterizable set_config() form
173
+ // (is_local=true scopes it to the current transaction).
174
+ return { sql: `SELECT set_config('transaction_timeout', $1, true)`, params: [`${seconds}s`] };
173
175
  },
174
176
  };
@@ -34,7 +34,10 @@ exports.postgresql = {
34
34
  await client.query(`SELECT pg_advisory_unlock($1)`, [lockId]);
35
35
  },
36
36
  statementTimeout(seconds) {
37
- return { sql: `SET LOCAL statement_timeout = $1`, params: [`${seconds}s`] };
37
+ // `SET LOCAL ... = $1` is a Postgres syntax error — SET does not accept
38
+ // bind parameters. `set_config(name, value, is_local=true)` is the
39
+ // parameterizable, transaction-local equivalent.
40
+ return { sql: `SELECT set_config('statement_timeout', $1, true)`, params: [`${seconds}s`] };
38
41
  },
39
42
  };
40
43
  // ---------------------------------------------------------------------------
@@ -152,7 +152,9 @@ exports.yugabytedb = {
152
152
  rowEstimates: SQL_ROW_ESTIMATES_YBDB,
153
153
  },
154
154
  statementTimeout(seconds) {
155
- // YugabyteDB supports standard PostgreSQL statement_timeout
156
- return { sql: `SET LOCAL statement_timeout = $1`, params: [`${seconds}s`] };
155
+ // YugabyteDB supports standard PostgreSQL statement_timeout. `SET LOCAL`
156
+ // cannot take a bind parameter, so use the parameterizable, transaction-
157
+ // local set_config() form.
158
+ return { sql: `SELECT set_config('statement_timeout', $1, true)`, params: [`${seconds}s`] };
157
159
  },
158
160
  };