Files
docker-volume-backup/src/baudolo/restore/db/mariadb.py
Kevin Veen-Birkenbach bb647c66ec feat(restore): refuse a dump the target engine cannot read
A restore with --empty destroys before it replays: the pre-clean drops the schema in one session and the dump goes in the next, with no rollback across the two. A dump the engine cannot parse therefore does not fail harmlessly, it leaves an emptied database behind. The version each side is on decides that up front, so the refusal lands before the first session opens.

Both engines state their origin in the dump's own header and spell it differently. Postgres names the source server; MariaDB opens with mariadb-dump's own version and names the server further down, so matching the first number would read the tool on one engine and the server on the other. A pg_dumpall stream carries no version line of its own at all - the first belongs to the first database's embedded pg_dump output, arbitrarily far down - hence the deep scan.

Restoring forward across a major version stays allowed; that is the upgrade path. Only backward is refused, with --no-version-check as the way out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 05:25:29 +02:00

103 lines
2.8 KiB
Python

from __future__ import annotations
import os
import sys
from ..run import docker_exec, docker_exec_sh
from .version import guard
def _pick_client(container: str) -> str:
"""
Prefer 'mariadb', fallback to 'mysql'.
Some MariaDB images no longer ship a 'mysql' binary, so we must not assume it exists.
"""
script = r"""
set -eu
if command -v mariadb >/dev/null 2>&1; then echo mariadb; exit 0; fi
if command -v mysql >/dev/null 2>&1; then echo mysql; exit 0; fi
exit 42
"""
try:
out = docker_exec_sh(container, script, capture=True).stdout.decode().strip()
if not out:
raise RuntimeError("empty client detection output")
return out
except Exception:
print(
"ERROR: neither 'mariadb' nor 'mysql' found in container.", file=sys.stderr
)
raise
def restore_mariadb_sql(
*,
container: str,
db_name: str,
user: str,
password: str,
sql_path: str,
empty: bool,
check_version: bool = True,
) -> None:
client = _pick_client(container)
if not os.path.isfile(sql_path):
raise FileNotFoundError(sql_path)
if check_version:
guard(
sql_path=sql_path,
engine="mariadb",
container=container,
user=user,
password=password,
client=client,
)
if empty:
# Do not hardcode 'mysql': MariaDB 11 images may not ship that binary.
result = docker_exec(
container,
[
client,
"-u",
user,
f"--password={password}",
"-N",
"-e",
f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{db_name}';",
],
capture=True,
)
tables = result.stdout.decode().split()
if tables:
# SET FOREIGN_KEY_CHECKS is session-scoped, so it must share one
# client session with the DROPs or FK constraints still fire.
drop_sql = (
"SET FOREIGN_KEY_CHECKS=0; "
+ " ".join(
f"DROP TABLE IF EXISTS `{db_name}`.`{tbl}`;" for tbl in tables
)
+ " SET FOREIGN_KEY_CHECKS=1;"
)
docker_exec(
container,
[
client,
"-u",
user,
f"--password={password}",
"-e",
drop_sql,
],
)
with open(sql_path, "rb") as f:
docker_exec(
container, [client, "-u", user, f"--password={password}", db_name], stdin=f
)
print(f"MariaDB/MySQL restore complete for db '{db_name}'.")