Skip to content

Latest commit

 

History

History
1573 lines (1213 loc) · 58.5 KB

File metadata and controls

1573 lines (1213 loc) · 58.5 KB

DDL-Generierungsregeln: d-migrate

Regeln für die Erzeugung datenbankspezifischer DDL-Statements aus dem neutralen Modell

Dokumenttyp: Spezifikation / Referenz


1. Allgemeine Regeln

1.1 Generierungsreihenfolge

DDL-Statements werden in einer definierten Reihenfolge erzeugt, um Abhängigkeiten zu respektieren:

1. Custom Types        (CREATE TYPE — nur PostgreSQL)
2. Sequences           (CREATE SEQUENCE — nur PostgreSQL)
3. Tabellen            (CREATE TABLE — topologisch sortiert nach Foreign Keys)
4. Indizes             (CREATE INDEX — nach den zugehörigen Tabellen)
5. Views               (CREATE VIEW — topologisch sortiert nach Abhängigkeiten)
6. Functions           (CREATE FUNCTION — nach Views)
7. Procedures          (CREATE PROCEDURE — nach Functions)
8. Triggers            (CREATE TRIGGER — nach Tabellen und Functions)

Topologische Sortierung: Tabellen mit Foreign Keys werden nach den referenzierten Tabellen erzeugt. Bei zirkulären Referenzen:

  1. Tabellen ohne FK-Constraints erzeugen
  2. FK-Constraints nachträglich via ALTER TABLE ... ADD CONSTRAINT hinzufügen

1.2 Header

Jedes generierte DDL beginnt mit einem Header-Kommentar:

-- Generated by d-migrate <version>
-- Source: neutral schema v<schema_version> "<schema_name>"
-- Target: <dialect> | Generated: <ISO-8601-timestamp>

Tool-Export-Determinismus (0.7.0): Bei d-migrate export flyway|liquibase| django|knex wird der Generated: <ISO-8601-timestamp> Laufzeit-Timestamp nicht in die Tool-Artefakte übernommen. Gleiches Schema + gleiche Flags muss identische Artefaktinhalte erzeugen. Provenienz bleibt im Report oder in stabilen, nicht zeitabhängigen Metadaten sichtbar.

schema generate-Determinismus (0.9.5):

  • Ohne weitere Flags bleibt der Header wie oben beschrieben und nutzt einen Laufzeit-Timestamp.
  • Ist SOURCE_DATE_EPOCH gesetzt, bleibt die Header-Form erhalten, aber Generated: wird aus diesem Unix-Epoch-Sekundenwert als UTC-Instant abgeleitet.
  • Mit --deterministic wird der Laufzeit-Timestamp aus DDL, JSON-DDL-Feldern und Sidecar-Report entfernt. Der Header endet dann bei -- Target: <dialect>.
  • Ist --deterministic zusammen mit SOURCE_DATE_EPOCH gesetzt, bestimmt --deterministic weiterhin die Output-Policy; der stabile Zeitwert wird nicht als volatile Provenienz ausgegeben.

1.3 Encoding

  • Generierte Dateien sind immer UTF-8 ohne BOM
  • Zeilenumbrüche: \n (LF), nicht \r\n (CRLF)

2. Identifier-Quoting

2.1 Regeln pro Dialekt

Regel PostgreSQL MySQL SQLite
Quote-Zeichen " (Double Quote) ` (Backtick) " (Double Quote)
Wann quoten Reservierte Wörter, Sonderzeichen, Groß-/Kleinschreibung Immer (konsistent) Reservierte Wörter, Sonderzeichen
Case-Sensitivity Unquoted → lowercase Systemabhängig Case-insensitive

2.2 Quoting-Strategie

Aktueller Vertrag: Immer quoten (fest verdrahtetes Defensive Quoting)

Begründung:

  • Konsistentes Verhalten über alle Dialekte
  • Keine Überraschungen bei reservierten Wörtern
  • Zukunftssicher (neue reservierte Wörter in DB-Updates)

Späterer Milestone: Nur reservierte Wörter quoten (geplante Option ddl.quote_identifiers: reserved_only)

Hinweis: Der aktuelle Implementierungsstand bietet keine öffentliche Quoting-Konfigurationsoption. Der Generator quotet derzeit immer. reserved_only ist nur als zukünftige Option dokumentiert und noch nicht im Generator, im Optionsmodell oder im CLI-/Config-Pfad verdrahtet.

In diesem Modus werden Identifier nur gequotet, wenn:

  • Der Name ein reserviertes Wort des Zieldialekts ist (je Dialekt gepflegte Liste)
  • Der Name Sonderzeichen enthält (Leerzeichen, Bindestriche, Punkte)
  • Der Name mit einer Ziffer beginnt
  • Der Name Groß-/Kleinschreibung erfordert, die vom Dialekt nicht default ist

Reservierte-Wörter-Listen werden pro Dialekt mitgeliefert (Quelle: offizielle Dokumentation der jeweiligen DB-Version).

-- PostgreSQL
CREATE TABLE "customers" (
    "id" SERIAL,
    "email" VARCHAR(254) NOT NULL
);

-- MySQL
CREATE TABLE `customers` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `email` VARCHAR(254) NOT NULL
);

-- SQLite
CREATE TABLE "customers" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "email" TEXT NOT NULL
);

2.3 Escape-Regeln

Zeichen im Identifier PostgreSQL MySQL SQLite
Quote-Zeichen selbst "" (verdoppeln) `` (verdoppeln) "" (verdoppeln)
Sonstige Sonderzeichen Innerhalb Quotes erlaubt Innerhalb Backticks erlaubt Innerhalb Quotes erlaubt

Beispiel:

-- Spalte heißt "user's name"
-- PostgreSQL/SQLite:
"user's name"

-- MySQL:
`user's name`

3. Tabellen-Generierung

3.1 CREATE TABLE Struktur

CREATE TABLE <quoted_name> (
    <spalten>,
    <inline_constraints>
) <table_options>;

3.2 Spalten

Reihenfolge innerhalb einer Spalte:

<quoted_name> <type> [NOT NULL] [DEFAULT <value>] [UNIQUE] [<inline_constraint>]

3.3 PostgreSQL

CREATE TABLE "orders" (
    "id" SERIAL,
    "customer_id" INTEGER NOT NULL,
    "order_date" TIMESTAMP NOT NULL,
    "total_amount" DECIMAL(10,2),
    "status" TEXT DEFAULT 'pending',
    "is_archived" BOOLEAN DEFAULT FALSE,
    PRIMARY KEY ("id"),
    CONSTRAINT "fk_orders_customer_id" FOREIGN KEY ("customer_id")
        REFERENCES "customers" ("id") ON DELETE RESTRICT
);

Besonderheiten:

  • identifierSERIAL (auto-increment via Sequence)
  • booleanBOOLEAN (native Unterstützung)
  • jsonJSONB (binäres JSON, performanter)
  • enum mit ref_type → separater CREATE TYPE (vor der Tabelle)
  • enum inline → TEXT + CHECK Constraint
  • datetime mit timezone: trueTIMESTAMP WITH TIME ZONE

3.4 MySQL

