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 للبحث بالبريد'"], // نسخة مشفّرة قابلة للاسترجاع من رقم الهاتف في جداول التحقق. // عمود phone_number صار يحمل مفتاح بحث أحادي الاتجاه (HMAC)، فلا يمكن // استرجاع الرقم منه — وخدمة العملاء تحتاجه لمتابعة من طلب رمزاً ولم // يُكمل تسجيله. ['phone_verification', 'phone_enc', "TEXT NULL DEFAULT NULL COMMENT 'الهاتف مشفّراً للاسترجاع'"], ['phone_verification_passenger', 'phone_enc', "TEXT NULL DEFAULT NULL COMMENT 'الهاتف مشفّراً للاسترجاع'"], ['phone_verification_service', 'phone_enc', "TEXT NULL DEFAULT NULL COMMENT 'الهاتف مشفّراً للاسترجاع'"], ['token_verification', 'phone_enc', "TEXT NULL DEFAULT NULL COMMENT 'الهاتف مشفّراً للاسترجاع'"], ['token_verification_driver', 'phone_enc', "TEXT NULL DEFAULT NULL COMMENT 'الهاتف مشفّراً للاسترجاع'"], // مفتاح ربط جداول التحقق: نفس صيغة otpPhoneKey، حتى يمكن ربط // phone_verification*.phone_number بصاحب الحساب دون مقارنة نصوص مشفّرة. ['driver', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط جداول التحقق'"], ['passengers', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط جداول التحقق'"], // جداول ملاحظات خدمة العملاء تُربط بالسائق/الراكب عبر الهاتف المشفّر، // وهو ربط يفشل لحظة أن يصبح التشفير عشوائياً. ['notesForDriverService', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط بالحساب'"], ['notesForPassengerService', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط بالحساب'"], // بقية الجداول التي يُبحث فيها بحقل مشفّر ['users', 'email_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC لبريد موظف الخدمة'"], ['users', 'phone_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC لهاتف موظف الخدمة'"], ['driver', 'national_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'], ['users', 'idx_users_email_bidx', 'email_bidx'], ['users', 'idx_users_phone_bidx', 'phone_bidx'], ['driver', 'idx_driver_national_bidx', 'national_bidx'], ['driver', 'idx_driver_phone_key', 'phone_key'], ['passengers', 'idx_passengers_phone_key', 'phone_key'], ['notesForDriverService', 'idx_notes_driver_phone_key', 'phone_key'], ['notesForPassengerService', 'idx_notes_passenger_phone_key', 'phone_key'], ]; $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"; }