turbine-orm 0.51.0 → 0.52.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 (74) hide show
  1. package/README.md +33 -5
  2. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  3. package/dist/cjs/client.d.ts +106 -2
  4. package/dist/cjs/client.js +111 -5
  5. package/dist/cjs/dialect.d.ts +33 -0
  6. package/dist/cjs/dialect.js +14 -0
  7. package/dist/cjs/engine-config.d.ts +49 -0
  8. package/dist/cjs/engine-config.js +19 -0
  9. package/dist/cjs/index-advisor.js +0 -0
  10. package/dist/cjs/index.d.ts +1 -1
  11. package/dist/cjs/index.js +3 -2
  12. package/dist/cjs/mssql.d.ts +8 -3
  13. package/dist/cjs/mssql.js +22 -3
  14. package/dist/cjs/mysql.d.ts +7 -3
  15. package/dist/cjs/mysql.js +20 -3
  16. package/dist/cjs/nested-write.d.ts +31 -0
  17. package/dist/cjs/nested-write.js +80 -2
  18. package/dist/cjs/powdb-introspect.d.ts +10 -1
  19. package/dist/cjs/powdb-introspect.js +10 -1
  20. package/dist/cjs/powdb.d.ts +116 -6
  21. package/dist/cjs/powdb.js +169 -10
  22. package/dist/cjs/powql.d.ts +161 -1
  23. package/dist/cjs/powql.js +299 -19
  24. package/dist/cjs/prisma-compat.d.ts +54 -8
  25. package/dist/cjs/prisma-compat.js +136 -20
  26. package/dist/cjs/query/batched-loader.d.ts +7 -0
  27. package/dist/cjs/query/batched-loader.js +97 -15
  28. package/dist/cjs/query/builder.d.ts +131 -5
  29. package/dist/cjs/query/builder.js +223 -19
  30. package/dist/cjs/query/compound-unique.js +0 -0
  31. package/dist/cjs/query/index.d.ts +1 -1
  32. package/dist/cjs/query/index.js +2 -1
  33. package/dist/cjs/query/warn-registry.d.ts +10 -0
  34. package/dist/cjs/query/warn-registry.js +10 -0
  35. package/dist/cjs/query/writes.js +115 -7
  36. package/dist/cjs/sqlite.d.ts +10 -4
  37. package/dist/cjs/sqlite.js +18 -4
  38. package/dist/cli/studio-ui.generated.js +1 -1
  39. package/dist/client.d.ts +106 -2
  40. package/dist/client.js +111 -5
  41. package/dist/dialect.d.ts +33 -0
  42. package/dist/dialect.js +14 -0
  43. package/dist/engine-config.d.ts +49 -0
  44. package/dist/engine-config.js +18 -0
  45. package/dist/index-advisor.js +0 -0
  46. package/dist/index.d.ts +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/mssql.d.ts +8 -3
  49. package/dist/mssql.js +22 -3
  50. package/dist/mysql.d.ts +7 -3
  51. package/dist/mysql.js +20 -3
  52. package/dist/nested-write.d.ts +31 -0
  53. package/dist/nested-write.js +79 -2
  54. package/dist/powdb-introspect.d.ts +10 -1
  55. package/dist/powdb-introspect.js +10 -1
  56. package/dist/powdb.d.ts +116 -6
  57. package/dist/powdb.js +167 -9
  58. package/dist/powql.d.ts +161 -1
  59. package/dist/powql.js +299 -19
  60. package/dist/prisma-compat.d.ts +54 -8
  61. package/dist/prisma-compat.js +136 -20
  62. package/dist/query/batched-loader.d.ts +7 -0
  63. package/dist/query/batched-loader.js +98 -16
  64. package/dist/query/builder.d.ts +131 -5
  65. package/dist/query/builder.js +222 -18
  66. package/dist/query/compound-unique.js +0 -0
  67. package/dist/query/index.d.ts +1 -1
  68. package/dist/query/index.js +1 -1
  69. package/dist/query/warn-registry.d.ts +10 -0
  70. package/dist/query/warn-registry.js +10 -0
  71. package/dist/query/writes.js +116 -8
  72. package/dist/sqlite.d.ts +10 -4
  73. package/dist/sqlite.js +19 -5
  74. package/package.json +3 -3
package/dist/client.d.ts CHANGED
@@ -180,7 +180,19 @@ export interface TurbineConfig {
180
180
  * owns the pool) and when coercing nested-relation JSON dates. This is the
181
181
  * Prisma/Rails/Django convention and makes results independent of the
182
182
  * server's local time zone. Default: `true`. Set `false` for the legacy
