Skip to content

Commit 4510686

Browse files
committed
feat: update uuid package to version 14.0.0 and add types for uuid; enhance migration handling with schema snapshots
1 parent 9f47b98 commit 4510686

10 files changed

Lines changed: 114 additions & 25 deletions

File tree

bridge/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"pg-query-stream": "^4.10.3",
3434
"pino": "^9.14.0",
3535
"ssh2": "^1.17.0",
36-
"uuid": "^8.3.2",
36+
"uuid": "^14.0.0",
3737
"ws": "^8.19.0"
3838
},
3939
"bin": "./dist/index.cjs",
@@ -52,6 +52,7 @@
5252
"@types/jest": "^30.0.0",
5353
"@types/node": "^20.0.0",
5454
"@types/pg": "^8.15.6",
55+
"@types/uuid": "^11.0.0",
5556
"@yao-pkg/pkg": "^5.12.0",
5657
"esbuild": "^0.27.0",
5758
"jest": "^30.2.0",

bridge/pnpm-lock.yaml

Lines changed: 19 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bridge/src/connectors/mariadb.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ export function streamQueryCancelable(
550550
conn = await pool.getConnection();
551551

552552
const [pidRows] = await conn.execute(GET_CONNECTION_ID);
553-
backendPid = pidRows[0].pid;
553+
backendPid = (pidRows as any[])[0].pid;
554554

555555
const raw = (conn as any).connection;
556556
query = raw.query(sql);
@@ -1443,7 +1443,15 @@ export async function connectToDatabase(
14431443
ensureDir(migrationsDir);
14441444
// 1️⃣ Baseline (ONLY if not read-only)
14451445
if (!options?.readOnly) {
1446-
baselineResult = await baselineIfNeeded(cfg, migrationsDir);
1446+
// Pass real schema snapshot so baseline contains actual DDL
1447+
let snapshot: any = undefined;
1448+
try {
1449+
const project = await projectStoreInstance.getProjectByDatabaseId(connectionId);
1450+
if (project) {
1451+
snapshot = await projectStoreInstance.getSchema(project.id) || undefined;
1452+
}
1453+
} catch { }
1454+
baselineResult = await baselineIfNeeded(cfg, migrationsDir, snapshot);
14471455
}
14481456

14491457
// 2️⃣ Load schema (read-only introspection)

bridge/src/connectors/mysql.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ export function streamQueryCancelable(
525525
conn = await pool.getConnection();
526526

527527
const [pidRows] = await conn.execute(GET_CONNECTION_ID);
528-
backendPid = pidRows[0].pid;
528+
backendPid = (pidRows as any[])[0].pid;
529529

530530
const raw = (conn as any).connection;
531531
query = raw.query(sql);
@@ -1110,7 +1110,7 @@ function groupMySQLIndexes(indexes: IndexInfo[]) {
11101110
}
11111111

11121112
return [...map.values()].map(group =>
1113-
group.sort((a, b) => a.seq_in_index - b.seq_in_index)
1113+
group.sort((a, b) => (a.seq_in_index ?? 0) - (b.seq_in_index ?? 0))
11141114
);
11151115
}
11161116

@@ -1418,7 +1418,15 @@ export async function connectToDatabase(
14181418
ensureDir(migrationsDir);
14191419
// 1️⃣ Baseline (ONLY if not read-only)
14201420
if (!options?.readOnly) {
1421-
baselineResult = await baselineIfNeeded(cfg, migrationsDir);
1421+
// Pass real schema snapshot so baseline contains actual DDL
1422+
let snapshot: any = undefined;
1423+
try {
1424+
const project = await projectStoreInstance.getProjectByDatabaseId(connectionId);
1425+
if (project) {
1426+
snapshot = await projectStoreInstance.getSchema(project.id) || undefined;
1427+
}
1428+
} catch {}
1429+
baselineResult = await baselineIfNeeded(cfg, migrationsDir, snapshot);
14221430
}
14231431

14241432
// 2️⃣ Load schema (read-only introspection)

bridge/src/connectors/postgres.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,7 +1176,7 @@ function groupIndexes(indexes: IndexInfo[]) {
11761176
}
11771177

11781178
return [...map.values()].map(group =>
1179-
group.sort((a, b) => a.ordinal_position - b.ordinal_position)
1179+
group.sort((a, b) => (a.ordinal_position ?? 0) - (b.ordinal_position ?? 0))
11801180
);
11811181
}
11821182

@@ -1508,7 +1508,15 @@ export async function connectToDatabase(
15081508
ensureDir(migrationsDir);
15091509

15101510
if (!options?.readOnly) {
1511-
baselineResult = await baselineIfNeeded(cfg, migrationsDir);
1511+
// Pass real schema snapshot so baseline contains actual DDL
1512+
let snapshot: any = undefined;
1513+
try {
1514+
const project = await projectStoreInstance.getProjectByDatabaseId(connectionId);
1515+
if (project) {
1516+
snapshot = await projectStoreInstance.getSchema(project.id) || undefined;
1517+
}
1518+
} catch { }
1519+
baselineResult = await baselineIfNeeded(cfg, migrationsDir, snapshot);
15121520
}
15131521

15141522
// 2️⃣ Load schema (read-only)

bridge/src/connectors/sqlite.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -738,8 +738,8 @@ export async function getSchemaMetadataBatch(
738738
is_foreign_key: fkColumns.has(col.name),
739739
is_unique: false, // will be updated below if unique index exists
740740
is_serial: col.pk > 0 && (col.type || '').toLowerCase() === 'integer',
741-
check_constraint: null,
742-
comment: null,
741+
check_constraint: undefined,
742+
comment: undefined,
743743
ordinal_position: col.cid + 1,
744744
}));
745745

@@ -1172,7 +1172,15 @@ export async function connectToDatabase(
11721172
ensureDir(migrationsDir);
11731173

11741174
if (!options?.readOnly) {
1175-
baselineResult = await baselineIfNeeded(cfg, migrationsDir);
1175+
// Pass real schema snapshot so baseline contains actual DDL
1176+
let snapshot: any = undefined;
1177+
try {
1178+
const project = await projectStoreInstance.getProjectByDatabaseId(connectionId);
1179+
if (project) {
1180+
snapshot = await projectStoreInstance.getSchema(project.id) || undefined;
1181+
}
1182+
} catch {}
1183+
baselineResult = await baselineIfNeeded(cfg, migrationsDir, snapshot);
11761184
}
11771185

11781186
const schema = await listSchemas(cfg);

bridge/src/handlers/migrationHandlers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,8 @@ export class MigrationHandlers {
314314
// For now, we write the SQL and apply it.
315315

316316
const migrationsDir = await projectStoreInstance.resolveMigrationsDir(dbId);
317-
const tmpPath = path.join(migrationsDir, "tmp_snapshot_apply.sql");
317+
const versionStr = Date.now().toString();
318+
const tmpPath = path.join(migrationsDir, `${versionStr}_tmp_snapshot_apply.sql`);
318319
fs.writeFileSync(tmpPath, baselineSQL, "utf8");
319320

320321
if (dbType === "mysql") await this.queryExecutor.mysql.applyMigration(conn, tmpPath);

bridge/src/handlers/projectHandlers.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ export class ProjectHandlers {
217217
await projectStoreInstance.saveSchema(projectId, liveSchemas, newHash, dbType);
218218

219219
if (newHash !== oldHash) {
220-
this.rpc.sendNotification("project.schema_changed", { projectId, newHash });
220+
this.rpc?.sendNotification?.("project.schema_changed", { projectId, newHash });
221221

222222
// Commit to Git if tracking
223223
try {
@@ -431,7 +431,10 @@ export class ProjectHandlers {
431431
pendingMigrations.push({ file, version, isDestructive, destructiveOps });
432432
}
433433

434-
// 6. Compute drift status
434+
// 6. Detect if all migration files are baseline-only (no real user migrations)
435+
const isBaselineOnly = hasMigrations && migrationFiles.every(f => f.includes("baseline"));
436+
437+
// 7. Compute drift status
435438
let driftStatus: "synced" | "drifted" | "unknown" = "unknown";
436439
if (pendingMigrations.length === 0 && hasMigrations) {
437440
// All migrations applied
@@ -449,10 +452,18 @@ export class ProjectHandlers {
449452
driftStatus = "synced";
450453
}
451454

452-
// 7. Compute available modes
455+
// 8. Compute available modes
453456
const availableModes: Array<"run_migrations" | "apply_snapshot" | "skip"> = ["skip"];
454457
let recommendedMode: "run_migrations" | "apply_snapshot" | "skip" = "skip";
455-
if (hasMigrations && pendingMigrations.length > 0) {
458+
459+
if (isBaselineOnly && hasSchemaSnapshot && targetDatabaseEmpty) {
460+
// Baseline-only + empty DB + schema.json exists:
461+
// The baseline file has no real DDL — use schema.json to reconstruct the DB
462+
availableModes.unshift("apply_snapshot");
463+
recommendedMode = "apply_snapshot";
464+
// Override: treat as "no real migrations" for the dialog
465+
driftStatus = "drifted";
466+
} else if (hasMigrations && pendingMigrations.length > 0 && !isBaselineOnly) {
456467
availableModes.unshift("run_migrations");
457468
recommendedMode = "run_migrations";
458469
}
@@ -461,17 +472,23 @@ export class ProjectHandlers {
461472
recommendedMode = "apply_snapshot";
462473
}
463474

475+
// For the dialog: if baseline-only + empty DB, report as "no real migrations"
476+
// so the frontend shows "Apply Schema Snapshot" (STATE 3) instead of
477+
// "Pending Migrations" (STATE 2) which would try to run the empty baseline
478+
const reportHasMigrations = isBaselineOnly && targetDatabaseEmpty ? false : hasMigrations;
479+
const reportPendingMigrations = isBaselineOnly && targetDatabaseEmpty ? [] : pendingMigrations;
480+
464481
const result = {
465-
hasMigrations,
466-
migrationCount: pendingMigrations.length,
482+
hasMigrations: reportHasMigrations,
483+
migrationCount: reportPendingMigrations.length,
467484
hasSchemaSnapshot,
468485
lockFileStatus,
469486
tamperedFiles,
470487
targetDatabaseEmpty,
471488
targetTableCount,
472489
driftStatus,
473490
driftDetails: undefined,
474-
pendingMigrations,
491+
pendingMigrations: reportPendingMigrations,
475492
availableModes,
476493
recommendedMode,
477494
};

bridge/src/services/projectStore.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export type SchemaFile = {
7777
version: number; // bumped to 2
7878
projectId: string;
7979
databaseId: string;
80-
dialect: "postgresql" | "mysql" | "sqlite";
80+
dialect: "postgresql" | "mysql" | "sqlite" | "unknown";
8181
schemas: SchemaSnapshot[];
8282
cachedAt: string;
8383
relwaveVersion: string;
@@ -391,7 +391,7 @@ export class ProjectStore {
391391
// Resolve engine from the linked database
392392
let engine: string | undefined;
393393
try {
394-
const db: DBMeta | null = await dbStoreInstance.getDB(params.databaseId);
394+
const db: DBMeta | undefined = await dbStoreInstance.getDB(params.databaseId);
395395
engine = db?.type;
396396
} catch {
397397
// db may not exist yet — that's OK
@@ -564,7 +564,7 @@ export class ProjectStore {
564564
// Resolve engine from the linked database
565565
let engine: string | undefined;
566566
try {
567-
const db: DBMeta | null = await dbStoreInstance.getDB(databaseId);
567+
const db: DBMeta | undefined = await dbStoreInstance.getDB(databaseId);
568568
engine = db?.type;
569569
} catch {
570570
// db may not exist yet
@@ -1384,7 +1384,7 @@ export class ProjectStore {
13841384
// ---- 3. Resolve engine from the linked database ----
13851385
let engine: string | undefined;
13861386
try {
1387-
const db: DBMeta | null = await dbStoreInstance.getDB(databaseId);
1387+
const db: DBMeta | undefined = await dbStoreInstance.getDB(databaseId);
13881388
engine = db?.type;
13891389
} catch {
13901390
// db may not exist yet

bridge/src/utils/baselineMigration.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,29 @@ export function generateBaselineSQL(snapshot: SchemaFile, version: string, name:
4141

4242
// Tables
4343
for (const table of schema.tables) {
44+
// Skip internal migration tracking tables
45+
if (["schema_migrations", "__relwave_migrations", "relwave_migrations"].includes(table.name)) {
46+
continue;
47+
}
48+
4449
const tableRef = dbType === "sqlite" ? quoteIdent(table.name, dbType) : `${quoteIdent(schema.name, dbType)}.${quoteIdent(table.name, dbType)}`;
4550

51+
// Check for sequences first (Postgres)
52+
if (dbType === "postgresql") {
53+
for (const col of table.columns) {
54+
if (col.defaultValue && col.defaultValue.includes("nextval(")) {
55+
// Extract 'windows_store_apps_id_seq' from nextval('windows_store_apps_id_seq'::regclass)
56+
const seqMatch = col.defaultValue.match(/nextval\('([^']+)'/i) || col.defaultValue.match(/nextval\("([^"]+)"/i);
57+
if (seqMatch && seqMatch[1]) {
58+
const seqName = seqMatch[1];
59+
const isSchemaQualified = seqName.includes(".");
60+
const seqRef = isSchemaQualified ? seqName : `${quoteIdent(schema.name, dbType)}.${quoteIdent(seqName, dbType)}`;
61+
upSQL += `CREATE SEQUENCE IF NOT EXISTS ${seqRef};\n`;
62+
}
63+
}
64+
}
65+
}
66+
4667
const columnDefs = table.columns.map(col => {
4768
let def = ` ${quoteIdent(col.name, dbType)} ${col.type}`;
4869
if (!col.nullable) def += " NOT NULL";

0 commit comments

Comments
 (0)