CREATE TABLE `orders` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `customer_id` INT NOT NULL,
    `order_date` DATETIME NOT NULL,
    `total_amount` DECIMAL(10,2),
    `status` ENUM('pending','processing','shipped','delivered','cancelled')
             DEFAULT 'pending',
    `is_archived` TINYINT(1) DEFAULT 0,
    PRIMARY KEY (`id`),
    CONSTRAINT `fk_orders_customer_id` FOREIGN KEY (`customer_id`)
        REFERENCES `customers` (`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Besonderheiten:

  • identifierINT NOT NULL AUTO_INCREMENT
  • booleanTINYINT(1), Defaults true/false1/0
  • jsonJSON (kein JSONB)
  • enum → inline ENUM(...) (kein separater Type)
  • datetime mit timezone: trueDATETIME + Warnung W100 (kein TZ-Support)
  • Immer ENGINE=InnoDB (FK-Support)
  • Immer DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

3.5 SQLite

CREATE TABLE "orders" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "customer_id" INTEGER NOT NULL REFERENCES "customers"("id") ON DELETE RESTRICT,
    "order_date" TEXT NOT NULL,
    "total_amount" REAL,
    "status" TEXT DEFAULT 'pending'
             CHECK ("status" IN ('pending','processing','shipped','delivered','cancelled')),
    "is_archived" INTEGER DEFAULT 0
);

Besonderheiten:

  • identifierINTEGER PRIMARY KEY AUTOINCREMENT
  • booleanINTEGER, Defaults true/false1/0
  • jsonTEXT (JSON-Funktionen verfügbar ab 3.38)
  • datetime, date, timeTEXT (ISO 8601 Format)
  • decimalREAL + Warnung W200 (Präzisionsverlust)
  • enumTEXT + CHECK Constraint
  • Foreign Keys inline oder als CONSTRAINT
  • Kein ALTER TABLE ADD COLUMN ... REFERENCES (FK müssen inline sein)

3.6 ALTER TABLE (Schema-Migration)

Hinweis: Dieser Abschnitt beschreibt den geplanten diff-basierten schema migrate-Pfad eines spaeteren Milestones. Der Befehl ist im aktuellen CLI-Umfang noch nicht verfuegbar.

Für schema migrate erzeugt der Generator ALTER TABLE-Statements. Die Syntax variiert pro Dialekt:

ADD COLUMN

-- PostgreSQL
ALTER TABLE "orders" ADD COLUMN "priority" SMALLINT DEFAULT 0;

-- MySQL
ALTER TABLE `orders` ADD COLUMN `priority` SMALLINT DEFAULT 0;

-- SQLite (ab 3.2.0, eingeschränkt: kein NOT NULL ohne Default, kein REFERENCES)
ALTER TABLE "orders" ADD COLUMN "priority" INTEGER DEFAULT 0;

ALTER COLUMN TYPE

-- PostgreSQL
ALTER TABLE "orders" ALTER COLUMN "notes" TYPE TEXT;
ALTER TABLE "orders" ALTER COLUMN "status" SET DEFAULT 'pending';
ALTER TABLE "orders" ALTER COLUMN "email" SET NOT NULL;

-- MySQL
ALTER TABLE `orders` MODIFY COLUMN `notes` TEXT;
ALTER TABLE `orders` ALTER COLUMN `status` SET DEFAULT 'pending';
ALTER TABLE `orders` MODIFY COLUMN `email` VARCHAR(254) NOT NULL;

-- SQLite: ALTER COLUMN TYPE ist NICHT unterstützt.
-- Workaround: Tabelle neu erstellen (Rename → Create → Copy → Drop)
-- Dieser Workaround wird automatisch generiert mit Warnung.

DROP COLUMN

-- PostgreSQL
ALTER TABLE "orders" DROP COLUMN "legacy_field";

-- MySQL
ALTER TABLE `orders` DROP COLUMN `legacy_field`;

-- SQLite (ab 3.35.0)
ALTER TABLE "orders" DROP COLUMN "legacy_field";
-- Für SQLite < 3.35.0: Tabellen-Rebuild-Workaround

ADD/DROP CONSTRAINT

-- PostgreSQL
ALTER TABLE "orders" ADD CONSTRAINT "fk_orders_product" FOREIGN KEY ("product_id")
    REFERENCES "products" ("id");
ALTER TABLE "orders" DROP CONSTRAINT "fk_orders_product";

-- MySQL
ALTER TABLE `orders` ADD CONSTRAINT `fk_orders_product` FOREIGN KEY (`product_id`)
    REFERENCES `products` (`id`);
ALTER TABLE `orders` DROP FOREIGN KEY `fk_orders_product`;

-- SQLite: ADD/DROP CONSTRAINT ist NICHT unterstützt.
-- Workaround: Tabellen-Rebuild (Rename → Create mit neuen Constraints → Copy → Drop)

3.7 SQLite ALTER TABLE Einschränkungen

SQLite hat die restriktivsten ALTER TABLE-Fähigkeiten. Der DDL-Generator muss diese Einschränkungen kennen:

Operation SQLite-Support Workaround
ADD COLUMN Ab 3.2.0 (eingeschränkt)
ADD COLUMN NOT NULL (ohne Default) Nicht unterstützt Default-Wert erzwingen
ADD COLUMN REFERENCES Nicht unterstützt Tabellen-Rebuild
DROP COLUMN Ab 3.35.0 Tabellen-Rebuild für ältere Versionen
ALTER COLUMN TYPE Nicht unterstützt Tabellen-Rebuild
ADD CONSTRAINT Nicht unterstützt Tabellen-Rebuild
DROP CONSTRAINT Nicht unterstützt Tabellen-Rebuild
RENAME COLUMN Ab 3.25.0 Tabellen-Rebuild für ältere Versionen

Tabellen-Rebuild-Strategie (automatisch generiert):

-- 1. Temporäre Tabelle erstellen
ALTER TABLE "orders" RENAME TO "_orders_old";

-- 2. Neue Tabelle mit gewünschter Struktur
CREATE TABLE "orders" (
    -- ... neue Spaltendefinitionen ...
);

-- 3. Daten kopieren
INSERT INTO "orders" (col1, col2, ...)
    SELECT col1, col2, ... FROM "_orders_old";

-- 4. Alte Tabelle löschen
DROP TABLE "_orders_old";

Wann wird der Rebuild erzeugt?

Kommando Verhalten
schema generate Kein Rebuild — erzeugt nur CREATE TABLE DDL (Neuerstellung). Der Rebuild ist nur bei Schema-Änderungen an bestehenden Tabellen relevant.
schema migrate (0.5.0) Rebuild wird generiert wenn eine ALTER-Operation für SQLite nicht unterstützt wird (z.B. ALTER COLUMN TYPE, ADD CONSTRAINT).

Hinweis: Der hier beschriebene Rebuild-Workaround gehoert zum geplanten schema migrate-Pfad eines spaeteren Milestones und ist nicht Teil des aktuellen CLI-Funktionsumfangs.

Für schema generate (0.2.0) ist der Rebuild-Workaround also nicht relevant — er wird erst mit schema migrate (0.5.0) implementiert.


4. Constraint-Generierung

4.1 Primary Key

Variante PostgreSQL MySQL SQLite
Einfach PRIMARY KEY ("id") PRIMARY KEY (\id`)` "id" INTEGER PRIMARY KEY
Composite PRIMARY KEY ("a", "b") PRIMARY KEY (\a`, `b`)` PRIMARY KEY ("a", "b")
Mit Name CONSTRAINT "pk_orders" PRIMARY KEY ("id") — (MySQL ignoriert PK-Name)

4.2 Foreign Key

-- PostgreSQL
CONSTRAINT "fk_orders_customer_id" FOREIGN KEY ("customer_id")
    REFERENCES "customers" ("id") ON DELETE RESTRICT ON UPDATE CASCADE

-- MySQL
CONSTRAINT `fk_orders_customer_id` FOREIGN KEY (`customer_id`)
    REFERENCES `customers` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE

-- SQLite (inline bevorzugt)
"customer_id" INTEGER NOT NULL REFERENCES "customers"("id")
    ON DELETE RESTRICT ON UPDATE CASCADE

ON DELETE / ON UPDATE Aktionen:

Neutral SQL
restrict RESTRICT
cascade CASCADE
set_null SET NULL
set_default SET DEFAULT
no_action NO ACTION

4.3 CHECK Constraint

Trusted Input: constraint.expression wird als Raw-SQL-Fragment direkt in die DDL-Ausgabe interpoliert. Der Wert stammt aus der Schema-YAML-Datei, die vom Schema-Autor kontrolliert wird. Keine Sanitization findet statt.

Diff-Migrationen (Plan-2 §F.5): CHECK- und EXCLUDE-Constraints werden im Diff-Pfad nur konservativ per SQL-Text verglichen. Der Vergleich normalisiert Zeilenenden auf LF und ignoriert umgebenden Whitespace, fuehrt aber keine semantische SQL-Kanonisierung durch. Unveraenderte Constraints blockieren andere Tabellenoperationen nicht. Hinzugefuegte, entfernte oder geaenderte CHECK-/EXCLUDE-Constraints bleiben mit CONSTRAINT_NOT_DIFFABLE blockierend, bis ein dialektspezifischer Render-, Enforcement- und Daten-Preflight-Vertrag existiert.

-- PostgreSQL
CONSTRAINT "chk_total_positive" CHECK ("total_amount" >= 0)

-- MySQL (ab 8.0.16)
CONSTRAINT `chk_total_positive` CHECK (`total_amount` >= 0)

-- SQLite
CHECK ("total_amount" >= 0)

4.4 UNIQUE Constraint

-- Als Spalten-Attribut (einfache Spalte)
"email" VARCHAR(254) NOT NULL UNIQUE

-- Als benannter Constraint (multi-column)
CONSTRAINT "uq_customer_date" UNIQUE ("customer_id", "order_date")

4.5 Constraint-Positionierung

Constraint-Typ Position
NOT NULL Inline an der Spalte
DEFAULT Inline an der Spalte
UNIQUE (einspaltig) Inline an der Spalte
PRIMARY KEY (einspaltig, ohne identifier) Am Ende der Tabelle
PRIMARY KEY (identifier) Inline an der Spalte (SQLite) oder am Ende
FOREIGN KEY Am Ende der Tabelle (PostgreSQL, MySQL) oder inline (SQLite)
CHECK Am Ende der Tabelle
UNIQUE (mehrspaltig) Am Ende der Tabelle

5. Index-Generierung

5.1 Standard-Index

CREATE INDEX "<name>" ON "<table>" ("<col1>", "<col2>");

Indexspalten koennen optional eine Richtung tragen:

CREATE INDEX "<name>" ON "<table>" ("<col1>" DESC, "<col2>" ASC);

Ohne explizite Richtung wird nur der Identifier gerendert. Reverse-Reader normalisieren aufsteigende/default Metadaten zu keiner expliziten Richtung und transportieren DESC verlustarm.

5.2 Index-Typen pro Dialekt

Neutral PostgreSQL MySQL (InnoDB) SQLite
btree USING BTREE (Default) USING BTREE (Default) Default
hash USING HASH Explizites USING HASH auf InnoDB nicht unterstützt → BTREE + W102 Nicht unterstützt → Default + W102
gin USING GIN Nicht unterstützt → Weglassen + W102 Nicht unterstützt → Weglassen + W102
gist USING GIST Nicht unterstützt → Weglassen + W102 Nicht unterstützt → Weglassen + W102
brin USING BRIN Nicht unterstützt → Weglassen + W102 Nicht unterstützt → Weglassen + W102

5.3 Unique-Index

CREATE UNIQUE INDEX "idx_customers_email" ON "customers" ("email");

5.4 Partial-Index

-- PostgreSQL
CREATE INDEX "idx_active_orders" ON "orders" ("status") WHERE "status" != 'cancelled';

-- SQLite
CREATE INDEX "idx_active_orders" ON "orders" ("status") WHERE "status" != 'cancelled';

Das neutrale Feld index.where ist ein Trusted-Input Raw-SQL-Praedikat fuer die Ziel-Engine. Es wird nicht geparst oder dialektuebergreifend transformiert.

Ziel Verhalten
PostgreSQL WHERE <predicate> wird gerendert und per Reverse gelesen
SQLite WHERE <predicate> wird gerendert und per Reverse aus sqlite_master.sql gelesen
MySQL Kein stiller Predicate-Verlust: Index wird uebersprungen und action_required E057 erzeugt

Partial-UNIQUE wird genauso behandelt. MySQL darf daraus keinen normalen Unique-Index erzeugen, weil das strenger waere als die Quelle.


6. Custom-Type-Generierung

6.1 Enum-Typen

-- PostgreSQL (separater Typ vor Tabellen)
CREATE TYPE "order_status" AS ENUM ('pending', 'processing', 'shipped', 'delivered', 'cancelled');

-- Verwendung in Tabelle:
"status" "order_status" DEFAULT 'pending'
-- MySQL (inline in Tabelle)
`status` ENUM('pending','processing','shipped','delivered','cancelled') DEFAULT 'pending'
-- SQLite (TEXT + CHECK)
"status" TEXT DEFAULT 'pending' CHECK ("status" IN ('pending','processing','shipped','delivered','cancelled'))

6.2 Composite-Typen

-- PostgreSQL (native Unterstützung)
CREATE TYPE "address" AS (
    "street" VARCHAR(200),
    "city" VARCHAR(100),
    "zip" CHAR(10),
    "country" CHAR(2)
);

Für MySQL/SQLite: Kein nativer Support. Verhalten abhängig von konfigurierter Fallback-Strategie:

  • json: JSON-Spalte verwenden
  • flatten: Einzelne Spalten pro Feld erzeugen
  • action_required: Fehler mit Hinweis (Default)

6.3 Domain-Typen

-- PostgreSQL
CREATE DOMAIN "positive_amount" AS DECIMAL(10,2) CHECK (VALUE >= 0);

Für MySQL/SQLite: Als Basistyp + CHECK Constraint inline an der Spalte.


7. Sequence-Generierung

-- PostgreSQL (native Unterstützung)
CREATE SEQUENCE "invoice_number_seq"
    START WITH 10000
    INCREMENT BY 1
    MINVALUE 10000
    MAXVALUE 99999999
    NO CYCLE
    CACHE 20;

MySQL: Gesteuert ueber --mysql-named-sequences (seit 0.9.3):

  • action_required (Default): Sequences werden mit E056 uebersprungen.
  • helper_table: Emulation ueber kanonische Hilfsobjekte:
    • Tabelle dmg_sequences mit Metadaten pro Sequence
    • Routinen dmg_nextval(seq_name) und dmg_setval(seq_name, value)
    • BEFORE INSERT-Trigger pro sequence-basierter Spalte
    • Lossy-Mapping-Warnung W115 (explizites NULL vs. ausgelassener Wert)
    • Cache-Warnung W114 (Preallocation nicht emuliert)
    • Transaktionswarnung W117 (Rollback retrahiert Inkremente)

Details: mysql-sequence-emulation-plan.md.

SQLite: Keine nativen benannten Sequenzen. Standard ist action_required (E056-Skip). Mit --sqlite-named-sequences helper_table (0.9.7) wird die Emulation eingeschaltet:

  • dmg_sequences-Hilfstabelle (TEXT-/INTEGER-Spalten gemäß Plan §3.2: managed_by, format_version, name, next_value, last_returned_value, exhausted, increment_by, min_value, max_value, cycle_enabled, cache_size)
  • Seed-INSERT pro SequenceDefinition (managed_by = 'd-migrate', format_version = 'sqlite-sequence-v1')
  • kanonisches BEFORE INSERT/AFTER INSERT-Trigger-Paar (dmg_seq_<table16>_<col16>_<hash10>_{bi,ai}) pro DefaultValue.SequenceNextVal-Spalte: BEFORE INSERT reserviert und inkrementiert atomar, AFTER INSERT schreibt den reservierten Wert per UPDATE … WHERE ROWID = NEW.ROWID in die Zeile (WITHOUT ROWIDE057-Skip)
  • Cache-Warnung W114 (SQLite ist Single-Writer, keine echte Preallocation; cache_size wird nur als Metadaten gespeichert)
  • W115 lossy-NULL-Semantik · W117 transaktions-bound · W119 NOT-NULL- und CHECK-IS NOT NULL-Suppression auf sequence-getragenen Spalten · W121 Conflict-Gap-INFO (ON CONFLICT DO UPDATE/DO NOTHING, INSERT OR IGNORE, INSERT OR FAIL multi-row) · W122 Warnung wenn auf derselben Tabelle nutzerdefinierte UPDATE-Trigger existieren (recursive_triggers = ON-Risiko)
  • Rollback-Inversion entfernt das Trigger-Paar und dmg_sequences
  • E058-Rollback-Preflight blockt den DROP wenn fremde Objekte dmg_sequences referenzieren; E124 blockt die Generierung wenn das neutrale Schema bereits Objekte mit reservierten Hilfsnamen (dmg_sequences oder dem kanonischen dmg_seq_*_{bi,ai}-Pattern) enthält
  • Reverse erkennt die Hilfsobjekte über den /* d-migrate:sqlite-sequence-v1 … */-Marker (primär) oder über das 5-Kriterien-Sekundär-Matching (Name, Event/Timing, WHEN-Klausel, Token-basierte Body-Prüfung) und faltet sie auf schema.sequences + DefaultValue.SequenceNextVal zurück; degradierte Fälle landen unter W116 (Sekundär-Match), W120 (Marker ok, Body modifiziert) oder W124 (User-BEFORE-INSERT-Trigger maskiert das Sequence-Trigger-Paar)

Details: sqlite-sequence-emulation-plan.md.

Diff-Migrationen (Plan-2 §E.3): PostgreSQL rendert im diffbasierten Migrationspfad deklarative CREATE SEQUENCE, ALTER SEQUENCE und DROP SEQUENCE-Operationen fuer die neutralen Attribute start, increment, minValue, maxValue, cycle und cache. sequence_nextval-Defaults werden im Plan nach einer im selben Diff erzeugten Sequence sortiert. Der live aktuelle Sequence-Wert wird in diesem Slice nicht uebernommen oder zurueckgesetzt; MySQL- und SQLite-Sequence-Migrationen sind seit 0.9.7 im helper_table-Modus live: CreateSequence emittiert einen INSERT INTO dmg_sequences, AlterSequence ein UPDATE, DropSequence ein DELETE (mit E058-Preflight + gebundenen Trigger-DROPs), RenameSequence ein UPDATE name+Trigger-Pair-Rebuild und AlterSequenceCurrentValue ein UPDATE next_value. AddColumn mit SequenceNextVal emittiert das Trigger-Paar gleich mit. supportsCurrentValue- Preserve ist im 0.9.7-E.3-Folge-Slice fuer SQLite auf true gesetzt: der SqliteSequenceCurrentValueProbe-Adapter liest dmg_sequences.next_value live, der SequencePreserveStage enthaelt SQLite in der Allowlist und blockt ohne --sqlite-named-sequences helper_table mit SEQUENCE_PRESERVE_OPT_IN_REQUIRED (Mapper auf MANUAL_ACTION_REQUIRED). Mit Opt-in emittiert AlterSequenceCurrentValue Up ein UPDATE dmg_sequences SET next_value = <probedValue> WHERE name = '<applyRef>' und Down ein spiegelgleiches UPDATE gegen probeSequenceRef.name (bei Rename der vor-Rename-Name). Ein fehlender restoreValue (typisch fuer CreateSequence ohne deterministischen Vorzustand) surfaced als SQLITE_SEQUENCE_CURRENT_VALUE_DOWN_ROLLBACK_IMPOSSIBLE-Skip, kein stiller UPDATE. Details siehe sqlite-sequence-emulation-plan.md §6.2 und Phasen F/G, plus den 0.9.7-E.3-Folge-Slice ImpPlan-0.9.7-sqlite-sequence-preserve-current-value.md.


8. View-Generierung

8.1 Standard-Views

-- PostgreSQL
CREATE OR REPLACE VIEW "active_orders" AS
    SELECT o.*, c."name" AS customer_name
    FROM "orders" o
    JOIN "customers" c ON o."customer_id" = c."id"
    WHERE o."status" NOT IN ('delivered', 'cancelled');

-- MySQL
CREATE OR REPLACE VIEW `active_orders` AS
    SELECT o.*, c.`name` AS customer_name
    FROM `orders` o
    JOIN `customers` c ON o.`customer_id` = c.`id`
    WHERE o.`status` NOT IN ('delivered', 'cancelled');

-- SQLite
CREATE VIEW IF NOT EXISTS "active_orders" AS
    SELECT o.*, c."name" AS customer_name
    FROM "orders" o
    JOIN "customers" c ON o."customer_id" = c."id"
    WHERE o."status" NOT IN ('delivered', 'cancelled');

8.2 Materialized Views

-- PostgreSQL (native Unterstützung)
CREATE MATERIALIZED VIEW "monthly_revenue" AS
    SELECT ...;

-- MySQL/SQLite: Nicht unterstützt → Standard-View + W103

Im diff-basierten schema migrate-Pfad werden Materialized Views nicht als normale Views gerendert. Operationen mit materialized: true blockieren, bis ein ausführbarer Refresh-/Staleness-Vertrag existiert. Der Migrationsreport weist diese Operationen als MATERIALIZED_VIEW aus und enthält unter materializedViews[] die Felder stalenessAfterUp, refreshSteps, locking und rollback; vor einem ausführbaren Vertrag steht stalenessAfterUp auf UNKNOWN_BLOCKED und der einzige geplante Refresh-Schritt ist BLOCKED_REFRESH_CONTRACT_REQUIRED.

8.3 View-Query-Transformation

View-Queries können dialektspezifische Funktionen enthalten. Der DDL-Generator führt regelbasierte Textsubstitution auf dem Query-String durch.

Automatisch transformierte Funktionen

Funktion PostgreSQL MySQL SQLite
NOW() Nativ Nativ datetime('now')
CURRENT_TIMESTAMP Nativ Nativ datetime('now')
CURRENT_DATE Nativ CURDATE() date('now')
CURRENT_TIME Nativ CURTIME() time('now')
DATE_TRUNC('month', col) Nativ DATE_FORMAT(col, '%Y-%m-01') strftime('%Y-%m-01', col)
DATE_TRUNC('year', col) Nativ DATE_FORMAT(col, '%Y-01-01') strftime('%Y-01-01', col)
DATE_TRUNC('day', col) Nativ DATE(col) date(col)
EXTRACT(YEAR FROM col) Nativ YEAR(col) CAST(strftime('%Y', col) AS INTEGER)
EXTRACT(MONTH FROM col) Nativ MONTH(col) CAST(strftime('%m', col) AS INTEGER)
COALESCE(a, b) Nativ Nativ Nativ
NULLIF(a, b) Nativ Nativ Nativ
CAST(x AS type) Nativ Nativ (Typ-Mapping) Nativ (Typ-Mapping)
CONCAT(a, b) Nativ Nativ a || b
LENGTH(s) Nativ CHAR_LENGTH(s) LENGTH(s)
SUBSTRING(s FROM n FOR m) Nativ SUBSTRING(s, n, m) SUBSTR(s, n, m)
BOOLEAN Literale (TRUE/FALSE) Nativ 1/0 1/0

Transparente Funktionen (in allen Dialekten identisch)

Diese Funktionen werden nicht transformiert, da sie überall unterstützt werden: COUNT, SUM, AVG, MIN, MAX, ABS, ROUND, UPPER, LOWER, TRIM, REPLACE, LIKE, IN, BETWEEN, CASE...WHEN, GROUP BY, ORDER BY, HAVING, LIMIT, OFFSET, DISTINCT, JOIN, LEFT JOIN, RIGHT JOIN, UNION, EXISTS, NOT EXISTS.

Nicht transformierbare Funktionen

Funktionen die nicht in der obigen Tabelle stehen und dialektspezifisch sind, werden nicht automatisch transformiert. Verhalten:

Situation Verhalten
source_dialect = Ziel-Dialekt Query wird 1:1 übernommen
source_dialect ≠ Ziel-Dialekt, unbekannte Funktion erkannt Query wird 1:1 übernommen + Warnung W111: "View query may contain dialect-specific functions"
Kein source_dialect gesetzt Query wird 1:1 übernommen (Annahme: Standard-SQL)

Die Erkennung unbekannter Funktionen erfolgt über eine einfache Heuristik: Wenn der Query Funktionsnamen enthält, die weder in der Transformationstabelle noch in der transparenten Liste stehen, wird W111 erzeugt. Für 0.2.0 ist dies eine Best-Effort-Prüfung — false positives sind akzeptabel.

Identifier-Quoting in View-Queries

Identifier in View-Queries werden gemäß Ziel-Dialekt gequotet (§2). Der Query-String wird dafür nicht vollständig geparst, sondern nur die bekannten Tabellen- und Spaltennamen (aus dependencies) werden ersetzt.


9. Partitionierungs-DDL

9.1 PostgreSQL

CREATE TABLE "orders" (
    "id"         SERIAL,
    "order_date" TIMESTAMP NOT NULL,
    "amount"     DECIMAL(10,2),
    PRIMARY KEY ("id", "order_date")
) PARTITION BY RANGE ("order_date");

CREATE TABLE "orders_2024" PARTITION OF "orders"
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

CREATE TABLE "orders_2025" PARTITION OF "orders"
    FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

9.2 MySQL

CREATE TABLE `orders` (
    `id`         INT NOT NULL AUTO_INCREMENT,
    `order_date` DATETIME NOT NULL,
    `amount`     DECIMAL(10,2),
    PRIMARY KEY (`id`, `order_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (YEAR(`order_date`)) (
    PARTITION orders_2024 VALUES LESS THAN (2025),
    PARTITION orders_2025 VALUES LESS THAN (2026)
);

