Mark A-2 as complete: Create domain hierarchy tables for events and tournaments, add Flyway migration script and tests for V2 schema; update roadmap with completed tasks.
This commit is contained in:
+79
@@ -0,0 +1,79 @@
|
|||||||
|
-- Domain hierarchy for Events/Tournaments within a tenant schema
|
||||||
|
-- This migration assumes each tenant schema represents one Veranstaltung (event).
|
||||||
|
|
||||||
|
-- veranstaltungen (singleton per tenant schema)
|
||||||
|
CREATE TABLE IF NOT EXISTS veranstaltungen (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- turniere (tournaments) – per Veranstaltung
|
||||||
|
CREATE TABLE IF NOT EXISTS turniere (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
veranstaltung_id UUID NOT NULL REFERENCES veranstaltungen(id) ON DELETE CASCADE,
|
||||||
|
oeps_turniernummer VARCHAR(50) NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_turniere_oeps_nr ON turniere(oeps_turniernummer);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_turniere_veranstaltung_id ON turniere(veranstaltung_id);
|
||||||
|
|
||||||
|
-- bewerbe (competitions) – per Turnier
|
||||||
|
CREATE TABLE IF NOT EXISTS bewerbe (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
turnier_id UUID NOT NULL REFERENCES turniere(id) ON DELETE CASCADE,
|
||||||
|
klasse VARCHAR(50) NOT NULL,
|
||||||
|
hoehe_cm INTEGER NULL,
|
||||||
|
bezeichnung TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bewerbe_turnier_id ON bewerbe(turnier_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bewerbe_klasse ON bewerbe(klasse);
|
||||||
|
|
||||||
|
-- abteilungen (sections/heats) – per Bewerb
|
||||||
|
CREATE TABLE IF NOT EXISTS abteilungen (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
bewerb_id UUID NOT NULL REFERENCES bewerbe(id) ON DELETE CASCADE,
|
||||||
|
nr INTEGER NOT NULL,
|
||||||
|
bezeichnung TEXT NOT NULL,
|
||||||
|
typ VARCHAR(32) NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
CONSTRAINT chk_abteilungen_typ CHECK (typ IN ('SEPARATE_SIEGEREHRUNG', 'ORGANISATORISCH'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_abteilungen_bewerb_nr ON abteilungen(bewerb_id, nr);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_abteilungen_bewerb_id ON abteilungen(bewerb_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_abteilungen_typ ON abteilungen(typ);
|
||||||
|
|
||||||
|
-- teilnehmer_konten – aggregated balances across all tournaments of one Veranstaltung
|
||||||
|
CREATE TABLE IF NOT EXISTS teilnehmer_konten (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
veranstaltung_id UUID NOT NULL REFERENCES veranstaltungen(id) ON DELETE CASCADE,
|
||||||
|
teilnehmer_id UUID NOT NULL,
|
||||||
|
saldo_cents BIGINT NOT NULL DEFAULT 0,
|
||||||
|
currency CHAR(3) NOT NULL DEFAULT 'EUR',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_tkonten_veranstaltung_teilnehmer ON teilnehmer_konten(veranstaltung_id, teilnehmer_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tkonten_veranstaltung_id ON teilnehmer_konten(veranstaltung_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tkonten_teilnehmer_id ON teilnehmer_konten(teilnehmer_id);
|
||||||
|
|
||||||
|
-- turnier_kassa – per tournament cash balance
|
||||||
|
CREATE TABLE IF NOT EXISTS turnier_kassa (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
turnier_id UUID NOT NULL REFERENCES turniere(id) ON DELETE CASCADE,
|
||||||
|
saldo_cents BIGINT NOT NULL DEFAULT 0,
|
||||||
|
currency CHAR(3) NOT NULL DEFAULT 'EUR',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_turnier_kassa_turnier ON turnier_kassa(turnier_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_turnier_kassa_turnier_id ON turnier_kassa(turnier_id);
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
package at.mocode.entries.service.migration
|
||||||
|
|
||||||
|
import org.flywaydb.core.Flyway
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
import org.junit.jupiter.api.TestInstance.Lifecycle
|
||||||
|
import org.testcontainers.containers.PostgreSQLContainer
|
||||||
|
import org.testcontainers.junit.jupiter.Container
|
||||||
|
import org.testcontainers.junit.jupiter.Testcontainers
|
||||||
|
import java.sql.Connection
|
||||||
|
|
||||||
|
@Testcontainers
|
||||||
|
@TestInstance(Lifecycle.PER_CLASS)
|
||||||
|
class DomainHierarchyMigrationTest {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Container
|
||||||
|
@JvmStatic
|
||||||
|
val postgres = PostgreSQLContainer<Nothing>("postgres:16-alpine").apply {
|
||||||
|
withDatabaseName("meldestelle")
|
||||||
|
withUsername("test")
|
||||||
|
withPassword("test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `tenant migration creates domain hierarchy tables`() {
|
||||||
|
val schema = "event_test"
|
||||||
|
|
||||||
|
// Run tenant migrations (V1 + V2)
|
||||||
|
Flyway.configure()
|
||||||
|
.dataSource(postgres.jdbcUrl, postgres.username, postgres.password)
|
||||||
|
.locations("classpath:db/tenant")
|
||||||
|
.schemas(schema)
|
||||||
|
.baselineOnMigrate(true)
|
||||||
|
.load()
|
||||||
|
.migrate()
|
||||||
|
|
||||||
|
java.sql.DriverManager.getConnection(postgres.jdbcUrl, postgres.username, postgres.password).use { conn ->
|
||||||
|
setSearchPath(conn, schema)
|
||||||
|
val expected = setOf(
|
||||||
|
"veranstaltungen",
|
||||||
|
"turniere",
|
||||||
|
"bewerbe",
|
||||||
|
"abteilungen",
|
||||||
|
"teilnehmer_konten",
|
||||||
|
"turnier_kassa"
|
||||||
|
)
|
||||||
|
val actual = loadTables(conn, schema, expected)
|
||||||
|
assertEquals(expected, actual, "Alle erwarteten Tabellen müssen existieren")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSearchPath(conn: Connection, schema: String) {
|
||||||
|
conn.createStatement().use { st -> st.execute("SET search_path TO \"$schema\"") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadTables(conn: Connection, schema: String, expected: Set<String>): Set<String> {
|
||||||
|
val sql = """
|
||||||
|
select table_name
|
||||||
|
from information_schema.tables
|
||||||
|
where table_schema = ? and table_type = 'BASE TABLE'
|
||||||
|
""".trimIndent()
|
||||||
|
return conn.prepareStatement(sql).use { ps ->
|
||||||
|
ps.setString(1, schema)
|
||||||
|
ps.executeQuery().use { rs ->
|
||||||
|
val names = mutableSetOf<String>()
|
||||||
|
while (rs.next()) names += rs.getString(1)
|
||||||
|
names.retainAll(expected)
|
||||||
|
names
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,14 +34,14 @@
|
|||||||
- [ ] Aktueller Status: Integrationstest temporär via `@Disabled` deaktiviert, um den Build zu entblocken; Re-Enable nach Stabilisierung der Jackson/Spring-Web-Konverter-Autokonfiguration
|
- [ ] Aktueller Status: Integrationstest temporär via `@Disabled` deaktiviert, um den Build zu entblocken; Re-Enable nach Stabilisierung der Jackson/Spring-Web-Konverter-Autokonfiguration
|
||||||
|
|
||||||
- [ ] **A-2** | Datenbankschema: Domänen-Hierarchie umsetzen
|
- [ ] **A-2** | Datenbankschema: Domänen-Hierarchie umsetzen
|
||||||
- [ ] Tabelle `veranstaltungen` anlegen (interne ID, Tenant-Grenze)
|
- [x] Tabelle `veranstaltungen` anlegen (interne ID, Tenant-Grenze)
|
||||||
- [ ] Tabelle `turniere` anlegen (FK → `veranstaltung_id`, OEPS-Turniernummer als eigenes Feld)
|
- [x] Tabelle `turniere` anlegen (FK → `veranstaltung_id`, OEPS-Turniernummer als eigenes Feld)
|
||||||
- [ ] Tabelle `bewerbe` anlegen (FK → `turnier_id`, Klasse, Höhe, Bezeichnung)
|
- [x] Tabelle `bewerbe` anlegen (FK → `turnier_id`, Klasse, Höhe, Bezeichnung)
|
||||||
- [ ] Tabelle `abteilungen` anlegen (FK → `bewerb_id`, `nr`, `bezeichnung`,
|
- [x] Tabelle `abteilungen` anlegen (FK → `bewerb_id`, `nr`, `bezeichnung`,
|
||||||
`typ: SEPARATE_SIEGEREHRUNG | ORGANISATORISCH`)
|
`typ: SEPARATE_SIEGEREHRUNG | ORGANISATORISCH`)
|
||||||
- [ ] Tabelle `teilnehmer_konten` anlegen (FK → `veranstaltung_id`, aggregiert Salden über Turniere)
|
- [x] Tabelle `teilnehmer_konten` anlegen (FK → `veranstaltung_id`, aggregiert Salden über Turniere)
|
||||||
- [ ] Tabelle `turnier_kassa` anlegen (FK → `turnier_id`, separate Kassa pro Turnier)
|
- [x] Tabelle `turnier_kassa` anlegen (FK → `turnier_id`, separate Kassa pro Turnier)
|
||||||
- [ ] Migrations-Skript schreiben und testen
|
- [x] Migrations-Skript schreiben und testen (`db/tenant/V2__domain_hierarchy.sql`, Test: `DomainHierarchyMigrationTest`)
|
||||||
|
|
||||||
- [ ] **A-3** | Validierungs-Grundlage: Turnierkategorie-Limits
|
- [ ] **A-3** | Validierungs-Grundlage: Turnierkategorie-Limits
|
||||||
- [ ] `Turnier.validate()`: Bewerbs-Klassen gegen Limits der Turnierkategorie prüfen (z.B. kein S-Springen auf
|
- [ ] `Turnier.validate()`: Bewerbs-Klassen gegen Limits der Turnierkategorie prüfen (z.B. kein S-Springen auf
|
||||||
|
|||||||
Reference in New Issue
Block a user