PDO::ERRMODE_EXCEPTION]); } catch (Throwable $t) { echo "Error connecting to DB: " . $t->getMessage() . PHP_EOL; exit(1); } echo "This script will backup table `conversations` and apply schema changes.\n"; if (!$force) { fwrite(STDOUT, "Proceed? (yes/no): "); $answer = trim(fgets(STDIN)); if (strtolower($answer) !== 'yes') { echo "Aborted by user.\n"; exit(0); } } // 1) Backup: get CREATE TABLE and all rows $timestamp = date('Ymd_His'); $backupDir = __DIR__ . '/../backups'; if (!is_dir($backupDir)) mkdir($backupDir, 0755, true); $backupFile = $backupDir . "/conversations_backup_{$timestamp}.sql"; try { // Show create $row = $pdo->query("SHOW CREATE TABLE `conversations`")->fetch(PDO::FETCH_ASSOC); $createSql = $row['Create Table'] ?? null; if (!$createSql) throw new Exception('Could not obtain CREATE TABLE for conversations'); $fh = fopen($backupFile, 'w'); fwrite($fh, "-- Backup of table `conversations` generated at {$timestamp}\n\n"); fwrite($fh, "DROP TABLE IF EXISTS `conversations`;\n"); fwrite($fh, $createSql . ";\n\n"); // Dump rows as INSERTs in batches $stmt = $pdo->query("SELECT * FROM `conversations`"); $cols = []; $insertCount = 0; $batchSize = 200; $rows = []; while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) { if (empty($cols)) $cols = array_keys($r); $values = array_map(function($v) use ($pdo) { if (is_null($v)) return 'NULL'; // Ensure non-binary scalars are properly quoted as strings return $pdo->quote((string)$v); }, array_values($r)); $rows[] = '(' . implode(',', $values) . ')'; if (count($rows) >= $batchSize) { $line = "INSERT INTO `conversations` (`" . implode('`,`', $cols) . "`) VALUES\n" . implode(",\n", $rows) . ";\n"; fwrite($fh, $line); $insertCount += count($rows); $rows = []; } } if (count($rows)) { $line = "INSERT INTO `conversations` (`" . implode('`,`', $cols) . "`) VALUES\n" . implode(",\n", $rows) . ";\n"; fwrite($fh, $line); $insertCount += count($rows); } fclose($fh); echo "Backup written to: {$backupFile} (rows: {$insertCount})\n"; } catch (Throwable $t) { echo "Backup failed: " . $t->getMessage() . PHP_EOL; exit(1); } // 2) Prepare ALTER statements (only add if missing) $columnsToAdd = [ 'message_id' => "VARCHAR(255) DEFAULT NULL", 'message_type' => "VARCHAR(32) DEFAULT 'text'", 'media_url' => "TEXT DEFAULT NULL", 'local_file' => "VARCHAR(255) DEFAULT NULL", 'local_thumb' => "VARCHAR(255) DEFAULT NULL", 'filename' => "VARCHAR(255) DEFAULT NULL", 'mime_type' => "VARCHAR(100) DEFAULT NULL", 'reply_to_message_id' => "VARCHAR(255) DEFAULT NULL", 'reaction_emoji' => "VARCHAR(64) DEFAULT NULL", 'reaction_to_message_id' => "VARCHAR(255) DEFAULT NULL", 'status' => "VARCHAR(32) DEFAULT 'received'" ]; // Gather existing columns and their types $existingColsStmt = $pdo->query("SHOW COLUMNS FROM `conversations`"); $existingCols = []; $existingTypes = []; // field -> Type foreach ($existingColsStmt->fetchAll(PDO::FETCH_ASSOC) as $c) { $existingCols[] = $c['Field']; $existingTypes[$c['Field']] = strtolower($c['Type']); } $alters = []; foreach ($columnsToAdd as $col => $type) { if (!in_array($col, $existingCols)) { $alters[] = "ADD COLUMN `{$col}` {$type}"; } else { // If the column exists but its type doesn't match the desired one, schedule a MODIFY $desiredType = strtolower(preg_replace('/\s+default\s+.*/i', '', $type)); // e.g. 'varchar(255)' if (!str_starts_with($existingTypes[$col], $desiredType)) { $alters[] = "MODIFY COLUMN `{$col}` {$type}"; echo "Will modify column type for {$col}: found {$existingTypes[$col]} -> desired {$desiredType}\n"; } } } try { if (count($alters)) { $sql = "ALTER TABLE `conversations` " . implode(', ', $alters) . ";"; $pdo->exec($sql); echo "Applied ALTER TABLE to add columns: " . implode(', ', array_keys($columnsToAdd)) . "\n"; } else { echo "No new columns to add.\n"; } } catch (Throwable $t) { echo "ALTER TABLE failed: " . $t->getMessage() . PHP_EOL; echo "You can manually inspect {$backupFile} and run the appropriate ALTERs.\n"; exit(1); } // 3) Add indexes if missing $indexes = [ 'idx_message_id' => ['cols' => ['message_id'], 'unique' => false], 'idx_reply' => ['cols' => ['reply_to_message_id'], 'unique' => false], ]; $existingIndexes = []; $rs = $pdo->query("SHOW INDEX FROM `conversations`"); foreach ($rs->fetchAll(PDO::FETCH_ASSOC) as $row) { $existingIndexes[$row['Key_name']][] = $row['Column_name']; } foreach ($indexes as $name => $info) { if (!isset($existingIndexes[$name])) { $colsSql = implode('`,`', $info['cols']); $sql = "CREATE INDEX `{$name}` ON `conversations` (`{$colsSql}`);"; try { $pdo->exec($sql); echo "Added index {$name} (" . implode(',', $info['cols']) . ")\n"; } catch (Throwable $t) { echo "Failed to add index {$name}: " . $t->getMessage() . PHP_EOL; } } else { echo "Index {$name} already exists.\n"; } } echo "Migration complete. Backup file: {$backupFile}\n"; return 0;