Hinweis: MySQL erfordert, dass der Partitionsschlüssel Teil des Primary Key ist.

9.3 SQLite

SQLite unterstützt keine native Partitionierung. Bei partitioning-Konfiguration wird action_required (E055) erzeugt.

9.4 Partitionstypen

Neutral PostgreSQL MySQL SQLite
range PARTITION BY RANGE PARTITION BY RANGE Nicht unterstützt
list PARTITION BY LIST PARTITION BY LIST Nicht unterstützt
hash PARTITION BY HASH PARTITION BY HASH Nicht unterstützt

10. Trigger-DDL

10.1 PostgreSQL

CREATE OR REPLACE FUNCTION "trg_fn_orders_updated_at"()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = CURRENT_TIMESTAMP;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER "trg_orders_updated_at"
    BEFORE UPDATE ON "orders"
    FOR EACH ROW
    EXECUTE FUNCTION "trg_fn_orders_updated_at"();

PostgreSQL-Besonderheit: Trigger-Logik liegt in einer separaten Trigger-Function.

10.2 MySQL

DELIMITER //
CREATE TRIGGER `trg_orders_updated_at`
    BEFORE UPDATE ON `orders`
    FOR EACH ROW
BEGIN
    SET NEW.updated_at = CURRENT_TIMESTAMP;
