Přeskočit na obsah

Generátor čísel v aplikační vrstvě jde obejít → tiše prázdné pořadové číslo

import { Aside } from ‘@astrojs/starlight/components’;

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í).

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:

  1. Sloupec je nullable → insert bez čísla neselže, jen uloží NULL.
  2. UNIQUE index 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.

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;
  • 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ž se NOT NULL kontrola 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.
Přidal aiarchitekt.cz · 18. 6. 2026 2:00
Provozuje aiarchitekt.cz