@vertz/db 0.2.12 → 0.2.14
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/README.md +65 -4
- package/dist/d1/index.d.ts +3 -11
- package/dist/d1/index.js +1 -1
- package/dist/index.d.ts +73 -72
- package/dist/index.js +188 -663
- package/dist/internals.d.ts +41 -12
- package/dist/internals.js +3 -1
- package/dist/shared/chunk-1x9z0p5e.js +977 -0
- package/dist/shared/chunk-8v70x3hq.js +105 -0
- package/dist/shared/{chunk-pnk6yzjv.js → chunk-ndxe1h28.js} +1 -1
- package/dist/shared/{chunk-sfmyxz6r.js → chunk-pkv8w501.js} +3 -2
- package/dist/shared/{chunk-0e1vy9qd.js → chunk-pxjcpnpx.js} +4 -85
- package/dist/sql/index.js +2 -2
- package/dist/sqlite/index.d.ts +3 -11
- package/dist/sqlite/index.js +1 -1
- package/package.json +3 -3
- package/dist/shared/chunk-agyds4jw.js +0 -302
- package/dist/shared/chunk-v2qm94qp.js +0 -23
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
import {
|
|
2
|
+
camelToSnake,
|
|
3
|
+
defaultPostgresDialect,
|
|
4
|
+
defaultSqliteDialect,
|
|
5
|
+
snakeToCamel
|
|
6
|
+
} from "./chunk-8v70x3hq.js";
|
|
7
|
+
import {
|
|
8
|
+
sha256Hex
|
|
9
|
+
} from "./chunk-ssga2xea.js";
|
|
10
|
+
import {
|
|
11
|
+
__require
|
|
12
|
+
} from "./chunk-j4kwq1gh.js";
|
|
13
|
+
|
|
14
|
+
// src/migration/runner.ts
|
|
15
|
+
import {
|
|
16
|
+
createMigrationQueryError,
|
|
17
|
+
err,
|
|
18
|
+
ok
|
|
19
|
+
} from "@vertz/errors";
|
|
20
|
+
var HISTORY_TABLE = "_vertz_migrations";
|
|
21
|
+
function buildCreateHistorySql(dialect) {
|
|
22
|
+
if (dialect.name === "sqlite") {
|
|
23
|
+
return `
|
|
24
|
+
CREATE TABLE IF NOT EXISTS "${HISTORY_TABLE}" (
|
|
25
|
+
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
"name" TEXT NOT NULL UNIQUE,
|
|
27
|
+
"checksum" TEXT NOT NULL,
|
|
28
|
+
"applied_at" TEXT NOT NULL DEFAULT (datetime('now'))
|
|
29
|
+
);
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
32
|
+
return `
|
|
33
|
+
CREATE TABLE IF NOT EXISTS "${HISTORY_TABLE}" (
|
|
34
|
+
"id" serial PRIMARY KEY,
|
|
35
|
+
"name" text NOT NULL UNIQUE,
|
|
36
|
+
"checksum" text NOT NULL,
|
|
37
|
+
"applied_at" timestamp with time zone NOT NULL DEFAULT now()
|
|
38
|
+
);
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
async function computeChecksum(sql) {
|
|
42
|
+
return sha256Hex(sql);
|
|
43
|
+
}
|
|
44
|
+
function parseMigrationName(filename) {
|
|
45
|
+
const match = filename.match(/^(\d+)_(.+)\.sql$/);
|
|
46
|
+
if (!match?.[1] || !match[2])
|
|
47
|
+
return null;
|
|
48
|
+
return {
|
|
49
|
+
timestamp: Number.parseInt(match[1], 10),
|
|
50
|
+
name: filename
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function createMigrationRunner(options) {
|
|
54
|
+
const dialect = options?.dialect ?? defaultPostgresDialect;
|
|
55
|
+
const createHistorySql = buildCreateHistorySql(dialect);
|
|
56
|
+
return {
|
|
57
|
+
async createHistoryTable(queryFn) {
|
|
58
|
+
try {
|
|
59
|
+
await queryFn(createHistorySql, []);
|
|
60
|
+
return ok(undefined);
|
|
61
|
+
} catch (cause) {
|
|
62
|
+
return err(createMigrationQueryError("Failed to create migration history table", {
|
|
63
|
+
sql: createHistorySql,
|
|
64
|
+
cause
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
async apply(queryFn, sql, name, options2) {
|
|
69
|
+
const checksum = await computeChecksum(sql);
|
|
70
|
+
const recordSql = `INSERT INTO "${HISTORY_TABLE}" ("name", "checksum") VALUES (${dialect.param(1)}, ${dialect.param(2)})`;
|
|
71
|
+
const statements = [sql, recordSql];
|
|
72
|
+
if (options2?.dryRun) {
|
|
73
|
+
return ok({
|
|
74
|
+
name,
|
|
75
|
+
sql,
|
|
76
|
+
checksum,
|
|
77
|
+
dryRun: true,
|
|
78
|
+
statements
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await queryFn(sql, []);
|
|
83
|
+
await queryFn(recordSql, [name, checksum]);
|
|
84
|
+
return ok({
|
|
85
|
+
name,
|
|
86
|
+
sql,
|
|
87
|
+
checksum,
|
|
88
|
+
dryRun: false,
|
|
89
|
+
statements
|
|
90
|
+
});
|
|
91
|
+
} catch (cause) {
|
|
92
|
+
return err(createMigrationQueryError(`Failed to apply migration: ${name}`, {
|
|
93
|
+
sql,
|
|
94
|
+
cause
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
async getApplied(queryFn) {
|
|
99
|
+
try {
|
|
100
|
+
const result = await queryFn(`SELECT "name", "checksum", "applied_at" FROM "${HISTORY_TABLE}" ORDER BY "id" ASC`, []);
|
|
101
|
+
return ok(result.rows.map((row) => ({
|
|
102
|
+
name: row.name,
|
|
103
|
+
checksum: row.checksum,
|
|
104
|
+
appliedAt: new Date(row.applied_at)
|
|
105
|
+
})));
|
|
106
|
+
} catch (cause) {
|
|
107
|
+
return err(createMigrationQueryError("Failed to retrieve applied migrations", {
|
|
108
|
+
cause
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
getPending(files, applied) {
|
|
113
|
+
const appliedNames = new Set(applied.map((a) => a.name));
|
|
114
|
+
return files.filter((f) => !appliedNames.has(f.name)).sort((a, b) => a.timestamp - b.timestamp);
|
|
115
|
+
},
|
|
116
|
+
async detectDrift(files, applied) {
|
|
117
|
+
const drifted = [];
|
|
118
|
+
const appliedMap = new Map(applied.map((a) => [a.name, a.checksum]));
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
const appliedChecksum = appliedMap.get(file.name);
|
|
121
|
+
if (appliedChecksum && appliedChecksum !== await computeChecksum(file.sql)) {
|
|
122
|
+
drifted.push(file.name);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return drifted;
|
|
126
|
+
},
|
|
127
|
+
detectOutOfOrder(files, applied) {
|
|
128
|
+
if (applied.length === 0)
|
|
129
|
+
return [];
|
|
130
|
+
const appliedNames = new Set(applied.map((a) => a.name));
|
|
131
|
+
const lastApplied = applied[applied.length - 1];
|
|
132
|
+
if (!lastApplied)
|
|
133
|
+
return [];
|
|
134
|
+
const lastAppliedFile = files.find((f) => f.name === lastApplied.name);
|
|
135
|
+
if (!lastAppliedFile)
|
|
136
|
+
return [];
|
|
137
|
+
return files.filter((f) => !appliedNames.has(f.name) && f.timestamp < lastAppliedFile.timestamp).map((f) => f.name);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/migration/validate-indexes.ts
|
|
143
|
+
var POSTGRES_ONLY_INDEX_TYPES = new Set(["hash", "gin", "gist", "brin"]);
|
|
144
|
+
function validateIndexes(tables, dialect) {
|
|
145
|
+
const warnings = [];
|
|
146
|
+
for (const [tableName, table] of Object.entries(tables)) {
|
|
147
|
+
for (const idx of table.indexes) {
|
|
148
|
+
if (idx.type && dialect === "sqlite" && POSTGRES_ONLY_INDEX_TYPES.has(idx.type)) {
|
|
149
|
+
warnings.push(`Index on "${tableName}" (${idx.columns.join(", ")}) uses type "${idx.type}" which is not supported on ${dialect}. The index type will be ignored.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return warnings;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/migration/auto-migrate.ts
|
|
157
|
+
import { isErr } from "@vertz/errors";
|
|
158
|
+
|
|
159
|
+
// src/migration/differ.ts
|
|
160
|
+
function columnSimilarity(a, b) {
|
|
161
|
+
let score = 0;
|
|
162
|
+
let total = 0;
|
|
163
|
+
total += 3;
|
|
164
|
+
if (a.type === b.type)
|
|
165
|
+
score += 3;
|
|
166
|
+
total += 1;
|
|
167
|
+
if (a.nullable === b.nullable)
|
|
168
|
+
score += 1;
|
|
169
|
+
total += 1;
|
|
170
|
+
if (a.primary === b.primary)
|
|
171
|
+
score += 1;
|
|
172
|
+
total += 1;
|
|
173
|
+
if (a.unique === b.unique)
|
|
174
|
+
score += 1;
|
|
175
|
+
return score / total;
|
|
176
|
+
}
|
|
177
|
+
function indexKey(idx) {
|
|
178
|
+
return JSON.stringify([idx.columns, idx.type ?? null, idx.where ?? null, idx.unique ?? false]);
|
|
179
|
+
}
|
|
180
|
+
function computeDiff(before, after) {
|
|
181
|
+
const changes = [];
|
|
182
|
+
for (const tableName of Object.keys(after.tables)) {
|
|
183
|
+
if (!(tableName in before.tables)) {
|
|
184
|
+
changes.push({ type: "table_added", table: tableName });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const tableName of Object.keys(before.tables)) {
|
|
188
|
+
if (!(tableName in after.tables)) {
|
|
189
|
+
changes.push({ type: "table_removed", table: tableName });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const tableName of Object.keys(after.tables)) {
|
|
193
|
+
if (!(tableName in before.tables))
|
|
194
|
+
continue;
|
|
195
|
+
const beforeTable = before.tables[tableName];
|
|
196
|
+
const afterTable = after.tables[tableName];
|
|
197
|
+
if (!beforeTable || !afterTable)
|
|
198
|
+
continue;
|
|
199
|
+
const removedCols = Object.keys(beforeTable.columns).filter((c) => !(c in afterTable.columns));
|
|
200
|
+
const addedCols = Object.keys(afterTable.columns).filter((c) => !(c in beforeTable.columns));
|
|
201
|
+
const renames = [];
|
|
202
|
+
const matchedRemoved = new Set;
|
|
203
|
+
const matchedAdded = new Set;
|
|
204
|
+
for (const removed of removedCols) {
|
|
205
|
+
const removedSnap = beforeTable.columns[removed];
|
|
206
|
+
if (!removedSnap)
|
|
207
|
+
continue;
|
|
208
|
+
let bestMatch = null;
|
|
209
|
+
let bestScore = 0;
|
|
210
|
+
for (const added of addedCols) {
|
|
211
|
+
if (matchedAdded.has(added))
|
|
212
|
+
continue;
|
|
213
|
+
const addedSnap = afterTable.columns[added];
|
|
214
|
+
if (!addedSnap)
|
|
215
|
+
continue;
|
|
216
|
+
const score = columnSimilarity(removedSnap, addedSnap);
|
|
217
|
+
if (score > bestScore) {
|
|
218
|
+
bestScore = score;
|
|
219
|
+
bestMatch = added;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (bestMatch && bestScore >= 0.7) {
|
|
223
|
+
renames.push({ oldCol: removed, newCol: bestMatch, confidence: bestScore });
|
|
224
|
+
matchedRemoved.add(removed);
|
|
225
|
+
matchedAdded.add(bestMatch);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const rename of renames) {
|
|
229
|
+
changes.push({
|
|
230
|
+
type: "column_renamed",
|
|
231
|
+
table: tableName,
|
|
232
|
+
oldColumn: rename.oldCol,
|
|
233
|
+
newColumn: rename.newCol,
|
|
234
|
+
confidence: rename.confidence
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
for (const colName of addedCols) {
|
|
238
|
+
if (!matchedAdded.has(colName)) {
|
|
239
|
+
changes.push({ type: "column_added", table: tableName, column: colName });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const colName of removedCols) {
|
|
243
|
+
if (!matchedRemoved.has(colName)) {
|
|
244
|
+
changes.push({ type: "column_removed", table: tableName, column: colName });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const colName of Object.keys(afterTable.columns)) {
|
|
248
|
+
if (!(colName in beforeTable.columns))
|
|
249
|
+
continue;
|
|
250
|
+
const beforeCol = beforeTable.columns[colName];
|
|
251
|
+
const afterCol = afterTable.columns[colName];
|
|
252
|
+
if (!beforeCol || !afterCol)
|
|
253
|
+
continue;
|
|
254
|
+
if (beforeCol.type !== afterCol.type || beforeCol.nullable !== afterCol.nullable || beforeCol.default !== afterCol.default) {
|
|
255
|
+
const change = {
|
|
256
|
+
type: "column_altered",
|
|
257
|
+
table: tableName,
|
|
258
|
+
column: colName
|
|
259
|
+
};
|
|
260
|
+
if (beforeCol.type !== afterCol.type) {
|
|
261
|
+
change.oldType = beforeCol.type;
|
|
262
|
+
change.newType = afterCol.type;
|
|
263
|
+
}
|
|
264
|
+
if (beforeCol.nullable !== afterCol.nullable) {
|
|
265
|
+
change.oldNullable = beforeCol.nullable;
|
|
266
|
+
change.newNullable = afterCol.nullable;
|
|
267
|
+
}
|
|
268
|
+
if (beforeCol.default !== afterCol.default) {
|
|
269
|
+
change.oldDefault = beforeCol.default;
|
|
270
|
+
change.newDefault = afterCol.default;
|
|
271
|
+
}
|
|
272
|
+
changes.push(change);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const beforeIndexKeys = new Set(beforeTable.indexes.map((i) => indexKey(i)));
|
|
276
|
+
const afterIndexKeys = new Set(afterTable.indexes.map((i) => indexKey(i)));
|
|
277
|
+
for (const idx of afterTable.indexes) {
|
|
278
|
+
const key = indexKey(idx);
|
|
279
|
+
if (!beforeIndexKeys.has(key)) {
|
|
280
|
+
const change = {
|
|
281
|
+
type: "index_added",
|
|
282
|
+
table: tableName,
|
|
283
|
+
columns: [...idx.columns]
|
|
284
|
+
};
|
|
285
|
+
if (idx.name)
|
|
286
|
+
change.indexName = idx.name;
|
|
287
|
+
if (idx.type)
|
|
288
|
+
change.indexType = idx.type;
|
|
289
|
+
if (idx.where)
|
|
290
|
+
change.indexWhere = idx.where;
|
|
291
|
+
if (idx.unique)
|
|
292
|
+
change.indexUnique = idx.unique;
|
|
293
|
+
changes.push(change);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const idx of beforeTable.indexes) {
|
|
297
|
+
const key = indexKey(idx);
|
|
298
|
+
if (!afterIndexKeys.has(key)) {
|
|
299
|
+
const change = {
|
|
300
|
+
type: "index_removed",
|
|
301
|
+
table: tableName,
|
|
302
|
+
columns: [...idx.columns]
|
|
303
|
+
};
|
|
304
|
+
if (idx.name)
|
|
305
|
+
change.indexName = idx.name;
|
|
306
|
+
if (idx.type)
|
|
307
|
+
change.indexType = idx.type;
|
|
308
|
+
if (idx.where)
|
|
309
|
+
change.indexWhere = idx.where;
|
|
310
|
+
if (idx.unique)
|
|
311
|
+
change.indexUnique = idx.unique;
|
|
312
|
+
changes.push(change);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const enumName of Object.keys(after.enums)) {
|
|
317
|
+
if (!(enumName in before.enums)) {
|
|
318
|
+
changes.push({ type: "enum_added", enumName });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const enumName of Object.keys(before.enums)) {
|
|
322
|
+
if (!(enumName in after.enums)) {
|
|
323
|
+
changes.push({ type: "enum_removed", enumName });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
for (const enumName of Object.keys(after.enums)) {
|
|
327
|
+
if (!(enumName in before.enums))
|
|
328
|
+
continue;
|
|
329
|
+
const beforeVals = before.enums[enumName];
|
|
330
|
+
const afterVals = after.enums[enumName];
|
|
331
|
+
if (!beforeVals || !afterVals)
|
|
332
|
+
continue;
|
|
333
|
+
const beforeSet = new Set(beforeVals);
|
|
334
|
+
const afterSet = new Set(afterVals);
|
|
335
|
+
const addedValues = afterVals.filter((v) => !beforeSet.has(v));
|
|
336
|
+
const removedValues = beforeVals.filter((v) => !afterSet.has(v));
|
|
337
|
+
if (addedValues.length > 0 || removedValues.length > 0) {
|
|
338
|
+
changes.push({ type: "enum_altered", enumName, addedValues, removedValues });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return { changes };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/migration/sql-generator.ts
|
|
345
|
+
function escapeSqlString(value) {
|
|
346
|
+
return value.replace(/'/g, "''");
|
|
347
|
+
}
|
|
348
|
+
function isEnumType(col, enums) {
|
|
349
|
+
const typeLower = col.type.toLowerCase();
|
|
350
|
+
if (enums) {
|
|
351
|
+
for (const enumName of Object.keys(enums)) {
|
|
352
|
+
if (typeLower === enumName.toLowerCase() || typeLower === enumName) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
function getEnumValues(col, enums) {
|
|
360
|
+
if (!enums)
|
|
361
|
+
return;
|
|
362
|
+
for (const [enumName, values] of Object.entries(enums)) {
|
|
363
|
+
if (col.type.toLowerCase() === enumName.toLowerCase() || col.type === enumName) {
|
|
364
|
+
return values;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
function columnDef(name, col, dialect, enums) {
|
|
370
|
+
const snakeName = camelToSnake(name);
|
|
371
|
+
const isEnum = isEnumType(col, enums);
|
|
372
|
+
let sqlType;
|
|
373
|
+
let checkConstraint;
|
|
374
|
+
if (isEnum && dialect.name === "sqlite") {
|
|
375
|
+
sqlType = dialect.mapColumnType("text");
|
|
376
|
+
const enumValues = getEnumValues(col, enums);
|
|
377
|
+
if (enumValues && enumValues.length > 0) {
|
|
378
|
+
const escapedValues = enumValues.map((v) => `'${escapeSqlString(v)}'`).join(", ");
|
|
379
|
+
checkConstraint = `CHECK("${snakeName}" IN (${escapedValues}))`;
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
if (dialect.name === "postgres" && col.type === col.type.toLowerCase()) {
|
|
383
|
+
sqlType = col.type;
|
|
384
|
+
} else {
|
|
385
|
+
const normalizedType = normalizeColumnType(col.type);
|
|
386
|
+
sqlType = dialect.mapColumnType(normalizedType);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const parts = [`"${snakeName}" ${sqlType}`];
|
|
390
|
+
if (checkConstraint) {
|
|
391
|
+
parts.push(checkConstraint);
|
|
392
|
+
}
|
|
393
|
+
if (!col.nullable) {
|
|
394
|
+
parts.push("NOT NULL");
|
|
395
|
+
}
|
|
396
|
+
if (col.unique) {
|
|
397
|
+
parts.push("UNIQUE");
|
|
398
|
+
}
|
|
399
|
+
if (col.default !== undefined) {
|
|
400
|
+
parts.push(`DEFAULT ${col.default}`);
|
|
401
|
+
}
|
|
402
|
+
return parts.join(" ");
|
|
403
|
+
}
|
|
404
|
+
function normalizeColumnType(type) {
|
|
405
|
+
const typeUpper = type.toUpperCase();
|
|
406
|
+
const typeMap = {
|
|
407
|
+
UUID: "uuid",
|
|
408
|
+
TEXT: "text",
|
|
409
|
+
INTEGER: "integer",
|
|
410
|
+
SERIAL: "serial",
|
|
411
|
+
BOOLEAN: "boolean",
|
|
412
|
+
TIMESTAMPTZ: "timestamp",
|
|
413
|
+
TIMESTAMP: "timestamp",
|
|
414
|
+
"DOUBLE PRECISION": "float",
|
|
415
|
+
JSONB: "json",
|
|
416
|
+
JSON: "json",
|
|
417
|
+
NUMERIC: "decimal",
|
|
418
|
+
REAL: "float",
|
|
419
|
+
VARCHAR: "varchar"
|
|
420
|
+
};
|
|
421
|
+
return typeMap[typeUpper] || type.toLowerCase();
|
|
422
|
+
}
|
|
423
|
+
function generateMigrationSql(changes, ctx, dialect = defaultPostgresDialect) {
|
|
424
|
+
const statements = [];
|
|
425
|
+
const tables = ctx?.tables;
|
|
426
|
+
const enums = ctx?.enums;
|
|
427
|
+
if (tables) {
|
|
428
|
+
const warnings = validateIndexes(tables, dialect.name);
|
|
429
|
+
for (const warning of warnings) {
|
|
430
|
+
console.warn(warning);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
for (const change of changes) {
|
|
434
|
+
switch (change.type) {
|
|
435
|
+
case "enum_added": {
|
|
436
|
+
if (!change.enumName)
|
|
437
|
+
break;
|
|
438
|
+
if (dialect.name === "postgres") {
|
|
439
|
+
const values = enums?.[change.enumName];
|
|
440
|
+
if (!values || values.length === 0)
|
|
441
|
+
break;
|
|
442
|
+
const enumSnakeName = camelToSnake(change.enumName);
|
|
443
|
+
const valuesStr = values.map((v) => `'${escapeSqlString(v)}'`).join(", ");
|
|
444
|
+
statements.push(`CREATE TYPE "${enumSnakeName}" AS ENUM (${valuesStr});`);
|
|
445
|
+
}
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
case "table_added": {
|
|
449
|
+
if (!change.table)
|
|
450
|
+
break;
|
|
451
|
+
const table = tables?.[change.table];
|
|
452
|
+
if (!table)
|
|
453
|
+
break;
|
|
454
|
+
const tableName = camelToSnake(change.table);
|
|
455
|
+
const cols = [];
|
|
456
|
+
const primaryKeys = [];
|
|
457
|
+
if (dialect.name === "postgres" && enums) {
|
|
458
|
+
for (const [, col] of Object.entries(table.columns)) {
|
|
459
|
+
if (isEnumType(col, enums)) {
|
|
460
|
+
const enumValues = getEnumValues(col, enums);
|
|
461
|
+
if (enumValues && enumValues.length > 0) {
|
|
462
|
+
const enumSnakeName = camelToSnake(col.type);
|
|
463
|
+
const alreadyEmitted = statements.some((s) => s.includes(`CREATE TYPE "${enumSnakeName}"`));
|
|
464
|
+
if (!alreadyEmitted) {
|
|
465
|
+
const valuesStr = enumValues.map((v) => `'${escapeSqlString(v)}'`).join(", ");
|
|
466
|
+
statements.push(`CREATE TYPE "${enumSnakeName}" AS ENUM (${valuesStr});`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
for (const [colName, col] of Object.entries(table.columns)) {
|
|
473
|
+
cols.push(` ${columnDef(colName, col, dialect, enums)}`);
|
|
474
|
+
if (col.primary) {
|
|
475
|
+
primaryKeys.push(`"${camelToSnake(colName)}"`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (primaryKeys.length > 0) {
|
|
479
|
+
cols.push(` PRIMARY KEY (${primaryKeys.join(", ")})`);
|
|
480
|
+
}
|
|
481
|
+
for (const fk of table.foreignKeys) {
|
|
482
|
+
const fkCol = camelToSnake(fk.column);
|
|
483
|
+
const fkTarget = camelToSnake(fk.targetTable);
|
|
484
|
+
const fkTargetCol = camelToSnake(fk.targetColumn);
|
|
485
|
+
cols.push(` FOREIGN KEY ("${fkCol}") REFERENCES "${fkTarget}" ("${fkTargetCol}")`);
|
|
486
|
+
}
|
|
487
|
+
statements.push(`CREATE TABLE "${tableName}" (
|
|
488
|
+
${cols.join(`,
|
|
489
|
+
`)}
|
|
490
|
+
);`);
|
|
491
|
+
for (const idx of table.indexes) {
|
|
492
|
+
const idxCols = idx.columns.map((c) => `"${camelToSnake(c)}"`).join(", ");
|
|
493
|
+
const idxName = idx.name ?? `idx_${tableName}_${idx.columns.map((c) => camelToSnake(c)).join("_")}`;
|
|
494
|
+
const unique = idx.unique ? "UNIQUE " : "";
|
|
495
|
+
const using = idx.type && dialect.name === "postgres" ? ` USING ${idx.type}` : "";
|
|
496
|
+
const where = idx.where ? ` WHERE ${idx.where}` : "";
|
|
497
|
+
statements.push(`CREATE ${unique}INDEX "${idxName}" ON "${tableName}"${using} (${idxCols})${where};`);
|
|
498
|
+
}
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
case "table_removed": {
|
|
502
|
+
if (!change.table)
|
|
503
|
+
break;
|
|
504
|
+
statements.push(`DROP TABLE "${camelToSnake(change.table)}";`);
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case "column_added": {
|
|
508
|
+
if (!change.table || !change.column)
|
|
509
|
+
break;
|
|
510
|
+
const col = tables?.[change.table]?.columns[change.column];
|
|
511
|
+
if (!col)
|
|
512
|
+
break;
|
|
513
|
+
statements.push(`ALTER TABLE "${camelToSnake(change.table)}" ADD COLUMN ${columnDef(change.column, col, dialect, enums)};`);
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
case "column_removed": {
|
|
517
|
+
if (!change.table || !change.column)
|
|
518
|
+
break;
|
|
519
|
+
statements.push(`ALTER TABLE "${camelToSnake(change.table)}" DROP COLUMN "${camelToSnake(change.column)}";`);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
case "column_altered": {
|
|
523
|
+
if (!change.table || !change.column)
|
|
524
|
+
break;
|
|
525
|
+
const snakeTable = camelToSnake(change.table);
|
|
526
|
+
const snakeCol = camelToSnake(change.column);
|
|
527
|
+
if (change.newType !== undefined) {
|
|
528
|
+
statements.push(`ALTER TABLE "${snakeTable}" ALTER COLUMN "${snakeCol}" TYPE ${change.newType};`);
|
|
529
|
+
}
|
|
530
|
+
if (change.newNullable !== undefined) {
|
|
531
|
+
if (change.newNullable) {
|
|
532
|
+
statements.push(`ALTER TABLE "${snakeTable}" ALTER COLUMN "${snakeCol}" DROP NOT NULL;`);
|
|
533
|
+
} else {
|
|
534
|
+
statements.push(`ALTER TABLE "${snakeTable}" ALTER COLUMN "${snakeCol}" SET NOT NULL;`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (change.newDefault !== undefined) {
|
|
538
|
+
if (change.newDefault) {
|
|
539
|
+
statements.push(`ALTER TABLE "${snakeTable}" ALTER COLUMN "${snakeCol}" SET DEFAULT ${change.newDefault};`);
|
|
540
|
+
} else {
|
|
541
|
+
statements.push(`ALTER TABLE "${snakeTable}" ALTER COLUMN "${snakeCol}" DROP DEFAULT;`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
case "column_renamed": {
|
|
547
|
+
if (!change.table || !change.oldColumn || !change.newColumn)
|
|
548
|
+
break;
|
|
549
|
+
statements.push(`ALTER TABLE "${camelToSnake(change.table)}" RENAME COLUMN "${camelToSnake(change.oldColumn)}" TO "${camelToSnake(change.newColumn)}";`);
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
case "index_added": {
|
|
553
|
+
if (!change.table || !change.columns)
|
|
554
|
+
break;
|
|
555
|
+
const snakeTable = camelToSnake(change.table);
|
|
556
|
+
const idxCols = change.columns.map((c) => `"${camelToSnake(c)}"`).join(", ");
|
|
557
|
+
const idxName = change.indexName ?? `idx_${snakeTable}_${change.columns.map((c) => camelToSnake(c)).join("_")}`;
|
|
558
|
+
const unique = change.indexUnique ? "UNIQUE " : "";
|
|
559
|
+
const using = change.indexType && dialect.name === "postgres" ? ` USING ${change.indexType}` : "";
|
|
560
|
+
const where = change.indexWhere ? ` WHERE ${change.indexWhere}` : "";
|
|
561
|
+
statements.push(`CREATE ${unique}INDEX "${idxName}" ON "${snakeTable}"${using} (${idxCols})${where};`);
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case "index_removed": {
|
|
565
|
+
if (!change.table || !change.columns)
|
|
566
|
+
break;
|
|
567
|
+
const snakeTable = camelToSnake(change.table);
|
|
568
|
+
const idxName = change.indexName ?? `idx_${snakeTable}_${change.columns.map((c) => camelToSnake(c)).join("_")}`;
|
|
569
|
+
statements.push(`DROP INDEX "${idxName}";`);
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
case "enum_removed": {
|
|
573
|
+
if (!change.enumName)
|
|
574
|
+
break;
|
|
575
|
+
if (dialect.name === "postgres") {
|
|
576
|
+
statements.push(`DROP TYPE "${camelToSnake(change.enumName)}";`);
|
|
577
|
+
}
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
case "enum_altered": {
|
|
581
|
+
if (!change.enumName || !change.addedValues)
|
|
582
|
+
break;
|
|
583
|
+
if (dialect.name === "postgres") {
|
|
584
|
+
const enumSnakeName = camelToSnake(change.enumName);
|
|
585
|
+
for (const val of change.addedValues) {
|
|
586
|
+
statements.push(`ALTER TYPE "${enumSnakeName}" ADD VALUE '${escapeSqlString(val)}';`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return statements.join(`
|
|
594
|
+
|
|
595
|
+
`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/migration/auto-migrate.ts
|
|
599
|
+
var DESTRUCTIVE_CHANGE_TYPES = ["table_removed", "column_removed"];
|
|
600
|
+
function isDestructiveChange(change) {
|
|
601
|
+
return DESTRUCTIVE_CHANGE_TYPES.includes(change.type);
|
|
602
|
+
}
|
|
603
|
+
async function autoMigrate(options) {
|
|
604
|
+
const { currentSchema, snapshotPath, db } = options;
|
|
605
|
+
let storage;
|
|
606
|
+
if (options.storage) {
|
|
607
|
+
storage = options.storage;
|
|
608
|
+
} else {
|
|
609
|
+
const { NodeSnapshotStorage } = await import("./chunk-dvwe5jsq.js");
|
|
610
|
+
storage = new NodeSnapshotStorage;
|
|
611
|
+
}
|
|
612
|
+
const previousSnapshot = await storage.load(snapshotPath);
|
|
613
|
+
const dialectObj = defaultSqliteDialect;
|
|
614
|
+
const runner = createMigrationRunner();
|
|
615
|
+
await runner.createHistoryTable(db);
|
|
616
|
+
if (!previousSnapshot) {
|
|
617
|
+
console.log("[auto-migrate] No previous snapshot found. Applying full schema...");
|
|
618
|
+
const diff = computeDiff({ version: 1, tables: {}, enums: {} }, currentSchema);
|
|
619
|
+
if (diff.changes.length > 0) {
|
|
620
|
+
for (const change of diff.changes) {
|
|
621
|
+
if (isDestructiveChange(change)) {
|
|
622
|
+
console.warn(`[auto-migrate] Warning: Destructive change detected on first run: ${change.type}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const sql = generateMigrationSql(diff.changes, {
|
|
626
|
+
tables: currentSchema.tables,
|
|
627
|
+
enums: currentSchema.enums
|
|
628
|
+
}, dialectObj);
|
|
629
|
+
if (sql.trim()) {
|
|
630
|
+
const result = await runner.apply(db, sql, "auto-migrate-initial");
|
|
631
|
+
if (isErr(result)) {
|
|
632
|
+
const error = result.error;
|
|
633
|
+
const errorMessage = typeof error === "object" && error !== null && "message" in error ? error.message : String(error);
|
|
634
|
+
const cause = typeof error === "object" && error !== null && "cause" in error ? error.cause : undefined;
|
|
635
|
+
const causeStr = cause ? ` (cause: ${String(cause)})` : "";
|
|
636
|
+
throw new Error(`Failed to apply initial schema: ${errorMessage}${causeStr}`);
|
|
637
|
+
}
|
|
638
|
+
console.log("[auto-migrate] Initial schema applied successfully.");
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
console.log("[auto-migrate] No schema changes to apply.");
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
console.log("[auto-migrate] Previous snapshot found. Computing diff...");
|
|
645
|
+
const diff = computeDiff(previousSnapshot, currentSchema);
|
|
646
|
+
if (diff.changes.length === 0) {
|
|
647
|
+
console.log("[auto-migrate] No schema changes detected.");
|
|
648
|
+
} else {
|
|
649
|
+
const destructiveChanges = diff.changes.filter(isDestructiveChange);
|
|
650
|
+
for (const change of destructiveChanges) {
|
|
651
|
+
let details = "";
|
|
652
|
+
if (change.type === "table_removed" && change.table) {
|
|
653
|
+
details = ` (table: ${change.table})`;
|
|
654
|
+
} else if (change.type === "column_removed" && change.table && change.column) {
|
|
655
|
+
details = ` (table: ${change.table}, column: ${change.column})`;
|
|
656
|
+
}
|
|
657
|
+
console.warn(`[auto-migrate] ⚠️ Warning: Destructive change detected${details}: ${change.type}`);
|
|
658
|
+
}
|
|
659
|
+
const sql = generateMigrationSql(diff.changes, {
|
|
660
|
+
tables: currentSchema.tables,
|
|
661
|
+
enums: currentSchema.enums
|
|
662
|
+
}, dialectObj);
|
|
663
|
+
if (sql.trim()) {
|
|
664
|
+
const result = await runner.apply(db, sql, `auto-migrate-${Date.now()}`);
|
|
665
|
+
if (isErr(result)) {
|
|
666
|
+
const error = result.error;
|
|
667
|
+
const errorMessage = typeof error === "object" && error !== null && "message" in error ? error.message : String(error);
|
|
668
|
+
const cause = typeof error === "object" && error !== null && "cause" in error ? error.cause : undefined;
|
|
669
|
+
const causeStr = cause ? ` (cause: ${String(cause)})` : "";
|
|
670
|
+
throw new Error(`Failed to apply migration: ${errorMessage}${causeStr}`);
|
|
671
|
+
}
|
|
672
|
+
console.log(`[auto-migrate] Applied ${diff.changes.length} change(s).`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
await storage.save(snapshotPath, currentSchema);
|
|
677
|
+
console.log("[auto-migrate] Snapshot saved.");
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/errors/db-error.ts
|
|
681
|
+
class DbError extends Error {
|
|
682
|
+
pgCode;
|
|
683
|
+
table;
|
|
684
|
+
query;
|
|
685
|
+
constructor(message) {
|
|
686
|
+
super(message);
|
|
687
|
+
this.name = new.target.name;
|
|
688
|
+
}
|
|
689
|
+
toJSON() {
|
|
690
|
+
const json = {
|
|
691
|
+
error: this.name,
|
|
692
|
+
code: this.code,
|
|
693
|
+
message: this.message
|
|
694
|
+
};
|
|
695
|
+
if (this.table !== undefined) {
|
|
696
|
+
json.table = this.table;
|
|
697
|
+
}
|
|
698
|
+
return json;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
class UniqueConstraintError extends DbError {
|
|
703
|
+
code = "UNIQUE_VIOLATION";
|
|
704
|
+
pgCode = "23505";
|
|
705
|
+
table;
|
|
706
|
+
query;
|
|
707
|
+
column;
|
|
708
|
+
value;
|
|
709
|
+
constructor(options) {
|
|
710
|
+
super(`Unique constraint violated on ${options.table}.${options.column}${options.value !== undefined ? ` (value: ${options.value})` : ""}`);
|
|
711
|
+
this.table = options.table;
|
|
712
|
+
this.column = options.column;
|
|
713
|
+
this.value = options.value;
|
|
714
|
+
this.query = options.query;
|
|
715
|
+
}
|
|
716
|
+
toJSON() {
|
|
717
|
+
return {
|
|
718
|
+
...super.toJSON(),
|
|
719
|
+
table: this.table,
|
|
720
|
+
column: this.column
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
class ForeignKeyError extends DbError {
|
|
726
|
+
code = "FOREIGN_KEY_VIOLATION";
|
|
727
|
+
pgCode = "23503";
|
|
728
|
+
table;
|
|
729
|
+
query;
|
|
730
|
+
constraint;
|
|
731
|
+
detail;
|
|
732
|
+
constructor(options) {
|
|
733
|
+
super(`Foreign key constraint "${options.constraint}" violated on table ${options.table}`);
|
|
734
|
+
this.table = options.table;
|
|
735
|
+
this.constraint = options.constraint;
|
|
736
|
+
this.detail = options.detail;
|
|
737
|
+
this.query = options.query;
|
|
738
|
+
}
|
|
739
|
+
toJSON() {
|
|
740
|
+
return {
|
|
741
|
+
...super.toJSON(),
|
|
742
|
+
table: this.table
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
class NotNullError extends DbError {
|
|
748
|
+
code = "NOT_NULL_VIOLATION";
|
|
749
|
+
pgCode = "23502";
|
|
750
|
+
table;
|
|
751
|
+
query;
|
|
752
|
+
column;
|
|
753
|
+
constructor(options) {
|
|
754
|
+
super(`Not-null constraint violated on ${options.table}.${options.column}`);
|
|
755
|
+
this.table = options.table;
|
|
756
|
+
this.column = options.column;
|
|
757
|
+
this.query = options.query;
|
|
758
|
+
}
|
|
759
|
+
toJSON() {
|
|
760
|
+
return {
|
|
761
|
+
...super.toJSON(),
|
|
762
|
+
table: this.table,
|
|
763
|
+
column: this.column
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
class CheckConstraintError extends DbError {
|
|
769
|
+
code = "CHECK_VIOLATION";
|
|
770
|
+
pgCode = "23514";
|
|
771
|
+
table;
|
|
772
|
+
query;
|
|
773
|
+
constraint;
|
|
774
|
+
constructor(options) {
|
|
775
|
+
super(`Check constraint "${options.constraint}" violated on table ${options.table}`);
|
|
776
|
+
this.table = options.table;
|
|
777
|
+
this.constraint = options.constraint;
|
|
778
|
+
this.query = options.query;
|
|
779
|
+
}
|
|
780
|
+
toJSON() {
|
|
781
|
+
return {
|
|
782
|
+
...super.toJSON(),
|
|
783
|
+
table: this.table
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
class NotFoundError extends DbError {
|
|
789
|
+
code = "NotFound";
|
|
790
|
+
table;
|
|
791
|
+
query;
|
|
792
|
+
constructor(table, query) {
|
|
793
|
+
super(`Record not found in table ${table}`);
|
|
794
|
+
this.table = table;
|
|
795
|
+
this.query = query;
|
|
796
|
+
}
|
|
797
|
+
toJSON() {
|
|
798
|
+
return {
|
|
799
|
+
...super.toJSON(),
|
|
800
|
+
table: this.table
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
class ConnectionError extends DbError {
|
|
806
|
+
code = "CONNECTION_ERROR";
|
|
807
|
+
constructor(message) {
|
|
808
|
+
super(`Database connection error: ${message}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
class ConnectionPoolExhaustedError extends ConnectionError {
|
|
813
|
+
code = "POOL_EXHAUSTED";
|
|
814
|
+
constructor(poolSize) {
|
|
815
|
+
super(`Connection pool exhausted (max: ${poolSize})`);
|
|
816
|
+
this.name = "ConnectionPoolExhaustedError";
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/errors/pg-parser.ts
|
|
821
|
+
function extractKeyDetail(detail) {
|
|
822
|
+
if (!detail)
|
|
823
|
+
return null;
|
|
824
|
+
const match = detail.match(/^Key \(([^)]+)\)=\(([^)]*)\)/);
|
|
825
|
+
if (!match)
|
|
826
|
+
return null;
|
|
827
|
+
return { column: match[1], value: match[2] };
|
|
828
|
+
}
|
|
829
|
+
function extractNotNullColumn(message) {
|
|
830
|
+
const match = message.match(/null value in column "([^"]+)"/);
|
|
831
|
+
return match ? match[1] : null;
|
|
832
|
+
}
|
|
833
|
+
function extractCheckConstraint(message) {
|
|
834
|
+
const match = message.match(/violates check constraint "([^"]+)"/);
|
|
835
|
+
return match ? match[1] : null;
|
|
836
|
+
}
|
|
837
|
+
function isConnectionErrorCode(code) {
|
|
838
|
+
return code.startsWith("08");
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
class UnknownDbError extends DbError {
|
|
842
|
+
code;
|
|
843
|
+
table;
|
|
844
|
+
query;
|
|
845
|
+
constructor(code, message, table, query) {
|
|
846
|
+
super(message);
|
|
847
|
+
this.code = code;
|
|
848
|
+
this.table = table;
|
|
849
|
+
this.query = query;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function parsePgError(pgError, query) {
|
|
853
|
+
const { code, message, table, column, constraint, detail } = pgError;
|
|
854
|
+
switch (code) {
|
|
855
|
+
case "23505": {
|
|
856
|
+
const keyDetail = extractKeyDetail(detail);
|
|
857
|
+
return new UniqueConstraintError({
|
|
858
|
+
table: table ?? "unknown",
|
|
859
|
+
column: column ?? keyDetail?.column ?? "unknown",
|
|
860
|
+
value: keyDetail?.value,
|
|
861
|
+
query
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
case "23503": {
|
|
865
|
+
return new ForeignKeyError({
|
|
866
|
+
table: table ?? "unknown",
|
|
867
|
+
constraint: constraint ?? "unknown",
|
|
868
|
+
detail,
|
|
869
|
+
query
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
case "23502": {
|
|
873
|
+
const extractedColumn = extractNotNullColumn(message);
|
|
874
|
+
return new NotNullError({
|
|
875
|
+
table: table ?? "unknown",
|
|
876
|
+
column: column ?? extractedColumn ?? "unknown",
|
|
877
|
+
query
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
case "23514": {
|
|
881
|
+
const extractedConstraint = extractCheckConstraint(message);
|
|
882
|
+
return new CheckConstraintError({
|
|
883
|
+
table: table ?? "unknown",
|
|
884
|
+
constraint: constraint ?? extractedConstraint ?? "unknown",
|
|
885
|
+
query
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
default: {
|
|
889
|
+
if (isConnectionErrorCode(code)) {
|
|
890
|
+
return new ConnectionError(message);
|
|
891
|
+
}
|
|
892
|
+
return new UnknownDbError(code, message, table, query);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/query/executor.ts
|
|
898
|
+
function isPgError(error) {
|
|
899
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && "message" in error && typeof error.message === "string";
|
|
900
|
+
}
|
|
901
|
+
async function executeQuery(queryFn, sql, params) {
|
|
902
|
+
try {
|
|
903
|
+
return await queryFn(sql, params);
|
|
904
|
+
} catch (error) {
|
|
905
|
+
if (isPgError(error)) {
|
|
906
|
+
throw parsePgError(error, sql);
|
|
907
|
+
}
|
|
908
|
+
throw error;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/query/helpers.ts
|
|
913
|
+
function getColumnNames(table) {
|
|
914
|
+
return Object.keys(table._columns);
|
|
915
|
+
}
|
|
916
|
+
function getDefaultColumns(table) {
|
|
917
|
+
return getColumnsWithoutAnnotations(table, []);
|
|
918
|
+
}
|
|
919
|
+
function getColumnsWithoutAnnotations(table, annotations) {
|
|
920
|
+
const allAnnotations = annotations.includes("hidden") ? annotations : [...annotations, "hidden"];
|
|
921
|
+
return Object.keys(table._columns).filter((key) => {
|
|
922
|
+
const col = table._columns[key];
|
|
923
|
+
if (!col)
|
|
924
|
+
return true;
|
|
925
|
+
const colAnnotations = col._meta._annotations;
|
|
926
|
+
return !allAnnotations.some((f) => colAnnotations[f]);
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
function resolveSelectColumns(table, select) {
|
|
930
|
+
if (!select) {
|
|
931
|
+
return getDefaultColumns(table);
|
|
932
|
+
}
|
|
933
|
+
if ("not" in select && select.not !== undefined) {
|
|
934
|
+
const notValue = select.not;
|
|
935
|
+
const flags = Array.isArray(notValue) ? notValue : [notValue];
|
|
936
|
+
return getColumnsWithoutAnnotations(table, flags);
|
|
937
|
+
}
|
|
938
|
+
return Object.keys(select).filter((k) => select[k] === true);
|
|
939
|
+
}
|
|
940
|
+
function getTimestampColumns(table) {
|
|
941
|
+
return Object.keys(table._columns).filter((key) => {
|
|
942
|
+
const col = table._columns[key];
|
|
943
|
+
return col ? col._meta.sqlType === "timestamp with time zone" : false;
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
function getPrimaryKeyColumns(table) {
|
|
947
|
+
return Object.keys(table._columns).filter((key) => {
|
|
948
|
+
const col = table._columns[key];
|
|
949
|
+
return col ? col._meta.primary : false;
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
function getReadOnlyColumns(table) {
|
|
953
|
+
return Object.keys(table._columns).filter((key) => {
|
|
954
|
+
const col = table._columns[key];
|
|
955
|
+
return col ? col._meta.isReadOnly : false;
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
function getAutoUpdateColumns(table) {
|
|
959
|
+
return Object.keys(table._columns).filter((key) => {
|
|
960
|
+
const col = table._columns[key];
|
|
961
|
+
return col ? col._meta.isAutoUpdate : false;
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/query/row-mapper.ts
|
|
966
|
+
function mapRow(row) {
|
|
967
|
+
const result = {};
|
|
968
|
+
for (const key of Object.keys(row)) {
|
|
969
|
+
result[snakeToCamel(key)] = row[key];
|
|
970
|
+
}
|
|
971
|
+
return result;
|
|
972
|
+
}
|
|
973
|
+
function mapRows(rows) {
|
|
974
|
+
return rows.map((row) => mapRow(row));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export { computeDiff, computeChecksum, parseMigrationName, createMigrationRunner, validateIndexes, generateMigrationSql, autoMigrate, DbError, UniqueConstraintError, ForeignKeyError, NotNullError, CheckConstraintError, NotFoundError, ConnectionError, ConnectionPoolExhaustedError, parsePgError, executeQuery, getColumnNames, getDefaultColumns, getColumnsWithoutAnnotations, resolveSelectColumns, getTimestampColumns, getPrimaryKeyColumns, getReadOnlyColumns, getAutoUpdateColumns, mapRow, mapRows };
|