END //
DELIMITER ;

10.4 MySQL DELIMITER-Regeln

DELIMITER wird für alle Statements benötigt, die einen BEGIN...END-Block enthalten:

Objekt-Typ DELIMITER nötig Begründung
CREATE TABLE Nein Kein BEGIN...END
CREATE INDEX Nein Kein BEGIN...END
CREATE VIEW Nein Kein BEGIN...END
CREATE TRIGGER Ja Enthält BEGIN...END
CREATE FUNCTION Ja Enthält BEGIN...END
CREATE PROCEDURE Ja Enthält BEGIN...END
CREATE EVENT Ja Kann BEGIN...END enthalten

Gruppierung: DELIMITER-Wechsel werden pro Statement gesetzt, nicht für die gesamte Datei:

-- Tabellen, Indizes, Views (ohne DELIMITER)
CREATE TABLE `customers` (...);
CREATE INDEX `idx_email` ON `customers` (`email`);

-- Trigger, Functions, Procedures (mit DELIMITER)
DELIMITER //
CREATE TRIGGER `trg_updated` ... BEGIN ... END //
DELIMITER ;

DELIMITER //
CREATE FUNCTION `calc_total`(...) ... BEGIN ... END //
DELIMITER ;

10.3 SQLite

CREATE TRIGGER "trg_orders_updated_at"
    BEFORE UPDATE ON "orders"
    FOR EACH ROW
