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
@@ -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
  *
@@ -198,6 +198,39 @@ class TransactionClient {
198
198
  throw (0, errors_js_1.wrapPgError)(err);
199
199
  }
200
200
  }
201
+ /**
202
+ * @internal The `turbine-orm/prisma-compat` adapter's transaction seam. NOT
203
+ * application API: use {@link raw}, whose tagged template makes concatenating
204
+ * a value into the SQL text impossible. This method takes the SQL text as a
205
+ * plain string, so the escaping discipline moves to the caller, which is
206
+ * exactly the property the typed-SQL escape hatch exists to remove. It is
207
+ * documented and kept structurally stable only because the compat adapter
208
+ * detects it by shape (`typeof tx.rawQuery === 'function'`) and refuses to
209
+ * run compat raw SQL on a pool connection when it is missing.
210
+ *
211
+ * Execute an already-parameterized statement on THIS transaction's own
212
+ * connection, returning the driver's `{ rows, rowCount }` pair. It differs
213
+ * from {@link raw} in two ways the adapter needs: it takes a prebuilt
214
+ * `(text, params)` pair rather than a tagged template (the placeholder
215
+ * numbering is the caller's, so nested `Prisma.sql` fragments can be
216
+ * flattened first), and it surfaces `rowCount` so an `$executeRaw`-style call
217
+ * can report affected rows.
218
+ *
219
+ * The statement runs on the transaction's dedicated connection, so it is
220
+ * inside the same BEGIN/COMMIT (and any active SAVEPOINT) as every other
221
+ * statement in the callback. Driver errors are translated by `wrapPgError`,
222
+ * exactly as pool-scoped and table-scoped queries are. Like {@link raw}, it
223
+ * emits no `$on('query')` event and runs no middleware.
224
+ */
225
+ async rawQuery(text, params = []) {
226
+ try {
227
+ const result = await this.client.query(text, params);
228
+ return { rows: result.rows, rowCount: result.rowCount };
229
+ }
230
+ catch (err) {
231
+ throw (0, errors_js_1.wrapPgError)(err);
232
+ }
233
+ }
201
234
  /**
202
235
  * Create a pool-like wrapper around the transaction client.
203
236
  * This allows QueryInterface to work with the transaction connection
@@ -248,7 +281,19 @@ class TurbineClient {
248
281
  /** The schema metadata this client was built from */
249
282
  schema;
250
283
  static int8ParserRegistered = false;
251
- static utcTimestampParserRegistered = false;
284
+ /**
285
+ * The `utcTimestamps` value the FIRST Turbine-owned pool in this process
286
+ * settled the OID 1114 read parser on, or `undefined` while no Turbine-owned
287
+ * client has been constructed yet.
288
+ *
289
+ * `pg.types.setTypeParser` is process-global by nature: there is one parser
290
+ * per OID for the whole pg module, so the READ side of `utcTimestamps` cannot
291
+ * be per client the way the WRITE side is. Recording the settled value (not
292
+ * just "registered yes/no") is what lets the constructor detect a second
293
+ * client asking for the opposite and refuse it, instead of handing back a
294
+ * client whose reads and writes disagree. See {@link assertUtcTimestampsAgree}.
295
+ */
296
+ static utcTimestampParserMode;
252
297
  logging;
253
298
  /** Active SQL dialect, owns transaction keywords, set_config, raw-SQL placeholders, capability flags. */
254
299
  dialect;
