Generátor čísel v aplikační vrstvě jde obejít → tiše prázdné pořadové číslo
import { Aside } from ‘@astrojs/starlight/components’;
Symptom
Sekce “Symptom”V UI u části záznamů (leadů) chybí pořadové číslo. Problém se opakovaně vracel — po každé „opravě” se za pár týdnů objevil znovu, jen u záznamů z jiného zdroje (webhook, hromadný import, migrační skript). Klasické whack-a-mole: každá oprava záplatovala jednu konkrétní cestu vzniku záznamu.
V DB konkrétně: stovky řádků s číslo IS NULL, přitom sloupec má UNIQUE index
(který násobné NULL povoluje, takže DB nic nehlásí).
Root cause
Sekce “Root cause”Pořadové číslo přiděloval pouze aplikační helper (generateNumber() volaný před
create()). Jenže nic nevynucuje, aby ho každá cesta vzniku zavolala — stačí
prisma.table.create({ data: { ... } }) (nebo raw INSERT, nebo migrační skript)
bez předchozího volání helperu a vznikne řádek bez čísla.
Dvě věci to dělají tichým:
- Sloupec je
nullable→ insert bez čísla neselže, jen uložíNULL. UNIQUEindex na nullable sloupci povoluje více NULL (Postgres) → ani unikátnost nezakřičí.
Takže každý nový zdroj insertu (nový webhook, jednorázový import, migrace) je další příležitost, jak číslo „zapomenout”. Proto se to vracelo.
Fix
Sekce “Fix”Přesunout přidělení čísla o vrstvu níž — do databáze, kde ho obejít nelze.
Jeden zdroj pravdy = sekvence + BEFORE INSERT trigger, který číslo doplní, když chybí:
CREATE SEQUENCE IF NOT EXISTS number_seq;
-- naseedovat na aktuální maximum, ať nextval pokračuje v řaděSELECT setval('number_seq', GREATEST((SELECT COALESCE(MAX(CAST("number" AS INTEGER)),0) FROM "Table" WHERE "number" ~ '^[0-9]+$'), 1), true);
CREATE OR REPLACE FUNCTION assign_number() RETURNS trigger AS $$BEGIN IF NEW."number" IS NULL OR NEW."number" = '' THEN NEW."number" := lpad(nextval('number_seq')::text, 6, '0'); END IF; RETURN NEW;END;$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_assign_number ON "Table";CREATE TRIGGER trg_assign_number BEFORE INSERT ON "Table" FOR EACH ROW EXECUTE FUNCTION assign_number();A aplikační generátor přepsat tak, aby četl ze STEJNÉ sekvence (SELECT nextval('number_seq')),
ne z vlastního čítače. Pak appka i trigger sdílí jeden counter → nemůžou se rozejít
ani vyrobit duplicitu. UNIQUE index zůstává jako poslední pojistka.
Tím dostane další pořadové číslo jakýkoli zdroj automaticky — Prisma, webhook, raw SQL, budoucí integrace.
Než pustíš trigger: doplň existující NULL
Sekce “Než pustíš trigger: doplň existující NULL”Jednorázově doplň prázdná čísla (řazení dle data vzniku, start nad maximem — existující čísla nepřečíslovávej, rozbiješ reference/audit):
WITH base AS (SELECT COALESCE(MAX(CAST("number" AS INTEGER)),0) m FROM "Table" WHERE "number" ~ '^[0-9]+$'),ordered AS (SELECT id, ROW_NUMBER() OVER (ORDER BY "createdAt", id) rn FROM "Table" WHERE "number" IS NULL OR "number"='')UPDATE "Table" t SET "number" = lpad(((SELECT m FROM base)+o.rn)::text,6,'0')FROM ordered o WHERE t.id = o.id;Poznámky
Sekce “Poznámky”- Gapy jsou OK u věcí jako leady/tikety (sekvence se nevrací při rollbacku transakce). U faktur/daňových dokladů gapy NEsmí být — tam se číslo musí generovat až po úspěšném commitu, ne ze sekvence.
- Pokud má být sloupec vždy vyplněný, zvaž navíc
NOT NULL— pak insert bez čísla spadne hlasitě i bez triggeru. S triggerem (BEFORE INSERT) obojí koexistuje: trigger doplní hodnotu dřív, než seNOT NULLkontrola vyhodnotí. - Příbuzný, ale jiný režim selhání téhož subsystému: drift sekvenčního čítače (čítač klesne pod maximum → kolize). Tady se naopak generátor úplně obejde.