diff --git a/prisma/migrations/20261012173000_protokoll_id_lueckenlos/migration.sql b/prisma/migrations/20261012173000_protokoll_id_lueckenlos/migration.sql new file mode 100644 index 0000000..5fe034d --- /dev/null +++ b/prisma/migrations/20261012173000_protokoll_id_lueckenlos/migration.sql @@ -0,0 +1,15 @@ +-- #032: Protokoll-IDs ohne Zweierschritte. +-- +-- Bisher zog der Spalten-Default (BIGSERIAL) eine Nummer aus der Sequenz und der Trigger +-- `protokoll_verketten` gleich noch eine – jede Zeile verbrauchte zwei IDs. Die ID muss aber +-- im Trigger unter der Advisory-Sperre vergeben werden: Eine vorher gezogene Nummer koennte +-- kleiner sein als die einer parallelen Transaktion, die zuerst verkettet, und die Kette liesse +-- sich in ID-Reihenfolge nicht mehr pruefen. Deshalb verzichtet der Default auf die Sequenz. +-- +-- 0 ist nur ein Platzhalter, damit `prisma.protokoll.create` ohne ID auskommt; der Trigger +-- ueberschreibt jede ID (auch eine mitgegebene). Die Sequenz bleibt der Spalte zugeordnet +-- (OWNED BY), deshalb findet `pg_get_serial_sequence` sie im Trigger weiterhin. +-- +-- Bestehende Zeilen bleiben, wie sie sind: Die ID ist Teil der Hash-Eingabe, Umnummerieren +-- wuerde die Kette brechen. Die alten Luecken bleiben also stehen. +ALTER TABLE "Protokoll" ALTER COLUMN "id" SET DEFAULT 0; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 07af2e3..f7f103e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -483,7 +483,9 @@ model Vorgang { /// Revisionssicheres Protokoll (#005). Append-only per Trigger; `hash` verkettet jede Zeile /// mit ihrer Vorgaengerin und wird in der Datenbank berechnet. model Protokoll { - id BigInt @id @default(autoincrement()) + /// Vergibt der Trigger `protokoll_verketten` unter der Advisory-Sperre aus der Sequenz; der + /// Default 0 ist nur ein Platzhalter, damit `create` ohne ID auskommt (#032). + id BigInt @id @default(0) zeitpunkt DateTime @default(now()) akteurId String? /// Name zum Zeitpunkt der Aktion – bleibt lesbar, auch wenn das Konto spaeter umbenannt wird. diff --git a/tests/datenbankregeln.test.ts b/tests/datenbankregeln.test.ts index 6e3b69b..a78b077 100644 --- a/tests/datenbankregeln.test.ts +++ b/tests/datenbankregeln.test.ts @@ -236,8 +236,30 @@ describe("Protokoll", () => { expect(pruefung?.ergebnis).toBe(zweiter.id); }); + it("vergibt IDs ohne Zweierschritte (#032)", async () => { + // Frueher zogen Spalten-Default und Trigger je eine Nummer aus der Sequenz. + const [a, b, c] = [await eintragen("eins"), await eintragen("zwei"), await eintragen("drei")]; + expect(b.id).toBe(a.id + 1n); + expect(c.id).toBe(a.id + 2n); + const [pruefung] = await prisma.$queryRaw< + { ergebnis: bigint | null }[] + >`SELECT protokoll_kette_pruefen() AS ergebnis`; + expect(pruefung?.ergebnis).toBeNull(); + }); + + it("ueberschreibt eine mitgegebene ID unter der Sperre (#032)", async () => { + const erster = await eintragen("eins"); + const zweiter = await prisma.protokoll.create({ + data: { id: 999n, akteurName: "Test", aktion: "zwei", ergebnis: "ERFOLG" }, + }); + expect(zweiter.id).toBe(erster.id + 1n); + expect(zweiter.vorgaengerHash).toBe(erster.hash); + }); + it("bleibt bei gleichzeitigem Schreiben eine lueckenlose Kette", async () => { - await Promise.all(Array.from({ length: 20 }, (_, i) => eintragen(`parallel-${i}`))); + const zeilen = await Promise.all(Array.from({ length: 20 }, (_, i) => eintragen(`parallel-${i}`))); + const ids = zeilen.map((z) => z.id).sort((x, y) => (x < y ? -1 : 1)); + expect(ids.at(-1)! - ids[0]!).toBe(19n); const [pruefung] = await prisma.$queryRaw< { ergebnis: bigint | null }[] >`SELECT protokoll_kette_pruefen() AS ergebnis`; diff --git a/tests/protokoll-ansicht.test.ts b/tests/protokoll-ansicht.test.ts index 9c2c649..653adae 100644 --- a/tests/protokoll-ansicht.test.ts +++ b/tests/protokoll-ansicht.test.ts @@ -75,7 +75,7 @@ beforeEach(async () => { describe("protokollSeite", () => { it("blaettert ueber die ID, neueste zuerst, ohne Luecken und Doppelte", async () => { - // IDs sind nicht lueckenlos (der Trigger zieht die Sequenz selbst) – nur die Ordnung zaehlt. + // IDs koennen Luecken haben (Rollbacks, alte Zeilen vor #032) – nur die Ordnung zaehlt. const ids: string[] = []; for (let i = 0; i < SEITENGROESSE + 7; i += 1) ids.unshift(String((await eintragen({ aktion: "host.angelegt" })).id));