@xubylele/schema-forge 1.6.1 → 1.8.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/README.md +147 -15
- package/dist/api.d.ts +101 -0
- package/dist/api.js +3623 -0
- package/dist/cli.js +696 -54
- package/package.json +19 -5
package/dist/cli.js
CHANGED
|
@@ -46,11 +46,11 @@ function parseSchema(source) {
|
|
|
46
46
|
"date"
|
|
47
47
|
]);
|
|
48
48
|
const validIdentifierPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
49
|
-
function
|
|
49
|
+
function normalizeColumnType6(type) {
|
|
50
50
|
return type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
51
51
|
}
|
|
52
52
|
function isValidColumnType2(type) {
|
|
53
|
-
const normalizedType =
|
|
53
|
+
const normalizedType = normalizeColumnType6(type);
|
|
54
54
|
if (validBaseColumnTypes.has(normalizedType)) {
|
|
55
55
|
return true;
|
|
56
56
|
}
|
|
@@ -174,7 +174,7 @@ function parseSchema(source) {
|
|
|
174
174
|
throw new Error(`Line ${lineNum}: Invalid column definition. Expected: <name> <type> [modifiers...]`);
|
|
175
175
|
}
|
|
176
176
|
const colName = tokens[0];
|
|
177
|
-
const colType =
|
|
177
|
+
const colType = normalizeColumnType6(tokens[1]);
|
|
178
178
|
validateIdentifier(colName, lineNum, "column");
|
|
179
179
|
if (!isValidColumnType2(colType)) {
|
|
180
180
|
throw new Error(`Line ${lineNum}: Invalid column type '${tokens[1]}'. Valid types: ${Array.from(validBaseColumnTypes).join(", ")}, varchar(n), numeric(p,s)`);
|
|
@@ -691,6 +691,75 @@ var init_diff = __esm({
|
|
|
691
691
|
}
|
|
692
692
|
});
|
|
693
693
|
|
|
694
|
+
// node_modules/@xubylele/schema-forge-core/dist/core/drift-analyzer.js
|
|
695
|
+
function normalizeColumnType2(type) {
|
|
696
|
+
return type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
697
|
+
}
|
|
698
|
+
function getSortedNames2(values) {
|
|
699
|
+
return Array.from(values).sort((left, right) => left.localeCompare(right));
|
|
700
|
+
}
|
|
701
|
+
function analyzeSchemaDrift(state, liveSchema) {
|
|
702
|
+
const stateTableNames = getSortedNames2(Object.keys(state.tables));
|
|
703
|
+
const liveTableNames = getSortedNames2(Object.keys(liveSchema.tables));
|
|
704
|
+
const liveTableNameSet = new Set(liveTableNames);
|
|
705
|
+
const stateTableNameSet = new Set(stateTableNames);
|
|
706
|
+
const missingTables = stateTableNames.filter((tableName) => !liveTableNameSet.has(tableName));
|
|
707
|
+
const extraTables = liveTableNames.filter((tableName) => !stateTableNameSet.has(tableName));
|
|
708
|
+
const commonTableNames = liveTableNames.filter((tableName) => stateTableNameSet.has(tableName));
|
|
709
|
+
const columnDifferences = [];
|
|
710
|
+
const typeMismatches = [];
|
|
711
|
+
for (const tableName of commonTableNames) {
|
|
712
|
+
const stateTable = state.tables[tableName];
|
|
713
|
+
const liveTable = liveSchema.tables[tableName];
|
|
714
|
+
if (!stateTable || !liveTable) {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const stateColumnNames = getSortedNames2(Object.keys(stateTable.columns));
|
|
718
|
+
const liveColumnsByName = new Map(liveTable.columns.map((column) => [column.name, column]));
|
|
719
|
+
const liveColumnNames = getSortedNames2(liveColumnsByName.keys());
|
|
720
|
+
const stateColumnNameSet = new Set(stateColumnNames);
|
|
721
|
+
const liveColumnNameSet = new Set(liveColumnNames);
|
|
722
|
+
const missingInLive = stateColumnNames.filter((columnName) => !liveColumnNameSet.has(columnName));
|
|
723
|
+
const extraInLive = liveColumnNames.filter((columnName) => !stateColumnNameSet.has(columnName));
|
|
724
|
+
if (missingInLive.length > 0 || extraInLive.length > 0) {
|
|
725
|
+
columnDifferences.push({
|
|
726
|
+
tableName,
|
|
727
|
+
missingInLive,
|
|
728
|
+
extraInLive
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
const commonColumns = stateColumnNames.filter((columnName) => liveColumnNameSet.has(columnName));
|
|
732
|
+
for (const columnName of commonColumns) {
|
|
733
|
+
const stateColumn = stateTable.columns[columnName];
|
|
734
|
+
const liveColumn = liveColumnsByName.get(columnName);
|
|
735
|
+
if (!stateColumn || !liveColumn) {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
const expectedType = stateColumn.type;
|
|
739
|
+
const actualType = liveColumn.type;
|
|
740
|
+
if (normalizeColumnType2(expectedType) !== normalizeColumnType2(actualType)) {
|
|
741
|
+
typeMismatches.push({
|
|
742
|
+
tableName,
|
|
743
|
+
columnName,
|
|
744
|
+
expectedType,
|
|
745
|
+
actualType
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
missingTables,
|
|
752
|
+
extraTables,
|
|
753
|
+
columnDifferences,
|
|
754
|
+
typeMismatches
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
var init_drift_analyzer = __esm({
|
|
758
|
+
"node_modules/@xubylele/schema-forge-core/dist/core/drift-analyzer.js"() {
|
|
759
|
+
"use strict";
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
694
763
|
// node_modules/@xubylele/schema-forge-core/dist/core/validator.js
|
|
695
764
|
function isValidColumnType(type) {
|
|
696
765
|
const normalizedType = type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
@@ -778,15 +847,15 @@ var init_validator = __esm({
|
|
|
778
847
|
});
|
|
779
848
|
|
|
780
849
|
// node_modules/@xubylele/schema-forge-core/dist/core/safety/operation-classifier.js
|
|
781
|
-
function
|
|
850
|
+
function normalizeColumnType3(type) {
|
|
782
851
|
return type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
783
852
|
}
|
|
784
853
|
function parseVarcharLength(type) {
|
|
785
|
-
const match =
|
|
854
|
+
const match = normalizeColumnType3(type).match(/^varchar\((\d+)\)$/);
|
|
786
855
|
return match ? Number(match[1]) : null;
|
|
787
856
|
}
|
|
788
857
|
function parseNumericType(type) {
|
|
789
|
-
const match =
|
|
858
|
+
const match = normalizeColumnType3(type).match(/^numeric\((\d+),(\d+)\)$/);
|
|
790
859
|
if (!match) {
|
|
791
860
|
return null;
|
|
792
861
|
}
|
|
@@ -796,8 +865,8 @@ function parseNumericType(type) {
|
|
|
796
865
|
};
|
|
797
866
|
}
|
|
798
867
|
function classifyTypeChange(from, to) {
|
|
799
|
-
const fromType =
|
|
800
|
-
const toType =
|
|
868
|
+
const fromType = normalizeColumnType3(from);
|
|
869
|
+
const toType = normalizeColumnType3(to);
|
|
801
870
|
const uuidInvolved = fromType === "uuid" || toType === "uuid";
|
|
802
871
|
if (uuidInvolved && fromType !== toType) {
|
|
803
872
|
return "DESTRUCTIVE";
|
|
@@ -869,15 +938,15 @@ var init_operation_classifier = __esm({
|
|
|
869
938
|
});
|
|
870
939
|
|
|
871
940
|
// node_modules/@xubylele/schema-forge-core/dist/core/safety/safety-checker.js
|
|
872
|
-
function
|
|
941
|
+
function normalizeColumnType4(type) {
|
|
873
942
|
return type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
874
943
|
}
|
|
875
944
|
function parseVarcharLength2(type) {
|
|
876
|
-
const match =
|
|
945
|
+
const match = normalizeColumnType4(type).match(/^varchar\((\d+)\)$/);
|
|
877
946
|
return match ? Number(match[1]) : null;
|
|
878
947
|
}
|
|
879
948
|
function parseNumericType2(type) {
|
|
880
|
-
const match =
|
|
949
|
+
const match = normalizeColumnType4(type).match(/^numeric\((\d+),(\d+)\)$/);
|
|
881
950
|
if (!match) {
|
|
882
951
|
return null;
|
|
883
952
|
}
|
|
@@ -887,8 +956,8 @@ function parseNumericType2(type) {
|
|
|
887
956
|
};
|
|
888
957
|
}
|
|
889
958
|
function generateTypeChangeMessage(from, to) {
|
|
890
|
-
const fromType =
|
|
891
|
-
const toType =
|
|
959
|
+
const fromType = normalizeColumnType4(from);
|
|
960
|
+
const toType = normalizeColumnType4(to);
|
|
892
961
|
const uuidInvolved = fromType === "uuid" || toType === "uuid";
|
|
893
962
|
if (uuidInvolved && fromType !== toType) {
|
|
894
963
|
return `Type changed from ${fromType} to ${toType} (likely incompatible cast)`;
|
|
@@ -953,8 +1022,8 @@ function checkOperationSafety(operation) {
|
|
|
953
1022
|
code: "ALTER_COLUMN_TYPE",
|
|
954
1023
|
table: operation.tableName,
|
|
955
1024
|
column: operation.columnName,
|
|
956
|
-
from:
|
|
957
|
-
to:
|
|
1025
|
+
from: normalizeColumnType4(operation.fromType),
|
|
1026
|
+
to: normalizeColumnType4(operation.toType),
|
|
958
1027
|
message: generateTypeChangeMessage(operation.fromType, operation.toType),
|
|
959
1028
|
operationKind: operation.kind
|
|
960
1029
|
};
|
|
@@ -2157,6 +2226,359 @@ var init_load_migrations = __esm({
|
|
|
2157
2226
|
}
|
|
2158
2227
|
});
|
|
2159
2228
|
|
|
2229
|
+
// node_modules/@xubylele/schema-forge-core/dist/core/sql/introspect-postgres.js
|
|
2230
|
+
function toTableKey(schema, table) {
|
|
2231
|
+
if (schema === DEFAULT_SCHEMA) {
|
|
2232
|
+
return table;
|
|
2233
|
+
}
|
|
2234
|
+
return `${schema}.${table}`;
|
|
2235
|
+
}
|
|
2236
|
+
function normalizeSchemas(schemas) {
|
|
2237
|
+
const values = schemas ?? [DEFAULT_SCHEMA];
|
|
2238
|
+
const deduped = /* @__PURE__ */ new Set();
|
|
2239
|
+
for (const schema of values) {
|
|
2240
|
+
const trimmed = schema.trim();
|
|
2241
|
+
if (trimmed.length > 0) {
|
|
2242
|
+
deduped.add(trimmed);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
if (deduped.size === 0) {
|
|
2246
|
+
deduped.add(DEFAULT_SCHEMA);
|
|
2247
|
+
}
|
|
2248
|
+
return Array.from(deduped).sort((a, b) => a.localeCompare(b));
|
|
2249
|
+
}
|
|
2250
|
+
function normalizeColumnType5(row) {
|
|
2251
|
+
const dataType = row.data_type.toLowerCase();
|
|
2252
|
+
const udtName = row.udt_name.toLowerCase();
|
|
2253
|
+
if (dataType === "character varying") {
|
|
2254
|
+
if (row.character_maximum_length !== null) {
|
|
2255
|
+
return `varchar(${row.character_maximum_length})`;
|
|
2256
|
+
}
|
|
2257
|
+
return "varchar";
|
|
2258
|
+
}
|
|
2259
|
+
if (dataType === "timestamp with time zone") {
|
|
2260
|
+
return "timestamptz";
|
|
2261
|
+
}
|
|
2262
|
+
if (dataType === "integer" || udtName === "int4") {
|
|
2263
|
+
return "int";
|
|
2264
|
+
}
|
|
2265
|
+
if (dataType === "bigint" || udtName === "int8") {
|
|
2266
|
+
return "bigint";
|
|
2267
|
+
}
|
|
2268
|
+
if (dataType === "numeric") {
|
|
2269
|
+
if (row.numeric_precision !== null && row.numeric_scale !== null) {
|
|
2270
|
+
return `numeric(${row.numeric_precision},${row.numeric_scale})`;
|
|
2271
|
+
}
|
|
2272
|
+
return "numeric";
|
|
2273
|
+
}
|
|
2274
|
+
if (dataType === "boolean" || udtName === "bool") {
|
|
2275
|
+
return "boolean";
|
|
2276
|
+
}
|
|
2277
|
+
if (dataType === "uuid") {
|
|
2278
|
+
return "uuid";
|
|
2279
|
+
}
|
|
2280
|
+
if (dataType === "text") {
|
|
2281
|
+
return "text";
|
|
2282
|
+
}
|
|
2283
|
+
if (dataType === "date") {
|
|
2284
|
+
return "date";
|
|
2285
|
+
}
|
|
2286
|
+
return dataType;
|
|
2287
|
+
}
|
|
2288
|
+
function normalizeConstraints(constraintRows, foreignKeyRows) {
|
|
2289
|
+
const constraints = /* @__PURE__ */ new Map();
|
|
2290
|
+
for (const row of constraintRows) {
|
|
2291
|
+
const key = `${row.table_schema}.${row.table_name}.${row.constraint_name}.${row.constraint_type}`;
|
|
2292
|
+
const existing = constraints.get(key);
|
|
2293
|
+
if (existing) {
|
|
2294
|
+
if (row.column_name !== null) {
|
|
2295
|
+
existing.columns.push({
|
|
2296
|
+
name: row.column_name,
|
|
2297
|
+
position: row.ordinal_position ?? Number.MAX_SAFE_INTEGER
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
if (!existing.checkClause && row.check_clause) {
|
|
2301
|
+
existing.checkClause = row.check_clause;
|
|
2302
|
+
}
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
constraints.set(key, {
|
|
2306
|
+
tableSchema: row.table_schema,
|
|
2307
|
+
tableName: row.table_name,
|
|
2308
|
+
name: row.constraint_name,
|
|
2309
|
+
type: row.constraint_type,
|
|
2310
|
+
columns: row.column_name === null ? [] : [{
|
|
2311
|
+
name: row.column_name,
|
|
2312
|
+
position: row.ordinal_position ?? Number.MAX_SAFE_INTEGER
|
|
2313
|
+
}],
|
|
2314
|
+
...row.check_clause ? { checkClause: row.check_clause } : {}
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
const normalized = [];
|
|
2318
|
+
for (const value of constraints.values()) {
|
|
2319
|
+
value.columns.sort((left, right) => {
|
|
2320
|
+
if (left.position !== right.position) {
|
|
2321
|
+
return left.position - right.position;
|
|
2322
|
+
}
|
|
2323
|
+
return left.name.localeCompare(right.name);
|
|
2324
|
+
});
|
|
2325
|
+
normalized.push({
|
|
2326
|
+
tableSchema: value.tableSchema,
|
|
2327
|
+
tableName: value.tableName,
|
|
2328
|
+
name: value.name,
|
|
2329
|
+
type: value.type,
|
|
2330
|
+
columns: value.columns.map((item) => item.name),
|
|
2331
|
+
...value.checkClause ? { checkClause: value.checkClause } : {}
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
const foreignKeys = /* @__PURE__ */ new Map();
|
|
2335
|
+
for (const row of foreignKeyRows) {
|
|
2336
|
+
const key = `${row.table_schema}.${row.table_name}.${row.constraint_name}`;
|
|
2337
|
+
const existing = foreignKeys.get(key);
|
|
2338
|
+
if (existing) {
|
|
2339
|
+
existing.local.push({ name: row.local_column_name, position: row.position });
|
|
2340
|
+
existing.referenced.push({ name: row.referenced_column_name, position: row.position });
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
foreignKeys.set(key, {
|
|
2344
|
+
tableSchema: row.table_schema,
|
|
2345
|
+
tableName: row.table_name,
|
|
2346
|
+
name: row.constraint_name,
|
|
2347
|
+
local: [{ name: row.local_column_name, position: row.position }],
|
|
2348
|
+
referencedTableSchema: row.referenced_table_schema,
|
|
2349
|
+
referencedTableName: row.referenced_table_name,
|
|
2350
|
+
referenced: [{ name: row.referenced_column_name, position: row.position }]
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
for (const value of foreignKeys.values()) {
|
|
2354
|
+
value.local.sort((left, right) => left.position - right.position || left.name.localeCompare(right.name));
|
|
2355
|
+
value.referenced.sort((left, right) => left.position - right.position || left.name.localeCompare(right.name));
|
|
2356
|
+
normalized.push({
|
|
2357
|
+
tableSchema: value.tableSchema,
|
|
2358
|
+
tableName: value.tableName,
|
|
2359
|
+
name: value.name,
|
|
2360
|
+
type: "FOREIGN KEY",
|
|
2361
|
+
columns: value.local.map((item) => item.name),
|
|
2362
|
+
referencedTableSchema: value.referencedTableSchema,
|
|
2363
|
+
referencedTableName: value.referencedTableName,
|
|
2364
|
+
referencedColumns: value.referenced.map((item) => item.name)
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
normalized.sort((left, right) => {
|
|
2368
|
+
if (left.tableSchema !== right.tableSchema) {
|
|
2369
|
+
return left.tableSchema.localeCompare(right.tableSchema);
|
|
2370
|
+
}
|
|
2371
|
+
if (left.tableName !== right.tableName) {
|
|
2372
|
+
return left.tableName.localeCompare(right.tableName);
|
|
2373
|
+
}
|
|
2374
|
+
const typeOrderDiff = CONSTRAINT_TYPE_ORDER[left.type] - CONSTRAINT_TYPE_ORDER[right.type];
|
|
2375
|
+
if (typeOrderDiff !== 0) {
|
|
2376
|
+
return typeOrderDiff;
|
|
2377
|
+
}
|
|
2378
|
+
if (left.name !== right.name) {
|
|
2379
|
+
return left.name.localeCompare(right.name);
|
|
2380
|
+
}
|
|
2381
|
+
return left.columns.join(",").localeCompare(right.columns.join(","));
|
|
2382
|
+
});
|
|
2383
|
+
return normalized;
|
|
2384
|
+
}
|
|
2385
|
+
async function introspectPostgresSchema(options) {
|
|
2386
|
+
const schemaFilter = normalizeSchemas(options.schemas);
|
|
2387
|
+
const [tableRows, columnRows, constraintRows, foreignKeyRows] = await Promise.all([
|
|
2388
|
+
options.query(TABLES_QUERY, [schemaFilter]),
|
|
2389
|
+
options.query(COLUMNS_QUERY, [schemaFilter]),
|
|
2390
|
+
options.query(CONSTRAINTS_QUERY, [schemaFilter]),
|
|
2391
|
+
options.query(FOREIGN_KEYS_QUERY, [schemaFilter])
|
|
2392
|
+
]);
|
|
2393
|
+
const sortedTables = [...tableRows].sort((left, right) => {
|
|
2394
|
+
if (left.table_schema !== right.table_schema) {
|
|
2395
|
+
return left.table_schema.localeCompare(right.table_schema);
|
|
2396
|
+
}
|
|
2397
|
+
return left.table_name.localeCompare(right.table_name);
|
|
2398
|
+
});
|
|
2399
|
+
const sortedColumns = [...columnRows].sort((left, right) => {
|
|
2400
|
+
if (left.table_schema !== right.table_schema) {
|
|
2401
|
+
return left.table_schema.localeCompare(right.table_schema);
|
|
2402
|
+
}
|
|
2403
|
+
if (left.table_name !== right.table_name) {
|
|
2404
|
+
return left.table_name.localeCompare(right.table_name);
|
|
2405
|
+
}
|
|
2406
|
+
if (left.ordinal_position !== right.ordinal_position) {
|
|
2407
|
+
return left.ordinal_position - right.ordinal_position;
|
|
2408
|
+
}
|
|
2409
|
+
return left.column_name.localeCompare(right.column_name);
|
|
2410
|
+
});
|
|
2411
|
+
const normalizedConstraints = normalizeConstraints(constraintRows, foreignKeyRows);
|
|
2412
|
+
const tableMap = /* @__PURE__ */ new Map();
|
|
2413
|
+
const columnMap = /* @__PURE__ */ new Map();
|
|
2414
|
+
for (const row of sortedTables) {
|
|
2415
|
+
const key = toTableKey(row.table_schema, row.table_name);
|
|
2416
|
+
const table = { name: key, columns: [], primaryKey: null };
|
|
2417
|
+
tableMap.set(key, table);
|
|
2418
|
+
columnMap.set(key, /* @__PURE__ */ new Map());
|
|
2419
|
+
}
|
|
2420
|
+
for (const row of sortedColumns) {
|
|
2421
|
+
const key = toTableKey(row.table_schema, row.table_name);
|
|
2422
|
+
const table = tableMap.get(key);
|
|
2423
|
+
const columnsByName = columnMap.get(key);
|
|
2424
|
+
if (!table || !columnsByName) {
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
const column = {
|
|
2428
|
+
name: row.column_name,
|
|
2429
|
+
type: normalizeColumnType5(row),
|
|
2430
|
+
nullable: row.is_nullable === "YES"
|
|
2431
|
+
};
|
|
2432
|
+
const normalizedDefault = normalizeDefault(row.column_default);
|
|
2433
|
+
if (normalizedDefault !== null) {
|
|
2434
|
+
column.default = normalizedDefault;
|
|
2435
|
+
}
|
|
2436
|
+
table.columns.push(column);
|
|
2437
|
+
columnsByName.set(column.name, column);
|
|
2438
|
+
}
|
|
2439
|
+
for (const constraint of normalizedConstraints) {
|
|
2440
|
+
const tableKey = toTableKey(constraint.tableSchema, constraint.tableName);
|
|
2441
|
+
const table = tableMap.get(tableKey);
|
|
2442
|
+
const columnsByName = columnMap.get(tableKey);
|
|
2443
|
+
if (!table || !columnsByName) {
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
if (constraint.type === "PRIMARY KEY") {
|
|
2447
|
+
if (constraint.columns.length === 1) {
|
|
2448
|
+
const column = columnsByName.get(constraint.columns[0]);
|
|
2449
|
+
if (column) {
|
|
2450
|
+
column.primaryKey = true;
|
|
2451
|
+
column.nullable = false;
|
|
2452
|
+
table.primaryKey = column.name;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
continue;
|
|
2456
|
+
}
|
|
2457
|
+
if (constraint.type === "UNIQUE") {
|
|
2458
|
+
if (constraint.columns.length === 1) {
|
|
2459
|
+
const column = columnsByName.get(constraint.columns[0]);
|
|
2460
|
+
if (column) {
|
|
2461
|
+
column.unique = true;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
if (constraint.type === "FOREIGN KEY") {
|
|
2467
|
+
if (constraint.columns.length === 1 && constraint.referencedColumns && constraint.referencedColumns.length === 1 && constraint.referencedTableName) {
|
|
2468
|
+
const column = columnsByName.get(constraint.columns[0]);
|
|
2469
|
+
if (column) {
|
|
2470
|
+
column.foreignKey = {
|
|
2471
|
+
table: toTableKey(constraint.referencedTableSchema ?? DEFAULT_SCHEMA, constraint.referencedTableName),
|
|
2472
|
+
column: constraint.referencedColumns[0]
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
const orderedTableNames = Array.from(tableMap.keys()).sort((left, right) => left.localeCompare(right));
|
|
2479
|
+
const tables = {};
|
|
2480
|
+
for (const tableName of orderedTableNames) {
|
|
2481
|
+
const table = tableMap.get(tableName);
|
|
2482
|
+
if (table) {
|
|
2483
|
+
tables[tableName] = table;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
return { tables };
|
|
2487
|
+
}
|
|
2488
|
+
var DEFAULT_SCHEMA, CONSTRAINT_TYPE_ORDER, TABLES_QUERY, COLUMNS_QUERY, CONSTRAINTS_QUERY, FOREIGN_KEYS_QUERY;
|
|
2489
|
+
var init_introspect_postgres = __esm({
|
|
2490
|
+
"node_modules/@xubylele/schema-forge-core/dist/core/sql/introspect-postgres.js"() {
|
|
2491
|
+
"use strict";
|
|
2492
|
+
init_normalize();
|
|
2493
|
+
DEFAULT_SCHEMA = "public";
|
|
2494
|
+
CONSTRAINT_TYPE_ORDER = {
|
|
2495
|
+
"PRIMARY KEY": 0,
|
|
2496
|
+
UNIQUE: 1,
|
|
2497
|
+
"FOREIGN KEY": 2,
|
|
2498
|
+
CHECK: 3
|
|
2499
|
+
};
|
|
2500
|
+
TABLES_QUERY = `
|
|
2501
|
+
SELECT
|
|
2502
|
+
table_schema,
|
|
2503
|
+
table_name
|
|
2504
|
+
FROM information_schema.tables
|
|
2505
|
+
WHERE table_type = 'BASE TABLE'
|
|
2506
|
+
AND table_schema = ANY($1::text[])
|
|
2507
|
+
`;
|
|
2508
|
+
COLUMNS_QUERY = `
|
|
2509
|
+
SELECT
|
|
2510
|
+
table_schema,
|
|
2511
|
+
table_name,
|
|
2512
|
+
column_name,
|
|
2513
|
+
ordinal_position,
|
|
2514
|
+
is_nullable,
|
|
2515
|
+
data_type,
|
|
2516
|
+
udt_name,
|
|
2517
|
+
character_maximum_length,
|
|
2518
|
+
numeric_precision,
|
|
2519
|
+
numeric_scale,
|
|
2520
|
+
column_default
|
|
2521
|
+
FROM information_schema.columns
|
|
2522
|
+
WHERE table_schema = ANY($1::text[])
|
|
2523
|
+
`;
|
|
2524
|
+
CONSTRAINTS_QUERY = `
|
|
2525
|
+
SELECT
|
|
2526
|
+
tc.table_schema,
|
|
2527
|
+
tc.table_name,
|
|
2528
|
+
tc.constraint_name,
|
|
2529
|
+
tc.constraint_type,
|
|
2530
|
+
kcu.column_name,
|
|
2531
|
+
kcu.ordinal_position,
|
|
2532
|
+
cc.check_clause
|
|
2533
|
+
FROM information_schema.table_constraints tc
|
|
2534
|
+
LEFT JOIN information_schema.key_column_usage kcu
|
|
2535
|
+
ON tc.constraint_catalog = kcu.constraint_catalog
|
|
2536
|
+
AND tc.constraint_schema = kcu.constraint_schema
|
|
2537
|
+
AND tc.constraint_name = kcu.constraint_name
|
|
2538
|
+
AND tc.table_schema = kcu.table_schema
|
|
2539
|
+
AND tc.table_name = kcu.table_name
|
|
2540
|
+
LEFT JOIN information_schema.check_constraints cc
|
|
2541
|
+
ON tc.constraint_catalog = cc.constraint_catalog
|
|
2542
|
+
AND tc.constraint_schema = cc.constraint_schema
|
|
2543
|
+
AND tc.constraint_name = cc.constraint_name
|
|
2544
|
+
WHERE tc.table_schema = ANY($1::text[])
|
|
2545
|
+
AND tc.constraint_type IN ('PRIMARY KEY', 'UNIQUE', 'CHECK')
|
|
2546
|
+
`;
|
|
2547
|
+
FOREIGN_KEYS_QUERY = `
|
|
2548
|
+
SELECT
|
|
2549
|
+
src_ns.nspname AS table_schema,
|
|
2550
|
+
src.relname AS table_name,
|
|
2551
|
+
con.conname AS constraint_name,
|
|
2552
|
+
src_attr.attname AS local_column_name,
|
|
2553
|
+
ref_ns.nspname AS referenced_table_schema,
|
|
2554
|
+
ref.relname AS referenced_table_name,
|
|
2555
|
+
ref_attr.attname AS referenced_column_name,
|
|
2556
|
+
src_key.ord AS position
|
|
2557
|
+
FROM pg_constraint con
|
|
2558
|
+
JOIN pg_class src
|
|
2559
|
+
ON src.oid = con.conrelid
|
|
2560
|
+
JOIN pg_namespace src_ns
|
|
2561
|
+
ON src_ns.oid = src.relnamespace
|
|
2562
|
+
JOIN pg_class ref
|
|
2563
|
+
ON ref.oid = con.confrelid
|
|
2564
|
+
JOIN pg_namespace ref_ns
|
|
2565
|
+
ON ref_ns.oid = ref.relnamespace
|
|
2566
|
+
JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS src_key(attnum, ord)
|
|
2567
|
+
ON TRUE
|
|
2568
|
+
JOIN LATERAL unnest(con.confkey) WITH ORDINALITY AS ref_key(attnum, ord)
|
|
2569
|
+
ON ref_key.ord = src_key.ord
|
|
2570
|
+
JOIN pg_attribute src_attr
|
|
2571
|
+
ON src_attr.attrelid = con.conrelid
|
|
2572
|
+
AND src_attr.attnum = src_key.attnum
|
|
2573
|
+
JOIN pg_attribute ref_attr
|
|
2574
|
+
ON ref_attr.attrelid = con.confrelid
|
|
2575
|
+
AND ref_attr.attnum = ref_key.attnum
|
|
2576
|
+
WHERE con.contype = 'f'
|
|
2577
|
+
AND src_ns.nspname = ANY($1::text[])
|
|
2578
|
+
`;
|
|
2579
|
+
}
|
|
2580
|
+
});
|
|
2581
|
+
|
|
2160
2582
|
// node_modules/@xubylele/schema-forge-core/dist/core/paths.js
|
|
2161
2583
|
function getProjectRoot2(cwd = process.cwd()) {
|
|
2162
2584
|
return cwd;
|
|
@@ -2219,6 +2641,7 @@ var init_errors = __esm({
|
|
|
2219
2641
|
var dist_exports = {};
|
|
2220
2642
|
__export(dist_exports, {
|
|
2221
2643
|
SchemaValidationError: () => SchemaValidationError,
|
|
2644
|
+
analyzeSchemaDrift: () => analyzeSchemaDrift,
|
|
2222
2645
|
applySqlOps: () => applySqlOps,
|
|
2223
2646
|
checkOperationSafety: () => checkOperationSafety,
|
|
2224
2647
|
checkSchemaSafety: () => checkSchemaSafety,
|
|
@@ -2237,6 +2660,7 @@ __export(dist_exports, {
|
|
|
2237
2660
|
getStatePath: () => getStatePath2,
|
|
2238
2661
|
getTableNamesFromSchema: () => getTableNamesFromSchema,
|
|
2239
2662
|
getTableNamesFromState: () => getTableNamesFromState,
|
|
2663
|
+
introspectPostgresSchema: () => introspectPostgresSchema,
|
|
2240
2664
|
legacyPkName: () => legacyPkName,
|
|
2241
2665
|
legacyUqName: () => legacyUqName,
|
|
2242
2666
|
loadMigrationSqlInput: () => loadMigrationSqlInput,
|
|
@@ -2274,6 +2698,7 @@ var init_dist = __esm({
|
|
|
2274
2698
|
"use strict";
|
|
2275
2699
|
init_parser();
|
|
2276
2700
|
init_diff();
|
|
2701
|
+
init_drift_analyzer();
|
|
2277
2702
|
init_validator();
|
|
2278
2703
|
init_validate();
|
|
2279
2704
|
init_safety();
|
|
@@ -2284,6 +2709,7 @@ var init_dist = __esm({
|
|
|
2284
2709
|
init_schema_to_dsl();
|
|
2285
2710
|
init_load_migrations();
|
|
2286
2711
|
init_split_statements();
|
|
2712
|
+
init_introspect_postgres();
|
|
2287
2713
|
init_fs();
|
|
2288
2714
|
init_normalize();
|
|
2289
2715
|
init_paths();
|
|
@@ -2293,22 +2719,33 @@ var init_dist = __esm({
|
|
|
2293
2719
|
});
|
|
2294
2720
|
|
|
2295
2721
|
// src/cli.ts
|
|
2296
|
-
var
|
|
2722
|
+
var import_commander8 = require("commander");
|
|
2297
2723
|
|
|
2298
2724
|
// package.json
|
|
2299
2725
|
var package_default = {
|
|
2300
2726
|
name: "@xubylele/schema-forge",
|
|
2301
|
-
version: "1.
|
|
2727
|
+
version: "1.8.0",
|
|
2302
2728
|
description: "Universal migration generator from schema DSL",
|
|
2303
2729
|
main: "dist/cli.js",
|
|
2304
2730
|
type: "commonjs",
|
|
2305
2731
|
bin: {
|
|
2306
2732
|
"schema-forge": "dist/cli.js"
|
|
2307
2733
|
},
|
|
2734
|
+
exports: {
|
|
2735
|
+
".": {
|
|
2736
|
+
require: "dist/cli.js",
|
|
2737
|
+
types: "dist/cli.d.ts"
|
|
2738
|
+
},
|
|
2739
|
+
"./api": {
|
|
2740
|
+
require: "dist/api.js",
|
|
2741
|
+
types: "dist/api.d.ts"
|
|
2742
|
+
}
|
|
2743
|
+
},
|
|
2308
2744
|
scripts: {
|
|
2309
|
-
build: "tsup src/cli.ts --format cjs --dts",
|
|
2745
|
+
build: "tsup src/cli.ts src/api.ts --format cjs --dts",
|
|
2310
2746
|
dev: "ts-node src/cli.ts",
|
|
2311
2747
|
test: "vitest",
|
|
2748
|
+
"test:integration:drift": "vitest run test/drift-realdb.integration.test.ts",
|
|
2312
2749
|
prepublishOnly: "npm run build",
|
|
2313
2750
|
"publish:public": "npm publish --access public",
|
|
2314
2751
|
changeset: "changeset",
|
|
@@ -2340,12 +2777,15 @@ var package_default = {
|
|
|
2340
2777
|
dependencies: {
|
|
2341
2778
|
boxen: "^8.0.1",
|
|
2342
2779
|
chalk: "^5.6.2",
|
|
2343
|
-
commander: "^14.0.3"
|
|
2780
|
+
commander: "^14.0.3",
|
|
2781
|
+
pg: "^8.19.0"
|
|
2344
2782
|
},
|
|
2345
2783
|
devDependencies: {
|
|
2346
|
-
"@changesets/cli": "^2.
|
|
2784
|
+
"@changesets/cli": "^2.30.0",
|
|
2347
2785
|
"@types/node": "^25.2.3",
|
|
2348
|
-
"@
|
|
2786
|
+
"@types/pg": "^8.18.0",
|
|
2787
|
+
"@xubylele/schema-forge-core": "^1.3.0",
|
|
2788
|
+
testcontainers: "^11.8.1",
|
|
2349
2789
|
"ts-node": "^10.9.2",
|
|
2350
2790
|
tsup: "^8.5.1",
|
|
2351
2791
|
typescript: "^5.9.3",
|
|
@@ -2492,6 +2932,14 @@ async function parseMigrationSql2(sql) {
|
|
|
2492
2932
|
const core = await loadCore();
|
|
2493
2933
|
return core.parseMigrationSql(sql);
|
|
2494
2934
|
}
|
|
2935
|
+
async function introspectPostgresSchema2(options) {
|
|
2936
|
+
const core = await loadCore();
|
|
2937
|
+
return core.introspectPostgresSchema(options);
|
|
2938
|
+
}
|
|
2939
|
+
async function analyzeSchemaDrift2(state, liveSchema) {
|
|
2940
|
+
const core = await loadCore();
|
|
2941
|
+
return core.analyzeSchemaDrift(state, liveSchema);
|
|
2942
|
+
}
|
|
2495
2943
|
async function applySqlOps2(ops) {
|
|
2496
2944
|
const core = await loadCore();
|
|
2497
2945
|
return core.applySqlOps(ops);
|
|
@@ -2513,6 +2961,40 @@ async function isSchemaValidationError(error2) {
|
|
|
2513
2961
|
return error2 instanceof core.SchemaValidationError;
|
|
2514
2962
|
}
|
|
2515
2963
|
|
|
2964
|
+
// src/core/postgres.ts
|
|
2965
|
+
var import_pg = require("pg");
|
|
2966
|
+
function resolvePostgresConnectionString(options = {}) {
|
|
2967
|
+
const explicitUrl = options.url?.trim();
|
|
2968
|
+
if (explicitUrl) {
|
|
2969
|
+
return explicitUrl;
|
|
2970
|
+
}
|
|
2971
|
+
const envUrl = process.env.DATABASE_URL?.trim();
|
|
2972
|
+
if (envUrl) {
|
|
2973
|
+
return envUrl;
|
|
2974
|
+
}
|
|
2975
|
+
throw new Error("PostgreSQL connection URL is required. Pass --url or set DATABASE_URL.");
|
|
2976
|
+
}
|
|
2977
|
+
function parseSchemaList(value) {
|
|
2978
|
+
if (!value) {
|
|
2979
|
+
return void 0;
|
|
2980
|
+
}
|
|
2981
|
+
const schemas = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
2982
|
+
return schemas.length > 0 ? schemas : void 0;
|
|
2983
|
+
}
|
|
2984
|
+
async function withPostgresQueryExecutor(connectionString, run) {
|
|
2985
|
+
const client = new import_pg.Client({ connectionString });
|
|
2986
|
+
await client.connect();
|
|
2987
|
+
const query = async (sql, params) => {
|
|
2988
|
+
const result = await client.query(sql, params ? [...params] : void 0);
|
|
2989
|
+
return result.rows;
|
|
2990
|
+
};
|
|
2991
|
+
try {
|
|
2992
|
+
return await run(query);
|
|
2993
|
+
} finally {
|
|
2994
|
+
await client.end();
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2516
2998
|
// src/utils/exitCodes.ts
|
|
2517
2999
|
var EXIT_CODES = {
|
|
2518
3000
|
/** Successful operation */
|
|
@@ -2647,7 +3129,6 @@ function hasDestructiveFindings(findings) {
|
|
|
2647
3129
|
}
|
|
2648
3130
|
|
|
2649
3131
|
// src/commands/diff.ts
|
|
2650
|
-
var REQUIRED_CONFIG_FIELDS = ["schemaFile", "stateFile"];
|
|
2651
3132
|
function resolveConfigPath(root, targetPath) {
|
|
2652
3133
|
return import_path7.default.isAbsolute(targetPath) ? targetPath : import_path7.default.join(root, targetPath);
|
|
2653
3134
|
}
|
|
@@ -2661,14 +3142,16 @@ async function runDiff(options = {}) {
|
|
|
2661
3142
|
throw new Error('SchemaForge project not initialized. Run "schema-forge init" first.');
|
|
2662
3143
|
}
|
|
2663
3144
|
const config = await readJsonFile(configPath, {});
|
|
2664
|
-
|
|
3145
|
+
const useLiveDatabase = Boolean(options.url || process.env.DATABASE_URL);
|
|
3146
|
+
const requiredFields = useLiveDatabase ? ["schemaFile"] : ["schemaFile", "stateFile"];
|
|
3147
|
+
for (const field of requiredFields) {
|
|
2665
3148
|
const value = config[field];
|
|
2666
3149
|
if (!value || typeof value !== "string") {
|
|
2667
3150
|
throw new Error(`Invalid config: '${field}' is required`);
|
|
2668
3151
|
}
|
|
2669
3152
|
}
|
|
2670
3153
|
const schemaPath = resolveConfigPath(root, config.schemaFile);
|
|
2671
|
-
const statePath = resolveConfigPath(root, config.stateFile);
|
|
3154
|
+
const statePath = config.stateFile ? resolveConfigPath(root, config.stateFile) : null;
|
|
2672
3155
|
const { provider } = resolveProvider(config.provider);
|
|
2673
3156
|
const schemaSource = await readTextFile(schemaPath);
|
|
2674
3157
|
const schema = await parseSchema2(schemaSource);
|
|
@@ -2680,7 +3163,17 @@ async function runDiff(options = {}) {
|
|
|
2680
3163
|
}
|
|
2681
3164
|
throw error2;
|
|
2682
3165
|
}
|
|
2683
|
-
const previousState = await
|
|
3166
|
+
const previousState = useLiveDatabase ? await withPostgresQueryExecutor(
|
|
3167
|
+
resolvePostgresConnectionString({ url: options.url }),
|
|
3168
|
+
async (query) => {
|
|
3169
|
+
const schemaFilters = parseSchemaList(options.schema);
|
|
3170
|
+
const liveSchema = await introspectPostgresSchema2({
|
|
3171
|
+
query,
|
|
3172
|
+
...schemaFilters ? { schemas: schemaFilters } : {}
|
|
3173
|
+
});
|
|
3174
|
+
return schemaToState2(liveSchema);
|
|
3175
|
+
}
|
|
3176
|
+
) : await loadState2(statePath ?? "");
|
|
2684
3177
|
const diff = await diffSchemas2(previousState, schema);
|
|
2685
3178
|
if (options.force) {
|
|
2686
3179
|
forceWarning("Are you sure to use --force? This option will bypass safety checks for destructive operations.");
|
|
@@ -2725,9 +3218,72 @@ Remove --safe flag or modify schema to avoid destructive changes.`
|
|
|
2725
3218
|
process.exitCode = EXIT_CODES.SUCCESS;
|
|
2726
3219
|
}
|
|
2727
3220
|
|
|
2728
|
-
// src/commands/
|
|
3221
|
+
// src/commands/doctor.ts
|
|
2729
3222
|
var import_commander2 = require("commander");
|
|
2730
3223
|
var import_path8 = __toESM(require("path"));
|
|
3224
|
+
function resolveConfigPath2(root, targetPath) {
|
|
3225
|
+
return import_path8.default.isAbsolute(targetPath) ? targetPath : import_path8.default.join(root, targetPath);
|
|
3226
|
+
}
|
|
3227
|
+
function hasDrift(report) {
|
|
3228
|
+
return report.missingTables.length > 0 || report.extraTables.length > 0 || report.columnDifferences.length > 0 || report.typeMismatches.length > 0;
|
|
3229
|
+
}
|
|
3230
|
+
function printDriftReport(report) {
|
|
3231
|
+
if (report.missingTables.length > 0) {
|
|
3232
|
+
console.log(`Missing tables in live DB: ${report.missingTables.join(", ")}`);
|
|
3233
|
+
}
|
|
3234
|
+
if (report.extraTables.length > 0) {
|
|
3235
|
+
console.log(`Extra tables in live DB: ${report.extraTables.join(", ")}`);
|
|
3236
|
+
}
|
|
3237
|
+
for (const difference of report.columnDifferences) {
|
|
3238
|
+
if (difference.missingInLive.length > 0) {
|
|
3239
|
+
console.log(`Missing columns in ${difference.tableName}: ${difference.missingInLive.join(", ")}`);
|
|
3240
|
+
}
|
|
3241
|
+
if (difference.extraInLive.length > 0) {
|
|
3242
|
+
console.log(`Extra columns in ${difference.tableName}: ${difference.extraInLive.join(", ")}`);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
for (const mismatch of report.typeMismatches) {
|
|
3246
|
+
console.log(`Type mismatch ${mismatch.tableName}.${mismatch.columnName}: ${mismatch.expectedType} -> ${mismatch.actualType}`);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
async function runDoctor(options = {}) {
|
|
3250
|
+
const root = getProjectRoot();
|
|
3251
|
+
const configPath = getConfigPath(root);
|
|
3252
|
+
if (!await fileExists(configPath)) {
|
|
3253
|
+
throw new Error('SchemaForge project not initialized. Run "schema-forge init" first.');
|
|
3254
|
+
}
|
|
3255
|
+
const config = await readJsonFile(configPath, {});
|
|
3256
|
+
if (!config.stateFile || typeof config.stateFile !== "string") {
|
|
3257
|
+
throw new Error("Invalid config: 'stateFile' is required");
|
|
3258
|
+
}
|
|
3259
|
+
const statePath = resolveConfigPath2(root, config.stateFile);
|
|
3260
|
+
const previousState = await loadState2(statePath);
|
|
3261
|
+
const schemaFilters = parseSchemaList(options.schema);
|
|
3262
|
+
const liveSchema = await withPostgresQueryExecutor(
|
|
3263
|
+
resolvePostgresConnectionString({ url: options.url }),
|
|
3264
|
+
(query) => introspectPostgresSchema2({
|
|
3265
|
+
query,
|
|
3266
|
+
...schemaFilters ? { schemas: schemaFilters } : {}
|
|
3267
|
+
})
|
|
3268
|
+
);
|
|
3269
|
+
const driftReport = await analyzeSchemaDrift2(previousState, liveSchema);
|
|
3270
|
+
const detected = hasDrift(driftReport);
|
|
3271
|
+
process.exitCode = detected ? EXIT_CODES.DRIFT_DETECTED : EXIT_CODES.SUCCESS;
|
|
3272
|
+
if (options.json) {
|
|
3273
|
+
console.log(JSON.stringify(driftReport, null, 2));
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
3276
|
+
if (!detected) {
|
|
3277
|
+
success("No schema drift detected");
|
|
3278
|
+
return;
|
|
3279
|
+
}
|
|
3280
|
+
console.log("Schema drift detected");
|
|
3281
|
+
printDriftReport(driftReport);
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
// src/commands/generate.ts
|
|
3285
|
+
var import_commander3 = require("commander");
|
|
3286
|
+
var import_path9 = __toESM(require("path"));
|
|
2731
3287
|
|
|
2732
3288
|
// src/core/utils.ts
|
|
2733
3289
|
function nowTimestamp2() {
|
|
@@ -2740,13 +3296,13 @@ function slugifyName2(name) {
|
|
|
2740
3296
|
}
|
|
2741
3297
|
|
|
2742
3298
|
// src/commands/generate.ts
|
|
2743
|
-
var
|
|
3299
|
+
var REQUIRED_CONFIG_FIELDS = [
|
|
2744
3300
|
"schemaFile",
|
|
2745
3301
|
"stateFile",
|
|
2746
3302
|
"outputDir"
|
|
2747
3303
|
];
|
|
2748
|
-
function
|
|
2749
|
-
return
|
|
3304
|
+
function resolveConfigPath3(root, targetPath) {
|
|
3305
|
+
return import_path9.default.isAbsolute(targetPath) ? targetPath : import_path9.default.join(root, targetPath);
|
|
2750
3306
|
}
|
|
2751
3307
|
async function runGenerate(options) {
|
|
2752
3308
|
if (options.safe && options.force) {
|
|
@@ -2758,15 +3314,15 @@ async function runGenerate(options) {
|
|
|
2758
3314
|
throw new Error('SchemaForge project not initialized. Run "schema-forge init" first.');
|
|
2759
3315
|
}
|
|
2760
3316
|
const config = await readJsonFile(configPath, {});
|
|
2761
|
-
for (const field of
|
|
3317
|
+
for (const field of REQUIRED_CONFIG_FIELDS) {
|
|
2762
3318
|
const value = config[field];
|
|
2763
3319
|
if (!value || typeof value !== "string") {
|
|
2764
3320
|
throw new Error(`Invalid config: '${field}' is required`);
|
|
2765
3321
|
}
|
|
2766
3322
|
}
|
|
2767
|
-
const schemaPath =
|
|
2768
|
-
const statePath =
|
|
2769
|
-
const outputDir =
|
|
3323
|
+
const schemaPath = resolveConfigPath3(root, config.schemaFile);
|
|
3324
|
+
const statePath = resolveConfigPath3(root, config.stateFile);
|
|
3325
|
+
const outputDir = resolveConfigPath3(root, config.outputDir);
|
|
2770
3326
|
const { provider, usedDefault } = resolveProvider(config.provider);
|
|
2771
3327
|
if (usedDefault) {
|
|
2772
3328
|
info("Provider not set; defaulting to postgres.");
|
|
@@ -2827,7 +3383,7 @@ Remove --safe flag or modify schema to avoid destructive changes.`
|
|
|
2827
3383
|
const slug = slugifyName2(options.name ?? "migration");
|
|
2828
3384
|
const fileName = `${timestamp}-${slug}.sql`;
|
|
2829
3385
|
await ensureDir(outputDir);
|
|
2830
|
-
const migrationPath =
|
|
3386
|
+
const migrationPath = import_path9.default.join(outputDir, fileName);
|
|
2831
3387
|
await writeTextFile(migrationPath, sql + "\n");
|
|
2832
3388
|
const nextState = await schemaToState2(schema);
|
|
2833
3389
|
await saveState2(statePath, nextState);
|
|
@@ -2836,14 +3392,14 @@ Remove --safe flag or modify schema to avoid destructive changes.`
|
|
|
2836
3392
|
}
|
|
2837
3393
|
|
|
2838
3394
|
// src/commands/import.ts
|
|
2839
|
-
var
|
|
2840
|
-
var
|
|
2841
|
-
function
|
|
2842
|
-
return
|
|
3395
|
+
var import_commander4 = require("commander");
|
|
3396
|
+
var import_path10 = __toESM(require("path"));
|
|
3397
|
+
function resolveConfigPath4(root, targetPath) {
|
|
3398
|
+
return import_path10.default.isAbsolute(targetPath) ? targetPath : import_path10.default.join(root, targetPath);
|
|
2843
3399
|
}
|
|
2844
3400
|
async function runImport(inputPath, options = {}) {
|
|
2845
3401
|
const root = getProjectRoot();
|
|
2846
|
-
const absoluteInputPath =
|
|
3402
|
+
const absoluteInputPath = resolveConfigPath4(root, inputPath);
|
|
2847
3403
|
const inputs = await loadMigrationSqlInput2(absoluteInputPath);
|
|
2848
3404
|
if (inputs.length === 0) {
|
|
2849
3405
|
throw new Error(`No .sql migration files found in: ${absoluteInputPath}`);
|
|
@@ -2854,7 +3410,7 @@ async function runImport(inputPath, options = {}) {
|
|
|
2854
3410
|
const result = await parseMigrationSql2(input.sql);
|
|
2855
3411
|
allOps.push(...result.ops);
|
|
2856
3412
|
parseWarnings.push(...result.warnings.map((item) => ({
|
|
2857
|
-
statement: `[${
|
|
3413
|
+
statement: `[${import_path10.default.basename(input.filePath)}] ${item.statement}`,
|
|
2858
3414
|
reason: item.reason
|
|
2859
3415
|
})));
|
|
2860
3416
|
}
|
|
@@ -2870,7 +3426,7 @@ async function runImport(inputPath, options = {}) {
|
|
|
2870
3426
|
}
|
|
2871
3427
|
}
|
|
2872
3428
|
}
|
|
2873
|
-
const schemaPath = targetPath ?
|
|
3429
|
+
const schemaPath = targetPath ? resolveConfigPath4(root, targetPath) : getSchemaFilePath(root);
|
|
2874
3430
|
await writeTextFile(schemaPath, dsl);
|
|
2875
3431
|
success(`Imported ${inputs.length} migration file(s) into ${schemaPath}`);
|
|
2876
3432
|
info(`Parsed ${allOps.length} supported DDL operation(s)`);
|
|
@@ -2888,7 +3444,7 @@ async function runImport(inputPath, options = {}) {
|
|
|
2888
3444
|
}
|
|
2889
3445
|
|
|
2890
3446
|
// src/commands/init.ts
|
|
2891
|
-
var
|
|
3447
|
+
var import_commander5 = require("commander");
|
|
2892
3448
|
async function runInit() {
|
|
2893
3449
|
const root = getProjectRoot();
|
|
2894
3450
|
const schemaForgeDir = getSchemaForgeDir(root);
|
|
@@ -2947,28 +3503,61 @@ table users {
|
|
|
2947
3503
|
process.exitCode = EXIT_CODES.SUCCESS;
|
|
2948
3504
|
}
|
|
2949
3505
|
|
|
3506
|
+
// src/commands/introspect.ts
|
|
3507
|
+
var import_commander6 = require("commander");
|
|
3508
|
+
var import_path11 = __toESM(require("path"));
|
|
3509
|
+
function resolveOutputPath(root, outputPath) {
|
|
3510
|
+
return import_path11.default.isAbsolute(outputPath) ? outputPath : import_path11.default.join(root, outputPath);
|
|
3511
|
+
}
|
|
3512
|
+
async function runIntrospect(options = {}) {
|
|
3513
|
+
const connectionString = resolvePostgresConnectionString({ url: options.url });
|
|
3514
|
+
const schemas = parseSchemaList(options.schema);
|
|
3515
|
+
const root = getProjectRoot();
|
|
3516
|
+
const schema = await withPostgresQueryExecutor(connectionString, (query) => introspectPostgresSchema2({
|
|
3517
|
+
query,
|
|
3518
|
+
...schemas ? { schemas } : {}
|
|
3519
|
+
}));
|
|
3520
|
+
const output = JSON.stringify(schema, null, 2);
|
|
3521
|
+
if (!options.json && !options.out) {
|
|
3522
|
+
info(`Introspected ${Object.keys(schema.tables).length} table(s) from PostgreSQL.`);
|
|
3523
|
+
}
|
|
3524
|
+
if (options.out) {
|
|
3525
|
+
const outputPath = resolveOutputPath(root, options.out);
|
|
3526
|
+
await writeTextFile(outputPath, `${output}
|
|
3527
|
+
`);
|
|
3528
|
+
success(`Live schema written to ${outputPath}`);
|
|
3529
|
+
}
|
|
3530
|
+
if (options.json || !options.out) {
|
|
3531
|
+
console.log(output);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
|
|
2950
3535
|
// src/commands/validate.ts
|
|
2951
|
-
var
|
|
2952
|
-
var
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
return import_path10.default.isAbsolute(targetPath) ? targetPath : import_path10.default.join(root, targetPath);
|
|
3536
|
+
var import_commander7 = require("commander");
|
|
3537
|
+
var import_path12 = __toESM(require("path"));
|
|
3538
|
+
function resolveConfigPath5(root, targetPath) {
|
|
3539
|
+
return import_path12.default.isAbsolute(targetPath) ? targetPath : import_path12.default.join(root, targetPath);
|
|
2956
3540
|
}
|
|
2957
3541
|
async function runValidate(options = {}) {
|
|
2958
3542
|
const root = getProjectRoot();
|
|
2959
3543
|
const configPath = getConfigPath(root);
|
|
3544
|
+
const useLiveDatabase = Boolean(options.url || process.env.DATABASE_URL);
|
|
2960
3545
|
if (!await fileExists(configPath)) {
|
|
2961
3546
|
throw new Error('SchemaForge project not initialized. Run "schema-forge init" first.');
|
|
2962
3547
|
}
|
|
2963
3548
|
const config = await readJsonFile(configPath, {});
|
|
2964
|
-
|
|
3549
|
+
const requiredFields = ["schemaFile", "stateFile"];
|
|
3550
|
+
for (const field of requiredFields) {
|
|
2965
3551
|
const value = config[field];
|
|
2966
3552
|
if (!value || typeof value !== "string") {
|
|
2967
3553
|
throw new Error(`Invalid config: '${field}' is required`);
|
|
2968
3554
|
}
|
|
2969
3555
|
}
|
|
2970
|
-
const schemaPath =
|
|
2971
|
-
|
|
3556
|
+
const schemaPath = resolveConfigPath5(root, config.schemaFile);
|
|
3557
|
+
if (!config.stateFile) {
|
|
3558
|
+
throw new Error("Invalid config: 'stateFile' is required");
|
|
3559
|
+
}
|
|
3560
|
+
const statePath = resolveConfigPath5(root, config.stateFile);
|
|
2972
3561
|
const schemaSource = await readTextFile(schemaPath);
|
|
2973
3562
|
const schema = await parseSchema2(schemaSource);
|
|
2974
3563
|
try {
|
|
@@ -2980,6 +3569,45 @@ async function runValidate(options = {}) {
|
|
|
2980
3569
|
throw error2;
|
|
2981
3570
|
}
|
|
2982
3571
|
const previousState = await loadState2(statePath);
|
|
3572
|
+
if (useLiveDatabase) {
|
|
3573
|
+
const schemaFilters = parseSchemaList(options.schema);
|
|
3574
|
+
const liveSchema = await withPostgresQueryExecutor(
|
|
3575
|
+
resolvePostgresConnectionString({ url: options.url }),
|
|
3576
|
+
(query) => introspectPostgresSchema2({
|
|
3577
|
+
query,
|
|
3578
|
+
...schemaFilters ? { schemas: schemaFilters } : {}
|
|
3579
|
+
})
|
|
3580
|
+
);
|
|
3581
|
+
const driftReport = await analyzeSchemaDrift2(previousState, liveSchema);
|
|
3582
|
+
const hasDrift2 = driftReport.missingTables.length > 0 || driftReport.extraTables.length > 0 || driftReport.columnDifferences.length > 0 || driftReport.typeMismatches.length > 0;
|
|
3583
|
+
process.exitCode = hasDrift2 ? EXIT_CODES.DRIFT_DETECTED : EXIT_CODES.SUCCESS;
|
|
3584
|
+
if (options.json) {
|
|
3585
|
+
console.log(JSON.stringify(driftReport, null, 2));
|
|
3586
|
+
return;
|
|
3587
|
+
}
|
|
3588
|
+
if (!hasDrift2) {
|
|
3589
|
+
success("No schema drift detected");
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
if (driftReport.missingTables.length > 0) {
|
|
3593
|
+
console.log(`Missing tables in live DB: ${driftReport.missingTables.join(", ")}`);
|
|
3594
|
+
}
|
|
3595
|
+
if (driftReport.extraTables.length > 0) {
|
|
3596
|
+
console.log(`Extra tables in live DB: ${driftReport.extraTables.join(", ")}`);
|
|
3597
|
+
}
|
|
3598
|
+
for (const difference of driftReport.columnDifferences) {
|
|
3599
|
+
if (difference.missingInLive.length > 0) {
|
|
3600
|
+
console.log(`Missing columns in ${difference.tableName}: ${difference.missingInLive.join(", ")}`);
|
|
3601
|
+
}
|
|
3602
|
+
if (difference.extraInLive.length > 0) {
|
|
3603
|
+
console.log(`Extra columns in ${difference.tableName}: ${difference.extraInLive.join(", ")}`);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
for (const mismatch of driftReport.typeMismatches) {
|
|
3607
|
+
console.log(`Type mismatch ${mismatch.tableName}.${mismatch.columnName}: ${mismatch.expectedType} -> ${mismatch.actualType}`);
|
|
3608
|
+
}
|
|
3609
|
+
return;
|
|
3610
|
+
}
|
|
2983
3611
|
const findings = await validateSchemaChanges2(previousState, schema);
|
|
2984
3612
|
const report = await toValidationReport2(findings);
|
|
2985
3613
|
if (isCI() && hasDestructiveFindings(findings)) {
|
|
@@ -3053,7 +3681,7 @@ async function seedLastSeenVersion(version) {
|
|
|
3053
3681
|
}
|
|
3054
3682
|
|
|
3055
3683
|
// src/cli.ts
|
|
3056
|
-
var program = new
|
|
3684
|
+
var program = new import_commander8.Command();
|
|
3057
3685
|
program.name("schema-forge").description("CLI tool for schema management and SQL generation").version(package_default.version).option("--safe", "Prevent execution of destructive operations").option("--force", "Force execution by bypassing safety checks and CI detection");
|
|
3058
3686
|
function validateFlagExclusivity(options) {
|
|
3059
3687
|
if (options.safe && options.force) {
|
|
@@ -3089,11 +3717,25 @@ program.command("generate").description("Generate SQL from schema files. In CI e
|
|
|
3089
3717
|
await handleError(error2);
|
|
3090
3718
|
}
|
|
3091
3719
|
});
|
|
3092
|
-
program.command("diff").description("Compare two schema versions and generate migration SQL. In CI environments (CI=true), exits with code 3 if destructive operations are detected unless --force is used.").action(async () => {
|
|
3720
|
+
program.command("diff").description("Compare two schema versions and generate migration SQL. In CI environments (CI=true), exits with code 3 if destructive operations are detected unless --force is used.").option("--url <string>", "PostgreSQL connection URL for live diff (defaults to DATABASE_URL)").option("--schema <list>", "Comma-separated schema names to introspect (default: public)").action(async (options) => {
|
|
3093
3721
|
try {
|
|
3094
3722
|
const globalOptions = program.opts();
|
|
3095
3723
|
validateFlagExclusivity(globalOptions);
|
|
3096
|
-
await runDiff(globalOptions);
|
|
3724
|
+
await runDiff({ ...options, ...globalOptions });
|
|
3725
|
+
} catch (error2) {
|
|
3726
|
+
await handleError(error2);
|
|
3727
|
+
}
|
|
3728
|
+
});
|
|
3729
|
+
program.command("doctor").description("Check live database drift against state. Exits with code 2 when drift is detected.").option("--json", "Output structured JSON").option("--url <string>", "PostgreSQL connection URL (defaults to DATABASE_URL)").option("--schema <list>", "Comma-separated schema names to introspect (default: public)").action(async (options) => {
|
|
3730
|
+
try {
|
|
3731
|
+
await runDoctor(options);
|
|
3732
|
+
} catch (error2) {
|
|
3733
|
+
await handleError(error2);
|
|
3734
|
+
}
|
|
3735
|
+
});
|
|
3736
|
+
program.command("introspect").description("Extract normalized live schema from PostgreSQL").option("--url <string>", "PostgreSQL connection URL (defaults to DATABASE_URL)").option("--schema <list>", "Comma-separated schema names (default: public)").option("--json", "Output normalized schema JSON to stdout").option("--out <path>", "Write normalized schema JSON to a file").action(async (options) => {
|
|
3737
|
+
try {
|
|
3738
|
+
await runIntrospect(options);
|
|
3097
3739
|
} catch (error2) {
|
|
3098
3740
|
await handleError(error2);
|
|
3099
3741
|
}
|
|
@@ -3105,7 +3747,7 @@ program.command("import").description("Import schema from SQL migrations").argum
|
|
|
3105
3747
|
await handleError(error2);
|
|
3106
3748
|
}
|
|
3107
3749
|
});
|
|
3108
|
-
program.command("validate").description("Detect destructive or risky schema changes. In CI environments (CI=true), exits with code 3 if destructive operations are detected.").option("--json", "Output structured JSON").action(async (options) => {
|
|
3750
|
+
program.command("validate").description("Detect destructive or risky schema changes. In CI environments (CI=true), exits with code 3 if destructive operations are detected.").option("--json", "Output structured JSON").option("--url <string>", "PostgreSQL connection URL for live drift validation (defaults to DATABASE_URL)").option("--schema <list>", "Comma-separated schema names to introspect (default: public)").action(async (options) => {
|
|
3109
3751
|
try {
|
|
3110
3752
|
await runValidate(options);
|
|
3111
3753
|
} catch (error2) {
|