Files
xamxam/app/migrations/run.php
Pontoporeia e3896811c4 Fix migrations and deploy issues + errors + linting
- scan both pending/ and applied/ dirs so remote catch-up works
- fix remote 500s: run.php handles per-statement errors so VIEW rebuilds run after duplicate columns; replace mb_strimwidth with substr (no mbstring extension on server)
- add missing migration: 015_license_custom.sql (column existed in schema.sql but was never migrated)
- remote: fgetcsv enclosure single-char + AdminLogger permission-denied
guard + deploy always migrates
- fix admin-filters wrapping: restore flex-wrap, flex-basis on
inputs/selects, shrink-protect buttons
- fix phpstan: remove redundant ?? [] after isset guard in
ThesisEditController
- biome: exclude vendored min.js via includes patterns;
lint whole js dir; modernise beforeunload-guard.js
2026-05-08 22:58:05 +02:00

101 lines
2.7 KiB
PHP

#!/usr/bin/env php
<?php
/**
* Run pending migrations on the production database.
*
* Usage: php app/migrations/run.php [DB_PATH]
*
* If no DB_PATH is given, defaults to storage/xamxam.db.
*
* Scans both migrations/pending/ and migrations/applied/ for .sql files.
* Each is applied in alphabetical order if not already tracked in _migrations.
*/
$root = dirname(__DIR__);
$dbPath = $argv[1] ?? ($root . '/storage/xamxam.db');
if (!file_exists($dbPath)) {
die("Database not found: $dbPath\n");
}
$pdo = new PDO('sqlite:' . $dbPath);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Create migrations tracking table
$pdo->exec("
CREATE TABLE IF NOT EXISTS _migrations (
name TEXT PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
");
$applied = $pdo->query("SELECT name FROM _migrations")->fetchAll(PDO::FETCH_COLUMN);
$pendingDir = $root . '/migrations/pending';
$appliedDir = $root . '/migrations/applied';
if (!is_dir($appliedDir)) {
mkdir($appliedDir, 0755, true);
}
// Collect .sql files from both pending and applied dirs
$files = [];
foreach ([$pendingDir, $appliedDir] as $dir) {
if (!is_dir($dir)) continue;
foreach (glob($dir . '/*.sql') as $f) {
$files[basename($f)] = $f;
}
}
ksort($files);
if (empty($files)) {
echo "No pending migration files.\n";
exit(0);
}
$count = 0;
foreach ($files as $name => $file) {
if (in_array($name, $applied, true)) {
echo "Skip (already applied): $name\n";
continue;
}
echo "Applying: $name\n";
$sql = file_get_contents($file);
// Split into individual statements to handle partial failures gracefully
// (e.g. ALTER TABLE may fail with "duplicate column" but DROP VIEW must still run)
$statements = array_filter(
array_map('trim', explode(';', $sql)),
fn($s) => $s !== ''
);
$errors = [];
foreach ($statements as $stmt) {
try {
$pdo->exec($stmt . ';');
} catch (PDOException $e) {
$msg = $e->getMessage();
if (stripos($msg, 'duplicate column name') !== false) {
echo " Skipping (column exists): " . substr($stmt, 0, 60) . "...\n";
continue;
}
$errors[] = $msg;
}
}
if (empty($errors)) {
$pdo->prepare("INSERT OR REPLACE INTO _migrations (name) VALUES (?)")->execute([$name]);
if (str_starts_with($file, $pendingDir)) {
rename($file, $appliedDir . '/' . $name);
}
$count++;
} else {
echo " FAILED: " . implode(' | ', $errors) . "\n";
throw new PDOException(implode(' | ', $errors));
}
}
echo "$count migration(s) applied.\n";