windmill-labs/windmill · error

Invalid migration name

Error message

Invalid migration name '${name}': use only letters, digits, '_' and '-'

What it means

`createMigration` scaffolds empty `<timestamp>_<name>.up.sql`/`.down.sql` files for a datatable migration. It validates the name against a regex allowing only letters, digits, underscore and hyphen, and throws this error for anything else (spaces, dots, slashes, etc.), preventing invalid filenames or path traversal.

Solutions

  1. Rename the migration using only letters, digits, '_' and '-', e.g. `add-users-table`.
  2. Quote the argument in your shell to avoid word-splitting, then replace offending characters.
  3. Use kebab-case or snake_case names going forward.

Example fix

// before
wmill datatable migrate new main add users table
// after
wmill datatable migrate new main add-users-table
Defensive patterns

Strategy: validation

Validate before calling

const RE = /^[A-Za-z0-9_-]+$/;
if (!RE.test(name)) throw new Error(`migration name '${name}' must match [A-Za-z0-9_-]`);

Type guard

function isValidMigrationName(n: string): boolean { return /^[A-Za-z0-9_-]+$/.test(n); }

Try / catch

try { createMigration(dt, name) } catch (e) { if (String(e).includes('Invalid migration name')) name = name.replace(/[^A-Za-z0-9_-]/g, '-'); }

Prevention

When it happens

Trigger: Running `wmill datatable migrate new <datatable> <name>` where `<name>` contains characters outside [A-Za-z0-9_-] — e.g. spaces, dots ('1.2.3'), slashes, or shell-expanded glob characters.

Common situations: Copy-pasting a description with spaces as the migration name; using dotted version numbers; accidentally passing a file path as the name.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/4601d45c40bdc482. Report an issue: GitHub.

Appendix: source

Thrown at cli/src/commands/datatable_migrations.ts:53

function nextMigrationTimestamp(dir: string): string {
  const now = Number(migrationTimestamp());
  let max = 0;
  if (fs.existsSync(dir)) {
    for (const file of fs.readdirSync(dir)) {
      const m = file.match(/^(\d+)_.*\.(up|down)\.sql$/);
      if (m) max = Math.max(max, Number(m[1]));
    }
  }
  return String(max >= now ? max + 1 : now);
}

/**
 * Scaffold a new migration under migrations/datatable/<datatable>/ as empty
 * `<timestamp>_<name>.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 = nextMigrationTimestamp(dir);
  const base = `${timestamp}_${name}`;
  const up = path.join(dir, `${base}.up.sql`);
  const down = path.join(dir, `${base}.down.sql`);
  // Frame the body in an explicit transaction so it applies atomically, matching
  // the template the UI's "New migration" modal seeds.
  const template = (direction: string) =>
    `-- ${direction} migration: ${name}\nBEGIN;\n\n-- Add your migration here\n\nEND;\n`;
  fs.writeFileSync(up, template("up"), "utf-8");
  fs.writeFileSync(down, template("down"), "utf-8");

  log.info(

View on GitHub (pinned to e474e8803c)