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:
2026-04-02 22:03:17 +02:00
parent 9902b2bb44
commit a8bc82eb91
3 changed files with 161 additions and 7 deletions
@@ -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);
@@ -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
}
}
}
}