@@ -257,6 +302,8 @@ class TurbineClient {
257
302
  queryListeners = new Set();
258
303
  queryOptions;
259
304
  errorMessagesSafe;
305
+ /** Whether `$on('query')` events carry real params (see `logQueryParams`). */
306
+ queryParamsVisible;
260
307
  /** True when Turbine created the pool and is responsible for tearing it down */
261
308
  ownsPool = true;
262
309
  /** Active LISTEN subscriptions, torn down on disconnect() so it never hangs */
@@ -292,6 +339,7 @@ class TurbineClient {
292
339
  this.logging = parent.logging;
293
340
  this.dialect = parent.dialect;
294
341
  this.errorMessagesSafe = parent.errorMessagesSafe;
342
+ this.queryParamsVisible = parent.queryParamsVisible;
295
343
  this.queryOptions = parent.queryOptions;
296
344
  this.middlewares = parent.middlewares; // shared reference: $use on parent flows through
297
345
  this.pool = parent.pool;
@@ -375,9 +423,18 @@ class TurbineClient {
375
423
  // ORM convention (Prisma, Rails, Django), and the only interpretation
376
424
  // that round-trips what Postgres stores, is UTC. Same ownership rule as
377
425
  // the int8 parser: never mutate parser state on external pools.
378
- if (ownsAnyPool && config.utcTimestamps !== false && !TurbineClient.utcTimestampParserRegistered) {
379
- pg_1.default.types.setTypeParser(1114, (val) => new Date(`${val.replace(' ', 'T')}Z`));
380
- TurbineClient.utcTimestampParserRegistered = true;
426
+ //
427
+ // Read registration is process-global and one-shot; the WRITE side of the
428
+ // same flag is per client (query/writes.ts). Two clients disagreeing about
429
+ // it therefore cannot both be served, so the disagreement is refused here
430
+ // rather than resolved silently into a client that does not round-trip.
431
+ if (ownsAnyPool) {
432
+ const wantUtcTimestamps = config.utcTimestamps !== false;
433
+ TurbineClient.assertUtcTimestampsAgree(wantUtcTimestamps);
434
+ if (wantUtcTimestamps && TurbineClient.utcTimestampParserMode === undefined) {
435
+ pg_1.default.types.setTypeParser(1114, (val) => new Date(`${val.replace(' ', 'T')}Z`));
436
+ }
437
+ TurbineClient.utcTimestampParserMode = wantUtcTimestamps;
381
438
  }
382
439
  this.logging = config.logging ?? false;
383
440
  this.dialect = config.dialect ?? dialect_js_1.postgresDialect;
@@ -385,6 +442,10 @@ class TurbineClient {
385
442
  // Respect env var kill switch
386
443
  const envDisablePrepared = typeof process !== 'undefined' && process.env?.TURBINE_DISABLE_PREPARED === '1';
387
444
  this.errorMessagesSafe = (config.errorMessages ?? 'safe') === 'safe';
445
+ // Query-event param visibility. One derived boolean, so the two config
446
+ // spellings can never disagree: `logQueryParams` wins when set, otherwise
447
+ // `errorMessages` keeps deciding exactly as it always has.
448
+ this.queryParamsVisible = config.logQueryParams ?? !this.errorMessagesSafe;
388
449
  this.queryOptions = {
389
450
  defaultLimit: config.defaultLimit,
390
451
  warnOnUnlimited: config.warnOnUnlimited,
@@ -409,7 +470,7 @@ class TurbineClient {
409
470
  _onQuery: (event) => {
410
471
  if (this.queryListeners.size === 0)
411
472
  return;
412
- const emitted = this.errorMessagesSafe ? { ...event, params: event.params.map(() => '[REDACTED]') } : event;
473
+ const emitted = this.queryParamsVisible ? event : { ...event, params: event.params.map(() => '[REDACTED]') };
413
474
  for (const listener of this.queryListeners) {
414
475
  try {
415
476
  listener(emitted);
@@ -522,6 +583,51 @@ class TurbineClient {
522
583
  this.$observe({ connectionString: observeUrl }).catch(() => { });
523
584
  }
524
585
  }
586
+ /**
587
+ * Refuse a `utcTimestamps` value that contradicts the one the process-global
588
+ * OID 1114 read parser was already settled on.
589
+ *
590
+ * The flag has two halves. The WRITE half is per client: a bound `Date` on a
591
+ * zone-less `date` / `timestamp` column is rewritten to a UTC literal unless
592
+ * the owning client opted out (`coerceWriteValue` in query/writes.ts). The
593
+ * READ half is the pg type parser for OID 1114, and `pg.types.setTypeParser`
594
+ * installs ONE parser per OID for the whole process, shared by every pool,
595
+ * every raw query, and any other library using the same pg module. There is
596
+ * no per-pool parser hook to bind it to, and moving the coercion into
597
+ * `parseRow` instead would leave every non-ORM read (raw SQL, `client.sql`,
598
+ * a caller's own `pool.query`) on the driver's value while changing the
599
+ * default path's output type, so the read half stays process-wide.
600
+ *
601
+ * That makes the mixed shape unserveable rather than merely awkward: the
602
+ * second client would write local calendar fields and read them back as UTC
603
+ * (or the reverse), so its own round trip is off by the process offset.
604
+ * A client that silently does not round-trip is the worst of the three
605
+ * outcomes, so construction fails with the two ways out.
606
+ *
607
+ * Only Turbine-owned pools take part. An external pool (Neon, Vercel
608
+ * Postgres, Hyperdrive) inherits the caller's parser configuration and
609
+ * Turbine never registers on its behalf, so it has no read half to contradict.
610
+ */
611
+ static assertUtcTimestampsAgree(want) {
612
+ const settled = TurbineClient.utcTimestampParserMode;
613
+ if (settled === undefined || settled === want)
614
+ return;
615
+ throw new errors_js_1.ValidationError(`[turbine] utcTimestamps: ${want} conflicts with utcTimestamps: ${settled}, which an earlier TurbineClient ` +
616
+ 'in this process already applied. The timestamp READ parser (pg OID 1114) is process-global, so it cannot ' +
617
+ 'differ per client, while the WRITE side is per client. Serving both values would give this client a ' +
618
+ `${want ? 'UTC write' : 'local write'} and a ${settled ? 'UTC read' : 'local read'}, so every zone-less ` +
619
+ '`timestamp` it writes would read back shifted by the process offset. Give every TurbineClient in this ' +
620
+ 'process the same `utcTimestamps` value, or run the odd one out in its own process.');
621
+ }
622
+ /**
623
+ * @internal Test-only: forget the process-global `utcTimestamps` decision so
624
+ * one process can exercise both mismatch directions. It does NOT restore the
625
+ * pg parser (parser registration is not reversible), so reads stay on
626
+ * whichever parser was installed first.
627
+ */
628
+ static resetUtcTimestampsForTests() {
629
+ TurbineClient.utcTimestampParserMode = undefined;
630
+ }
525
631
  // -------------------------------------------------------------------------
526
632
  // Middleware, intercept all queries
527
633
  // -------------------------------------------------------------------------
@@ -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. */
@@ -146,6 +146,20 @@ exports.postgresDialect = {
146
146
  sql += ' ON CONFLICT DO NOTHING';
147
147
  return { sql: `${sql}${this.buildReturningClause(input.returning)}`, params: columnArrays };
148
148
  },
149
+ buildDefaultValuesInsertStatement(input) {
150
+ const conflict = input.skipDuplicates ? ' ON CONFLICT DO NOTHING' : '';
151
+ if (input.rowCount === 1) {
152
+ return `INSERT INTO ${input.table} DEFAULT VALUES${conflict}${this.buildReturningClause(input.returning)}`;
153
+ }
154
+ // N all-defaults rows: `DEFAULT VALUES` is single-row only, so feed the
155
+ // INSERT a zero-column SELECT of N source rows instead. Every column still
156
+ // takes its default (nothing is projected into the INSERT), and unlike
157
+ // `VALUES (DEFAULT), (DEFAULT)` it does not depend on the table's first
158
+ // column accepting the DEFAULT keyword. `rowCount` is the caller's array
159
+ // length, so the literal is an internal integer, not a bound user value.
160
+ return (`INSERT INTO ${input.table} SELECT FROM generate_series(1, ${input.rowCount})` +
161
+ `${conflict}${this.buildReturningClause(input.returning)}`);
162
+ },
149
163
  buildUpsertStatement(input) {
150
164
  return (`INSERT INTO ${input.table} (${input.insertColumns.join(', ')}) VALUES (${input.valuePlaceholders.join(', ')})` +
151
165
  ` 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,19 @@
1
+ "use strict";
2
+ /**
3
+ * Shared client-config seam for the non-Postgres engine factories
4
+ * (`turbine-orm/sqlite`, `turbine-orm/mysql`, `turbine-orm/mssql`).
5
+ *
6
+ * Each factory builds its own driver pool and then hands a {@link TurbineConfig}
7
+ * to `TurbineClient`. Historically each one listed the config keys it forwarded
8
+ * by hand, which made the forwarded set an ALLOWLIST: every option added to
9
+ * `TurbineConfig` afterwards silently became unreachable on three engines until
10
+ * someone remembered to edit three literals (that is exactly how `errorMessages`
11
+ * and later `logQueryParams` ended up Postgres-only).
12
+ *
13
+ * {@link EngineClientConfig} inverts the default. Every `TurbineConfig` field is
14
+ * forwarded EXCEPT the ones listed below, so a new client option is engine-wide
15
+ * the day it lands and only a deliberate `Omit` entry can take it away again.
16
+ * Each engine's options interface extends this type and its factory spreads the
17
+ * config through verbatim.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
Binary file
@@ -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/cjs/index.js CHANGED
@@ -34,8 +34,8 @@
34
34
  * ```
35
35
  */
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.isDateType = exports.camelToSnake = exports.validateChannel = exports.QueryInterface = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.pipelineSupported = exports.executePipeline = exports.PgMetricsSink = exports.HttpJsonSink = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UnsupportedFeatureError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.ReadOnlyError = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
38
- exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.DestructivePushRefusal = exports.schemaDefToMetadata = exports.table = exports.isDocFieldIndexDef = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.applyManyToManyRelations = exports.withDbFieldNames = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = void 0;
37
+ exports.camelToSnake = exports.validateChannel = exports.QueryInterface = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.pipelineSupported = exports.executePipeline = exports.PgMetricsSink = exports.HttpJsonSink = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UnsupportedFeatureError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.ReadOnlyError = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
38
+ exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.DestructivePushRefusal = exports.schemaDefToMetadata = exports.table = exports.isDocFieldIndexDef = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.applyManyToManyRelations = exports.withDbFieldNames = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = void 0;
39
39
  var index_js_1 = require("./adapters/index.js");
40
40
  Object.defineProperty(exports, "alloydb", { enumerable: true, get: function () { return index_js_1.alloydb; } });
41
41
  Object.defineProperty(exports, "cockroachdb", { enumerable: true, get: function () { return index_js_1.cockroachdb; } });
@@ -96,6 +96,7 @@ Object.defineProperty(exports, "pipelineSupported", { enumerable: true, get: fun
96
96
  // Query builder
97
97
  var index_js_2 = require("./query/index.js");
98
98
  Object.defineProperty(exports, "AUTO_ASSUMED_ROUND_TRIP_MS", { enumerable: true, get: function () { return index_js_2.AUTO_ASSUMED_ROUND_TRIP_MS; } });
99
+ Object.defineProperty(exports, "AUTO_COUNT_BATCH_MIN_PARENT_ROWS", { enumerable: true, get: function () { return index_js_2.AUTO_COUNT_BATCH_MIN_PARENT_ROWS; } });
99
100
  Object.defineProperty(exports, "AUTO_JOIN_PENALTY_MS_PER_ROW", { enumerable: true, get: function () { return index_js_2.AUTO_JOIN_PENALTY_MS_PER_ROW; } });
100
101
  Object.defineProperty(exports, "AUTO_TO_ONE_JOIN_MAX_ROWS", { enumerable: true, get: function () { return index_js_2.AUTO_TO_ONE_JOIN_MAX_ROWS; } });
101
102
  Object.defineProperty(exports, "AUTO_TO_ONE_JOIN_ROWS_MAX", { enumerable: true, get: function () { return index_js_2.AUTO_TO_ONE_JOIN_ROWS_MAX; } });
@@ -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/cjs/mssql.js CHANGED
@@ -603,6 +603,22 @@ exports.mssqlDialect = {
603
603
  params: input.rowValues.flat(),
604
604
  };
605
605
  },
606
+ buildDefaultValuesInsertStatement(input) {
607
+ // T-SQL puts OUTPUT between the (absent) column list and the value source,
608
+ // so `DEFAULT VALUES` follows it, exactly like the VALUES form above.
609
+ if (input.skipDuplicates) {
610
+ throw new errors_js_1.UnsupportedFeatureError('createMany({ skipDuplicates: true })', 'mssql', 'SQL Server has no ON CONFLICT DO NOTHING equivalent, pre-filter conflicting rows or use a MERGE.');
611
+ }
612
+ // `DEFAULT VALUES` is single-row only in T-SQL, and the multi-row VALUES
613
+ // form needs one DEFAULT keyword per column, which rows of pure defaults
614
+ // do not name.
615
+ if (input.rowCount !== 1) {
616
+ throw new errors_js_1.UnsupportedFeatureError(`createMany with ${input.rowCount} empty data rows`, 'mssql', 'SQL Server INSERT … DEFAULT VALUES inserts a single row and has no multi-row form; ' +
617
+ 'issue one create({ data: {} }) per row.');
618
+ }
619
+ const out = mssqlOutput(input.returning, 'INSERTED');
620
+ return `INSERT INTO ${input.table}${out} DEFAULT VALUES`;
621
+ },
606
622
  buildUpsertStatement(input) {
607
623
  // MERGE is the SQL Server upsert. The MERGE statement MUST end with `;`.
608
624
  // CONCURRENCY CAVEAT: MERGE is not a substitute for a UNIQUE/PK constraint -
@@ -1291,13 +1307,16 @@ async function turbineMssql(target, schema, options = {}) {
1291
1307
  throw err;
1292
1308
  // A non-version probe failure (permissions, etc.) should not block startup.
1293
1309
  }
1310
+ // Everything that is not a SQL Server introspection option is client config
1311
+ // and rides through untouched, so a new TurbineConfig option works here the
1312
+ // day it lands. The engine-owned keys come last, so no caller can unbind the
1313
+ // dialect.
1314
+ const { schema: _dbSchema, ...clientConfig } = options;
1294
1315
  const client = new client_js_1.TurbineClient({
1316
+ ...clientConfig,
1295
1317
  pool,
1296
1318
  dialect: exports.mssqlDialect,
1297
1319
  preparedStatements: false,
1298
- logging: options.logging,
1299
- defaultLimit: options.defaultLimit,
1300
- warnOnUnlimited: options.warnOnUnlimited,
1301
1320
  }, schema);
1302
1321
  if (owns) {
1303
1322
  // Turbine built this pool, so disconnect()/end() must close it. External pools
@@ -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/cjs/mysql.js CHANGED
@@ -524,6 +524,20 @@ exports.mysqlDialect = {
524
524
  params: input.rowValues.flat(),
525
525
  };
526
526
  },
527
+ buildDefaultValuesInsertStatement(input) {
528
+ // MySQL is the one engine whose empty `VALUES ()` tuple is legal, and it
529
+ // repeats for any row count, so this is the same statement the column-driven
530
+ // builders already emitted for an empty `data`.
531
+ if (input.skipDuplicates) {
532
+ // `ON DUPLICATE KEY UPDATE` needs a column to assign, and there is no
533
+ // column here. `INSERT IGNORE` would swallow unrelated errors too, so
534
+ // refuse rather than quietly change what the flag means.
535
+ throw new errors_js_1.UnsupportedFeatureError('createMany({ data: [{}, …], skipDuplicates: true })', 'mysql', 'MySQL ON DUPLICATE KEY UPDATE needs a column assignment, and rows of pure defaults name none; ' +
536
+ 'supply the conflict column in each row.');
537
+ }
538
+ const tuples = Array.from({ length: input.rowCount }, () => '()').join(', ');
539
+ return `INSERT INTO ${input.table} () VALUES ${tuples}`;
540
+ },
527
541
  buildUpsertStatement(input) {
528
542
  // MySQL ignores the explicit conflict target, ON DUPLICATE KEY UPDATE keys
529
543
  // off the table's PK/unique indexes. The `where`-derived conflictColumns
@@ -974,13 +988,16 @@ async function turbineMysql(target, schema, options = {}) {
974
988
  // Probe the server version (fail fast on MySQL < 8.0 / MariaDB).
975
989
  const versionRows = (await pool.query('SELECT VERSION() AS v')).rows;
976
990
  assertSupportedVersion(String(versionRows[0]?.v ?? ''));
991
+ // Everything that is not a pool-construction option is client config and
992
+ // rides through untouched, so a new TurbineConfig option works here the day
993
+ // it lands. The engine-owned keys come last, so no caller can unbind the
994
+ // dialect.
995
+ const { connectionLimit: _connectionLimit, ...clientConfig } = options;
977
996
  const client = new client_js_1.TurbineClient({
997
+ ...clientConfig,
978
998
  pool,
979
999
  dialect: exports.mysqlDialect,
980
1000
  preparedStatements: false,
981
- logging: options.logging,
982
- defaultLimit: options.defaultLimit,
983
- warnOnUnlimited: options.warnOnUnlimited,
984
1001
  }, schema);
985
1002
  if (owns) {
986
1003
  // Turbine built this pool, so disconnect()/end() must close it. External