CLI-Tool für datenbankunabhängige Migrationen und Datenverwaltung
Dokumenttyp: Architektur-Spezifikation
Die Module unter
hexagon/undadapters/bilden die hexagonale Verzeichnisstruktur ab (siehe §1.2 / §2.1). Weitere Module wieai,testdataunddocsbeschreiben geplante Erweiterungen. I18n bleibt ohne separates Top-Level-i18n-Modul in die bestehenden Modulgrenzen integriert.
┌─────────────────────────────────────────────┐
│ d-migrate │
Benutzer ───CLI──▶│ │
│ ┌─────────┐ ┌──────────┐ ┌─────────────┐ │
CI/CD ──Script───▶│ │ Schema │ │ Daten │ │ Integration │ │
│ │ Mgmt │ │ Mgmt │ │ Adapter │ │
│ └────┬────┘ └────┬─────┘ └──────┬──────┘ │
│ │ │ │ │
│ ┌────▼───────────▼──────────────▼───────┐ │
│ │ Core Engine │ │
│ │ (Neutrales Modell, Validierung, │ │
│ │ Type-Mapping, Streaming-Pipeline) │ │
│ └────┬───────────┬──────────────┬───────┘ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌─────▼──────┐ │
│ │ DB │ │ Format │ │ KI │ │
│ │ Driver │ │ Codec │ │ Provider │ │
│ │ (JDBC) │ │ │ │ │ │
│ └────┬────┘ └────┬────┘ └─────┬──────┘ │
└───────┼───────────┼─────────────┼───────────┘
│ │ │
┌───────▼──┐ ┌─────▼────┐ ┌─────▼──────┐
│PostgreSQL│ │JSON/YAML │ │Ollama │
│MySQL │ │CSV/SQL │ │LM Studio │
│SQLite │ │ │ │OpenAI │
│(Oracle) │ │ │ │Anthropic │
│(MSSQL) │ │ │ │xAI/Gemini │
│ │ │ │ │vLLM/TGI │
└──────────┘ └──────────┘ └────────────┘
Hexagonale Architektur (Ports & Adapters) mit klarer Trennung zwischen:
- Domain Core: Neutrales Schema-Modell (spezifiziert in neutral-model-spec.md), Validierungslogik, Diff- und Planungslogik - keine externen Abhängigkeiten
- Ports: Interfaces für Datenbank-Zugriff, Datei-I/O, KI-Provider und datenbankspezifisches Type-Mapping
- Adapters: Konkrete Implementierungen (JDBC-Driver, Jackson-Serializer, HTTP-Clients, Dialekt-spezifische TypeMapper)
Zuordnung der Gradle-Module zu den Architekturschichten:
d-migrate/
├── hexagon/ ← Innerhalb des Hexagons
│ ├── core/ ← Domain (Modell, Validierung, Typsystem)
│ ├── ports/ ← Port-Interfaces (Output) + Datentypen
│ └── application/ ← Use Cases (Runner-Klassen)
│
├── adapters/ ← Außerhalb des Hexagons
│ ├── driving/
│ │ └── cli/ ← Clikt-Shells, Wiring, OutputFormatter
│ └── driven/
│ ├── driver-common/ ← Abstract-Base-Klassen, HikariCP
│ ├── driver-postgresql/
│ ├── driver-mysql/
│ ├── driver-sqlite/
│ ├── formats/ ← JSON/YAML/CSV Codec-Implementierungen
│ └── streaming/ ← Pipeline-Infrastruktur
| Schicht | Modul | Rolle |
|---|---|---|
| Domain Core | hexagon:core |
Neutrales Modell, Validierung, Typsystem, FK-Topo-Sort — keine externen Deps |
| Ports | hexagon:ports-common |
Gemeinsame Typen (DatabaseDialect, SqlIdentifiers, DialectCapabilities, ConnectionPool, SchemaCodec, DataExportFormat) |
| Ports | hexagon:ports-read |
Lese-Ports (SchemaReader, DdlGenerator, DataReader, DataChunkReader, FormatReadOptions, ManualActionRequired) |
| Ports | hexagon:ports-write |
Schreib-Ports (DataWriter, TableImportSession, ImportOptions, ExportOptions, DataChunkWriter, Checkpoint, Streaming-Typen) |
| Ports | hexagon:ports |
Aggregator-Modul — re-exportiert ports-common, ports-read, ports-write für bestehende Consumer |
| Application | hexagon:application |
Use-Case-Runner (SchemaGenerateRunner, DataExportRunner, ExportResumeCoordinator, ImportResumeCoordinator) |
| Profiling | hexagon:profiling |
Profiling-Domänenmodell, Rule-Engine, Services — keine Treiber-Abhängigkeit |
| Driving Adapter | adapters:driving:cli |
CLI-Einstiegspunkt (Clikt), Wiring aller Module |
| Driven Adapter | adapters:driven:driver-common |
Gemeinsame DB-Infrastruktur (AbstractDdlGenerator, HikariConnectionPoolFactory, SqlIdentifiers, …) |
| Driven Adapter | adapters:driven:driver-postgresql |
PostgreSQL-Implementierung der DatabaseDriver-Fassade |
| Driven Adapter | adapters:driven:driver-mysql |
MySQL-Implementierung der DatabaseDriver-Fassade |
| Driven Adapter | adapters:driven:driver-sqlite |
SQLite-Implementierung der DatabaseDriver-Fassade |
| Driven Adapter | adapters:driven:driver-*-profiling |
Optionale Profiling-Adapter pro Dialekt (Introspection, LogicalTypeResolver, ProfilingData) |
| Driven Adapter | adapters:driven:formats |
Serialisierung/Deserialisierung (JSON, YAML, CSV) |
| Driven Adapter | adapters:driven:integrations |
Tool-Exporter (Flyway, Liquibase, Django, Knex) |
| Driven Adapter | adapters:driven:streaming |
Streaming-Pipeline (StreamingExporter, StreamingImporter, TableExporter, TableImporter) |
| Driven Adapter | adapters:driven:text-icu |
ICU4J-basierte UnicodeTextService-Implementierung (IcuUnicodeTextService); haelt ICU4J aus dem Application-Layer fern |
adapters:driving:cli (Clikt)
│
▼
┌────────────────────────────────┐
│ Hexagon │
│ │
│ ┌──────────────────────────┐ │
│ │ hexagon:application │ │ ← Use Cases (Runner)
│ └────────────┬─────────────┘ │
│ │ │
│ ┌────────────▼─────────────┐ │
│ │ ports-read / ports-write│ │ ← Port-Interfaces
│ └────────────┬─────────────┘ │
│ ports-common │ ← Gemeinsame Typen
│ │ │
│ ┌────────────▼─────────────┐ │
│ │ hexagon:core │ │ ← Domain-Modell
│ └──────────────────────────┘ │
│ │
│ hexagon:profiling (optional) │ ← Profiling-Domäne
└────────────────┬───────────────┘
│
┌─────────────┼──────────────┐
▼ ▼ ▼
driver-common formats streaming
driver-pg/my/sl
driver-*-profiling (optional)
Erzwungene Regeln (durch Gradle-Abhängigkeiten garantiert):
hexagon:corehat keine Abhängigkeiten auf andere Modulehexagon:ports-commonhängt nur vonhexagon:coreabhexagon:ports-readhängt nur vonports-commonabhexagon:ports-writehängt vonports-commonundports-readabhexagon:portsist ein Aggregator und re-exportiert alle drei Port-Modulehexagon:applicationhängt nur vom Hexagon-Inneren ab, nicht von Adaptern- Driven Adapters dürfen in main nicht voneinander abhängen (Ausnahme:
Adapter dürfen die geteilte JDBC-Basis
adapters:driven:driver-commonnutzen) - Treiber-Kernmodule hängen nicht von
hexagon:profilingab; Profiling-Adapter sind optionale Zusatzmodule
Zusätzlich, durch eine Architektur-Fitness-Function (Gate) statt durch Gradle garantiert — denn
java.sql ist JDK-intern und ließe sich nicht über Modul-Abhängigkeiten ausschließen:
- Die Ports-Schicht (
hexagon:ports-*) exponiert in ihren Signaturen keinjava.sql; JDBC (Treiber-Technologie) lebt ausschließlich in den Adaptern. Ein neutralesDatabaseConnection-Handle inhexagon:ports-commonträgt die von den Ports benötigten Fähigkeiten (Pool-Rückgabe viaAutoCloseable, Transaktions-Lebenszyklus, Ausführung bereits gerenderter Statements, Session-Reset); die JDBC-gebundene Implementierung (JdbcDatabaseConnection, Wrapper um die Hikari-Connection) liegt inadapters:driven:driver-common, wo die Adapter sie zur realen Connection auspacken. - Driving Adapter (
adapters:driving:*) sind Composition Roots: Sie verdrahten konkrete Adapter und Use Cases, halten aber keine produktive JDBC-Ausführung. Produktive Driving-Quellen enthalten keinejava.sql-/javax.sql-Imports, keine produktivenjava.sql.-/javax.sql.-FQNs, keinasJdbcund keine direkteJdbcDatabaseConnection-Nutzung.
- Database-Agnostic First: Alle internen Datenstrukturen sind datenbankunabhängig; datenbankspezifisches Verhalten lebt ausschließlich in austauschbaren Adaptern.
- Convention over Configuration: Sinnvolle Defaults überall, explizite Konfiguration nur wo nötig.
- Streaming by Default: Datenverarbeitung ist grundsätzlich streaming-basiert (beliebig große Datenmengen).
- Fail-Safe: Standardmäßig transaktionale Verarbeitung ohne stillschweigende Teilmigrationen; Best-Effort nur explizit konfigurierbar.
- Privacy by Design: Lokale Verarbeitung als Standard, externe APIs nur opt-in.
Die Module hexagon:core, hexagon:ports-common, hexagon:ports-read,
hexagon:ports-write, hexagon:ports (Aggregator), hexagon:application,
hexagon:profiling sowie die Adapter-Module unter adapters/ sind
implementiert. hexagon:ports ist in drei Teilmodule zerlegt; der Aggregator
re-exportiert alle drei für bestehende Consumer. Die Profiling-Adapter liegen
in optionalen driver-*-profiling-Modulen.
Weitere Module (ai, testdata) beschreiben den geplanten Soll-Zustand
für geplante Erweiterungen.
d-migrate/
├── build.gradle.kts
├── settings.gradle.kts
│
├── hexagon/
│ ├── core/ # Domain Core (keine externen Deps)
│ │ └── dev/dmigrate/core/
│ │ ├── model/ # SchemaDefinition, NeutralType, …
│ │ ├── validation/ # SchemaValidator, ValidationResult
│ │ └── data/ # DataChunk, DataFilter, ColumnDescriptor
│ │
│ ├── ports-common/ # Gemeinsame Port-Typen
│ │ └── dev/dmigrate/
│ │ ├── driver/
│ │ │ ├── DatabaseDialect.kt
│ │ │ ├── DialectCapabilities.kt # Capability-Modell pro Dialekt
│ │ │ ├── SqlIdentifiers.kt # Zentrales Identifier-Quoting
│ │ │ ├── TypeMapper.kt
│ │ │ ├── connection/ # ConnectionPool, ConnectionConfig, JdbcUrlBuilder, PoolSettings
│ │ │ └── data/ # ResumeMarker
│ │ └── format/
│ │ ├── SchemaCodec.kt
│ │ └── data/ # DataExportFormat
│ │
│ ├── ports-read/ # Lese-Ports (read-only Consumer)
│ │ └── dev/dmigrate/
│ │ ├── driver/
│ │ │ ├── DdlGenerator.kt # + DdlResult, DdlPhase, TransformationNote, …
│ │ │ ├── ManualActionRequired.kt
│ │ │ ├── SchemaReader.kt
│ │ │ └── data/ # DataReader, TableLister, ChunkSequence
│ │ └── format/data/ # DataChunkReader, FormatReadOptions
│ │
│ ├── ports-write/ # Schreib-Ports
│ │ └── dev/dmigrate/
│ │ ├── driver/data/ # DataWriter, TableImportSession, ImportOptions
│ │ ├── format/data/ # DataChunkWriter, ExportOptions
│ │ ├── migration/ # MigrationBundle, ToolMigrationExporter
│ │ └── streaming/ # ExportOutput, ExportResult, PipelineConfig, Checkpoint
│ │
│ ├── ports/ # Aggregator — re-exportiert ports-common/-read/-write
│ │
│ └── application/ # Use Cases (Runner-Klassen)
│ └── dev/dmigrate/cli/commands/
│ ├── SchemaGenerateRunner.kt
│ ├── DataExportRunner.kt
│ └── DataExportHelpers.kt
│
├── adapters/
│ ├── driving/
│ │ └── cli/ # CLI-Einstiegspunkt (Clikt)
│ │ └── dev/dmigrate/cli/
│ │ ├── Main.kt # Bootstrap + Wiring
│ │ ├── commands/ # Clikt-Commands (SchemaCommands, DataCommands, SchemaGenerateHelpers)
│ │ ├── config/ # NamedConnectionResolver
│ │ └── output/ # OutputFormatter
│ │
│ └── driven/
│ ├── driver-common/ # Gemeinsame DB-Infrastruktur
│ │ └── dev/dmigrate/driver/
│ │ ├── AbstractDdlGenerator.kt
│ │ ├── ViewQueryTransformer.kt
│ │ ├── connection/ # HikariConnectionPoolFactory, ConnectionUrlParser, LogScrubber
│ │ └── data/ # AbstractJdbcDataReader
│ │
│ ├── driver-postgresql/ # PostgreSQL DatabaseDriver
│ │ └── dev/dmigrate/driver/postgresql/
│ │ ├── PostgresDriver.kt # class : DatabaseDriver
│ │ ├── PostgresDdlGenerator.kt
│ │ ├── PostgresTypeMapper.kt
│ │ ├── PostgresDataReader.kt
│ │ ├── PostgresTableLister.kt
│ │ └── PostgresJdbcUrlBuilder.kt
│ │
│ ├── driver-mysql/ # MySQL DatabaseDriver (analog)
│ ├── driver-sqlite/ # SQLite DatabaseDriver (analog)
│ │
│ ├── formats/ # Serialisierung / Deserialisierung
│ │ └── dev/dmigrate/format/
│ │ ├── yaml/YamlSchemaCodec.kt
│ │ ├── report/TransformationReportWriter.kt
│ │ └── data/ # Json/Yaml/CsvChunkWriter+Reader, DefaultFactories, ValueSerializer/Deserializer, EncodingDetector
│ │
│ └── streaming/ # Streaming-Pipeline
│ └── dev/dmigrate/streaming/
│ └── StreamingExporter.kt
Zielbild-Erweiterung:
integrations/(Flyway, Liquibase, Django, Knex). Geplante, noch nicht implementierte Module:ai/(Ollama, LM Studio, OpenAI, Anthropic, …),testdata/(Faker, KI-gestuetzt) — siehe Roadmap.
adapters:driving:cli
├── hexagon:application ──▶ hexagon:core, hexagon:ports
├── hexagon:ports (Aggregator) ──▶ ports-common, ports-read, ports-write
│ ├── hexagon:ports-common ──▶ hexagon:core
│ ├── hexagon:ports-read ──▶ ports-common
│ └── hexagon:ports-write ──▶ ports-common, ports-read
├── hexagon:profiling ──▶ hexagon:core (optional, kein Treiber-Dep)
├── adapters:driven:driver-common ──▶ hexagon:ports, HikariCP, SLF4J
├── adapters:driven:driver-postgresql ──▶ hexagon:ports, driver-common
├── adapters:driven:driver-mysql ──▶ hexagon:ports, driver-common
├── adapters:driven:driver-sqlite ──▶ hexagon:ports, driver-common
├── adapters:driven:driver-*-profiling ──▶ driver-*, hexagon:profiling (optional)
├── adapters:driven:formats ──▶ ports-read, ports-write, Jackson, DSL-JSON, SnakeYAML, Univocity
├── adapters:driven:integrations ──▶ hexagon:ports, driver-common
├── adapters:driven:streaming ──▶ ports-read, ports-write
└── adapters:driven:text-icu ──▶ hexagon:ports-common, ICU4J
Regel: hexagon:core hat KEINE Abhängigkeit auf andere Module. ports-common hängt nur von core ab. ports-read nur von ports-common. ports-write von ports-common und ports-read. hexagon:application hängt nur vom Hexagon-Inneren ab, nie von Adaptern. Driven Adapters dürfen in produktivem Code nicht voneinander abhängen (Ausnahme: die geteilte JDBC-Basis adapters:driven:driver-common als Adapter-Sink). Treiber-Kernmodule hängen nicht von hexagon:profiling ab. Driving Adapter sind Composition Roots für Wiring, aber nicht für produktive JDBC-Ausführung oder JDBC-Unwrap.
Das DatabaseDriver-Interface exponiert folgende Ports:
/**
* Port: Abstraktion für Datenbankzugriff.
* Jeder Treiber implementiert dieses Interface.
*/
interface DatabaseDriver {
val dialect: DatabaseDialect
fun ddlGenerator(): DdlGenerator
fun dataReader(): DataReader
fun tableLister(): TableLister
fun dataWriter(): DataWriter
fun urlBuilder(): JdbcUrlBuilder
}TableLister existiert produktiv und wird bereits für Auto-Discovery im
Datenexport genutzt. TypeMapper ist kein exponierter Port — er ist internes
Implementierungsdetail von DdlGenerator (via AbstractDdlGenerator).
schemaReader() ist Teil der Driver-Fassade und wird von PostgreSQL, MySQL
und SQLite bereitgestellt:
interface DatabaseDriver {
// … bestehende Ports …
fun schemaReader(): SchemaReader
}SchemaReader liefert ein Ergebnisobjekt, das neben dem Schema auch
Reverse-Notes und übersprungene Objekte transportiert:
interface SchemaReader {
fun read(
pool: ConnectionPool,
options: SchemaReadOptions = SchemaReadOptions(),
): SchemaReadResult
}
data class SchemaReadResult(
val schema: SchemaDefinition,
val notes: List<SchemaReadNote> = emptyList(),
val skippedObjects: List<SkippedObject> = emptyList(),
)Wichtig:
SchemaReaderliefert nicht ein nacktesSchemaDefinition, sondern einSchemaReadResultmitschema,notesund optionalskippedObjects.TableListerbleibt als bestehender Zwischenport für Export-Pfade sichtbar und wird nicht still aus der Architektur gestrichen.SchemaWriter(DDL-Generierung via neutrales Modell) wird in der bestehenden Codebasis durchDdlGeneratorabgedeckt. Ein separatesSchemaWriter-Interface bleibt ein optionaler Erweiterungspfad.
interface DataReader {
/** Streaming-basierter Datenexport */
fun streamTable(
connection: ConnectionPool,
table: String,
filter: DataFilter? = null,
chunkSize: Int = 10_000
): Flow<DataChunk>
}
interface DataWriter {
/** Transaktionaler Datenimport */
suspend fun importChunk(
connection: ConnectionPool,
table: String,
chunk: DataChunk
): ImportResult
}
interface FormatCodec<T> {
val format: DataFormat
fun read(input: InputStream, options: FormatOptions): T
fun write(output: OutputStream, value: T, options: FormatOptions)
}
data class FormatOptions(
val encoding: TextEncoding = TextEncoding.UTF8,
val bomMode: BomMode = BomMode.AUTO,
// Optional; wenn gesetzt, wird die Zone in expliziten Konvertierungen
// via TemporalFormatPolicy.toZoned(...) eingesetzt. Der Caller leitet
// sie typischerweise aus ResolvedI18nSettings.timezone ab.
val timezone: ZoneId? = null
)Treiber werden über Java ServiceLoader automatisch erkannt:
# META-INF/services/dev.dmigrate.driver.DatabaseDriver
dev.dmigrate.driver.postgresql.PostgresDriver
dev.dmigrate.driver.mysql.MysqlDriver
dev.dmigrate.driver.sqlite.SqliteDriver
Dadurch können neue Treiber (Oracle, MSSQL) als separate JARs hinzugefügt werden, ohne den Core zu ändern.
Wichtig für Distribution und Deployment:
- JVM/Fat-JAR und Docker: Dynamisches Nachladen separater Treiber-JARs wird direkt unterstützt.
- GraalVM Native Image: Unterstützte Treiber werden zur Build-Zeit eingebunden. Zusätzliche Treiber erfordern ein neues Native-Build-Profil oder den Wechsel auf die JVM-Distribution.
/**
* Streaming-Pipeline für Datenverarbeitung.
* Verarbeitet Daten chunkweise ohne vollständiges Laden in den Speicher.
*/
class StreamingPipeline(
private val sourceDriver: DatabaseDriver,
private val targetDriver: DatabaseDriver,
private val transformer: DataTransformer,
private val checkpointStore: CheckpointStore
) {
suspend fun execute(
sourceConnection: DatabaseConnection,
targetConnection: DatabaseConnection,
tables: List<String>,
foreignKeys: List<ForeignKeyDefinition>,
config: PipelineConfig
): PipelineResult {
val graph = DependencyGraph.build(tables, foreignKeys)
val executionOrder = graph.topologicalSort()
val sourceReader = sourceDriver.dataReader()
val targetWriter = targetDriver.dataWriter()
// Unabhängige Tabellen parallel, abhängige sequentiell
return coroutineScope {
for (group in executionOrder.parallelGroups()) {
group.map { table ->
async(Dispatchers.IO) {
processTable(
table = table,
sourceConnection = sourceConnection,
targetConnection = targetConnection,
sourceReader = sourceReader,
targetWriter = targetWriter,
config = config
)
}
}.awaitAll()
}
PipelineResult.success(processedTables = tables)
}
}
private suspend fun processTable(
table: String,
sourceConnection: DatabaseConnection,
targetConnection: DatabaseConnection,
sourceReader: DataReader,
targetWriter: DataWriter,
config: PipelineConfig
) {
val checkpoint = checkpointStore.load(table)
sourceReader
.streamTable(
connection = sourceConnection,
table = table,
filter = checkpoint?.let { DataFilter.resumeAfter(it.lastProcessedId) },
chunkSize = config.chunkSize
)
.onEach { chunk ->
val transformed = transformer.transform(chunk)
targetWriter.importChunk(
connection = targetConnection,
table = table,
chunk = transformed
)
checkpointStore.save(table, chunk.lastId, chunk.count)
}
.collect()
}
}Die vollständige Typ-Mapping-Tabelle (18 neutrale Typen mit Attributen) und die Validierungsregeln sind in der Neutrales-Modell-Spezifikation §3 definiert.
/**
* Forward Type-Mapping vom neutralen Typ zum DB-spezifischen Typ.
* Pure Function — kein State, vollständig testbar.
*/
interface TypeMapper {
val dialect: DatabaseDialect
fun toSql(type: NeutralType): String
fun toDefaultSql(default: DefaultValue, type: NeutralType): String
}
// Beispiel: PostgreSQL Forward Type Mapper
class PostgresTypeMapper : TypeMapper {
override val dialect = DatabaseDialect.POSTGRESQL
override fun toSql(type: NeutralType): String = when (type) {
is NeutralType.Identifier -> if (type.autoIncrement) "SERIAL" else "INTEGER"
is NeutralType.Integer -> "INTEGER"
is NeutralType.BigInteger -> "BIGINT"
is NeutralType.Text -> if (type.maxLength != null) "VARCHAR(${type.maxLength})" else "TEXT"
is NeutralType.Char -> "CHAR(${type.length})"
is NeutralType.SmallInt -> "SMALLINT"
is NeutralType.Float -> when (type.floatPrecision) {
FloatPrecision.SINGLE -> "REAL"
FloatPrecision.DOUBLE -> "DOUBLE PRECISION"
}
is NeutralType.Xml -> "XML"
is NeutralType.BooleanType -> "BOOLEAN"
is NeutralType.Json -> "JSONB"
is NeutralType.Binary -> "BYTEA"
is NeutralType.Uuid -> "UUID"
is NeutralType.DateTime -> if (type.timezone) "TIMESTAMP WITH TIME ZONE" else "TIMESTAMP"
is NeutralType.Decimal -> "DECIMAL(${type.precision},${type.scale})"
is NeutralType.Enum -> "TEXT" // PostgreSQL ENUM via CREATE TYPE
// ...
}
}Reverse-Mapping ist nicht Teil von TypeMapper; es liegt in den
driver-spezifischen Metadata-Mapping-Komponenten. Der aktuelle PostgreSQL-Pfad
mapped 32-bit serial/Identity auf NeutralType.Identifier(autoIncrement = true). 64-bit bigserial/bigint identity wird bewusst als
NeutralType.BigInteger plus Diagnose transportiert, damit die Breite nicht zu
SERIAL kollabiert. NeutralType.BigIdentifier existiert nicht. Der geplante
64-bit-Identity-Vertrag wird als separates Spaltenmetadatum
ColumnGeneration.Identity modelliert, nicht als weiterer NeutralType.
schema generate uebergibt Generator-Optionen als separaten Konfigurationspfad
an den DDL-Generator. Diese Optionen sind nicht Teil des neutralen
Schema-Modells (d.h. sie sind weder in SchemaDefinition noch in
ColumnDefinition gespeichert) — sie steuern ausschliesslich das
Generierungsverhalten.
adapters:driving:cli
SchemaGenerateCommand
│ parst --target, --spatial-profile, --generate-rollback
│
▼
GeneratorOptions(
dialect = DatabaseDialect.POSTGRESQL,
spatialProfile = SpatialProfile.POSTGIS, ← aus CLI-Flag oder Dialect-Default
generateRollback = false
)
│
▼
hexagon:application
SchemaGenerateRunner.run(schema: SchemaDefinition, options: GeneratorOptions)
│
▼
hexagon:ports
DdlGenerator.generate(schema: SchemaDefinition, options: GeneratorOptions): DdlResult
│
▼
adapters:driven:driver-postgresql / driver-mysql / driver-sqlite
Konkrete DDL-Generierung mit spatialProfile aus options
GeneratorOptions wird im hexagon:ports-Modul definiert, damit alle
Driven-Adapter denselben Kontrakt implementieren. hexagon:core kennt
GeneratorOptions nicht — es beschreibt nur das Schema-Modell und die
Schema-Validierungsregeln.
| Aspekt | Verortung | Begruendung |
|---|---|---|
type: geometry |
hexagon:core — neutrales Modell |
Gehoert zur portablen Schema-Definition |
geometry_type, srid |
hexagon:core — ColumnDefinition |
Portable Schema-Metadaten |
| E120, E121 | hexagon:core — SchemaValidator |
Schema-/Modellregeln, unabhaengig vom Zieldialekt |
spatialProfile |
hexagon:ports — GeneratorOptions |
Generator-Konfiguration, dialektabhaengig |
| E052, W120 | Driven Adapter — DDL-Generator | Generator-/Report-Regeln, entstehen erst bei schema generate |
Die Architektur unterscheidet explizit zwischen zwei Ebenen:
-
schema validateprueft das neutrale Schema-Modell inhexagon:core. Hier entstehen E120 (unbekanntergeometry_type) und E121 (srid<= 0). Diese Pruefungen sind dialektunabhaengig. -
schema generatebewertet die Generierbarkeit im gewaehlten Zielprofil in den Driven Adaptern. Hier entstehen E052 (Tabelle blockiert wegen Profilnone) und W120 (SRID-Uebertragung best-effort). Diese Codes existieren nicht auf Schema-Ebene — sie sind Ergebnisse der konkreten Transformation.
Ein Schema, das E120 oder E121 erzeugt, wird von schema validate zurueckgewiesen.
schema generate ruft schema validate implizit auf und bricht bei
Validierungsfehlern ab (Exit-Code 3), bevor irgendwelche DDL erzeugt wird.
Die Spatial-Typsystem-Basis (geometry, geometry_type, srid,
GeneratorOptions) ist Voraussetzung fuer Reverse Engineering und
Daten-Transfer: schema reverse muss Spatial-Spalten in das neutrale Modell
zurueckfuehren koennen, und schema compare muss Geometry-Spalten korrekt
vergleichen. Ohne diese Modell- und Validierungsbasis waere das nicht
typsicher moeglich.
Der Tool-Export-Pfad stellt externe Migrationstools als Driven Adapter bereit.
Im Hexagon bleibt er ein tool-neutraler Generate-Use-Case: Die Application
erzeugt aus neutralem Schema, validierter Generator-Konfiguration und
DDL-Ergebnis ein MigrationBundle; die Integrations-Adapter rendern daraus
tool-spezifische Artefakte. Der Pfad fuehrt keine Pflicht-
Laufzeitabhaengigkeiten auf Tool-Runtimes ein und mutiert keine bestehenden
Tool-Projektdateien.
Die tool-neutrale Exportfamilie liegt im Package dev.dmigrate.migration:
MigrationBundle— kapselt Identity, Schema, DDL-Payloads und Rollback-StateMigrationIdentity— Tool, Dialekt, Version, Versionsquelle, SlugMigrationDdlPayload— generatornahesDdlResultplus timestamp-bereinigte deterministische SQL-DarstellungMigrationRollback— sealed:NotRequestedoderRequested(down)ArtifactRelativePath— validierter relativer Pfad (kein Escape, kein absolut)ToolMigrationExporter— Port fuer Tool-Adapter:render(bundle) -> ToolExportResultToolExportResult— Artefaktliste plus export-spezifischeToolExportNotes
Generator-Diagnostik (DdlResult.notes, skippedObjects) bleibt im Bundle
erhalten und wird nicht in Export-Notes umgedeutet.
MigrationIdentityResolver— Version-/Slug-Aufloesung (I/O-frei)MigrationVersionValidator— tool-spezifische VersionsformateMigrationSlugNormalizer—schema.name→ dateinamenfaehiger SlugDdlNormalizer— einmalige Timestamp-Bereinigung fuer deterministische InhalteArtifactCollisionChecker— in-run- und Dateisystem-Kollisionen (I/O-frei)ToolExportRunner— Orchestrator: Schema lesen → validieren → DDL erzeugen → Bundle bauen → Exporter aufrufen → Kollisionen pruefen → Artefakte schreiben → Report und Diagnostik ausgeben
Vier Implementierungen von ToolMigrationExporter, alle side-effect-frei:
FlywayMigrationExporter—V<version>__<slug>.sql+ opt.U...sqlLiquibaseMigrationExporter— versionierter XML-Changelog mitchangeSetDjangoMigrationExporter—RunSQL-Migration ausresult.statementsKnexMigrationExporter— CommonJS mitknex.raw()ausresult.statements
Das Modul haengt nur von hexagon:ports ab und fuehrt keine Tool-Runtime-
Abhaengigkeiten ein.
ExportCommand— Clikt-Gruppe unterd-migrate exportExportFlywayCommand,ExportLiquibaseCommand,ExportDjangoCommand,ExportKnexCommand— duenne Schalen, dieToolExportRequestbauen und anToolExportRunnerdelegieren
Die fokussierte Runtime-Matrix (Flyway→PostgreSQL, Liquibase→PostgreSQL,
Django→SQLite, Knex→SQLite) wird als Integrations-Tests in
adapters:driven:integrations ausgefuehrt, markiert mit
NamedTag("integration") und steuerbar ueber -PintegrationTests.
Daten-Profiling liegt in einem dedizierten Profiling-Modul, das Spaltenstatistiken, Qualitaetswarnungen und Zieltyp-Kompatibilitaet fuer bestehende Datenbanken liefert.
Eigenes Hexagon-Modul mit:
- Domaenenmodell:
DatabaseProfile,TableProfile,ColumnProfile,ProfileWarning,TargetTypeCompatibility - Typsystem:
LogicalType(datenorientiert, getrennt vonNeutralType),TargetLogicalType,Severity,WarningCode - Rule-Engine:
WarningEvaluatormit 8 migrationsrelevanten Regeln (rein funktional, kein JDBC) - Outbound-Ports:
SchemaIntrospectionPort,ProfilingDataPort,LogicalTypeResolverPort - Services:
ProfileTableService,ProfileDatabaseService
DatabaseDriver bleibt unveraendert — Profiling-Ports werden ueber einen
separaten ProfilingAdapterSet-Lookup verdrahtet.
PostgreSQL, MySQL und SQLite implementieren je:
SchemaIntrospectionAdapter— eigene Profiling-Projektion mit rohemdbTypeProfilingDataAdapter— Aggregat-Queries, Top-N, KompatibilitaetspruefungenLogicalTypeResolver— DB-Typ →LogicalTypeMapping
DataProfileRunner— Orchestrator analog zuSchemaReverseRunnerDataProfileRequest— DTO mitsource,tables,schema,topN,format,output
DataProfileCommandunterd-migrate data profile- JSON (Default) oder YAML-Report via
ProfileReportWriter
Der Default-Report ist byte-reproduzierbar: stabile Tabellen-/Spaltenreihenfolge,
stabile topValues-Sortierung, kein laufzeitvariables generatedAt.
Das phasenbezogene DDL-Modell klassifiziert DDL-Statements in PRE_DATA und
POST_DATA. Damit kann schema generate --split pre-post importfreundliche
Artefakte erzeugen, bei denen Trigger erst nach einem Datenimport aktiviert
werden.
enum class DdlPhase {
/** Structural DDL: tables, columns, sequences, indexes, constraints. */
PRE_DATA,
/** Deferred DDL: triggers, functions, procedures, views with routine deps. */
POST_DATA,
}
data class DdlStatement(
val sql: String,
val notes: List<TransformationNote> = emptyList(),
val phase: DdlPhase = DdlPhase.PRE_DATA,
)DdlResult bietet Filtermethoden pro Phase:
statementsForPhase(phase)— Statements einer PhaserenderPhase(phase)— gerenderte DDL einer PhasenotesForPhase(phase)/skippedObjectsForPhase(phase)— Diagnostik pro Phase
| Phase | Objekte |
|---|---|
PRE_DATA |
Custom Types, Sequences, Tabellen (topologisch sortiert), Indizes, Constraints, Views ohne Routinen-Abhaengigkeiten |
POST_DATA |
Functions, Procedures, Triggers, Views mit Routinen-Abhaengigkeiten |
Die Zuordnung von Views erfolgt ueber den ViewPhaseClassifier
(adapters:driven:driver-common), der drei Regeln anwendet:
- Deklarierte Abhaengigkeiten:
dependencies.functionsim Schema →POST_DATA - Inferierte Funktionsaufrufe: Query-Text wird auf Funktionsnamen geparst →
POST_DATA - Transitive Propagation: Views, die von einer
POST_DATA-View abhaengen →POST_DATA
Views ohne Query-Text und ohne deklarierte dependencies.functions erzeugen
bei vorhandenen Functions im Schema den Fehlercode E060.
SchemaGenerateRunner
│ SplitMode: SINGLE | PRE_POST
▼
AbstractDdlGenerator.generate()
│ weist DdlPhase pro Statement zu
│ (PRE_DATA default, POST_DATA explizit fuer Routinen/Trigger)
▼
DdlResult
│
├─ SINGLE: renderAll() → eine Ausgabedatei
└─ PRE_POST: renderPhase(PRE_DATA) → *.pre-data.sql
renderPhase(POST_DATA) → *.post-data.sql
Der Default-Modus SINGLE bleibt rueckwaertskompatibel — alle Statements
werden in einer Datei ausgegeben, die Phase-Information wird ignoriert.
schema migrate --execute gegen MySQL/PostgreSQL-Targets traegt zwei
unabhaengige Live-DB-Probe-Stages im Render-Pfad — beide folgen dem
F.5 CheckPreflightProbe-Adapter-Muster: stateless Port in
hexagon:ports-read, JDBC-Adapter im Driver-Modul, Application-Layer-
Stage entscheidet ueber Skip/Probe/Block, CLI-Wiring im :adapters:driving:cli-
Modul.
Vor jedem Sequence-/SequenceNextVal-Render verifiziert die Pipeline,
dass die dmg_sequences-Helper-Table (Tabelle + dmg_nextval/dmg_setval-
Routinen + per-Sequenz-Zeile + spaltenbezogener dmg_seq_…_bi-Trigger)
der kanonischen Form entspricht. Drift wird mit
E124_MYSQL_SEQUENCE_DRIFT_TABLE/_ROUTINE/_ROW/_TRIGGER (alle
MANUAL_ACTION_REQUIRED) geblockt; PK-Verlust und Body-Signatur-
Vergleich (nicht nur Marker-Substring) sind Teil des Vertrags.
plan
→ MysqlSequenceCanonicityStage.run(probe, request, target, dialect, plan)
→ fuer jede Sequence-Op + jede AddColumn/AlterColumnDefault mit SequenceNextVal:
SUPPORT_TABLE + NEXTVAL_ROUTINE + SETVAL_ROUTINE + SEQUENCE_ROW + SUPPORT_TRIGGER
→ Declarations (CANONICAL/DRIFT/MISSING/NOT_RUN_*/PROBE_RUNTIME_ERROR)
→ DdlGenerationOptions.dialectContext (MySql.sequenceCanonicity)
→ MysqlDiffSequenceOps.canonicityBlocks(op, intent, ctx) gated Create/Alter/Drop/Rename
→ MigrationDdlResult.mysqlSequenceCanonicity → SchemaMigrateReport
Schluesselkomponenten:
- Port:
MysqlSequenceCanonicityProbe+ sealedMysqlSequenceCanonicityKind(SUPPORT_TABLE/NEXTVAL_ROUTINE/SETVAL_ROUTINE/SEQUENCE_ROW/SUPPORT_TRIGGER) +MysqlSequenceCanonicityStatus. - Gate:
MysqlSequenceCanonicityGate.decide(declaration, intent)routet per(status × OpIntent)-Matrix;MISSING + DROPfuerSEQUENCE_ROW/SUPPORT_TABLEblockt mitE124_MYSQL_SEQUENCE_MISSING_FOR_DROP. - Adapter:
MysqlSequenceCanonicityProbeAdapter(driver-mysql) nutztINFORMATION_SCHEMA.COLUMNS+SHOW CREATE FUNCTION/TRIGGER; 1305/1360 MySQL-Errors →MISSING. - Stage:
MysqlSequenceCanonicityStage(hexagon:application) im Render- Pipeline-Flow.
Per Default verliert eine Sequenz ihren runtime-Wert beim Migrieren —
CREATE SEQUENCE … START WITH 1 setzt nextval auf 1, auch wenn das
Live-Target bereits bei 5000 stand. preserveCurrentValue: true auf
SequenceDefinition aktiviert opt-in pro Sequenz einen Follow-up-Pfad,
der den geprobten last_value/next_value ueber die Migration rettet.
plan(initial)
→ SequencePreserveStage.run(probe, request, target, dialect, plan)
→ fuer jede Create/Alter/RenameSequence mit preserveCurrentValue=true:
SequenceCurrentValueProbe(target, ref) → Read/NotFound/Failed/NotApplicable
→ AlterSequenceCurrentValue-FollowUp direkt hinter der parent-Op
(dependencies = setOf(parent.id))
| INFO SEQUENCE_PRESERVE_NOT_FOUND (CreateSequence ohne Vorzustand)
| BLOCKER SEQUENCE_PRESERVE_PROBE_FAILED/REQUIRES_DB_TARGET/NOT_SUPPORTED_BY_DIALECT/CONFIG_INVALID
→ augmentierter Plan ersetzt das Original (auch fuer migration-plan.v1-Artefakt!)
→ Renderer:
PG → SELECT setval('<seq>', <value>, <is_called>);
MySQL → UPDATE dmg_sequences SET next_value = <v>
WHERE name = <key> AND managed_by IN (...) AND format_version IN (...);
SQLite (helper_table opt-in via --sqlite-named-sequences)
→ UPDATE "dmg_sequences" SET "next_value" = <v>
WHERE "name" = <key>;
(ohne Opt-in: BLOCKER SEQUENCE_PRESERVE_OPT_IN_REQUIRED → MANUAL_ACTION_REQUIRED)
Schluesselkomponenten:
- Port:
SequenceCurrentValueProbe+ sealedSequenceCurrentValueProbeResult(Read{value, matchedRows, isCalled?, managedBy?, formatVersion?},Failed{code, message},NotFound,NotApplicable). - Werttyp:
SequenceObjectRef(name, schema, dialect: RenameProjectionDialect)als Probe-Eingabe. - DiffOp-Subtyp:
DiffOperation.AlterSequenceCurrentValuemitpairId/probeSequenceRef/applySequenceRef/currentValue/isCalled?/restoreValue?/restoreIsCalled?/rollbackImpossible/rollbackImpossibleReason?/revertAfterRename. - Renderer:
PostgresDiffSequenceOps.renderAlterSequenceCurrentValue(PG),MysqlDiffSequenceOps.renderAlterSequenceCurrentValue(MySQL),SqliteDiffSequenceOps.renderAlterSequenceCurrentValue(SQLite — Up aufapplySequenceRef, Down aufprobeSequenceRefmitrestoreValue;restoreValue == null⇒ Skip mitSQLITE_SEQUENCE_CURRENT_VALUE_DOWN_ROLLBACK_IMPOSSIBLE). - JDBC-Adapter:
PostgresSequenceCurrentValueProbe(SQLSTATE 42P01/42501-Mapping),MysqlSequenceCurrentValueProbe(Error-Code 1146/1142 +managed_by/format_version-Set-Validation ueberMysqlSequenceSupportNaming.SUPPORTED_MANAGED_BY/SUPPORTED_FORMAT_VERSIONS),SqliteSequenceCurrentValueProbe(no-such-table-Message →NotFound, SQLITE_PERM/SQLITE_AUTH →PROBE_PERMISSION_DENIED; Validierung gegenSqliteSequenceNaming.MANAGED_BY/FORMAT_VERSION). - Stage:
SequencePreserveStage(hexagon:application) — Kandidaten- Filter, file-Target-Priority-Blocker, Dialekt-Allowlist (PG/MySQL/SQLite), SQLite-helper_table-Opt-in-Gate (SEQUENCE_PRESERVE_OPT_IN_REQUIRED), Probe-Routing, Plan-Augmentation. - CLI:
SequenceCurrentValueProbeRunnerdispatcht perSequenceObjectRef.dialectan die PG/MySQL/SQLite-Adapter.--sqlite-named-sequences helper_tableaufschema migrateschaltet den SQLite-Probe-Pfad frei.
AlterSequenceCurrentValue-Follow-ups landen direkt hinter ihrer
parent-Op im augmentierten Plan und sind in dependencies auf die
parent-Op-ID gepinnt. Das signierte migration-plan.v1-Artefakt
(--plan-artefact) erhaelt den augmentierten Plan — ein Operator,
der nur das Artefakt liest, sieht das setval/UPDATE-Statement.
Beide Stages folgen dem gleichen Schichtschnitt — neue Live-DB-Probes
(z.B. fuer CHECK-Constraints) folgen diesem Pattern, sodass
:hexagon:application keine driver-spezifischen Importe sieht und CLI-Wiring
konsistent bleibt:
| Schicht | Drift-Check | preserveCurrentValue |
|---|---|---|
Port (hexagon:ports-read) |
MysqlSequenceCanonicityProbe |
SequenceCurrentValueProbe |
| Result-Typ | MysqlSequenceCanonicityDeclaration |
sealed SequenceCurrentValueProbeResult |
| Application-Layer Stage | MysqlSequenceCanonicityStage |
SequencePreserveStage |
| JDBC-Adapter | MysqlSequenceCanonicityProbeAdapter |
PostgresSequenceCurrentValueProbe, MysqlSequenceCurrentValueProbe, SqliteSequenceCurrentValueProbe |
| CLI-Runner | MysqlSequenceCanonicityProbeRunner |
SequenceCurrentValueProbeRunner |
| Renderer-Anbindung | MysqlDiffSequenceOps.canonicityBlocks |
Stage augmentiert Plan; Renderer emittiert Follow-up |
| Classifier-Codes | E124_MYSQL_SEQUENCE_DRIFT_* → MANUAL_ACTION_REQUIRED |
SEQUENCE_PRESERVE_PROBE_FAILED / _CONFIG_INVALID / _REQUIRES_DB_TARGET / _OPT_IN_REQUIRED → MANUAL_ACTION_REQUIRED; _NOT_SUPPORTED_BY_DIALECT → DIALECT_UNSUPPORTED_OPERATION |
schema reverse und der DB-Operand in schema compare / schema migrate muessen pro Dialekt nicht nur Strukturobjekte (Tabellen,
Indices, FKs) sondern auch Programmability-Objekte (Trigger,
Stored-Functions, Procedures, Views, Sequences) mit allen
Identity-Attributen korrekt aus dem Live-Katalog projizieren. Sonst
emittiert ein anschliessender Compare gegen ein File-Schema mit
identischen Werten spurious Replace-Diagnosen, die der Operator
manuell wegsortieren muss.
Fuer diese Treue gelten insbesondere zwei Reader-Vertraege:
sqlite_master liefert Trigger als rohen CREATE TRIGGER-Text;
der Reader muss den DDL-String parsen, da SQLite kein
strukturiertes information_schema hat. Naive Substring-Suche ist
nicht ausreichend, weil sie WHEN-Klauseln verlieren und INSTEAD OF
mit BEFORE verwechseln kann.
- Parser:
SqliteTriggerSqlParser(token-basiert, string- und comment-aware) extrahierttiming(BEFORE / AFTER / INSTEAD OF),event(INSERT / UPDATE / DELETE),forEach(ROW),condition(WHEN-Klausel inkl. Comment-Stripping) undbody(Multi-Statement zwischenBEGINund letztemEND, mit einem Trim des optionalen trailing;fuer Renderer-Symmetrie). - Reader-Routing:
SqliteSchemaReader.readTriggersruft den Parser; R212-rejected Trigger (schema-qualifizierte Namen wiemain.trg) werden aus der Trigger-Map ausgeschlossen, damit downstream Object-Key-Kollisionen mit derschema.table-Form ausgeschlossen sind. - Diagnostics:
R210(timing missing) /R211(event missing) sind jetztACTION_REQUIRED(vorherWARNING);R212(schema-qualified,ACTION_REQUIRED) undR213(UPDATE OF cols,WARNING) sind neu. - Round-Trip-Vertrag: Reverse → Renderer-DDL → DB → Reverse
bleibt bit-identisch (
bodyohne trailing;weil der Renderer;\nEND;unconditional anhaengt); die YAML-Codec-Seite ist viaSchemaNodeProgrammabilityTriggerRoundtripTestseparat gepinnt.
information_schema.routines liefert pro Routine ein
security_type, einen definer-String ('user'@'host') und einen
sql_mode-Snapshot zur Erzeugungszeit. Der Reader projiziert diese
Identity-Attribute, damit file-zu-DB-Diffs gegen ein File-Schema mit
explizitem SQL SECURITY DEFINER oder sql_mode keine spurious-
Replace-Diagnosen produzieren.
- MetadataQueries:
listFunctions/listProceduresprojizierensecurity_type,definer,sql_modeausinformation_schema.routines. - Reader:
readFunctions/readProcedurespopulierenFunctionDefinition.security/definer/sqlMode(analog aufProcedureDefinition). Leeressql_modewird zunullnormalisiert; unbekanntesecurity_type-Werte (z.B. aelteres MySQL oder eingeschraenkteinformation_schema-Sicht) fallen ebenfalls aufnullzurueck. - Comparator-Symmetrie:
RoutineIdentityNormalizer.normalizeMysqlSqlModesortiert / dedupliziert diesql_mode-Liste, sodass Reihenfolge-Drift im Live-Katalog keinen spurious-Replace ausloest.
/**
* Hierarchische Konfiguration.
* Lädt aus mehreren Quellen mit definierter Priorität.
*/
data class DmigrateConfig(
val database: DatabaseConfig,
val export: ExportConfig,
val ai: AiConfig,
val i18n: I18nConfig,
val pipeline: PipelineConfig,
val documentation: DocumentationConfig = DocumentationConfig()
)
data class AiConfig(
val defaultBackend: String = "ollama",
val backends: Map<String, AiBackendConfig>,
val privacy: PrivacyConfig = PrivacyConfig()
)
data class PrivacyConfig(
val preferLocal: Boolean = true,
val allowExternal: Boolean = false
)
data class PipelineConfig(
val chunkSize: Int = 10_000,
val parallelism: Int = Runtime.getRuntime().availableProcessors(),
val checkpointInterval: Int = 10_000,
val retryAttempts: Int = 3,
val retryDelayMs: Long = 1_000
)
data class I18nConfig(
val defaultLocale: String = "en",
// Optional im YAML. Wird vom I18nSettingsResolver aufgeloest:
// i18n.default_timezone -> ZoneId.systemDefault() -> UTC (Error-Fallback)
// UTC ist der Safety-Net-Fallback, nicht der allgemeine Default.
val defaultTimezone: String? = null,
val normalizeUnicode: UnicodeNormalization = UnicodeNormalization.NFC
)
data class DocumentationConfig(
val enabledFormats: Set<String> = setOf("markdown"),
val includeErDiagrams: Boolean = true,
val includeLocalizedLabels: Boolean = true
)Architekturvertrag:
defaultLocalebeschreibt den Produktdefault; Root-/Fallback-Bundle ist Englisch (messages.properties).- Die effektive I18n-Konfiguration wird ueber denselben Pfadvertrag wie die bestehende CLI-Konfiguration bestimmt:
--config>D_MIGRATE_CONFIG>./.d-migrate.yaml. defaultTimezoneist optional und wird durchI18nSettingsResolverin der Reihenfolgei18n.default_timezone->ZoneId.systemDefault()->UTC(Error-/Leer-Fallback) zu einerZoneIdaufgeloest; die aufgeloeste Zone greift nur in expliziten Konvertierungen, nicht als Serialisierungs-Offset fuer lokale Werte.--langist ein CLI-Override:--langhat Vorrang vorD_MIGRATE_LANGund ist strikt auf gebundelte Produktsprachen (de,en) beschraenkt.
┌──────────────────────────────────────────────┐
│ Logging │
├──────────────────────────────────────────────┤
│ Level │ Inhalt │
│──────────┼───────────────────────────────────│
│ ERROR │ Fehlgeschlagene Operationen │
│ WARN │ Fallback-Typen, Deprecations │
│ INFO │ Start/Ende von Operationen │
│ DEBUG │ SQL-Statements, Chunk-Verarbeitung│
│ TRACE │ Einzelne Datensätze (nur Dev) │
├──────────────────────────────────────────────┤
│ Trennung: Technische Logs (EN) vs. │
│ User-Meldungen (lokalisiert) │
└──────────────────────────────────────────────┘
- Framework: SLF4J + Logback
- Strukturiertes Logging: JSON-Format für maschinelle Auswertung
- Audit-Log: Separate Datei für alle DB-verändernden Operationen
- KI-Audit-Log: Archivierung von Quell-/Zielcode und Metadaten aller KI-Transformationen (
LN-030,LN-031) - Sensible Daten: Passwörter und API-Keys werden NICHT geloggt
Credentials-Handling:
┌─────────────────────────────────────────────┐
│ 1. Umgebungsvariablen (bevorzugt) │
│ 2. Encrypted Config File (~/.d-migrate/ │
│ credentials.enc, AES-256) │
│ 3. CLI-Prompt (interaktiv) │
│ 4. NIEMALS: Klartext in Config-Dateien │
└─────────────────────────────────────────────┘
Verbindungen:
- TLS/SSL für alle DB-Verbindungen (konfigurierbar)
- Certificate Pinning optional
- Connection-String-Validierung gegen Injection
Rollenbasierte Zugriffskontrolle (LN-028):
- Rollen: reader (Export, Schema-Ansicht), writer (Import, Migration), admin (Konfiguration, Rollback)
- Konfiguration über .d-migrate.yaml oder Umgebungsvariablen
- Enforcement in CLI-Commands vor Ausführung kritischer Operationen
/**
* Retry mit exponentiellem Backoff für transiente Fehler.
*/
suspend fun <T> withRetry(
maxAttempts: Int = 3,
initialDelayMs: Long = 1_000,
block: suspend () -> T
): T {
var lastException: Throwable? = null
repeat(maxAttempts) { attempt ->
try {
return block()
} catch (e: TransientException) {
lastException = e
delay(initialDelayMs * (1L shl attempt))
}
}
throw RetryExhaustedException(maxAttempts, lastException!!)
}- Textbasierte Formate verwenden standardmaessig UTF-8.
- Dateiimporte erkennen UTF-8/UTF-16 sowie BOM-Markierungen automatisch; weitere Encodings sind explizit konfigurierbar.
- Exportformate erhalten Encoding-Metadaten, sofern das Zielformat diese transportieren kann; fuer CSV erfolgt dies optional ueber Sidecar-Dateien.
- Temporale Werte folgen stabilen ISO-8601-Profilen fuer Serialisierung;
OffsetDateTimebleibt offsethaltig,LocalDateTimebleibt lokal ohne stille Umdeutung zu UTC oder JVM-Zone. Die Default-Zeitzone wird in der Reihenfolgei18n.default_timezone->ZoneId.systemDefault()->UTC(Error-/Leer-Fallback) aufgeloest und greift nur in expliziten Konvertierungen ueberTemporalFormatPolicy.toZoned(...). - Locale-sensible Werte wie Zahlen- und Waehrungsdarstellungen werden an Ein-/Ausgabegrenzen normalisiert, damit interne Verarbeitung formatunabhaengig bleibt.
- Unicode-Normalisierung dient als Utility fuer Vergleiche, Metadaten und Darstellungsstabilitaet; Nutzdatenpayloads werden dadurch nicht still umgeschrieben.
- BOM-Erkennung und CSV-BOM-Verhalten nutzen den bestehenden Encoding-Unterbau und sind Teil des Formatvertrags.
- Strukturierte JSON-/YAML-Ausgaben bleiben sprachstabil: Feldnamen, Codes und freie Fehlermeldungstexte bleiben englisch, lokalisiert werden nur menschenlesbare Plain-Text-Ausgaben.
- Optionale Validierungsbausteine fuer E.164-Telefonnummern bleiben ein Erweiterungspfad und gehoeren nicht zum Mindestvertrag.
- Schema-Format: rückwärtskompatibel für 2 Major-Versionen.
- CLI-Argumente: deprecated Flags bleiben 2 Minor-Versionen erhalten.
- Export-Formate: stabile Formate ab 1.0; JSON/YAML versionieren Metadaten im Dokument, CSV optional über Sidecar-Datei.
Gradle (Kotlin DSL) mit Multi-Module-Setup:
Das folgende Build-Skript beschreibt die implementierte Multi-Module-Codebasis:
// build.gradle.kts (Root)
plugins {
kotlin("jvm") version "2.1.0"
id("org.graalvm.buildtools.native") version "0.10.4"
}
allprojects {
group = "dev.dmigrate"
version = "<project-version>"
}
subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
dependencies {
testImplementation("io.kotest:kotest-runner-junit5:5.9.0")
testImplementation("io.kotest:kotest-assertions-core:5.9.0")
}
tasks.test {
useJUnitPlatform()
}
}| Bibliothek | Version | Modul | Zweck |
|---|---|---|---|
| Kotlin Stdlib | 2.1.x | Alle | Sprach-Grundlagen |
| Kotlin Coroutines | 1.9.x | streaming, cli | Async/Parallel |
| Clikt | 5.x | cli | CLI-Framework |
| Jackson (YAML/JSON) | 2.18.x | formats | Serialisierung |
| PostgreSQL JDBC | 42.x | driver-postgres | DB-Zugriff |
| MySQL Connector/J | 9.x | driver-mysql | DB-Zugriff |
| SQLite JDBC | 3.47.x | driver-sqlite | DB-Zugriff |
| HikariCP | 6.x | drivers | Connection Pooling |
| ICU4J | 76.x | adapters/driven/text-icu | Unicode-Verarbeitung (versteckt hinter dev.dmigrate.text.UnicodeTextService in hexagon:ports-common; Composition Root verdrahtet IcuUnicodeTextService in adapters/driving/cli und adapters/driving/mcp) |
| Ktor Client | 3.x | ai | HTTP für KI-APIs |
| SLF4J + Logback | 2.x/1.5 | Alle | Logging |
| Kotest | 5.9.x | Test | Test-Framework |
| Jqwik | 1.9.x | Test | Property-Based Testing |
| Testcontainers | 1.20.x | Test | Docker-basierte DB-Tests |
Distribution-Formate:
1. GitHub Release Assets
→ ZIP/TAR mit launcherbasiertem `bin/d-migrate`
→ Fat JAR für `java -jar d-migrate-<version>-all.jar`
→ Kanonischer Build: `:adapters:driving:cli:assembleReleaseAssets`
→ Distribution: GitHub Releases
2. OCI Image (ghcr.io/pt9912/d-migrate)
→ docker run --rm -v $(pwd):/work ghcr.io/pt9912/d-migrate:latest schema validate --source /work/schema.yaml
→ Basis: eclipse-temurin:21-jre-noble (Ubuntu 24.04, glibc, ZGC)
→ Kanonischer Build: Dockerfile-Stage `runtime` (docker build --target runtime)
→ Ein Bauweg je Image-Klasse — dieselbe Stage, die lokal geprüft wird, wird publiziert
→ Für CI/CD-Pipelines und Nutzer ohne JDK
3. Homebrew-Basis
→ Formula im Repository unter `packaging/homebrew/d-migrate.rb`
→ Konsumiert das publizierte GitHub-Release-ZIP
→ Verifikation nach Publish über einen ephemeren Tap:
brew tap-new <lokal>/<name> --no-git, Formula hineinkopieren, brew trust, brew install <lokal>/<name>/d-migrate
(ein Pfad-Aufruf `brew install --formula <datei>.rb` wird von Homebrew abgelehnt)
4. Native Binaries (GraalVM Native Image)
→ Eigenständige, Java-freie Binaries für Linux und Windows, am GitHub-Release
→ Dynamisch gegen glibc gelinkt — für musl/Alpine gelten JVM-Artefakt oder Container-Image
→ Zusätzlich als Container-Image `…:<version>-native` (Binary statt JVM)
5. Zukunftspfade
→ SDKMAN
→ Scoop
┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐
│ Commit │───▶│ Build │───▶│ Test │───▶│ Publish │
└──────────┘ │ │ │ │ │ │
│ Compile │ │ Unit │ │ Maven │
│ Lint │ │ Integr. │ │ GitHub │
│ Format │ │ Property │ │ Docker │
└──────────┘ │ Coverage │ │ Native │
└───────────┘ └──────────┘
Tools:
- Build: Gradle
- CI: GitHub Actions
- Lint: ktlint
- Coverage: Kover (Kotlin Coverage)
- Security: Dependabot, Snyk
- Release: Gradle + GitHub Actions
| Gate | Schwellwert |
|---|---|
| Unit-Test-Pass | 100% |
| Code-Coverage (Line) | ≥ 80% |
| Coverage Type-Mapping | 100% |
| ktlint | 0 Violations |
| Dependency Vulnerabilities | 0 Critical/High |
| Build-Dauer | < 5 Minuten |
adapters/driven/formats/src/test/resources/fixtures/
├── schemas/ # Neutrale Schema-Definitionen
│ ├── minimal.yaml # 1 Tabelle, 2 Spalten (Smoke-Test)
│ ├── e-commerce.yaml # Referenz-Schema (Lastenheft Anhang B)
│ ├── all-types.yaml # Alle 18 neutralen Typen
│ └── edge-cases.yaml # Unicode, Sonderzeichen, zirkuläre FKs
│
├── 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
│
├── invalid/ # Ungültige Schemas (Validierungstests)
│ ├── missing-primary-key.yaml # → E008
│ ├── broken-reference.yaml # → E002
│ ├── duplicate-column.yaml # → E004
│ ├── invalid-default.yaml # → E009
│ └── missing-decimal-precision.yaml # → E010
│
└── data/ # Testdaten für Import/Export
├── customers.json
├── customers.csv
└── customers.yaml
Namenskonvention: <schema-name>.<dialekt>.<format>
Golden-Master-Workflow:
- Schema-Fixture laden (
schemas/minimal.yaml) - DDL generieren (z.B. für PostgreSQL)
- Ergebnis gegen Golden Master vergleichen (
ddl/minimal.postgresql.sql) - Bei gewollten Änderungen: Golden Master aktualisieren und committen
| # | Entscheidung | Status | Begründung |
|---|---|---|---|
| 1 | Kotlin als Implementierungssprache | Akzeptiert | JDBC-Ökosystem, Null-Safety, Coroutines |
| 2 | Hexagonale Architektur | Akzeptiert | Testbarkeit, Austauschbarkeit der Adapter |
| 3 | Gradle Multi-Module | Akzeptiert | Modulare Builds, unabhängige Versionierung |
| 4 | JDBC statt R2DBC | Akzeptiert | Breitere Treiberunterstützung, Streaming via ResultSet |
| 5 | Kotlin Coroutines statt Threads | Akzeptiert | Strukturierte Concurrency, leichtgewichtig |
| 6 | Jackson für Serialisierung | Akzeptiert | YAML + JSON Support, bewährt, performant |
| 7 | SPI für Driver-Plugin-System | Akzeptiert | Standard-Java-Mechanismus, kein Framework nötig |
| 8 | GraalVM Native Image | Akzeptiert | Single-Binary, schneller Start, höherer Build-Aufwand |
| 9 | Testcontainers für DB-Tests | Akzeptiert | Echte Datenbanken im Test, reproduzierbar |
| 10 | YAML als Schema-Definitionsformat | Akzeptiert | Menschenlesbar, Git-freundlich, kommentierbar |
1. Neues Modul erstellen: d-migrate-driver-oracle/
2. Interface DatabaseDriver implementieren
3. TypeMapper für Oracle-spezifische Typen implementieren
4. ServiceLoader-Registrierung in META-INF/services/
5. Tests mit Testcontainers + Oracle XE Image
→ Kein Core-Code muss geändert werden
1. `AiProviderPort` implementieren (`hexagon/application/ai/`)
2. HTTP-Client oder lokales Adapter-Protokoll für Provider-API ergänzen
3. Konfiguration in AiBackendConfig ergänzen
4. ServiceLoader-Registrierung für JVM/Docker oder Native-Build-Profil erweitern
→ Kein Core-Code muss geändert werden
1. Interface FormatCodec implementieren
2. Serializer/Deserializer für das Format
3. CLI-Command ergänzen (1 Zeile: neuer --format Wert)
→ Core bleibt unverändert
1. ToolMigrationExporter in adapters:driven:integrations implementieren
2. render(bundle) gibt ToolExportResult mit relativen Artefakten zurück
3. Exporter in ExportCommands.kt unter exporterLookup registrieren
4. Neues ExportXyzCommand als Clikt-Subcommand unter ExportCommand
5. Tests: Renderer-Unit-Tests + Runtime-Integrationstest
→ Hexagon-Code bleibt unverändert
Entwickler-Maschine CI/CD-Pipeline
┌────────────────────┐ ┌─────────────────────┐
│ │ │ GitHub Actions │
│ d-migrate (CLI) │ │ │
│ ┌──────────────┐ │ │ ┌───────────────┐ │
│ │ Launcher oder│ │ │ │ Testcontainers│ │
│ │ Fat JAR │ │ │ │ ┌───────────┐ │ │
│ └──────┬───────┘ │ │ │ │ PostgreSQL│ │ │
│ │ │ │ │ │ MySQL │ │ │
│ ▼ │ │ │ │ SQLite │ │ │
│ ┌──────────────┐ │ │ │ └───────────┘ │ │
│ │ Lokale │ │ │ └───────────────┘ │
│ │ Datenbank │ │ └─────────────────────┘
│ └──────────────┘ │
│ │ │ Distribution
│ ▼ │ ┌─────────────────────┐
│ ┌──────────────┐ │ │ GitHub Releases │
│ │ Ollama │ │ │ GHCR (OCI) │
│ │ (optional) │ │ │ Homebrew-Basis │
│ └──────────────┘ │ │ spätere Kanäle │
└────────────────────┘ └─────────────────────┘
- Lastenheft — Vollständige Anforderungsspezifikation
- Neutrales-Modell-Spezifikation — YAML-Format, Typsystem, DDL-Parser, Validierung
- CLI-Spezifikation — Exit-Codes, Ausgabeformate, Kommando-Referenz
- DDL-Generierungsregeln — Quoting, Statement-Ordering, Dialekt-Besonderheiten
- Connection- und Konfigurationsspezifikation — URL-Format,
.d-migrate.yaml-Schema
External consumers that only need schema reading and data reading
(e.g. a future source-d-migrate adapter for d-browser) should
depend on:
Stable integration types:
hexagon:core—SchemaDefinition, neutral model types,TableDependencySorthexagon:ports—SchemaReader,SchemaReadOptions,SchemaReadResult,DataReader,TableLister,DatabaseDialect,ConnectionPool,FormatReadOptions,DataChunkReaderFactoryadapters:driven:driver-common—JdbcMetadataSession, connection poolingadapters:driven:formats— format readers (JSON/YAML/CSV)
Explicitly NOT part of the read surface:
ImportOptions,DataWriter,TableImportSession(write-oriented)StreamingImporter,StreamingExporter(orchestration)DataImportRunner,DataExportRunner(CLI)hexagon:profilinganddriver-*-profilingmodules (optional)DatabaseDriver/DatabaseDriverRegistry(mixed facade)
Verification: test:consumer-read-probe builds against the read
surface and compiles without write/CLI/profiling imports.
Version: 1.8 Stand: 2026-04-20 Status: Architektur-Zielbild mit implementierten Kernpfaden und markierten Erweiterungsbereichen