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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6802026dbd
commit
57e22477fb
@@ -0,0 +1,163 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* scripts/migrate.php
|
||||||
|
*
|
||||||
|
* مُشغّل ترحيلات بسيط يعمل من سطر الأوامر داخل حاوية php.
|
||||||
|
* يستخدم نفس إعدادات الاتصال في core/Database — لا تحتاج phpMyAdmin ولا
|
||||||
|
* تمرير كلمة مرور القاعدة يدوياً.
|
||||||
|
*
|
||||||
|
* الاستخدام:
|
||||||
|
* php scripts/migrate.php --status # ماذا سيُطبَّق وماذا هو مطبَّق
|
||||||
|
* php scripts/migrate.php --dry-run # عرض العبارات دون تنفيذ
|
||||||
|
* php scripts/migrate.php # التطبيق
|
||||||
|
*
|
||||||
|
* كل خطوة idempotent: تُفحص حالة المخطط قبل التنفيذ، فإعادة التشغيل آمنة
|
||||||
|
* ولا تُنتج خطأ "Duplicate column".
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
http_response_code(403);
|
||||||
|
exit("This script runs from the command line only.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../core/bootstrap.php';
|
||||||
|
|
||||||
|
$options = getopt('', ['dry-run', 'status']);
|
||||||
|
$dryRun = isset($options['dry-run']);
|
||||||
|
$status = isset($options['status']);
|
||||||
|
|
||||||
|
$con = Database::get('main');
|
||||||
|
|
||||||
|
/** هل يوجد العمود؟ */
|
||||||
|
function hasColumn(PDO $con, string $table, string $column): bool
|
||||||
|
{
|
||||||
|
$st = $con->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";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user