diff --git a/cli/src/commands/datatable/datatable.ts b/cli/src/commands/datatable/datatable.ts index a5cfe08bf3..35f8481bba 100644 --- a/cli/src/commands/datatable/datatable.ts +++ b/cli/src/commands/datatable/datatable.ts @@ -10,6 +10,7 @@ import { runCatalogQuery } from "../../utils/catalog.ts"; import { psql as psqlDatatable } from "./psql.ts"; import { serve as serveDatatable } from "./serve.ts"; import { + createMigration, rollbackMigrations, runMigrations, } from "../datatable_migrations.ts"; @@ -45,6 +46,13 @@ async function run( await runCatalogQuery(opts, "datatable", name, sql); } +function migrateNew( + opts: GlobalOptions & { datatable?: string }, + name: string, +) { + createMigration(opts.datatable ?? DEFAULT_DATATABLE_NAME, name); +} + async function migrateUp(opts: GlobalOptions, name?: string) { const workspace = await resolveWorkspace(opts); await requireLogin(opts); @@ -58,7 +66,14 @@ async function migrateDown(opts: GlobalOptions, name?: string) { } const migrateCommand = new Command() - .description("apply or roll back datatable migrations") + .description("manage datatable migrations") + .command("new", "scaffold a new migration (.up.sql / .down.sql files)") + .arguments("") + .option( + "-d --datatable ", + "Target datatable (default: main)", + ) + .action(migrateNew as any) .command("up", "apply all pending migrations to a datatable") .arguments("[name:string]") .action(migrateUp as any) diff --git a/cli/src/commands/datatable_migrations.ts b/cli/src/commands/datatable_migrations.ts index 966f63fa11..4ea7ea34b6 100644 --- a/cli/src/commands/datatable_migrations.ts +++ b/cli/src/commands/datatable_migrations.ts @@ -11,6 +11,47 @@ import { Confirm } from "@cliffy/prompt/confirm"; // target data table, as `_.up.sql` (and optional `.down.sql`). const MIGRATIONS_DIR = "datatable_migrations"; +// Migration names map directly onto file names and the DB `name` column. +const MIGRATION_NAME_RE = /^[a-zA-Z0-9_-]+$/; + +/** Current UTC time as a YYYYMMDDHHMMSS migration version. */ +function migrationTimestamp(): string { + const d = new Date(); + const p = (n: number) => String(n).padStart(2, "0"); + return ( + `${d.getUTCFullYear()}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}` + + `${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}` + ); +} + +/** + * Scaffold a new migration under datatable_migrations// as empty + * `_.up.sql` and `.down.sql` files. Purely local — no network. + */ +export function createMigration(datatable: string, name: string): void { + if (!MIGRATION_NAME_RE.test(name)) { + throw new Error( + `Invalid migration name '${name}': use only letters, digits, '_' and '-'`, + ); + } + const dir = path.join(process.cwd(), MIGRATIONS_DIR, datatable); + fs.mkdirSync(dir, { recursive: true }); + + const timestamp = migrationTimestamp(); + const base = `${timestamp}_${name}`; + const up = path.join(dir, `${base}.up.sql`); + const down = path.join(dir, `${base}.down.sql`); + fs.writeFileSync(up, `-- up migration: ${name}\n`, "utf-8"); + fs.writeFileSync(down, `-- down migration: ${name}\n`, "utf-8"); + + log.info( + colors.green(`Created migration ${base} in ${MIGRATIONS_DIR}/${datatable}/`), + ); + for (const f of [up, down]) { + log.info(colors.gray(` ${path.relative(process.cwd(), f)}`)); + } +} + /** * Apply the workspace's pending migrations to a data table (forwards migrations * recorded in `_wm_migrations`). Mirrors `wmill datatable migrate up`.