@b9g/zen 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bun.js CHANGED
@@ -3,15 +3,15 @@ import {
3
3
  placeholder,
4
4
  quoteIdent,
5
5
  renderDDL
6
- } from "../chunk-NBXBBEMA.js";
6
+ } from "./_chunks/chunk-2C6KOX4F.js";
7
7
  import {
8
8
  generateDDL,
9
9
  generateViewDDL
10
- } from "../chunk-ARUUB3H4.js";
10
+ } from "./_chunks/chunk-2R6FDKLS.js";
11
11
  import {
12
12
  getTableMeta,
13
13
  resolveSQLBuiltin
14
- } from "../chunk-BEX6VPES.js";
14
+ } from "./_chunks/chunk-DKLSJISE.js";
15
15
 
16
16
  // src/bun.ts
17
17
  import { SQL } from "bun";
@@ -370,7 +370,9 @@ var BunDriver = class {
370
370
  },
371
371
  transaction: async () => {
372
372
  throw new Error("Nested transactions are not supported");
373
- }
373
+ },
374
+ getColumns: this.getColumns.bind(this),
375
+ explain: this.explain.bind(this)
374
376
  };
375
377
  return await fn(txDriver);
376
378
  });
@@ -524,6 +526,15 @@ var BunDriver = class {
524
526
  async getColumns(tableName) {
525
527
  return await this.#getColumns(tableName);
526
528
  }
529
+ async explain(strings, values) {
530
+ await this.#ensureSqliteInit();
531
+ const { sql, params } = buildSQL(strings, values, this.#dialect);
532
+ const explainPrefix = this.#dialect === "sqlite" ? "EXPLAIN QUERY PLAN " : "EXPLAIN ";
533
+ return await this.#sql.unsafe(
534
+ explainPrefix + sql,
535
+ params
536
+ );
537
+ }
527
538
  // ==========================================================================
528
539
  // Introspection Helpers (private)
529
540
  // ==========================================================================
@@ -742,7 +753,7 @@ var BunDriver = class {
742
753
  return applied;
743
754
  }
744
755
  async #addColumn(table, fieldName) {
745
- const { generateColumnDDL } = await import("../ddl-OT6HPLQY.js");
756
+ const { generateColumnDDL } = await import("./_chunks/ddl-32B7E53E.js");
746
757
  const zodType = table.schema.shape[fieldName];
747
758
  const fieldMeta = table.meta.fields[fieldName] || {};
748
759
  const colTemplate = generateColumnDDL(
@@ -0,0 +1,52 @@
1
+ /**
2
+ * SQL Builtins - SQL-native values with no JavaScript equivalent.
3
+ *
4
+ * These symbols represent database functions that are evaluated at query time,
5
+ * not in JavaScript. They're used for default values and expressions.
6
+ */
7
+ /**
8
+ * Current timestamp - resolves to CURRENT_TIMESTAMP (standard SQL).
9
+ *
10
+ * @example
11
+ * createdAt: z.date().db.inserted(CURRENT_TIMESTAMP)
12
+ * updatedAt: z.date().db.updated(NOW)
13
+ */
14
+ export declare const CURRENT_TIMESTAMP: unique symbol;
15
+ /**
16
+ * Current date - resolves to CURRENT_DATE (standard SQL).
17
+ *
18
+ * @example
19
+ * dateOnly: z.date().db.inserted(CURRENT_DATE)
20
+ */
21
+ export declare const CURRENT_DATE: unique symbol;
22
+ /**
23
+ * Current time - resolves to CURRENT_TIME (standard SQL).
24
+ *
25
+ * @example
26
+ * timeOnly: z.string().db.inserted(CURRENT_TIME)
27
+ */
28
+ export declare const CURRENT_TIME: unique symbol;
29
+ /**
30
+ * Ergonomic alias for CURRENT_TIMESTAMP.
31
+ *
32
+ * @example
33
+ * createdAt: z.date().db.inserted(NOW)
34
+ */
35
+ export declare const NOW: typeof CURRENT_TIMESTAMP;
36
+ /**
37
+ * Ergonomic alias for CURRENT_DATE.
38
+ *
39
+ * @example
40
+ * dateOnly: z.date().db.inserted(TODAY)
41
+ */
42
+ export declare const TODAY: typeof CURRENT_DATE;
43
+ /** SQL builtins - SQL-native values with no JavaScript representation */
44
+ export type SQLBuiltin = typeof CURRENT_TIMESTAMP | typeof CURRENT_DATE | typeof CURRENT_TIME;
45
+ /**
46
+ * Check if a value is a known SQL builtin.
47
+ */
48
+ export declare function isSQLBuiltin(value: unknown): value is SQLBuiltin;
49
+ /**
50
+ * Resolve a SQL builtin symbol to its SQL representation.
51
+ */
52
+ export declare function resolveSQLBuiltin(sym: symbol): string;
@@ -0,0 +1,511 @@
1
+ /**
2
+ * Database wrapper - the main API for schema-driven SQL.
3
+ *
4
+ * Provides typed queries with entity normalization and reference resolution.
5
+ * Extends EventTarget for IndexedDB-style migration events.
6
+ */
7
+ import type { Table, View, Queryable, Row, Insert, FullTableOnly, JoinedRow } from "./table.js";
8
+ import { CURRENT_TIMESTAMP, CURRENT_DATE, CURRENT_TIME, NOW, TODAY, isSQLBuiltin, resolveSQLBuiltin, type SQLBuiltin } from "./builtins.js";
9
+ export { CURRENT_TIMESTAMP, CURRENT_DATE, CURRENT_TIME, NOW, TODAY, isSQLBuiltin, resolveSQLBuiltin, type SQLBuiltin, };
10
+ /**
11
+ * Encode data for database insert/update operations.
12
+ * Converts app values → DB values using .db.encode() functions.
13
+ *
14
+ * Priority order:
15
+ * 1. Custom field-level .db.encode() (always wins)
16
+ * 2. Driver.encodeValue() (dialect-specific)
17
+ * 3. Auto-encode fallback (JSON.stringify, Date→string)
18
+ *
19
+ * @param table - The table definition
20
+ * @param data - The data to encode
21
+ * @param driver - Optional driver for dialect-specific encoding
22
+ */
23
+ export declare function encodeData<T extends Table<any>>(table: T, data: Record<string, unknown>, driver?: Driver): Record<string, unknown>;
24
+ import { decodeData } from "./table.js";
25
+ export { decodeData };
26
+ /**
27
+ * Database driver interface.
28
+ *
29
+ * Drivers own all SQL generation and dialect-specific behavior. They receive
30
+ * template parts (strings + values) and build SQL with native placeholders.
31
+ *
32
+ * This keeps the Database class dialect-agnostic - it only interacts with
33
+ * drivers through this interface and the `supportsReturning` capability flag.
34
+ */
35
+ export interface Driver {
36
+ /**
37
+ * Execute a query and return all rows.
38
+ * Driver joins strings with native placeholders (? or $1, $2, ...).
39
+ */
40
+ all<T = Record<string, unknown>>(strings: TemplateStringsArray, values: unknown[]): Promise<T[]>;
41
+ /**
42
+ * Execute a query and return the first row.
43
+ */
44
+ get<T = Record<string, unknown>>(strings: TemplateStringsArray, values: unknown[]): Promise<T | null>;
45
+ /**
46
+ * Execute a statement and return the number of affected rows.
47
+ */
48
+ run(strings: TemplateStringsArray, values: unknown[]): Promise<number>;
49
+ /**
50
+ * Execute a query and return a single value, or null if no rows.
51
+ */
52
+ val<T = unknown>(strings: TemplateStringsArray, values: unknown[]): Promise<T | null>;
53
+ /**
54
+ * Close the database connection.
55
+ */
56
+ close(): Promise<void>;
57
+ /**
58
+ * Execute a function within a database transaction.
59
+ */
60
+ transaction<T>(fn: (txDriver: Driver) => Promise<T>): Promise<T>;
61
+ /**
62
+ * Whether this driver supports RETURNING clause for INSERT/UPDATE.
63
+ * - SQLite: true
64
+ * - PostgreSQL: true
65
+ * - MySQL: false
66
+ * - MariaDB 10.5+: true
67
+ */
68
+ readonly supportsReturning: boolean;
69
+ /**
70
+ * Execute a function while holding an exclusive migration lock.
71
+ */
72
+ withMigrationLock?<T>(fn: () => Promise<T>): Promise<T>;
73
+ /**
74
+ * Encode a JS value for database insertion.
75
+ * Called for each value before building the SQL query.
76
+ *
77
+ * @param value - The JS value to encode
78
+ * @param fieldType - The field type: "text", "integer", "real", "boolean", "datetime", "json"
79
+ * @returns The encoded value suitable for this database dialect
80
+ */
81
+ encodeValue?(value: unknown, fieldType: string): unknown;
82
+ /**
83
+ * Decode a database value to JS.
84
+ * Called for each column value after query execution.
85
+ *
86
+ * @param value - The raw database value
87
+ * @param fieldType - The field type: "text", "integer", "real", "boolean", "datetime", "json"
88
+ * @returns The decoded JS value
89
+ */
90
+ decodeValue?(value: unknown, fieldType: string): unknown;
91
+ /**
92
+ * Ensure a table exists with its columns and indexes.
93
+ *
94
+ * **For new tables**: Creates the table with full structure including
95
+ * primary key, unique constraints, foreign keys, and indexes.
96
+ *
97
+ * **For existing tables**: Only performs safe, additive operations:
98
+ * - Adds missing columns
99
+ * - Adds missing non-unique indexes
100
+ *
101
+ * Throws SchemaDriftError if existing table has missing constraints
102
+ * (directs user to run ensureConstraints).
103
+ */
104
+ ensureTable?<T extends Table<any>>(table: T): Promise<EnsureResult>;
105
+ /**
106
+ * Ensure constraints (unique, foreign key) are applied to an existing table.
107
+ *
108
+ * Performs preflight checks to detect data violations before applying
109
+ * constraints. Throws ConstraintPreflightError if violations found.
110
+ */
111
+ ensureConstraints?<T extends Table<any>>(table: T): Promise<EnsureResult>;
112
+ /**
113
+ * Ensure a view exists in the database.
114
+ *
115
+ * Creates the view if it doesn't exist, or replaces it if it does.
116
+ * The base table must already exist.
117
+ */
118
+ ensureView?<T extends View<any>>(view: T): Promise<EnsureResult>;
119
+ /**
120
+ * Copy column data for safe rename migrations.
121
+ *
122
+ * Executes: UPDATE <table> SET <toField> = <fromField> WHERE <toField> IS NULL
123
+ *
124
+ * @param table The table to update
125
+ * @param fromField Source column (may be legacy/not in schema)
126
+ * @param toField Destination column (must exist in schema)
127
+ * @returns Number of rows updated
128
+ */
129
+ copyColumn?<T extends Table<any>>(table: T, fromField: string, toField: string): Promise<number>;
130
+ /** Introspection: list columns for a table (name, type, nullability). */
131
+ getColumns(tableName: string): Promise<{
132
+ name: string;
133
+ type?: string;
134
+ notnull?: boolean;
135
+ }[]>;
136
+ /** Get the query execution plan for a SQL query. */
137
+ explain(strings: TemplateStringsArray, values: unknown[]): Promise<Record<string, unknown>[]>;
138
+ }
139
+ /**
140
+ * Result from ensure operations.
141
+ */
142
+ export interface EnsureResult {
143
+ /** Whether any DDL was executed (false = no-op) */
144
+ applied: boolean;
145
+ }
146
+ /**
147
+ * Event fired when database version increases during open().
148
+ *
149
+ * Similar to IndexedDB's IDBVersionChangeEvent combined with
150
+ * ServiceWorker's ExtendableEvent (for waitUntil support).
151
+ *
152
+ * **Migration model**: Zealot uses monotonic, forward-only versioning:
153
+ * - Versions are integers that only increase: 1 → 2 → 3 → ...
154
+ * - Downgrading (e.g., 3 → 2) is NOT supported
155
+ * - Branching version histories are NOT supported
156
+ * - Each version should be deployed once and never modified
157
+ *
158
+ * **Best practices**:
159
+ * - Use conditional checks: `if (e.oldVersion < 2) { ... }`
160
+ * - Prefer additive changes (new columns, indexes) over destructive ones
161
+ * - Never modify past migrations - add new versions instead
162
+ * - Keep migrations idempotent when possible (use db.ensureTable())
163
+ */
164
+ export declare class DatabaseUpgradeEvent extends Event {
165
+ #private;
166
+ readonly oldVersion: number;
167
+ readonly newVersion: number;
168
+ constructor(type: string, init: {
169
+ oldVersion: number;
170
+ newVersion: number;
171
+ });
172
+ /**
173
+ * Extend the event lifetime until the promise settles.
174
+ * Like ExtendableEvent.waitUntil() from ServiceWorker.
175
+ */
176
+ waitUntil(promise: Promise<void>): void;
177
+ /**
178
+ * @internal Wait for all waitUntil promises to settle.
179
+ */
180
+ _settle(): Promise<void>;
181
+ }
182
+ /**
183
+ * Tagged template query function that returns normalized entities.
184
+ */
185
+ export type TaggedQuery<T> = (strings: TemplateStringsArray, ...values: unknown[]) => Promise<T>;
186
+ /**
187
+ * Transaction context with query methods.
188
+ *
189
+ * Provides the same query interface as Database, but bound to a single
190
+ * connection for the duration of the transaction.
191
+ */
192
+ export declare class Transaction {
193
+ #private;
194
+ constructor(driver: Driver);
195
+ all<T extends Queryable<any, any>>(table: T): TaggedQuery<Row<T>[]>;
196
+ all<T extends Queryable<any, any>, Rest extends Queryable<any, any>[]>(tables: [T, ...Rest]): TaggedQuery<JoinedRow<T, [T, ...Rest]>[]>;
197
+ get<T extends Queryable<any, any>>(table: T, id: string | number): Promise<Row<T> | null>;
198
+ get<T extends Queryable<any, any>>(table: T): TaggedQuery<Row<T> | null>;
199
+ get<T extends Queryable<any, any>, Rest extends Queryable<any, any>[]>(tables: [T, ...Rest]): TaggedQuery<JoinedRow<T, [T, ...Rest]> | null>;
200
+ insert<T extends Table<any>>(table: T & FullTableOnly<T>, data: Insert<T>): Promise<Row<T>>;
201
+ insert<T extends Table<any>>(table: T & FullTableOnly<T>, data: Insert<T>[]): Promise<Row<T>[]>;
202
+ update<T extends Table<any>>(table: T & FullTableOnly<T>, data: Partial<Insert<T>>, id: string | number): Promise<Row<T> | null>;
203
+ update<T extends Table<any>>(table: T & FullTableOnly<T>, data: Partial<Insert<T>>, ids: (string | number)[]): Promise<(Row<T> | null)[]>;
204
+ update<T extends Table<any>>(table: T & FullTableOnly<T>, data: Partial<Insert<T>>): TaggedQuery<Row<T>[]>;
205
+ delete<T extends Table<any>>(table: T, id: string | number): Promise<number>;
206
+ delete<T extends Table<any>>(table: T, ids: (string | number)[]): Promise<number>;
207
+ delete<T extends Table<any>>(table: T): TaggedQuery<number>;
208
+ softDelete<T extends Table<any>>(table: T, id: string | number): Promise<number>;
209
+ softDelete<T extends Table<any>>(table: T, ids: (string | number)[]): Promise<number>;
210
+ softDelete<T extends Table<any>>(table: T): TaggedQuery<number>;
211
+ query<T = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
212
+ exec(strings: TemplateStringsArray, ...values: unknown[]): Promise<number>;
213
+ val<T>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T | null>;
214
+ /**
215
+ * Print the generated SQL and parameters without executing.
216
+ * Useful for debugging query composition and fragment expansion.
217
+ * Note: SQL shown uses ? placeholders; actual query may use $1, $2 etc.
218
+ */
219
+ print(strings: TemplateStringsArray, ...values: unknown[]): {
220
+ sql: string;
221
+ params: unknown[];
222
+ };
223
+ }
224
+ /**
225
+ * Database wrapper with typed queries and entity normalization.
226
+ * Extends EventTarget for IndexedDB-style "upgradeneeded" events.
227
+ *
228
+ * @example
229
+ * const db = new Database(driver);
230
+ *
231
+ * db.addEventListener("upgradeneeded", (e) => {
232
+ * e.waitUntil(runMigrations(e));
233
+ * });
234
+ *
235
+ * await db.open(2);
236
+ */
237
+ export declare class Database extends EventTarget {
238
+ #private;
239
+ constructor(driver: Driver, options?: {
240
+ tables?: Table<any>[];
241
+ });
242
+ /**
243
+ * Current database schema version.
244
+ * Returns 0 if database has never been opened.
245
+ */
246
+ get version(): number;
247
+ /**
248
+ * Open the database at a specific version.
249
+ *
250
+ * If the requested version is higher than the current version,
251
+ * fires an "upgradeneeded" event and waits for all waitUntil()
252
+ * promises before completing.
253
+ *
254
+ * Migration safety: Uses exclusive locking to prevent race conditions
255
+ * when multiple processes attempt migrations simultaneously.
256
+ *
257
+ * @example
258
+ * db.addEventListener("upgradeneeded", (e) => {
259
+ * e.waitUntil(runMigrations(e));
260
+ * });
261
+ * await db.open(2);
262
+ */
263
+ open(version: number): Promise<void>;
264
+ /**
265
+ * Query multiple entities with joins and reference resolution.
266
+ *
267
+ * @example
268
+ * // Single table
269
+ * const posts = await db.all(Posts)`WHERE published = ${true}`;
270
+ *
271
+ * // Multi-table with joins (typed!)
272
+ * const posts = await db.all([Posts, Users])`
273
+ * JOIN users ON users.id = posts.author_id
274
+ * WHERE published = ${true}
275
+ * `;
276
+ * posts[0].author.name // typed as string!
277
+ */
278
+ all<T extends Queryable<any, any>>(table: T): TaggedQuery<Row<T>[]>;
279
+ all<T extends Queryable<any, any>, Rest extends Queryable<any, any>[]>(tables: [T, ...Rest]): TaggedQuery<JoinedRow<T, [T, ...Rest]>[]>;
280
+ /**
281
+ * Query a single entity.
282
+ *
283
+ * @example
284
+ * // By primary key
285
+ * const post = await db.get(Posts, postId);
286
+ *
287
+ * // With query
288
+ * const post = await db.get(Posts)`WHERE slug = ${slug}`;
289
+ *
290
+ * // Multi-table (typed!)
291
+ * const post = await db.get([Posts, Users])`
292
+ * JOIN users ON users.id = posts.author_id
293
+ * WHERE posts.id = ${postId}
294
+ * `;
295
+ * post?.author.name // typed as string!
296
+ */
297
+ get<T extends Queryable<any, any>>(table: T, id: string | number): Promise<Row<T> | null>;
298
+ get<T extends Queryable<any, any>>(table: T): TaggedQuery<Row<T> | null>;
299
+ get<T extends Queryable<any, any>, Rest extends Queryable<any, any>[]>(tables: [T, ...Rest]): TaggedQuery<JoinedRow<T, [T, ...Rest]> | null>;
300
+ /**
301
+ * Insert one or more entities.
302
+ *
303
+ * Uses RETURNING to get the actual inserted row(s) (with DB defaults).
304
+ *
305
+ * @example
306
+ * // Single insert
307
+ * const user = await db.insert(Users, {
308
+ * id: crypto.randomUUID(),
309
+ * email: "alice@example.com",
310
+ * name: "Alice",
311
+ * });
312
+ *
313
+ * // Bulk insert
314
+ * const users = await db.insert(Users, [
315
+ * { id: "1", email: "alice@example.com", name: "Alice" },
316
+ * { id: "2", email: "bob@example.com", name: "Bob" },
317
+ * ]);
318
+ */
319
+ insert<T extends Table<any>>(table: T & FullTableOnly<T>, data: Insert<T>): Promise<Row<T>>;
320
+ insert<T extends Table<any>>(table: T & FullTableOnly<T>, data: Insert<T>[]): Promise<Row<T>[]>;
321
+ /**
322
+ * Update entities.
323
+ *
324
+ * @example
325
+ * // Update by primary key
326
+ * const user = await db.update(Users, { name: "Bob" }, userId);
327
+ *
328
+ * // Update multiple by primary keys
329
+ * const users = await db.update(Users, { active: true }, [id1, id2, id3]);
330
+ *
331
+ * // Update with custom WHERE clause
332
+ * const count = await db.update(Users, { active: false })`WHERE lastLogin < ${cutoff}`;
333
+ */
334
+ update<T extends Table<any>>(table: T & FullTableOnly<T>, data: Partial<Insert<T>>, id: string | number): Promise<Row<T> | null>;
335
+ update<T extends Table<any>>(table: T & FullTableOnly<T>, data: Partial<Insert<T>>, ids: (string | number)[]): Promise<(Row<T> | null)[]>;
336
+ update<T extends Table<any>>(table: T & FullTableOnly<T>, data: Partial<Insert<T>>): TaggedQuery<Row<T>[]>;
337
+ /**
338
+ * Delete entities.
339
+ *
340
+ * @example
341
+ * // Delete by primary key (returns 0 or 1)
342
+ * const count = await db.delete(Users, userId);
343
+ *
344
+ * // Delete multiple by primary keys
345
+ * const count = await db.delete(Users, [id1, id2, id3]);
346
+ *
347
+ * // Delete with custom WHERE clause
348
+ * const count = await db.delete(Users)`WHERE inactive = ${true}`;
349
+ */
350
+ delete<T extends Table<any>>(table: T, id: string | number): Promise<number>;
351
+ delete<T extends Table<any>>(table: T, ids: (string | number)[]): Promise<number>;
352
+ delete<T extends Table<any>>(table: T): TaggedQuery<number>;
353
+ /**
354
+ * Soft delete entities by marking the soft delete field with the current timestamp.
355
+ *
356
+ * @example
357
+ * // Soft delete by primary key (returns 0 or 1)
358
+ * const count = await db.softDelete(Users, userId);
359
+ *
360
+ * // Soft delete multiple by primary keys
361
+ * const count = await db.softDelete(Users, [id1, id2, id3]);
362
+ *
363
+ * // Soft delete with custom WHERE clause
364
+ * const count = await db.softDelete(Users)`WHERE inactive = ${true}`;
365
+ */
366
+ softDelete<T extends Table<any>>(table: T, id: string | number): Promise<number>;
367
+ softDelete<T extends Table<any>>(table: T, ids: (string | number)[]): Promise<number>;
368
+ softDelete<T extends Table<any>>(table: T): TaggedQuery<number>;
369
+ /**
370
+ * Execute a raw query and return rows.
371
+ *
372
+ * @example
373
+ * const counts = await db.query<{ count: number }>`
374
+ * SELECT COUNT(*) as count FROM posts WHERE author_id = ${userId}
375
+ * `;
376
+ */
377
+ query<T = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
378
+ /**
379
+ * Execute a statement (INSERT, UPDATE, DELETE, DDL).
380
+ *
381
+ * @example
382
+ * await db.exec`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY)`;
383
+ */
384
+ exec(strings: TemplateStringsArray, ...values: unknown[]): Promise<number>;
385
+ /**
386
+ * Execute a query and return a single value.
387
+ *
388
+ * @example
389
+ * const count = await db.val<number>`SELECT COUNT(*) FROM posts`;
390
+ */
391
+ val<T>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T | null>;
392
+ /**
393
+ * Print the generated SQL and parameters without executing.
394
+ * Useful for debugging query composition and fragment expansion.
395
+ * Note: SQL shown uses ? placeholders; actual query may use $1, $2 etc.
396
+ */
397
+ print(strings: TemplateStringsArray, ...values: unknown[]): {
398
+ sql: string;
399
+ params: unknown[];
400
+ };
401
+ /**
402
+ * Get the query execution plan without running the query.
403
+ *
404
+ * Returns the database's EXPLAIN output for the given query.
405
+ * The format varies by database:
406
+ * - SQLite: EXPLAIN QUERY PLAN output
407
+ * - PostgreSQL: EXPLAIN output
408
+ * - MySQL: EXPLAIN output
409
+ *
410
+ * @example
411
+ * const plan = await db.explain`SELECT * FROM ${Users} WHERE email = ${"test@example.com"}`;
412
+ * console.log(plan);
413
+ */
414
+ explain(strings: TemplateStringsArray, ...values: unknown[]): Promise<Record<string, unknown>[]>;
415
+ /**
416
+ * Ensure a table exists with its columns and indexes.
417
+ *
418
+ * **For new tables**: Creates the table with full structure including
419
+ * primary key, unique constraints, foreign keys, and indexes.
420
+ *
421
+ * **For existing tables**: Only performs safe, additive operations:
422
+ * - Adds missing columns
423
+ * - Adds missing non-unique indexes
424
+ *
425
+ * Unique constraints and foreign keys on existing tables require
426
+ * explicit `ensureConstraints()` call (they can fail or lock).
427
+ *
428
+ * @throws {EnsureError} If DDL execution fails
429
+ * @throws {SchemaDriftError} If existing table has missing constraints
430
+ * (directs user to run ensureConstraints)
431
+ *
432
+ * @example
433
+ * // In migration handler
434
+ * await db.ensureTable(Users);
435
+ * await db.ensureTable(Posts); // FK to Users - ensure Users first
436
+ */
437
+ ensureTable<T extends Table<any>>(table: T): Promise<EnsureResult>;
438
+ /**
439
+ * Ensure a view exists in the database.
440
+ *
441
+ * Creates the view if it doesn't exist, or replaces it if it does.
442
+ * The base table must already exist.
443
+ *
444
+ * @example
445
+ * const ActiveUsers = view("active_users", Users)`WHERE ${Users.cols.deletedAt} IS NULL`;
446
+ * await db.ensureTable(Users);
447
+ * await db.ensureView(ActiveUsers);
448
+ */
449
+ ensureView<T extends View<any>>(viewObj: T): Promise<EnsureResult>;
450
+ /**
451
+ * Ensure constraints (unique, foreign key) are applied to an existing table.
452
+ *
453
+ * **WARNING**: This operation can be expensive and cause locks on large tables.
454
+ * It performs preflight checks to detect data violations before applying constraints.
455
+ *
456
+ * For each declared constraint:
457
+ * 1. Preflight: Check for violations (duplicates for UNIQUE, orphans for FK)
458
+ * 2. If violations found: Throw ConstraintPreflightError with diagnostic query
459
+ * 3. If clean: Apply the constraint
460
+ *
461
+ * @throws {Error} If table doesn't exist
462
+ * @throws {ConstraintPreflightError} If data violates a constraint
463
+ * @throws {EnsureError} If DDL execution fails
464
+ *
465
+ * @example
466
+ * // After ensuring table structure
467
+ * await db.ensureTable(Users);
468
+ * // Explicitly apply constraints (may lock, may fail)
469
+ * await db.ensureConstraints(Users);
470
+ */
471
+ ensureConstraints<T extends Table<any>>(table: T): Promise<EnsureResult>;
472
+ /**
473
+ * Copy column data for safe rename migrations.
474
+ *
475
+ * Executes: UPDATE <table> SET <toField> = <fromField> WHERE <toField> IS NULL
476
+ *
477
+ * This is idempotent - rows where toField already has a value are skipped.
478
+ * The fromField may be a legacy column not in the current schema.
479
+ *
480
+ * @param table The table to update
481
+ * @param fromField Source column (may be legacy/not in schema)
482
+ * @param toField Destination column (must exist in schema)
483
+ * @returns Number of rows updated
484
+ *
485
+ * @example
486
+ * // Rename "email" to "emailAddress":
487
+ * // 1. Add new column
488
+ * await db.ensureTable(UsersWithEmailAddress);
489
+ * // 2. Copy data
490
+ * const updated = await db.copyColumn(Users, "email", "emailAddress");
491
+ * // 3. Later: remove old column (manual migration)
492
+ */
493
+ copyColumn<T extends Table<any>>(table: T, fromField: string, toField: string): Promise<number>;
494
+ /**
495
+ * Execute a function within a database transaction.
496
+ *
497
+ * If the function completes successfully, the transaction is committed.
498
+ * If the function throws an error, the transaction is rolled back.
499
+ *
500
+ * All operations within the transaction callback use the same database
501
+ * connection, ensuring transactional consistency.
502
+ *
503
+ * @example
504
+ * await db.transaction(async (tx) => {
505
+ * const user = await tx.insert(users, { id: "1", name: "Alice" });
506
+ * await tx.insert(posts, { id: "1", authorId: user.id, title: "Hello" });
507
+ * // If any insert fails, both are rolled back
508
+ * });
509
+ */
510
+ transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
511
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * DDL generation from table definitions.
3
+ *
4
+ * Generates CREATE TABLE statements for SQLite, PostgreSQL, and MySQL.
5
+ * Uses only Zod's public APIs - no _def access.
6
+ */
7
+ import { z } from "zod";
8
+ import type { Table, View } from "./table.js";
9
+ import { type SQLTemplate } from "./template.js";
10
+ export type SQLDialect = "sqlite" | "postgresql" | "mysql";
11
+ export interface DDLOptions {
12
+ dialect?: SQLDialect;
13
+ ifNotExists?: boolean;
14
+ }
15
+ /**
16
+ * Generate a single column definition as a template with ident markers.
17
+ * @internal Used by driver ensureTable implementations.
18
+ */
19
+ export declare function generateColumnDDL(fieldName: string, zodType: z.ZodType, fieldMeta: Record<string, any>, dialect?: SQLDialect): SQLTemplate;
20
+ /**
21
+ * Generate CREATE TABLE DDL as a template with ident markers.
22
+ * Pass to driver.run() to execute, or use renderDDL() in tests.
23
+ */
24
+ export declare function generateDDL<T extends Table<any>>(table: T, options?: DDLOptions): SQLTemplate;
25
+ /**
26
+ * Generate CREATE VIEW DDL as a template.
27
+ *
28
+ * For views, the DDL is:
29
+ * DROP VIEW IF EXISTS "view_name";
30
+ * CREATE VIEW "view_name" AS SELECT * FROM "base_table" WHERE ...;
31
+ *
32
+ * Note: We use DROP + CREATE because SQLite doesn't support CREATE OR REPLACE VIEW.
33
+ */
34
+ export declare function generateViewDDL<T extends View<any>>(viewObj: T, _options?: DDLOptions): SQLTemplate;