@b9g/zen 0.1.2 → 0.1.3
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/CHANGELOG.md +22 -0
- package/README.md +120 -99
- package/package.json +3 -3
- package/{chunk-NBXBBEMA.js → src/_chunks/chunk-2C6KOX4F.js} +1 -1
- package/{chunk-ARUUB3H4.js → src/_chunks/chunk-2R6FDKLS.js} +1 -1
- package/{chunk-BEX6VPES.js → src/_chunks/chunk-DKLSJISE.js} +24 -0
- package/{ddl-OT6HPLQY.js → src/_chunks/ddl-32B7E53E.js} +2 -2
- package/src/bun.js +4 -4
- package/src/impl/builtins.d.ts +52 -0
- package/src/impl/database.d.ts +495 -0
- package/src/impl/ddl.d.ts +34 -0
- package/src/impl/errors.d.ts +195 -0
- package/src/impl/query.d.ts +249 -0
- package/src/impl/sql.d.ts +47 -0
- package/src/impl/table.d.ts +961 -0
- package/src/impl/template.d.ts +75 -0
- package/src/mysql.js +3 -3
- package/src/postgres.js +3 -3
- package/src/sqlite.js +3 -3
- package/src/zen.js +2 -2
|
@@ -0,0 +1,495 @@
|
|
|
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
|
+
/** Optional 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
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Result from ensure operations.
|
|
139
|
+
*/
|
|
140
|
+
export interface EnsureResult {
|
|
141
|
+
/** Whether any DDL was executed (false = no-op) */
|
|
142
|
+
applied: boolean;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Event fired when database version increases during open().
|
|
146
|
+
*
|
|
147
|
+
* Similar to IndexedDB's IDBVersionChangeEvent combined with
|
|
148
|
+
* ServiceWorker's ExtendableEvent (for waitUntil support).
|
|
149
|
+
*
|
|
150
|
+
* **Migration model**: Zealot uses monotonic, forward-only versioning:
|
|
151
|
+
* - Versions are integers that only increase: 1 → 2 → 3 → ...
|
|
152
|
+
* - Downgrading (e.g., 3 → 2) is NOT supported
|
|
153
|
+
* - Branching version histories are NOT supported
|
|
154
|
+
* - Each version should be deployed once and never modified
|
|
155
|
+
*
|
|
156
|
+
* **Best practices**:
|
|
157
|
+
* - Use conditional checks: `if (e.oldVersion < 2) { ... }`
|
|
158
|
+
* - Prefer additive changes (new columns, indexes) over destructive ones
|
|
159
|
+
* - Never modify past migrations - add new versions instead
|
|
160
|
+
* - Keep migrations idempotent when possible (use db.ensureTable())
|
|
161
|
+
*/
|
|
162
|
+
export declare class DatabaseUpgradeEvent extends Event {
|
|
163
|
+
#private;
|
|
164
|
+
readonly oldVersion: number;
|
|
165
|
+
readonly newVersion: number;
|
|
166
|
+
constructor(type: string, init: {
|
|
167
|
+
oldVersion: number;
|
|
168
|
+
newVersion: number;
|
|
169
|
+
});
|
|
170
|
+
/**
|
|
171
|
+
* Extend the event lifetime until the promise settles.
|
|
172
|
+
* Like ExtendableEvent.waitUntil() from ServiceWorker.
|
|
173
|
+
*/
|
|
174
|
+
waitUntil(promise: Promise<void>): void;
|
|
175
|
+
/**
|
|
176
|
+
* @internal Wait for all waitUntil promises to settle.
|
|
177
|
+
*/
|
|
178
|
+
_settle(): Promise<void>;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Tagged template query function that returns normalized entities.
|
|
182
|
+
*/
|
|
183
|
+
export type TaggedQuery<T> = (strings: TemplateStringsArray, ...values: unknown[]) => Promise<T>;
|
|
184
|
+
/**
|
|
185
|
+
* Transaction context with query methods.
|
|
186
|
+
*
|
|
187
|
+
* Provides the same query interface as Database, but bound to a single
|
|
188
|
+
* connection for the duration of the transaction.
|
|
189
|
+
*/
|
|
190
|
+
export declare class Transaction {
|
|
191
|
+
#private;
|
|
192
|
+
constructor(driver: Driver);
|
|
193
|
+
all<T extends Queryable<any, any>>(table: T): TaggedQuery<Row<T>[]>;
|
|
194
|
+
all<T extends Queryable<any, any>, Rest extends Queryable<any, any>[]>(tables: [T, ...Rest]): TaggedQuery<JoinedRow<T, [T, ...Rest]>[]>;
|
|
195
|
+
get<T extends Queryable<any, any>>(table: T, id: string | number): Promise<Row<T> | null>;
|
|
196
|
+
get<T extends Queryable<any, any>>(table: T): TaggedQuery<Row<T> | null>;
|
|
197
|
+
get<T extends Queryable<any, any>, Rest extends Queryable<any, any>[]>(tables: [T, ...Rest]): TaggedQuery<JoinedRow<T, [T, ...Rest]> | null>;
|
|
198
|
+
insert<T extends Table<any>>(table: T & FullTableOnly<T>, data: Insert<T>): Promise<Row<T>>;
|
|
199
|
+
insert<T extends Table<any>>(table: T & FullTableOnly<T>, data: Insert<T>[]): Promise<Row<T>[]>;
|
|
200
|
+
update<T extends Table<any>>(table: T & FullTableOnly<T>, data: Partial<Insert<T>>, id: string | number): Promise<Row<T> | null>;
|
|
201
|
+
update<T extends Table<any>>(table: T & FullTableOnly<T>, data: Partial<Insert<T>>, ids: (string | number)[]): Promise<(Row<T> | null)[]>;
|
|
202
|
+
update<T extends Table<any>>(table: T & FullTableOnly<T>, data: Partial<Insert<T>>): TaggedQuery<Row<T>[]>;
|
|
203
|
+
delete<T extends Table<any>>(table: T, id: string | number): Promise<number>;
|
|
204
|
+
delete<T extends Table<any>>(table: T, ids: (string | number)[]): Promise<number>;
|
|
205
|
+
delete<T extends Table<any>>(table: T): TaggedQuery<number>;
|
|
206
|
+
softDelete<T extends Table<any>>(table: T, id: string | number): Promise<number>;
|
|
207
|
+
softDelete<T extends Table<any>>(table: T, ids: (string | number)[]): Promise<number>;
|
|
208
|
+
softDelete<T extends Table<any>>(table: T): TaggedQuery<number>;
|
|
209
|
+
query<T = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
|
|
210
|
+
exec(strings: TemplateStringsArray, ...values: unknown[]): Promise<number>;
|
|
211
|
+
val<T>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T | null>;
|
|
212
|
+
/**
|
|
213
|
+
* Print the generated SQL and parameters without executing.
|
|
214
|
+
* Useful for debugging query composition and fragment expansion.
|
|
215
|
+
* Note: SQL shown uses ? placeholders; actual query may use $1, $2 etc.
|
|
216
|
+
*/
|
|
217
|
+
print(strings: TemplateStringsArray, ...values: unknown[]): {
|
|
218
|
+
sql: string;
|
|
219
|
+
params: unknown[];
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Database wrapper with typed queries and entity normalization.
|
|
224
|
+
* Extends EventTarget for IndexedDB-style "upgradeneeded" events.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* const db = new Database(driver);
|
|
228
|
+
*
|
|
229
|
+
* db.addEventListener("upgradeneeded", (e) => {
|
|
230
|
+
* e.waitUntil(runMigrations(e));
|
|
231
|
+
* });
|
|
232
|
+
*
|
|
233
|
+
* await db.open(2);
|
|
234
|
+
*/
|
|
235
|
+
export declare class Database extends EventTarget {
|
|
236
|
+
#private;
|
|
237
|
+
constructor(driver: Driver, options?: {
|
|
238
|
+
tables?: Table<any>[];
|
|
239
|
+
});
|
|
240
|
+
/**
|
|
241
|
+
* Current database schema version.
|
|
242
|
+
* Returns 0 if database has never been opened.
|
|
243
|
+
*/
|
|
244
|
+
get version(): number;
|
|
245
|
+
/**
|
|
246
|
+
* Open the database at a specific version.
|
|
247
|
+
*
|
|
248
|
+
* If the requested version is higher than the current version,
|
|
249
|
+
* fires an "upgradeneeded" event and waits for all waitUntil()
|
|
250
|
+
* promises before completing.
|
|
251
|
+
*
|
|
252
|
+
* Migration safety: Uses exclusive locking to prevent race conditions
|
|
253
|
+
* when multiple processes attempt migrations simultaneously.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* db.addEventListener("upgradeneeded", (e) => {
|
|
257
|
+
* e.waitUntil(runMigrations(e));
|
|
258
|
+
* });
|
|
259
|
+
* await db.open(2);
|
|
260
|
+
*/
|
|
261
|
+
open(version: number): Promise<void>;
|
|
262
|
+
/**
|
|
263
|
+
* Query multiple entities with joins and reference resolution.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* // Single table
|
|
267
|
+
* const posts = await db.all(Posts)`WHERE published = ${true}`;
|
|
268
|
+
*
|
|
269
|
+
* // Multi-table with joins (typed!)
|
|
270
|
+
* const posts = await db.all([Posts, Users])`
|
|
271
|
+
* JOIN users ON users.id = posts.author_id
|
|
272
|
+
* WHERE published = ${true}
|
|
273
|
+
* `;
|
|
274
|
+
* posts[0].author.name // typed as string!
|
|
275
|
+
*/
|
|
276
|
+
all<T extends Queryable<any, any>>(table: T): TaggedQuery<Row<T>[]>;
|
|
277
|
+
all<T extends Queryable<any, any>, Rest extends Queryable<any, any>[]>(tables: [T, ...Rest]): TaggedQuery<JoinedRow<T, [T, ...Rest]>[]>;
|
|
278
|
+
/**
|
|
279
|
+
* Query a single entity.
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* // By primary key
|
|
283
|
+
* const post = await db.get(Posts, postId);
|
|
284
|
+
*
|
|
285
|
+
* // With query
|
|
286
|
+
* const post = await db.get(Posts)`WHERE slug = ${slug}`;
|
|
287
|
+
*
|
|
288
|
+
* // Multi-table (typed!)
|
|
289
|
+
* const post = await db.get([Posts, Users])`
|
|
290
|
+
* JOIN users ON users.id = posts.author_id
|
|
291
|
+
* WHERE posts.id = ${postId}
|
|
292
|
+
* `;
|
|
293
|
+
* post?.author.name // typed as string!
|
|
294
|
+
*/
|
|
295
|
+
get<T extends Queryable<any, any>>(table: T, id: string | number): Promise<Row<T> | null>;
|
|
296
|
+
get<T extends Queryable<any, any>>(table: T): TaggedQuery<Row<T> | null>;
|
|
297
|
+
get<T extends Queryable<any, any>, Rest extends Queryable<any, any>[]>(tables: [T, ...Rest]): TaggedQuery<JoinedRow<T, [T, ...Rest]> | null>;
|
|
298
|
+
/**
|
|
299
|
+
* Insert one or more entities.
|
|
300
|
+
*
|
|
301
|
+
* Uses RETURNING to get the actual inserted row(s) (with DB defaults).
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* // Single insert
|
|
305
|
+
* const user = await db.insert(Users, {
|
|
306
|
+
* id: crypto.randomUUID(),
|
|
307
|
+
* email: "alice@example.com",
|
|
308
|
+
* name: "Alice",
|
|
309
|
+
* });
|
|
310
|
+
*
|
|
311
|
+
* // Bulk insert
|
|
312
|
+
* const users = await db.insert(Users, [
|
|
313
|
+
* { id: "1", email: "alice@example.com", name: "Alice" },
|
|
314
|
+
* { id: "2", email: "bob@example.com", name: "Bob" },
|
|
315
|
+
* ]);
|
|
316
|
+
*/
|
|
317
|
+
insert<T extends Table<any>>(table: T & FullTableOnly<T>, data: Insert<T>): Promise<Row<T>>;
|
|
318
|
+
insert<T extends Table<any>>(table: T & FullTableOnly<T>, data: Insert<T>[]): Promise<Row<T>[]>;
|
|
319
|
+
/**
|
|
320
|
+
* Update entities.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* // Update by primary key
|
|
324
|
+
* const user = await db.update(Users, { name: "Bob" }, userId);
|
|
325
|
+
*
|
|
326
|
+
* // Update multiple by primary keys
|
|
327
|
+
* const users = await db.update(Users, { active: true }, [id1, id2, id3]);
|
|
328
|
+
*
|
|
329
|
+
* // Update with custom WHERE clause
|
|
330
|
+
* const count = await db.update(Users, { active: false })`WHERE lastLogin < ${cutoff}`;
|
|
331
|
+
*/
|
|
332
|
+
update<T extends Table<any>>(table: T & FullTableOnly<T>, data: Partial<Insert<T>>, id: string | number): Promise<Row<T> | null>;
|
|
333
|
+
update<T extends Table<any>>(table: T & FullTableOnly<T>, data: Partial<Insert<T>>, ids: (string | number)[]): Promise<(Row<T> | null)[]>;
|
|
334
|
+
update<T extends Table<any>>(table: T & FullTableOnly<T>, data: Partial<Insert<T>>): TaggedQuery<Row<T>[]>;
|
|
335
|
+
/**
|
|
336
|
+
* Delete entities.
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* // Delete by primary key (returns 0 or 1)
|
|
340
|
+
* const count = await db.delete(Users, userId);
|
|
341
|
+
*
|
|
342
|
+
* // Delete multiple by primary keys
|
|
343
|
+
* const count = await db.delete(Users, [id1, id2, id3]);
|
|
344
|
+
*
|
|
345
|
+
* // Delete with custom WHERE clause
|
|
346
|
+
* const count = await db.delete(Users)`WHERE inactive = ${true}`;
|
|
347
|
+
*/
|
|
348
|
+
delete<T extends Table<any>>(table: T, id: string | number): Promise<number>;
|
|
349
|
+
delete<T extends Table<any>>(table: T, ids: (string | number)[]): Promise<number>;
|
|
350
|
+
delete<T extends Table<any>>(table: T): TaggedQuery<number>;
|
|
351
|
+
/**
|
|
352
|
+
* Soft delete entities by marking the soft delete field with the current timestamp.
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* // Soft delete by primary key (returns 0 or 1)
|
|
356
|
+
* const count = await db.softDelete(Users, userId);
|
|
357
|
+
*
|
|
358
|
+
* // Soft delete multiple by primary keys
|
|
359
|
+
* const count = await db.softDelete(Users, [id1, id2, id3]);
|
|
360
|
+
*
|
|
361
|
+
* // Soft delete with custom WHERE clause
|
|
362
|
+
* const count = await db.softDelete(Users)`WHERE inactive = ${true}`;
|
|
363
|
+
*/
|
|
364
|
+
softDelete<T extends Table<any>>(table: T, id: string | number): Promise<number>;
|
|
365
|
+
softDelete<T extends Table<any>>(table: T, ids: (string | number)[]): Promise<number>;
|
|
366
|
+
softDelete<T extends Table<any>>(table: T): TaggedQuery<number>;
|
|
367
|
+
/**
|
|
368
|
+
* Execute a raw query and return rows.
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* const counts = await db.query<{ count: number }>`
|
|
372
|
+
* SELECT COUNT(*) as count FROM posts WHERE author_id = ${userId}
|
|
373
|
+
* `;
|
|
374
|
+
*/
|
|
375
|
+
query<T = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
|
|
376
|
+
/**
|
|
377
|
+
* Execute a statement (INSERT, UPDATE, DELETE, DDL).
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* await db.exec`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY)`;
|
|
381
|
+
*/
|
|
382
|
+
exec(strings: TemplateStringsArray, ...values: unknown[]): Promise<number>;
|
|
383
|
+
/**
|
|
384
|
+
* Execute a query and return a single value.
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* const count = await db.val<number>`SELECT COUNT(*) FROM posts`;
|
|
388
|
+
*/
|
|
389
|
+
val<T>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T | null>;
|
|
390
|
+
/**
|
|
391
|
+
* Print the generated SQL and parameters without executing.
|
|
392
|
+
* Useful for debugging query composition and fragment expansion.
|
|
393
|
+
* Note: SQL shown uses ? placeholders; actual query may use $1, $2 etc.
|
|
394
|
+
*/
|
|
395
|
+
print(strings: TemplateStringsArray, ...values: unknown[]): {
|
|
396
|
+
sql: string;
|
|
397
|
+
params: unknown[];
|
|
398
|
+
};
|
|
399
|
+
/**
|
|
400
|
+
* Ensure a table exists with its columns and indexes.
|
|
401
|
+
*
|
|
402
|
+
* **For new tables**: Creates the table with full structure including
|
|
403
|
+
* primary key, unique constraints, foreign keys, and indexes.
|
|
404
|
+
*
|
|
405
|
+
* **For existing tables**: Only performs safe, additive operations:
|
|
406
|
+
* - Adds missing columns
|
|
407
|
+
* - Adds missing non-unique indexes
|
|
408
|
+
*
|
|
409
|
+
* Unique constraints and foreign keys on existing tables require
|
|
410
|
+
* explicit `ensureConstraints()` call (they can fail or lock).
|
|
411
|
+
*
|
|
412
|
+
* @throws {EnsureError} If DDL execution fails
|
|
413
|
+
* @throws {SchemaDriftError} If existing table has missing constraints
|
|
414
|
+
* (directs user to run ensureConstraints)
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* // In migration handler
|
|
418
|
+
* await db.ensureTable(Users);
|
|
419
|
+
* await db.ensureTable(Posts); // FK to Users - ensure Users first
|
|
420
|
+
*/
|
|
421
|
+
ensureTable<T extends Table<any>>(table: T): Promise<EnsureResult>;
|
|
422
|
+
/**
|
|
423
|
+
* Ensure a view exists in the database.
|
|
424
|
+
*
|
|
425
|
+
* Creates the view if it doesn't exist, or replaces it if it does.
|
|
426
|
+
* The base table must already exist.
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* const ActiveUsers = view("active_users", Users)`WHERE ${Users.cols.deletedAt} IS NULL`;
|
|
430
|
+
* await db.ensureTable(Users);
|
|
431
|
+
* await db.ensureView(ActiveUsers);
|
|
432
|
+
*/
|
|
433
|
+
ensureView<T extends View<any>>(viewObj: T): Promise<EnsureResult>;
|
|
434
|
+
/**
|
|
435
|
+
* Ensure constraints (unique, foreign key) are applied to an existing table.
|
|
436
|
+
*
|
|
437
|
+
* **WARNING**: This operation can be expensive and cause locks on large tables.
|
|
438
|
+
* It performs preflight checks to detect data violations before applying constraints.
|
|
439
|
+
*
|
|
440
|
+
* For each declared constraint:
|
|
441
|
+
* 1. Preflight: Check for violations (duplicates for UNIQUE, orphans for FK)
|
|
442
|
+
* 2. If violations found: Throw ConstraintPreflightError with diagnostic query
|
|
443
|
+
* 3. If clean: Apply the constraint
|
|
444
|
+
*
|
|
445
|
+
* @throws {Error} If table doesn't exist
|
|
446
|
+
* @throws {ConstraintPreflightError} If data violates a constraint
|
|
447
|
+
* @throws {EnsureError} If DDL execution fails
|
|
448
|
+
*
|
|
449
|
+
* @example
|
|
450
|
+
* // After ensuring table structure
|
|
451
|
+
* await db.ensureTable(Users);
|
|
452
|
+
* // Explicitly apply constraints (may lock, may fail)
|
|
453
|
+
* await db.ensureConstraints(Users);
|
|
454
|
+
*/
|
|
455
|
+
ensureConstraints<T extends Table<any>>(table: T): Promise<EnsureResult>;
|
|
456
|
+
/**
|
|
457
|
+
* Copy column data for safe rename migrations.
|
|
458
|
+
*
|
|
459
|
+
* Executes: UPDATE <table> SET <toField> = <fromField> WHERE <toField> IS NULL
|
|
460
|
+
*
|
|
461
|
+
* This is idempotent - rows where toField already has a value are skipped.
|
|
462
|
+
* The fromField may be a legacy column not in the current schema.
|
|
463
|
+
*
|
|
464
|
+
* @param table The table to update
|
|
465
|
+
* @param fromField Source column (may be legacy/not in schema)
|
|
466
|
+
* @param toField Destination column (must exist in schema)
|
|
467
|
+
* @returns Number of rows updated
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* // Rename "email" to "emailAddress":
|
|
471
|
+
* // 1. Add new column
|
|
472
|
+
* await db.ensureTable(UsersWithEmailAddress);
|
|
473
|
+
* // 2. Copy data
|
|
474
|
+
* const updated = await db.copyColumn(Users, "email", "emailAddress");
|
|
475
|
+
* // 3. Later: remove old column (manual migration)
|
|
476
|
+
*/
|
|
477
|
+
copyColumn<T extends Table<any>>(table: T, fromField: string, toField: string): Promise<number>;
|
|
478
|
+
/**
|
|
479
|
+
* Execute a function within a database transaction.
|
|
480
|
+
*
|
|
481
|
+
* If the function completes successfully, the transaction is committed.
|
|
482
|
+
* If the function throws an error, the transaction is rolled back.
|
|
483
|
+
*
|
|
484
|
+
* All operations within the transaction callback use the same database
|
|
485
|
+
* connection, ensuring transactional consistency.
|
|
486
|
+
*
|
|
487
|
+
* @example
|
|
488
|
+
* await db.transaction(async (tx) => {
|
|
489
|
+
* const user = await tx.insert(users, { id: "1", name: "Alice" });
|
|
490
|
+
* await tx.insert(posts, { id: "1", authorId: user.id, title: "Hello" });
|
|
491
|
+
* // If any insert fails, both are rolled back
|
|
492
|
+
* });
|
|
493
|
+
*/
|
|
494
|
+
transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
|
|
495
|
+
}
|
|
@@ -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;
|