BEGIN
    UPDATE "orders" SET "updated_at" = datetime('now')
    WHERE rowid = NEW.rowid;
END;

SQLite-Besonderheit: Kein direktes NEW.col = value; stattdessen UPDATE-Statement nötig.

10.5 Trigger-Body-Transformation

Trigger-Bodies können dialektspezifische prozedurale Logik enthalten. Es gelten die gleichen Regeln wie für Functions/Procedures (§11):

Situation Verhalten
source_dialect = Ziel-Dialekt Body wird 1:1 übernommen
source_dialect ≠ Ziel-Dialekt, Body vorhanden Body wird übersprungen, action_required (E053) erzeugt
source_dialect ≠ Ziel-Dialekt, Body leer/null action_required (E053): Trigger muss manuell implementiert werden
Kein source_dialect angegeben Body wird 1:1 übernommen (Annahme: dialektneutral)

PostgreSQL-Sonderfall: Trigger-Logik liegt in einer separaten Function. Wenn ein Trigger nach PostgreSQL generiert wird, erzeugt der Generator automatisch eine Trigger-Function aus dem Body:

Trigger-Body → CREATE FUNCTION trg_fn_<name>() ... + CREATE TRIGGER ... EXECUTE FUNCTION trg_fn_<name>()

Wenn ein PostgreSQL-Trigger in einen anderen Dialekt transformiert werden soll, wird die Trigger-Function aufgelöst und der Body in den Trigger integriert (oder action_required bei komplexer Logik).


11. Function- und Procedure-DDL

Function- und Procedure-Bodys enthalten dialektspezifische prozedurale Logik (PL/pgSQL, MySQL-Procedural SQL, etc.), die nicht rein regelbasiert transformiert werden kann.

Strategie:

  • Wenn source_dialect = target_dialect: Body wird 1:1 übernommen
  • Wenn Dialekte unterschiedlich: KI-gestützte Transformation erforderlich (siehe design.md §4 und Beispiel Stored Procedure Migration)
  • Fallback ohne KI: action_required (E053) wird erzeugt mit Hinweis auf d-migrate transform procedure

Die Hülle (CREATE FUNCTION/PROCEDURE, Parameter, Return-Typ) wird regelbasiert generiert:

-- PostgreSQL
CREATE OR REPLACE FUNCTION "calculate_total"("p_order_id" INTEGER)
RETURNS DECIMAL(10,2) AS $$
<body>
$$ LANGUAGE plpgsql;

-- MySQL
DELIMITER //
CREATE FUNCTION `calculate_total`(p_order_id INT)
RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
    <body>
END //
DELIMITER ;

12. Rollback-DDL (Down-Migration)

Für jedes Up-Statement wird ein inverses Down-Statement erzeugt:

Up-Statement Down-Statement
CREATE TABLE "x" DROP TABLE IF EXISTS "x"
CREATE INDEX "i" ON "x" DROP INDEX IF EXISTS "i"
CREATE TYPE "t" DROP TYPE IF EXISTS "t"
CREATE VIEW "v" DROP VIEW IF EXISTS "v"
CREATE FUNCTION "f" DROP FUNCTION IF EXISTS "f"
CREATE SEQUENCE "s" DROP SEQUENCE IF EXISTS "s"
ALTER TABLE ADD COLUMN "c" ALTER TABLE DROP COLUMN "c"
ALTER TABLE ADD CONSTRAINT "k" ALTER TABLE DROP CONSTRAINT "k"
ALTER TABLE ALTER COLUMN "c" TYPE t ALTER TABLE ALTER COLUMN "c" TYPE <alter_typ>

Execution-Reports gruppieren gerenderte Statements unabhängig vom SQL-Text: Jede Gruppe trägt eine stabile statementGroupId, Operation-IDs, Statement-Indexrange, transactionScope und transactionBoundary. Gemischte Transaction-Scope-Streams werden vor Ausführung blockiert; Renderer dürfen keine still gemischten Runner-/Stream-/No-Transaction-Statements erzeugen.

Nicht-reversible Operationen erzeugen einen Kommentar:

-- WARNING: Irreversible operation. Original data cannot be recovered.
-- Original column type was: VARCHAR(100)
DROP COLUMN "legacy_field";

Diff-basierte Rollback-Artefakte aus schema migrate --generate-rollback werden als ausführbarer SQL-Body plus d-migrate rollback-sql v2- Metadatenblock geschrieben. Der Body bleibt das einzige ausführbare SQL. Der Header enthält statementIndex[] mit UTF-8-Byte-Ranges, Statement-Hashes, Operation-IDs, Phase, Risiko und transactionScope. Zusätzlich bindet der Header rollbackComplete, partialRollback und skippedOperationIds[]. Partielle Rollback-Artefakte müssen partialRollback=true, rollbackComplete=false und mindestens eine ausgelassene Operation maschinenlesbar ausweisen. schema rollback --execute rekonstruiert Statements aus diesen validierten Body-Ranges; ein Split anhand leerer Zeilen ist für v2 verboten. rollback-sql v1 wird nur als Legacy-Lesepfad unterstützt.


13. Formatierung

13.1 SQL-Formatierung

  • Schlüsselwörter: UPPERCASE (CREATE TABLE, NOT NULL, REFERENCES)
  • Identifikatoren: lowercase (gequotet)
  • Einrückung: 4 Spaces (keine Tabs)
  • Eine Spalte pro Zeile
  • Constraints am Ende der Tabelle, jeweils eigene Zeile
  • Leere Zeile zwischen Tabellen-Definitionen
  • Kommentare: -- Präfix

13.2 Beispiel

-- Generated by d-migrate 0.1.0
-- Source: neutral schema v1.0.0 "E-Commerce System"
-- Target: postgresql | Generated: 2026-04-05T10:30:00Z

CREATE TYPE "order_status" AS ENUM (
    'pending',
    'processing',
    'shipped',
    'delivered',
    'cancelled'
);

CREATE TABLE "customers" (
    "id"         SERIAL,
    "email"      VARCHAR(254) NOT NULL,
    "name"       VARCHAR(100) NOT NULL,
    "metadata"   JSONB,
    "created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY ("id"),
    UNIQUE ("email")
);

CREATE INDEX "idx_customers_email" ON "customers" ("email");

