@b9g/zen 0.1.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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +1044 -0
- package/chunk-2IEEEMRN.js +38 -0
- package/chunk-56M5Z3A6.js +1346 -0
- package/chunk-QXGEP5PB.js +310 -0
- package/ddl-NAJM37GQ.js +9 -0
- package/package.json +102 -0
- package/src/bun.d.ts +50 -0
- package/src/bun.js +906 -0
- package/src/mysql.d.ts +62 -0
- package/src/mysql.js +573 -0
- package/src/postgres.d.ts +62 -0
- package/src/postgres.js +555 -0
- package/src/sqlite.d.ts +43 -0
- package/src/sqlite.js +447 -0
- package/src/zen.d.ts +14 -0
- package/src/zen.js +2143 -0
package/src/postgres.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
/// <reference types="./postgres.d.ts" />
|
|
2
|
+
import {
|
|
3
|
+
quoteIdent,
|
|
4
|
+
renderDDL
|
|
5
|
+
} from "../chunk-2IEEEMRN.js";
|
|
6
|
+
import {
|
|
7
|
+
generateColumnDDL,
|
|
8
|
+
generateDDL
|
|
9
|
+
} from "../chunk-QXGEP5PB.js";
|
|
10
|
+
import {
|
|
11
|
+
ConstraintPreflightError,
|
|
12
|
+
EnsureError,
|
|
13
|
+
SchemaDriftError,
|
|
14
|
+
getTableMeta,
|
|
15
|
+
resolveSQLBuiltin
|
|
16
|
+
} from "../chunk-56M5Z3A6.js";
|
|
17
|
+
|
|
18
|
+
// src/postgres.ts
|
|
19
|
+
import {
|
|
20
|
+
ConstraintViolationError,
|
|
21
|
+
isSQLBuiltin,
|
|
22
|
+
isSQLIdentifier
|
|
23
|
+
} from "./zen.js";
|
|
24
|
+
import postgres from "postgres";
|
|
25
|
+
var DIALECT = "postgresql";
|
|
26
|
+
function quoteIdent2(name) {
|
|
27
|
+
return quoteIdent(name, DIALECT);
|
|
28
|
+
}
|
|
29
|
+
function buildSQL(strings, values) {
|
|
30
|
+
let sql = strings[0];
|
|
31
|
+
const params = [];
|
|
32
|
+
let paramIndex = 1;
|
|
33
|
+
for (let i = 0; i < values.length; i++) {
|
|
34
|
+
const value = values[i];
|
|
35
|
+
if (isSQLBuiltin(value)) {
|
|
36
|
+
sql += resolveSQLBuiltin(value) + strings[i + 1];
|
|
37
|
+
} else if (isSQLIdentifier(value)) {
|
|
38
|
+
sql += quoteIdent2(value.name) + strings[i + 1];
|
|
39
|
+
} else {
|
|
40
|
+
sql += `$${paramIndex++}` + strings[i + 1];
|
|
41
|
+
params.push(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { sql, params };
|
|
45
|
+
}
|
|
46
|
+
var PostgresDriver = class {
|
|
47
|
+
supportsReturning = true;
|
|
48
|
+
#sql;
|
|
49
|
+
constructor(url, options = {}) {
|
|
50
|
+
this.#sql = postgres(url, {
|
|
51
|
+
max: options.max ?? 10,
|
|
52
|
+
idle_timeout: options.idleTimeout ?? 30,
|
|
53
|
+
connect_timeout: options.connectTimeout ?? 30
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Convert PostgreSQL errors to Zealot errors.
|
|
58
|
+
*/
|
|
59
|
+
#handleError(error) {
|
|
60
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
61
|
+
const code = error.code;
|
|
62
|
+
const message = error.message || String(error);
|
|
63
|
+
const constraint = error.constraint_name || error.constraint;
|
|
64
|
+
const table = error.table_name || error.table;
|
|
65
|
+
const column = error.column_name || error.column;
|
|
66
|
+
let kind = "unknown";
|
|
67
|
+
if (code === "23505")
|
|
68
|
+
kind = "unique";
|
|
69
|
+
else if (code === "23503")
|
|
70
|
+
kind = "foreign_key";
|
|
71
|
+
else if (code === "23514")
|
|
72
|
+
kind = "check";
|
|
73
|
+
else if (code === "23502")
|
|
74
|
+
kind = "not_null";
|
|
75
|
+
if (kind !== "unknown") {
|
|
76
|
+
throw new ConstraintViolationError(
|
|
77
|
+
message,
|
|
78
|
+
{
|
|
79
|
+
kind,
|
|
80
|
+
constraint,
|
|
81
|
+
table,
|
|
82
|
+
column
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
cause: error
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
async all(strings, values) {
|
|
93
|
+
try {
|
|
94
|
+
const { sql, params } = buildSQL(strings, values);
|
|
95
|
+
const result = await this.#sql.unsafe(sql, params);
|
|
96
|
+
return result;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return this.#handleError(error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async get(strings, values) {
|
|
102
|
+
try {
|
|
103
|
+
const { sql, params } = buildSQL(strings, values);
|
|
104
|
+
const result = await this.#sql.unsafe(sql, params);
|
|
105
|
+
return result[0] ?? null;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return this.#handleError(error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async run(strings, values) {
|
|
111
|
+
try {
|
|
112
|
+
const { sql, params } = buildSQL(strings, values);
|
|
113
|
+
const result = await this.#sql.unsafe(sql, params);
|
|
114
|
+
return result.count;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return this.#handleError(error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async val(strings, values) {
|
|
120
|
+
try {
|
|
121
|
+
const { sql, params } = buildSQL(strings, values);
|
|
122
|
+
const result = await this.#sql.unsafe(sql, params);
|
|
123
|
+
const row = result[0];
|
|
124
|
+
if (!row)
|
|
125
|
+
return null;
|
|
126
|
+
const rowValues = Object.values(row);
|
|
127
|
+
return rowValues[0];
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return this.#handleError(error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async close() {
|
|
133
|
+
await this.#sql.end();
|
|
134
|
+
}
|
|
135
|
+
async transaction(fn) {
|
|
136
|
+
const handleError = this.#handleError.bind(this);
|
|
137
|
+
const result = await this.#sql.begin(async (txSql) => {
|
|
138
|
+
const txDriver = {
|
|
139
|
+
supportsReturning: true,
|
|
140
|
+
all: async (strings, values) => {
|
|
141
|
+
try {
|
|
142
|
+
const { sql, params } = buildSQL(strings, values);
|
|
143
|
+
const result2 = await txSql.unsafe(sql, params);
|
|
144
|
+
return result2;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return handleError(error);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
get: async (strings, values) => {
|
|
150
|
+
try {
|
|
151
|
+
const { sql, params } = buildSQL(strings, values);
|
|
152
|
+
const result2 = await txSql.unsafe(sql, params);
|
|
153
|
+
return result2[0] ?? null;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return handleError(error);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
run: async (strings, values) => {
|
|
159
|
+
try {
|
|
160
|
+
const { sql, params } = buildSQL(strings, values);
|
|
161
|
+
const result2 = await txSql.unsafe(sql, params);
|
|
162
|
+
return result2.count;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return handleError(error);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
val: async (strings, values) => {
|
|
168
|
+
try {
|
|
169
|
+
const { sql, params } = buildSQL(strings, values);
|
|
170
|
+
const result2 = await txSql.unsafe(sql, params);
|
|
171
|
+
const row = result2[0];
|
|
172
|
+
if (!row)
|
|
173
|
+
return null;
|
|
174
|
+
const rowValues = Object.values(row);
|
|
175
|
+
return rowValues[0];
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return handleError(error);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
close: async () => {
|
|
181
|
+
},
|
|
182
|
+
transaction: async () => {
|
|
183
|
+
throw new Error("Nested transactions are not supported");
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
return await fn(txDriver);
|
|
187
|
+
});
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
async withMigrationLock(fn) {
|
|
191
|
+
const MIGRATION_LOCK_ID = 1952393421;
|
|
192
|
+
await this.#sql`SELECT pg_advisory_lock(${MIGRATION_LOCK_ID})`;
|
|
193
|
+
try {
|
|
194
|
+
return await fn();
|
|
195
|
+
} finally {
|
|
196
|
+
await this.#sql`SELECT pg_advisory_unlock(${MIGRATION_LOCK_ID})`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// ========================================================================
|
|
200
|
+
// Schema Management Methods
|
|
201
|
+
// ========================================================================
|
|
202
|
+
/**
|
|
203
|
+
* Ensure table exists with the specified structure.
|
|
204
|
+
* Creates table if missing, adds missing columns/indexes.
|
|
205
|
+
* Throws SchemaDriftError if constraints are missing.
|
|
206
|
+
*/
|
|
207
|
+
async ensureTable(table) {
|
|
208
|
+
const tableName = table.name;
|
|
209
|
+
let step = 0;
|
|
210
|
+
let applied = false;
|
|
211
|
+
try {
|
|
212
|
+
const exists = await this.#tableExists(tableName);
|
|
213
|
+
if (!exists) {
|
|
214
|
+
step = 1;
|
|
215
|
+
const ddlTemplate = generateDDL(table, { dialect: DIALECT });
|
|
216
|
+
const ddlSQL = renderDDL(ddlTemplate[0], ddlTemplate.slice(1), DIALECT);
|
|
217
|
+
for (const stmt of ddlSQL.split(";").filter((s) => s.trim())) {
|
|
218
|
+
await this.#sql.unsafe(stmt.trim());
|
|
219
|
+
}
|
|
220
|
+
applied = true;
|
|
221
|
+
} else {
|
|
222
|
+
step = 2;
|
|
223
|
+
const columnsApplied = await this.#ensureMissingColumns(table);
|
|
224
|
+
applied = applied || columnsApplied;
|
|
225
|
+
step = 3;
|
|
226
|
+
const indexesApplied = await this.#ensureMissingIndexes(table);
|
|
227
|
+
applied = applied || indexesApplied;
|
|
228
|
+
step = 4;
|
|
229
|
+
await this.#checkMissingConstraints(table);
|
|
230
|
+
}
|
|
231
|
+
return { applied };
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error instanceof SchemaDriftError || error instanceof ConstraintPreflightError) {
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
throw new EnsureError(
|
|
237
|
+
`Failed to ensure table "${tableName}" exists (step ${step})`,
|
|
238
|
+
{
|
|
239
|
+
operation: "ensureTable",
|
|
240
|
+
table: tableName,
|
|
241
|
+
step
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
cause: error
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Ensure constraints exist on the table.
|
|
251
|
+
* Applies unique and foreign key constraints with preflight checks.
|
|
252
|
+
*/
|
|
253
|
+
async ensureConstraints(table) {
|
|
254
|
+
const tableName = table.name;
|
|
255
|
+
let step = 0;
|
|
256
|
+
let applied = false;
|
|
257
|
+
try {
|
|
258
|
+
step = 1;
|
|
259
|
+
const existingConstraints = await this.#getConstraints(tableName);
|
|
260
|
+
step = 2;
|
|
261
|
+
const uniqueApplied = await this.#ensureUniqueConstraints(
|
|
262
|
+
table,
|
|
263
|
+
existingConstraints
|
|
264
|
+
);
|
|
265
|
+
applied = applied || uniqueApplied;
|
|
266
|
+
step = 3;
|
|
267
|
+
const fkApplied = await this.#ensureForeignKeys(
|
|
268
|
+
table,
|
|
269
|
+
existingConstraints
|
|
270
|
+
);
|
|
271
|
+
applied = applied || fkApplied;
|
|
272
|
+
return { applied };
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error instanceof SchemaDriftError || error instanceof ConstraintPreflightError) {
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
throw new EnsureError(
|
|
278
|
+
`Failed to ensure constraints on table "${tableName}" (step ${step})`,
|
|
279
|
+
{
|
|
280
|
+
operation: "ensureConstraints",
|
|
281
|
+
table: tableName,
|
|
282
|
+
step
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
cause: error
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// ========================================================================
|
|
291
|
+
// Private Helper Methods
|
|
292
|
+
// ========================================================================
|
|
293
|
+
async #tableExists(tableName) {
|
|
294
|
+
const result = await this.#sql`
|
|
295
|
+
SELECT EXISTS (
|
|
296
|
+
SELECT FROM information_schema.tables
|
|
297
|
+
WHERE table_schema = 'public'
|
|
298
|
+
AND table_name = ${tableName}
|
|
299
|
+
) as exists
|
|
300
|
+
`;
|
|
301
|
+
return result[0]?.exists ?? false;
|
|
302
|
+
}
|
|
303
|
+
async #getColumns(tableName) {
|
|
304
|
+
const result = await this.#sql`
|
|
305
|
+
SELECT column_name, data_type, is_nullable
|
|
306
|
+
FROM information_schema.columns
|
|
307
|
+
WHERE table_schema = 'public'
|
|
308
|
+
AND table_name = ${tableName}
|
|
309
|
+
ORDER BY ordinal_position
|
|
310
|
+
`;
|
|
311
|
+
return result.map((row) => ({
|
|
312
|
+
name: row.column_name,
|
|
313
|
+
type: row.data_type,
|
|
314
|
+
notnull: row.is_nullable === "NO"
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
async #getIndexes(tableName) {
|
|
318
|
+
const result = await this.#sql`
|
|
319
|
+
SELECT indexname, indexdef
|
|
320
|
+
FROM pg_indexes
|
|
321
|
+
WHERE schemaname = 'public'
|
|
322
|
+
AND tablename = ${tableName}
|
|
323
|
+
`;
|
|
324
|
+
return result.map((row) => {
|
|
325
|
+
const match = row.indexdef.match(/\((.*?)\)/);
|
|
326
|
+
const columns = match ? match[1].split(",").map((c) => c.trim().replace(/"/g, "")) : [];
|
|
327
|
+
const unique = row.indexdef.includes("UNIQUE INDEX");
|
|
328
|
+
return {
|
|
329
|
+
name: row.indexname,
|
|
330
|
+
columns,
|
|
331
|
+
unique
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async #getConstraints(tableName) {
|
|
336
|
+
const result = await this.#sql`
|
|
337
|
+
SELECT
|
|
338
|
+
tc.constraint_name,
|
|
339
|
+
tc.constraint_type,
|
|
340
|
+
array_agg(kcu.column_name ORDER BY kcu.ordinal_position)::text as column_names,
|
|
341
|
+
ccu.table_name as foreign_table_name,
|
|
342
|
+
array_agg(ccu.column_name ORDER BY kcu.ordinal_position)::text as foreign_column_names
|
|
343
|
+
FROM information_schema.table_constraints tc
|
|
344
|
+
LEFT JOIN information_schema.key_column_usage kcu
|
|
345
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
346
|
+
AND tc.table_schema = kcu.table_schema
|
|
347
|
+
LEFT JOIN information_schema.constraint_column_usage ccu
|
|
348
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
349
|
+
AND tc.table_schema = ccu.table_schema
|
|
350
|
+
WHERE tc.table_schema = 'public'
|
|
351
|
+
AND tc.table_name = ${tableName}
|
|
352
|
+
GROUP BY tc.constraint_name, tc.constraint_type, ccu.table_name
|
|
353
|
+
`;
|
|
354
|
+
return result.map((row) => {
|
|
355
|
+
let type;
|
|
356
|
+
if (row.constraint_type === "UNIQUE")
|
|
357
|
+
type = "unique";
|
|
358
|
+
else if (row.constraint_type === "FOREIGN KEY")
|
|
359
|
+
type = "foreign_key";
|
|
360
|
+
else if (row.constraint_type === "PRIMARY KEY")
|
|
361
|
+
type = "primary_key";
|
|
362
|
+
else
|
|
363
|
+
type = "check";
|
|
364
|
+
const parseArray = (str) => {
|
|
365
|
+
if (!str)
|
|
366
|
+
return [];
|
|
367
|
+
const match = str.match(/^\{(.*)\}$/);
|
|
368
|
+
return match ? match[1].split(",").map((s) => s.trim()) : [];
|
|
369
|
+
};
|
|
370
|
+
return {
|
|
371
|
+
name: row.constraint_name,
|
|
372
|
+
type,
|
|
373
|
+
columns: parseArray(row.column_names),
|
|
374
|
+
referencedTable: row.foreign_table_name ?? void 0,
|
|
375
|
+
referencedColumns: row.foreign_column_names ? parseArray(row.foreign_column_names) : void 0
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
async #ensureMissingColumns(table) {
|
|
380
|
+
const existingCols = await this.#getColumns(table.name);
|
|
381
|
+
const existingColNames = new Set(existingCols.map((c) => c.name));
|
|
382
|
+
const schemaFields = Object.keys(table.schema.shape);
|
|
383
|
+
let applied = false;
|
|
384
|
+
for (const fieldName of schemaFields) {
|
|
385
|
+
if (!existingColNames.has(fieldName)) {
|
|
386
|
+
await this.#addColumn(table, fieldName);
|
|
387
|
+
applied = true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return applied;
|
|
391
|
+
}
|
|
392
|
+
async #addColumn(table, fieldName) {
|
|
393
|
+
const zodType = table.schema.shape[fieldName];
|
|
394
|
+
const fieldMeta = getTableMeta(table).fields[fieldName] || {};
|
|
395
|
+
const colTemplate = generateColumnDDL(
|
|
396
|
+
fieldName,
|
|
397
|
+
zodType,
|
|
398
|
+
fieldMeta,
|
|
399
|
+
DIALECT
|
|
400
|
+
);
|
|
401
|
+
const colSQL = renderDDL(colTemplate[0], colTemplate.slice(1), DIALECT);
|
|
402
|
+
await this.#sql.unsafe(
|
|
403
|
+
`ALTER TABLE ${quoteIdent2(table.name)} ADD COLUMN IF NOT EXISTS ${colSQL}`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
async #ensureMissingIndexes(table) {
|
|
407
|
+
const existingIndexes = await this.#getIndexes(table.name);
|
|
408
|
+
const existingIndexNames = new Set(existingIndexes.map((idx) => idx.name));
|
|
409
|
+
const meta = getTableMeta(table);
|
|
410
|
+
let applied = false;
|
|
411
|
+
for (const fieldName of meta.indexed) {
|
|
412
|
+
const indexName = `idx_${table.name}_${fieldName}`;
|
|
413
|
+
if (!existingIndexNames.has(indexName)) {
|
|
414
|
+
await this.#createIndex(table.name, [fieldName], false);
|
|
415
|
+
applied = true;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
for (const indexCols of table.indexes) {
|
|
419
|
+
const indexName = `idx_${table.name}_${indexCols.join("_")}`;
|
|
420
|
+
if (!existingIndexNames.has(indexName)) {
|
|
421
|
+
await this.#createIndex(table.name, indexCols, false);
|
|
422
|
+
applied = true;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return applied;
|
|
426
|
+
}
|
|
427
|
+
async #createIndex(tableName, columns, unique) {
|
|
428
|
+
const prefix = unique ? "uniq" : "idx";
|
|
429
|
+
const indexName = `${prefix}_${tableName}_${columns.join("_")}`;
|
|
430
|
+
const uniqueClause = unique ? "UNIQUE " : "";
|
|
431
|
+
const columnList = columns.map(quoteIdent2).join(", ");
|
|
432
|
+
const sql = `CREATE ${uniqueClause}INDEX IF NOT EXISTS ${quoteIdent2(indexName)} ON ${quoteIdent2(tableName)} (${columnList})`;
|
|
433
|
+
await this.#sql.unsafe(sql);
|
|
434
|
+
return indexName;
|
|
435
|
+
}
|
|
436
|
+
async #checkMissingConstraints(table) {
|
|
437
|
+
const existingConstraints = await this.#getConstraints(table.name);
|
|
438
|
+
const meta = getTableMeta(table);
|
|
439
|
+
for (const fieldName of Object.keys(meta.fields)) {
|
|
440
|
+
const fieldMeta = meta.fields[fieldName];
|
|
441
|
+
if (fieldMeta.unique) {
|
|
442
|
+
const hasConstraint = existingConstraints.some(
|
|
443
|
+
(c) => c.type === "unique" && c.columns.length === 1 && c.columns[0] === fieldName
|
|
444
|
+
);
|
|
445
|
+
if (!hasConstraint) {
|
|
446
|
+
throw new SchemaDriftError(
|
|
447
|
+
`Table "${table.name}" is missing UNIQUE constraint on column "${fieldName}"`,
|
|
448
|
+
{
|
|
449
|
+
table: table.name,
|
|
450
|
+
drift: `missing unique:${fieldName}`,
|
|
451
|
+
suggestion: `Run ensureConstraints() to apply constraints`
|
|
452
|
+
}
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
for (const ref of meta.references) {
|
|
458
|
+
const hasFK = existingConstraints.some(
|
|
459
|
+
(c) => c.type === "foreign_key" && c.columns.length === 1 && c.columns[0] === ref.fieldName && c.referencedTable === ref.table.name && c.referencedColumns?.[0] === ref.referencedField
|
|
460
|
+
);
|
|
461
|
+
if (!hasFK) {
|
|
462
|
+
throw new SchemaDriftError(
|
|
463
|
+
`Table "${table.name}" is missing FOREIGN KEY constraint on column "${ref.fieldName}"`,
|
|
464
|
+
{
|
|
465
|
+
table: table.name,
|
|
466
|
+
drift: `missing foreign_key:${ref.fieldName}->${ref.table.name}.${ref.referencedField}`,
|
|
467
|
+
suggestion: `Run ensureConstraints() to apply constraints`
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async #ensureUniqueConstraints(table, existingConstraints) {
|
|
474
|
+
const meta = getTableMeta(table);
|
|
475
|
+
let applied = false;
|
|
476
|
+
for (const fieldName of Object.keys(meta.fields)) {
|
|
477
|
+
const fieldMeta = meta.fields[fieldName];
|
|
478
|
+
if (fieldMeta.unique) {
|
|
479
|
+
const hasConstraint = existingConstraints.some(
|
|
480
|
+
(c) => c.type === "unique" && c.columns.includes(fieldName)
|
|
481
|
+
);
|
|
482
|
+
if (!hasConstraint) {
|
|
483
|
+
await this.#preflightUnique(table.name, [fieldName]);
|
|
484
|
+
await this.#createIndex(table.name, [fieldName], true);
|
|
485
|
+
applied = true;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return applied;
|
|
490
|
+
}
|
|
491
|
+
async #ensureForeignKeys(table, existingConstraints) {
|
|
492
|
+
const meta = getTableMeta(table);
|
|
493
|
+
let applied = false;
|
|
494
|
+
for (const ref of meta.references) {
|
|
495
|
+
const hasFK = existingConstraints.some(
|
|
496
|
+
(c) => c.type === "foreign_key" && c.columns.length === 1 && c.columns[0] === ref.fieldName && c.referencedTable === ref.table.name && c.referencedColumns?.[0] === ref.referencedField
|
|
497
|
+
);
|
|
498
|
+
if (!hasFK) {
|
|
499
|
+
await this.#preflightForeignKey(
|
|
500
|
+
table.name,
|
|
501
|
+
ref.fieldName,
|
|
502
|
+
ref.table.name,
|
|
503
|
+
ref.referencedField
|
|
504
|
+
);
|
|
505
|
+
const constraintName = `fk_${table.name}_${ref.fieldName}`;
|
|
506
|
+
const onDelete = ref.onDelete ? ` ON DELETE ${ref.onDelete.toUpperCase()}` : "";
|
|
507
|
+
await this.#sql.unsafe(
|
|
508
|
+
`ALTER TABLE ${quoteIdent2(table.name)} ADD CONSTRAINT ${quoteIdent2(constraintName)} FOREIGN KEY (${quoteIdent2(ref.fieldName)}) REFERENCES ${quoteIdent2(ref.table.name)} (${quoteIdent2(ref.referencedField)})${onDelete}`
|
|
509
|
+
);
|
|
510
|
+
applied = true;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return applied;
|
|
514
|
+
}
|
|
515
|
+
async #preflightUnique(tableName, columns) {
|
|
516
|
+
const columnList = columns.map(quoteIdent2).join(", ");
|
|
517
|
+
const result = await this.#sql.unsafe(
|
|
518
|
+
`SELECT COUNT(*) as count FROM ${quoteIdent2(tableName)} GROUP BY ${columnList} HAVING COUNT(*) > 1`
|
|
519
|
+
);
|
|
520
|
+
const violationCount = result.length;
|
|
521
|
+
if (violationCount > 0) {
|
|
522
|
+
const diagQuery = `SELECT ${columnList}, COUNT(*) FROM ${quoteIdent2(tableName)} GROUP BY ${columnList} HAVING COUNT(*) > 1`;
|
|
523
|
+
throw new ConstraintPreflightError(
|
|
524
|
+
`Cannot add UNIQUE constraint on "${tableName}"(${columns.join(", ")}): duplicate values exist`,
|
|
525
|
+
{
|
|
526
|
+
table: tableName,
|
|
527
|
+
constraint: `unique:${columns.join(",")}`,
|
|
528
|
+
violationCount,
|
|
529
|
+
query: diagQuery
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async #preflightForeignKey(tableName, column, refTable, refColumn) {
|
|
535
|
+
const result = await this.#sql.unsafe(
|
|
536
|
+
`SELECT COUNT(*) as count FROM ${quoteIdent2(tableName)} t WHERE t.${quoteIdent2(column)} IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${quoteIdent2(refTable)} r WHERE r.${quoteIdent2(refColumn)} = t.${quoteIdent2(column)})`
|
|
537
|
+
);
|
|
538
|
+
const violationCount = parseInt(result[0]?.count ?? "0", 10);
|
|
539
|
+
if (violationCount > 0) {
|
|
540
|
+
const diagQuery = `SELECT t.${quoteIdent2(column)} FROM ${quoteIdent2(tableName)} t WHERE t.${quoteIdent2(column)} IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${quoteIdent2(refTable)} r WHERE r.${quoteIdent2(refColumn)} = t.${quoteIdent2(column)})`;
|
|
541
|
+
throw new ConstraintPreflightError(
|
|
542
|
+
`Cannot add FOREIGN KEY constraint on "${tableName}"(${column}): ${violationCount} orphaned rows exist`,
|
|
543
|
+
{
|
|
544
|
+
table: tableName,
|
|
545
|
+
constraint: `foreign_key:${column}->${refTable}.${refColumn}`,
|
|
546
|
+
violationCount,
|
|
547
|
+
query: diagQuery
|
|
548
|
+
}
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
export {
|
|
554
|
+
PostgresDriver as default
|
|
555
|
+
};
|
package/src/sqlite.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* better-sqlite3 adapter for @b9g/zen
|
|
3
|
+
*
|
|
4
|
+
* Provides a Driver implementation for better-sqlite3 (Node.js).
|
|
5
|
+
* The connection is persistent - call close() when done.
|
|
6
|
+
*
|
|
7
|
+
* Requires: better-sqlite3
|
|
8
|
+
*/
|
|
9
|
+
import type { Driver, EnsureResult } from "./zen.js";
|
|
10
|
+
import type { Table } from "./impl/table.js";
|
|
11
|
+
/**
|
|
12
|
+
* SQLite driver using better-sqlite3.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* import SQLiteDriver from "@b9g/zen/sqlite";
|
|
16
|
+
* import {Database} from "@b9g/zen";
|
|
17
|
+
*
|
|
18
|
+
* const driver = new SQLiteDriver("file:app.db");
|
|
19
|
+
* const db = new Database(driver);
|
|
20
|
+
*
|
|
21
|
+
* db.addEventListener("upgradeneeded", (e) => {
|
|
22
|
+
* e.waitUntil(runMigrations(e));
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* await db.open(1);
|
|
26
|
+
*
|
|
27
|
+
* // When done:
|
|
28
|
+
* await driver.close();
|
|
29
|
+
*/
|
|
30
|
+
export default class SQLiteDriver implements Driver {
|
|
31
|
+
#private;
|
|
32
|
+
readonly supportsReturning = true;
|
|
33
|
+
constructor(url: string);
|
|
34
|
+
all<T>(strings: TemplateStringsArray, values: unknown[]): Promise<T[]>;
|
|
35
|
+
get<T>(strings: TemplateStringsArray, values: unknown[]): Promise<T | null>;
|
|
36
|
+
run(strings: TemplateStringsArray, values: unknown[]): Promise<number>;
|
|
37
|
+
val<T>(strings: TemplateStringsArray, values: unknown[]): Promise<T | null>;
|
|
38
|
+
close(): Promise<void>;
|
|
39
|
+
transaction<T>(fn: (txDriver: Driver) => Promise<T>): Promise<T>;
|
|
40
|
+
withMigrationLock<T>(fn: () => Promise<T>): Promise<T>;
|
|
41
|
+
ensureTable<T extends Table<any>>(table: T): Promise<EnsureResult>;
|
|
42
|
+
ensureConstraints<T extends Table<any>>(table: T): Promise<EnsureResult>;
|
|
43
|
+
}
|