183
- * local-time interpretation.
183
+ * local-time interpretation, which also turns off the matching WRITE-side
184
+ * rewrite (a bound `Date` on a zone-less `date` / `timestamp` column is then
185
+ * serialized by the driver in the process's zone).
186
+ *
187
+ * PER PROCESS, NOT PER CLIENT. The read half is a pg type parser, and
188
+ * `pg.types.setTypeParser` installs one parser per OID for the whole
189
+ * process. The first Turbine-owned client settles it for every later one, so
190
+ * constructing a second client with the OPPOSITE value throws a
191
+ * `ValidationError` rather than handing back a client whose writes and reads
192
+ * disagree. Give every client in the process the same value, or isolate the
193
+ * odd one in its own process. Clients on an EXTERNAL pool never register a
194
+ * parser and never take part in the check: they inherit whatever parser
195
+ * configuration the caller's driver has.
184
196
  */
185
197
  utcTimestamps?: boolean;
186
198
  /**
@@ -297,6 +309,23 @@ export interface TurbineConfig {
297
309
  * programmatic access regardless of mode.
298
310
  */
299
311
  errorMessages?: ErrorMessageMode;
312
+ /**
313
+ * Whether `$on('query')` listeners receive the real bound parameter values.
314
+ *
315
+ * Off by default: every entry of `event.params` is replaced with
316
+ * `'[REDACTED]'` before any listener sees it, so query logs cannot carry
317
+ * user data into a log sink.
318
+ *
319
+ * This is an alias with a discoverable name, NOT a second switch. The
320
+ * redaction has always been governed by {@link errorMessages}, which nobody
321
+ * looks under when they want to see query params. Resolution order, single
322
+ * source of truth: when `logQueryParams` is set it decides; otherwise
323
+ * `errorMessages: 'verbose'` reveals params exactly as it did before. So
324
+ * `logQueryParams: true` with `errorMessages: 'safe'` shows params in query
325
+ * events while keeping error MESSAGES redacted, which is the combination
326
+ * that was previously unreachable.
327
+ */
328
+ logQueryParams?: boolean;
300
329
  /**
301
330
  * Enable prepared statements. Queries are submitted with `{ name, text, values }`
302
331
  * to the pg driver, which caches the parse+plan on the server per connection.
@@ -470,6 +499,34 @@ export declare class TransactionClient {
470
499
  * Execute a raw SQL query within this transaction.
471
500
  */
472
501
  raw<T extends Record<string, unknown> = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
502
+ /**
503
+ * @internal The `turbine-orm/prisma-compat` adapter's transaction seam. NOT
504
+ * application API: use {@link raw}, whose tagged template makes concatenating
505
+ * a value into the SQL text impossible. This method takes the SQL text as a
506
+ * plain string, so the escaping discipline moves to the caller, which is
507
+ * exactly the property the typed-SQL escape hatch exists to remove. It is
508
+ * documented and kept structurally stable only because the compat adapter
509
+ * detects it by shape (`typeof tx.rawQuery === 'function'`) and refuses to
510
+ * run compat raw SQL on a pool connection when it is missing.
511
+ *
512
+ * Execute an already-parameterized statement on THIS transaction's own
513
+ * connection, returning the driver's `{ rows, rowCount }` pair. It differs
514
+ * from {@link raw} in two ways the adapter needs: it takes a prebuilt
515
+ * `(text, params)` pair rather than a tagged template (the placeholder
516
+ * numbering is the caller's, so nested `Prisma.sql` fragments can be
517
+ * flattened first), and it surfaces `rowCount` so an `$executeRaw`-style call
518
+ * can report affected rows.
519
+ *
520
+ * The statement runs on the transaction's dedicated connection, so it is
521
+ * inside the same BEGIN/COMMIT (and any active SAVEPOINT) as every other
522
+ * statement in the callback. Driver errors are translated by `wrapPgError`,
523
+ * exactly as pool-scoped and table-scoped queries are. Like {@link raw}, it
524
+ * emits no `$on('query')` event and runs no middleware.
525
+ */
526
+ rawQuery<T extends object = Record<string, unknown>>(text: string, params?: readonly unknown[]): Promise<{
527
+ rows: T[];
528
+ rowCount: number | null;
529
+ }>;
473
530
  /**
474
531
  * Create a pool-like wrapper around the transaction client.
475
532
  * This allows QueryInterface to work with the transaction connection
@@ -487,7 +544,19 @@ export declare class TurbineClient {
487
544
  /** The schema metadata this client was built from */
488
545
  readonly schema: SchemaMetadata;
489
546
  private static int8ParserRegistered;
490
- private static utcTimestampParserRegistered;
547
+ /**
548
+ * The `utcTimestamps` value the FIRST Turbine-owned pool in this process
549
+ * settled the OID 1114 read parser on, or `undefined` while no Turbine-owned
550
+ * client has been constructed yet.
551
+ *
552
+ * `pg.types.setTypeParser` is process-global by nature: there is one parser
553
+ * per OID for the whole pg module, so the READ side of `utcTimestamps` cannot
554
+ * be per client the way the WRITE side is. Recording the settled value (not
555
+ * just "registered yes/no") is what lets the constructor detect a second
556
+ * client asking for the opposite and refuse it, instead of handing back a
557
+ * client whose reads and writes disagree. See {@link assertUtcTimestampsAgree}.
558
+ */
559
+ private static utcTimestampParserMode;
491
560
  private readonly logging;
492
561
  /** Active SQL dialect, owns transaction keywords, set_config, raw-SQL placeholders, capability flags. */
493
562
  private readonly dialect;
@@ -496,6 +565,8 @@ export declare class TurbineClient {
496
565
  private readonly queryListeners;
497
566
  private queryOptions;
498
567
  private readonly errorMessagesSafe;
568
+ /** Whether `$on('query')` events carry real params (see `logQueryParams`). */
569
+ private readonly queryParamsVisible;
499
570
  /** True when Turbine created the pool and is responsible for tearing it down */
500
571
  private readonly ownsPool;
501
572
  /** Active LISTEN subscriptions, torn down on disconnect() so it never hangs */
@@ -520,6 +591,39 @@ export declare class TurbineClient {
520
591
  /** Lazily-built, cached primary-only view returned by {@link $primary}. */
521
592
  private primaryView?;
522
593
  constructor(config: TurbineConfig | undefined, schema: SchemaMetadata);
594
+ /**
595
+ * Refuse a `utcTimestamps` value that contradicts the one the process-global
596
+ * OID 1114 read parser was already settled on.
597
+ *
598
+ * The flag has two halves. The WRITE half is per client: a bound `Date` on a
599
+ * zone-less `date` / `timestamp` column is rewritten to a UTC literal unless
600
+ * the owning client opted out (`coerceWriteValue` in query/writes.ts). The
601
+ * READ half is the pg type parser for OID 1114, and `pg.types.setTypeParser`
602
+ * installs ONE parser per OID for the whole process, shared by every pool,
603
+ * every raw query, and any other library using the same pg module. There is
604
+ * no per-pool parser hook to bind it to, and moving the coercion into
605
+ * `parseRow` instead would leave every non-ORM read (raw SQL, `client.sql`,
606
+ * a caller's own `pool.query`) on the driver's value while changing the
607
+ * default path's output type, so the read half stays process-wide.
608
+ *
609
+ * That makes the mixed shape unserveable rather than merely awkward: the
610
+ * second client would write local calendar fields and read them back as UTC
611
+ * (or the reverse), so its own round trip is off by the process offset.
612
+ * A client that silently does not round-trip is the worst of the three
613
+ * outcomes, so construction fails with the two ways out.
614
+ *
615
+ * Only Turbine-owned pools take part. An external pool (Neon, Vercel
616
+ * Postgres, Hyperdrive) inherits the caller's parser configuration and
617
+ * Turbine never registers on its behalf, so it has no read half to contradict.
618
+ */
619
+ private static assertUtcTimestampsAgree;
620
+ /**
621
+ * @internal Test-only: forget the process-global `utcTimestamps` decision so
622
+ * one process can exercise both mismatch directions. It does NOT restore the
623
+ * pg parser (parser registration is not reversible), so reads stay on
624
+ * whichever parser was installed first.
625
+ */
626
+ static resetUtcTimestampsForTests(): void;
523
627
  /**
524
628
  * Register a middleware function that runs around every query.
525
629
  *
package/dist/client.js CHANGED
@@ -191,6 +191,39 @@ export class TransactionClient {
191
191
  throw wrapPgError(err);
192
192
  }
193
193
  }
194
+ /**
195
+ * @internal The `turbine-orm/prisma-compat` adapter's transaction seam. NOT
196
+ * application API: use {@link raw}, whose tagged template makes concatenating
197
+ * a value into the SQL text impossible. This method takes the SQL text as a
198
+ * plain string, so the escaping discipline moves to the caller, which is
199
+ * exactly the property the typed-SQL escape hatch exists to remove. It is
200
+ * documented and kept structurally stable only because the compat adapter
201
+ * detects it by shape (`typeof tx.rawQuery === 'function'`) and refuses to
202
+ * run compat raw SQL on a pool connection when it is missing.
203
+ *
204
+ * Execute an already-parameterized statement on THIS transaction's own
205
+ * connection, returning the driver's `{ rows, rowCount }` pair. It differs
206
+ * from {@link raw} in two ways the adapter needs: it takes a prebuilt
207
+ * `(text, params)` pair rather than a tagged template (the placeholder
208
+ * numbering is the caller's, so nested `Prisma.sql` fragments can be
209
+ * flattened first), and it surfaces `rowCount` so an `$executeRaw`-style call
210
+ * can report affected rows.
211
+ *
212
+ * The statement runs on the transaction's dedicated connection, so it is
213
+ * inside the same BEGIN/COMMIT (and any active SAVEPOINT) as every other
214
+ * statement in the callback. Driver errors are translated by `wrapPgError`,
215
+ * exactly as pool-scoped and table-scoped queries are. Like {@link raw}, it
216
+ * emits no `$on('query')` event and runs no middleware.
217
+ */
218
+ async rawQuery(text, params = []) {
219
+ try {
220
+ const result = await this.client.query(text, params);
221
+ return { rows: result.rows, rowCount: result.rowCount };
222
+ }
223
+ catch (err) {
224
+ throw wrapPgError(err);
225
+ }
226
+ }
194
227
  /**
195
228
  * Create a pool-like wrapper around the transaction client.
196
229
  * This allows QueryInterface to work with the transaction connection
@@ -240,7 +273,19 @@ export class TurbineClient {
240
273
  /** The schema metadata this client was built from */
241
274
  schema;
242
275
  static int8ParserRegistered = false;
243
- static utcTimestampParserRegistered = false;
276
+ /**
277
+ * The `utcTimestamps` value the FIRST Turbine-owned pool in this process
278
+ * settled the OID 1114 read parser on, or `undefined` while no Turbine-owned
279
+ * client has been constructed yet.
280
+ *
281
+ * `pg.types.setTypeParser` is process-global by nature: there is one parser
282
+ * per OID for the whole pg module, so the READ side of `utcTimestamps` cannot
283
+ * be per client the way the WRITE side is. Recording the settled value (not
284
+ * just "registered yes/no") is what lets the constructor detect a second
285
+ * client asking for the opposite and refuse it, instead of handing back a
286
+ * client whose reads and writes disagree. See {@link assertUtcTimestampsAgree}.
287
+ */
288
+ static utcTimestampParserMode;
244
289
  logging;
245
290
  /** Active SQL dialect, owns transaction keywords, set_config, raw-SQL placeholders, capability flags. */
246
291
  dialect;
@@ -249,6 +294,8 @@ export class TurbineClient {
249
294
  queryListeners = new Set();
250
295
  queryOptions;
251
296
  errorMessagesSafe;
297
+ /** Whether `$on('query')` events carry real params (see `logQueryParams`). */
298
+ queryParamsVisible;
252
299
  /** True when Turbine created the pool and is responsible for tearing it down */
253
300
  ownsPool = true;
254
301
  /** Active LISTEN subscriptions, torn down on disconnect() so it never hangs */
@@ -284,6 +331,7 @@ export class TurbineClient {
284
331
  this.logging = parent.logging;
285
332
  this.dialect = parent.dialect;
286
333
  this.errorMessagesSafe = parent.errorMessagesSafe;
334
+ this.queryParamsVisible = parent.queryParamsVisible;
287
335
  this.queryOptions = parent.queryOptions;
288
336
  this.middlewares = parent.middlewares; // shared reference: $use on parent flows through
289
337
  this.pool = parent.pool;
@@ -367,9 +415,18 @@ export class TurbineClient {
367
415
  // ORM convention (Prisma, Rails, Django), and the only interpretation
368
416
  // that round-trips what Postgres stores, is UTC. Same ownership rule as
369
417
  // the int8 parser: never mutate parser state on external pools.
370
- if (ownsAnyPool && config.utcTimestamps !== false && !TurbineClient.utcTimestampParserRegistered) {
371
- pg.types.setTypeParser(1114, (val) => new Date(`${val.replace(' ', 'T')}Z`));
372
- TurbineClient.utcTimestampParserRegistered = true;
418
+ //
419
+ // Read registration is process-global and one-shot; the WRITE side of the
420
+ // same flag is per client (query/writes.ts). Two clients disagreeing about
421
+ // it therefore cannot both be served, so the disagreement is refused here
422
+ // rather than resolved silently into a client that does not round-trip.
423
+ if (ownsAnyPool) {
424
+ const wantUtcTimestamps = config.utcTimestamps !== false;
425
+ TurbineClient.assertUtcTimestampsAgree(wantUtcTimestamps);
426
+ if (wantUtcTimestamps && TurbineClient.utcTimestampParserMode === undefined) {
427
+ pg.types.setTypeParser(1114, (val) => new Date(`${val.replace(' ', 'T')}Z`));
428
+ }
429
+ TurbineClient.utcTimestampParserMode = wantUtcTimestamps;
373
430
  }
374
431
  this.logging = config.logging ?? false;
375
432
  this.dialect = config.dialect ?? postgresDialect;
@@ -377,6 +434,10 @@ export class TurbineClient {
377
434
  // Respect env var kill switch
378
435
  const envDisablePrepared = typeof process !== 'undefined' && process.env?.TURBINE_DISABLE_PREPARED === '1';
379
436
  this.errorMessagesSafe = (config.errorMessages ?? 'safe') === 'safe';
437
+ // Query-event param visibility. One derived boolean, so the two config
438
+ // spellings can never disagree: `logQueryParams` wins when set, otherwise
439
+ // `errorMessages` keeps deciding exactly as it always has.
440
+ this.queryParamsVisible = config.logQueryParams ?? !this.errorMessagesSafe;
380
441
  this.queryOptions = {
381
442
  defaultLimit: config.defaultLimit,
382
443
  warnOnUnlimited: config.warnOnUnlimited,
@@ -401,7 +462,7 @@ export class TurbineClient {
401
462
  _onQuery: (event) => {
402
463
  if (this.queryListeners.size === 0)
403
464
  return;
404
- const emitted = this.errorMessagesSafe ? { ...event, params: event.params.map(() => '[REDACTED]') } : event;
465
+ const emitted = this.queryParamsVisible ? event : { ...event, params: event.params.map(() => '[REDACTED]') };
405
466
  for (const listener of this.queryListeners) {
406
467
  try {
407
468
  listener(emitted);
@@ -514,6 +575,51 @@ export class TurbineClient {
514
575
  this.$observe({ connectionString: observeUrl }).catch(() => { });
515
576
  }
516
577
  }
578
+ /**
579
+ * Refuse a `utcTimestamps` value that contradicts the one the process-global
580
+ * OID 1114 read parser was already settled on.
581
+ *
582
+ * The flag has two halves. The WRITE half is per client: a bound `Date` on a
583
+ * zone-less `date` / `timestamp` column is rewritten to a UTC literal unless
584
+ * the owning client opted out (`coerceWriteValue` in query/writes.ts). The
585
+ * READ half is the pg type parser for OID 1114, and `pg.types.setTypeParser`
586
+ * installs ONE parser per OID for the whole process, shared by every pool,
587
+ * every raw query, and any other library using the same pg module. There is
588
+ * no per-pool parser hook to bind it to, and moving the coercion into
589
+ * `parseRow` instead would leave every non-ORM read (raw SQL, `client.sql`,
590
+ * a caller's own `pool.query`) on the driver's value while changing the
591
+ * default path's output type, so the read half stays process-wide.
592
+ *
593
+ * That makes the mixed shape unserveable rather than merely awkward: the
594
+ * second client would write local calendar fields and read them back as UTC
595
+ * (or the reverse), so its own round trip is off by the process offset.
596
+ * A client that silently does not round-trip is the worst of the three
597
+ * outcomes, so construction fails with the two ways out.
598
+ *
599
+ * Only Turbine-owned pools take part. An external pool (Neon, Vercel
600
+ * Postgres, Hyperdrive) inherits the caller's parser configuration and
601
+ * Turbine never registers on its behalf, so it has no read half to contradict.
602
+ */
603
+ static assertUtcTimestampsAgree(want) {
604
+ const settled = TurbineClient.utcTimestampParserMode;
605
+ if (settled === undefined || settled === want)
606
+ return;
607
+ throw new ValidationError(`[turbine] utcTimestamps: ${want} conflicts with utcTimestamps: ${settled}, which an earlier TurbineClient ` +
608
+ 'in this process already applied. The timestamp READ parser (pg OID 1114) is process-global, so it cannot ' +
609
+ 'differ per client, while the WRITE side is per client. Serving both values would give this client a ' +
610
+ `${want ? 'UTC write' : 'local write'} and a ${settled ? 'UTC read' : 'local read'}, so every zone-less ` +
611
+ '`timestamp` it writes would read back shifted by the process offset. Give every TurbineClient in this ' +
612
+ 'process the same `utcTimestamps` value, or run the odd one out in its own process.');
613
+ }
614
+ /**
615
+ * @internal Test-only: forget the process-global `utcTimestamps` decision so
616
+ * one process can exercise both mismatch directions. It does NOT restore the
617
+ * pg parser (parser registration is not reversible), so reads stay on
618
+ * whichever parser was installed first.
619
+ */
620
+ static resetUtcTimestampsForTests() {
621
+ TurbineClient.utcTimestampParserMode = undefined;
622
+ }
517
623
  // -------------------------------------------------------------------------
518
624
  // Middleware, intercept all queries
519
625
  // -------------------------------------------------------------------------
package/dist/dialect.d.ts CHANGED
@@ -52,6 +52,20 @@ export interface BulkInsertStatementInput {
52
52
  */
53
53
  requireRowValues?: boolean;
54
54
  }
55
+ export interface DefaultValuesInsertStatementInput {
56
+ /** SQL-ready quoted table name. */
57
+ table: string;
58
+ /**
59
+ * How many all-defaults rows to insert. Always a positive integer (the length
60
+ * of the caller's `data` array), never a user-supplied value, so a dialect may
61
+ * render it as a literal.
62
+ */
63
+ rowCount: number;
64
+ /** Skip duplicate rows when supported by the dialect. */
65
+ skipDuplicates?: boolean;
66
+ /** Optional SQL-ready RETURNING selection. */
67
+ returning?: ReturningSelection;
68
+ }
55
69
  /**
56
70
  * The cast/decode pair for one JSON-divergent column type. See
57
71
  * {@link Dialect.jsonWireRule}. The two halves MUST agree: a cast with no
@@ -382,6 +396,25 @@ export interface Dialect {
382
396
  buildInsertStatement(input: InsertStatementInput): string;
383
397
  /** Build a multi-row bulk INSERT statement and its dialect-shaped params. */
384
398
  buildBulkInsertStatement(input: BulkInsertStatementInput): BuiltStatement;
399
+ /**
400
+ * Build an INSERT that binds NO column values, so every column takes its
401
+ * declared default: `create({ data: {} })` and `createMany({ data: [{}, …] })`.
402
+ *
403
+ * The naive `INSERT INTO t () VALUES ()` the column-driven builders produce for
404
+ * an empty `data` is a syntax error on every engine but MySQL, and an empty
405
+ * `data` object is easy to reach honestly (a handler that assembles its payload
406
+ * from optional request fields, against a table where every column has a
407
+ * default or is nullable). The correct statement differs per engine, so it
408
+ * lives here rather than as an engine test in the write builders.
409
+ *
410
+ * Implementations MUST honor `rowCount`, and MUST throw an
411
+ * {@link UnsupportedFeatureError} naming the engine when the engine cannot
412
+ * express the requested shape (several engines have a single-row-only
413
+ * `DEFAULT VALUES` and no multi-row equivalent) rather than emitting SQL the
414
+ * database will reject. Optional: a dialect that omits it raises E017 for both
415
+ * shapes.
416
+ */
417
+ buildDefaultValuesInsertStatement?(input: DefaultValuesInsertStatementInput): string;
385
418
  /** Build an upsert statement. Inputs are SQL-ready quoted fragments. */
386
419
  buildUpsertStatement(input: UpsertStatementInput): string;
387
420
  /** Whether native ILIKE is supported. */
package/dist/dialect.js CHANGED
@@ -110,6 +110,20 @@ export const postgresDialect = {
110
110
  sql += ' ON CONFLICT DO NOTHING';
111
111
  return { sql: `${sql}${this.buildReturningClause(input.returning)}`, params: columnArrays };
112
112
  },
113
+ buildDefaultValuesInsertStatement(input) {
114
+ const conflict = input.skipDuplicates ? ' ON CONFLICT DO NOTHING' : '';
115
+ if (input.rowCount === 1) {
116
+ return `INSERT INTO ${input.table} DEFAULT VALUES${conflict}${this.buildReturningClause(input.returning)}`;
117
+ }
118
+ // N all-defaults rows: `DEFAULT VALUES` is single-row only, so feed the
119
+ // INSERT a zero-column SELECT of N source rows instead. Every column still
120
+ // takes its default (nothing is projected into the INSERT), and unlike
121
+ // `VALUES (DEFAULT), (DEFAULT)` it does not depend on the table's first
122
+ // column accepting the DEFAULT keyword. `rowCount` is the caller's array
123
+ // length, so the literal is an internal integer, not a bound user value.
124
+ return (`INSERT INTO ${input.table} SELECT FROM generate_series(1, ${input.rowCount})` +
125
+ `${conflict}${this.buildReturningClause(input.returning)}`);
126
+ },
113
127
  buildUpsertStatement(input) {
114
128
  return (`INSERT INTO ${input.table} (${input.insertColumns.join(', ')}) VALUES (${input.valuePlaceholders.join(', ')})` +
115
129
  ` ON CONFLICT (${input.conflictColumns.join(', ')}) DO UPDATE SET ${input.updateSetClauses.join(', ')}` +
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Shared client-config seam for the non-Postgres engine factories
3
+ * (`turbine-orm/sqlite`, `turbine-orm/mysql`, `turbine-orm/mssql`).
4
+ *
5
+ * Each factory builds its own driver pool and then hands a {@link TurbineConfig}
6
+ * to `TurbineClient`. Historically each one listed the config keys it forwarded
7
+ * by hand, which made the forwarded set an ALLOWLIST: every option added to
8
+ * `TurbineConfig` afterwards silently became unreachable on three engines until
9
+ * someone remembered to edit three literals (that is exactly how `errorMessages`
10
+ * and later `logQueryParams` ended up Postgres-only).
11
+ *
12
+ * {@link EngineClientConfig} inverts the default. Every `TurbineConfig` field is
13
+ * forwarded EXCEPT the ones listed below, so a new client option is engine-wide
14
+ * the day it lands and only a deliberate `Omit` entry can take it away again.
15
+ * Each engine's options interface extends this type and its factory spreads the
16
+ * config through verbatim.
17
+ */
18
+ import type { TurbineConfig } from './client.js';
19
+ /**
20
+ * `TurbineConfig` keys an engine factory does NOT forward. Every exclusion is
21
+ * deliberate; nothing else about `TurbineConfig` is engine-specific.
22
+ *
23
+ * - `pool`, `dialect`, `preparedStatements`: owned by the factory. It binds
24
+ * the driver pool and its dialect, and pins prepared statements off (these
25
+ * drivers cache plans themselves). Letting a caller set them would unbind
26
+ * the engine from its own SQL.
27
+ * - `connectionString`, `host`, `port`, `database`, `user`, `password`, `ssl`,
28
+ * `poolSize`, `idleTimeoutMs`, `connectionTimeoutMs`, `max`,
29
+ * `idleTimeoutMillis`, `connectionTimeoutMillis`: pg connection and
30
+ * pool-tuning fields, ignored by `TurbineClient` whenever `pool` is set (it
31
+ * always is here). Each engine takes its connection target as its first
32
+ * argument instead, in that driver's own vocabulary.
33
+ * - `replicas`: entries are Postgres connection strings or pg-compatible
34
+ * pools, which `TurbineClient` opens as `pg.Pool`s. There is no engine
35
+ * equivalent to route reads to.
36
+ *
37
+ * Everything else is forwarded, including options that are Postgres-only by
38
+ * CAPABILITY rather than by plumbing (`jsonEncoding: 'positional'`). Those throw
39
+ * a typed `UnsupportedFeatureError` (E017) when used, which is a better answer
40
+ * than an option the type system says does not exist.
41
+ */
42
+ type EngineOwnedConfigKey = 'pool' | 'dialect' | 'preparedStatements' | 'connectionString' | 'host' | 'port' | 'database' | 'user' | 'password' | 'ssl' | 'poolSize' | 'idleTimeoutMs' | 'connectionTimeoutMs' | 'max' | 'idleTimeoutMillis' | 'connectionTimeoutMillis' | 'replicas';
43
+ /**
44
+ * The client-level options every engine factory accepts and forwards to
45
+ * `TurbineClient` unchanged: all of {@link TurbineConfig} minus the connection
46
+ * and dialect plumbing the factory owns (see {@link EngineOwnedConfigKey}).
47
+ */
48
+ export type EngineClientConfig = Omit<TurbineConfig, EngineOwnedConfigKey>;
49
+ export {};
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shared client-config seam for the non-Postgres engine factories
3
+ * (`turbine-orm/sqlite`, `turbine-orm/mysql`, `turbine-orm/mssql`).
4
+ *
5
+ * Each factory builds its own driver pool and then hands a {@link TurbineConfig}
6
+ * to `TurbineClient`. Historically each one listed the config keys it forwarded
7
+ * by hand, which made the forwarded set an ALLOWLIST: every option added to
8
+ * `TurbineConfig` afterwards silently became unreachable on three engines until
9
+ * someone remembered to edit three literals (that is exactly how `errorMessages`
10
+ * and later `logQueryParams` ended up Postgres-only).
11
+ *
12
+ * {@link EngineClientConfig} inverts the default. Every `TurbineConfig` field is
13
+ * forwarded EXCEPT the ones listed below, so a new client option is engine-wide
14
+ * the day it lands and only a deliberate `Omit` entry can take it away again.
15
+ * Each engine's options interface extends this type and its factory spreads the
16
+ * config through verbatim.
17
+ */
18
+ export {};
Binary file
package/dist/index.d.ts CHANGED
@@ -43,7 +43,7 @@ export { type IntrospectOptions, introspect } from './introspect.js';
43
43
  export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
44
44
  export { HttpJsonSink, type HttpJsonSinkOptions, type MetricsFlushBatch, type MetricsFlushRow, type ObserveConfig, type ObserveHandle, type ObserveSink, PgMetricsSink, type PgMetricsSinkOptions, } from './observe.js';
45
45
  export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
46
- export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
46
+ export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
47
47
  export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
48
48
  export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
49
49
  export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, withDbFieldNames, } from './schema.js';
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ export { HttpJsonSink, PgMetricsSink, } from './observe.js';
49
49
  // Pipeline
50
50
  export { executePipeline, pipelineSupported } from './pipeline.js';
51
51
  // Query builder
52
- export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, } from './query/index.js';
52
+ export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, } from './query/index.js';
53
53
  // Realtime, LISTEN/NOTIFY pub/sub
54
54
  export { validateChannel } from './realtime.js';
55
55
  // Schema utilities
package/dist/mssql.d.ts CHANGED
@@ -89,8 +89,9 @@
89
89
  * await db.disconnect();
90
90
  * ```
91
91
  */
92
- import { type PgCompatPool, type PgCompatPoolClient, TurbineClient, type TurbineConfig } from './client.js';
92
+ import { type PgCompatPool, type PgCompatPoolClient, TurbineClient } from './client.js';
93
93
  import { type Dialect, type IntrospectOptions } from './dialect.js';
94
+ import type { EngineClientConfig } from './engine-config.js';
94
95
  import { type SchemaMetadata } from './schema.js';
95
96
  interface MssqlRequest {
96
97
  input(name: string, value: unknown): MssqlRequest;
@@ -204,8 +205,12 @@ interface MssqlConnectionConfig {
204
205
  trustServerCertificate?: boolean;
205
206
  };
206
207
  }
207
- /** Options for {@link turbineMssql}. Mirrors the relevant {@link TurbineConfig} fields. */
208
- export interface TurbineMssqlOptions extends Pick<TurbineConfig, 'logging' | 'defaultLimit' | 'warnOnUnlimited'> {
208
+ /**
209
+ * Options for {@link turbineMssql}: every client-level `TurbineConfig` field the
210
+ * engine can honour (see {@link EngineClientConfig}) plus the SQL Server schema
211
+ * below.
212
+ */
213
+ export interface TurbineMssqlOptions extends EngineClientConfig {
209
214
  /** SQL Server schema for introspection / DDL (default `dbo`). */
210
215
  schema?: string;
211
216
  }
package/dist/mssql.js CHANGED
@@ -592,6 +592,22 @@ export const mssqlDialect = {
592
592
  params: input.rowValues.flat(),
593
593
  };
594
594
  },
595
+ buildDefaultValuesInsertStatement(input) {
596
+ // T-SQL puts OUTPUT between the (absent) column list and the value source,
597
+ // so `DEFAULT VALUES` follows it, exactly like the VALUES form above.
598
+ if (input.skipDuplicates) {
599
+ throw new UnsupportedFeatureError('createMany({ skipDuplicates: true })', 'mssql', 'SQL Server has no ON CONFLICT DO NOTHING equivalent, pre-filter conflicting rows or use a MERGE.');
600
+ }
601
+ // `DEFAULT VALUES` is single-row only in T-SQL, and the multi-row VALUES
602
+ // form needs one DEFAULT keyword per column, which rows of pure defaults
603
+ // do not name.
604
+ if (input.rowCount !== 1) {
605
+ throw new UnsupportedFeatureError(`createMany with ${input.rowCount} empty data rows`, 'mssql', 'SQL Server INSERT … DEFAULT VALUES inserts a single row and has no multi-row form; ' +
606
+ 'issue one create({ data: {} }) per row.');
607
+ }
608
+ const out = mssqlOutput(input.returning, 'INSERTED');
609
+ return `INSERT INTO ${input.table}${out} DEFAULT VALUES`;
610
+ },
595
611
  buildUpsertStatement(input) {
596
612
  // MERGE is the SQL Server upsert. The MERGE statement MUST end with `;`.
597
613
  // CONCURRENCY CAVEAT: MERGE is not a substitute for a UNIQUE/PK constraint -
@@ -1280,13 +1296,16 @@ export async function turbineMssql(target, schema, options = {}) {
1280
1296
  throw err;
1281
1297
  // A non-version probe failure (permissions, etc.) should not block startup.
1282
1298
  }
1299
+ // Everything that is not a SQL Server introspection option is client config
1300
+ // and rides through untouched, so a new TurbineConfig option works here the
1301
+ // day it lands. The engine-owned keys come last, so no caller can unbind the
1302
+ // dialect.
1303
+ const { schema: _dbSchema, ...clientConfig } = options;
1283
1304
  const client = new TurbineClient({
1305
+ ...clientConfig,
1284
1306
  pool,
1285
1307
  dialect: mssqlDialect,
1286
1308
  preparedStatements: false,
1287
- logging: options.logging,
1288
- defaultLimit: options.defaultLimit,
1289
- warnOnUnlimited: options.warnOnUnlimited,
1290
1309
  }, schema);
1291
1310
  if (owns) {
1292
1311
  // Turbine built this pool, so disconnect()/end() must close it. External pools
package/dist/mysql.d.ts CHANGED
@@ -62,8 +62,9 @@
62
62
  * await db.disconnect();
63
63
  * ```
64
64
  */
65
- import { type PgCompatPool, type PgCompatPoolClient, TurbineClient, type TurbineConfig } from './client.js';
65
+ import { type PgCompatPool, type PgCompatPoolClient, TurbineClient } from './client.js';
66
66
  import { type Dialect, type IntrospectOptions } from './dialect.js';
67
+ import type { EngineClientConfig } from './engine-config.js';
67
68
  import { type SchemaMetadata } from './schema.js';
68
69
  /** mysql2's `[result, fields]` tuple. `result` is rows (SELECT) or a header (write). */
69
70
  type Mysql2Result = [unknown, unknown];
@@ -145,8 +146,11 @@ interface MysqlConnectionConfig {
145
146
  password?: string;
146
147
  database?: string;
147
148
  }
148
- /** Options for {@link turbineMysql}. Mirrors the relevant {@link TurbineConfig} fields. */
149
- export interface TurbineMysqlOptions extends Pick<TurbineConfig, 'logging' | 'defaultLimit' | 'warnOnUnlimited'> {
149
+ /**
150
+ * Options for {@link turbineMysql}: every client-level `TurbineConfig` field the
151
+ * engine can honour (see {@link EngineClientConfig}) plus the pool size below.
152
+ */
153
+ export interface TurbineMysqlOptions extends EngineClientConfig {
150
154
  /** Maximum number of pooled connections (when Turbine builds the pool). Default: 10. */
151
155
  connectionLimit?: number;
152
156
  }
package/dist/mysql.js CHANGED
@@ -513,6 +513,20 @@ export const mysqlDialect = {
513
513
  params: input.rowValues.flat(),
514
514
  };
515
515
  },
516
+ buildDefaultValuesInsertStatement(input) {
517
+ // MySQL is the one engine whose empty `VALUES ()` tuple is legal, and it
518
+ // repeats for any row count, so this is the same statement the column-driven
519
+ // builders already emitted for an empty `data`.
520
+ if (input.skipDuplicates) {
521
+ // `ON DUPLICATE KEY UPDATE` needs a column to assign, and there is no
522
+ // column here. `INSERT IGNORE` would swallow unrelated errors too, so
523
+ // refuse rather than quietly change what the flag means.
524
+ throw new UnsupportedFeatureError('createMany({ data: [{}, …], skipDuplicates: true })', 'mysql', 'MySQL ON DUPLICATE KEY UPDATE needs a column assignment, and rows of pure defaults name none; ' +
525
+ 'supply the conflict column in each row.');
526
+ }
527
+ const tuples = Array.from({ length: input.rowCount }, () => '()').join(', ');
528
+ return `INSERT INTO ${input.table} () VALUES ${tuples}`;
529
+ },
516
530
  buildUpsertStatement(input) {
517
531
  // MySQL ignores the explicit conflict target, ON DUPLICATE KEY UPDATE keys
518
532
  // off the table's PK/unique indexes. The `where`-derived conflictColumns
@@ -963,13 +977,16 @@ export async function turbineMysql(target, schema, options = {}) {
963
977
  // Probe the server version (fail fast on MySQL < 8.0 / MariaDB).
964
978
  const versionRows = (await pool.query('SELECT VERSION() AS v')).rows;
965
979
  assertSupportedVersion(String(versionRows[0]?.v ?? ''));
980
+ // Everything that is not a pool-construction option is client config and
981
+ // rides through untouched, so a new TurbineConfig option works here the day
982
+ // it lands. The engine-owned keys come last, so no caller can unbind the
983
+ // dialect.
984
+ const { connectionLimit: _connectionLimit, ...clientConfig } = options;
966
985
  const client = new TurbineClient({
986
+ ...clientConfig,
967
987
  pool,
968
988
  dialect: mysqlDialect,
969
989
  preparedStatements: false,
970
- logging: options.logging,
971
- defaultLimit: options.defaultLimit,
972
- warnOnUnlimited: options.warnOnUnlimited,
973
990
  }, schema);
974
991
  if (owns) {
975
992
  // Turbine built this pool, so disconnect()/end() must close it. External