turbine-orm 0.27.0 → 0.28.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 (52) hide show
  1. package/README.md +17 -13
  2. package/dist/cjs/cli/config.js +20 -3
  3. package/dist/cjs/cli/destructive.js +47 -31
  4. package/dist/cjs/cli/index.js +273 -71
  5. package/dist/cjs/cli/mcp.js +788 -0
  6. package/dist/cjs/cli/migrate.js +95 -20
  7. package/dist/cjs/cli/studio.js +3 -2
  8. package/dist/cjs/client.js +267 -34
  9. package/dist/cjs/dialect.js +2 -0
  10. package/dist/cjs/generate.js +171 -7
  11. package/dist/cjs/index.js +4 -1
  12. package/dist/cjs/introspect.js +177 -4
  13. package/dist/cjs/query/batched-loader.js +148 -0
  14. package/dist/cjs/query/builder.js +714 -133
  15. package/dist/cjs/schema-builder.js +59 -4
  16. package/dist/cjs/schema-sql.js +315 -6
  17. package/dist/cjs/seed.js +66 -0
  18. package/dist/cli/config.d.ts +9 -2
  19. package/dist/cli/config.js +19 -3
  20. package/dist/cli/destructive.js +47 -31
  21. package/dist/cli/index.d.ts +52 -1
  22. package/dist/cli/index.js +272 -74
  23. package/dist/cli/mcp.d.ts +17 -0
  24. package/dist/cli/mcp.js +781 -0
  25. package/dist/cli/migrate.d.ts +37 -0
  26. package/dist/cli/migrate.js +92 -20
  27. package/dist/cli/studio.d.ts +3 -2
  28. package/dist/cli/studio.js +3 -2
  29. package/dist/client.d.ts +136 -1
  30. package/dist/client.js +267 -34
  31. package/dist/dialect.d.ts +17 -0
  32. package/dist/dialect.js +2 -0
  33. package/dist/generate.d.ts +17 -0
  34. package/dist/generate.js +171 -10
  35. package/dist/index.d.ts +4 -3
  36. package/dist/index.js +2 -0
  37. package/dist/introspect.d.ts +20 -1
  38. package/dist/introspect.js +175 -4
  39. package/dist/query/batched-loader.d.ts +29 -2
  40. package/dist/query/batched-loader.js +148 -1
  41. package/dist/query/builder.d.ts +156 -8
  42. package/dist/query/builder.js +715 -134
  43. package/dist/query/index.d.ts +1 -1
  44. package/dist/query/types.d.ts +113 -8
  45. package/dist/schema-builder.d.ts +73 -8
  46. package/dist/schema-builder.js +59 -4
  47. package/dist/schema-sql.d.ts +67 -0
  48. package/dist/schema-sql.js +310 -6
  49. package/dist/schema.d.ts +53 -0
  50. package/dist/seed.d.ts +4 -0
  51. package/dist/seed.js +63 -0
  52. package/package.json +2 -3