CREATE TABLE "orders" (
    "id"           SERIAL,
    "customer_id"  INTEGER NOT NULL,
    "order_date"   TIMESTAMP NOT NULL,
    "total_amount" DECIMAL(10,2),
    "status"       "order_status" DEFAULT 'pending',
    "notes"        TEXT,
    PRIMARY KEY ("id"),
    CONSTRAINT "fk_orders_customer_id" FOREIGN KEY ("customer_id")
        REFERENCES "customers" ("id") ON DELETE RESTRICT,
    CONSTRAINT "chk_total_positive" CHECK ("total_amount" >= 0)
);

CREATE INDEX "idx_orders_customer_date" ON "orders" ("customer_id", "order_date" DESC);

14. Transformationshinweise und Report

14.1 Inline-Hinweise im DDL

Hinweise werden als SQL-Kommentare direkt vor dem betroffenen Statement eingefügt:

-- [W102] HASH index not supported on InnoDB, using BTREE
CREATE INDEX `idx_orders_status` ON `orders` (`status`);

-- [E053] action_required: Function body requires KI-assisted transformation
-- Use: d-migrate transform procedure --procedure calculate_total --ai-backend ollama
-- Skipped: CREATE FUNCTION `calculate_total` (source_dialect: postgresql, target: mysql)

14.2 Transformations-Report (Sidecar-Datei)

Wenn --output verwendet wird, erzeugt der Generator neben der DDL-Datei einen Report:

--output schema.sql     → schema.sql + schema.report.yaml
--output out/ddl.sql    → out/ddl.sql + out/ddl.report.yaml

Report-Schema (YAML):

source:
  schema: "E-Commerce System"
  version: "1.0.0"
  file: "schema.yaml"
target:
  dialect: mysql
  generated_at: "2026-04-05T10:30:00Z"
  generator: "d-migrate 0.2.0"

summary:
  statements: 12
  notes: 4
  warnings: 2
  action_required: 1
  skipped_objects: 1

notes:
  - type: warning
    code: W102
    object: "idx_orders_status"
    message: "HASH index not supported on InnoDB, using BTREE"

  - type: action_required
    code: E053
    object: "calculate_total"
    message: "Function body requires KI-assisted transformation"
    hint: "d-migrate transform procedure --procedure calculate_total --ai-backend ollama"

skipped_objects:
  - type: function
    name: "calculate_total"
    reason: "source_dialect (postgresql) differs from target (mysql), KI transformation required"
    code: E053
    hint: "d-migrate transform procedure --procedure calculate_total --ai-backend ollama"

Der Report ist bewusst statement- und diagnoseorientiert. Er zaehlt die erzeugten DDL-Statements sowie Notes / Warnings / action_required / skipped_objects, aber keine objektartbezogenen Summen wie Tabellen, Indizes, Views oder Funktionen.

14.3 Verhalten bei action_required

Situation DDL-Output Report Exit-Code stderr
Nur info/warning Vollständig generiert Alle Notes 0 Warnungen ausgeben
action_required vorhanden Generiert ohne übersprungene Objekte Alle Notes + skipped_objects 0 Warnungen + Hinweise auf übersprungene Objekte
Fehler (z.B. ungültiges Schema) Nicht generiert Nicht erzeugt 3 Fehler ausgeben

Wichtig: action_required ist kein Fehler — die DDL-Generierung wird fortgesetzt, das betroffene Objekt (Function, Trigger-Body, etc.) wird übersprungen und im Report dokumentiert. Der Exit-Code bleibt 0, damit CI/CD-Pipelines nicht abbrechen. Die skipped_objects-Liste im Report macht die Lücken transparent.

stderr-Ausgabe bei action_required:

⚠ Warning [E053]: Function 'calculate_total' skipped — requires KI-assisted transformation
  → Hint: d-migrate transform procedure --procedure calculate_total --ai-backend ollama

14.4 CLI-Ausgabe bei schema generate

stdout: DDL-Output (oder in --output-Datei) stderr: Warnungen und action_required-Hinweise

Bei --output-format json wird die DDL als ddl-Feld im JSON eingebettet:

{
  "command": "schema.generate",
  "status": "completed",
  "exit_code": 0,
  "target": "mysql",
  "ddl": "CREATE TABLE `customers` (...);\\n...",
  "notes": [...],
  "skipped_objects": [...],
  "warnings": 2,
  "action_required": 1
}

15. Golden-Master-Teststrategie (0.2.0)

15.1 Fixture-Layout

adapters/driven/formats/src/test/resources/fixtures/
├── schemas/                          # Eingabe-Schemas (bestehend aus 0.1.0)
│   ├── minimal.yaml
│   ├── e-commerce.yaml
│   ├── all-types.yaml
│   └── full-featured.yaml
│
└── ddl/                              # Erwartete DDL-Ausgaben (Golden Masters)
    ├── minimal.postgresql.sql
    ├── minimal.mysql.sql
    ├── minimal.sqlite.sql
    ├── e-commerce.postgresql.sql
    ├── e-commerce.mysql.sql
    ├── e-commerce.sqlite.sql
    ├── all-types.postgresql.sql
    ├── all-types.mysql.sql
    └── all-types.sqlite.sql

Namenskonvention: <schema>.<dialekt>.sql

15.2 Coverage-Matrix

Schema Feature-Schwerpunkt PG MY SQ
minimal Basis (1 Tabelle, PK, 2 Spalten)
e-commerce FK, Enum (ref_type), Indizes, CHECK, Defaults
all-types Alle 18 neutralen Typen
full-featured Custom Types, Partitioning, Procedures, Functions, Views, Triggers, Sequences

Gesamt: 4 Schemas × 3 Dialekte = 12 Golden-Master-Dateien

15.3 Test-Methodik

class DdlGoldenMasterTest : FunSpec({
    val schemas = listOf("minimal", "e-commerce", "all-types", "full-featured")
    val dialects = listOf("postgresql", "mysql", "sqlite")

    for (schema in schemas) {
        for (dialect in dialects) {
            test("$schema generates correct $dialect DDL") {
                val input = loadSchema("schemas/$schema.yaml")
                val expected = loadGoldenMaster("ddl/$schema.$dialect.sql")
                val actual = generateDdl(input, dialect)
                actual shouldBe expected
            }
        }
    }
})

15.4 Nicht-deterministische Elemente

Der DDL-Header enthält im normalen Modus einen Timestamp (Generated: <ISO-8601>). Für Golden-Master-Tests:

  • Option A (empfohlen): schema generate --deterministic bzw. DdlGenerationOptions(deterministic = true) verwenden.
  • Option B: SOURCE_DATE_EPOCH bzw. DdlGenerationOptions(generatedAt = ...) auf einen festen Wert setzen.
  • Option C: Header-Zeilen bei Vergleich ignorieren, insbesondere die -- Target: ... | Generated: ...-Zeile.

15.5 Golden-Master-Aktualisierung

Bei gewollten DDL-Änderungen:

# Golden Masters neu generieren
./gradlew :adapters:driven:driver-common:updateGoldenMasters

# Diff prüfen
git diff adapters/driven/formats/src/test/resources/fixtures/ddl/

# Committen wenn korrekt
git add adapters/driven/formats/src/test/resources/fixtures/ddl/

16. Spatial-Spalten-Generierung (0.5.5)

Dieser Abschnitt beschreibt verbindlich, wie schema generate Spalten mit type: geometry in datenbankspezifisches DDL ueberfuehrt. Er gilt fuer Milestone 0.5.5 (Spatial Phase 1).

Nicht Teil dieses Abschnitts: type: geography, z, m, Spatial-Indizes und automatische Erkennung oder Installation von Datenbankerweiterungen.

16.1 Spatial-Profil

Da schema generate ohne Live-Datenbankverbindung laeuft, kann die Verfuegbarkeit von PostGIS oder SpatiaLite nicht automatisch geprueft werden. Stattdessen steuert ein explizites Spatial-Profil die DDL-Generierung fuer geometry-Spalten. Das Profil ist Teil der Generator-Konfiguration — es gehoert nicht zum neutralen Schema-Modell und wird nicht in YAML-Feldern abgelegt.

Zulässige Werte und Defaults je Zieldialekt:

Zieldialekt Zulässige Profile Default
postgresql postgis, none postgis
mysql native, none native
sqlite spatialite, none none

