@xubylele/schema-forge 1.4.0 → 1.5.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/dist/cli.js +138 -2254
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -29,7 +29,7 @@ var import_commander6 = require("commander");
|
|
|
29
29
|
// package.json
|
|
30
30
|
var package_default = {
|
|
31
31
|
name: "@xubylele/schema-forge",
|
|
32
|
-
version: "1.
|
|
32
|
+
version: "1.5.0",
|
|
33
33
|
description: "Universal migration generator from schema DSL",
|
|
34
34
|
main: "dist/cli.js",
|
|
35
35
|
type: "commonjs",
|
|
@@ -69,6 +69,7 @@ var package_default = {
|
|
|
69
69
|
node: ">=18.0.0"
|
|
70
70
|
},
|
|
71
71
|
dependencies: {
|
|
72
|
+
"@xubylele/schema-forge-core": "^1.0.4",
|
|
72
73
|
boxen: "^8.0.1",
|
|
73
74
|
chalk: "^5.6.2",
|
|
74
75
|
commander: "^14.0.3"
|
|
@@ -85,375 +86,7 @@ var package_default = {
|
|
|
85
86
|
|
|
86
87
|
// src/commands/diff.ts
|
|
87
88
|
var import_commander = require("commander");
|
|
88
|
-
var
|
|
89
|
-
|
|
90
|
-
// src/core/normalize.ts
|
|
91
|
-
function normalizeIdent(input) {
|
|
92
|
-
return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
93
|
-
}
|
|
94
|
-
function pkName(table) {
|
|
95
|
-
return `pk_${normalizeIdent(table)}`;
|
|
96
|
-
}
|
|
97
|
-
function uqName(table, column) {
|
|
98
|
-
return `uq_${normalizeIdent(table)}_${normalizeIdent(column)}`;
|
|
99
|
-
}
|
|
100
|
-
function legacyPkName(table) {
|
|
101
|
-
return `${normalizeIdent(table)}_pkey`;
|
|
102
|
-
}
|
|
103
|
-
function legacyUqName(table, column) {
|
|
104
|
-
return `${normalizeIdent(table)}_${normalizeIdent(column)}_key`;
|
|
105
|
-
}
|
|
106
|
-
function normalizeSpacesOutsideQuotes(value) {
|
|
107
|
-
let result = "";
|
|
108
|
-
let inSingleQuote = false;
|
|
109
|
-
let inDoubleQuote = false;
|
|
110
|
-
let pendingSpace = false;
|
|
111
|
-
for (const char of value) {
|
|
112
|
-
if (char === "'" && !inDoubleQuote) {
|
|
113
|
-
if (pendingSpace && result.length > 0 && result[result.length - 1] !== " ") {
|
|
114
|
-
result += " ";
|
|
115
|
-
}
|
|
116
|
-
pendingSpace = false;
|
|
117
|
-
inSingleQuote = !inSingleQuote;
|
|
118
|
-
result += char;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (char === '"' && !inSingleQuote) {
|
|
122
|
-
if (pendingSpace && result.length > 0 && result[result.length - 1] !== " ") {
|
|
123
|
-
result += " ";
|
|
124
|
-
}
|
|
125
|
-
pendingSpace = false;
|
|
126
|
-
inDoubleQuote = !inDoubleQuote;
|
|
127
|
-
result += char;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (!inSingleQuote && !inDoubleQuote && /\s/.test(char)) {
|
|
131
|
-
pendingSpace = true;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (pendingSpace && result.length > 0 && result[result.length - 1] !== " ") {
|
|
135
|
-
result += " ";
|
|
136
|
-
}
|
|
137
|
-
pendingSpace = false;
|
|
138
|
-
result += char;
|
|
139
|
-
}
|
|
140
|
-
return result.trim();
|
|
141
|
-
}
|
|
142
|
-
function normalizeKnownFunctionsOutsideQuotes(value) {
|
|
143
|
-
let result = "";
|
|
144
|
-
let inSingleQuote = false;
|
|
145
|
-
let inDoubleQuote = false;
|
|
146
|
-
let buffer = "";
|
|
147
|
-
function flushBuffer() {
|
|
148
|
-
if (!buffer) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
result += buffer.replace(/\bnow\s*\(\s*\)/gi, "now()").replace(/\bgen_random_uuid\s*\(\s*\)/gi, "gen_random_uuid()");
|
|
152
|
-
buffer = "";
|
|
153
|
-
}
|
|
154
|
-
for (const char of value) {
|
|
155
|
-
if (char === "'" && !inDoubleQuote) {
|
|
156
|
-
flushBuffer();
|
|
157
|
-
inSingleQuote = !inSingleQuote;
|
|
158
|
-
result += char;
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (char === '"' && !inSingleQuote) {
|
|
162
|
-
flushBuffer();
|
|
163
|
-
inDoubleQuote = !inDoubleQuote;
|
|
164
|
-
result += char;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (inSingleQuote || inDoubleQuote) {
|
|
168
|
-
result += char;
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
buffer += char;
|
|
172
|
-
}
|
|
173
|
-
flushBuffer();
|
|
174
|
-
return result;
|
|
175
|
-
}
|
|
176
|
-
function normalizePunctuationOutsideQuotes(value) {
|
|
177
|
-
let result = "";
|
|
178
|
-
let inSingleQuote = false;
|
|
179
|
-
let inDoubleQuote = false;
|
|
180
|
-
for (let index = 0; index < value.length; index++) {
|
|
181
|
-
const char = value[index];
|
|
182
|
-
if (char === "'" && !inDoubleQuote) {
|
|
183
|
-
inSingleQuote = !inSingleQuote;
|
|
184
|
-
result += char;
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
if (char === '"' && !inSingleQuote) {
|
|
188
|
-
inDoubleQuote = !inDoubleQuote;
|
|
189
|
-
result += char;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (!inSingleQuote && !inDoubleQuote && (char === "(" || char === ")")) {
|
|
193
|
-
while (result.endsWith(" ")) {
|
|
194
|
-
result = result.slice(0, -1);
|
|
195
|
-
}
|
|
196
|
-
result += char;
|
|
197
|
-
let lookahead = index + 1;
|
|
198
|
-
while (lookahead < value.length && value[lookahead] === " ") {
|
|
199
|
-
lookahead++;
|
|
200
|
-
}
|
|
201
|
-
index = lookahead - 1;
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
if (!inSingleQuote && !inDoubleQuote && char === ",") {
|
|
205
|
-
while (result.endsWith(" ")) {
|
|
206
|
-
result = result.slice(0, -1);
|
|
207
|
-
}
|
|
208
|
-
result += ", ";
|
|
209
|
-
let lookahead = index + 1;
|
|
210
|
-
while (lookahead < value.length && value[lookahead] === " ") {
|
|
211
|
-
lookahead++;
|
|
212
|
-
}
|
|
213
|
-
index = lookahead - 1;
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
result += char;
|
|
217
|
-
}
|
|
218
|
-
return result;
|
|
219
|
-
}
|
|
220
|
-
function normalizeDefault(expr) {
|
|
221
|
-
if (expr === void 0 || expr === null) {
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
const trimmed = expr.trim();
|
|
225
|
-
if (trimmed.length === 0) {
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
const normalizedSpacing = normalizeSpacesOutsideQuotes(trimmed);
|
|
229
|
-
const normalizedPunctuation = normalizePunctuationOutsideQuotes(normalizedSpacing);
|
|
230
|
-
return normalizeKnownFunctionsOutsideQuotes(normalizedPunctuation);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// src/core/diff.ts
|
|
234
|
-
function getTableNamesFromState(state) {
|
|
235
|
-
return new Set(Object.keys(state.tables));
|
|
236
|
-
}
|
|
237
|
-
function getTableNamesFromSchema(schema) {
|
|
238
|
-
return new Set(Object.keys(schema.tables));
|
|
239
|
-
}
|
|
240
|
-
function getColumnNamesFromState(stateColumns) {
|
|
241
|
-
return new Set(Object.keys(stateColumns));
|
|
242
|
-
}
|
|
243
|
-
function getColumnNamesFromSchema(dbColumns) {
|
|
244
|
-
return new Set(dbColumns.map((column) => column.name));
|
|
245
|
-
}
|
|
246
|
-
function getSortedNames(names) {
|
|
247
|
-
return Array.from(names).sort((a, b) => a.localeCompare(b));
|
|
248
|
-
}
|
|
249
|
-
function normalizeColumnType(type) {
|
|
250
|
-
return type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
251
|
-
}
|
|
252
|
-
function resolveStatePrimaryKey(table) {
|
|
253
|
-
return table.primaryKey ?? Object.entries(table.columns).find(([, column]) => column.primaryKey)?.[0] ?? null;
|
|
254
|
-
}
|
|
255
|
-
function resolveSchemaPrimaryKey(table) {
|
|
256
|
-
return table.primaryKey ?? table.columns.find((column) => column.primaryKey)?.name ?? null;
|
|
257
|
-
}
|
|
258
|
-
function normalizeNullable(nullable) {
|
|
259
|
-
return nullable ?? true;
|
|
260
|
-
}
|
|
261
|
-
function diffSchemas(oldState, newSchema) {
|
|
262
|
-
const operations = [];
|
|
263
|
-
const oldTableNames = getTableNamesFromState(oldState);
|
|
264
|
-
const newTableNames = getTableNamesFromSchema(newSchema);
|
|
265
|
-
const sortedNewTableNames = getSortedNames(newTableNames);
|
|
266
|
-
const sortedOldTableNames = getSortedNames(oldTableNames);
|
|
267
|
-
for (const tableName of sortedNewTableNames) {
|
|
268
|
-
if (!oldTableNames.has(tableName)) {
|
|
269
|
-
operations.push({
|
|
270
|
-
kind: "create_table",
|
|
271
|
-
table: newSchema.tables[tableName]
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
const commonTableNames = sortedNewTableNames.filter(
|
|
276
|
-
(tableName) => oldTableNames.has(tableName)
|
|
277
|
-
);
|
|
278
|
-
for (const tableName of commonTableNames) {
|
|
279
|
-
const newTable = newSchema.tables[tableName];
|
|
280
|
-
const oldTable = oldState.tables[tableName];
|
|
281
|
-
if (!newTable || !oldTable) {
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
for (const column of newTable.columns) {
|
|
285
|
-
const previousColumn = oldTable.columns[column.name];
|
|
286
|
-
if (!previousColumn) {
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
const previousType = normalizeColumnType(previousColumn.type);
|
|
290
|
-
const currentType = normalizeColumnType(column.type);
|
|
291
|
-
if (previousType !== currentType) {
|
|
292
|
-
operations.push({
|
|
293
|
-
kind: "column_type_changed",
|
|
294
|
-
tableName,
|
|
295
|
-
columnName: column.name,
|
|
296
|
-
fromType: previousColumn.type,
|
|
297
|
-
toType: column.type
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
for (const tableName of commonTableNames) {
|
|
303
|
-
const newTable = newSchema.tables[tableName];
|
|
304
|
-
const oldTable = oldState.tables[tableName];
|
|
305
|
-
if (!newTable || !oldTable) {
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
const previousPrimaryKey = resolveStatePrimaryKey(oldTable);
|
|
309
|
-
const currentPrimaryKey = resolveSchemaPrimaryKey(newTable);
|
|
310
|
-
if (previousPrimaryKey !== null && previousPrimaryKey !== currentPrimaryKey) {
|
|
311
|
-
operations.push({
|
|
312
|
-
kind: "drop_primary_key_constraint",
|
|
313
|
-
tableName
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
for (const tableName of commonTableNames) {
|
|
318
|
-
const newTable = newSchema.tables[tableName];
|
|
319
|
-
const oldTable = oldState.tables[tableName];
|
|
320
|
-
if (!newTable || !oldTable) {
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
for (const column of newTable.columns) {
|
|
324
|
-
const previousColumn = oldTable.columns[column.name];
|
|
325
|
-
if (!previousColumn) {
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
const previousUnique = previousColumn.unique ?? false;
|
|
329
|
-
const currentUnique = column.unique ?? false;
|
|
330
|
-
if (previousUnique !== currentUnique) {
|
|
331
|
-
operations.push({
|
|
332
|
-
kind: "column_unique_changed",
|
|
333
|
-
tableName,
|
|
334
|
-
columnName: column.name,
|
|
335
|
-
from: previousUnique,
|
|
336
|
-
to: currentUnique
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
for (const tableName of commonTableNames) {
|
|
342
|
-
const newTable = newSchema.tables[tableName];
|
|
343
|
-
const oldTable = oldState.tables[tableName];
|
|
344
|
-
if (!newTable || !oldTable) {
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
for (const column of newTable.columns) {
|
|
348
|
-
const previousColumn = oldTable.columns[column.name];
|
|
349
|
-
if (!previousColumn) {
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
const previousNullable = normalizeNullable(previousColumn.nullable);
|
|
353
|
-
const currentNullable = normalizeNullable(column.nullable);
|
|
354
|
-
if (previousNullable !== currentNullable) {
|
|
355
|
-
operations.push({
|
|
356
|
-
kind: "column_nullability_changed",
|
|
357
|
-
tableName,
|
|
358
|
-
columnName: column.name,
|
|
359
|
-
from: previousNullable,
|
|
360
|
-
to: currentNullable
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
for (const tableName of commonTableNames) {
|
|
366
|
-
const newTable = newSchema.tables[tableName];
|
|
367
|
-
const oldTable = oldState.tables[tableName];
|
|
368
|
-
if (!newTable || !oldTable) {
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
for (const column of newTable.columns) {
|
|
372
|
-
const previousColumn = oldTable.columns[column.name];
|
|
373
|
-
if (!previousColumn) {
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
const previousDefault = normalizeDefault(previousColumn.default);
|
|
377
|
-
const currentDefault = normalizeDefault(column.default);
|
|
378
|
-
if (previousDefault !== currentDefault) {
|
|
379
|
-
operations.push({
|
|
380
|
-
kind: "column_default_changed",
|
|
381
|
-
tableName,
|
|
382
|
-
columnName: column.name,
|
|
383
|
-
fromDefault: previousDefault,
|
|
384
|
-
toDefault: currentDefault
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
for (const tableName of commonTableNames) {
|
|
390
|
-
const newTable = newSchema.tables[tableName];
|
|
391
|
-
const oldTable = oldState.tables[tableName];
|
|
392
|
-
if (!newTable || !oldTable) {
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
const oldColumnNames = getColumnNamesFromState(oldTable.columns);
|
|
396
|
-
for (const column of newTable.columns) {
|
|
397
|
-
if (!oldColumnNames.has(column.name)) {
|
|
398
|
-
operations.push({
|
|
399
|
-
kind: "add_column",
|
|
400
|
-
tableName,
|
|
401
|
-
column
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
for (const tableName of commonTableNames) {
|
|
407
|
-
const newTable = newSchema.tables[tableName];
|
|
408
|
-
const oldTable = oldState.tables[tableName];
|
|
409
|
-
if (!newTable || !oldTable) {
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
const previousPrimaryKey = resolveStatePrimaryKey(oldTable);
|
|
413
|
-
const currentPrimaryKey = resolveSchemaPrimaryKey(newTable);
|
|
414
|
-
if (currentPrimaryKey !== null && previousPrimaryKey !== currentPrimaryKey) {
|
|
415
|
-
operations.push({
|
|
416
|
-
kind: "add_primary_key_constraint",
|
|
417
|
-
tableName,
|
|
418
|
-
columnName: currentPrimaryKey
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
for (const tableName of commonTableNames) {
|
|
423
|
-
const newTable = newSchema.tables[tableName];
|
|
424
|
-
const oldTable = oldState.tables[tableName];
|
|
425
|
-
if (!newTable || !oldTable) {
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
const newColumnNames = getColumnNamesFromSchema(newTable.columns);
|
|
429
|
-
for (const columnName of Object.keys(oldTable.columns)) {
|
|
430
|
-
if (!newColumnNames.has(columnName)) {
|
|
431
|
-
operations.push({
|
|
432
|
-
kind: "drop_column",
|
|
433
|
-
tableName,
|
|
434
|
-
columnName
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
for (const tableName of sortedOldTableNames) {
|
|
440
|
-
if (!newTableNames.has(tableName)) {
|
|
441
|
-
operations.push({
|
|
442
|
-
kind: "drop_table",
|
|
443
|
-
tableName
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return { operations };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// src/core/errors.ts
|
|
451
|
-
var SchemaValidationError = class extends Error {
|
|
452
|
-
constructor(message) {
|
|
453
|
-
super(message);
|
|
454
|
-
this.name = "SchemaValidationError";
|
|
455
|
-
}
|
|
456
|
-
};
|
|
89
|
+
var import_path3 = __toESM(require("path"));
|
|
457
90
|
|
|
458
91
|
// src/core/fs.ts
|
|
459
92
|
var import_fs = require("fs");
|
|
@@ -509,329 +142,6 @@ async function writeJsonFile(filePath, data) {
|
|
|
509
142
|
throw new Error(`Failed to write JSON file ${filePath}: ${error2}`);
|
|
510
143
|
}
|
|
511
144
|
}
|
|
512
|
-
async function findFiles(dirPath, pattern) {
|
|
513
|
-
const results = [];
|
|
514
|
-
try {
|
|
515
|
-
const items = await import_fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
516
|
-
for (const item of items) {
|
|
517
|
-
const fullPath = import_path.default.join(dirPath, item.name);
|
|
518
|
-
if (item.isDirectory()) {
|
|
519
|
-
const subResults = await findFiles(fullPath, pattern);
|
|
520
|
-
results.push(...subResults);
|
|
521
|
-
} else if (item.isFile() && pattern.test(item.name)) {
|
|
522
|
-
results.push(fullPath);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
} catch (error2) {
|
|
526
|
-
throw new Error(`Failed to find files in ${dirPath}: ${error2}`);
|
|
527
|
-
}
|
|
528
|
-
return results;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// src/utils/output.ts
|
|
532
|
-
var import_boxen = __toESM(require("boxen"));
|
|
533
|
-
var import_chalk = require("chalk");
|
|
534
|
-
var isInteractive = Boolean(process.stdout?.isTTY);
|
|
535
|
-
var colorsEnabled = isInteractive && process.env.FORCE_COLOR !== "0" && !("NO_COLOR" in process.env);
|
|
536
|
-
var color = new import_chalk.Chalk({ level: colorsEnabled ? 3 : 0 });
|
|
537
|
-
var theme = {
|
|
538
|
-
primary: color.cyanBright,
|
|
539
|
-
success: color.hex("#00FF88"),
|
|
540
|
-
warning: color.hex("#FFD166"),
|
|
541
|
-
error: color.hex("#EF476F"),
|
|
542
|
-
accent: color.magentaBright
|
|
543
|
-
};
|
|
544
|
-
function success(message) {
|
|
545
|
-
const text = theme.success(`[OK] ${message}`);
|
|
546
|
-
if (!isInteractive) {
|
|
547
|
-
console.log(text);
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
try {
|
|
551
|
-
console.log(
|
|
552
|
-
(0, import_boxen.default)(text, {
|
|
553
|
-
padding: 1,
|
|
554
|
-
borderColor: "cyan",
|
|
555
|
-
borderStyle: "round"
|
|
556
|
-
})
|
|
557
|
-
);
|
|
558
|
-
} catch {
|
|
559
|
-
console.log(text);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
function info(message) {
|
|
563
|
-
console.log(theme.primary(message));
|
|
564
|
-
}
|
|
565
|
-
function warning(message) {
|
|
566
|
-
console.warn(theme.warning(`[WARN] ${message}`));
|
|
567
|
-
}
|
|
568
|
-
function error(message) {
|
|
569
|
-
console.error(theme.error(`[ERROR] ${message}`));
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// src/core/parser.ts
|
|
573
|
-
var SchemaParser = class {
|
|
574
|
-
/**
|
|
575
|
-
* Parse a schema from a JSON file
|
|
576
|
-
*/
|
|
577
|
-
async parseSchemaFile(filePath) {
|
|
578
|
-
try {
|
|
579
|
-
const schema = await readJsonFile(filePath, {});
|
|
580
|
-
return this.normalizeSchema(schema);
|
|
581
|
-
} catch (error2) {
|
|
582
|
-
throw new Error(`Failed to parse schema file ${filePath}: ${error2}`);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* Parse multiple schema files from a directory
|
|
587
|
-
*/
|
|
588
|
-
async parseSchemaDirectory(dirPath) {
|
|
589
|
-
const schemaFiles = await findFiles(dirPath, /\.schema\.json$/);
|
|
590
|
-
const schemas = [];
|
|
591
|
-
for (const file of schemaFiles) {
|
|
592
|
-
try {
|
|
593
|
-
const schema = await this.parseSchemaFile(file);
|
|
594
|
-
schemas.push(schema);
|
|
595
|
-
} catch (error2) {
|
|
596
|
-
const reason = error2 instanceof Error ? error2.message : String(error2);
|
|
597
|
-
warning(`Could not parse ${file}: ${reason}`);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
return schemas;
|
|
601
|
-
}
|
|
602
|
-
/**
|
|
603
|
-
* Merge multiple schemas into one
|
|
604
|
-
*/
|
|
605
|
-
mergeSchemas(schemas) {
|
|
606
|
-
if (schemas.length === 0) {
|
|
607
|
-
throw new Error("Cannot merge empty schema array");
|
|
608
|
-
}
|
|
609
|
-
const baseSchema = schemas[0];
|
|
610
|
-
const mergedTables = [];
|
|
611
|
-
for (const schema of schemas) {
|
|
612
|
-
for (const table of schema.tables) {
|
|
613
|
-
const existingIndex = mergedTables.findIndex((t) => t.name === table.name);
|
|
614
|
-
if (existingIndex >= 0) {
|
|
615
|
-
warning(`Duplicate table '${table.name}' found, using first occurrence`);
|
|
616
|
-
} else {
|
|
617
|
-
mergedTables.push(table);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
return {
|
|
622
|
-
version: baseSchema.version,
|
|
623
|
-
database: baseSchema.database,
|
|
624
|
-
tables: mergedTables
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
/**
|
|
628
|
-
* Normalize schema to ensure consistent structure
|
|
629
|
-
*/
|
|
630
|
-
normalizeSchema(schema) {
|
|
631
|
-
return {
|
|
632
|
-
version: schema.version || "1.0.0",
|
|
633
|
-
database: schema.database || "postgres",
|
|
634
|
-
tables: schema.tables.map((table) => ({
|
|
635
|
-
...table,
|
|
636
|
-
fields: table.fields.map((field) => ({
|
|
637
|
-
...field,
|
|
638
|
-
required: field.required ?? false,
|
|
639
|
-
unique: field.unique ?? false
|
|
640
|
-
})),
|
|
641
|
-
indexes: table.indexes || [],
|
|
642
|
-
constraints: table.constraints || []
|
|
643
|
-
}))
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* Convert schema to JSON string
|
|
648
|
-
*/
|
|
649
|
-
schemaToJson(schema, pretty = true) {
|
|
650
|
-
return pretty ? JSON.stringify(schema, null, 2) : JSON.stringify(schema);
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* Parse schema from JSON string
|
|
654
|
-
*/
|
|
655
|
-
parseSchemaString(jsonString) {
|
|
656
|
-
try {
|
|
657
|
-
const schema = JSON.parse(jsonString);
|
|
658
|
-
return this.normalizeSchema(schema);
|
|
659
|
-
} catch (error2) {
|
|
660
|
-
throw new Error(`Failed to parse schema JSON: ${error2}`);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
};
|
|
664
|
-
var defaultParser = new SchemaParser();
|
|
665
|
-
function parseSchema(source) {
|
|
666
|
-
const lines = source.split("\n");
|
|
667
|
-
const tables = {};
|
|
668
|
-
let currentLine = 0;
|
|
669
|
-
const validBaseColumnTypes = /* @__PURE__ */ new Set([
|
|
670
|
-
"uuid",
|
|
671
|
-
"varchar",
|
|
672
|
-
"text",
|
|
673
|
-
"int",
|
|
674
|
-
"bigint",
|
|
675
|
-
"boolean",
|
|
676
|
-
"timestamptz",
|
|
677
|
-
"date"
|
|
678
|
-
]);
|
|
679
|
-
function normalizeColumnType3(type) {
|
|
680
|
-
return type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
681
|
-
}
|
|
682
|
-
function isValidColumnType2(type) {
|
|
683
|
-
const normalizedType = normalizeColumnType3(type);
|
|
684
|
-
if (validBaseColumnTypes.has(normalizedType)) {
|
|
685
|
-
return true;
|
|
686
|
-
}
|
|
687
|
-
return /^varchar\(\d+\)$/.test(normalizedType) || /^numeric\(\d+,\d+\)$/.test(normalizedType);
|
|
688
|
-
}
|
|
689
|
-
function cleanLine(line) {
|
|
690
|
-
const commentIndex = line.search(/(?:\/\/|#)/);
|
|
691
|
-
if (commentIndex !== -1) {
|
|
692
|
-
line = line.substring(0, commentIndex);
|
|
693
|
-
}
|
|
694
|
-
return line.trim();
|
|
695
|
-
}
|
|
696
|
-
function parseForeignKey(fkRef, lineNum) {
|
|
697
|
-
const parts = fkRef.split(".");
|
|
698
|
-
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
699
|
-
throw new Error(`Line ${lineNum}: Invalid foreign key format '${fkRef}'. Expected format: table.column`);
|
|
700
|
-
}
|
|
701
|
-
return {
|
|
702
|
-
table: parts[0],
|
|
703
|
-
column: parts[1]
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
function parseColumn(line, lineNum) {
|
|
707
|
-
const tokens = line.split(/\s+/).filter((t) => t.length > 0);
|
|
708
|
-
const modifiers = /* @__PURE__ */ new Set(["pk", "unique", "nullable", "default", "fk"]);
|
|
709
|
-
if (tokens.length < 2) {
|
|
710
|
-
throw new Error(`Line ${lineNum}: Invalid column definition. Expected: <name> <type> [modifiers...]`);
|
|
711
|
-
}
|
|
712
|
-
const colName = tokens[0];
|
|
713
|
-
const colType = normalizeColumnType3(tokens[1]);
|
|
714
|
-
if (!isValidColumnType2(colType)) {
|
|
715
|
-
throw new Error(
|
|
716
|
-
`Line ${lineNum}: Invalid column type '${tokens[1]}'. Valid types: ${Array.from(validBaseColumnTypes).join(", ")}, varchar(n), numeric(p,s)`
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
const column = {
|
|
720
|
-
name: colName,
|
|
721
|
-
type: colType,
|
|
722
|
-
nullable: true
|
|
723
|
-
};
|
|
724
|
-
let i = 2;
|
|
725
|
-
while (i < tokens.length) {
|
|
726
|
-
const modifier = tokens[i];
|
|
727
|
-
switch (modifier) {
|
|
728
|
-
case "pk":
|
|
729
|
-
column.primaryKey = true;
|
|
730
|
-
i++;
|
|
731
|
-
break;
|
|
732
|
-
case "unique":
|
|
733
|
-
column.unique = true;
|
|
734
|
-
i++;
|
|
735
|
-
break;
|
|
736
|
-
case "nullable":
|
|
737
|
-
column.nullable = true;
|
|
738
|
-
i++;
|
|
739
|
-
break;
|
|
740
|
-
case "not":
|
|
741
|
-
if (tokens[i + 1] !== "null") {
|
|
742
|
-
throw new Error(`Line ${lineNum}: Unknown modifier 'not'`);
|
|
743
|
-
}
|
|
744
|
-
column.nullable = false;
|
|
745
|
-
i += 2;
|
|
746
|
-
break;
|
|
747
|
-
case "default":
|
|
748
|
-
i++;
|
|
749
|
-
if (i >= tokens.length) {
|
|
750
|
-
throw new Error(`Line ${lineNum}: 'default' modifier requires a value`);
|
|
751
|
-
}
|
|
752
|
-
{
|
|
753
|
-
const defaultTokens = [];
|
|
754
|
-
while (i < tokens.length && !modifiers.has(tokens[i])) {
|
|
755
|
-
defaultTokens.push(tokens[i]);
|
|
756
|
-
i++;
|
|
757
|
-
}
|
|
758
|
-
if (defaultTokens.length === 0) {
|
|
759
|
-
throw new Error(`Line ${lineNum}: 'default' modifier requires a value`);
|
|
760
|
-
}
|
|
761
|
-
column.default = defaultTokens.join(" ");
|
|
762
|
-
}
|
|
763
|
-
break;
|
|
764
|
-
case "fk":
|
|
765
|
-
i++;
|
|
766
|
-
if (i >= tokens.length) {
|
|
767
|
-
throw new Error(`Line ${lineNum}: 'fk' modifier requires a table.column reference`);
|
|
768
|
-
}
|
|
769
|
-
column.foreignKey = parseForeignKey(tokens[i], lineNum);
|
|
770
|
-
i++;
|
|
771
|
-
break;
|
|
772
|
-
default:
|
|
773
|
-
throw new Error(`Line ${lineNum}: Unknown modifier '${modifier}'`);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
return column;
|
|
777
|
-
}
|
|
778
|
-
function parseTableBlock(startLine) {
|
|
779
|
-
const firstLine = cleanLine(lines[startLine]);
|
|
780
|
-
const match = firstLine.match(/^table\s+(\w+)\s*\{?\s*$/);
|
|
781
|
-
if (!match) {
|
|
782
|
-
throw new Error(`Line ${startLine + 1}: Invalid table definition. Expected: table <name> {`);
|
|
783
|
-
}
|
|
784
|
-
const tableName = match[1];
|
|
785
|
-
if (tables[tableName]) {
|
|
786
|
-
throw new Error(`Line ${startLine + 1}: Duplicate table definition '${tableName}'`);
|
|
787
|
-
}
|
|
788
|
-
const columns = [];
|
|
789
|
-
let lineIdx = startLine + 1;
|
|
790
|
-
let foundClosingBrace = false;
|
|
791
|
-
while (lineIdx < lines.length) {
|
|
792
|
-
const cleaned = cleanLine(lines[lineIdx]);
|
|
793
|
-
if (!cleaned) {
|
|
794
|
-
lineIdx++;
|
|
795
|
-
continue;
|
|
796
|
-
}
|
|
797
|
-
if (cleaned === "}") {
|
|
798
|
-
foundClosingBrace = true;
|
|
799
|
-
break;
|
|
800
|
-
}
|
|
801
|
-
try {
|
|
802
|
-
const column = parseColumn(cleaned, lineIdx + 1);
|
|
803
|
-
columns.push(column);
|
|
804
|
-
} catch (error2) {
|
|
805
|
-
throw error2;
|
|
806
|
-
}
|
|
807
|
-
lineIdx++;
|
|
808
|
-
}
|
|
809
|
-
if (!foundClosingBrace) {
|
|
810
|
-
throw new Error(`Line ${startLine + 1}: Table '${tableName}' block not closed (missing '}')`);
|
|
811
|
-
}
|
|
812
|
-
const primaryKeyColumn = columns.find((column) => column.primaryKey)?.name ?? null;
|
|
813
|
-
tables[tableName] = {
|
|
814
|
-
name: tableName,
|
|
815
|
-
columns,
|
|
816
|
-
...primaryKeyColumn !== null && { primaryKey: primaryKeyColumn }
|
|
817
|
-
};
|
|
818
|
-
return lineIdx;
|
|
819
|
-
}
|
|
820
|
-
while (currentLine < lines.length) {
|
|
821
|
-
const cleaned = cleanLine(lines[currentLine]);
|
|
822
|
-
if (!cleaned) {
|
|
823
|
-
currentLine++;
|
|
824
|
-
continue;
|
|
825
|
-
}
|
|
826
|
-
if (cleaned.startsWith("table ")) {
|
|
827
|
-
currentLine = parseTableBlock(currentLine);
|
|
828
|
-
} else {
|
|
829
|
-
throw new Error(`Line ${currentLine + 1}: Unexpected content '${cleaned}'. Expected table definition.`);
|
|
830
|
-
}
|
|
831
|
-
currentLine++;
|
|
832
|
-
}
|
|
833
|
-
return { tables };
|
|
834
|
-
}
|
|
835
145
|
|
|
836
146
|
// src/core/paths.ts
|
|
837
147
|
var import_path2 = __toESM(require("path"));
|
|
@@ -856,568 +166,129 @@ function getStatePath(root, config) {
|
|
|
856
166
|
return import_path2.default.join(schemaForgeDir, fileName);
|
|
857
167
|
}
|
|
858
168
|
|
|
859
|
-
// src/core/
|
|
860
|
-
var
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
this.root = root;
|
|
865
|
-
}
|
|
866
|
-
/**
|
|
867
|
-
* Initialize a new SchemaForge project
|
|
868
|
-
*/
|
|
869
|
-
async initializeProject(directory = ".", force = false) {
|
|
870
|
-
const configPath = import_path3.default.join(directory, "schemaforge.config.json");
|
|
871
|
-
if (await fileExists(configPath) && !force) {
|
|
872
|
-
throw new Error("SchemaForge project already initialized. Use --force to overwrite.");
|
|
873
|
-
}
|
|
874
|
-
const defaultConfig = {
|
|
875
|
-
version: "1.0.0",
|
|
876
|
-
database: "postgres",
|
|
877
|
-
schemaDir: "schemas",
|
|
878
|
-
outputDir: "output",
|
|
879
|
-
migrationDir: "migrations"
|
|
880
|
-
};
|
|
881
|
-
await writeJsonFile(configPath, defaultConfig);
|
|
882
|
-
await ensureDir(import_path3.default.join(directory, defaultConfig.schemaDir));
|
|
883
|
-
await ensureDir(import_path3.default.join(directory, defaultConfig.outputDir));
|
|
884
|
-
await ensureDir(import_path3.default.join(directory, defaultConfig.migrationDir));
|
|
885
|
-
const exampleSchema = {
|
|
886
|
-
version: "1.0.0",
|
|
887
|
-
database: "postgres",
|
|
888
|
-
tables: [
|
|
889
|
-
{
|
|
890
|
-
name: "users",
|
|
891
|
-
fields: [
|
|
892
|
-
{ name: "id", type: "uuid", required: true, unique: true },
|
|
893
|
-
{ name: "email", type: "string", required: true, unique: true, length: 255 },
|
|
894
|
-
{ name: "name", type: "string", required: true, length: 255 },
|
|
895
|
-
{ name: "created_at", type: "datetime", required: true }
|
|
896
|
-
],
|
|
897
|
-
indexes: [
|
|
898
|
-
{ name: "idx_users_email", fields: ["email"], unique: true }
|
|
899
|
-
]
|
|
900
|
-
}
|
|
901
|
-
]
|
|
902
|
-
};
|
|
903
|
-
const exampleSchemaPath = import_path3.default.join(
|
|
904
|
-
directory,
|
|
905
|
-
defaultConfig.schemaDir,
|
|
906
|
-
"example.schema.json"
|
|
907
|
-
);
|
|
908
|
-
await writeJsonFile(exampleSchemaPath, exampleSchema);
|
|
909
|
-
this.config = defaultConfig;
|
|
910
|
-
}
|
|
911
|
-
/**
|
|
912
|
-
* Load configuration from file
|
|
913
|
-
*/
|
|
914
|
-
async loadConfig(directory = ".") {
|
|
915
|
-
const configPath = import_path3.default.join(directory, "schemaforge.config.json");
|
|
916
|
-
if (!await fileExists(configPath)) {
|
|
917
|
-
throw new Error('SchemaForge project not initialized. Run "schemaforge init" first.');
|
|
918
|
-
}
|
|
919
|
-
this.config = await readJsonFile(configPath, {});
|
|
920
|
-
return this.config;
|
|
921
|
-
}
|
|
922
|
-
/**
|
|
923
|
-
* Save configuration to file
|
|
924
|
-
*/
|
|
925
|
-
async saveConfig(config, directory = ".") {
|
|
926
|
-
const configPath = import_path3.default.join(directory, "schemaforge.config.json");
|
|
927
|
-
await writeJsonFile(configPath, config);
|
|
928
|
-
this.config = config;
|
|
929
|
-
}
|
|
930
|
-
/**
|
|
931
|
-
* Get current configuration
|
|
932
|
-
*/
|
|
933
|
-
getConfig() {
|
|
934
|
-
return this.config;
|
|
935
|
-
}
|
|
936
|
-
/**
|
|
937
|
-
* Update configuration
|
|
938
|
-
*/
|
|
939
|
-
updateConfig(updates) {
|
|
940
|
-
if (!this.config) {
|
|
941
|
-
throw new Error("No configuration loaded");
|
|
942
|
-
}
|
|
943
|
-
this.config = { ...this.config, ...updates };
|
|
169
|
+
// src/core/provider.ts
|
|
170
|
+
var DEFAULT_PROVIDER = "postgres";
|
|
171
|
+
function resolveProvider(provider) {
|
|
172
|
+
if (!provider) {
|
|
173
|
+
return { provider: DEFAULT_PROVIDER, usedDefault: true };
|
|
944
174
|
}
|
|
945
|
-
|
|
946
|
-
* Check if project is initialized
|
|
947
|
-
*/
|
|
948
|
-
async isInitialized(directory = ".") {
|
|
949
|
-
const configPath = import_path3.default.join(directory, "schemaforge.config.json");
|
|
950
|
-
return await fileExists(configPath);
|
|
951
|
-
}
|
|
952
|
-
/**
|
|
953
|
-
* Get schema directory path
|
|
954
|
-
*/
|
|
955
|
-
getSchemaDir() {
|
|
956
|
-
if (!this.config) {
|
|
957
|
-
throw new Error("No configuration loaded");
|
|
958
|
-
}
|
|
959
|
-
return import_path3.default.join(this.root, this.config.schemaDir);
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* Get output directory path
|
|
963
|
-
*/
|
|
964
|
-
getOutputDir() {
|
|
965
|
-
if (!this.config) {
|
|
966
|
-
throw new Error("No configuration loaded");
|
|
967
|
-
}
|
|
968
|
-
return import_path3.default.join(this.root, this.config.outputDir);
|
|
969
|
-
}
|
|
970
|
-
/**
|
|
971
|
-
* Get migration directory path
|
|
972
|
-
*/
|
|
973
|
-
getMigrationDir() {
|
|
974
|
-
if (!this.config) {
|
|
975
|
-
throw new Error("No configuration loaded");
|
|
976
|
-
}
|
|
977
|
-
return import_path3.default.join(this.root, this.config.migrationDir);
|
|
978
|
-
}
|
|
979
|
-
};
|
|
980
|
-
async function schemaToState(schema) {
|
|
981
|
-
const tables = {};
|
|
982
|
-
for (const [tableName, table] of Object.entries(schema.tables)) {
|
|
983
|
-
const columns = {};
|
|
984
|
-
const primaryKeyColumn = table.primaryKey ?? table.columns.find((column) => column.primaryKey)?.name ?? null;
|
|
985
|
-
for (const column of table.columns) {
|
|
986
|
-
columns[column.name] = {
|
|
987
|
-
type: column.type,
|
|
988
|
-
...column.primaryKey !== void 0 && { primaryKey: column.primaryKey },
|
|
989
|
-
...column.unique !== void 0 && { unique: column.unique },
|
|
990
|
-
nullable: column.nullable ?? true,
|
|
991
|
-
...column.default !== void 0 && { default: column.default },
|
|
992
|
-
...column.foreignKey !== void 0 && { foreignKey: column.foreignKey }
|
|
993
|
-
};
|
|
994
|
-
}
|
|
995
|
-
tables[tableName] = {
|
|
996
|
-
columns,
|
|
997
|
-
...primaryKeyColumn !== null && { primaryKey: primaryKeyColumn }
|
|
998
|
-
};
|
|
999
|
-
}
|
|
1000
|
-
return {
|
|
1001
|
-
version: 1,
|
|
1002
|
-
tables
|
|
1003
|
-
};
|
|
1004
|
-
}
|
|
1005
|
-
async function loadState(statePath) {
|
|
1006
|
-
return await readJsonFile(statePath, { version: 1, tables: {} });
|
|
1007
|
-
}
|
|
1008
|
-
async function saveState(statePath, state) {
|
|
1009
|
-
const dirPath = import_path3.default.dirname(statePath);
|
|
1010
|
-
await ensureDir(dirPath);
|
|
1011
|
-
await writeJsonFile(statePath, state);
|
|
175
|
+
return { provider, usedDefault: false };
|
|
1012
176
|
}
|
|
1013
|
-
var defaultStateManager = new StateManager();
|
|
1014
177
|
|
|
1015
|
-
// src/
|
|
1016
|
-
var
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
validateSchema(schema) {
|
|
1021
|
-
const errors = [];
|
|
1022
|
-
if (!schema.version) {
|
|
1023
|
-
errors.push({
|
|
1024
|
-
path: "schema.version",
|
|
1025
|
-
message: "Schema version is required",
|
|
1026
|
-
severity: "error"
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
if (!schema.database) {
|
|
1030
|
-
errors.push({
|
|
1031
|
-
path: "schema.database",
|
|
1032
|
-
message: "Database type is required",
|
|
1033
|
-
severity: "error"
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
if (!schema.tables || schema.tables.length === 0) {
|
|
1037
|
-
errors.push({
|
|
1038
|
-
path: "schema.tables",
|
|
1039
|
-
message: "Schema must contain at least one table",
|
|
1040
|
-
severity: "error"
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
if (schema.tables) {
|
|
1044
|
-
const tableNames = /* @__PURE__ */ new Set();
|
|
1045
|
-
for (let i = 0; i < schema.tables.length; i++) {
|
|
1046
|
-
const table = schema.tables[i];
|
|
1047
|
-
const tableErrors = this.validateTable(table, i);
|
|
1048
|
-
errors.push(...tableErrors);
|
|
1049
|
-
if (tableNames.has(table.name)) {
|
|
1050
|
-
errors.push({
|
|
1051
|
-
path: `schema.tables[${i}].name`,
|
|
1052
|
-
message: `Duplicate table name: ${table.name}`,
|
|
1053
|
-
severity: "error"
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
tableNames.add(table.name);
|
|
1057
|
-
}
|
|
1058
|
-
errors.push(...this.validateReferences(schema));
|
|
1059
|
-
}
|
|
1060
|
-
return {
|
|
1061
|
-
valid: errors.filter((e) => e.severity === "error").length === 0,
|
|
1062
|
-
errors
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
/**
|
|
1066
|
-
* Validate a table
|
|
1067
|
-
*/
|
|
1068
|
-
validateTable(table, tableIndex) {
|
|
1069
|
-
const errors = [];
|
|
1070
|
-
const basePath = `schema.tables[${tableIndex}]`;
|
|
1071
|
-
if (!table.name || table.name.trim() === "") {
|
|
1072
|
-
errors.push({
|
|
1073
|
-
path: `${basePath}.name`,
|
|
1074
|
-
message: "Table name is required",
|
|
1075
|
-
severity: "error"
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
if (table.name && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table.name)) {
|
|
1079
|
-
errors.push({
|
|
1080
|
-
path: `${basePath}.name`,
|
|
1081
|
-
message: `Invalid table name '${table.name}': must start with letter or underscore and contain only alphanumeric characters and underscores`,
|
|
1082
|
-
severity: "error"
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
if (!table.fields || table.fields.length === 0) {
|
|
1086
|
-
errors.push({
|
|
1087
|
-
path: `${basePath}.fields`,
|
|
1088
|
-
message: `Table '${table.name}' must have at least one field`,
|
|
1089
|
-
severity: "error"
|
|
1090
|
-
});
|
|
1091
|
-
}
|
|
1092
|
-
if (table.fields) {
|
|
1093
|
-
const fieldNames = /* @__PURE__ */ new Set();
|
|
1094
|
-
for (let i = 0; i < table.fields.length; i++) {
|
|
1095
|
-
const field = table.fields[i];
|
|
1096
|
-
const fieldErrors = this.validateField(field, basePath, i);
|
|
1097
|
-
errors.push(...fieldErrors);
|
|
1098
|
-
if (fieldNames.has(field.name)) {
|
|
1099
|
-
errors.push({
|
|
1100
|
-
path: `${basePath}.fields[${i}].name`,
|
|
1101
|
-
message: `Duplicate field name: ${field.name}`,
|
|
1102
|
-
severity: "error"
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
|
-
fieldNames.add(field.name);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
return errors;
|
|
1109
|
-
}
|
|
1110
|
-
/**
|
|
1111
|
-
* Validate a field
|
|
1112
|
-
*/
|
|
1113
|
-
validateField(field, tablePath, fieldIndex) {
|
|
1114
|
-
const errors = [];
|
|
1115
|
-
const basePath = `${tablePath}.fields[${fieldIndex}]`;
|
|
1116
|
-
if (!field.name || field.name.trim() === "") {
|
|
1117
|
-
errors.push({
|
|
1118
|
-
path: `${basePath}.name`,
|
|
1119
|
-
message: "Field name is required",
|
|
1120
|
-
severity: "error"
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
if (field.name && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field.name)) {
|
|
1124
|
-
errors.push({
|
|
1125
|
-
path: `${basePath}.name`,
|
|
1126
|
-
message: `Invalid field name '${field.name}': must start with letter or underscore and contain only alphanumeric characters and underscores`,
|
|
1127
|
-
severity: "error"
|
|
1128
|
-
});
|
|
1129
|
-
}
|
|
1130
|
-
if (!field.type) {
|
|
1131
|
-
errors.push({
|
|
1132
|
-
path: `${basePath}.type`,
|
|
1133
|
-
message: "Field type is required",
|
|
1134
|
-
severity: "error"
|
|
1135
|
-
});
|
|
1136
|
-
}
|
|
1137
|
-
if (field.type === "enum") {
|
|
1138
|
-
if (!field.enumValues || field.enumValues.length === 0) {
|
|
1139
|
-
errors.push({
|
|
1140
|
-
path: `${basePath}.enumValues`,
|
|
1141
|
-
message: "Enum type requires enumValues array",
|
|
1142
|
-
severity: "error"
|
|
1143
|
-
});
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
if (field.type === "string" && field.length && field.length <= 0) {
|
|
1147
|
-
errors.push({
|
|
1148
|
-
path: `${basePath}.length`,
|
|
1149
|
-
message: "String length must be greater than 0",
|
|
1150
|
-
severity: "error"
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
return errors;
|
|
1154
|
-
}
|
|
1155
|
-
/**
|
|
1156
|
-
* Validate foreign key references
|
|
1157
|
-
*/
|
|
1158
|
-
validateReferences(schema) {
|
|
1159
|
-
const errors = [];
|
|
1160
|
-
const tableNames = new Set(schema.tables.map((t) => t.name));
|
|
1161
|
-
for (let i = 0; i < schema.tables.length; i++) {
|
|
1162
|
-
const table = schema.tables[i];
|
|
1163
|
-
for (let j = 0; j < table.fields.length; j++) {
|
|
1164
|
-
const field = table.fields[j];
|
|
1165
|
-
if (field.references) {
|
|
1166
|
-
const refTable = field.references.table;
|
|
1167
|
-
const refField = field.references.field;
|
|
1168
|
-
if (!tableNames.has(refTable)) {
|
|
1169
|
-
errors.push({
|
|
1170
|
-
path: `schema.tables[${i}].fields[${j}].references.table`,
|
|
1171
|
-
message: `Referenced table '${refTable}' does not exist`,
|
|
1172
|
-
severity: "error"
|
|
1173
|
-
});
|
|
1174
|
-
} else {
|
|
1175
|
-
const referencedTable = schema.tables.find((t) => t.name === refTable);
|
|
1176
|
-
if (referencedTable) {
|
|
1177
|
-
const referencedField = referencedTable.fields.find((f) => f.name === refField);
|
|
1178
|
-
if (!referencedField) {
|
|
1179
|
-
errors.push({
|
|
1180
|
-
path: `schema.tables[${i}].fields[${j}].references.field`,
|
|
1181
|
-
message: `Referenced field '${refField}' does not exist in table '${refTable}'`,
|
|
1182
|
-
severity: "error"
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
return errors;
|
|
1191
|
-
}
|
|
1192
|
-
};
|
|
1193
|
-
var defaultValidator = new SchemaValidator();
|
|
1194
|
-
var VALID_BASE_COLUMN_TYPES = [
|
|
1195
|
-
"uuid",
|
|
1196
|
-
"varchar",
|
|
1197
|
-
"text",
|
|
1198
|
-
"int",
|
|
1199
|
-
"bigint",
|
|
1200
|
-
"boolean",
|
|
1201
|
-
"timestamptz",
|
|
1202
|
-
"date"
|
|
1203
|
-
];
|
|
1204
|
-
function isValidColumnType(type) {
|
|
1205
|
-
const normalizedType = type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
1206
|
-
if (VALID_BASE_COLUMN_TYPES.includes(normalizedType)) {
|
|
1207
|
-
return true;
|
|
1208
|
-
}
|
|
1209
|
-
return /^varchar\(\d+\)$/.test(normalizedType) || /^numeric\(\d+,\d+\)$/.test(normalizedType);
|
|
1210
|
-
}
|
|
1211
|
-
function validateSchema(schema) {
|
|
1212
|
-
validateDuplicateTables(schema);
|
|
1213
|
-
for (const tableName in schema.tables) {
|
|
1214
|
-
const table = schema.tables[tableName];
|
|
1215
|
-
validateTableColumns(tableName, table, schema.tables);
|
|
178
|
+
// src/domain.ts
|
|
179
|
+
var corePromise;
|
|
180
|
+
async function loadCore() {
|
|
181
|
+
if (!corePromise) {
|
|
182
|
+
corePromise = import("@xubylele/schema-forge-core");
|
|
1216
183
|
}
|
|
184
|
+
return corePromise;
|
|
1217
185
|
}
|
|
1218
|
-
function
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
for (const tableName of tableNames) {
|
|
1222
|
-
if (seen.has(tableName)) {
|
|
1223
|
-
throw new Error(`Duplicate table: '${tableName}'`);
|
|
1224
|
-
}
|
|
1225
|
-
seen.add(tableName);
|
|
1226
|
-
}
|
|
186
|
+
async function parseSchema(source) {
|
|
187
|
+
const core = await loadCore();
|
|
188
|
+
return core.parseSchema(source);
|
|
1227
189
|
}
|
|
1228
|
-
function
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
for (const column of table.columns) {
|
|
1232
|
-
if (columnNames.has(column.name)) {
|
|
1233
|
-
throw new Error(`Table '${tableName}': duplicate column '${column.name}'`);
|
|
1234
|
-
}
|
|
1235
|
-
columnNames.add(column.name);
|
|
1236
|
-
if (column.primaryKey) {
|
|
1237
|
-
primaryKeyColumns.push(column.name);
|
|
1238
|
-
}
|
|
1239
|
-
if (!isValidColumnType(column.type)) {
|
|
1240
|
-
throw new Error(
|
|
1241
|
-
`Table '${tableName}', column '${column.name}': type '${column.type}' is not valid. Supported types: ${VALID_BASE_COLUMN_TYPES.join(", ")}, varchar(n), numeric(p,s)`
|
|
1242
|
-
);
|
|
1243
|
-
}
|
|
1244
|
-
if (column.foreignKey) {
|
|
1245
|
-
const fkTable = column.foreignKey.table;
|
|
1246
|
-
const fkColumn = column.foreignKey.column;
|
|
1247
|
-
if (!allTables[fkTable]) {
|
|
1248
|
-
throw new Error(
|
|
1249
|
-
`Table '${tableName}', column '${column.name}': referenced table '${fkTable}' does not exist`
|
|
1250
|
-
);
|
|
1251
|
-
}
|
|
1252
|
-
const referencedTable = allTables[fkTable];
|
|
1253
|
-
const columnExists = referencedTable.columns.some((col) => col.name === fkColumn);
|
|
1254
|
-
if (!columnExists) {
|
|
1255
|
-
throw new Error(
|
|
1256
|
-
`Table '${tableName}', column '${column.name}': table '${fkTable}' does not have column '${fkColumn}'`
|
|
1257
|
-
);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
if (primaryKeyColumns.length > 1) {
|
|
1262
|
-
throw new Error(`Table '${tableName}': can only have one primary key (found ${primaryKeyColumns.length})`);
|
|
1263
|
-
}
|
|
1264
|
-
const normalizedPrimaryKey = table.primaryKey ?? primaryKeyColumns[0] ?? null;
|
|
1265
|
-
if (table.primaryKey && !columnNames.has(table.primaryKey)) {
|
|
1266
|
-
throw new Error(
|
|
1267
|
-
`Table '${tableName}': primary key column '${table.primaryKey}' does not exist`
|
|
1268
|
-
);
|
|
1269
|
-
}
|
|
1270
|
-
if (table.primaryKey && primaryKeyColumns.length === 1 && primaryKeyColumns[0] !== table.primaryKey) {
|
|
1271
|
-
throw new Error(
|
|
1272
|
-
`Table '${tableName}': column-level primary key '${primaryKeyColumns[0]}' does not match table primary key '${table.primaryKey}'`
|
|
1273
|
-
);
|
|
1274
|
-
}
|
|
1275
|
-
if (normalizedPrimaryKey) {
|
|
1276
|
-
const pkMatches = table.columns.filter((column) => column.name === normalizedPrimaryKey);
|
|
1277
|
-
if (pkMatches.length !== 1) {
|
|
1278
|
-
throw new Error(
|
|
1279
|
-
`Table '${tableName}': primary key column '${normalizedPrimaryKey}' is invalid`
|
|
1280
|
-
);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
190
|
+
async function validateSchema(schema) {
|
|
191
|
+
const core = await loadCore();
|
|
192
|
+
core.validateSchema(schema);
|
|
1283
193
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
const statements = [];
|
|
1288
|
-
for (const operation of diff.operations) {
|
|
1289
|
-
const sql = generateOperation(operation, provider, sqlConfig);
|
|
1290
|
-
if (sql) {
|
|
1291
|
-
statements.push(sql);
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
return statements.join("\n\n");
|
|
194
|
+
async function diffSchemas(previousState, currentSchema) {
|
|
195
|
+
const core = await loadCore();
|
|
196
|
+
return core.diffSchemas(previousState, currentSchema);
|
|
1295
197
|
}
|
|
1296
|
-
function
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
return generateCreateTable(operation.table, provider, sqlConfig);
|
|
1300
|
-
case "drop_table":
|
|
1301
|
-
return generateDropTable(operation.tableName);
|
|
1302
|
-
case "column_type_changed":
|
|
1303
|
-
return generateAlterColumnType(
|
|
1304
|
-
operation.tableName,
|
|
1305
|
-
operation.columnName,
|
|
1306
|
-
operation.toType
|
|
1307
|
-
);
|
|
1308
|
-
case "column_nullability_changed":
|
|
1309
|
-
return generateAlterColumnNullability(
|
|
1310
|
-
operation.tableName,
|
|
1311
|
-
operation.columnName,
|
|
1312
|
-
operation.to
|
|
1313
|
-
);
|
|
1314
|
-
case "add_column":
|
|
1315
|
-
return generateAddColumn(operation.tableName, operation.column, provider, sqlConfig);
|
|
1316
|
-
case "column_default_changed":
|
|
1317
|
-
return generateAlterColumnDefault(
|
|
1318
|
-
operation.tableName,
|
|
1319
|
-
operation.columnName,
|
|
1320
|
-
operation.toDefault
|
|
1321
|
-
);
|
|
1322
|
-
case "drop_column":
|
|
1323
|
-
return generateDropColumn(operation.tableName, operation.columnName);
|
|
1324
|
-
case "column_unique_changed":
|
|
1325
|
-
return operation.to ? generateAddUniqueConstraint(operation.tableName, operation.columnName) : generateDropUniqueConstraint(operation.tableName, operation.columnName);
|
|
1326
|
-
case "drop_primary_key_constraint":
|
|
1327
|
-
return generateDropPrimaryKeyConstraint(operation.tableName);
|
|
1328
|
-
case "add_primary_key_constraint":
|
|
1329
|
-
return generateAddPrimaryKeyConstraint(operation.tableName, operation.columnName);
|
|
1330
|
-
}
|
|
198
|
+
async function generateSql(diff, provider, config) {
|
|
199
|
+
const core = await loadCore();
|
|
200
|
+
return core.generateSql(diff, provider, config);
|
|
1331
201
|
}
|
|
1332
|
-
function
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
);
|
|
1336
|
-
const lines = ["CREATE TABLE " + table.name + " ("];
|
|
1337
|
-
columnDefs.forEach((colDef, index) => {
|
|
1338
|
-
const isLast = index === columnDefs.length - 1;
|
|
1339
|
-
lines.push(" " + colDef + (isLast ? "" : ","));
|
|
1340
|
-
});
|
|
1341
|
-
lines.push(");");
|
|
1342
|
-
return lines.join("\n");
|
|
202
|
+
async function schemaToState(schema) {
|
|
203
|
+
const core = await loadCore();
|
|
204
|
+
return core.schemaToState(schema);
|
|
1343
205
|
}
|
|
1344
|
-
function
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
parts.push(
|
|
1348
|
-
`references ${column.foreignKey.table}(${column.foreignKey.column})`
|
|
1349
|
-
);
|
|
1350
|
-
}
|
|
1351
|
-
if (column.primaryKey) {
|
|
1352
|
-
parts.push("primary key");
|
|
1353
|
-
}
|
|
1354
|
-
if (column.unique) {
|
|
1355
|
-
parts.push("unique");
|
|
1356
|
-
}
|
|
1357
|
-
if (column.nullable === false) {
|
|
1358
|
-
parts.push("not null");
|
|
1359
|
-
}
|
|
1360
|
-
if (column.default !== void 0) {
|
|
1361
|
-
parts.push("default " + column.default);
|
|
1362
|
-
} else if (column.type === "uuid" && column.primaryKey && provider === "supabase") {
|
|
1363
|
-
parts.push("default gen_random_uuid()");
|
|
1364
|
-
}
|
|
1365
|
-
return parts.join(" ");
|
|
206
|
+
async function loadState(statePath) {
|
|
207
|
+
const core = await loadCore();
|
|
208
|
+
return core.loadState(statePath);
|
|
1366
209
|
}
|
|
1367
|
-
function
|
|
1368
|
-
|
|
210
|
+
async function saveState(statePath, state) {
|
|
211
|
+
const core = await loadCore();
|
|
212
|
+
return core.saveState(statePath, state);
|
|
1369
213
|
}
|
|
1370
|
-
function
|
|
1371
|
-
const
|
|
1372
|
-
return
|
|
214
|
+
async function validateSchemaChanges(previousState, currentSchema) {
|
|
215
|
+
const core = await loadCore();
|
|
216
|
+
return core.validateSchemaChanges(previousState, currentSchema);
|
|
1373
217
|
}
|
|
1374
|
-
function
|
|
1375
|
-
|
|
218
|
+
async function toValidationReport(findings) {
|
|
219
|
+
const core = await loadCore();
|
|
220
|
+
return core.toValidationReport(findings);
|
|
1376
221
|
}
|
|
1377
|
-
function
|
|
1378
|
-
|
|
222
|
+
async function parseMigrationSql(sql) {
|
|
223
|
+
const core = await loadCore();
|
|
224
|
+
return core.parseMigrationSql(sql);
|
|
1379
225
|
}
|
|
1380
|
-
function
|
|
1381
|
-
const
|
|
1382
|
-
return
|
|
226
|
+
async function applySqlOps(ops) {
|
|
227
|
+
const core = await loadCore();
|
|
228
|
+
return core.applySqlOps(ops);
|
|
1383
229
|
}
|
|
1384
|
-
function
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
return generateDropConstraintStatements(tableName, [deterministicConstraintName, fallbackConstraintName]);
|
|
230
|
+
async function schemaToDsl(schema) {
|
|
231
|
+
const core = await loadCore();
|
|
232
|
+
return core.schemaToDsl(schema);
|
|
1388
233
|
}
|
|
1389
|
-
function
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
return generateDropConstraintStatements(tableName, [deterministicConstraintName, fallbackConstraintName]);
|
|
234
|
+
async function loadMigrationSqlInput(inputPath) {
|
|
235
|
+
const core = await loadCore();
|
|
236
|
+
return core.loadMigrationSqlInput(inputPath);
|
|
1393
237
|
}
|
|
1394
|
-
function
|
|
1395
|
-
const
|
|
1396
|
-
return
|
|
238
|
+
async function createSchemaValidationError(message) {
|
|
239
|
+
const core = await loadCore();
|
|
240
|
+
return new core.SchemaValidationError(message);
|
|
1397
241
|
}
|
|
1398
|
-
function
|
|
1399
|
-
const
|
|
1400
|
-
return
|
|
1401
|
-
(constraintName) => `ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS ${constraintName};`
|
|
1402
|
-
).join("\n");
|
|
242
|
+
async function isSchemaValidationError(error2) {
|
|
243
|
+
const core = await loadCore();
|
|
244
|
+
return error2 instanceof core.SchemaValidationError;
|
|
1403
245
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
246
|
+
|
|
247
|
+
// src/utils/output.ts
|
|
248
|
+
var import_boxen = __toESM(require("boxen"));
|
|
249
|
+
var import_chalk = require("chalk");
|
|
250
|
+
var isInteractive = Boolean(process.stdout?.isTTY);
|
|
251
|
+
var colorsEnabled = isInteractive && process.env.FORCE_COLOR !== "0" && !("NO_COLOR" in process.env);
|
|
252
|
+
var color = new import_chalk.Chalk({ level: colorsEnabled ? 3 : 0 });
|
|
253
|
+
var theme = {
|
|
254
|
+
primary: color.cyanBright,
|
|
255
|
+
success: color.hex("#00FF88"),
|
|
256
|
+
warning: color.hex("#FFD166"),
|
|
257
|
+
error: color.hex("#EF476F"),
|
|
258
|
+
accent: color.magentaBright
|
|
259
|
+
};
|
|
260
|
+
function success(message) {
|
|
261
|
+
const text = theme.success(`[OK] ${message}`);
|
|
262
|
+
if (!isInteractive) {
|
|
263
|
+
console.log(text);
|
|
264
|
+
return;
|
|
1407
265
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
266
|
+
try {
|
|
267
|
+
console.log(
|
|
268
|
+
(0, import_boxen.default)(text, {
|
|
269
|
+
padding: 1,
|
|
270
|
+
borderColor: "cyan",
|
|
271
|
+
borderStyle: "round"
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
} catch {
|
|
275
|
+
console.log(text);
|
|
1413
276
|
}
|
|
1414
|
-
|
|
277
|
+
}
|
|
278
|
+
function info(message) {
|
|
279
|
+
console.log(theme.primary(message));
|
|
280
|
+
}
|
|
281
|
+
function warning(message) {
|
|
282
|
+
console.warn(theme.warning(`[WARN] ${message}`));
|
|
283
|
+
}
|
|
284
|
+
function error(message) {
|
|
285
|
+
console.error(theme.error(`[ERROR] ${message}`));
|
|
1415
286
|
}
|
|
1416
287
|
|
|
1417
288
|
// src/commands/diff.ts
|
|
1418
289
|
var REQUIRED_CONFIG_FIELDS = ["schemaFile", "stateFile"];
|
|
1419
290
|
function resolveConfigPath(root, targetPath) {
|
|
1420
|
-
return
|
|
291
|
+
return import_path3.default.isAbsolute(targetPath) ? targetPath : import_path3.default.join(root, targetPath);
|
|
1421
292
|
}
|
|
1422
293
|
async function runDiff() {
|
|
1423
294
|
const root = getProjectRoot();
|
|
@@ -1434,33 +305,30 @@ async function runDiff() {
|
|
|
1434
305
|
}
|
|
1435
306
|
const schemaPath = resolveConfigPath(root, config.schemaFile);
|
|
1436
307
|
const statePath = resolveConfigPath(root, config.stateFile);
|
|
1437
|
-
|
|
1438
|
-
throw new Error(`Unsupported provider '${config.provider}'.`);
|
|
1439
|
-
}
|
|
1440
|
-
const provider = config.provider ?? "postgres";
|
|
308
|
+
const { provider } = resolveProvider(config.provider);
|
|
1441
309
|
const schemaSource = await readTextFile(schemaPath);
|
|
1442
|
-
const schema = parseSchema(schemaSource);
|
|
310
|
+
const schema = await parseSchema(schemaSource);
|
|
1443
311
|
try {
|
|
1444
|
-
validateSchema(schema);
|
|
312
|
+
await validateSchema(schema);
|
|
1445
313
|
} catch (error2) {
|
|
1446
314
|
if (error2 instanceof Error) {
|
|
1447
|
-
throw
|
|
315
|
+
throw await createSchemaValidationError(error2.message);
|
|
1448
316
|
}
|
|
1449
317
|
throw error2;
|
|
1450
318
|
}
|
|
1451
319
|
const previousState = await loadState(statePath);
|
|
1452
|
-
const diff = diffSchemas(previousState, schema);
|
|
320
|
+
const diff = await diffSchemas(previousState, schema);
|
|
1453
321
|
if (diff.operations.length === 0) {
|
|
1454
322
|
success("No changes detected");
|
|
1455
323
|
return;
|
|
1456
324
|
}
|
|
1457
|
-
const sql = generateSql(diff, provider, config.sql);
|
|
325
|
+
const sql = await generateSql(diff, provider, config.sql);
|
|
1458
326
|
console.log(sql);
|
|
1459
327
|
}
|
|
1460
328
|
|
|
1461
329
|
// src/commands/generate.ts
|
|
1462
330
|
var import_commander2 = require("commander");
|
|
1463
|
-
var
|
|
331
|
+
var import_path4 = __toESM(require("path"));
|
|
1464
332
|
|
|
1465
333
|
// src/core/utils.ts
|
|
1466
334
|
function nowTimestamp() {
|
|
@@ -1479,7 +347,7 @@ var REQUIRED_CONFIG_FIELDS2 = [
|
|
|
1479
347
|
"outputDir"
|
|
1480
348
|
];
|
|
1481
349
|
function resolveConfigPath2(root, targetPath) {
|
|
1482
|
-
return
|
|
350
|
+
return import_path4.default.isAbsolute(targetPath) ? targetPath : import_path4.default.join(root, targetPath);
|
|
1483
351
|
}
|
|
1484
352
|
async function runGenerate(options) {
|
|
1485
353
|
const root = getProjectRoot();
|
|
@@ -1497,36 +365,33 @@ async function runGenerate(options) {
|
|
|
1497
365
|
const schemaPath = resolveConfigPath2(root, config.schemaFile);
|
|
1498
366
|
const statePath = resolveConfigPath2(root, config.stateFile);
|
|
1499
367
|
const outputDir = resolveConfigPath2(root, config.outputDir);
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
}
|
|
1503
|
-
const provider = config.provider ?? "postgres";
|
|
1504
|
-
if (!config.provider) {
|
|
368
|
+
const { provider, usedDefault } = resolveProvider(config.provider);
|
|
369
|
+
if (usedDefault) {
|
|
1505
370
|
info("Provider not set; defaulting to postgres.");
|
|
1506
371
|
}
|
|
1507
372
|
info("Generating SQL...");
|
|
1508
373
|
const schemaSource = await readTextFile(schemaPath);
|
|
1509
|
-
const schema = parseSchema(schemaSource);
|
|
374
|
+
const schema = await parseSchema(schemaSource);
|
|
1510
375
|
try {
|
|
1511
|
-
validateSchema(schema);
|
|
376
|
+
await validateSchema(schema);
|
|
1512
377
|
} catch (error2) {
|
|
1513
378
|
if (error2 instanceof Error) {
|
|
1514
|
-
throw
|
|
379
|
+
throw await createSchemaValidationError(error2.message);
|
|
1515
380
|
}
|
|
1516
381
|
throw error2;
|
|
1517
382
|
}
|
|
1518
383
|
const previousState = await loadState(statePath);
|
|
1519
|
-
const diff = diffSchemas(previousState, schema);
|
|
384
|
+
const diff = await diffSchemas(previousState, schema);
|
|
1520
385
|
if (diff.operations.length === 0) {
|
|
1521
386
|
info("No changes detected");
|
|
1522
387
|
return;
|
|
1523
388
|
}
|
|
1524
|
-
const sql = generateSql(diff, provider, config.sql);
|
|
389
|
+
const sql = await generateSql(diff, provider, config.sql);
|
|
1525
390
|
const timestamp = nowTimestamp();
|
|
1526
391
|
const slug = slugifyName(options.name ?? "migration");
|
|
1527
392
|
const fileName = `${timestamp}-${slug}.sql`;
|
|
1528
393
|
await ensureDir(outputDir);
|
|
1529
|
-
const migrationPath =
|
|
394
|
+
const migrationPath = import_path4.default.join(outputDir, fileName);
|
|
1530
395
|
await writeTextFile(migrationPath, sql + "\n");
|
|
1531
396
|
const nextState = await schemaToState(schema);
|
|
1532
397
|
await saveState(statePath, nextState);
|
|
@@ -1535,840 +400,9 @@ async function runGenerate(options) {
|
|
|
1535
400
|
|
|
1536
401
|
// src/commands/import.ts
|
|
1537
402
|
var import_commander3 = require("commander");
|
|
1538
|
-
var
|
|
1539
|
-
|
|
1540
|
-
// src/core/sql/apply-ops.ts
|
|
1541
|
-
function toSchemaColumn(column) {
|
|
1542
|
-
return {
|
|
1543
|
-
name: column.name,
|
|
1544
|
-
type: column.type,
|
|
1545
|
-
nullable: column.nullable,
|
|
1546
|
-
...column.default !== void 0 ? { default: column.default } : {},
|
|
1547
|
-
...column.unique !== void 0 ? { unique: column.unique } : {},
|
|
1548
|
-
...column.primaryKey !== void 0 ? { primaryKey: column.primaryKey } : {}
|
|
1549
|
-
};
|
|
1550
|
-
}
|
|
1551
|
-
function applySingleColumnConstraint(table, constraint) {
|
|
1552
|
-
if (constraint.columns.length !== 1) {
|
|
1553
|
-
return false;
|
|
1554
|
-
}
|
|
1555
|
-
const targetColumn = table.columns.find((column) => column.name === constraint.columns[0]);
|
|
1556
|
-
if (!targetColumn) {
|
|
1557
|
-
return false;
|
|
1558
|
-
}
|
|
1559
|
-
if (constraint.type === "PRIMARY_KEY") {
|
|
1560
|
-
table.primaryKey = targetColumn.name;
|
|
1561
|
-
targetColumn.primaryKey = true;
|
|
1562
|
-
targetColumn.nullable = false;
|
|
1563
|
-
return true;
|
|
1564
|
-
}
|
|
1565
|
-
targetColumn.unique = true;
|
|
1566
|
-
return true;
|
|
1567
|
-
}
|
|
1568
|
-
function clearConstraintByName(table, name) {
|
|
1569
|
-
if (name.endsWith("_pkey") || name.startsWith("pk_")) {
|
|
1570
|
-
if (table.primaryKey) {
|
|
1571
|
-
const pkColumn = table.columns.find((column) => column.name === table.primaryKey);
|
|
1572
|
-
if (pkColumn) {
|
|
1573
|
-
pkColumn.primaryKey = false;
|
|
1574
|
-
}
|
|
1575
|
-
table.primaryKey = null;
|
|
1576
|
-
}
|
|
1577
|
-
return;
|
|
1578
|
-
}
|
|
1579
|
-
if (name.endsWith("_key") || name.startsWith("uq_")) {
|
|
1580
|
-
for (const column of table.columns) {
|
|
1581
|
-
if (column.unique) {
|
|
1582
|
-
column.unique = false;
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
function getOrCreateTable(tables, name) {
|
|
1588
|
-
if (!tables[name]) {
|
|
1589
|
-
tables[name] = { name, columns: [] };
|
|
1590
|
-
}
|
|
1591
|
-
return tables[name];
|
|
1592
|
-
}
|
|
1593
|
-
function applySqlOps(ops) {
|
|
1594
|
-
const tables = {};
|
|
1595
|
-
const warnings = [];
|
|
1596
|
-
for (const op of ops) {
|
|
1597
|
-
switch (op.kind) {
|
|
1598
|
-
case "CREATE_TABLE": {
|
|
1599
|
-
const table = {
|
|
1600
|
-
name: op.table,
|
|
1601
|
-
columns: op.columns.map(toSchemaColumn)
|
|
1602
|
-
};
|
|
1603
|
-
for (const column of table.columns) {
|
|
1604
|
-
if (column.primaryKey) {
|
|
1605
|
-
table.primaryKey = column.name;
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
for (const constraint of op.constraints) {
|
|
1609
|
-
const applied = applySingleColumnConstraint(table, constraint);
|
|
1610
|
-
if (!applied) {
|
|
1611
|
-
warnings.push({
|
|
1612
|
-
statement: `CREATE TABLE ${op.table}`,
|
|
1613
|
-
reason: `Constraint ${constraint.type}${constraint.name ? ` (${constraint.name})` : ""} is unsupported for schema reconstruction`
|
|
1614
|
-
});
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
tables[op.table] = table;
|
|
1618
|
-
break;
|
|
1619
|
-
}
|
|
1620
|
-
case "ADD_COLUMN": {
|
|
1621
|
-
const table = getOrCreateTable(tables, op.table);
|
|
1622
|
-
table.columns = table.columns.filter((column) => column.name !== op.column.name);
|
|
1623
|
-
table.columns.push(toSchemaColumn(op.column));
|
|
1624
|
-
if (op.column.primaryKey) {
|
|
1625
|
-
table.primaryKey = op.column.name;
|
|
1626
|
-
}
|
|
1627
|
-
break;
|
|
1628
|
-
}
|
|
1629
|
-
case "ALTER_COLUMN_TYPE": {
|
|
1630
|
-
const table = tables[op.table];
|
|
1631
|
-
if (!table) {
|
|
1632
|
-
break;
|
|
1633
|
-
}
|
|
1634
|
-
const column = table.columns.find((item) => item.name === op.column);
|
|
1635
|
-
if (column) {
|
|
1636
|
-
column.type = op.toType;
|
|
1637
|
-
}
|
|
1638
|
-
break;
|
|
1639
|
-
}
|
|
1640
|
-
case "SET_NOT_NULL": {
|
|
1641
|
-
const table = tables[op.table];
|
|
1642
|
-
const column = table?.columns.find((item) => item.name === op.column);
|
|
1643
|
-
if (column) {
|
|
1644
|
-
column.nullable = false;
|
|
1645
|
-
}
|
|
1646
|
-
break;
|
|
1647
|
-
}
|
|
1648
|
-
case "DROP_NOT_NULL": {
|
|
1649
|
-
const table = tables[op.table];
|
|
1650
|
-
const column = table?.columns.find((item) => item.name === op.column);
|
|
1651
|
-
if (column) {
|
|
1652
|
-
column.nullable = true;
|
|
1653
|
-
}
|
|
1654
|
-
break;
|
|
1655
|
-
}
|
|
1656
|
-
case "SET_DEFAULT": {
|
|
1657
|
-
const table = tables[op.table];
|
|
1658
|
-
const column = table?.columns.find((item) => item.name === op.column);
|
|
1659
|
-
if (column) {
|
|
1660
|
-
column.default = op.expr;
|
|
1661
|
-
}
|
|
1662
|
-
break;
|
|
1663
|
-
}
|
|
1664
|
-
case "DROP_DEFAULT": {
|
|
1665
|
-
const table = tables[op.table];
|
|
1666
|
-
const column = table?.columns.find((item) => item.name === op.column);
|
|
1667
|
-
if (column) {
|
|
1668
|
-
column.default = null;
|
|
1669
|
-
}
|
|
1670
|
-
break;
|
|
1671
|
-
}
|
|
1672
|
-
case "ADD_CONSTRAINT": {
|
|
1673
|
-
const table = tables[op.table];
|
|
1674
|
-
if (!table) {
|
|
1675
|
-
break;
|
|
1676
|
-
}
|
|
1677
|
-
const applied = applySingleColumnConstraint(table, op.constraint);
|
|
1678
|
-
if (!applied) {
|
|
1679
|
-
warnings.push({
|
|
1680
|
-
statement: `ALTER TABLE ${op.table} ADD CONSTRAINT ${op.constraint.name ?? "<unnamed>"}`,
|
|
1681
|
-
reason: `Constraint ${op.constraint.type} is unsupported for schema reconstruction`
|
|
1682
|
-
});
|
|
1683
|
-
}
|
|
1684
|
-
break;
|
|
1685
|
-
}
|
|
1686
|
-
case "DROP_CONSTRAINT": {
|
|
1687
|
-
const table = tables[op.table];
|
|
1688
|
-
if (!table) {
|
|
1689
|
-
break;
|
|
1690
|
-
}
|
|
1691
|
-
clearConstraintByName(table, op.name);
|
|
1692
|
-
break;
|
|
1693
|
-
}
|
|
1694
|
-
case "DROP_COLUMN": {
|
|
1695
|
-
const table = tables[op.table];
|
|
1696
|
-
if (!table) {
|
|
1697
|
-
break;
|
|
1698
|
-
}
|
|
1699
|
-
table.columns = table.columns.filter((column) => column.name !== op.column);
|
|
1700
|
-
if (table.primaryKey === op.column) {
|
|
1701
|
-
table.primaryKey = null;
|
|
1702
|
-
}
|
|
1703
|
-
break;
|
|
1704
|
-
}
|
|
1705
|
-
case "DROP_TABLE": {
|
|
1706
|
-
delete tables[op.table];
|
|
1707
|
-
break;
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
const schema = { tables };
|
|
1712
|
-
return { schema, warnings };
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
// src/core/sql/load-migrations.ts
|
|
1716
|
-
var import_fs6 = require("fs");
|
|
1717
|
-
var import_path6 = __toESM(require("path"));
|
|
1718
|
-
async function loadMigrationSqlInput(inputPath) {
|
|
1719
|
-
const stats = await import_fs6.promises.stat(inputPath);
|
|
1720
|
-
if (stats.isFile()) {
|
|
1721
|
-
if (!inputPath.toLowerCase().endsWith(".sql")) {
|
|
1722
|
-
throw new Error(`Input file must be a .sql file: ${inputPath}`);
|
|
1723
|
-
}
|
|
1724
|
-
return [{ filePath: inputPath, sql: await readTextFile(inputPath) }];
|
|
1725
|
-
}
|
|
1726
|
-
if (!stats.isDirectory()) {
|
|
1727
|
-
throw new Error(`Input path must be a .sql file or directory: ${inputPath}`);
|
|
1728
|
-
}
|
|
1729
|
-
const sqlFiles = await findFiles(inputPath, /\.sql$/i);
|
|
1730
|
-
sqlFiles.sort((left, right) => import_path6.default.basename(left).localeCompare(import_path6.default.basename(right)));
|
|
1731
|
-
const result = [];
|
|
1732
|
-
for (const filePath of sqlFiles) {
|
|
1733
|
-
result.push({
|
|
1734
|
-
filePath,
|
|
1735
|
-
sql: await readTextFile(filePath)
|
|
1736
|
-
});
|
|
1737
|
-
}
|
|
1738
|
-
return result;
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
// src/core/sql/split-statements.ts
|
|
1742
|
-
function splitSqlStatements(sql) {
|
|
1743
|
-
const statements = [];
|
|
1744
|
-
let current = "";
|
|
1745
|
-
let inSingleQuote = false;
|
|
1746
|
-
let inDoubleQuote = false;
|
|
1747
|
-
let inLineComment = false;
|
|
1748
|
-
let inBlockComment = false;
|
|
1749
|
-
let dollarTag = null;
|
|
1750
|
-
let index = 0;
|
|
1751
|
-
while (index < sql.length) {
|
|
1752
|
-
const char = sql[index];
|
|
1753
|
-
const next = index + 1 < sql.length ? sql[index + 1] : "";
|
|
1754
|
-
if (inLineComment) {
|
|
1755
|
-
current += char;
|
|
1756
|
-
if (char === "\n") {
|
|
1757
|
-
inLineComment = false;
|
|
1758
|
-
}
|
|
1759
|
-
index++;
|
|
1760
|
-
continue;
|
|
1761
|
-
}
|
|
1762
|
-
if (inBlockComment) {
|
|
1763
|
-
current += char;
|
|
1764
|
-
if (char === "*" && next === "/") {
|
|
1765
|
-
current += next;
|
|
1766
|
-
inBlockComment = false;
|
|
1767
|
-
index += 2;
|
|
1768
|
-
continue;
|
|
1769
|
-
}
|
|
1770
|
-
index++;
|
|
1771
|
-
continue;
|
|
1772
|
-
}
|
|
1773
|
-
if (!inSingleQuote && !inDoubleQuote && dollarTag === null) {
|
|
1774
|
-
if (char === "-" && next === "-") {
|
|
1775
|
-
current += char + next;
|
|
1776
|
-
inLineComment = true;
|
|
1777
|
-
index += 2;
|
|
1778
|
-
continue;
|
|
1779
|
-
}
|
|
1780
|
-
if (char === "/" && next === "*") {
|
|
1781
|
-
current += char + next;
|
|
1782
|
-
inBlockComment = true;
|
|
1783
|
-
index += 2;
|
|
1784
|
-
continue;
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
if (!inDoubleQuote && dollarTag === null && char === "'") {
|
|
1788
|
-
current += char;
|
|
1789
|
-
if (inSingleQuote && next === "'") {
|
|
1790
|
-
current += next;
|
|
1791
|
-
index += 2;
|
|
1792
|
-
continue;
|
|
1793
|
-
}
|
|
1794
|
-
inSingleQuote = !inSingleQuote;
|
|
1795
|
-
index++;
|
|
1796
|
-
continue;
|
|
1797
|
-
}
|
|
1798
|
-
if (!inSingleQuote && dollarTag === null && char === '"') {
|
|
1799
|
-
current += char;
|
|
1800
|
-
if (inDoubleQuote && next === '"') {
|
|
1801
|
-
current += next;
|
|
1802
|
-
index += 2;
|
|
1803
|
-
continue;
|
|
1804
|
-
}
|
|
1805
|
-
inDoubleQuote = !inDoubleQuote;
|
|
1806
|
-
index++;
|
|
1807
|
-
continue;
|
|
1808
|
-
}
|
|
1809
|
-
if (!inSingleQuote && !inDoubleQuote) {
|
|
1810
|
-
if (dollarTag === null && char === "$") {
|
|
1811
|
-
const remainder = sql.slice(index);
|
|
1812
|
-
const match = remainder.match(/^\$[a-zA-Z_][a-zA-Z0-9_]*\$|^\$\$/);
|
|
1813
|
-
if (match) {
|
|
1814
|
-
dollarTag = match[0];
|
|
1815
|
-
current += match[0];
|
|
1816
|
-
index += match[0].length;
|
|
1817
|
-
continue;
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
if (dollarTag !== null && sql.startsWith(dollarTag, index)) {
|
|
1821
|
-
current += dollarTag;
|
|
1822
|
-
index += dollarTag.length;
|
|
1823
|
-
dollarTag = null;
|
|
1824
|
-
continue;
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
if (!inSingleQuote && !inDoubleQuote && dollarTag === null && char === ";") {
|
|
1828
|
-
const statement = current.trim();
|
|
1829
|
-
if (statement.length > 0) {
|
|
1830
|
-
statements.push(statement);
|
|
1831
|
-
}
|
|
1832
|
-
current = "";
|
|
1833
|
-
index++;
|
|
1834
|
-
continue;
|
|
1835
|
-
}
|
|
1836
|
-
current += char;
|
|
1837
|
-
index++;
|
|
1838
|
-
}
|
|
1839
|
-
const tail = current.trim();
|
|
1840
|
-
if (tail.length > 0) {
|
|
1841
|
-
statements.push(tail);
|
|
1842
|
-
}
|
|
1843
|
-
return statements;
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
// src/core/sql/parse-migration.ts
|
|
1847
|
-
var COLUMN_CONSTRAINT_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1848
|
-
"primary",
|
|
1849
|
-
"unique",
|
|
1850
|
-
"not",
|
|
1851
|
-
"null",
|
|
1852
|
-
"default",
|
|
1853
|
-
"constraint",
|
|
1854
|
-
"references",
|
|
1855
|
-
"check"
|
|
1856
|
-
]);
|
|
1857
|
-
function normalizeSqlType(type) {
|
|
1858
|
-
return type.trim().toLowerCase().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
1859
|
-
}
|
|
1860
|
-
function unquoteIdentifier(value) {
|
|
1861
|
-
const trimmed = value.trim();
|
|
1862
|
-
if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
|
|
1863
|
-
return trimmed.slice(1, -1).replace(/""/g, '"');
|
|
1864
|
-
}
|
|
1865
|
-
return trimmed;
|
|
1866
|
-
}
|
|
1867
|
-
function normalizeIdentifier(identifier) {
|
|
1868
|
-
const parts = identifier.trim().split(".").map((part) => unquoteIdentifier(part)).filter((part) => part.length > 0);
|
|
1869
|
-
const leaf = parts.length > 0 ? parts[parts.length - 1] : identifier.trim();
|
|
1870
|
-
return leaf.toLowerCase();
|
|
1871
|
-
}
|
|
1872
|
-
function removeSqlComments(statement) {
|
|
1873
|
-
let result = "";
|
|
1874
|
-
let inSingleQuote = false;
|
|
1875
|
-
let inDoubleQuote = false;
|
|
1876
|
-
let inLineComment = false;
|
|
1877
|
-
let inBlockComment = false;
|
|
1878
|
-
for (let index = 0; index < statement.length; index++) {
|
|
1879
|
-
const char = statement[index];
|
|
1880
|
-
const next = index + 1 < statement.length ? statement[index + 1] : "";
|
|
1881
|
-
if (inLineComment) {
|
|
1882
|
-
if (char === "\n") {
|
|
1883
|
-
inLineComment = false;
|
|
1884
|
-
result += char;
|
|
1885
|
-
}
|
|
1886
|
-
continue;
|
|
1887
|
-
}
|
|
1888
|
-
if (inBlockComment) {
|
|
1889
|
-
if (char === "*" && next === "/") {
|
|
1890
|
-
inBlockComment = false;
|
|
1891
|
-
index++;
|
|
1892
|
-
}
|
|
1893
|
-
continue;
|
|
1894
|
-
}
|
|
1895
|
-
if (!inSingleQuote && !inDoubleQuote) {
|
|
1896
|
-
if (char === "-" && next === "-") {
|
|
1897
|
-
inLineComment = true;
|
|
1898
|
-
index++;
|
|
1899
|
-
continue;
|
|
1900
|
-
}
|
|
1901
|
-
if (char === "/" && next === "*") {
|
|
1902
|
-
inBlockComment = true;
|
|
1903
|
-
index++;
|
|
1904
|
-
continue;
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
if (char === "'" && !inDoubleQuote) {
|
|
1908
|
-
if (inSingleQuote && next === "'") {
|
|
1909
|
-
result += "''";
|
|
1910
|
-
index++;
|
|
1911
|
-
continue;
|
|
1912
|
-
}
|
|
1913
|
-
inSingleQuote = !inSingleQuote;
|
|
1914
|
-
result += char;
|
|
1915
|
-
continue;
|
|
1916
|
-
}
|
|
1917
|
-
if (char === '"' && !inSingleQuote) {
|
|
1918
|
-
if (inDoubleQuote && next === '"') {
|
|
1919
|
-
result += '""';
|
|
1920
|
-
index++;
|
|
1921
|
-
continue;
|
|
1922
|
-
}
|
|
1923
|
-
inDoubleQuote = !inDoubleQuote;
|
|
1924
|
-
result += char;
|
|
1925
|
-
continue;
|
|
1926
|
-
}
|
|
1927
|
-
result += char;
|
|
1928
|
-
}
|
|
1929
|
-
return result.trim();
|
|
1930
|
-
}
|
|
1931
|
-
function splitTopLevelComma(input) {
|
|
1932
|
-
const parts = [];
|
|
1933
|
-
let current = "";
|
|
1934
|
-
let depth = 0;
|
|
1935
|
-
let inSingleQuote = false;
|
|
1936
|
-
let inDoubleQuote = false;
|
|
1937
|
-
for (let index = 0; index < input.length; index++) {
|
|
1938
|
-
const char = input[index];
|
|
1939
|
-
const next = index + 1 < input.length ? input[index + 1] : "";
|
|
1940
|
-
if (char === "'" && !inDoubleQuote) {
|
|
1941
|
-
current += char;
|
|
1942
|
-
if (inSingleQuote && next === "'") {
|
|
1943
|
-
current += next;
|
|
1944
|
-
index++;
|
|
1945
|
-
continue;
|
|
1946
|
-
}
|
|
1947
|
-
inSingleQuote = !inSingleQuote;
|
|
1948
|
-
continue;
|
|
1949
|
-
}
|
|
1950
|
-
if (char === '"' && !inSingleQuote) {
|
|
1951
|
-
current += char;
|
|
1952
|
-
if (inDoubleQuote && next === '"') {
|
|
1953
|
-
current += next;
|
|
1954
|
-
index++;
|
|
1955
|
-
continue;
|
|
1956
|
-
}
|
|
1957
|
-
inDoubleQuote = !inDoubleQuote;
|
|
1958
|
-
continue;
|
|
1959
|
-
}
|
|
1960
|
-
if (!inSingleQuote && !inDoubleQuote) {
|
|
1961
|
-
if (char === "(") {
|
|
1962
|
-
depth++;
|
|
1963
|
-
} else if (char === ")") {
|
|
1964
|
-
depth = Math.max(0, depth - 1);
|
|
1965
|
-
} else if (char === "," && depth === 0) {
|
|
1966
|
-
const segment = current.trim();
|
|
1967
|
-
if (segment.length > 0) {
|
|
1968
|
-
parts.push(segment);
|
|
1969
|
-
}
|
|
1970
|
-
current = "";
|
|
1971
|
-
continue;
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
current += char;
|
|
1975
|
-
}
|
|
1976
|
-
const tail = current.trim();
|
|
1977
|
-
if (tail.length > 0) {
|
|
1978
|
-
parts.push(tail);
|
|
1979
|
-
}
|
|
1980
|
-
return parts;
|
|
1981
|
-
}
|
|
1982
|
-
function tokenize(segment) {
|
|
1983
|
-
const tokens = [];
|
|
1984
|
-
let current = "";
|
|
1985
|
-
let depth = 0;
|
|
1986
|
-
let inSingleQuote = false;
|
|
1987
|
-
let inDoubleQuote = false;
|
|
1988
|
-
for (let index = 0; index < segment.length; index++) {
|
|
1989
|
-
const char = segment[index];
|
|
1990
|
-
const next = index + 1 < segment.length ? segment[index + 1] : "";
|
|
1991
|
-
if (char === "'" && !inDoubleQuote) {
|
|
1992
|
-
current += char;
|
|
1993
|
-
if (inSingleQuote && next === "'") {
|
|
1994
|
-
current += next;
|
|
1995
|
-
index++;
|
|
1996
|
-
continue;
|
|
1997
|
-
}
|
|
1998
|
-
inSingleQuote = !inSingleQuote;
|
|
1999
|
-
continue;
|
|
2000
|
-
}
|
|
2001
|
-
if (char === '"' && !inSingleQuote) {
|
|
2002
|
-
current += char;
|
|
2003
|
-
if (inDoubleQuote && next === '"') {
|
|
2004
|
-
current += next;
|
|
2005
|
-
index++;
|
|
2006
|
-
continue;
|
|
2007
|
-
}
|
|
2008
|
-
inDoubleQuote = !inDoubleQuote;
|
|
2009
|
-
continue;
|
|
2010
|
-
}
|
|
2011
|
-
if (!inSingleQuote && !inDoubleQuote) {
|
|
2012
|
-
if (char === "(") {
|
|
2013
|
-
depth++;
|
|
2014
|
-
} else if (char === ")") {
|
|
2015
|
-
depth = Math.max(0, depth - 1);
|
|
2016
|
-
}
|
|
2017
|
-
if (/\s/.test(char) && depth === 0) {
|
|
2018
|
-
if (current.length > 0) {
|
|
2019
|
-
tokens.push(current);
|
|
2020
|
-
current = "";
|
|
2021
|
-
}
|
|
2022
|
-
continue;
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
current += char;
|
|
2026
|
-
}
|
|
2027
|
-
if (current.length > 0) {
|
|
2028
|
-
tokens.push(current);
|
|
2029
|
-
}
|
|
2030
|
-
return tokens;
|
|
2031
|
-
}
|
|
2032
|
-
function parseColumnDefinition(segment) {
|
|
2033
|
-
const tokens = tokenize(segment);
|
|
2034
|
-
if (tokens.length < 2) {
|
|
2035
|
-
return null;
|
|
2036
|
-
}
|
|
2037
|
-
const name = normalizeIdentifier(tokens[0]);
|
|
2038
|
-
let cursor = 1;
|
|
2039
|
-
const typeTokens = [];
|
|
2040
|
-
while (cursor < tokens.length) {
|
|
2041
|
-
const lower = tokens[cursor].toLowerCase();
|
|
2042
|
-
if (COLUMN_CONSTRAINT_KEYWORDS.has(lower)) {
|
|
2043
|
-
break;
|
|
2044
|
-
}
|
|
2045
|
-
typeTokens.push(tokens[cursor]);
|
|
2046
|
-
cursor++;
|
|
2047
|
-
}
|
|
2048
|
-
if (typeTokens.length === 0) {
|
|
2049
|
-
return null;
|
|
2050
|
-
}
|
|
2051
|
-
const parsed = {
|
|
2052
|
-
name,
|
|
2053
|
-
type: normalizeSqlType(typeTokens.join(" ")),
|
|
2054
|
-
nullable: true
|
|
2055
|
-
};
|
|
2056
|
-
while (cursor < tokens.length) {
|
|
2057
|
-
const lower = tokens[cursor].toLowerCase();
|
|
2058
|
-
if (lower === "primary" && tokens[cursor + 1]?.toLowerCase() === "key") {
|
|
2059
|
-
parsed.primaryKey = true;
|
|
2060
|
-
parsed.nullable = false;
|
|
2061
|
-
cursor += 2;
|
|
2062
|
-
continue;
|
|
2063
|
-
}
|
|
2064
|
-
if (lower === "unique") {
|
|
2065
|
-
parsed.unique = true;
|
|
2066
|
-
cursor++;
|
|
2067
|
-
continue;
|
|
2068
|
-
}
|
|
2069
|
-
if (lower === "not" && tokens[cursor + 1]?.toLowerCase() === "null") {
|
|
2070
|
-
parsed.nullable = false;
|
|
2071
|
-
cursor += 2;
|
|
2072
|
-
continue;
|
|
2073
|
-
}
|
|
2074
|
-
if (lower === "null") {
|
|
2075
|
-
parsed.nullable = true;
|
|
2076
|
-
cursor++;
|
|
2077
|
-
continue;
|
|
2078
|
-
}
|
|
2079
|
-
if (lower === "default") {
|
|
2080
|
-
cursor++;
|
|
2081
|
-
const defaultTokens = [];
|
|
2082
|
-
while (cursor < tokens.length) {
|
|
2083
|
-
const probe = tokens[cursor].toLowerCase();
|
|
2084
|
-
if (probe === "constraint" || probe === "references" || probe === "check" || probe === "not" && tokens[cursor + 1]?.toLowerCase() === "null" || probe === "null" || probe === "unique" || probe === "primary" && tokens[cursor + 1]?.toLowerCase() === "key") {
|
|
2085
|
-
break;
|
|
2086
|
-
}
|
|
2087
|
-
defaultTokens.push(tokens[cursor]);
|
|
2088
|
-
cursor++;
|
|
2089
|
-
}
|
|
2090
|
-
parsed.default = normalizeDefault(defaultTokens.join(" "));
|
|
2091
|
-
continue;
|
|
2092
|
-
}
|
|
2093
|
-
cursor++;
|
|
2094
|
-
}
|
|
2095
|
-
return parsed;
|
|
2096
|
-
}
|
|
2097
|
-
function parseCreateTableConstraint(segment) {
|
|
2098
|
-
const normalized = segment.trim().replace(/\s+/g, " ");
|
|
2099
|
-
const constraintMatch = normalized.match(/^constraint\s+([^\s]+)\s+(primary\s+key|unique)\s*\((.+)\)$/i);
|
|
2100
|
-
if (constraintMatch) {
|
|
2101
|
-
const [, rawName, kind, rawColumns] = constraintMatch;
|
|
2102
|
-
const columns = splitTopLevelComma(rawColumns).map((item) => normalizeIdentifier(item));
|
|
2103
|
-
if (kind.toLowerCase().includes("primary")) {
|
|
2104
|
-
return { type: "PRIMARY_KEY", name: normalizeIdentifier(rawName), columns };
|
|
2105
|
-
}
|
|
2106
|
-
return { type: "UNIQUE", name: normalizeIdentifier(rawName), columns };
|
|
2107
|
-
}
|
|
2108
|
-
const barePk = normalized.match(/^primary\s+key\s*\((.+)\)$/i);
|
|
2109
|
-
if (barePk) {
|
|
2110
|
-
const columns = splitTopLevelComma(barePk[1]).map((item) => normalizeIdentifier(item));
|
|
2111
|
-
return { type: "PRIMARY_KEY", columns };
|
|
2112
|
-
}
|
|
2113
|
-
const bareUnique = normalized.match(/^unique\s*\((.+)\)$/i);
|
|
2114
|
-
if (bareUnique) {
|
|
2115
|
-
const columns = splitTopLevelComma(bareUnique[1]).map((item) => normalizeIdentifier(item));
|
|
2116
|
-
return { type: "UNIQUE", columns };
|
|
2117
|
-
}
|
|
2118
|
-
return null;
|
|
2119
|
-
}
|
|
2120
|
-
function parseAlterTablePrefix(stmt) {
|
|
2121
|
-
const match = stmt.match(/^alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?(.+)$/i);
|
|
2122
|
-
if (!match) {
|
|
2123
|
-
return null;
|
|
2124
|
-
}
|
|
2125
|
-
const remainder = match[1].trim();
|
|
2126
|
-
const tokens = tokenize(remainder);
|
|
2127
|
-
if (tokens.length < 2) {
|
|
2128
|
-
return null;
|
|
2129
|
-
}
|
|
2130
|
-
const tableToken = tokens[0];
|
|
2131
|
-
const table = normalizeIdentifier(tableToken);
|
|
2132
|
-
const rest = remainder.slice(tableToken.length).trim();
|
|
2133
|
-
return { table, rest };
|
|
2134
|
-
}
|
|
2135
|
-
function parseCreateTable(stmt) {
|
|
2136
|
-
const match = stmt.match(/^create\s+table\s+(?:if\s+not\s+exists\s+)?(.+?)\s*\((.*)\)$/is);
|
|
2137
|
-
if (!match) {
|
|
2138
|
-
return null;
|
|
2139
|
-
}
|
|
2140
|
-
const table = normalizeIdentifier(match[1]);
|
|
2141
|
-
const body = match[2];
|
|
2142
|
-
const segments = splitTopLevelComma(body);
|
|
2143
|
-
const columns = [];
|
|
2144
|
-
const constraints = [];
|
|
2145
|
-
for (const segment of segments) {
|
|
2146
|
-
const constraint = parseCreateTableConstraint(segment);
|
|
2147
|
-
if (constraint) {
|
|
2148
|
-
constraints.push(constraint);
|
|
2149
|
-
continue;
|
|
2150
|
-
}
|
|
2151
|
-
const column = parseColumnDefinition(segment);
|
|
2152
|
-
if (column) {
|
|
2153
|
-
columns.push(column);
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
return {
|
|
2157
|
-
kind: "CREATE_TABLE",
|
|
2158
|
-
table,
|
|
2159
|
-
columns,
|
|
2160
|
-
constraints
|
|
2161
|
-
};
|
|
2162
|
-
}
|
|
2163
|
-
function parseAlterTableAddColumn(stmt) {
|
|
2164
|
-
const prefix = parseAlterTablePrefix(stmt);
|
|
2165
|
-
if (!prefix) {
|
|
2166
|
-
return null;
|
|
2167
|
-
}
|
|
2168
|
-
const match = prefix.rest.match(/^add\s+column\s+(?:if\s+not\s+exists\s+)?(.+)$/i);
|
|
2169
|
-
if (!match) {
|
|
2170
|
-
return null;
|
|
2171
|
-
}
|
|
2172
|
-
const column = parseColumnDefinition(match[1]);
|
|
2173
|
-
if (!column) {
|
|
2174
|
-
return null;
|
|
2175
|
-
}
|
|
2176
|
-
return { kind: "ADD_COLUMN", table: prefix.table, column };
|
|
2177
|
-
}
|
|
2178
|
-
function parseAlterColumnType(stmt) {
|
|
2179
|
-
const prefix = parseAlterTablePrefix(stmt);
|
|
2180
|
-
if (!prefix) {
|
|
2181
|
-
return null;
|
|
2182
|
-
}
|
|
2183
|
-
const match = prefix.rest.match(/^alter\s+column\s+([^\s]+)\s+type\s+(.+)$/i);
|
|
2184
|
-
if (!match) {
|
|
2185
|
-
return null;
|
|
2186
|
-
}
|
|
2187
|
-
const column = normalizeIdentifier(match[1]);
|
|
2188
|
-
const toType = normalizeSqlType(match[2].replace(/\s+using\s+[\s\S]*$/i, "").trim());
|
|
2189
|
-
return {
|
|
2190
|
-
kind: "ALTER_COLUMN_TYPE",
|
|
2191
|
-
table: prefix.table,
|
|
2192
|
-
column,
|
|
2193
|
-
toType
|
|
2194
|
-
};
|
|
2195
|
-
}
|
|
2196
|
-
function parseSetDropNotNull(stmt) {
|
|
2197
|
-
const prefix = parseAlterTablePrefix(stmt);
|
|
2198
|
-
if (!prefix) {
|
|
2199
|
-
return null;
|
|
2200
|
-
}
|
|
2201
|
-
const setMatch = prefix.rest.match(/^alter\s+column\s+([^\s]+)\s+set\s+not\s+null$/i);
|
|
2202
|
-
if (setMatch) {
|
|
2203
|
-
return {
|
|
2204
|
-
kind: "SET_NOT_NULL",
|
|
2205
|
-
table: prefix.table,
|
|
2206
|
-
column: normalizeIdentifier(setMatch[1])
|
|
2207
|
-
};
|
|
2208
|
-
}
|
|
2209
|
-
const dropMatch = prefix.rest.match(/^alter\s+column\s+([^\s]+)\s+drop\s+not\s+null$/i);
|
|
2210
|
-
if (dropMatch) {
|
|
2211
|
-
return {
|
|
2212
|
-
kind: "DROP_NOT_NULL",
|
|
2213
|
-
table: prefix.table,
|
|
2214
|
-
column: normalizeIdentifier(dropMatch[1])
|
|
2215
|
-
};
|
|
2216
|
-
}
|
|
2217
|
-
return null;
|
|
2218
|
-
}
|
|
2219
|
-
function parseSetDropDefault(stmt) {
|
|
2220
|
-
const prefix = parseAlterTablePrefix(stmt);
|
|
2221
|
-
if (!prefix) {
|
|
2222
|
-
return null;
|
|
2223
|
-
}
|
|
2224
|
-
const setMatch = prefix.rest.match(/^alter\s+column\s+([^\s]+)\s+set\s+default\s+(.+)$/i);
|
|
2225
|
-
if (setMatch) {
|
|
2226
|
-
return {
|
|
2227
|
-
kind: "SET_DEFAULT",
|
|
2228
|
-
table: prefix.table,
|
|
2229
|
-
column: normalizeIdentifier(setMatch[1]),
|
|
2230
|
-
expr: normalizeDefault(setMatch[2].trim()) ?? setMatch[2].trim()
|
|
2231
|
-
};
|
|
2232
|
-
}
|
|
2233
|
-
const dropMatch = prefix.rest.match(/^alter\s+column\s+([^\s]+)\s+drop\s+default$/i);
|
|
2234
|
-
if (dropMatch) {
|
|
2235
|
-
return {
|
|
2236
|
-
kind: "DROP_DEFAULT",
|
|
2237
|
-
table: prefix.table,
|
|
2238
|
-
column: normalizeIdentifier(dropMatch[1])
|
|
2239
|
-
};
|
|
2240
|
-
}
|
|
2241
|
-
return null;
|
|
2242
|
-
}
|
|
2243
|
-
function parseAddDropConstraint(stmt) {
|
|
2244
|
-
const prefix = parseAlterTablePrefix(stmt);
|
|
2245
|
-
if (!prefix) {
|
|
2246
|
-
return null;
|
|
2247
|
-
}
|
|
2248
|
-
const addMatch = prefix.rest.match(/^add\s+constraint\s+([^\s]+)\s+(primary\s+key|unique)\s*\((.+)\)$/i);
|
|
2249
|
-
if (addMatch) {
|
|
2250
|
-
const [, rawName, kind, rawColumns] = addMatch;
|
|
2251
|
-
const columns = splitTopLevelComma(rawColumns).map((item) => normalizeIdentifier(item));
|
|
2252
|
-
const constraint = kind.toLowerCase().includes("primary") ? { type: "PRIMARY_KEY", name: normalizeIdentifier(rawName), columns } : { type: "UNIQUE", name: normalizeIdentifier(rawName), columns };
|
|
2253
|
-
return {
|
|
2254
|
-
kind: "ADD_CONSTRAINT",
|
|
2255
|
-
table: prefix.table,
|
|
2256
|
-
constraint
|
|
2257
|
-
};
|
|
2258
|
-
}
|
|
2259
|
-
const dropMatch = prefix.rest.match(/^drop\s+constraint\s+(?:if\s+exists\s+)?([^\s]+)(?:\s+cascade)?$/i);
|
|
2260
|
-
if (dropMatch) {
|
|
2261
|
-
return {
|
|
2262
|
-
kind: "DROP_CONSTRAINT",
|
|
2263
|
-
table: prefix.table,
|
|
2264
|
-
name: normalizeIdentifier(dropMatch[1])
|
|
2265
|
-
};
|
|
2266
|
-
}
|
|
2267
|
-
return null;
|
|
2268
|
-
}
|
|
2269
|
-
function parseDropColumn(stmt) {
|
|
2270
|
-
const prefix = parseAlterTablePrefix(stmt);
|
|
2271
|
-
if (!prefix) {
|
|
2272
|
-
return null;
|
|
2273
|
-
}
|
|
2274
|
-
const match = prefix.rest.match(/^drop\s+column\s+(?:if\s+exists\s+)?([^\s]+)(?:\s+cascade)?$/i);
|
|
2275
|
-
if (!match) {
|
|
2276
|
-
return null;
|
|
2277
|
-
}
|
|
2278
|
-
return {
|
|
2279
|
-
kind: "DROP_COLUMN",
|
|
2280
|
-
table: prefix.table,
|
|
2281
|
-
column: normalizeIdentifier(match[1])
|
|
2282
|
-
};
|
|
2283
|
-
}
|
|
2284
|
-
function parseDropTable(stmt) {
|
|
2285
|
-
const match = stmt.match(/^drop\s+table\s+(?:if\s+exists\s+)?([^\s]+)(?:\s+cascade)?$/i);
|
|
2286
|
-
if (!match) {
|
|
2287
|
-
return null;
|
|
2288
|
-
}
|
|
2289
|
-
return {
|
|
2290
|
-
kind: "DROP_TABLE",
|
|
2291
|
-
table: normalizeIdentifier(match[1])
|
|
2292
|
-
};
|
|
2293
|
-
}
|
|
2294
|
-
var PARSERS = [
|
|
2295
|
-
parseCreateTable,
|
|
2296
|
-
parseAlterTableAddColumn,
|
|
2297
|
-
parseAlterColumnType,
|
|
2298
|
-
parseSetDropNotNull,
|
|
2299
|
-
parseSetDropDefault,
|
|
2300
|
-
parseAddDropConstraint,
|
|
2301
|
-
parseDropColumn,
|
|
2302
|
-
parseDropTable
|
|
2303
|
-
];
|
|
2304
|
-
function parseMigrationSql(sql) {
|
|
2305
|
-
const statements = splitSqlStatements(sql);
|
|
2306
|
-
const ops = [];
|
|
2307
|
-
const warnings = [];
|
|
2308
|
-
for (const raw of statements) {
|
|
2309
|
-
const stmt = removeSqlComments(raw).trim();
|
|
2310
|
-
if (!stmt) {
|
|
2311
|
-
continue;
|
|
2312
|
-
}
|
|
2313
|
-
let parsed = null;
|
|
2314
|
-
for (const parseFn of PARSERS) {
|
|
2315
|
-
parsed = parseFn(stmt);
|
|
2316
|
-
if (parsed) {
|
|
2317
|
-
break;
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
if (parsed) {
|
|
2321
|
-
ops.push(parsed);
|
|
2322
|
-
} else {
|
|
2323
|
-
warnings.push({
|
|
2324
|
-
statement: stmt,
|
|
2325
|
-
reason: "Unsupported or unrecognized statement"
|
|
2326
|
-
});
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
return { ops, warnings };
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
// src/core/sql/schema-to-dsl.ts
|
|
2333
|
-
function renderColumn(column) {
|
|
2334
|
-
const parts = [column.name, column.type];
|
|
2335
|
-
if (column.primaryKey) {
|
|
2336
|
-
parts.push("pk");
|
|
2337
|
-
}
|
|
2338
|
-
if (column.unique) {
|
|
2339
|
-
parts.push("unique");
|
|
2340
|
-
}
|
|
2341
|
-
if (column.nullable === false && !column.primaryKey) {
|
|
2342
|
-
parts.push("not null");
|
|
2343
|
-
}
|
|
2344
|
-
if (column.default !== void 0 && column.default !== null) {
|
|
2345
|
-
parts.push(`default ${column.default}`);
|
|
2346
|
-
}
|
|
2347
|
-
return ` ${parts.join(" ")}`;
|
|
2348
|
-
}
|
|
2349
|
-
function schemaToDsl(schema) {
|
|
2350
|
-
const tableNames = Object.keys(schema.tables).sort((left, right) => left.localeCompare(right));
|
|
2351
|
-
const blocks = tableNames.map((tableName) => {
|
|
2352
|
-
const table = schema.tables[tableName];
|
|
2353
|
-
const lines = [`table ${table.name} {`];
|
|
2354
|
-
for (const column of table.columns) {
|
|
2355
|
-
lines.push(renderColumn(column));
|
|
2356
|
-
}
|
|
2357
|
-
lines.push("}");
|
|
2358
|
-
return lines.join("\n");
|
|
2359
|
-
});
|
|
2360
|
-
if (blocks.length === 0) {
|
|
2361
|
-
return "# SchemaForge schema definition\n";
|
|
2362
|
-
}
|
|
2363
|
-
return `# SchemaForge schema definition
|
|
2364
|
-
|
|
2365
|
-
${blocks.join("\n\n")}
|
|
2366
|
-
`;
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
// src/commands/import.ts
|
|
403
|
+
var import_path5 = __toESM(require("path"));
|
|
2370
404
|
function resolveConfigPath3(root, targetPath) {
|
|
2371
|
-
return
|
|
405
|
+
return import_path5.default.isAbsolute(targetPath) ? targetPath : import_path5.default.join(root, targetPath);
|
|
2372
406
|
}
|
|
2373
407
|
async function runImport(inputPath, options = {}) {
|
|
2374
408
|
const root = getProjectRoot();
|
|
@@ -2380,15 +414,15 @@ async function runImport(inputPath, options = {}) {
|
|
|
2380
414
|
const allOps = [];
|
|
2381
415
|
const parseWarnings = [];
|
|
2382
416
|
for (const input of inputs) {
|
|
2383
|
-
const result = parseMigrationSql(input.sql);
|
|
417
|
+
const result = await parseMigrationSql(input.sql);
|
|
2384
418
|
allOps.push(...result.ops);
|
|
2385
419
|
parseWarnings.push(...result.warnings.map((item) => ({
|
|
2386
|
-
statement: `[${
|
|
420
|
+
statement: `[${import_path5.default.basename(input.filePath)}] ${item.statement}`,
|
|
2387
421
|
reason: item.reason
|
|
2388
422
|
})));
|
|
2389
423
|
}
|
|
2390
|
-
const applied = applySqlOps(allOps);
|
|
2391
|
-
const dsl = schemaToDsl(applied.schema);
|
|
424
|
+
const applied = await applySqlOps(allOps);
|
|
425
|
+
const dsl = await schemaToDsl(applied.schema);
|
|
2392
426
|
let targetPath = options.out;
|
|
2393
427
|
if (!targetPath) {
|
|
2394
428
|
const configPath = getConfigPath(root);
|
|
@@ -2453,8 +487,8 @@ table users {
|
|
|
2453
487
|
await writeTextFile(schemaFilePath, schemaContent);
|
|
2454
488
|
success(`Created ${schemaFilePath}`);
|
|
2455
489
|
const config = {
|
|
2456
|
-
provider: "
|
|
2457
|
-
outputDir: "
|
|
490
|
+
provider: "postgres",
|
|
491
|
+
outputDir: "migrations",
|
|
2458
492
|
schemaFile: "schemaforge/schema.sf",
|
|
2459
493
|
stateFile: "schemaforge/state.json",
|
|
2460
494
|
sql: {
|
|
@@ -2470,7 +504,7 @@ table users {
|
|
|
2470
504
|
};
|
|
2471
505
|
await writeJsonFile(statePath, state);
|
|
2472
506
|
success(`Created ${statePath}`);
|
|
2473
|
-
const outputDir = "
|
|
507
|
+
const outputDir = "migrations";
|
|
2474
508
|
await ensureDir(outputDir);
|
|
2475
509
|
success(`Created ${outputDir}`);
|
|
2476
510
|
success("Project initialized successfully");
|
|
@@ -2481,160 +515,10 @@ table users {
|
|
|
2481
515
|
|
|
2482
516
|
// src/commands/validate.ts
|
|
2483
517
|
var import_commander5 = require("commander");
|
|
2484
|
-
var
|
|
2485
|
-
|
|
2486
|
-
// src/core/validate.ts
|
|
2487
|
-
function normalizeColumnType2(type) {
|
|
2488
|
-
return type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
2489
|
-
}
|
|
2490
|
-
function parseVarcharLength(type) {
|
|
2491
|
-
const match = normalizeColumnType2(type).match(/^varchar\((\d+)\)$/);
|
|
2492
|
-
return match ? Number(match[1]) : null;
|
|
2493
|
-
}
|
|
2494
|
-
function parseNumericType(type) {
|
|
2495
|
-
const match = normalizeColumnType2(type).match(/^numeric\((\d+),(\d+)\)$/);
|
|
2496
|
-
if (!match) {
|
|
2497
|
-
return null;
|
|
2498
|
-
}
|
|
2499
|
-
return {
|
|
2500
|
-
precision: Number(match[1]),
|
|
2501
|
-
scale: Number(match[2])
|
|
2502
|
-
};
|
|
2503
|
-
}
|
|
2504
|
-
function classifyTypeChange(from, to) {
|
|
2505
|
-
const fromType = normalizeColumnType2(from);
|
|
2506
|
-
const toType = normalizeColumnType2(to);
|
|
2507
|
-
const uuidInvolved = fromType === "uuid" || toType === "uuid";
|
|
2508
|
-
if (uuidInvolved && fromType !== toType) {
|
|
2509
|
-
return {
|
|
2510
|
-
severity: "error",
|
|
2511
|
-
message: `Type changed from ${fromType} to ${toType} (likely incompatible cast)`
|
|
2512
|
-
};
|
|
2513
|
-
}
|
|
2514
|
-
if (fromType === "int" && toType === "bigint") {
|
|
2515
|
-
return {
|
|
2516
|
-
severity: "warning",
|
|
2517
|
-
message: "Type widened from int to bigint"
|
|
2518
|
-
};
|
|
2519
|
-
}
|
|
2520
|
-
if (fromType === "bigint" && toType === "int") {
|
|
2521
|
-
return {
|
|
2522
|
-
severity: "error",
|
|
2523
|
-
message: "Type narrowed from bigint to int (likely incompatible cast)"
|
|
2524
|
-
};
|
|
2525
|
-
}
|
|
2526
|
-
if (fromType === "text" && parseVarcharLength(toType) !== null) {
|
|
2527
|
-
return {
|
|
2528
|
-
severity: "error",
|
|
2529
|
-
message: `Type changed from text to ${toType} (may truncate existing values)`
|
|
2530
|
-
};
|
|
2531
|
-
}
|
|
2532
|
-
if (parseVarcharLength(fromType) !== null && toType === "text") {
|
|
2533
|
-
return {
|
|
2534
|
-
severity: "warning",
|
|
2535
|
-
message: "Type widened from varchar(n) to text"
|
|
2536
|
-
};
|
|
2537
|
-
}
|
|
2538
|
-
const fromVarcharLength = parseVarcharLength(fromType);
|
|
2539
|
-
const toVarcharLength = parseVarcharLength(toType);
|
|
2540
|
-
if (fromVarcharLength !== null && toVarcharLength !== null) {
|
|
2541
|
-
if (toVarcharLength >= fromVarcharLength) {
|
|
2542
|
-
return {
|
|
2543
|
-
severity: "warning",
|
|
2544
|
-
message: `Type widened from varchar(${fromVarcharLength}) to varchar(${toVarcharLength})`
|
|
2545
|
-
};
|
|
2546
|
-
}
|
|
2547
|
-
return {
|
|
2548
|
-
severity: "error",
|
|
2549
|
-
message: `Type narrowed from varchar(${fromVarcharLength}) to varchar(${toVarcharLength})`
|
|
2550
|
-
};
|
|
2551
|
-
}
|
|
2552
|
-
const fromNumeric = parseNumericType(fromType);
|
|
2553
|
-
const toNumeric = parseNumericType(toType);
|
|
2554
|
-
if (fromNumeric && toNumeric && fromNumeric.scale === toNumeric.scale) {
|
|
2555
|
-
if (toNumeric.precision >= fromNumeric.precision) {
|
|
2556
|
-
return {
|
|
2557
|
-
severity: "warning",
|
|
2558
|
-
message: `Type widened from numeric(${fromNumeric.precision},${fromNumeric.scale}) to numeric(${toNumeric.precision},${toNumeric.scale})`
|
|
2559
|
-
};
|
|
2560
|
-
}
|
|
2561
|
-
return {
|
|
2562
|
-
severity: "error",
|
|
2563
|
-
message: `Type narrowed from numeric(${fromNumeric.precision},${fromNumeric.scale}) to numeric(${toNumeric.precision},${toNumeric.scale})`
|
|
2564
|
-
};
|
|
2565
|
-
}
|
|
2566
|
-
return {
|
|
2567
|
-
severity: "warning",
|
|
2568
|
-
message: `Type changed from ${fromType} to ${toType} (compatibility unknown)`
|
|
2569
|
-
};
|
|
2570
|
-
}
|
|
2571
|
-
function validateSchemaChanges(previousState, currentSchema) {
|
|
2572
|
-
const findings = [];
|
|
2573
|
-
const diff = diffSchemas(previousState, currentSchema);
|
|
2574
|
-
for (const operation of diff.operations) {
|
|
2575
|
-
switch (operation.kind) {
|
|
2576
|
-
case "drop_table":
|
|
2577
|
-
findings.push({
|
|
2578
|
-
severity: "error",
|
|
2579
|
-
code: "DROP_TABLE",
|
|
2580
|
-
table: operation.tableName,
|
|
2581
|
-
message: "Table removed"
|
|
2582
|
-
});
|
|
2583
|
-
break;
|
|
2584
|
-
case "drop_column":
|
|
2585
|
-
findings.push({
|
|
2586
|
-
severity: "error",
|
|
2587
|
-
code: "DROP_COLUMN",
|
|
2588
|
-
table: operation.tableName,
|
|
2589
|
-
column: operation.columnName,
|
|
2590
|
-
message: "Column removed"
|
|
2591
|
-
});
|
|
2592
|
-
break;
|
|
2593
|
-
case "column_type_changed": {
|
|
2594
|
-
const classification = classifyTypeChange(operation.fromType, operation.toType);
|
|
2595
|
-
findings.push({
|
|
2596
|
-
severity: classification.severity,
|
|
2597
|
-
code: "ALTER_COLUMN_TYPE",
|
|
2598
|
-
table: operation.tableName,
|
|
2599
|
-
column: operation.columnName,
|
|
2600
|
-
from: normalizeColumnType2(operation.fromType),
|
|
2601
|
-
to: normalizeColumnType2(operation.toType),
|
|
2602
|
-
message: classification.message
|
|
2603
|
-
});
|
|
2604
|
-
break;
|
|
2605
|
-
}
|
|
2606
|
-
case "column_nullability_changed":
|
|
2607
|
-
if (operation.from && !operation.to) {
|
|
2608
|
-
findings.push({
|
|
2609
|
-
severity: "warning",
|
|
2610
|
-
code: "SET_NOT_NULL",
|
|
2611
|
-
table: operation.tableName,
|
|
2612
|
-
column: operation.columnName,
|
|
2613
|
-
message: "Column changed to NOT NULL (may fail if data contains NULLs)"
|
|
2614
|
-
});
|
|
2615
|
-
}
|
|
2616
|
-
break;
|
|
2617
|
-
default:
|
|
2618
|
-
break;
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
return findings;
|
|
2622
|
-
}
|
|
2623
|
-
function toValidationReport(findings) {
|
|
2624
|
-
const errors = findings.filter((finding) => finding.severity === "error");
|
|
2625
|
-
const warnings = findings.filter((finding) => finding.severity === "warning");
|
|
2626
|
-
return {
|
|
2627
|
-
hasErrors: errors.length > 0,
|
|
2628
|
-
hasWarnings: warnings.length > 0,
|
|
2629
|
-
errors: errors.map(({ severity, ...finding }) => finding),
|
|
2630
|
-
warnings: warnings.map(({ severity, ...finding }) => finding)
|
|
2631
|
-
};
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
// src/commands/validate.ts
|
|
518
|
+
var import_path6 = __toESM(require("path"));
|
|
2635
519
|
var REQUIRED_CONFIG_FIELDS3 = ["schemaFile", "stateFile"];
|
|
2636
520
|
function resolveConfigPath4(root, targetPath) {
|
|
2637
|
-
return
|
|
521
|
+
return import_path6.default.isAbsolute(targetPath) ? targetPath : import_path6.default.join(root, targetPath);
|
|
2638
522
|
}
|
|
2639
523
|
async function runValidate(options = {}) {
|
|
2640
524
|
const root = getProjectRoot();
|
|
@@ -2652,18 +536,18 @@ async function runValidate(options = {}) {
|
|
|
2652
536
|
const schemaPath = resolveConfigPath4(root, config.schemaFile);
|
|
2653
537
|
const statePath = resolveConfigPath4(root, config.stateFile);
|
|
2654
538
|
const schemaSource = await readTextFile(schemaPath);
|
|
2655
|
-
const schema = parseSchema(schemaSource);
|
|
539
|
+
const schema = await parseSchema(schemaSource);
|
|
2656
540
|
try {
|
|
2657
|
-
validateSchema(schema);
|
|
541
|
+
await validateSchema(schema);
|
|
2658
542
|
} catch (error2) {
|
|
2659
543
|
if (error2 instanceof Error) {
|
|
2660
|
-
throw
|
|
544
|
+
throw await createSchemaValidationError(error2.message);
|
|
2661
545
|
}
|
|
2662
546
|
throw error2;
|
|
2663
547
|
}
|
|
2664
548
|
const previousState = await loadState(statePath);
|
|
2665
|
-
const findings = validateSchemaChanges(previousState, schema);
|
|
2666
|
-
const report = toValidationReport(findings);
|
|
549
|
+
const findings = await validateSchemaChanges(previousState, schema);
|
|
550
|
+
const report = await toValidationReport(findings);
|
|
2667
551
|
if (options.json) {
|
|
2668
552
|
console.log(JSON.stringify(report, null, 2));
|
|
2669
553
|
process.exitCode = report.hasErrors ? 1 : 0;
|
|
@@ -2694,8 +578,8 @@ async function runValidate(options = {}) {
|
|
|
2694
578
|
// src/cli.ts
|
|
2695
579
|
var program = new import_commander6.Command();
|
|
2696
580
|
program.name("schema-forge").description("CLI tool for schema management and SQL generation").version(package_default.version);
|
|
2697
|
-
function handleError(error2) {
|
|
2698
|
-
if (error2 instanceof
|
|
581
|
+
async function handleError(error2) {
|
|
582
|
+
if (await isSchemaValidationError(error2) && error2 instanceof Error) {
|
|
2699
583
|
error(error2.message);
|
|
2700
584
|
process.exitCode = 2;
|
|
2701
585
|
return;
|
|
@@ -2711,35 +595,35 @@ program.command("init").description("Initialize a new schema project").action(as
|
|
|
2711
595
|
try {
|
|
2712
596
|
await runInit();
|
|
2713
597
|
} catch (error2) {
|
|
2714
|
-
handleError(error2);
|
|
598
|
+
await handleError(error2);
|
|
2715
599
|
}
|
|
2716
600
|
});
|
|
2717
601
|
program.command("generate").description("Generate SQL from schema files").option("--name <string>", "Schema name to generate").action(async (options) => {
|
|
2718
602
|
try {
|
|
2719
603
|
await runGenerate(options);
|
|
2720
604
|
} catch (error2) {
|
|
2721
|
-
handleError(error2);
|
|
605
|
+
await handleError(error2);
|
|
2722
606
|
}
|
|
2723
607
|
});
|
|
2724
608
|
program.command("diff").description("Compare two schema versions and generate migration SQL").action(async () => {
|
|
2725
609
|
try {
|
|
2726
610
|
await runDiff();
|
|
2727
611
|
} catch (error2) {
|
|
2728
|
-
handleError(error2);
|
|
612
|
+
await handleError(error2);
|
|
2729
613
|
}
|
|
2730
614
|
});
|
|
2731
615
|
program.command("import").description("Import schema from SQL migrations").argument("<path>", "Path to .sql file or migrations directory").option("--out <path>", "Output schema file path").action(async (targetPath, options) => {
|
|
2732
616
|
try {
|
|
2733
617
|
await runImport(targetPath, options);
|
|
2734
618
|
} catch (error2) {
|
|
2735
|
-
handleError(error2);
|
|
619
|
+
await handleError(error2);
|
|
2736
620
|
}
|
|
2737
621
|
});
|
|
2738
622
|
program.command("validate").description("Detect destructive or risky schema changes").option("--json", "Output structured JSON").action(async (options) => {
|
|
2739
623
|
try {
|
|
2740
624
|
await runValidate(options);
|
|
2741
625
|
} catch (error2) {
|
|
2742
|
-
handleError(error2);
|
|
626
|
+
await handleError(error2);
|
|
2743
627
|
}
|
|
2744
628
|
});
|
|
2745
629
|
program.parse(process.argv);
|