decolua/9router · warning

[DB][migrate] pre-schema backup failed (continuing): ${e.mes

Error message

[DB][migrate] pre-schema backup failed (continuing): ${e.message}

What it means

runMigrationOnce in the SQLite layer takes a lightweight backup of the database before mutating the schema (when stored backupSchemaVersion < SCHEMA_VERSION and the DB is not fresh). If makeBackupDir, backupDbLite, or pruneOldBackups throws, the migration logs this warning and continues anyway — migrations proceed WITHOUT a pre-schema backup. This is deliberate fail-open behavior so a backup failure (e.g. full disk, read-only dir) doesn't block app startup.

Source

Thrown at src/lib/db/migrate.js:242

  // Prune stale backups every boot so old oversized backups shrink to KEEP.
  pruneOldBackups();

  // Bootstrap _meta so we can read the stored backup schema version below
  // (runVersionedMigrations also ensures this, but we need it earlier here).
  adapter.exec(buildCreateTableSql("_meta", TABLES._meta));

  // Detect a pending schema change via the central SCHEMA_VERSION const.
  // A lightweight backup is taken BEFORE any schema mutation below.
  const storedSchemaVer = parseInt(getMetaSync(adapter, "backupSchemaVersion", "0"), 10) || 0;
  const schemaChanging = !fresh && storedSchemaVer < SCHEMA_VERSION;
  if (schemaChanging) {
    try {
      const backupDir = makeBackupDir(`schema-${storedSchemaVer}-to-${SCHEMA_VERSION}`);
      backupDbLite(adapter, backupDir);
      pruneOldBackups();
      console.log(`[DB][migrate] pre-schema backup ${storedSchemaVer} → ${SCHEMA_VERSION}: ${backupDir}`);
    } catch (e) {
      console.warn(`[DB][migrate] pre-schema backup failed (continuing): ${e.message}`);
    }
  }

  // 1. Always run versioned migrations chain (skip-version safe)
  const migInfo = runVersionedMigrations(adapter);

  // 2. Additive sync (auto add missing columns/indexes declared in TABLES)
  syncSchemaFromTables(adapter);

  // Stamp the schema version we just reached so future boots skip re-backup.
  setMetaSync(adapter, "backupSchemaVersion", SCHEMA_VERSION);

  // 3. One-time legacy JSON import (only if DB was fresh on entry)
  const alreadyImported = fs.existsSync(MIGRATED_MARKER);
  const legacyMain = readJsonSafe(LEGACY_FILES.main);
  const legacyUsage = readJsonSafe(LEGACY_FILES.usage);
  const legacyDisabled = readJsonSafe(LEGACY_FILES.disabled);
  const legacyDetails = readJsonSafe(LEGACY_FILES.details);

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Check disk space and permissions on the DB directory (resolve via DATA_DIR or ~/.9router) and fix, then restart so the next schema change backs up correctly.
  2. Re-run the process with write permission to the data dir (chown/chmod the dir or run as the correct user).
  3. Manually copy the SQLite DB file before upgrading versions if automated backups keep failing.
  4. If migrations themselves then fail, restore from a manual snapshot; the backup warning means no safety net existed.

Example fix

// before: silently continues without backup
} catch (e) {
  console.warn(`[DB][migrate] pre-schema backup failed (continuing): ${e.message}`);
}
// after: fail fast when backup is not possible in critical envs
} catch (e) {
  if (process.env.STRICT_DB_BACKUP === "1") throw e;
  console.warn(`[DB][migrate] pre-schema backup failed (continuing): ${e.message}`);
}
Defensive patterns

Strategy: fallback

Validate before calling

// before boot, ensure the DB dir is writable
import fs from "fs";
const dir = process.env.DATA_DIR || require("os").homedir() + "/.9router";
fs.mkdirSync(dir, { recursive: true });
fs.accessSync(dir, fs.constants.W_OK);

Try / catch

// wrap init; if you require backups, fail closed
try {
  await initAdapter();
  console.warn("[DB] started WITHOUT a verified pre-schema backup");
} catch (e) {
  if (e.message.includes("backup failed")) {
    console.error("Backup failed and strict mode is on — aborting");
    process.exit(1);
  }
  throw e;
}

Prevention

When it happens

Trigger: Booting with an existing DB whose backupSchemaVersion is lower than SCHEMA_VERSION while the backup step throws: DATA_DIR/backup directory cannot be created (EACCES/ENOENT), disk full, backupDbLite copy fails on a locked/native-SQLite handle, or pruneOldBackups hits an IO error.

Common situations: Running the server as a different user than the one that owns ~/.9router; read-only or full disk in containers; Windows file locks on the SQLite file; NUCLEAR/restricted home permissions after OS upgrade; Docker volume mounted read-only.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/e32a2fb5c1abe76e. Report an issue: GitHub.