Bedeutung der Profilwerte:

  • postgis: PostgreSQL-DDL wird mit PostGIS-kompatiblen Geometrietypen erzeugt.
  • native: MySQL-DDL wird mit den nativen Spatial Data Types von MySQL erzeugt.
  • spatialite: SQLite-DDL verwendet die AddGeometryColumn()-Strategie von SpatiaLite.
  • none: geometry-Spalten werden nicht generiert; die gesamte Tabelle wird als action_required (E052) markiert. Das ist fuer PostgreSQL, MySQL und SQLite ein zulaessiger Generatorpfad, wenn Spatial-DDL bewusst unterdrueckt werden soll.

Unzulaessige Dialekt/Profil-Kombinationen (z.B. --target mysql --spatial-profile postgis) erzeugen einen Nutzungsfehler (Exit-Code 2) noch vor der DDL-Generierung.

16.2 PostgreSQL / PostGIS (Profil: postgis)

geometry-Spalten werden als PostGIS-geometry-Typ mit explizitem Subtyp und SRID generiert.

Typ-Syntax:

geometry(<GeometryType>, <srid>)

geometry_type wird in PostGIS-Schreibweise mit grossem Anfangsbuchstaben emittiert (z.B. Point, Polygon, MultiPolygon). Wenn geometry_type nicht angegeben ist, gilt der Default geometry (lowercase im neutralen Modell, grossgeschrieben im PostGIS-Ausdruck: Geometry).

Wenn srid nicht angegeben ist, wird 0 als Platzhalter verwendet.

Beispiel:

CREATE TABLE "places" (
    "id"       SERIAL,
    "location" geometry(Point, 4326),
    PRIMARY KEY ("id")
);

Regeln:

  • Ohne explizite Extension-Install-Policy emittiert der Migrate-Pfad kein automatisches CREATE EXTENSION IF NOT EXISTS postgis;. Stattdessen blockieren extension-abhaengige Operationen, wenn die Zielverfuegbarkeit nicht VERIFIED_PRESENT ist.
  • Mit schema migrate --allow-extension-install darf PostgreSQL vor der ersten PostGIS-abhaengigen Operation CREATE EXTENSION IF NOT EXISTS "postgis"; rendern, auch wenn die Availability MISSING oder UNKNOWN ist. Das Statement erscheint im Report unter extensionInstallStatements und wird als Side-Effect mit manueller Bestaetigung markiert.
  • Rollback-Statement: ALTER TABLE "<table>" DROP COLUMN "<column>";

16.3 PostgreSQL / PostGIS (Profil: none)

Wenn das Spatial-Profil fuer PostgreSQL auf none gesetzt ist, wird die gesamte Tabelle blockiert. Es wird keine partielle DDL ohne die betroffene Spatial-Spalte erzeugt.

Stattdessen wird die Tabelle als action_required mit Code E052 markiert und in skipped_objects des Reports aufgefuehrt. Der Rest der DDL-Generierung wird fortgesetzt (andere Tabellen werden normal erzeugt).

Report-Eintrag (Beispiel):

notes:
  - type: action_required
    code: E052
    object: places
    message: "Table blocked: spatial column 'location' cannot be generated with
              PostgreSQL spatial profile 'none'"
    hint: "Use --spatial-profile postgis or map the column manually"

skipped_objects:
  - type: table
    name: places
    reason: "Spatial profile 'none': geometry column 'location' requires manual handling"

16.4 MySQL (Profil: native)

MySQL unterstuetzt native Spatial Data Types. geometry_type wird direkt auf den passenden nativen MySQL-Typ abgebildet.

Typ-Mapping:

geometry_type im neutralen Modell MySQL-DDL-Typ
geometry GEOMETRY
point POINT
linestring LINESTRING
polygon POLYGON
multipoint MULTIPOINT
multilinestring MULTILINESTRING
multipolygon MULTIPOLYGON
geometrycollection GEOMETRYCOLLECTION

MySQL hat kein Profil nonenative ist der einzige zulaessige Wert.

SRID-Handling: MySQL unterstuetzt SRID-Constraints ab Version 8.0. Falls die SRID-Information nicht im gewuenschten DDL-Stil ausgedrueckt werden kann, wird der Typ ohne explizite SRID-Angabe emittiert und der Transformationsreport enthaelt eine Warnung W120.

Beispiel ohne W120 (SRID korrekt uebernommen):

CREATE TABLE `places` (
    `id`       INT NOT NULL AUTO_INCREMENT,
    `location` POINT SRID 4326,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Rollback-Statement: ALTER TABLE `<table>` DROP COLUMN `<column>`;

16.5 SQLite / SpatiaLite (Profil: spatialite)

SpatiaLite registriert Geometriespalten ueber eine Metadaten-Funktion statt direkt in CREATE TABLE. Deshalb wird eine zweistufige Strategie verwendet:

  1. CREATE TABLE ohne die geometry-Spalte erzeugen.
  2. Fuer jede geometry-Spalte ein SELECT AddGeometryColumn(...) emittieren.

Beispiel:

CREATE TABLE "places" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT
);

SELECT AddGeometryColumn('places', 'location', 4326, 'POINT', 'XY');

AddGeometryColumn-Signatur: (table_name, column_name, srid, type, coord_dimension)

  • srid: Der Wert aus dem neutralen Modell; fehlt srid, wird 0 verwendet.
  • type: geometry_type in Grossbuchstaben (z.B. POINT, POLYGON).
  • coord_dimension: Immer 'XY' in Phase 1 (Z/M sind nicht Teil von 0.5.5).

Rollback: Das Rollback-Statement fuer AddGeometryColumn ist SELECT DiscardGeometryColumn('<table>', '<column>');. Fuer die zugehoerige Tabelle gilt das normale DROP TABLE IF EXISTS.

16.6 SQLite / SpatiaLite (Profil: none)

Wenn das Spatial-Profil fuer SQLite auf none gesetzt ist (das ist der Default), wird die gesamte Tabelle blockiert. Es wird kein stiller Fallback auf TEXT oder BLOB erzeugt.

Die Tabelle wird als action_required mit Code E052 markiert, analog zu §16.3 (PostgreSQL mit Profil none). Partielle DDL ohne die Spatial-Spalte ist kein zulaessiger 0.5.5-Pfad.

16.7 Rollback-Regeln fuer Spatial-Statements

Generiertes Statement Rollback-Statement
CREATE TABLE "t" (...) ohne geometry DROP TABLE IF EXISTS "t"
SELECT AddGeometryColumn('t', 'c', ...) SELECT DiscardGeometryColumn('t', 'c')
geometry(Point, 4326) als Spalte in CREATE TABLE Teil des normalen DROP TABLE IF EXISTS "t"

Kanonische Rollback-Semantik fuer PostgreSQL und MySQL (0.5.5): Da Geometry-Spalten in 0.5.5 ausschliesslich ueber CREATE TABLE (nicht ueber ALTER TABLE ADD COLUMN) erzeugt werden, ist der Rollback-Pfad fuer PostgreSQL und MySQL in 0.5.5 immer DROP TABLE IF EXISTS. Spaltenbezogene Rollback-Pfade (ALTER TABLE ... DROP COLUMN) sind erst bei spaeterer Migrationsunterstuetzung relevant und nicht Teil von 0.5.5.

Wenn Rollback generiert wird (--generate-rollback) und das Profil spatialite ist, werden die DiscardGeometryColumn-Aufrufe in umgekehrter Reihenfolge vor dem DROP TABLE emittiert.

16.8 Fehler- und Warnungs-Codes fuer Spatial

Diese Codes ergaenzen die allgemeinen Codes aus §4. Die Codes E020, E120 und E121 entstehen bei schema validate (Schema-/Modellregeln); E052 bis E057 sowie W113 und W120 entstehen bei schema generate (Generator-/Report-Regeln).