package/dist/client.js CHANGED
@@ -68,6 +68,29 @@ const ISOLATION_LEVELS = {
68
68
  * rejecting loudly before it reaches the database.
69
69
  */
70
70
  const GUC_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/;
71
+ /**
72
+ * The read-only `QueryInterface` operations that a read-replica setup may route
73
+ * to a replica pool. Every other method (all writes, plus internals) stays on
74
+ * the primary. Kept as a Set so the routing proxy's `get` trap is O(1).
75
+ */
76
+ const READ_OPERATIONS = new Set([
77
+ 'findMany',
78
+ 'findFirst',
79
+ 'findUnique',
80
+ 'findFirstOrThrow',
81
+ 'findUniqueOrThrow',
82
+ 'count',
83
+ 'aggregate',
84
+ 'groupBy',
85
+ 'findManyStream',
86
+ ]);
87
+ /**
88
+ * Internal marker on the config object that tells the `TurbineClient`
89
+ * constructor to build a lightweight "primary-only" view sharing an existing
90
+ * client's primary pool, dialect, query options, and middleware — instead of
91
+ * creating a fresh pool. Produced solely by `$primary()`; never public.
92
+ */
93
+ const PRIMARY_VIEW = Symbol('turbine.primaryView');
71
94
  // ---------------------------------------------------------------------------
72
95
  // TransactionClient — provides typed table accessors within a transaction
73
96
  // ---------------------------------------------------------------------------
@@ -210,7 +233,55 @@ export class TurbineClient {
210
233
  ownsPool = true;
211
234
  /** Active LISTEN subscriptions — torn down on disconnect() so it never hangs */
212
235
  activeSubscriptions = new Set();
236
+ /**
237
+ * Read-replica pools in round-robin order. Empty when no replicas are
238
+ * configured, in which case `table()` takes the original single-pool path.
239
+ */
240
+ replicaPools;
241
+ /**
242
+ * The subset of {@link replicaPools} that Turbine created from connection
243
+ * strings and must close on `disconnect()`. External replica pools are not
244
+ * listed here (caller owns their lifecycle).
245
+ */
246
+ ownedReplicaPools;
247
+ /** Rotating index for round-robin replica selection (advances per read op). */
248
+ replicaCursor = 0;
249
+ /** Per-replica `table → QueryInterface` caches, indexed like {@link replicaPools}. */
250
+ replicaTableCaches;
251
+ /** Cache of per-table routing proxies (only used when replicas are present). */
252
+ routingProxyCache = new Map();
253
+ /** Lazily-built, cached primary-only view returned by {@link $primary}. */
254
+ primaryView;
213
255
  constructor(config = {}, schema) {
256
+ // Primary-only view: $primary() constructs this to share the parent's
257
+ // primary pool + derived state instead of creating a fresh pool. It owns
258
+ // no pool and no replicas, so every operation (reads included) runs on the
259
+ // primary and disconnect() is a no-op on the shared pool.
260
+ const seed = config[PRIMARY_VIEW];
261
+ if (seed) {
262
+ const parent = seed.parent;
263
+ this.schema = schema;
264
+ this.logging = parent.logging;
265
+ this.dialect = parent.dialect;
266
+ this.errorMessagesSafe = parent.errorMessagesSafe;
267
+ this.queryOptions = parent.queryOptions;
268
+ this.middlewares = parent.middlewares; // shared reference: $use on parent flows through
269
+ this.pool = parent.pool;
270
+ this.ownsPool = false;
271
+ this.replicaPools = [];
272
+ this.ownedReplicaPools = [];
273
+ this.replicaTableCaches = [];
274
+ for (const tableName of Object.keys(schema.tables)) {
275
+ const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
276
+ if (!(camelName in this)) {
277
+ Object.defineProperty(this, camelName, {
278
+ get: () => this.table(tableName),
279
+ enumerable: true,
280
+ });
281
+ }
282
+ }
283
+ return;
284
+ }
214
285
  // Constructing without schema metadata previously crashed deep in the
215
286
  // constructor with an opaque "Cannot read properties of undefined
216
287
  // (reading 'tables')". Fail fast with an actionable message instead.
@@ -234,11 +305,16 @@ export class TurbineClient {
234
305
  * of returning a string is correct and matches the generated TypeScript type
235
306
  * (numeric → string). Users who want number can cast explicitly in SQL.
236
307
  */
237
- // Only register the int8 parser when we own the pg driver. External
238
- // pools (Neon HTTP, Vercel Postgres) may ship their own pg-types fork
239
- // and rely on their own parser configuration — don't mutate global state
240
- // we don't own.
241
- if (!config.pool && !TurbineClient.int8ParserRegistered) {
308
+ // Only register the int8 parser when the PRIMARY pool is Turbine-owned.
309
+ // External pools (Neon HTTP, Vercel Postgres) may ship their own pg-types
310
+ // fork and rely on their own parser configuration — registration is
311
+ // process-global, so flipping it because a string replica exists alongside
312
+ // an external primary would silently change the external primary's parsing
313
+ // too. String replicas configured next to an external primary therefore
314
+ // inherit the caller's parser configuration (documented). Registration is
315
+ // constructor-gated by the static flags, so it happens at most once.
316
+ const ownsAnyPool = !config.pool;
317
+ if (ownsAnyPool && !TurbineClient.int8ParserRegistered) {
242
318
  pg.types.setTypeParser(20, (val) => {
243
319
  const n = Number(val);
244
320
  return Number.isSafeInteger(n) ? n : val;
@@ -251,7 +327,7 @@ export class TurbineClient {
251
327
  // ORM convention (Prisma, Rails, Django) — and the only interpretation
252
328
  // that round-trips what Postgres stores — is UTC. Same ownership rule as
253
329
  // the int8 parser: never mutate parser state on external pools.
254
- if (!config.pool && config.utcTimestamps !== false && !TurbineClient.utcTimestampParserRegistered) {
330
+ if (ownsAnyPool && config.utcTimestamps !== false && !TurbineClient.utcTimestampParserRegistered) {
255
331
  pg.types.setTypeParser(1114, (val) => new Date(`${val.replace(' ', 'T')}Z`));
256
332
  TurbineClient.utcTimestampParserRegistered = true;
257
333
  }
@@ -267,6 +343,7 @@ export class TurbineClient {
267
343
  utcTimestamps: config.utcTimestamps,
268
344
  relationLoadStrategy: config.relationLoadStrategy,
269
345
  jsonEncoding: config.jsonEncoding,
346
+ globalFilters: config.globalFilters,
270
347
  preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),
271
348
  sqlCache: config.sqlCache ?? true,
272
349
  dialect: config.dialect,
@@ -331,6 +408,34 @@ export class TurbineClient {
331
408
  console.log(`[turbine] Pool created — max ${poolConfig.max} connections, ${Object.keys(schema.tables).length} tables`);
332
409
  }
333
410
  }
411
+ // Build read-replica pools (if any). String entries become owned pg.Pools
412
+ // sharing the primary's tuning knobs; PgCompatPool entries are external and
413
+ // used as-is. Replica selection is round-robin in this array order.
414
+ this.replicaPools = [];
415
+ this.ownedReplicaPools = [];
416
+ for (const replica of config.replicas ?? []) {
417
+ if (typeof replica === 'string') {
418
+ const replicaPool = new pg.Pool({
419
+ connectionString: replica,
420
+ max: config.poolSize ?? 10,
421
+ idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
422
+ connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
423
+ ...(config.ssl !== undefined ? { ssl: config.ssl } : {}),
424
+ });
425
+ replicaPool.on('error', (err) => {
426
+ console.error('[turbine] Unexpected replica pool error:', err.message);
427
+ });
428
+ this.replicaPools.push(replicaPool);
429
+ this.ownedReplicaPools.push(replicaPool);
430
+ }
431
+ else {
432
+ this.replicaPools.push(replica);
433
+ }
434
+ }
435
+ this.replicaTableCaches = this.replicaPools.map(() => new Map());
436
+ if (this.logging && this.replicaPools.length > 0) {
437
+ console.log(`[turbine] ${this.replicaPools.length} read replica(s) configured (${this.ownedReplicaPools.length} owned)`);
438
+ }
334
439
  // Auto-create typed table accessors for all tables in the schema
335
440
  for (const tableName of Object.keys(schema.tables)) {
336
441
  const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
@@ -382,8 +487,14 @@ export class TurbineClient {
382
487
  */
383
488
  $use(middleware) {
384
489
  this.middlewares.push(middleware);
385
- // Clear table cache so new QueryInterfaces pick up the middleware
490
+ // Clear table caches so new QueryInterfaces pick up the middleware. Covers
491
+ // the primary cache plus, when replicas are configured, the routing proxies
492
+ // and per-replica caches. The primary-view (if built) shares the middleware
493
+ // array by reference, so its QueryInterfaces observe the new middleware too.
386
494
  this.tableCache.clear();
495
+ this.routingProxyCache.clear();
496
+ for (const cache of this.replicaTableCaches)
497
+ cache.clear();
387
498
  }
388
499
  // -------------------------------------------------------------------------
389
500
  // Event emitter — subscribe to query lifecycle events
@@ -422,17 +533,110 @@ export class TurbineClient {
422
533
  /**
423
534
  * Get a QueryInterface for a table.
424
535
  * Results are cached — calling `table('users')` twice returns the same instance.
536
+ *
537
+ * When read replicas are configured, this returns a thin routing proxy: the
538
+ * read-only operations in {@link READ_OPERATIONS} are dispatched to a
539
+ * round-robin replica-bound QueryInterface (so an entire read — base rows and
540
+ * any batched sub-queries — runs against a single consistent replica), while
541
+ * writes and every other member fall through to the primary-bound instance.
542
+ * With no replicas the original single-pool instance is returned directly.
425
543
  */
426
544
  table(name) {
545
+ if (this.replicaPools.length === 0) {
546
+ return this.primaryTableQI(name);
547
+ }
548
+ let proxy = this.routingProxyCache.get(name);
549
+ if (!proxy) {
550
+ proxy = this.createRoutingAccessor(name);
551
+ this.routingProxyCache.set(name, proxy);
552
+ }
553
+ return proxy;
554
+ }
555
+ /** Get (and cache) the primary-pool-bound QueryInterface for a table. */
556
+ primaryTableQI(name) {
427
557
  let qi = this.tableCache.get(name);
428
558
  if (!qi) {
429
- qi = this.queryOptions?.queryInterfaceFactory
430
- ? this.queryOptions.queryInterfaceFactory(this.pool, name, this.schema, this.middlewares, this.queryOptions)
431
- : new QueryInterface(this.pool, name, this.schema, this.middlewares, this.queryOptions);
559
+ qi = this.buildTableQI(this.pool, name);
432
560
  this.tableCache.set(name, qi);
433
561
  }
434
562
  return qi;
435
563
  }
564
+ /**
565
+ * Advance the round-robin cursor and return the QueryInterface bound to the
566
+ * selected replica pool for `name` (cached per replica).
567
+ */
568
+ nextReplicaTableQI(name) {
569
+ const index = this.replicaCursor % this.replicaPools.length;
570
+ // Reset before overflow so the cursor never grows unbounded.
571
+ this.replicaCursor = this.replicaCursor + 1 >= Number.MAX_SAFE_INTEGER ? 0 : this.replicaCursor + 1;
572
+ // index is always in-bounds (`% length`); the pools/cache entries exist.
573
+ const cache = this.replicaTableCaches[index];
574
+ const pool = this.replicaPools[index];
575
+ let qi = cache.get(name);
576
+ if (!qi) {
577
+ qi = this.buildTableQI(pool, name);
578
+ cache.set(name, qi);
579
+ }
580
+ return qi;
581
+ }
582
+ /** Construct a QueryInterface bound to `pool` (honoring any injected factory). */
583
+ buildTableQI(pool, name) {
584
+ const asPgPool = pool;
585
+ return this.queryOptions?.queryInterfaceFactory
586
+ ? this.queryOptions.queryInterfaceFactory(asPgPool, name, this.schema, this.middlewares, this.queryOptions)
587
+ : new QueryInterface(asPgPool, name, this.schema, this.middlewares, this.queryOptions);
588
+ }
589
+ /**
590
+ * Build the read/write routing proxy for a table. The proxy targets the
591
+ * primary QueryInterface (so writes, `build*`, and every non-read member work
592
+ * unchanged); read operations are intercepted and dispatched to a replica.
593
+ */
594
+ createRoutingAccessor(name) {
595
+ const primaryQI = this.primaryTableQI(name);
596
+ const client = this;
597
+ return new Proxy(primaryQI, {
598
+ get(target, prop, receiver) {
599
+ if (typeof prop === 'string' && READ_OPERATIONS.has(prop)) {
600
+ // Pick the replica at CALL time so round-robin advances per operation.
601
+ return (...args) => {
602
+ const replicaQI = client.nextReplicaTableQI(name);
603
+ const method = replicaQI[prop];
604
+ if (typeof method !== 'function') {
605
+ return Reflect.get(target, prop, receiver);
606
+ }
607
+ return method.apply(replicaQI, args);
608
+ };
609
+ }
610
+ return Reflect.get(target, prop, receiver);
611
+ },
612
+ });
613
+ }
614
+ /**
615
+ * Return a view of this client that pins EVERY operation — reads included —
616
+ * to the primary pool, bypassing replica routing. Use it to read your own
617
+ * write without replication lag, or for any read that must see the latest
618
+ * committed data.
619
+ *
620
+ * The view shares the primary pool, schema, dialect, query options, and
621
+ * middleware; it owns nothing, so its `disconnect()` is a no-op. When no
622
+ * replicas are configured this simply returns the client itself (already
623
+ * primary-only). The view is cached — repeated calls return the same instance.
624
+ *
625
+ * @example
626
+ * ```ts
627
+ * await db.users.create({ data: { email: 'a@b.com' } });
628
+ * // Read-after-write: guaranteed to see the row just inserted.
629
+ * const user = await db.$primary().users.findFirst({ where: { email: 'a@b.com' } });
630
+ * ```
631
+ */
632
+ $primary() {
633
+ if (this.replicaPools.length === 0)
634
+ return this;
635
+ if (!this.primaryView) {
636
+ this.primaryView = new TurbineClient({ [PRIMARY_VIEW]: { parent: this } }, this.schema);
637
+ }
638
+ return this.primaryView;
639
+ }
436
640
  // -------------------------------------------------------------------------
437
641
  // Pipeline — batch multiple queries into one round-trip
438
642
  // -------------------------------------------------------------------------
@@ -573,29 +777,13 @@ export class TurbineClient {
573
777
  client.release();
574
778
  }
575
779
  }
576
- // -------------------------------------------------------------------------
577
- // $transaction Prisma-style typed transaction API
578
- // -------------------------------------------------------------------------
579
- /**
580
- * Execute a function within a database transaction with full typed table accessors.
581
- *
582
- * The `tx` object provides the same table accessor API as the main client.
583
- * Supports nested transactions via SAVEPOINTs, timeouts, and isolation levels.
584
- *
585
- * @example
586
- * ```ts
587
- * await db.$transaction(async (tx) => {
588
- * const user = await tx.users.create({ data: { email: 'a@b.com' } });
589
- * await tx.posts.create({ data: { userId: user.id, title: 'Hello' } });
590
- * });
591
- *
592
- * // With options:
593
- * await db.$transaction(async (tx) => {
594
- * // ...
595
- * }, { timeout: 5000, isolationLevel: 'Serializable' });
596
- * ```
597
- */
598
- async $transaction(fn, options) {
780
+ async $transaction(fnOrQueries, options) {
781
+ // Batch overload: an array of DeferredQuery objects runs atomically inside
782
+ // one BEGIN…COMMIT, reusing the raw transaction machinery below.
783
+ if (Array.isArray(fnOrQueries)) {
784
+ return this.transactionBatch(fnOrQueries);
785
+ }
786
+ const fn = fnOrQueries;
599
787
  const client = await this.pool.connect();
600
788
  const timeout = options?.timeout;
601
789
  /**
@@ -711,6 +899,38 @@ export class TurbineClient {
711
899
  releaseOnce();
712
900
  }
713
901
  }
902
+ /**
903
+ * Execute a batch of {@link DeferredQuery} objects atomically inside one
904
+ * transaction. Backs the `$transaction([...])` array overload. Reuses the raw
905
+ * {@link transaction} machinery (BEGIN/COMMIT/ROLLBACK + connection release);
906
+ * queries run sequentially on the single transaction connection and each
907
+ * result is passed through its query's `transform`.
908
+ */
909
+ async transactionBatch(queries) {
910
+ if (queries.length === 0) {
911
+ return [];
912
+ }
913
+ return this.transaction(async (client) => {
914
+ const results = [];
915
+ for (const dq of queries) {
916
+ let raw;
917
+ try {
918
+ // Non-RETURNING engines (resultStrategy 'reselect', e.g. MySQL)
919
+ // attach a reselect plan that runs the write plus a follow-up SELECT;
920
+ // running dq.sql alone would transform a row-less write result.
921
+ raw =
922
+ this.dialect.resultStrategy === 'reselect' && dq.reselect
923
+ ? await dq.reselect((sql, params) => client.query(sql, params))
924
+ : await client.query(dq.sql, dq.params);
925
+ }
926
+ catch (err) {
927
+ throw wrapPgError(err);
928
+ }
929
+ results.push(dq.transform(raw));
930
+ }
931
+ return results;
932
+ });
933
+ }
714
934
  /**
715
935
  * Convenience wrapper around `$transaction` for the multi-tenant / RLS case:
716
936
  * runs `fn` inside a transaction with the given session GUCs applied via
@@ -867,9 +1087,22 @@ export class TurbineClient {
867
1087
  }
868
1088
  this.activeSubscriptions.clear();
869
1089
  }
1090
+ // Close owned (string-configured) replica pools regardless of whether the
1091
+ // primary is owned — external replica pools are left untouched (caller owns
1092
+ // their lifecycle), same contract as an external primary.
1093
+ for (const replicaPool of this.ownedReplicaPools) {
1094
+ try {
1095
+ await replicaPool.end();
1096
+ }
1097
+ catch (err) {
1098
+ if (this.logging) {
1099
+ console.error('[turbine] Error closing replica pool:', err.message);
1100
+ }
1101
+ }
1102
+ }
870
1103
  if (!this.ownsPool) {
871
1104
  if (this.logging) {
872
- console.log('[turbine] disconnect() skipped — external pool is not owned by Turbine');
1105
+ console.log('[turbine] disconnect() skipped — external primary pool is not owned by Turbine');
873
1106
  }
874
1107
  return;
875
1108
  }
package/dist/dialect.d.ts CHANGED
@@ -47,6 +47,14 @@ export interface UpsertStatementInput {
47
47
  conflictColumns: string[];
48
48
  /** SQL-ready update SET clauses. */
49
49
  updateSetClauses: string[];
50
+ /**
51
+ * Optional SQL-ready predicate (no `WHERE` keyword) restricting the
52
+ * conflict-UPDATE to matching rows — used by global filters (soft-delete /
53
+ * multi-tenancy) so an upsert never resurrects/steals a row outside the
54
+ * filter. Only honored by dialects that set
55
+ * {@link Dialect.supportsUpsertUpdateWhere}; others must never receive it.
56
+ */
57
+ updateWhere?: string;
50
58
  /** Optional SQL-ready RETURNING selection. */
51
59
  returning?: string;
52
60
  }
@@ -270,6 +278,15 @@ export interface Dialect {
270
278
  wrapJsonSubresult(subquery: string, fallback: string): string;
271
279
  /** Whether INSERT/UPDATE/DELETE support RETURNING rows. */
272
280
  readonly supportsReturning: boolean;
281
+ /**
282
+ * Whether {@link buildUpsertStatement} honors {@link UpsertStatementInput.updateWhere}
283
+ * (a predicate on the conflict-UPDATE, e.g. Postgres `ON CONFLICT … DO UPDATE
284
+ * SET … WHERE …`). Global filters only push a conflict-UPDATE predicate when
285
+ * this is true, so engines whose upsert cannot express one (MySQL
286
+ * `ON DUPLICATE KEY UPDATE`) never receive an orphaned parameter. Optional —
287
+ * absent is treated as `false`.
288
+ */
289
+ readonly supportsUpsertUpdateWhere?: boolean;
273
290
  /** Whether this dialect/engine supports pgvector distance ops (KNN / distance WHERE). */
274
291
  readonly supportsVector: boolean;
275
292
  /** Whether this dialect/engine supports LISTEN/NOTIFY realtime pub/sub. */
package/dist/dialect.js CHANGED
@@ -12,6 +12,7 @@ export const postgresDialect = {
12
12
  name: 'postgresql',
13
13
  resultStrategy: 'returning',
14
14
  supportsReturning: true,
15
+ supportsUpsertUpdateWhere: true,
15
16
  supportsILike: true,
16
17
  jsonPathSupport: 'native',
17
18
  emptyJsonArrayLiteral: "'[]'::json",
@@ -85,6 +86,7 @@ export const postgresDialect = {
85
86
  buildUpsertStatement(input) {
86
87
  return (`INSERT INTO ${input.table} (${input.insertColumns.join(', ')}) VALUES (${input.valuePlaceholders.join(', ')})` +
87
88
  ` ON CONFLICT (${input.conflictColumns.join(', ')}) DO UPDATE SET ${input.updateSetClauses.join(', ')}` +
89
+ (input.updateWhere ? ` WHERE ${input.updateWhere}` : '') +
88
90
  this.buildReturningClause(input.returning));
89
91
  },
90
92
  buildInsensitiveLike(column, paramRef) {
@@ -16,6 +16,13 @@ export interface GenerateOptions {
16
16
  outDir?: string;
17
17
  /** Redact connection string from generated comments */
18
18
  connectionString?: string;
19
+ /**
20
+ * Also emit `zod.ts` with per-table `XSchema` / `XCreateSchema` /
21
+ * `XUpdateSchema` Zod validators (H1). The file imports the user-side `zod`
22
+ * package — it is never imported by Turbine's runtime, so Zod stays out of the
23
+ * library's dependency graph. Default: `false`.
24
+ */
25
+ zod?: boolean;
19
26
  }
20
27
  export declare function generate(options: GenerateOptions): {
21
28
  outDir: string;
@@ -27,3 +34,13 @@ export declare function generate(options: GenerateOptions): {
27
34
  * generator output without writing files to disk.
28
35
  */
29
36
  export declare function generateTypes(schema: SchemaMetadata): string;
37
+ /**
38
+ * Generate the contents of `zod.ts`. Emits, per table, `XSchema` (the full
39
+ * row), `XCreateSchema` (PK/defaulted/nullable columns optional, STORED
40
+ * generated columns omitted), and `XUpdateSchema` (PK + STORED generated
41
+ * columns omitted, every remaining column optional). Exported so tests can pin
42
+ * the output without writing files.
43
+ */
44
+ export declare function generateZod(schema: SchemaMetadata): string;
45
+ export declare function generateMetadata(schema: SchemaMetadata): string;
46
+ export declare function generateIndex(schema: SchemaMetadata): string;