Changes between Initial Version and Version 1 of DatabaseProgramming


Ignore:
Timestamp:
06/16/26 00:45:35 (3 days ago)
Author:
231166
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • DatabaseProgramming

    v1 v1  
     1= DatabaseProgramming: Имплементација на Функции, Тригери и Процедури =
     2
     3Оваа страница го деталниот преглед на програмските објекти креирани на ниво на базата на податоци. Преку нивна имплементација, деловната логика на стриминг платформата е централизирана директно на серверот. Ова гарантира максимална безбедност, заштита на податочниот интегритет и драстично намалување на мрежниот сообраќај помеѓу апликацијата и базата.
     4
     5---
     6
     7== 1. Кориснички дефинирани функции (User-Defined Functions) ==
     8
     9Функциите во системот овозможуваат брзо пресметување, агрегирање и деловна обработка на податоците, враќајќи готови резултати до корисничкиот интерфејс без потреба од повторливи `JOIN` операции во апликативниот код.
     10
     11=== Имплементирани од Кристијан Атанасов ===
     12
     13==== 1. get_avg_rating(p_content_id) ====
     14* '''Опис и примена:''' Ја пресметува средната вредност на сите рејтинзи за одредена содржина и ја заокружува на две децимали. Се користи при пребарување и прикажување на картичките за филмови/серии на почетната страна на корисничкиот интерфејс.
     15{{{
     16#!sql
     17CREATE FUNCTION get_avg_rating(p_content_id INT)
     18RETURNS NUMERIC AS $$
     19DECLARE
     20  v_avg NUMERIC;
     21BEGIN
     22  SELECT ROUND(AVG(RatingValue), 2)
     23  INTO v_avg
     24  FROM Rating
     25  WHERE ContentContentID = p_content_id;
     26
     27  RETURN v_avg;
     28END;
     29$$ LANGUAGE plpgsql;
     30}}}
     31
     32==== 2. get_user_watch_count(p_user_id) ====
     33* '''Опис и примена:''' Враќа вкупен број на содржини кои некој корисник ги проследил. Ова е критично за генерирање на кориснички профилни статистики и за алгоритмите за препорака (Recommendation Engines).
     34{{{
     35#!sql
     36CREATE FUNCTION get_user_watch_count(p_user_id INT)
     37RETURNS INT AS $$
     38DECLARE
     39  v_count INT;
     40BEGIN
     41  SELECT COUNT(*)
     42  INTO v_count
     43  FROM WatchHistory
     44  WHERE UserUserID = p_user_id;
     45
     46  RETURN v_count;
     47END;
     48$$ LANGUAGE plpgsql;
     49}}}
     50
     51==== 3. get_content_genres(p_content_id) ====
     52* '''Опис и примена:''' Наместо апликацијата постојано да прави комплексни спојувања со табелата за врски `Content_Genre` и матичната `Genre`, оваа функција директно ги враќа сите жанрови за даден медиум споени во една текстуална низа (String).
     53{{{
     54#!sql
     55CREATE FUNCTION get_content_genres(p_content_id INT)
     56RETURNS VARCHAR AS $$
     57DECLARE
     58  v_genres VARCHAR;
     59BEGIN
     60  SELECT STRING_AGG(g.Name, ', ' ORDER BY g.Name)
     61  INTO v_genres
     62  FROM Genre g
     63  JOIN Content_Genre cg ON cg.GenreGenreID = g.GenreID
     64  WHERE cg.ContentContentID = p_content_id;
     65
     66  RETURN COALESCE(v_genres, 'No genres found');
     67END;
     68$$ LANGUAGE plpgsql;
     69}}}
     70
     71=== Имплементирани од Дамјан Димовски ===
     72
     73==== 4. get_subscription_info(p_user_id) ====
     74* '''Опис и примена:''' Враќа табеларан приказ со детали за активниот претплатнички план на корисникот. Оваа функција ја повикува безбедносниот систем (Middleware) при секое пуштање на видео за да потврди дали корисникот платил и какви се неговите привилегии.
     75{{{
     76#!sql
     77CREATE FUNCTION get_subscription_info(p_user_id INT)
     78RETURNS TABLE(plan_name VARCHAR, price NUMERIC, max_devices INT, status VARCHAR) AS $$
     79BEGIN
     80  RETURN QUERY
     81  SELECT
     82    s.Name,
     83    s.Price,
     84    s.MaxDevices,
     85    us.Status
     86  FROM User_Subscription us
     87  JOIN Subscription s ON s.SubscriptionID = us.SubscriptionSubscriptionID
     88  WHERE us.UserUserID = p_user_id
     89  LIMIT 1;
     90END;
     91$$ LANGUAGE plpgsql;
     92}}}
     93
     94==== 5. get_series_episode_count(p_series_id) ====
     95* '''Опис и примена:''' Го пресметува вкупниот број на епизоди во сите сезони за една конкретна серија. Се користи во корисничкиот интерфејс за да се прикаже информација од типот: *"Серијата содржи вкупно 48 епизоди"*.
     96{{{
     97#!sql
     98CREATE FUNCTION get_series_episode_count(p_series_id INT)
     99RETURNS INT AS $$
     100DECLARE
     101  v_count INT;
     102BEGIN
     103  SELECT COUNT(e.EpisodeID)
     104  INTO v_count
     105  FROM Episode e
     106  JOIN Season sea ON sea.SeasonID = e.SeasonSeasonID
     107  WHERE sea.SeriesSeriesID = p_series_id;
     108
     109  RETURN v_count;
     110END;
     111$$ LANGUAGE plpgsql;
     112}}}
     113
     114==== 6. get_top_content_by_genre(p_genre_name) ====
     115* '''Опис и примена:''' Враќа динамичка табела од топ 10 најдобро оценети содржини кои припаѓаат на специфичен жанр. Идеално за прикажување на персонализирани ленти со содржини (на пр. *"Топ 10 Sci-Fi Филмови"*).
     116{{{
     117#!sql
     118CREATE FUNCTION get_top_content_by_genre(p_genre_name VARCHAR)
     119RETURNS TABLE(title VARCHAR, avg_rating NUMERIC, total_ratings BIGINT) AS $$
     120BEGIN
     121  RETURN QUERY
     122  SELECT
     123    m.title,
     124    ROUND(AVG(r.RatingValue), 2) AS avg_rating,
     125    COUNT(r.RatingID) AS total_ratings
     126  FROM Media m
     127  JOIN Content_Genre cg ON cg.ContentContentID = m.ContentID
     128  JOIN Genre g ON g.GenreID = cg.GenreGenreID
     129  JOIN Rating r ON r.ContentContentID = m.ContentID
     130  WHERE g.Name = p_genre_name
     131  GROUP BY m.title
     132  ORDER BY avg_rating DESC
     133  LIMIT 10;
     134END;
     135$$ LANGUAGE plpgsql;
     136}}}
     137
     138---
     139
     140== 2. Базни тригери (Database Triggers) ==
     141
     142Тригерите во системот гарантираат автоматско извршување на логиката во заднина и го штитат интегритетот на податоците без разлика на начинот на кој апликацијата комуницира со базата.
     143
     144=== Имплементирани од Кристијан Атанасов ===
     145
     146==== 1. Логирање на избришани корисници (`trigger_log_user_delete`) ====
     147* '''Зошто постои во реален свет:''' Поради безбедносни ревизии (Auditing), системот мора да зачува трага кога и кој профил бил отстранет, без притоа да ги чува лозинките и сензитивните податоци на корисникот.
     148{{{
     149#!sql
     150CREATE TABLE IF NOT EXISTS User_Delete_Log (
     151  LogID      SERIAL PRIMARY KEY,
     152  UserID     INT,
     153  Username   VARCHAR(255),
     154  Email      VARCHAR(255),
     155  DeletedAt  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
     156);
     157
     158CREATE FUNCTION trg_log_user_delete()
     159RETURNS TRIGGER AS $$
     160BEGIN
     161  INSERT INTO User_Delete_Log (UserID, Username, Email)
     162  VALUES (OLD.UserID, OLD.Username, OLD.Email);
     163  RETURN OLD;
     164END;
     165$$ LANGUAGE plpgsql;
     166
     167CREATE TRIGGER trigger_log_user_delete
     168BEFORE DELETE ON "User"
     169FOR EACH ROW
     170EXECUTE FUNCTION trg_log_user_delete();
     171}}}
     172
     173==== 2. Валидација на опсег на рејтинг (`trigger_validate_rating`) ====
     174* '''Зошто постои во реален свет:''' Оневозможува малициозни корисници или системски багови на фронтендот да внесат невалиден рејтинг (на пр. оцена 15 или -5) кој би ја нарушил комплетната статистика на платформата. Вредноста строго мора да биде помеѓу 1 и 10.
     175{{{
     176#!sql
     177CREATE FUNCTION trg_validate_rating()
     178RETURNS TRIGGER AS $$
     179BEGIN
     180  IF NEW.RatingValue < 1 OR NEW.RatingValue > 10 THEN
     181    RAISE EXCEPTION 'RatingValue mora da bide pomegu 1 i 10, vnesena vrednost: %', NEW.RatingValue;
     182  END IF;
     183  RETURN NEW;
     184END;
     185$$ LANGUAGE plpgsql;
     186
     187CREATE TRIGGER trigger_validate_rating
     188BEFORE INSERT OR UPDATE ON Rating
     189FOR EACH ROW
     190EXECUTE FUNCTION trg_validate_rating();
     191}}}
     192
     193==== 3. Автоматско комплетирање на прогрес (`trigger_auto_complete_progress`) ====
     194* '''Зошто постои во реален свет:''' Кога корисникот ќе стигне до одреден висок процент од видеото (90% или повеќе), системот претпоставува дека ја изгледал целата содржина (вклучувајќи ги кредитите) и автоматски го поставува прогресот на 100% за да се отстрани од корисничката лента "Продолжи со гледање".
     195{{{
     196#!sql
     197CREATE FUNCTION trg_auto_complete_progress()
     198RETURNS TRIGGER AS $$
     199DECLARE
     200  v_duration INT;
     201BEGIN
     202  SELECT duration INTO v_duration
     203  FROM Watchable
     204  WHERE WatchableID = NEW.WatchableWatchableID;
     205
     206  IF NEW.Progress_percentage >= 90 THEN
     207    NEW.Progress_percentage := 100;
     208  END IF;
     209
     210  RETURN NEW;
     211END;
     212$$ LANGUAGE plpgsql;
     213
     214CREATE TRIGGER trigger_auto_complete_progress
     215BEFORE INSERT ON WatchHistory
     216FOR EACH ROW
     217EXECUTE FUNCTION trg_auto_complete_progress();
     218}}}
     219
     220=== Имплементирани од Дамјан Димовски ===
     221
     222==== 4. Автоматско истекување на претплата (`trigger_auto_expire_subscription`) ====
     223* '''Зошто постои во реален свет:''' Спречува корисници со поминат рок на картичката или откажана претплата да продолжат бесплатно да го користат сервисот. При секој обид за промена на претплатата, ако `End_date` е помал од денешниот датум, статусот инстант станува `Expired`.
     224{{{
     225#!sql
     226CREATE FUNCTION trg_auto_expire_subscription()
     227RETURNS TRIGGER AS $$
     228BEGIN
     229  IF NEW.End_date IS NOT NULL AND NEW.End_date < CURRENT_DATE AND NEW.Status != 'Expired' THEN
     230    NEW.Status := 'Expired';
     231  END IF;
     232  RETURN NEW;
     233END;
     234$$ LANGUAGE plpgsql;
     235
     236CREATE TRIGGER trigger_auto_expire_subscription
     237BEFORE UPDATE ON User_Subscription
     238FOR EACH ROW
     239EXECUTE FUNCTION trg_auto_expire_subscription();
     240}}}
     241
     242==== 5. Контрола на максимален број уреди (`trigger_check_max_devices`) ====
     243* '''Зошто постои во реален свет:''' Ја штити бизнис логиката на компанијата од нелегално споделување на лозинки. Ако корисникот има „Basic“ пакет кој дозволува само 1 уред, а се обиде да се најави од втор уред, тригерот фрла исклучок и го блокира внесувањето во табелата `Devices`.
     244{{{
     245#!sql
     246CREATE FUNCTION trg_check_max_devices()
     247RETURNS TRIGGER AS $$
     248DECLARE
     249  v_max_devices INT;
     250  v_current_devices INT;
     251BEGIN
     252  SELECT s.MaxDevices INTO v_max_devices
     253  FROM User_Subscription us
     254  JOIN Subscription s ON s.SubscriptionID = us.SubscriptionSubscriptionID
     255  WHERE us.UserSubscriptionID = NEW.UserSubscriptionID;
     256
     257  SELECT COUNT(*) INTO v_current_devices
     258  FROM Devices
     259  WHERE UserSubscriptionID = NEW.UserSubscriptionID;
     260
     261  IF v_current_devices >= v_max_devices THEN
     262    RAISE EXCEPTION 'Maksimalen broj na uredi e dostигнат: % od %', v_current_devices, v_max_devices;
     263  END IF;
     264
     265  RETURN NEW;
     266END;
     267$$ LANGUAGE plpgsql;
     268
     269CREATE TRIGGER trigger_check_max_devices
     270BEFORE INSERT ON Devices
     271FOR EACH ROW
     272EXECUTE FUNCTION trg_check_max_devices();
     273}}}
     274
     275==== 6. Логови за статус на претплати (`trigger_log_subscription_status_change`) ====
     276* '''Зошто постои во реален свет:''' Ова овозможува финансиска ревизија. Кога претплатата ќе премине од `Active` во `Cancelled` или `Suspended`, секоја промена мора хронолошки да се зачува за корисничката поддршка да може да види историја на трансакции.
     277{{{
     278#!sql
     279CREATE TABLE Subscription_Status_Log (
     280  LogID       SERIAL PRIMARY KEY,
     281  UserSubID   INT,
     282  UserID      INT,
     283  OldStatus   VARCHAR(255),
     284  NewStatus   VARCHAR(255),
     285  ChangedAt   TIMESTAMP DEFAULT CURRENT_TIMESTAMP
     286);
     287
     288CREATE FUNCTION trg_log_subscription_status_change()
     289RETURNS TRIGGER AS $$
     290BEGIN
     291  IF OLD.Status <> NEW.Status THEN
     292    INSERT INTO Subscription_Status_Log (UserSubID, UserID, OldStatus, NewStatus)
     293    VALUES (OLD.UserSubscriptionID, OLD.UserUserID, OLD.Status, NEW.Status);
     294  END IF;
     295  RETURN NEW;
     296END;
     297$$ LANGUAGE plpgsql;
     298
     299CREATE TRIGGER trigger_log_subscription_status_change
     300AFTER UPDATE ON User_Subscription
     301FOR EACH ROW
     302EXECUTE FUNCTION trg_log_subscription_status_change();
     303}}}
     304
     305---
     306
     307== 3. Зачувани Процедури (Stored Procedures) ==
     308
     309За разлика од функциите, процедурите се користат за извршување на комплексни деловни трансакции кои модифицираат повеќе табели одеднаш по принципот "сè или ништо" (Трансакциска сигурност).
     310
     311=== Имплементирани од Кристијан Атанасов ===
     312
     313==== 1. Регистрација на нов корисник со почетна претплата (`sp_register_user`) ====
     314* '''Зошто постои во реален свет:''' Кога се регистрирате на стриминг сервис, тоа не е обичен единечен `INSERT`. Мора истовремено да се креира корисничкиот профил, да му се креира инстанца за претплата во `User_Subscription` и да му се постави почетен рок од 30 дена. Оваа процедура го извршува тоа како една атомска операција.
     315{{{
     316#!sql
     317CREATE OR REPLACE PROCEDURE sp_register_user(
     318  p_first_name   VARCHAR,
     319  p_last_name    VARCHAR,
     320  p_username     VARCHAR,
     321  p_email        VARCHAR,
     322  p_password     VARCHAR,
     323  p_subscription_id INT
     324)
     325LANGUAGE plpgsql AS $$
     326DECLARE
     327  v_user_id INT;
     328BEGIN
     329  INSERT INTO "User" (FirstName, LastName, Username, Email, password, Date_registered)
     330  VALUES (p_first_name, p_last_name, p_username, p_email, p_password, CURRENT_DATE)
     331  RETURNING UserID INTO v_user_id;
     332
     333  INSERT INTO User_Subscription (UserUserID, SubscriptionSubscriptionID, Start_date, End_date, Status, Auto_renew)
     334  VALUES (v_user_id, p_subscription_id, CURRENT_DATE, CURRENT_DATE + INTERVAL '30 days', 'Active', 1);
     335
     336  RAISE NOTICE 'Korisnikot % % uspeshno registriran so UserID = %', p_first_name, p_last_name, v_user_id;
     337
     338EXCEPTION
     339  WHEN unique_violation THEN
     340    RAISE EXCEPTION 'Email % veke postoi vo sistemot.', p_email;
     341  WHEN foreign_key_violation THEN
     342    RAISE EXCEPTION 'Subscription so ID % ne postoi.', p_subscription_id;
     343END;
     344$$;
     345}}}
     346
     347==== 2. Додавање во watchlist со заштита од дупликати (`sp_add_to_watchlist`) ====
     348* '''Зошто постои во реален свет:''' Наместо апликацијата прво да прави `SELECT` за да провери дали корисникот веќе го додал филмот во "Моја Листа", процедурата на ниво на база ја врши таа проверка и спречува појава на дупликат записи, оптимизирајќи ги перформансите на серверот.
     349{{{
     350#!sql
     351CREATE OR REPLACE PROCEDURE sp_add_to_watchlist(
     352  p_user_id    INT,
     353  p_content_id INT
     354)
     355LANGUAGE plpgsql AS $$
     356DECLARE
     357  v_exists INT;
     358BEGIN
     359  SELECT COUNT(*) INTO v_exists
     360  FROM Watchlist
     361  WHERE UserUserID = p_user_id AND ContentContentID = p_content_id;
     362
     363  IF v_exists > 0 THEN
     364    RAISE NOTICE 'Sodrzhinata veke e vo watchlist-ot na korisnikot.';
     365    RETURN;
     366  END IF;
     367
     368  IF NOT EXISTS (SELECT 1 FROM Media WHERE ContentID = p_content_id) THEN
     369    RAISE EXCEPTION 'Sodrzhinata so ID % ne postoi.', p_content_id;
     370  END IF;
     371
     372  INSERT INTO Watchlist (UserUserID, ContentContentID, dateAdded)
     373  VALUES (p_user_id, p_content_id, CURRENT_DATE);
     374
     375  RAISE NOTICE 'Sodrzhinata % dodadena vo watchlist za korisnik %.', p_content_id, p_user_id;
     376
     377EXCEPTION
     378  WHEN foreign_key_violation THEN
     379    RAISE EXCEPTION 'Korisnik % ili sodrzina % ne postoi.', p_user_id, p_content_id;
     380END;
     381$$;
     382}}}
     383
     384=== Имплементирани од Дамјан Димовски ===
     385
     386==== 3. Снимање на кориснички прогрес и рејтинг нотификација (`sp_record_watch`) ====
     387* '''Зошто постои во реален свет:''' При секое стиснување на копчето пауза или при исклучување на видеото, апликацијата во позадина го праќа моменталниот тренд на гледање. Оваа процедура го запишува тоа во `WatchHistory`, соработува со тригерот за 90% комплетирање, и доколку корисникот го изгледал филмот, испраќа известување за добивање рејтинг (точно како познатите поп-ап прозорци на стриминг сервисите).
     388{{{
     389#!sql
     390CREATE OR REPLACE PROCEDURE sp_record_watch(
     391  p_user_id      INT,
     392  p_content_id   INT,
     393  p_watchable_id INT,
     394  p_device_id    INT,
     395  p_progress     INT
     396)
     397LANGUAGE plpgsql AS $$
     398BEGIN
     399  IF p_progress < 0 OR p_progress > 100 THEN
     400    RAISE EXCEPTION 'Progress mora da bide pomegu 0 i 100.';
     401  END IF;
     402
     403  INSERT INTO WatchHistory (WatchedAt, Progress_percentage, UserUserID, ContentContentID, WatchableWatchableID, DevicesDeviceID)
     404  VALUES (CURRENT_DATE, p_progress, p_user_id, p_content_id, p_watchable_id, p_device_id);
     405
     406  IF p_progress >= 90 THEN
     407    IF NOT EXISTS (
     408      SELECT 1 FROM Rating
     409      WHERE UserUserID = p_user_id AND ContentContentID = p_content_id
     410    ) THEN
     411      RAISE NOTICE 'Korisnik % ja zavrsil sodrzhinata %. Pokanete go da ostavi rejting.', p_user_id, p_content_id;
     412    END IF;
     413  END IF;
     414
     415EXCEPTION
     416  WHEN foreign_key_violation THEN
     417    RAISE EXCEPTION 'Nevaliden user, content, watchable ili device ID.';
     418END;
     419$$;
     420}}}
     421
     422==== 4. Паметно поднесување и ажурирање на рејтинг (`sp_submit_rating`) ====
     423* '''Зошто постои во реален свет:''' Оваа процедура воведува две клучни деловни правила: Прво, спречува корисникот да остави оцена за филм кој воопшто никогаш не го пуштил (нема запис во `WatchHistory`). Второ, таа е паметна (Upsert логика) - ако корисникот веќе оценил, ќе се изврши `UPDATE`, а ако оценува за првпат, се прави нов `INSERT`.
     424{{{
     425#!sql
     426CREATE OR REPLACE PROCEDURE sp_submit_rating(
     427  p_user_id    INT,
     428  p_content_id INT,
     429  p_rating     INT
     430)
     431LANGUAGE plpgsql AS $$
     432DECLARE
     433  v_watched INT;
     434BEGIN
     435  SELECT COUNT(*) INTO v_watched
     436  FROM WatchHistory
     437  WHERE UserUserID = p_user_id AND ContentContentID = p_content_id;
     438
     439  IF v_watched = 0 THEN
     440    RAISE EXCEPTION 'Korisnikot ne ja gledal sodrzhinata i ne moze da ostavi rejting.';
     441  END IF;
     442
     443  IF EXISTS (SELECT 1 FROM Rating WHERE UserUserID = p_user_id AND ContentContentID = p_content_id) THEN
     444    UPDATE Rating
     445    SET RatingValue = p_rating, Rating_Date = CURRENT_DATE
     446    WHERE UserUserID = p_user_id AND ContentContentID = p_content_id;
     447
     448    RAISE NOTICE 'Rejtingot azhurirani na % za sodrzina %.', p_rating, p_content_id;
     449  ELSE
     450    INSERT INTO Rating (Rating_Date, RatingValue, UserUserID, ContentContentID)
     451    VALUES (CURRENT_DATE, p_rating, p_user_id, p_content_id);
     452
     453    RAISE NOTICE 'Nov rejting % vnesen za sodrzina %.', p_rating, p_content_id;
     454  END IF;
     455
     456EXCEPTION
     457  WHEN check_violation THEN
     458    RAISE EXCEPTION 'Rejtingot mora da bide pomegu 1 i 10.';
     459END;
     460$$;
     461}}}
     462
     463==== 5. Комплетна деактивирање на кориснички профил (`sp_deactivate_user`) ====
     464* '''Зошто постои во реален свет:''' Кога корисникот ќе избере опција "Откажи ја сметката", процесот во заднина не смее да остави сирачиња во базата (orphan records). Оваа процедура ги исклучува сите негови активни претплати во `User_Subscription`, ги исфрла и брише сите негови активни сесии на уреди од табелата `Devices` и прави безбедно затворање на сметката.
     465{{{
     466#!sql
     467CREATE OR REPLACE PROCEDURE sp_deactivate_user(
     468  p_user_id INT
     469)
     470LANGUAGE plpgsql AS $$
     471DECLARE
     472  v_count INT;
     473BEGIN
     474  IF NOT EXISTS (SELECT 1 FROM "User" WHERE UserID = p_user_id) THEN
     475    RAISE EXCEPTION 'Korisnik so ID % ne postoi.', p_user_id;
     476  END IF;
     477
     478  SELECT COUNT(*) INTO v_count
     479  FROM User_Subscription
     480  WHERE UserUserID = p_user_id AND Status = 'Active';
     481
     482  UPDATE User_Subscription
     483  SET Status = 'Cancelled', End_date = CURRENT_DATE
     484  WHERE UserUserID = p_user_id AND Status = 'Active';
     485
     486  DELETE FROM Devices
     487  WHERE UserSubscriptionID IN (
     488    SELECT UserSubscriptionID FROM User_Subscription
     489    WHERE UserUserID = p_user_id
     490  );
     491
     492  RAISE NOTICE 'Korisnik % deaktiviran. % pretplati otkazani, uredite izbrishani.', p_user_id, v_count;
     493
     494EXCEPTION
     495  WHEN OTHERS THEN
     496    RAISE EXCEPTION 'Greshka pri deaktiviranje na korisnik %: %', p_user_id, SQLERRM;
     497END;
     498$$;
     499}}}
     500
     501Целосниот код за наште тригери,процедури и функции може да го погледнете тука: [attachment:console_12.sql console_12.sql] [attachment:console_13.sql console_13.sql]