feat(cli): push local datatable migrations before running on migrate up

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-07-03 13:29:52 +02:00
parent a3f0b82f70
commit 54aed77ab7
2 changed files with 64 additions and 0 deletions
+4
View File
@@ -11,6 +11,7 @@ import { psql as psqlDatatable } from "./psql.ts";
import { serve as serveDatatable } from "./serve.ts";
import {
createMigration,
pushLocalMigrations,
rollbackMigrations,
runMigrations,
} from "../datatable_migrations.ts";
@@ -73,6 +74,9 @@ async function migrateUp(opts: GlobalOptions & { datatable?: string }) {
return;
}
for (const dt of targets) {
// Push any locally-created/edited migration files first (without running
// them), so `migrate up` works even before a `wmill sync push`.
await pushLocalMigrations(workspace.workspaceId, dt);
await runMigrations(workspace.workspaceId, dt);
}
}
+60
View File
@@ -213,6 +213,66 @@ export async function pushMigrationFromDisk(
});
}
/**
* Upsert the on-disk migrations of a data table to the workspace, so a freshly
* created migration file works with `wmill datatable migrate up` even without a
* prior `wmill sync push`. Pushes only migrations that are new or edited
* (compared against the workspace's current definitions); it never deletes
* remote migrations absent on disk and never touches other item kinds.
*/
export async function pushLocalMigrations(
workspace: string,
datatableName: string,
): Promise<void> {
const dir = path.join(process.cwd(), MIGRATIONS_DIR, datatableName);
if (!fs.existsSync(dir)) return;
// Local migrations are identified by their `.up.sql` file (the up file is
// mandatory); this deliberately ignores files that were only deleted locally.
const local: { timestamp: number; name: string }[] = [];
for (const file of fs.readdirSync(dir)) {
const m = file.match(/^(\d+)_(.*)\.up\.sql$/);
if (m) local.push({ timestamp: Number(m[1]), name: m[2] });
}
if (local.length === 0) return;
const remote = await wmill.listDatatableMigrations({ workspace });
const remoteByTs = new Map(
remote
.filter((r) => r.datatable === datatableName)
.map((r) => [r.timestamp, r] as const),
);
for (const { timestamp, name } of local) {
const base = `${timestamp}_${name}`;
const code_up = await readTextFile(path.join(dir, `${base}.up.sql`));
const downPath = path.join(dir, `${base}.down.sql`);
const code_down = fs.existsSync(downPath)
? await readTextFile(downPath)
: undefined;
const r = remoteByTs.get(timestamp);
const unchanged =
r !== undefined &&
r.name === name &&
r.code_up === code_up &&
(r.code_down ?? undefined) === code_down;
if (unchanged) continue;
log.info(colors.green(`Pushing datatable_migration ${datatableName}/${base}`));
await wmill.upsertDatatableMigration({
workspace,
datatableName,
requestBody: {
timestamp,
name,
code_up,
...(code_down !== undefined ? { code_down } : {}),
},
});
}
}
/**
* After a push that introduced new migrations, list them and (interactively)
* offer to run them, equivalent to `wmill datatable migrate up` on each affected