Changes between Initial Version and Version 1 of DatabaseProgramming


Ignore:
Timestamp:
06/15/26 04:50:18 (7 days ago)
Author:
231102
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • DatabaseProgramming

    v1 v1  
     1= Функции, Процедури и Тригери =
     2
     3Во оваа фаза беа имплементирани три функции, три процедури и три тригери кои ја поддржуваат основната бизнис логика на системот IDontKnow. Тие автоматизираат процеси како пресметка на кредитно-пондериран просек и отворен долг, проверка на слободни места, регистрирање наплата со автоматска алокација кон обврски, упис на студент на курс, сериско генерирање обврски за школарина, како и валидација на оценки, ажурирање на отворената сума и контрола на капацитетот на курсевите преку тригери.
     4
     5Сите рутини се креирани во базата `advdb_202526l_prj_idontknow` (PostgreSQL 17.9).
     6
     7[attachment:13_routines.sql 13_routines.sql]
     8
     9
     10== Функции ==
     11
     12Функциите се користат за пресметување и враќање на вредности кои се потребни во повеќе делови од системот и може да се вградат директно во прашалник, поглед или друга рутина.
     13
     14=== 1. fn_prosek_student ===
     15
     16{{{
     17CREATE OR REPLACE FUNCTION fn_prosek_student(
     18    p_student_id INTEGER,
     19    p_sistem_ocenuvanje_id INTEGER DEFAULT NULL
     20)
     21RETURNS NUMERIC
     22LANGUAGE sql
     23STABLE
     24AS $$
     25    SELECT ROUND(SUM(sov.ocena_vrednost::numeric * ak.krediti)
     26                     / NULLIF(SUM(ak.krediti), 0), 2)
     27    FROM sm_student_ocena_zapis soz
     28        JOIN sm_student_upisan_kurs suk ON soz.student_upisan_kurs_id = suk.id
     29        JOIN sm_student_semestar_upis ssu ON suk.student_semestar_upis_id = ssu.id
     30        JOIN sm_studenti_smerovi ss ON ssu.studenti_smerovi_id = ss.id
     31        JOIN sm_kurs_realizacija kr ON suk.kurs_realizacija_id = kr.id
     32        JOIN sm_akreditacija ak ON kr.akreditacija_id = ak.id
     33        JOIN sm_sistem_ocenuvanje_vrednost sov ON soz.sistem_ocenuvanje_vrednost_id = sov.id
     34    WHERE ss.student_id = p_student_id
     35      AND sov.polozitelna
     36      AND (p_sistem_ocenuvanje_id IS NULL OR sov.sistem_ocenuvanje_id = p_sistem_ocenuvanje_id);
     37$$;
     38}}}
     39
     40Функцијата `fn_prosek_student` го пресметува кредитно-пондерираниот просек од положените оценки на даден студент. Како влезни параметри прима `p_student_id` и опционално `p_sistem_ocenuvanje_id`, со кој пресметката може да се ограничи на еден систем на оценување. Поминувајќи низ синџирот на табели од `sm_student_ocena_zapis` до `sm_akreditacija`, ја дели сумата на (вредност на оценка × кредити) со вкупните кредити и резултатот го заокружува на две децимали.
     41
     42Реализирана е како функција бидејќи враќа единечна вредност што директно се вградува во прашалници — на пример листа на студенти со просек над праг, услов за стипендија или уверение (`SELECT indeks, fn_prosek_student(s.id) FROM sm_studenti s`), што не е возможно со процедура. За масовни извештаи постои погледот `vw_studentski_oceni`; функцијата е за поединечна, параметризирана употреба во произволен контекст.
     43
     44=== 2. fn_otvoren_dolg ===
     45
     46{{{
     47CREATE OR REPLACE FUNCTION fn_otvoren_dolg(
     48    p_user_id INTEGER,
     49    p_vid_valuta_id INTEGER
     50)
     51RETURNS NUMERIC
     52LANGUAGE sql
     53STABLE
     54AS $$
     55    SELECT COALESCE(SUM(fo.otvorena_suma), 0)
     56    FROM fin_finansiska_obvrska fo
     57    WHERE fo.user_id = p_user_id
     58      AND fo.vid_valuta_id = p_vid_valuta_id
     59      AND fo.otvorena_suma > 0
     60      AND fo.status_finansiska_obvrska_id IN
     61          (SELECT id FROM fin_status_finansiska_obvrska WHERE ime IN ('Izdadena', 'Delumno platena'));
     62$$;
     63}}}
     64
     65Функцијата `fn_otvoren_dolg` ја враќа вкупната отворена сума на корисник во дадена валута. Прима `p_user_id` и `p_vid_valuta_id`, и собира `otvorena_suma` само од обврските во статус `Izdadena` или `Delumno platena`, враќајќи 0 ако нема такви.
     66
     67Истата проверка ја користат повеќе места — блокада при упис, опомени и работа на шалтер — па е издвоена како функција со повратна вредност што може да се вгради дури и во услов (`WHERE fn_otvoren_dolg(...) > 0`). Како процедура би морала да враќа преку OUT-параметар и не би можела да се користи на тој начин.
     68
     69=== 3. fn_slobodni_mesta ===
     70
     71{{{
     72CREATE OR REPLACE FUNCTION fn_slobodni_mesta(p_kurs_realizacija_id INTEGER)
     73RETURNS INTEGER
     74LANGUAGE sql
     75STABLE
     76AS $$
     77    SELECT kr.kapacitet_studenti
     78               - (SELECT COUNT(*)::int
     79                  FROM sm_student_upisan_kurs suk
     80                  WHERE suk.kurs_realizacija_id = kr.id
     81                    AND suk.status_slushanje = 'UPISAN')
     82    FROM sm_kurs_realizacija kr
     83    WHERE kr.id = p_kurs_realizacija_id;
     84$$;
     85}}}
     86
     87Функцијата `fn_slobodni_mesta` го враќа бројот на слободни места на дадена реализација на курс — `kapacitet_studenti` намалено за бројот на активни (`UPISAN`) уписи. Доколку капацитетот е `NULL`, тоа значи неограничен број места.
     88
     89Се прикажува при пребарување курсеви и се користи внатре во процедурата `pr_zapisi_student_na_kurs`, па затоа е функција што враќа вредност. Контролата на капацитетот ја спроведува тригерот `trg_kapacitet_kurs`; функцијата само информира, додека тригерот е тој што го гарантира ограничувањето.
     90
     91== Процедури ==
     92
     93Процедурите се користат за извршување деловни операции врз базата кои опфаќаат повеќе чекори и табели, се повикуваат експлицитно со `CALL` и единствени смеат да користат `COMMIT` / `ROLLBACK` внатре.
     94
     95=== 1. pr_registriraj_naplata ===
     96
     97{{{
     98CREATE OR REPLACE PROCEDURE pr_registriraj_naplata(
     99    p_user_id INTEGER,
     100    p_suma NUMERIC,
     101    p_vid_valuta_id INTEGER,
     102    p_organizaciona_edinica_id INTEGER,
     103    p_referenca VARCHAR DEFAULT NULL,
     104    p_korisnik_naplata_id INTEGER DEFAULT NULL
     105)
     106LANGUAGE plpgsql
     107AS $$
     108DECLARE
     109    v_naplata_id INTEGER;
     110    v_smetka_id  INTEGER;
     111    v_ostatok    NUMERIC := p_suma;
     112    v_obvrska    RECORD;
     113    v_alocirano  NUMERIC;
     114BEGIN
     115    IF p_suma <= 0 THEN
     116        RAISE EXCEPTION 'Сумата мора да биде позитивна (добиено: %)', p_suma;
     117    END IF;
     118
     119    -- активна трансакциска сметка на организационата единица (приоритет: иста валута)
     120    SELECT oes.transakciska_smetka_id
     121    INTO v_smetka_id
     122    FROM or_organizaciona_edinica_smetki oes
     123        JOIN fin_transakciski_smetki ts ON oes.transakciska_smetka_id = ts.id
     124    WHERE oes.organizaciona_edinica_id = p_organizaciona_edinica_id
     125      AND oes.aktivna
     126      AND ts.aktivna
     127    ORDER BY (ts.vid_valuta_id = p_vid_valuta_id) DESC, ts.id
     128    LIMIT 1;
     129
     130    IF v_smetka_id IS NULL THEN
     131        RAISE EXCEPTION 'Организационата единица % нема активна трансакциска сметка',
     132            p_organizaciona_edinica_id;
     133    END IF;
     134
     135    INSERT INTO fin_naplati (user_id, korisnik_naplata_id, transakciska_smetka_id,
     136                             organizaciona_edinica_id, naplatena_suma, vid_valuta_id,
     137                             status_transakcija_id, nadvoresna_referenca,
     138                             requested_at, confirmed_at, data)
     139    VALUES (p_user_id, COALESCE(p_korisnik_naplata_id, p_user_id), v_smetka_id,
     140            p_organizaciona_edinica_id, p_suma, p_vid_valuta_id,
     141            (SELECT id FROM fin_status_transakcija WHERE ime_status = 'ACCEPTED'),
     142            p_referenca, now(), now(), now())
     143    RETURNING id INTO v_naplata_id;
     144
     145    FOR v_obvrska IN
     146        SELECT fo.id, fo.otvorena_suma
     147        FROM fin_finansiska_obvrska fo
     148        WHERE fo.user_id = p_user_id
     149          AND fo.vid_valuta_id = p_vid_valuta_id
     150          AND fo.otvorena_suma > 0
     151        ORDER BY fo.datum_dospevanje NULLS LAST, fo.id
     152        FOR UPDATE
     153    LOOP
     154        EXIT WHEN v_ostatok <= 0;
     155        v_alocirano := LEAST(v_ostatok, v_obvrska.otvorena_suma);
     156        INSERT INTO fin_naplata_alokacija (naplata_id, finansiska_obvrska_id,
     157                                           alocirana_suma, created_at)
     158        VALUES (v_naplata_id, v_obvrska.id, v_alocirano, now());
     159        v_ostatok := v_ostatok - v_alocirano;
     160    END LOOP;
     161
     162    IF v_ostatok > 0 THEN
     163        RAISE NOTICE 'Наплата %: % останува неалоцирано (преплата).', v_naplata_id, v_ostatok;
     164    END IF;
     165END;
     166$$;
     167}}}
     168
     169Процедурата `pr_registriraj_naplata` регистрира наплата и автоматски ја алоцира кон отворените обврски на корисникот. Откако ќе валидира дека сумата е позитивна и ќе ја најде активната трансакциска сметка на организационата единица (со приоритет на иста валута), внесува запис во `fin_naplati`, па во циклус ги покрива обврските по редослед на најстаро доспевање, внесувајќи редови во `fin_naplata_alokacija`. Ако остане непокриен дел (преплата), тој останува неалоциран и се пријавува со `NOTICE`.
     170
     171Ова е деловна операција составена од еден INSERT во `fin_naplati` и повеќе INSERT-и во `fin_naplata_alokacija`, која се извршува атомски и не враќа вредност за прашалник — затоа е процедура, а не функција. Намерно не ја менува `otvorena_suma`: тоа го прави тригерот `trg_azuriraj_otvorena_suma`, така што изведената вредност останува конзистентна без оглед дали алокацијата ја внела оваа процедура, друга апликација или рачен SQL. Процедурата одлучува како се распределува наплатата, а тригерот гарантира книговодствена конзистентност.
     172
     173=== 2. pr_zapisi_student_na_kurs ===
     174
     175{{{
     176CREATE OR REPLACE PROCEDURE pr_zapisi_student_na_kurs(
     177    p_student_id INTEGER,
     178    p_kurs_realizacija_id INTEGER
     179)
     180LANGUAGE plpgsql
     181AS $$
     182DECLARE
     183    v_ssu_id   INTEGER;
     184    v_user_id  INTEGER;
     185    v_dolg     NUMERIC;
     186    v_valuta   INTEGER;
     187    v_slobodni INTEGER;
     188BEGIN
     189    SELECT ssu.id
     190    INTO v_ssu_id
     191    FROM sm_student_semestar_upis ssu
     192        JOIN sm_studenti_smerovi ss ON ssu.studenti_smerovi_id = ss.id
     193        JOIN sm_kurs_realizacija kr ON kr.id = p_kurs_realizacija_id
     194    WHERE ss.student_id = p_student_id
     195      AND ssu.akademski_semestar_id = kr.akademski_semestar_id
     196    ORDER BY ssu.datum_upis DESC
     197    LIMIT 1;
     198
     199    IF v_ssu_id IS NULL THEN
     200        RAISE EXCEPTION 'Студентот % нема упис во семестарот на курсот %',
     201            p_student_id, p_kurs_realizacija_id;
     202    END IF;
     203
     204    IF EXISTS (SELECT 1
     205               FROM sm_student_upisan_kurs
     206               WHERE student_semestar_upis_id = v_ssu_id
     207                 AND kurs_realizacija_id = p_kurs_realizacija_id) THEN
     208        RAISE EXCEPTION 'Студентот % е веќе запишан на курсот %',
     209            p_student_id, p_kurs_realizacija_id;
     210    END IF;
     211
     212    SELECT s.user_id
     213    INTO v_user_id
     214    FROM sm_studenti s
     215    WHERE s.id = p_student_id;
     216
     217    FOR v_valuta IN
     218        SELECT DISTINCT vid_valuta_id
     219        FROM fin_finansiska_obvrska
     220        WHERE user_id = v_user_id
     221          AND otvorena_suma > 0
     222    LOOP
     223        v_dolg := fn_otvoren_dolg(v_user_id, v_valuta);
     224        IF v_dolg > 0 THEN
     225            RAISE EXCEPTION 'Студентот % има отворен долг % (валута %) — уписот е блокиран',
     226                p_student_id, v_dolg, v_valuta;
     227        END IF;
     228    END LOOP;
     229
     230    v_slobodni := fn_slobodni_mesta(p_kurs_realizacija_id);
     231    IF v_slobodni IS NOT NULL AND v_slobodni <= 0 THEN
     232        RAISE EXCEPTION 'Нема слободни места на курсот %', p_kurs_realizacija_id;
     233    END IF;
     234
     235    INSERT INTO sm_student_upisan_kurs (student_semestar_upis_id, kurs_realizacija_id, status_slushanje)
     236    VALUES (v_ssu_id, p_kurs_realizacija_id, 'UPISAN');
     237END;
     238$$;
     239}}}
     240
     241Процедурата `pr_zapisi_student_na_kurs` запишува студент на курс. Го наоѓа семестралниот упис на студентот за семестарот на курсот, одбива дупликат упис, одбива ако студентот има отворен долг (преку `fn_otvoren_dolg`) и одбива ако нема слободни места (преку `fn_slobodni_mesta`), а потоа внесува запис во `sm_student_upisan_kurs` со статус `UPISAN`.
     242
     243Повеќечекорната валидациона секвенца со завршно запишување, повикана експлицитно од акција на корисникот, ја прави процедура; таа воедно демонстрира компонирање бидејќи ги повикува функциите `fn_otvoren_dolg` и `fn_slobodni_mesta`. Проверката на долг е политика на уписот, а не интегритет на податоците (администратор може по исклучок да запише студент со долг преку директен INSERT), па затоа не е тригер; капацитетот, напротив, е физичко ограничување и е заштитен со тригер.
     244
     245=== 3. pr_generiraj_obvrski_za_semestar ===
     246
     247{{{
     248CREATE OR REPLACE PROCEDURE pr_generiraj_obvrski_za_semestar(p_akademski_semestar_id INTEGER)
     249LANGUAGE plpgsql
     250AS $$
     251DECLARE
     252    v_cenovnik RECORD;
     253    v_oe       INTEGER;
     254    v_opis     VARCHAR;
     255    v_student  RECORD;
     256    v_brojac   INTEGER := 0;
     257BEGIN
     258    SELECT asem.organizaciona_edinica_id,
     259           c.id AS cenovnik_id,
     260           c.iznos,
     261           c.vid_valuta_id,
     262           asem.tip_naplata_organizaciona_edinica_id
     263    INTO v_cenovnik
     264    FROM sm_akademski_semestar asem
     265        JOIN fin_tip_naplati_cenovnik c
     266            ON c.tip_naplata_organizaciona_edinica_id = asem.tip_naplata_organizaciona_edinica_id
     267                AND c.aktivna
     268    WHERE asem.id = p_akademski_semestar_id
     269    ORDER BY c.prioritet
     270    LIMIT 1;
     271
     272    IF v_cenovnik IS NULL THEN
     273        RAISE EXCEPTION 'Нема активен ценовник за семестар %', p_akademski_semestar_id;
     274    END IF;
     275
     276    v_oe := v_cenovnik.organizaciona_edinica_id;
     277    v_opis := 'Школарина семестар ' || p_akademski_semestar_id;
     278
     279    FOR v_student IN
     280        SELECT DISTINCT s.user_id
     281        FROM sm_student_semestar_upis ssu
     282            JOIN sm_studenti_smerovi ss ON ssu.studenti_smerovi_id = ss.id
     283            JOIN sm_studenti s ON ss.student_id = s.id
     284        WHERE ssu.akademski_semestar_id = p_akademski_semestar_id
     285          AND NOT EXISTS (SELECT 1
     286                          FROM fin_finansiska_obvrska fo
     287                          WHERE fo.user_id = s.user_id
     288                            AND fo.opis = 'Школарина семестар ' || p_akademski_semestar_id)
     289    LOOP
     290        INSERT INTO fin_finansiska_obvrska (user_id, organizaciona_edinica_id,
     291                                            tip_naplata_organizaciona_edinica_id,
     292                                            tip_naplati_cenovnik_id,
     293                                            status_finansiska_obvrska_id,
     294                                            datum_kreiranje, datum_dospevanje,
     295                                            originalna_suma, otvorena_suma,
     296                                            vid_valuta_id, opis)
     297        VALUES (v_student.user_id, v_oe, v_cenovnik.tip_naplata_organizaciona_edinica_id,
     298                v_cenovnik.cenovnik_id,
     299                (SELECT id FROM fin_status_finansiska_obvrska WHERE ime = 'Izdadena'),
     300                now(), CURRENT_DATE + 30, v_cenovnik.iznos, v_cenovnik.iznos,
     301                v_cenovnik.vid_valuta_id, v_opis);
     302        v_brojac := v_brojac + 1;
     303        IF v_brojac % 1000 = 0 THEN
     304            COMMIT; -- дозволено само во процедура
     305        END IF;
     306    END LOOP;
     307
     308    RAISE NOTICE 'Генерирани % обврски за семестар %', v_brojac, p_akademski_semestar_id;
     309END;
     310$$;
     311}}}
     312
     313Процедурата `pr_generiraj_obvrski_za_semestar` сериски генерира обврски за школарина за сите студенти запишани во даден академски семестар, според активниот ценовник на семестарот. Прескокнува студенти кои веќе имаат таква обврска (идемпотентно според `opis`), а на секои 1000 внесени редови прави `COMMIT`.
     314
     315Ова е единствената рутина што мора да биде процедура: периодичниот `COMMIT` врз batch од десетици илјади редови е дозволен само во процедура — функциите и тригерите во PostgreSQL не смеат да завршуваат трансакции.
     316
     317== Тригери ==
     318
     319Тригерите се користат за автоматско извршување логика при внесување или промена на податоци во табелите. Тие се единствениот механизам кој гарантира дека правилото важи без разлика кој и како пишува во табелата — апликација, процедура или рачен INSERT.
     320
     321=== 1. trg_validna_ocena ===
     322
     323{{{
     324CREATE OR REPLACE FUNCTION trgf_validna_ocena()
     325RETURNS TRIGGER
     326LANGUAGE plpgsql
     327AS $$
     328DECLARE
     329    v_status VARCHAR;
     330BEGIN
     331    SELECT status_slushanje
     332    INTO v_status
     333    FROM sm_student_upisan_kurs
     334    WHERE id = NEW.student_upisan_kurs_id;
     335
     336    IF v_status IS DISTINCT FROM 'UPISAN' THEN
     337        RAISE EXCEPTION 'Оценка може да се внесе само за активен упис (статус: %)', v_status;
     338    END IF;
     339
     340    IF NEW.osvoeni_poeni IS NOT NULL AND (NEW.osvoeni_poeni < 0 OR NEW.osvoeni_poeni > 100) THEN
     341        RAISE EXCEPTION 'Освоени поени мора да бидат 0–100 (добиено: %)', NEW.osvoeni_poeni;
     342    END IF;
     343
     344    IF NEW.datum_zapis > now() THEN
     345        RAISE EXCEPTION 'Датумот на запис не смее да биде во иднина';
     346    END IF;
     347
     348    RETURN NEW;
     349END;
     350$$;
     351
     352DROP TRIGGER IF EXISTS trg_validna_ocena ON sm_student_ocena_zapis;
     353CREATE TRIGGER trg_validna_ocena
     354    BEFORE INSERT ON sm_student_ocena_zapis
     355    FOR EACH ROW
     356    EXECUTE FUNCTION trgf_validna_ocena();
     357}}}
     358
     359Тригерот `trg_validna_ocena` се активира пред внесување оценка во `sm_student_ocena_zapis`. Тој одбива оценка ако уписот не е во статус `UPISAN`, ако освоените поени се надвор од опсегот 0–100, или ако датумот на запис е во иднина.
     360
     361Реализиран е како тригер бидејќи станува збор за интегритет на податоците — невалидна оценка е невалидна без оглед кој ја внесува (апликација, import или рачен INSERT), што процедура не би го гарантирала. CHECK-ограничување не е доволно бидејќи правилото за статусот бара поглед во друга табела (`sm_student_upisan_kurs`).
     362
     363=== 2. trg_azuriraj_otvorena_suma ===
     364
     365{{{
     366CREATE OR REPLACE FUNCTION trgf_azuriraj_otvorena_suma()
     367RETURNS TRIGGER
     368LANGUAGE plpgsql
     369AS $$
     370DECLARE
     371    v_nova NUMERIC;
     372BEGIN
     373    SELECT otvorena_suma - NEW.alocirana_suma
     374    INTO v_nova
     375    FROM fin_finansiska_obvrska
     376    WHERE id = NEW.finansiska_obvrska_id
     377    FOR UPDATE;
     378
     379    IF v_nova < 0 THEN
     380        RAISE EXCEPTION 'Алокацијата (%) ја надминува отворената сума на обврската %',
     381            NEW.alocirana_suma, NEW.finansiska_obvrska_id;
     382    END IF;
     383
     384    UPDATE fin_finansiska_obvrska
     385    SET otvorena_suma                = v_nova,
     386        status_finansiska_obvrska_id = CASE
     387                                           WHEN v_nova = 0 THEN
     388                                               (SELECT id FROM fin_status_finansiska_obvrska WHERE ime = 'Platena')
     389                                           ELSE
     390                                               (SELECT id FROM fin_status_finansiska_obvrska WHERE ime = 'Delumno platena')
     391            END
     392    WHERE id = NEW.finansiska_obvrska_id;
     393
     394    RETURN NEW;
     395END;
     396$$;
     397
     398DROP TRIGGER IF EXISTS trg_azuriraj_otvorena_suma ON fin_naplata_alokacija;
     399CREATE TRIGGER trg_azuriraj_otvorena_suma
     400    AFTER INSERT ON fin_naplata_alokacija
     401    FOR EACH ROW
     402    EXECUTE FUNCTION trgf_azuriraj_otvorena_suma();
     403}}}
     404
     405Тригерот `trg_azuriraj_otvorena_suma` се активира по внесување алокација во `fin_naplata_alokacija`. Тој ја намалува `otvorena_suma` на соодветната обврска за алоцираната сума, одбива алокација што би ја однела сумата под нула, и автоматски го поставува статусот на `Platena` (кога ќе достигне нула) или `Delumno platena`.
     406
     407`otvorena_suma` е изведена вредност што мора да се ажурира секогаш кога ќе настане алокација, па е врзана за настанот преку тригер, а не за повикувачот. Кога би била дел од процедурата `pr_registriraj_naplata`, секој иден писател на алокации (на пр. модул за поврат или миграција) би морал да не заборави да ја ажурира. Тригерот е `AFTER INSERT` затоа што редот прво мора да постои како валиден, а ажурирањето на другата табела е последица.
     408
     409=== 3. trg_kapacitet_kurs ===
     410
     411{{{
     412CREATE OR REPLACE FUNCTION trgf_kapacitet_kurs()
     413RETURNS TRIGGER
     414LANGUAGE plpgsql
     415AS $$
     416DECLARE
     417    v_kapacitet INTEGER;
     418    v_upisani   INTEGER;
     419BEGIN
     420    IF NEW.status_slushanje <> 'UPISAN' THEN
     421        RETURN NEW;
     422    END IF;
     423
     424    SELECT kapacitet_studenti
     425    INTO v_kapacitet
     426    FROM sm_kurs_realizacija
     427    WHERE id = NEW.kurs_realizacija_id
     428    FOR UPDATE; -- сериализира конкурентни уписи на иста реализација
     429
     430    IF v_kapacitet IS NULL THEN
     431        RETURN NEW; -- неограничен капацитет
     432    END IF;
     433
     434    SELECT COUNT(*)
     435    INTO v_upisani
     436    FROM sm_student_upisan_kurs
     437    WHERE kurs_realizacija_id = NEW.kurs_realizacija_id
     438      AND status_slushanje = 'UPISAN';
     439
     440    IF v_upisani >= v_kapacitet THEN
     441        RAISE EXCEPTION 'Курсот % е полн (капацитет %)', NEW.kurs_realizacija_id, v_kapacitet;
     442    END IF;
     443
     444    RETURN NEW;
     445END;
     446$$;
     447
     448DROP TRIGGER IF EXISTS trg_kapacitet_kurs ON sm_student_upisan_kurs;
     449CREATE TRIGGER trg_kapacitet_kurs
     450    BEFORE INSERT ON sm_student_upisan_kurs
     451    FOR EACH ROW
     452    EXECUTE FUNCTION trgf_kapacitet_kurs();
     453}}}
     454
     455Тригерот `trg_kapacitet_kurs` се активира пред внесување упис во `sm_student_upisan_kurs`. За нов упис со статус `UPISAN` ја заклучува реализацијата (`FOR UPDATE`), го брои бројот на активни уписи и одбива ако е достигнат `kapacitet_studenti` (при што `NULL` значи неограничено).
     456
     457Капацитетот мора да издржи и конкурентни уписи — двајца студенти што истовремено го земаат последното место. Проверка само во процедура би била race-condition бидејќи двете трансакции би виделе слободно место; тригерот со заклучување на редот на реализацијата ги сериализира таквите инсерти. Процедурата `pr_zapisi_student_na_kurs` сепак повикува `fn_slobodni_mesta` однапред за љубезна порака, но тригерот е последната, непробивна одбрана.