turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

OPS-009

Block the admin when migrations are pending

Code that queries a column the database does not have fails with an unstyled fatal. A guard that names the missing migration turns that into a two-minute fix.

Upload a release and forget the migration, and the first admin page that touches the new column dies with a database error. On a production site that means an unstyled white page with a query in it, and no clue which file was missed.

A guard at the top of the admin removes the entire failure class. Compare the migration files on disk to the rows in the migrations table; if any are missing, show a 503 that names them and links to the schema screen.

Exempt login, logout and the migration runner itself, or the guard locks you out of the page that fixes it.

PHP
function require_schema_current(PDO $pdo): void
{
    $script = basename((string) ($_SERVER['SCRIPT_NAME'] ?? ''));
    if (in_array($script, ['login.php', 'logout.php', 'schema.php', 'migrate.php'], true)) {
        return;                                      // never block the way out
    }

    $pending = pending_migrations($pdo);
    if ($pending === []) {
        return;
    }

    http_response_code(503);
    header('Retry-After: 300');
    echo '<!doctype html><meta charset="utf-8"><title>Database is behind</title>';
    echo '<style>body{font:16px system-ui;margin:60px auto;max-width:34rem;color:#101A24}'
       . 'code{background:#EEF1F5;padding:2px 6px;border-radius:3px}</style>';
    echo '<h1>The database is behind the code</h1>';
    echo '<p>' . count($pending) . ' migration(s) have not been applied, so parts of the '
       . 'admin would fail. Apply them and this page clears itself.</p><ul>';
    foreach ($pending as $file) {
        echo '<li><code>' . htmlspecialchars($file) . '</code></li>';
    }
    echo '</ul><p><a href="schema.php">Open the schema screen</a></p>';
    exit;
}

function pending_migrations(PDO $pdo): array
{
    try {
        $done = $pdo->query('SELECT file FROM migrations')->fetchAll(PDO::FETCH_COLUMN);
    } catch (PDOException $e) {
        $done = [];                                  // no table yet: everything is pending
    }
    $files = array_map('basename', glob(APP_ROOT . '/migrations/*.sql') ?: []);
    sort($files);

    return array_values(array_diff($files, $done));
}

Using it

Put the same check on the public side as a softer version: log it, and keep serving. Visitors should not see a maintenance page because an admin screen would break.

Make the fatal error page styled and helpful too. The guard catches the predictable case; the generic handler catches the rest.

Link to a schema screen that lists applied and pending files, with the mark-as-applied escape hatch.

What bites people

Exempt the right scripts. A guard that blocks the schema screen is a guard that requires database access to clear.

A 503 with Retry-After is the honest status. A 500 tells monitoring the site is broken; a 200 with an error page tells it nothing.

If the migrations table itself is missing, treat every file as pending rather than throwing.

This one touches real money. Point it at a throwaway wallet and watch a full cycle before you trust it with a live balance.

Also in Admin and Operations