From 57e22477fb1b8e31c616b3ce2c65d3700970dd13 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sat, 25 Jul 2026 15:35:34 +0300 Subject: [PATCH] Add an idempotent CLI migration runner phpMyAdmin is unreachable on this deployment, so schema changes need a path that does not depend on it. migrate.php reuses core/Database, meaning no credentials are passed on the command line, and checks information_schema before each ALTER so re-running is safe and never fails on a duplicate column. Supports --status and --dry-run. Covers the blind-index columns and the missing adminUser status/approved_by/ approved_at columns. Every change is additive and nullable, so applying it to a running database changes no behaviour on its own. Co-Authored-By: Claude Opus 5 --- backend/scripts/migrate.php | 163 ++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 backend/scripts/migrate.php diff --git a/backend/scripts/migrate.php b/backend/scripts/migrate.php new file mode 100644 index 00000000..6e60cef6 --- /dev/null +++ b/backend/scripts/migrate.php @@ -0,0 +1,163 @@ +prepare( + "SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?" + ); + $st->execute([$table, $column]); + return (int) $st->fetchColumn() > 0; +} + +/** هل يوجد الفهرس؟ */ +function hasIndex(PDO $con, string $table, string $index): bool +{ + $st = $con->prepare( + "SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?" + ); + $st->execute([$table, $index]); + return (int) $st->fetchColumn() > 0; +} + +function tableExists(PDO $con, string $table): bool +{ + $st = $con->prepare( + "SELECT COUNT(*) FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?" + ); + $st->execute([$table]); + return (int) $st->fetchColumn() > 0; +} + +// ── تعريف الترحيلات ────────────────────────────────────────── +// كلها إضافية: أعمدة جديدة تقبل NULL. لا تُعدَّل ولا تُحذف بيانات قائمة، +// ولا يقرأها أي كود قبل تعبئتها، فتطبيقها على قاعدة تعمل بلا أثر. +$columns = [ + ['driver', 'phone_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للبحث بالهاتف'"], + ['driver', 'email_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للبحث بالبريد'"], + ['driver', 'name_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للاسم بعد التطبيع'"], + ['passengers', 'phone_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للبحث بالهاتف'"], + ['passengers', 'email_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للبحث بالبريد'"], + ['passengers', 'name_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للاسم بعد التطبيع'"], + ['adminUser', 'phone_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للبحث بالهاتف'"], + ['adminUser', 'email_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC للبحث بالبريد'"], + // أعمدة موافقات المشرفين المفقودة في هذا النشر + ['adminUser', 'status', "VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active | pending | suspended | rejected'"], + ['adminUser', 'approved_by', "VARCHAR(32) NULL DEFAULT NULL"], + ['adminUser', 'approved_at', "TIMESTAMP NULL DEFAULT NULL"], +]; + +$indexes = [ + ['driver', 'idx_driver_phone_bidx', 'phone_bidx'], + ['driver', 'idx_driver_email_bidx', 'email_bidx'], + ['driver', 'idx_driver_name_bidx', 'name_bidx'], + ['passengers', 'idx_passengers_phone_bidx', 'phone_bidx'], + ['passengers', 'idx_passengers_email_bidx', 'email_bidx'], + ['passengers', 'idx_passengers_name_bidx', 'name_bidx'], + ['adminUser', 'idx_adminuser_phone_bidx', 'phone_bidx'], + ['adminUser', 'idx_adminuser_email_bidx', 'email_bidx'], +]; + +$applied = 0; +$skipped = 0; +$missing = []; + +echo "── Siro schema migration ──\n"; +echo "database: " . $con->query('SELECT DATABASE()')->fetchColumn() . "\n"; +if ($dryRun) echo "mode: DRY RUN (nothing will be executed)\n"; +if ($status) echo "mode: STATUS ONLY\n"; +echo "\n"; + +foreach ($columns as [$table, $column, $definition]) { + if (!tableExists($con, $table)) { + $missing[$table] = true; + continue; + } + if (hasColumn($con, $table, $column)) { + echo " = $table.$column already present\n"; + $skipped++; + continue; + } + + $sql = "ALTER TABLE `$table` ADD COLUMN `$column` $definition"; + if ($status || $dryRun) { + echo " + would run: $sql\n"; + continue; + } + + try { + $con->exec($sql); + echo " + $table.$column added\n"; + $applied++; + } catch (PDOException $e) { + echo " ✘ $table.$column failed: " . $e->getMessage() . "\n"; + } +} + +foreach ($indexes as [$table, $index, $column]) { + if (!tableExists($con, $table) || !hasColumn($con, $table, $column)) continue; + if (hasIndex($con, $table, $index)) { + echo " = $table.$index already present\n"; + $skipped++; + continue; + } + + $sql = "ALTER TABLE `$table` ADD INDEX `$index` (`$column`)"; + if ($status || $dryRun) { + echo " + would run: $sql\n"; + continue; + } + + try { + $con->exec($sql); + echo " + $table.$index created\n"; + $applied++; + } catch (PDOException $e) { + echo " ✘ $table.$index failed: " . $e->getMessage() . "\n"; + } +} + +foreach (array_keys($missing) as $table) { + echo " ! table `$table` does not exist in this database — skipped\n"; +} + +echo "\napplied: $applied · already in place: $skipped\n"; + +if (!$status && !$dryRun && $applied > 0) { + echo "\nNext: populate the indexes\n"; + echo " php scripts/backfill_blind_index.php --dry-run\n"; + echo " php scripts/backfill_blind_index.php\n"; +}