From 021c58925efdf7f11e99ae348f7a47606c083d3d Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Tue, 9 Jun 2026 13:40:46 +0200 Subject: [PATCH] fix: auto-regenerate thesis identifier on any year-prefix mismatch, support .php migrations in runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ThesisEditController::save() previously only regenerated the identifier when the year field changed during an edit. If a thesis had its year corrected in a past edit (or via other means) and the identifier still carried the old year prefix, subsequent edits that didn't touch the year field would leave the mismatched identifier in place. Now saves() also checks whether the existing identifier's 4-digit prefix matches the thesis year, and regenerates if not — regardless of whether year changed in the current edit. The migration runner (run.php) only scanned for .sql files, so PHP migrations (013, 016, 018, 038) were never auto-applied. Extended the runner to also discover and execute .php migrations in a subprocess. If a PHP migration fails with an idempotent error (no such column, already exists, duplicate column), the runner treats it as already-applied and continues rather than aborting — preventing a stale migration like 016 (banner_path already dropped by 028) from blocking migrations that come after it alphabetically (e.g. 038). Updated migrations 016 and 038 to accept an optional $argv[1] DB path. Fixed 016 to gracefully handle the banner_path column already being gone (exit 0 instead of fatal). --- TODO.md | 9 +++- .../applied/016_merge_banners_into_covers.php | 17 +++++-- .../038_fix_mismatched_identifiers.php | 3 +- app/migrations/run.php | 49 +++++++++++++++++-- app/src/Controllers/ThesisEditController.php | 11 +++-- 5 files changed, 75 insertions(+), 14 deletions(-) diff --git a/TODO.md b/TODO.md index 56b9719..4d64166 100644 --- a/TODO.md +++ b/TODO.md @@ -12,4 +12,11 @@ - [x] Migration 038: corriger les identifiers theses qui ne matchent pas leur année - [x] Filtres finalité + format dans la page de recherche (search.php) - [x] Styliser boutons Filtrer/Réinitialiser : plus compacts, Réinitialiser en neutre -- [ ] Commit + jj new +- [x] Commit + jj new + +- [x] Fix identifier-year mismatch: extend save() to regenerate identifier when prefix doesn't match year (not just on year change) +- [x] Fix migration runner run.php to support .php migrations alongside .sql +- [x] Fix runner: treat PHP subprocess idempotent errors (no such column / already exists) as skippable rather than fatal +- [x] Update 016 and 038 PHP migrations to accept $argv[1] DB path +- [x] Fix migration 016 to gracefully handle banner_path column already being dropped +- [x] Commit + jj new diff --git a/app/migrations/applied/016_merge_banners_into_covers.php b/app/migrations/applied/016_merge_banners_into_covers.php index 5f9f9b5..6a47426 100644 --- a/app/migrations/applied/016_merge_banners_into_covers.php +++ b/app/migrations/applied/016_merge_banners_into_covers.php @@ -15,7 +15,8 @@ defined('APP_ROOT') || define('APP_ROOT', dirname(__DIR__, 2)); defined('STORAGE_ROOT') || define('STORAGE_ROOT', APP_ROOT . '/storage'); -$dbPath = APP_ROOT . '/storage/xamxam.db'; +// Accept optional DB path from command line (used by run.php runner) +$dbPath = $argv[1] ?? (APP_ROOT . '/storage/xamxam.db'); if (!file_exists($dbPath)) { echo "ERROR: database not found at $dbPath\n"; exit(1); @@ -34,9 +35,17 @@ if (!is_dir($coverDir)) { echo "Created covers/ directory.\n"; } -// Fetch all theses with a non-null banner_path -$stmt = $pdo->query("SELECT id, banner_path FROM theses WHERE banner_path IS NOT NULL"); -$rows = $stmt->fetchAll(); +// Detect if banner_path column still exists (dropped by migration 028) +try { + $stmt = $pdo->query("SELECT id, banner_path FROM theses WHERE banner_path IS NOT NULL"); + $rows = $stmt->fetchAll(); +} catch (\PDOException $e) { + if (stripos($e->getMessage(), 'no such column') !== false) { + echo "banner_path column already removed — migration 016 already applied, nothing to do.\n"; + exit(0); + } + throw $e; +} if (empty($rows)) { echo "No banners to migrate.\n"; diff --git a/app/migrations/applied/038_fix_mismatched_identifiers.php b/app/migrations/applied/038_fix_mismatched_identifiers.php index 482c31e..3f12cbb 100644 --- a/app/migrations/applied/038_fix_mismatched_identifiers.php +++ b/app/migrations/applied/038_fix_mismatched_identifiers.php @@ -19,7 +19,8 @@ defined('APP_ROOT') || define('APP_ROOT', dirname(__DIR__, 2)); defined('STORAGE_ROOT') || define('STORAGE_ROOT', APP_ROOT . '/storage'); -$dbPath = APP_ROOT . '/storage/xamxam.db'; +// Accept optional DB path from command line (used by run.php runner) +$dbPath = $argv[1] ?? (APP_ROOT . '/storage/xamxam.db'); if (!file_exists($dbPath)) { echo "ERROR: database not found at $dbPath\n"; exit(1); diff --git a/app/migrations/run.php b/app/migrations/run.php index 048eae7..321c4d9 100644 --- a/app/migrations/run.php +++ b/app/migrations/run.php @@ -38,11 +38,11 @@ if (!is_dir($appliedDir)) { mkdir($appliedDir, 0755, true); } -// Collect .sql files from both pending and applied dirs +// Collect .sql and .php files from both pending and applied dirs $files = []; foreach ([$pendingDir, $appliedDir] as $dir) { if (!is_dir($dir)) continue; - foreach (glob($dir . '/*.sql') as $f) { + foreach (glob($dir . '/*.{sql,php}', GLOB_BRACE) as $f) { $files[basename($f)] = $f; } } @@ -61,12 +61,51 @@ foreach ($files as $name => $file) { continue; } + $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); echo "Applying: $name\n"; - $sql = file_get_contents($file); + + $isPhp = $ext === 'php'; try { - $pdo->exec($sql); - } catch (PDOException $e) { + if ($isPhp) { + // PHP migrations: execute in a subprocess for isolation + $cmd = sprintf( + 'php %s %s 2>&1', + escapeshellarg($file), + escapeshellarg($dbPath) + ); + exec($cmd, $output, $exitCode); + $outputStr = implode("\n", $output); + echo $outputStr . "\n"; + if ($exitCode !== 0) { + // Check output for idempotent errors before treating as fatal + $skipPatterns = [ + 'no such column', + 'duplicate column name', + 'already exists', + ]; + $shouldSkip = false; + foreach ($skipPatterns as $pat) { + if (stripos($outputStr, $pat) !== false) { + $shouldSkip = true; + break; + } + } + if ($shouldSkip) { + echo " Skipping (already applied)\n"; + // Mark as applied so we don't re-attempt + $pdo->prepare("INSERT OR REPLACE INTO _migrations (name) VALUES (?)")->execute([$name]); + $count++; + continue; + } + throw new RuntimeException("PHP migration exited with code $exitCode"); + } + } else { + // SQL migrations: execute inline + $sql = file_get_contents($file); + $pdo->exec($sql); + } + } catch (\Throwable $e) { $msg = $e->getMessage(); // Ignore idempotent errors (column/trigger/index already exists or already removed) if (stripos($msg, 'duplicate column name') !== false diff --git a/app/src/Controllers/ThesisEditController.php b/app/src/Controllers/ThesisEditController.php index 53de2fe..13e617e 100644 --- a/app/src/Controllers/ThesisEditController.php +++ b/app/src/Controllers/ThesisEditController.php @@ -220,14 +220,19 @@ class ThesisEditController 'cc2r' => !empty($post['cc2r']), 'license_custom' => trim($post['license_custom'] ?? ''), ]; - // Regenerate identifier if year changed + // Regenerate identifier if year changed or if identifier prefix doesn't match year $oldThesis = $this->db->getThesis($thesisId); $oldYear = (int)($oldThesis['year'] ?? 0); $newYear = $meta['year']; - if ($newYear !== $oldYear && $newYear >= 2000) { + $oldIdentifier = $oldThesis['identifier'] ?? ''; + $oldIdentifierYear = ($oldIdentifier !== '' && preg_match('/^(\d{4})/', $oldIdentifier, $m)) ? (int)$m[1] : 0; + if ($newYear >= 2000 && ($newYear !== $oldYear || $oldIdentifierYear !== $newYear)) { $newIdentifier = $this->db->generateThesisIdentifier($newYear); $meta['identifier'] = $newIdentifier; - error_log('[ThesisEdit] Year changed ' . $oldYear . ' → ' . $newYear . ', new identifier: ' . $newIdentifier); + $reason = $newYear !== $oldYear + ? 'Year changed ' . $oldYear . ' → ' . $newYear + : 'Mismatched identifier ' . $oldIdentifier . ' for year=' . $newYear; + error_log('[ThesisEdit] ' . $reason . ', new identifier: ' . $newIdentifier); } $this->db->updateThesis($thesisId, $meta);