Schema-Versionierung ohne Framework-Abhängigkeit
Ein Migrations Runner sorgt dafür, dass Datenbankschemata über mehrere Umgebungen hinweg nachvollziehbar und in der richtigen Reihenfolge verändert werden. Dieser Artikel zeigt, wie ein eigener Migrations Runner in Bash entsteht, mit Tracking-Tabelle, Checksum-Prüfung, transaktionaler Ausführung und einer schlanken Kommandozeilen-Schnittstelle für up, down und status.
Inhaltsverzeichnis
- 1. Warum ein eigener Migrations Runner
- 2. Kernkonzept: Migrationsdatei, Version und Tracking
- 3. Die Tracking-Tabelle in der Datenbank anlegen
- 4. Migrationsdateien auffinden und sortieren
- 5. Checksums zur Integritätsprüfung
- 6. Migrationen transaktional ausführen
- 7. Rollback mit Down-Migrationen
- 8. Kommandozeilen-Interface: up, down, status
- 9. Migrations Runner im Vergleich zu Alternativen
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein eigener Migrations Runner
Sobald ein Projekt mehrere Umgebungen wie Entwicklung, Staging und Produktion betreibt, wird die manuelle Ausführung von SQL-Skripten schnell zum Risiko. Ohne festen Prozess weiß niemand mehr zuverlässig, welche Schema-Änderung auf welcher Umgebung bereits angewendet wurde. Ein Migrations Runner löst dieses Problem, indem er jede Änderung als eigene, versionierte Datei behandelt und den Anwendungsstatus in der Datenbank selbst protokolliert.
Viele Frameworks bringen bereits einen eigenen Migrations Runner mit, etwa Laravel oder Symfony mit Doctrine. Für Projekte ohne ein solches Framework, für reine PHP-Legacy-Anwendungen oder für plattformübergreifende Infrastruktur-Skripte lohnt sich jedoch ein eigener, schlanker Migrations Runner in Bash, der ohne zusätzliche Laufzeitumgebung auskommt und direkt mit dem mysql Kommandozeilenclient arbeitet.
Der Kernvorteil eines selbst geschriebenen Migrations Runners liegt in der vollständigen Kontrolle über das Verhalten: Wie werden Fehler behandelt, wie wird ein Rollback ausgelöst, wie werden Checksums geprüft. Die folgenden Abschnitte bauen einen produktionstauglichen Migrations Runner Schritt für Schritt auf.
2. Kernkonzept: Migrationsdatei, Version und Tracking
Jede Migration besteht aus zwei Teilen: einer Nummer, die die Reihenfolge festlegt, und einem beschreibenden Namen. Ein bewährtes Namensschema für einen Migrations Runner ist 0001_create_users_table.sql, gefolgt optional von einer separaten 0001_create_users_table.down.sql Datei für den Rollback. Diese Konvention macht die Reihenfolge allein durch Sortierung der Dateinamen eindeutig, ganz ohne zusätzliche Metadaten-Datei.
Damit der Migrations Runner weiß, welche Migrationen bereits angewendet wurden, braucht er eine eigene Tabelle in der Zieldatenbank, die als Protokoll dient. Diese Tabelle speichert Version, Ausführungszeitpunkt und eine Prüfsumme der Migrationsdatei. Damit lässt sich zu jedem Zeitpunkt exakt feststellen, welchen Schema-Stand eine Datenbank hat, unabhängig davon, wer die Migration ausgeführt hat.
3. Die Tracking-Tabelle in der Datenbank anlegen
Die Tracking-Tabelle selbst wird beim ersten Start des Migrations Runners automatisch angelegt, falls sie noch nicht existiert. Das macht den Runner von Anfang an idempotent: Ein erneuter Aufruf auf einer frischen Datenbank funktioniert genauso wie auf einer bereits migrierten. Die Spalte checksum ist dabei entscheidend, denn sie erlaubt später die Erkennung nachträglich veränderter Migrationsdateien.
Ein Migrations Runner sollte die Tracking-Tabelle mit einem eindeutigen Index auf die Versionsnummer versehen. Damit verhindert die Datenbank selbst, dass dieselbe Migration versehentlich zweimal eingetragen wird, selbst wenn zwei Instanzen des Skripts parallel gestartet werden sollten.
-- schema_migrations.sql — tracking table for the migration runner
CREATE TABLE IF NOT EXISTS schema_migrations (
version VARCHAR(20) NOT NULL,
name VARCHAR(255) NOT NULL,
checksum CHAR(64) NOT NULL,
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (version)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4. Migrationsdateien auffinden und sortieren
Der Migrations Runner muss alle Migrationsdateien in einem festen Verzeichnis finden und in aufsteigender Reihenfolge verarbeiten. Ein häufiger Fehler ist, die alphabetische Standardsortierung des Dateisystems zu vertrauen, ohne sie explizit mit sort zu erzwingen. Auf manchen Dateisystemen ist die Reihenfolge von find ohne -print0 | sort -z nicht garantiert.
Ausgeschlossen werden müssen dabei konsequent alle .down.sql Dateien, denn diese gehören zum Rollback-Pfad und dürfen niemals bei der normalen Vorwärtsmigration mitverarbeitet werden. Ein sauberer Migrations Runner filtert diese über ein Namensmuster heraus, statt sich auf ein separates Verzeichnis zu verlassen.
#!/usr/bin/env bash
# discover-migrations.sh — find and sort pending migration files
set -euo pipefail
readonly MIGRATIONS_DIR="./migrations"
discover_pending() {
local db_name="$1"
local -a applied=()
local -a pending=()
# Load already applied versions into an array
while IFS= read -r version; do
applied+=("$version")
done < <(mysql -N -B -e "SELECT version FROM schema_migrations ORDER BY version;" "$db_name")
# Find all forward migration files, sorted, excluding rollback files
while IFS= read -r -d '' file; do
local base version
base="$(basename "$file")"
[[ "$base" == *.down.sql ]] && continue
version="${base%%_*}"
if [[ ! " ${applied[*]:-} " == *" $version "* ]]; then
pending+=("$file")
fi
done < <(find "$MIGRATIONS_DIR" -maxdepth 1 -name "*.sql" -print0 | sort -z)
printf '%s\n' "${pending[@]:-}"
}
5. Checksums zur Integritätsprüfung
Ein subtiles, aber gefährliches Problem: Eine bereits angewendete Migrationsdatei wird nachträglich verändert, etwa weil jemand einen Tippfehler korrigieren wollte. Ohne Prüfung würde der Migrations Runner diese Änderung niemals bemerken, weil die Datei laut Tracking-Tabelle als bereits ausgeführt gilt. Mit sha256sum lässt sich dieses Risiko eliminieren, indem bei jedem Lauf die aktuelle Prüfsumme mit der gespeicherten verglichen wird.
Weicht die Prüfsumme ab, sollte der Migrations Runner den Vorgang sofort abbrechen und eine klare Fehlermeldung ausgeben, statt die Abweichung stillschweigend zu ignorieren. Nur so bleibt garantiert, dass alle Umgebungen tatsächlich denselben Migrationsverlauf durchlaufen haben, und nicht nur denselben Dateinamen.
#!/usr/bin/env bash
# verify-checksum.sh — detect tampering with already applied migrations
set -euo pipefail
verify_applied_checksums() {
local db_name="$1"
while IFS=$'\t' read -r version stored_checksum; do
local file
file="$(find ./migrations -maxdepth 1 -name "${version}_*.sql" ! -name "*.down.sql" | head -n 1)"
if [[ -z "$file" ]]; then
echo "[ERROR] Migration file for version ${version} is missing on disk" >&2
exit 1
fi
local current_checksum
current_checksum="$(sha256sum "$file" | cut -d' ' -f1)"
if [[ "$current_checksum" != "$stored_checksum" ]]; then
echo "[ERROR] Checksum mismatch for ${version}: file was modified after being applied" >&2
exit 1
fi
done < <(mysql -N -B -e "SELECT version, checksum FROM schema_migrations;" "$db_name")
echo "[OK] All applied migrations match their stored checksum"
}
6. Migrationen transaktional ausführen
Jede Migration sollte als eine einzige atomare Transaktion laufen: entweder wird sowohl die Schema-Änderung als auch der Eintrag in der Tracking-Tabelle übernommen, oder beides wird zurückgerollt. Der Migrations Runner erreicht das, indem er das SQL der Migrationsdatei und den Insert in schema_migrations in einer gemeinsamen Session mit START TRANSACTION und COMMIT ausführt.
Wichtig zu wissen: DDL-Anweisungen wie CREATE TABLE oder ALTER TABLE lösen in MySQL einen impliziten Commit aus und lassen sich daher nicht in derselben Transaktion wie der Tracking-Insert zurückrollen. Ein realistischer Migrations Runner muss diese Einschränkung kennen und stattdessen mit klaren Fehlermeldungen und manuellen Rollback-Skripten arbeiten, statt sich auf automatische Transaktionssicherheit für DDL zu verlassen.
#!/usr/bin/env bash
# apply-migration.sh — run one migration and record it atomically
set -euo pipefail
apply_migration() {
local db_name="$1"
local file="$2"
local base version name checksum
base="$(basename "$file")"
version="${base%%_*}"
name="${base#*_}"
name="${name%.sql}"
checksum="$(sha256sum "$file" | cut -d' ' -f1)"
echo "[RUN] ${version}: ${name}"
# DML statements can be wrapped in a transaction; DDL auto-commits in MySQL
mysql "$db_name" <<-SQL
SOURCE ${file};
INSERT INTO schema_migrations (version, name, checksum)
VALUES ('${version}', '${name}', '${checksum}');
SQL
echo "[OK] ${version} applied and recorded"
}
7. Rollback mit Down-Migrationen
Für jede Vorwärtsmigration sollte optional eine passende .down.sql Datei existieren, die die Änderung gezielt rückgängig macht. Der Migrations Runner muss beim Rollback zunächst die zuletzt angewendete Version aus der Tracking-Tabelle ermitteln, die zugehörige Down-Datei ausführen und danach den Eintrag aus schema_migrations entfernen.
Fehlt eine Down-Datei, sollte der Migrations Runner den Rollback verweigern und eine eindeutige Fehlermeldung ausgeben, statt stillschweigend nichts zu tun. Gerade bei destruktiven Änderungen wie DROP COLUMN ist ein fehlender Rollback-Pfad ein bewusstes Signal, dass diese Migration in der Produktion nicht ohne Datenverlust rückgängig gemacht werden kann.
#!/usr/bin/env bash
# rollback.sh — revert the most recently applied migration
set -euo pipefail
rollback_last() {
local db_name="$1"
local last_version
last_version="$(mysql -N -B -e \
"SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1;" "$db_name")"
if [[ -z "$last_version" ]]; then
echo "[INFO] No migrations to roll back" >&2
return 0
fi
local down_file
down_file="$(find ./migrations -maxdepth 1 -name "${last_version}_*.down.sql" | head -n 1)"
if [[ -z "$down_file" ]]; then
echo "[ERROR] No down migration found for ${last_version}, refusing to roll back" >&2
exit 1
fi
echo "[ROLLBACK] ${last_version}"
mysql "$db_name" < "$down_file"
mysql "$db_name" -e "DELETE FROM schema_migrations WHERE version = '${last_version}';"
echo "[OK] ${last_version} rolled back"
}
8. Kommandozeilen-Interface: up, down, status
Ein nutzerfreundlicher Migrations Runner bietet drei einfache Subkommandos: up führt alle ausstehenden Migrationen aus, down rollt die letzte Migration zurück, und status zeigt an, welche Migrationen bereits angewendet sind und welche noch fehlen. Diese Struktur orientiert sich bewusst an etablierten Tools wie Flyway oder golang-migrate, damit Entwickler sich sofort zurechtfinden.
Die status Ausgabe eines guten Migrations Runners sollte klar zwischen angewendeten und ausstehenden Migrationen unterscheiden, idealerweise farblich abgesetzt im Terminal. So sieht ein Entwickler auf einen Blick, ob die lokale Datenbank noch auf dem aktuellen Stand ist, bevor er mit der Arbeit beginnt.
#!/usr/bin/env bash
# migrate.sh — CLI entry point: up, down, status
set -euo pipefail
source ./lib/discover-migrations.sh
source ./lib/verify-checksum.sh
source ./lib/apply-migration.sh
source ./lib/rollback.sh
readonly DB_NAME="${DB_NAME:?Set DB_NAME environment variable}"
usage() { echo "Usage: $0 {up|down|status}" >&2; exit 1; }
cmd_up() {
verify_applied_checksums "$DB_NAME"
while IFS= read -r file; do
[[ -z "$file" ]] && continue
apply_migration "$DB_NAME" "$file"
done < <(discover_pending "$DB_NAME")
}
cmd_status() {
echo "Applied migrations:"
mysql "$DB_NAME" -e "SELECT version, name, applied_at FROM schema_migrations ORDER BY version;"
echo "Pending migrations:"
discover_pending "$DB_NAME"
}
case "${1:-}" in
up) cmd_up ;;
down) rollback_last "$DB_NAME" ;;
status) cmd_status ;;
*) usage ;;
esac
9. Migrations Runner im Vergleich zu Alternativen
Ein Bash-basierter Migrations Runner ist nicht die einzige Option, aber für bestimmte Konstellationen die pragmatischste. Wer bereits ein PHP- oder Java-Framework mit eingebauter Migrationsverwaltung nutzt, sollte diese vorziehen. Für polyglotte Infrastruktur-Repositories oder reine SQL-Projekte ohne Anwendungsframework bietet ein eigener Migrations Runner dagegen maximale Kontrolle bei minimalen Abhängigkeiten.
| Werkzeug | Abhängigkeiten | Checksum-Prüfung | Geeignet für |
|---|---|---|---|
| Bash Migrations Runner | Nur mysql CLI | Selbst implementiert | Legacy-PHP, polyglotte Repos |
| Flyway | JVM erforderlich | Eingebaut | Java-Projekte, Enterprise-Setups |
| golang-migrate | Separates Binary | Über Dirty-Flag | Go-Services, CLI-Tools |
| Framework-Migrationen (Doctrine, Laravel) | Volles Framework nötig | Über ORM-Metadaten | Anwendungen mit passendem Framework |
Für viele Deployment-Pipelines, die ohnehin mit Bash-Skripten arbeiten, ist der Aufwand für einen eigenen Migrations Runner gering im Vergleich zum Nutzen: Ein einheitliches Werkzeug für alle Datenbank-Repositories, unabhängig davon, welche Anwendungssprache jeweils darüber liegt.
Mironsoft
Shell-Automatisierung, Datenbank-Tooling und Deployment-Infrastruktur
Ein Migrations Runner, dem euer Team vertraut?
Wir bauen einen zuverlässigen Migrations Runner für eure Datenbank-Umgebungen, mit Checksum-Prüfung, Rollback-Pfaden und sauberer Integration in eure bestehende Deployment-Pipeline.
Skript-Entwicklung
Individueller Migrations Runner passend zu eurem Datenbank-Stack
Sicherheit
Checksum-Prüfung und Rollback-Strategien gegen Schema-Drift
Deployment-Integration
Nahtlose Einbindung in bestehende Release- und CI-Prozesse
10. Zusammenfassung
Ein eigener Migrations Runner in Bash macht Schema-Änderungen über mehrere Umgebungen hinweg nachvollziehbar, ohne dass ein komplettes Framework für die reine Verwaltung von SQL-Dateien nötig wäre. Eine Tracking-Tabelle protokolliert angewendete Versionen, Checksums erkennen nachträgliche Manipulation, und ein sauberes CLI mit up, down und status macht die tägliche Nutzung einfach.
Wichtig bleibt, die Grenzen der Transaktionalität bei DDL-Anweisungen zu kennen und Rollback-Pfade bewusst zu gestalten, statt sie als selbstverständlich vorauszusetzen. Ein Migrations Runner, der diese Punkte berücksichtigt, ist auch für kleinere Teams ohne dediziertes Datenbank-Framework eine robuste, wartbare Lösung für Schema-Versionierung.
Migrations Runner in Bash — Das Wichtigste auf einen Blick
Tracking
Eigene Tabelle mit Version, Name, Checksum und Zeitstempel protokolliert jeden angewendeten Stand.
Integrität
sha256sum erkennt nachträglich veränderte Migrationsdateien und verhindert stille Abweichungen.
DDL-Grenzen
Schema-Änderungen lösen in MySQL einen impliziten Commit aus, echte Transaktionssicherheit gibt es nur für DML.
CLI
up, down und status als klare Subkommandos, angelehnt an etablierte Migrationswerkzeuge.