Mit folgendem PHP-Code lässt sich eine MySQL-Datenbank von Version 5.x zu 8.0 migrieren, so wie das bei manchen Webhosting-Anbietern wie 1blu aktuell notwendig ist:
- IP-Adresse, Tabellen-Prefix und Datenbank-Zugangsdaten hinterlegen
- PHP-Datei im Root-Verzeichnis des Webspaces als
db_migrate.phphochladen - Preflight Check durchführen
- Bei WordPress-Websites Wartungsmodus über den Button Enable Maintenance aktivieren
- Datenbank kopieren und überprüfen
- Datenbank-Zugangsdaten in der
wp-config.phpauf die neue Datenbank umstellen - Website / WordPress gründlich testen
db_migrate.phpwieder löschen
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('max_execution_time', 120);
if (($_SERVER['REMOTE_ADDR'] ?? '') !== 'XXX.XXX.XXX.XXX') {
http_response_code(403);
exit('Forbidden');
}
const TABLE_PREFIX = 'XXXXX_';
$databases = [
'old' => [
'host' => 'XXXXXXXXXXXXX',
'user' => 'XXXXXXXXXXXXX',
'pass' => 'XXXXXXXXXXXXX',
'name' => 'XXXXXXXXXXXXX',
'charset' => 'utf8mb4',
],
'new' => [
'host' => 'XXXXXXXXXXXXX',
'user' => 'XXXXXXXXXXXXX',
'pass' => 'XXXXXXXXXXXXX',
'name' => 'XXXXXXXXXXXXX',
'charset' => 'utf8mb4',
],
];
function get_old(array $databases): mysqli {
$db = $databases['old'];
$conn = new mysqli($db['host'], $db['user'], $db['pass'], $db['name']);
if ($conn->connect_errno) {
throw new RuntimeException("OLD DB connection failed: " . $conn->connect_error);
}
$conn->set_charset($db['charset']);
$conn->query("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
try { $conn->query("SET SESSION sql_mode = ''"); } catch (Throwable $e) {}
return $conn;
}
function get_new(array $databases): mysqli {
$db = $databases['new'];
$conn = new mysqli($db['host'], $db['user'], $db['pass'], $db['name']);
if ($conn->connect_errno) {
throw new RuntimeException("NEW DB connection failed: " . $conn->connect_error);
}
$conn->set_charset($db['charset']);
$conn->query("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
try { $conn->query("SET SESSION sql_mode = ''"); } catch (Throwable $e) {}
try { $conn->query("SET SESSION innodb_strict_mode = 0"); } catch (Throwable $e) {}
return $conn;
}
function json_out(array $data): never {
header('Content-Type: application/json');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function adapt_ddl_for_mysql8(string $ddl, bool $isView = false): string {
if ($isView) {
$ddl = preg_replace('/\bDEFINER\s*=\s*\S+\s*/i', '', $ddl);
$ddl = preg_replace('/\bSQL\s+SECURITY\s+DEFINER\b/i', 'SQL SECURITY INVOKER', $ddl);
return $ddl;
}
// Upgrade MEDIUMINT/INT UNSIGNED → BIGINT UNSIGNED to prevent overflow
// when migrating across MySQL versions (auto-increment IDs, FKs, composite PKs)
$ddl = preg_replace('/\b(MEDIUMINT|INT)\(\d+\)\s+unsigned/i', 'BIGINT unsigned', $ddl);
$ddl = preg_replace('/\b(MEDIUMINT|INT)\s+unsigned/i', 'BIGINT unsigned', $ddl);
// Strip remaining integer display widths deprecated in MySQL 8.0
$ddl = preg_replace('/\b(TINYINT|SMALLINT|BIGINT)\(\d+\)/i', '$1', $ddl);
return $ddl;
}
// ── API Actions ──────────────────────────────────────────────────
if (isset($_GET['action'])) {
try {
switch ($_GET['action']) {
case 'maintenance': {
$enable = ($_GET['enable'] ?? '1') === '1';
$file = __DIR__ . '/.maintenance';
if ($enable) {
file_put_contents($file, "<?php \$upgrading = time(); ?>\n");
json_out(['ok' => true, 'enabled' => true]);
} else {
if (file_exists($file)) @unlink($file);
json_out(['ok' => true, 'enabled' => false]);
}
}
case 'preflight': {
$checks = [];
$allOk = true;
try {
$old = get_old($databases);
$cs = $old->character_set_name();
$checks[] = ['name' => 'OLD connection (utf8mb4)', 'ok' => $cs === 'utf8mb4', 'detail' => "charset: $cs"];
$r = $old->query("SELECT @@max_allowed_packet");
$pkt = ($r && $rw = $r->fetch_row()) ? (int)$rw[0] : 0;
$checks[] = ['name' => 'OLD max_allowed_packet', 'ok' => $pkt >= 1048576,
'detail' => number_format($pkt / 1048576, 1) . ' MB'];
$r = $old->query("SELECT @@sql_mode");
$sm = ($r && $rw = $r->fetch_row()) ? $rw[0] : '';
$checks[] = ['name' => 'OLD sql_mode', 'ok' => true, 'detail' => $sm ?: '(empty)'];
$r = $old->query("SHOW TABLES LIKE '" . $old->real_escape_string(TABLE_PREFIX) . "%'");
$tc = $r ? $r->num_rows : 0;
$checks[] = ['name' => 'OLD tables (' . TABLE_PREFIX . '*)', 'ok' => $tc > 0, 'detail' => "$tc tables"];
$r = $old->query("SELECT CHARACTER_SET_NAME, COUNT(*) as cnt
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = '" . $old->real_escape_string($databases['old']['name']) . "'
AND TABLE_NAME LIKE '" . $old->real_escape_string(TABLE_PREFIX) . "%'
AND CHARACTER_SET_NAME IS NOT NULL
GROUP BY CHARACTER_SET_NAME ORDER BY cnt DESC");
$csList = [];
if ($r) while ($rw = $r->fetch_assoc()) $csList[] = $rw['CHARACTER_SET_NAME'] . ': ' . $rw['cnt'];
$checks[] = ['name' => 'OLD column charsets', 'ok' => true, 'detail' => implode(', ', $csList) ?: 'none'];
$old->close();
} catch (Throwable $e) {
$checks[] = ['name' => 'OLD connection', 'ok' => false, 'detail' => $e->getMessage()];
$allOk = false;
}
try {
$new = get_new($databases);
$cs = $new->character_set_name();
$checks[] = ['name' => 'NEW connection (utf8mb4)', 'ok' => $cs === 'utf8mb4', 'detail' => "charset: $cs"];
$r = $new->query("SELECT @@max_allowed_packet");
$pkt = ($r && $rw = $r->fetch_row()) ? (int)$rw[0] : 0;
$checks[] = ['name' => 'NEW max_allowed_packet', 'ok' => $pkt >= 1048576,
'detail' => number_format($pkt / 1048576, 1) . ' MB'];
$r = $new->query("SELECT @@sql_mode");
$sm = ($r && $rw = $r->fetch_row()) ? $rw[0] : '';
$checks[] = ['name' => 'NEW sql_mode', 'ok' => true, 'detail' => $sm ?: '(empty)'];
$new->close();
} catch (Throwable $e) {
$checks[] = ['name' => 'NEW connection', 'ok' => false, 'detail' => $e->getMessage()];
$allOk = false;
}
foreach ($checks as $c) { if (!$c['ok']) $allOk = false; }
json_out(['ok' => true, 'all_ok' => $allOk, 'checks' => $checks]);
}
case 'list_tables': {
$old = get_old($databases);
$res = $old->query("SHOW FULL TABLES LIKE '" . $old->real_escape_string(TABLE_PREFIX) . "%'");
$baseTables = [];
$views = [];
while ($row = $res->fetch_row()) {
$isView = (strtoupper($row[1]) === 'VIEW');
$cnt = 0;
if (!$isView) {
$cntRes = $old->query("SELECT COUNT(*) FROM `{$row[0]}`");
$cnt = (int)($cntRes ? $cntRes->fetch_row()[0] : 0);
}
$entry = [
'name' => $row[0],
'rows' => $cnt,
'is_view' => $isView,
];
if ($isView) {
$views[] = $entry;
} else {
$baseTables[] = $entry;
}
}
$old->close();
json_out(['ok' => true, 'tables' => array_merge($baseTables, $views)]);
}
case 'create_table': {
$table = $_GET['table'] ?? '';
if (!$table || strpos($table, TABLE_PREFIX) !== 0) {
json_out(['ok' => false, 'error' => 'Invalid table name']);
}
$old = get_old($databases);
$new = get_new($databases);
$new->query("SET foreign_key_checks = 0");
$res = $old->query("SHOW CREATE TABLE `" . $old->real_escape_string($table) . "`");
if (!$res) {
json_out(['ok' => false, 'error' => 'Cannot read structure: ' . $old->error]);
}
$row = $res->fetch_assoc();
$isView = isset($row['Create View']);
$ddl = $isView ? $row['Create View'] : $row['Create Table'];
$ddl = adapt_ddl_for_mysql8($ddl, $isView);
if ($isView) {
$new->query("DROP VIEW IF EXISTS `" . $new->real_escape_string($table) . "`");
} else {
$new->query("DROP TABLE IF EXISTS `" . $new->real_escape_string($table) . "`");
}
if (!$new->query($ddl)) {
json_out(['ok' => false, 'error' => 'CREATE failed: ' . $new->error, 'is_view' => $isView]);
}
$old->close();
$new->close();
json_out(['ok' => true, 'table' => $table, 'is_view' => $isView]);
}
case 'copy_batch': {
$table = $_GET['table'] ?? '';
$offset = (int)($_GET['offset'] ?? 0);
$batch = (int)($_GET['batch'] ?? 500);
if (!$table || strpos($table, TABLE_PREFIX) !== 0) {
json_out(['ok' => false, 'error' => 'Invalid table name']);
}
$old = get_old($databases);
$new = get_new($databases);
$new->query("SET foreign_key_checks = 0");
$new->query("SET unique_checks = 0");
$pkRes = $old->query("SHOW KEYS FROM `" . $old->real_escape_string($table) . "` WHERE Key_name = 'PRIMARY'");
$pkCols = [];
if ($pkRes) {
while ($pkRow = $pkRes->fetch_assoc()) {
$pkCols[(int)$pkRow['Seq_in_index']] = '`' . $pkRow['Column_name'] . '`';
}
ksort($pkCols);
}
$orderBy = !empty($pkCols) ? ' ORDER BY ' . implode(',', $pkCols) : '';
$res = $old->query("SELECT * FROM `" . $old->real_escape_string($table) . "`$orderBy LIMIT $offset, $batch");
if (!$res) {
json_out(['ok' => false, 'error' => 'SELECT failed: ' . $old->error]);
}
$numFields = $res->field_count;
$fields = $res->fetch_fields();
$numericTypes = [
MYSQLI_TYPE_TINY, MYSQLI_TYPE_SHORT, MYSQLI_TYPE_LONG,
MYSQLI_TYPE_LONGLONG, MYSQLI_TYPE_INT24,
MYSQLI_TYPE_FLOAT, MYSQLI_TYPE_DOUBLE, MYSQLI_TYPE_DECIMAL,
MYSQLI_TYPE_NEWDECIMAL,
];
$binaryIdx = [];
foreach ($fields as $i => $f) {
if ($f->charsetnr === 63 && !in_array($f->type, $numericTypes)) {
$binaryIdx[$i] = true;
}
}
$copied = 0;
$valueSets = [];
$new->begin_transaction();
while ($row = $res->fetch_row()) {
$vals = [];
for ($i = 0; $i < $numFields; $i++) {
if ($row[$i] === null) {
$vals[] = 'NULL';
} elseif (isset($binaryIdx[$i])) {
$vals[] = "X'" . bin2hex($row[$i]) . "'";
} else {
$vals[] = "'" . $new->real_escape_string($row[$i]) . "'";
}
}
$valueSets[] = '(' . implode(',', $vals) . ')';
$copied++;
if (count($valueSets) >= 50) {
$sql = "INSERT INTO `" . $new->real_escape_string($table) . "` VALUES " . implode(',', $valueSets);
if (!$new->query($sql)) {
$new->rollback();
json_out([
'ok' => false,
'error' => 'INSERT failed at offset ' . ($offset + $copied - count($valueSets)) . ': ' . $new->error,
]);
}
$valueSets = [];
}
}
if (count($valueSets) > 0) {
$sql = "INSERT INTO `" . $new->real_escape_string($table) . "` VALUES " . implode(',', $valueSets);
if (!$new->query($sql)) {
$new->rollback();
json_out([
'ok' => false,
'error' => 'INSERT failed at offset ' . ($offset + $copied - count($valueSets)) . ': ' . $new->error,
]);
}
}
$new->commit();
$old->close();
$new->close();
json_out([
'ok' => true,
'table' => $table,
'offset' => $offset,
'copied' => $copied,
'done' => $copied < $batch,
]);
}
case 'verify': {
$old = get_old($databases);
$new = get_new($databases);
$res = $old->query("SHOW TABLES LIKE '" . $old->real_escape_string(TABLE_PREFIX) . "%'");
$results = [];
$allGood = true;
while ($row = $res->fetch_row()) {
$table = $row[0];
$oldCount = $old->query("SELECT COUNT(*) FROM `$table`")->fetch_row()[0];
$chk = $new->query("SHOW TABLES LIKE '" . $new->real_escape_string($table) . "'");
if ($chk->num_rows === 0) {
$results[] = [
'table' => $table,
'old_rows' => (int)$oldCount,
'new_rows' => null,
'match' => false,
'error' => 'Table missing in NEW DB',
];
$allGood = false;
continue;
}
$newCount = $new->query("SELECT COUNT(*) FROM `$table`")->fetch_row()[0];
$match = ((int)$oldCount === (int)$newCount);
if (!$match) $allGood = false;
$entry = [
'table' => $table,
'old_rows' => (int)$oldCount,
'new_rows' => (int)$newCount,
'match' => $match,
];
if ($match && (int)$oldCount > 0) {
$pkRes = $old->query("SHOW KEYS FROM `$table` WHERE Key_name = 'PRIMARY'");
$pkCol = null;
if ($pkRes && ($pkRow = $pkRes->fetch_assoc())) {
$pkCol = $pkRow['Column_name'];
}
if ($pkCol) {
$esc = $old->real_escape_string($pkCol);
$oldInfo = $old->query("SELECT MIN(`$esc`) as mn, MAX(`$esc`) as mx FROM `$table`");
$newInfo = $new->query("SELECT MIN(`$esc`) as mn, MAX(`$esc`) as mx FROM `$table`");
if ($oldInfo && $newInfo) {
$ov = $oldInfo->fetch_assoc();
$nv = $newInfo->fetch_assoc();
$rangeMatch = ($ov['mn'] === $nv['mn'] && $ov['mx'] === $nv['mx']);
$entry['pk_range_match'] = $rangeMatch;
if (!$rangeMatch) {
$allGood = false;
$entry['old_pk_range'] = $ov['mn'] . '..' . $ov['mx'];
$entry['new_pk_range'] = $nv['mn'] . '..' . $nv['mx'];
}
}
}
}
$results[] = $entry;
}
$old->close();
$new->close();
json_out(['ok' => true, 'all_good' => $allGood, 'tables' => $results]);
}
case 'verify_serialized': {
$target = $_GET['db'] ?? 'new';
$conn = ($target === 'old') ? get_old($databases) : get_new($databases);
$checkTargets = [
TABLE_PREFIX . 'options' => ['option_value', "option_value LIKE 'a:%' OR option_value LIKE 'O:%'", 'option_name'],
TABLE_PREFIX . 'postmeta' => ['meta_value', "meta_value LIKE 'a:%' OR meta_value LIKE 'O:%'", 'meta_key'],
TABLE_PREFIX . 'usermeta' => ['meta_value', "meta_value LIKE 'a:%' OR meta_value LIKE 'O:%'", 'meta_key'],
];
$totalChecked = 0;
$totalFailed = 0;
$failedEntries = [];
foreach ($checkTargets as $tbl => [$col, $where, $nameCol]) {
$chk = $conn->query("SHOW TABLES LIKE '" . $conn->real_escape_string($tbl) . "'");
if (!$chk || $chk->num_rows === 0) continue;
$r = $conn->query("SELECT `$nameCol`, `$col` FROM `$tbl`
WHERE $where ORDER BY LENGTH(`$col`) DESC LIMIT 200");
if (!$r) continue;
while ($rw = $r->fetch_row()) {
$totalChecked++;
if (@unserialize($rw[1]) === false && $rw[1] !== 'b:0;') {
$totalFailed++;
if (count($failedEntries) < 20) {
$failedEntries[] = [
'table' => $tbl,
'key' => $rw[0],
'size' => strlen($rw[1]),
'preview' => mb_substr($rw[1], 0, 120),
];
}
}
}
}
$conn->close();
json_out([
'ok' => true,
'db' => $target,
'checked' => $totalChecked,
'failed' => $totalFailed,
'entries' => $failedEntries,
]);
}
default:
json_out(['ok' => false, 'error' => 'Unknown action']);
}
} catch (Throwable $e) {
json_out(['ok' => false, 'error' => $e->getMessage()]);
}
}
$maintenanceActive = file_exists(__DIR__ . '/.maintenance');
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>DB Migration — OLD → NEW</title>
<style>
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 2em; color: #222; background: #f8f8f8;
}
h1 { border-bottom: 2px solid #333; padding-bottom: .3em; }
.controls { margin-bottom: 1.2em; display: flex; align-items: center; gap: .7em; flex-wrap: wrap; }
button {
padding: .5em 1.2em; font-size: .95em; border-radius: 6px; border: none;
color: #fff; cursor: pointer;
}
button:disabled { background: #999 !important; cursor: not-allowed; }
#preflightBtn { background: #2c3e50; }
#startBtn { background: #1a5276; }
#resyncBtn { background: #d35400; }
#verifyBtn { background: #27ae60; }
#verifySerBtn { background: #16a085; }
#maintBtn { background: <?= $maintenanceActive ? '#c0392b' : '#8e44ad' ?>; }
label { font-size: .95em; }
input[type=number] { width: 80px; padding: .3em; border-radius: 4px; border: 1px solid #aaa; }
.progress-bar {
width: 100%; background: #ddd; border-radius: 4px; height: 26px;
margin-bottom: 1em; overflow: hidden;
}
.progress-fill {
height: 100%; background: #27ae60; transition: width 0.3s;
text-align: center; color: #fff; font-size: .85em; line-height: 26px;
min-width: 40px;
}
#log {
background: #fff; border: 1px solid #ddd; border-radius: 6px;
padding: 1em; max-height: 55vh; overflow-y: auto; font-size: .88em;
font-family: "SF Mono", Menlo, Consolas, monospace; white-space: pre-wrap;
}
.ok { color: #27ae60; }
.err { color: #c0392b; font-weight: bold; }
.inf { color: #2c3e50; }
.dim { color: #888; }
.verify-table {
border-collapse: collapse; width: 100%; margin-top: .5em; font-size: .88em;
}
.verify-table th, .verify-table td {
border: 1px solid #ddd; padding: 5px 10px; text-align: left;
}
.verify-table th { background: #eee; }
.row-ok { background: #eafaf1; }
.row-bad { background: #fdedec; }
.warn { color: #888; font-size: .85em; margin-top: 2em; }
</style>
</head>
<body>
<h1>Database Migration: OLD → NEW</h1>
<p class="inf">Only tables with prefix <code>u3zo_</code> will be copied.
Connection charset: <code>utf8mb4</code>. The OLD database is read-only (SELECT only).</p>
<div class="controls">
<button id="preflightBtn" onclick="runPreflight()">Preflight Check</button>
<button id="startBtn" onclick="startMigration()">Start Full Copy</button>
<button id="resyncBtn" onclick="resyncMismatches()">Re-sync Mismatches</button>
<button id="verifyBtn" onclick="runVerify()">Verify Copy</button>
<button id="verifySerBtn" onclick="runVerifySerialized()">Verify Serialized</button>
<button id="maintBtn"
data-enabled="<?= $maintenanceActive ? '1' : '0' ?>"
onclick="toggleMaintenance()">
<?= $maintenanceActive ? 'Disable Maintenance' : 'Enable Maintenance' ?>
</button>
<label>Batch: <input type="number" id="batchSize" value="500" min="50" max="5000"></label>
</div>
<div class="progress-bar"><div class="progress-fill" id="progress" style="width:0%">0 %</div></div>
<div id="log"></div>
<p class="warn">Delete this file immediately after migration. It contains database credentials.</p>
<script>
const logEl = document.getElementById('log');
const progress = document.getElementById('progress');
const actionBtns = document.querySelectorAll('button[id]');
function setAllButtons(disabled) {
actionBtns.forEach(b => { if (b.id !== 'maintBtn') b.disabled = disabled; });
}
function log(msg, cls = 'inf') {
logEl.innerHTML += '<span class="' + cls + '">' + msg + '</span>\n';
logEl.scrollTop = logEl.scrollHeight;
}
function pct(p, etaSec) {
const v = Math.round(p);
progress.style.width = Math.max(v, 2) + '%';
let text = v + ' %';
if (etaSec !== undefined && etaSec > 0 && isFinite(etaSec)) {
const m = Math.floor(etaSec / 60);
const s = Math.ceil(etaSec % 60);
text += m > 0 ? (' \u2014 ~' + m + 'm ' + s + 's left') : (' \u2014 ~' + s + 's left');
}
progress.textContent = text;
}
async function api(params) {
const qs = new URLSearchParams(params).toString();
const r = await fetch('db_migrate.php?' + qs);
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
}
// ── Preflight ────────────────────────────────────────────────
async function runPreflight() {
setAllButtons(true);
logEl.innerHTML = '';
pct(0);
log('Running preflight checks...\n');
let res;
try { res = await api({ action: 'preflight' }); }
catch (e) { log('NETWORK ERROR: ' + e.message, 'err'); setAllButtons(false); return; }
if (!res.ok) { log('ERROR: ' + res.error, 'err'); setAllButtons(false); return; }
for (const c of res.checks) {
const icon = c.ok ? '\u2713' : '\u2717';
log(' ' + icon + ' ' + c.name + ': ' + c.detail, c.ok ? 'ok' : 'err');
}
log('');
if (res.all_ok) {
log('\u2713 ALL PREFLIGHT CHECKS PASSED \u2014 ready to migrate', 'ok');
pct(100);
} else {
log('\u2717 SOME CHECKS FAILED \u2014 review above before migrating', 'err');
}
setAllButtons(false);
}
// ── Copy helpers ─────────────────────────────────────────────
async function copyTable(name, rows, idx, total, batch, ps) {
log('[' + idx + '/' + total + '] ' + name + ' (' + rows.toLocaleString() + ' rows)');
const createRes = await api({ action: 'create_table', table: name });
if (!createRes.ok) {
log(' \u2717 CREATE failed: ' + createRes.error, 'err');
return { ok: false, isView: createRes.is_view || false };
}
if (createRes.is_view) {
log(' \u2713 View created', 'ok');
return { ok: true, isView: true };
}
log(' \u2713 Structure created', 'ok');
if (rows === 0) { log(' \u2013 Empty table, skipped data copy', 'dim'); return { ok: true }; }
let offset = 0, done = false;
while (!done) {
let res;
try { res = await api({ action: 'copy_batch', table: name, offset: offset, batch: batch }); }
catch (e) { log(' \u2717 Network error at offset ' + offset + ': ' + e.message, 'err'); return { ok: false }; }
if (!res.ok) { log(' \u2717 Error at offset ' + offset + ': ' + res.error, 'err'); return { ok: false }; }
offset += res.copied;
ps.migrated += res.copied;
done = res.done;
if (ps.totalRows > 0) {
const elapsed = (performance.now() - ps.startTime) / 1000;
const rate = ps.migrated / elapsed;
const remain = (ps.totalRows - ps.migrated) / rate;
pct((ps.migrated / ps.totalRows) * 100, remain);
}
if (!done && offset % (batch * 4) === 0) {
log(' ... ' + offset.toLocaleString() + ' / ' + rows.toLocaleString(), 'dim');
}
}
log(' \u2713 ' + offset.toLocaleString() + ' rows copied', 'ok');
return { ok: true };
}
// ── Full Copy ────────────────────────────────────────────────
async function startMigration() {
const maintBtn = document.getElementById('maintBtn');
if (maintBtn.dataset.enabled !== '1') {
if (!confirm('Maintenance mode is NOT enabled.\nThe live site may modify data during copy, causing inconsistencies.\n\nContinue anyway?')) {
return;
}
}
setAllButtons(true);
logEl.innerHTML = '';
pct(0);
const batch = parseInt(document.getElementById('batchSize').value) || 500;
log('Fetching table list (prefix: ' + 'u3zo_' + ')...');
let listRes;
try { listRes = await api({ action: 'list_tables' }); }
catch (e) { log('NETWORK ERROR: ' + e.message, 'err'); setAllButtons(false); return; }
if (!listRes.ok) { log('ERROR: ' + listRes.error, 'err'); setAllButtons(false); return; }
const tables = listRes.tables;
const ps = {
migrated: 0,
totalRows: tables.reduce((s, t) => s + t.rows, 0),
startTime: performance.now(),
};
log('Found ' + tables.length + ' tables, ' + ps.totalRows.toLocaleString() + ' total rows.\n', 'ok');
const failedViews = [];
for (let i = 0; i < tables.length; i++) {
const t = tables[i];
const result = await copyTable(t.name, t.rows, i + 1, tables.length, batch, ps);
if (!result.ok && result.isView) failedViews.push(t);
}
if (failedViews.length > 0) {
log('\n--- Retrying ' + failedViews.length + ' failed VIEW(s) ---\n', 'inf');
for (const v of failedViews) {
log('Retrying VIEW: ' + v.name);
const r = await api({ action: 'create_table', table: v.name });
if (r.ok) { log(' \u2713 View created', 'ok'); }
else { log(' \u2717 Still failing: ' + r.error, 'err'); }
}
}
const elapsed = ((performance.now() - ps.startTime) / 1000).toFixed(1);
log('\n========================================', 'ok');
log('Copy complete! ' + ps.migrated.toLocaleString() + ' rows in ' + elapsed + 's', 'ok');
log('Click "Verify Copy" to validate, or "Re-sync Mismatches" for targeted fixes.\n', 'inf');
pct(100);
setAllButtons(false);
}
// ── Re-sync ──────────────────────────────────────────────────
async function resyncMismatches() {
setAllButtons(true);
logEl.innerHTML = '';
pct(0);
const batch = parseInt(document.getElementById('batchSize').value) || 500;
log('Running verification to find mismatches...\n');
let vRes;
try { vRes = await api({ action: 'verify' }); }
catch (e) { log('NETWORK ERROR: ' + e.message, 'err'); setAllButtons(false); return; }
if (!vRes.ok) { log('ERROR: ' + vRes.error, 'err'); setAllButtons(false); return; }
const mismatched = vRes.tables.filter(t =>
!t.match || t.pk_range_match === false || t.error
);
if (mismatched.length === 0) {
log('\u2713 No mismatches found \u2014 everything is in sync!', 'ok');
pct(100);
setAllButtons(false);
return;
}
log('Found ' + mismatched.length + ' table(s) needing re-sync:\n', 'inf');
for (const t of mismatched) {
const reason = t.error ? t.error
: !t.match ? 'row count: ' + (t.old_rows||0) + ' vs ' + (t.new_rows||0)
: 'PK range: ' + (t.old_pk_range||'?') + ' vs ' + (t.new_pk_range||'?');
log(' \u2022 ' + t.table + ' (' + reason + ')', 'dim');
}
log('');
const ps = {
migrated: 0,
totalRows: mismatched.reduce((s, t) => s + (t.old_rows || 0), 0),
startTime: performance.now(),
};
const failedViews = [];
for (let i = 0; i < mismatched.length; i++) {
const t = mismatched[i];
const result = await copyTable(t.table, t.old_rows || 0, i + 1, mismatched.length, batch, ps);
if (!result.ok && result.isView) failedViews.push(t);
}
if (failedViews.length > 0) {
log('\n--- Retrying ' + failedViews.length + ' failed VIEW(s) ---\n', 'inf');
for (const v of failedViews) {
log('Retrying VIEW: ' + v.table);
const r = await api({ action: 'create_table', table: v.table });
if (r.ok) { log(' \u2713 View created', 'ok'); }
else { log(' \u2717 Still failing: ' + r.error, 'err'); }
}
}
const elapsed = ((performance.now() - ps.startTime) / 1000).toFixed(1);
log('\n========================================', 'ok');
log('Re-sync complete! ' + ps.migrated.toLocaleString() + ' rows across '
+ mismatched.length + ' table(s) in ' + elapsed + 's', 'ok');
log('\nRunning final verification...\n', 'inf');
await doVerify();
setAllButtons(false);
}
// ── Verify ───────────────────────────────────────────────────
async function doVerify() {
let vRes;
try { vRes = await api({ action: 'verify' }); }
catch (e) { log('NETWORK ERROR: ' + e.message, 'err'); return; }
if (!vRes.ok) { log('ERROR: ' + vRes.error, 'err'); return; }
let html = '<table class="verify-table">';
html += '<tr><th>Table</th><th>OLD rows</th><th>NEW rows</th><th>Row match</th><th>PK Range</th></tr>';
for (const t of vRes.tables) {
const cls = t.match && t.pk_range_match !== false ? 'row-ok' : 'row-bad';
const rowIcon = t.match ? '\u2713' : '\u2717';
let csText = '\u2013';
if (t.error) {
csText = '<span class="err">' + t.error + '</span>';
} else if (t.pk_range_match === true) {
csText = '<span class="ok">\u2713 match</span>';
} else if (t.pk_range_match === false) {
csText = '<span class="err">\u2717 mismatch (' + t.old_pk_range + ' vs ' + t.new_pk_range + ')</span>';
}
const newRows = t.new_rows !== null ? t.new_rows.toLocaleString() : '\u2013';
html += '<tr class="' + cls + '"><td>' + t.table + '</td><td>' + t.old_rows.toLocaleString() + '</td>'
+ '<td>' + newRows + '</td><td>' + rowIcon + '</td><td>' + csText + '</td></tr>';
}
html += '</table>';
logEl.innerHTML += html;
logEl.scrollTop = logEl.scrollHeight;
if (vRes.all_good) {
log('\n\u2713 ALL TABLES VERIFIED SUCCESSFULLY', 'ok');
} else {
log('\n\u2717 SOME TABLES STILL HAVE MISMATCHES \u2014 check table above', 'err');
log('Enable maintenance mode, then re-sync again.', 'inf');
}
}
async function runVerify() {
setAllButtons(true);
log('\n--- Verification ---\n');
log('Comparing row counts and checksums between OLD and NEW...\n');
await doVerify();
setAllButtons(false);
}
// ── Verify Serialized ────────────────────────────────────────
async function runVerifySerialized() {
setAllButtons(true);
log('\n--- Serialized Data Verification (NEW DB) ---\n');
let res;
try { res = await api({ action: 'verify_serialized', db: 'new' }); }
catch (e) { log('NETWORK ERROR: ' + e.message, 'err'); setAllButtons(false); return; }
if (!res.ok) { log('ERROR: ' + res.error, 'err'); setAllButtons(false); return; }
log('Checked: ' + res.checked + ' serialized values in ' + res.db.toUpperCase() + ' DB');
if (res.failed === 0) {
log('\u2713 All values unserialize() successfully', 'ok');
} else {
log('\u2717 ' + res.failed + ' / ' + res.checked + ' values FAILED unserialize()', 'err');
for (const e of res.entries) {
log(' \u2022 ' + e.table + ' / ' + e.key + ' (' + e.size + ' bytes)', 'dim');
log(' ' + e.preview.substring(0, 100), 'dim');
}
}
setAllButtons(false);
}
// ── Maintenance Mode ─────────────────────────────────────────
async function toggleMaintenance() {
const btn = document.getElementById('maintBtn');
const enabling = btn.dataset.enabled !== '1';
let res;
try { res = await api({ action: 'maintenance', enable: enabling ? '1' : '0' }); }
catch (e) { log('NETWORK ERROR: ' + e.message, 'err'); return; }
if (!res.ok) { log('ERROR: ' + (res.error || 'unknown'), 'err'); return; }
btn.dataset.enabled = res.enabled ? '1' : '0';
btn.textContent = res.enabled ? 'Disable Maintenance' : 'Enable Maintenance';
btn.style.background = res.enabled ? '#c0392b' : '#8e44ad';
log(res.enabled
? '\u2717 Maintenance mode ENABLED \u2014 site shows "briefly unavailable"'
: '\u2713 Maintenance mode DISABLED \u2014 site is live',
res.enabled ? 'err' : 'ok');
}
</script>
</body>
</html>