Zum Inhalt springen

Migrations

Das Modul wippy/migration stellt ein Datenbankmigrations-Framework bereit mit einer kleinen DSL zur Definition von Schema-Aenderungen, einem Runner, der sie entdeckt und ausfuehrt, sowie einem Bootloader, der ausstehende Migrationen fuer jede im Projekt registrierte target_db ausfuehrt.

Migrationen unterstuetzen SQLite, PostgreSQL und MySQL mit treiberspezifischen up/down-Implementierungen, die nebeneinander definiert werden.

Fuege das Modul deinem Projekt hinzu:

Terminal-Fenster
wippy add wippy/migration
wippy install

Deklariere die Abhaengigkeit und die Anwendungsdatenbank, auf die die Migrationen abzielen:

version: "1.0"
namespace: app
entries:
- name: app_db
kind: db.sql.sqlite
path: ./data/app.db
- name: dep.migration
kind: ns.dependency
component: wippy/migration
version: "*"

Der Migrations-Bootloader registriert sich bei wippy/bootloader mit Order 20. Wenn die Anwendung startet, entdeckt er jeden Migrationseintrag in der Registry, gruppiert sie nach meta.target_db und fuehrt ausstehende Migrationen gegen jede Datenbank aus.

Eine Migration ist ein function.lua-Eintrag mit meta.type: migration. Der Eintrag gibt eine von migration.define(...) erzeugte Funktion zurueck.

entries:
- name: 01_create_users_table
kind: function.lua
meta:
type: migration
target_db: app:app_db
timestamp: "2025-01-15T10:00:00Z"
source: file://01_create_users_table.lua
imports:
migration: wippy.migration:migration
return require("migration").define(function()
migration("Create users table", function()
database("sqlite", function()
up(function(db)
local ok, err = db:execute([[
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)
]])
if err then error(err) end
end)
down(function(db)
db:execute("DROP TABLE IF EXISTS users")
end)
end)
database("postgres", function()
up(function(db)
db:execute([[
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)
]])
end)
down(function(db)
db:execute("DROP TABLE IF EXISTS users")
end)
end)
end)
end)
FeldErforderlichBeschreibung
meta.typejaMuss fuer die Erkennung "migration" sein
meta.target_dbjaRegistry-ID der Zieldatenbank
meta.timestampneinISO-8601-Zeitstempel zur Sortierung, wenn mehrere Migrationen dieselbe Datenbank betreffen
meta.tagsneinArray von Tags; der Runner kann Migrationen nach Tag filtern

Migrationen fuer eine Datenbank laufen in aufsteigender meta.timestamp-Reihenfolge.

Innerhalb der an migration.define uebergebenen Funktion stehen drei verschachtelte Funktionen zur Verfuegung:

FunktionBeschreibung
migration(description, fn)Eine neue Migration mit menschenlesbarer Beschreibung oeffnen
database(type, fn)Eine Implementierung fuer "sqlite", "postgres" oder "mysql" deklarieren
up(fn) / down(fn)Vorwaerts- und Rollback-Funktionen definieren
after(fn)Optionaler Post-Migration-Hook (gleiche Transaktion)

Jede up/down/after-Funktion erhaelt ein Transaktionsobjekt, keine Rohverbindung. Alle drei Operationen laufen in einer einzigen Transaktion, die bei Fehlern zurueckgerollt wird.

local rows, err = db:query(sql, params) -- SELECT, returns array of rows
local result, err = db:execute(sql, params) -- INSERT/UPDATE/DDL, returns { rows_affected, last_insert_id }
local stmt, err = db:prepare(sql) -- prepared statement

Verwende stets parametrisierte Abfragen:

db:execute("INSERT INTO users (name, email) VALUES (?, ?)", { "Alice", "alice@example.com" })

Ein Aufruf von error(...) bricht die Migration ab und rollt die Transaktion zurueck. Umhuelle jedes Statement, das fehlschlagen kann:

up(function(db)
local _, err = db:execute("CREATE TABLE ...")
if err then error(err) end
end)

Der Runner wird als Bibliothek fuer die programmatische Nutzung bereitgestellt:

imports:
runner: wippy.migration:runner
local runner = require("runner").setup("app:app_db")
local result = runner:run() -- apply all pending migrations
local result = runner:run_next() -- apply the next pending migration
local result = runner:rollback({ id = "app:01_create_users_table" })
local status = runner:status() -- list applied + pending migrations

Wendet jede ausstehende Migration fuer die konfigurierte Datenbank an. Gibt eine Zusammenfassung zurueck:

{
status = "complete", -- "complete" or "error"
migrations_found = 3,
migrations_applied = 2,
migrations_skipped = 1,
migrations_failed = 0,
duration = 0.123,
migrations = { ... }, -- per-migration status
skipped_details = { ... },
}

Optionen:

OptionBeschreibung
tagsArray von Tags; nur Migrationen, deren meta.tags sich schneiden, werden beruecksichtigt

Rollt eine einzelne Migration per ID zurueck (erforderlich):

runner:rollback({ id = "app:01_create_users_table" })

Gibt { applied = {...}, pending = {...} } zurueck, sortiert nach applied_at bzw. meta.timestamp.

wippy.migration:registry bietet direkte Registry-Abfragen:

FunktionBeschreibung
registry.find({ target_db, tags })Alle Migrationseintraege zurueckgeben, die den Kriterien entsprechen
registry.get(id)Einen einzelnen Migrationseintrag per ID zurueckgeben
registry.get_target_dbs()Jede eindeutige meta.target_db zurueckgeben, die in Migrationen vorkommt
registry.get_tags()Jeden eindeutigen Tag zurueckgeben, der in Migrationen vorkommt

Der Bootloader verwendet diese, um beim Start die vollstaendige Menge an Zieldatenbanken zu entdecken.

Der Runner erzeugt bei der ersten Ausfuehrung in jeder Zieldatenbank eine Tabelle wippy_migrations. Angewendete Migrationen werden per ID aufgezeichnet, sodass nachfolgende Laeufe sie ueberspringen. Die Tracking-Tabelle wird automatisch erzeugt; schreibe keine eigene Migration, um sie anzulegen.

  • Eine logische Aenderung pro Migration - eine Tabelle anlegen, eine Spalte hinzufuegen, einen Index erstellen.
  • Ein echtes down schreiben - wenn ein Rollback unmoeglich ist (Datenverlust), dokumentiere das und wirf einen Fehler, anstatt stillschweigend Erfolg zurueckzumelden.
  • Idempotenz bevorzugen - CREATE TABLE IF NOT EXISTS und DROP TABLE IF EXISTS ueberstehen wiederholte Ausfuehrungen ohne besondere Behandlung.
  • DDL und DML trennen - vermeide es nach Moeglichkeit, Daten in derselben Migration zu seeden, die eine Tabelle anlegt.
  • Beide Richtungen testen - wende die Migration an, rolle sie zurueck und verifiziere, dass das Schema dem Ausgangszustand entspricht.