Code Typ Ebene Meldung
E120 Validierungsfehler schema validate Unknown geometry_type value
E121 Validierungsfehler schema validate srid must be greater than 0
E020 Validierungsfehler schema validate Declared view dependency references non-existent view
E052 action_required schema generate Spatial object cannot be generated with the chosen spatial profile
E053 action_required schema generate Dialect-specific SQL content requires manual transformation or implementation
E054 action_required schema generate Object type is not supported in the target dialect
E055 action_required schema generate Partitioning is not supported in the target dialect
E056 action_required schema generate Named sequence cannot be generated natively and needs emulation/manual handling
E057 action_required schema generate Feature combination not generable: MySQL — partial index predicate cannot be generated; SQLite (helper_table) — WITHOUT ROWID table with SequenceNextVal column (AFTER INSERT trigger requires ROWID)
E058 action_required schema rollback SQLite (helper_table): external objects reference dmg_sequences; rollback aborts before any DROP to avoid dangling references
E060 Diagnostik schema generate --split / schema rollback Phasenkonflikt im Split-Modus; SQLite zusätzlich: ATTACHed Datenbanken detektiert beim Rollback-Preflight
W113 Warnung schema generate View dependencies could not be fully topologically sorted; original order is used for the remaining views
W114 Warnung schema generate Sequence cache value stored but not emulated as preallocation (MySQL + SQLite helper_table; SQLite ist Single-Writer und profitiert nicht von Caching)
W115 Warnung schema generate SequenceNextVal uses lossy trigger semantics (MySQL + SQLite helper_table); explicit NULL is treated like omitted value
W116 Warnung schema reverse Sequence metadata reconstructed, but required support objects (routines/triggers) are missing or degraded (MySQL + SQLite)
W117 Warnung schema generate Sequence values are transaction-bound in helper-table mode; rollback retracts increments (MySQL + SQLite)
W119 Warnung schema generate SQLite (helper_table): NOT NULL und CHECK-IS NOT NULL auf sequence-getragener Spalte werden unterdrückt, weil der _bi-Trigger NULL injizieren muss; Wert wird vom _ai-Trigger garantiert
W120 Warnung schema generate / schema reverse MySQL: SRID could not be fully transferred. SQLite (helper_table-Reverse): Marker stimmt, aber Trigger-Body wurde modifiziert; Sequence-Zuordnung bleibt, aber Emulation evtl. nicht funktional
W121 Info schema generate SQLite (helper_table): Conflict-Gap-INFO — ON CONFLICT DO UPDATE/DO NOTHING, INSERT OR IGNORE, INSERT OR FAIL (multi-row) verbrauchen einen Sequence-Wert ohne Insert
W122 Warnung schema generate SQLite (helper_table): AFTER INSERT-Sequence-Trigger führt UPDATE auf der Zieltabelle aus; bei PRAGMA recursive_triggers = ON feuern bestehende UPDATE-Trigger auf derselben Tabelle
W123 Warnung schema rollback SQLite (helper_table): ATTACHed Datenbanken detektiert; Rollback kann Abhängigkeiten über Schemen nicht prüfen — --force-rollback erforderlich
W124 Warnung schema reverse SQLite (helper_table): nutzerdefinierter BEFORE INSERT-Trigger auf der Zieltabelle ist vor dem kanonischen _bi-Trigger erzeugt worden; Sequence-Vergabe kann maskiert werden

E120: Wird erzeugt, wenn geometry_type einen Wert enthaelt, der nicht in der zulaessigen Wertemenge liegt: geometry, point, linestring, polygon, multipoint, multilinestring, multipolygon, geometrycollection.

E121: Wird erzeugt, wenn srid angegeben ist und den Wert 0 oder einen negativen Wert hat. Eine fehlende srid ist zulaessig und erzeugt keinen Fehler.

E020: Wird erzeugt, wenn dependencies.views auf eine View verweist, die im neutralen Schema nicht existiert.

E052 (Spatial): Wird erzeugt, wenn ein Spatial-Objekt mit dem gewählten Spatial-Profil nicht generiert werden kann. Die gesamte betroffene Tabelle wird blockiert — partielle DDL ohne die Spatial-Spalte wird nicht erzeugt.

E053 (Dialekt-Transformation): Wird erzeugt, wenn View-Query, Function-/Procedure-Body oder Trigger-Body nicht automatisch zwischen Dialekten transformiert werden kann oder ein Body manuell ergänzt werden muss.

E054 (Nicht unterstützter Objekttyp): Wird erzeugt, wenn der Zieldialekt den Objekttyp nicht unterstützt, z. B. Composite Types, EXCLUDE-Constraints, SQLite-Functions oder SQLite-Procedures.

E055 (Partitionierung): Wird erzeugt, wenn die konfigurierte Partitionierung im Zieldialekt nicht unterstützt wird.

E056 (Sequence-/Emulationsfall): Wird erzeugt, wenn eine benannte Sequence im Zieldialekt nicht nativ erzeugt werden kann und manuelle Emulation oder Nacharbeit erforderlich ist.

E057 (Partial Index): Wird erzeugt, wenn ein Index ein where-Praedikat traegt, der Zieldialekt aber keine native Partial-Index-Semantik unterstuetzt. Der Index wird uebersprungen; insbesondere Partial-UNIQUE darf nicht als normaler Unique-Index ausgegeben werden.

W113: Wird erzeugt, wenn deklarierte oder best-effort abgeleitete View-Abhaengigkeiten keine vollstaendige topologische Sortierung erlauben. Die verbleibenden Views werden dann in ihrer Originalreihenfolge emittiert.

W120: Wird erzeugt, wenn srid-Metadaten nicht vollstaendig in die Ziel-DDL uebertragen werden konnten (Best-Effort-Pfad, insbesondere bei MySQL fuer aeltere Server-Versionen). Die DDL wird trotzdem erzeugt; der Hinweis erscheint im Report und auf stderr.


17. Phasenbezogene DDL-Ordnung (0.9.2)

Ab 0.9.2 traegt jedes DDL-Statement eine DdlPhase (PRE_DATA oder POST_DATA). Im Default-Modus (--split single) hat das keine Auswirkung — alle Statements werden wie bisher in einer Datei ausgegeben. Mit --split pre-post werden die Statements nach Phase getrennt.

17.1 Zuordnung zu Phasen

Phase Objekte Begruendung
PRE_DATA Custom Types, Sequences, Tabellen (topologisch sortiert), Indizes, Constraints, einfache Views Strukturelle DDL, die vor einem Datenimport stehen muss
POST_DATA Functions, Procedures, Triggers, Views mit Routinen-Abhaengigkeiten Ausfuehrbare Objekte, die bei aktivem Datenimport stoeren koennen

17.2 Reihenfolge innerhalb der Phasen

PRE_DATA (analog zur bisherigen Gesamtreihenfolge aus §1.1):

1. Custom Types        (CREATE TYPE)
2. Sequences           (CREATE SEQUENCE)
3. Tabellen            (CREATE TABLE — topologisch sortiert nach FK)
4. Indizes             (CREATE INDEX)
5. Circular-FK-Nachzuegler (ALTER TABLE ADD CONSTRAINT)
6. Views ohne Routinen-Abhaengigkeit (CREATE VIEW)

POST_DATA:

1. Views mit Routinen-Abhaengigkeit (CREATE VIEW)
2. Functions           (CREATE FUNCTION)
3. Procedures          (CREATE PROCEDURE)
4. Triggers            (CREATE TRIGGER)

17.3 View-Phasenzuordnung

Views werden ueber den ViewPhaseClassifier einer Phase zugewiesen:

  1. Deklarierte Abhaengigkeiten: Hat eine View dependencies.functions, wird sie POST_DATA zugeordnet.
  2. Inferierte Funktionsaufrufe: Der Query-Text wird auf Funktionsnamen geparst, die im Schema als Functions definiert sind. Treffer → POST_DATA.
  3. Transitive Propagation: Views, die von einer POST_DATA-View abhaengen (ueber dependencies.views), werden ebenfalls POST_DATA.
  4. Fallback: Views ohne Query-Text und ohne deklarierte dependencies.functions erzeugen bei vorhandenen Functions im Schema den Fehlercode E060.

17.4 Ausgabeartefakte

Modus Textausgabe (--output) JSON-Ausgabe (--output-format json)
single <name>.sql (alle Statements) "ddl": "..."
pre-post <name>.pre-data.sql, <name>.post-data.sql "ddl_parts": { "pre_data": "...", "post_data": "..." }

Bei --split pre-post wird die Originaldatei (<name>.sql) nicht geschrieben. Der Sidecar-Report bleibt ein einzelnes Artefakt mit split_mode: pre-post.

Notes und skipped_objects tragen im Split-Modus optional ein phase-Feld ("pre-data" oder "post-data", Kebab-Case).

17.5 Einschraenkungen

  • --split pre-post erfordert --output oder --output-format json (Exit 2)
  • --split pre-post kann nicht mit --generate-rollback kombiniert werden (Exit 2)
  • E060 bei nicht zuordenbaren Views fuehrt zu Exit 2

Verwandte Dokumentation


Version: 1.2 Stand: 2026-04-20 Status: Spezifikation fuer 0.9.2 (§17 Phasenbezogene DDL-Ordnung hinzugefuegt)