Changes between Initial Version and Version 1 of AdvancedDatabaseDevelopment


Ignore:
Timestamp:
09/16/26 19:28:40 (12 days ago)
Author:
233149
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedDatabaseDevelopment

    v1 v1  
     1== Напреден развој на базата
     2
     3Сите објекти опишани подолу се во скриптата [attachment:advanced.sql], која се
     4стартува по schema_creation.sql и data_load.sql. Скриптата е повторлива - секој
     5објект се креира со OR REPLACE или претходно се брише.
     6
     7Основните ограничувања на ниво на колона и референцијалниот интегритет се
     8документирани во Фаза 2. Овде се опишани само оние правила кои '''не можат''' да
     9се изразат со обично ограничување.
     10
     11== 1. Повеќе факултети во иста база
     12
     13=== Опис на барањето
     14
     15Системот е замислен да опслужува повеќе факултети од ист универзитет. Секој
     16факултет има свои студенти, професори, предмети, студиски програми и активни
     17семестри. Податоците на еден факултет не смеат да бидат видливи ниту достапни од
     18друг факултет, а притоа сите работат врз иста база и иста шема.
     19
     20Ова носи три проблеми кои не се решаваат со надворешни клучеви:
     21
     22 * секое барање мора автоматски да биде ограничено на тековниот факултет, без секој SELECT да мора да памети WHERE faculty_id = ...
     23 * шифрата на предметот е уникатна '''во рамки на факултет''', а не глобално - два факултета смеат да имаат предмет со иста шифра
     24 * запишувањето мора да поврзе студент, студиска програма и семестар кои '''сите припаѓаат на ист факултет''' - надворешен клуч може да провери дека секој од нив постои, но не и дека се од ист факултет
     25
     26=== Имплементација
     27
     28'''Табела и колона за наемател (tenant)'''
     29
     30{{{#!sql
     31CREATE TABLE faculty (
     32    id         INTEGER      GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
     33    short_name VARCHAR(20)  NOT NULL UNIQUE,
     34    full_name  VARCHAR(200) NOT NULL,
     35    university VARCHAR(200) NOT NULL
     36);
     37
     38ALTER TABLE users            ADD COLUMN faculty_id INTEGER NOT NULL DEFAULT 1 REFERENCES faculty (id);
     39ALTER TABLE major            ADD COLUMN faculty_id INTEGER NOT NULL DEFAULT 1 REFERENCES faculty (id);
     40ALTER TABLE subjects         ADD COLUMN faculty_id INTEGER NOT NULL DEFAULT 1 REFERENCES faculty (id);
     41ALTER TABLE active_semesters ADD COLUMN faculty_id INTEGER NOT NULL DEFAULT 1 REFERENCES faculty (id);
     42ALTER TABLE documents        ADD COLUMN faculty_id INTEGER NOT NULL DEFAULT 1 REFERENCES faculty (id);
     43}}}
     44
     45Колоната faculty_id се додава само на кореновите ентитети. Сите останати табели
     46го наследуваат факултетот преку нив, па не се повторува насекаде.
     47
     48'''Уникатност во рамки на факултет'''
     49
     50{{{#!sql
     51ALTER TABLE subjects DROP CONSTRAINT subjects_code_key;
     52ALTER TABLE subjects DROP CONSTRAINT subjects_name_key;
     53
     54CREATE UNIQUE INDEX subjects_faculty_code_key ON subjects (faculty_id, code);
     55CREATE UNIQUE INDEX subjects_faculty_name_key ON subjects (faculty_id, name);
     56}}}
     57
     58'''Функција и процедура за тековен факултет'''
     59
     60{{{#!sql
     61CREATE OR REPLACE FUNCTION current_faculty()
     62    RETURNS INTEGER
     63    LANGUAGE plpgsql STABLE
     64AS $$
     65BEGIN
     66    RETURN nullif(current_setting('app.current_faculty', TRUE), '')::INTEGER;
     67EXCEPTION
     68    WHEN others THEN RETURN NULL;
     69END;
     70$$;
     71
     72CREATE OR REPLACE PROCEDURE set_current_faculty(p_faculty INTEGER)
     73    LANGUAGE plpgsql
     74AS $$
     75BEGIN
     76    IF p_faculty IS NOT NULL AND NOT EXISTS (SELECT 1 FROM faculty WHERE id = p_faculty) THEN
     77        RAISE EXCEPTION 'Faculty % does not exist', p_faculty;
     78    END IF;
     79    PERFORM set_config('app.current_faculty', coalesce(p_faculty::TEXT, ''), FALSE);
     80END;
     81$$;
     82}}}
     83
     84'''Политики за безбедност на ниво на редица (Row Level Security)'''
     85
     86{{{#!sql
     87ALTER TABLE users ENABLE ROW LEVEL SECURITY;
     88
     89CREATE POLICY tenant_isolation ON users
     90    USING (current_faculty() IS NULL OR faculty_id = current_faculty());
     91}}}
     92
     93Истата политика се поставува и врз major, subjects, active_semesters и documents.
     94Кога апликацијата ќе повика {{{CALL set_current_faculty(2)}}}, сите барања во таа
     95сесија автоматски гледаат само податоци од факултет 2, без ниту една измена во
     96самите SQL барања.
     97
     98'''Забелешка:''' сопственикот на табелата ги заобиколува политиките, освен ако не
     99се употреби FORCE ROW LEVEL SECURITY. Затоа во продукција апликацијата се
     100поврзува со посебна улога која не е сопственик на шемата.
     101
     102'''Тригер за интегритет меѓу факултети'''
     103
     104{{{#!sql
     105CREATE OR REPLACE FUNCTION check_enrolment_same_faculty()
     106    RETURNS TRIGGER
     107    LANGUAGE plpgsql
     108AS $$
     109DECLARE
     110    f_student  INTEGER;
     111    f_major    INTEGER;
     112    f_semester INTEGER;
     113BEGIN
     114    SELECT faculty_id INTO f_student  FROM users            WHERE id = NEW.user_id;
     115    SELECT faculty_id INTO f_major    FROM major            WHERE id = NEW.major_id;
     116    SELECT faculty_id INTO f_semester FROM active_semesters WHERE id = NEW.semester_id;
     117
     118    IF f_student IS DISTINCT FROM f_major OR f_student IS DISTINCT FROM f_semester THEN
     119        RAISE EXCEPTION
     120            'Cross-faculty enrolment: student belongs to faculty %, major to %, semester to %',
     121            f_student, f_major, f_semester;
     122    END IF;
     123    RETURN NEW;
     124END;
     125$$;
     126
     127CREATE TRIGGER trg_enrolment_same_faculty
     128    BEFORE INSERT OR UPDATE ON enrolled_semesters
     129    FOR EACH ROW EXECUTE FUNCTION check_enrolment_same_faculty();
     130}}}
     131
     132== 2. Правила за запишување семестар
     133
     134=== Опис на барањето
     135
     136Запишувањето на семестар има четири правила кои базата треба сама да ги чува, без
     137да зависи од тоа дали апликацијата ќе ги провери:
     138
     139 * предметот мора да биде понуден од студиската програма на запишувањето
     140 * професорот мора навистина да го предава тој предмет во тој семестар
     141 * предусловите на предметот мора да бидат положени
     142 * запишувањето смее да има најмногу 5 предмети и најмногу 30 кредити
     143
     144Ниту едно од овие не може да се напише како CHECK ограничување, бидејќи сите
     145бараат читање од други табели.
     146
     147=== Имплементација - тригери
     148
     149'''Предметот мора да е во студиската програма'''
     150
     151{{{#!sql
     152CREATE OR REPLACE FUNCTION check_subject_in_major()
     153    RETURNS TRIGGER
     154    LANGUAGE plpgsql
     155AS $$
     156DECLARE
     157    v_major INTEGER;
     158BEGIN
     159    SELECT major_id INTO v_major FROM enrolled_semesters WHERE id = NEW.enrolled_semesters_id;
     160
     161    IF NOT EXISTS (SELECT 1 FROM major_subjects
     162                    WHERE major_id = v_major AND subject_id = NEW.subjects_id) THEN
     163        RAISE EXCEPTION 'Subject % is not offered by study programme %',
     164            NEW.subjects_id, v_major;
     165    END IF;
     166    RETURN NEW;
     167END;
     168$$;
     169
     170CREATE TRIGGER trg_subject_in_major
     171    BEFORE INSERT OR UPDATE ON semesters_subjects
     172    FOR EACH ROW EXECUTE FUNCTION check_subject_in_major();
     173}}}
     174
     175'''Професорот мора да го предава предметот тој семестар'''
     176
     177{{{#!sql
     178CREATE OR REPLACE FUNCTION check_professor_teaches()
     179    RETURNS TRIGGER
     180    LANGUAGE plpgsql
     181AS $$
     182DECLARE
     183    v_semester INTEGER;
     184    v_role     user_role;
     185BEGIN
     186    SELECT semester_id INTO v_semester FROM enrolled_semesters WHERE id = NEW.enrolled_semesters_id;
     187    SELECT role        INTO v_role     FROM users              WHERE id = NEW.professor_id;
     188
     189    IF v_role <> 'prof' THEN
     190        RAISE EXCEPTION 'User % is not a professor', NEW.professor_id;
     191    END IF;
     192
     193    IF NOT EXISTS (SELECT 1 FROM professour_subjects
     194                    WHERE prof_id = NEW.professor_id
     195                      AND subject_id = NEW.subjects_id
     196                      AND active_semester_id = v_semester) THEN
     197        RAISE EXCEPTION 'Professor % does not teach subject % in semester %',
     198            NEW.professor_id, NEW.subjects_id, v_semester;
     199    END IF;
     200    RETURN NEW;
     201END;
     202$$;
     203
     204CREATE TRIGGER trg_professor_teaches
     205    BEFORE INSERT OR UPDATE ON semesters_subjects
     206    FOR EACH ROW EXECUTE FUNCTION check_professor_teaches();
     207}}}
     208
     209'''Предусловите мора да се положени'''
     210
     211{{{#!sql
     212CREATE OR REPLACE FUNCTION check_prerequisites_passed()
     213    RETURNS TRIGGER
     214    LANGUAGE plpgsql
     215AS $$
     216DECLARE
     217    v_user    INTEGER;
     218    v_missing TEXT;
     219BEGIN
     220    SELECT user_id INTO v_user FROM enrolled_semesters WHERE id = NEW.enrolled_semesters_id;
     221
     222    SELECT string_agg(s.code, ', ' ORDER BY s.code) INTO v_missing
     223    FROM dependency_subject d
     224    JOIN subjects s ON s.id = d.dependency_id
     225    WHERE d.subject_id = NEW.subjects_id
     226      AND NOT EXISTS (
     227          SELECT 1
     228          FROM passed_subjects ps
     229          JOIN semesters_subjects ss ON ss.id = ps.enrolled_id
     230          JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
     231          WHERE es.user_id = v_user AND ss.subjects_id = d.dependency_id);
     232
     233    IF v_missing IS NOT NULL THEN
     234        RAISE EXCEPTION 'Prerequisite(s) not passed for subject %: %',
     235            NEW.subjects_id, v_missing;
     236    END IF;
     237    RETURN NEW;
     238END;
     239$$;
     240
     241CREATE TRIGGER trg_prerequisites_passed
     242    BEFORE INSERT ON semesters_subjects
     243    FOR EACH ROW EXECUTE FUNCTION check_prerequisites_passed();
     244}}}
     245
     246'''Одложен тригер за големина на запишувањето'''
     247
     248Ова правило е поставено како '''одложен''' (DEFERRABLE INITIALLY DEFERRED)
     249тригер, што значи дека се проверува дури при потврда на трансакцијата, а не по
     250секој ред. Така апликацијата може да ги внесе петте предмети еден по еден, или да
     251замени еден предмет со друг во иста трансакција, без правилото да се прекрши на
     252средина од работата.
     253
     254{{{#!sql
     255CREATE OR REPLACE FUNCTION check_enrolment_size()
     256    RETURNS TRIGGER
     257    LANGUAGE plpgsql
     258AS $$
     259DECLARE
     260    v_enrolment INTEGER := coalesce(NEW.enrolled_semesters_id, OLD.enrolled_semesters_id);
     261    v_count     INTEGER;
     262    v_credits   INTEGER;
     263BEGIN
     264    SELECT count(*), coalesce(sum(s.awarded_credits), 0)
     265      INTO v_count, v_credits
     266    FROM semesters_subjects ss
     267    JOIN subjects s ON s.id = ss.subjects_id
     268    WHERE ss.enrolled_semesters_id = v_enrolment;
     269
     270    IF v_count > 5 THEN
     271        RAISE EXCEPTION 'Enrolment % has % subjects; the maximum is 5', v_enrolment, v_count;
     272    END IF;
     273
     274    IF v_credits > 30 THEN
     275        RAISE EXCEPTION 'Enrolment % has % credits; the maximum is 30', v_enrolment, v_credits;
     276    END IF;
     277
     278    RETURN NULL;
     279END;
     280$$;
     281
     282CREATE CONSTRAINT TRIGGER trg_enrolment_size
     283    AFTER INSERT OR UPDATE OR DELETE ON semesters_subjects
     284    DEFERRABLE INITIALLY DEFERRED
     285    FOR EACH ROW EXECUTE FUNCTION check_enrolment_size();
     286}}}
     287
     288== 3. Правила за оценување и известувања
     289
     290=== Опис на барањето
     291
     292 * оценка не смее да има датум во иднина
     293 * оценка смее да внесе само професорот кој го предава тој предмет на тој студент
     294 * положен предмет автоматски значи дека има потпис
     295 * студентот треба веднаш да добие известување кога ќе биде оценет
     296
     297Втората точка е проверка на овластување во самата база. Апликацијата и онака ја
     298прави истата проверка, но доколку некој пристапи до базата директно, правилото
     299пак важи.
     300
     301=== Имплементација - тригери
     302
     303{{{#!sql
     304CREATE OR REPLACE FUNCTION check_grade_rules()
     305    RETURNS TRIGGER
     306    LANGUAGE plpgsql
     307AS $$
     308DECLARE
     309    v_professor INTEGER;
     310    v_actor     INTEGER;
     311BEGIN
     312    IF NEW.date_passed > now() THEN
     313        RAISE EXCEPTION 'A grade cannot be dated in the future (%)', NEW.date_passed;
     314    END IF;
     315
     316    SELECT professor_id INTO v_professor
     317    FROM semesters_subjects WHERE id = NEW.enrolled_id;
     318
     319    v_actor := nullif(current_setting('app.current_user_id', TRUE), '')::INTEGER;
     320    IF v_actor IS NOT NULL AND v_actor <> v_professor THEN
     321        RAISE EXCEPTION 'User % may not grade this subject; it is taught by %',
     322            v_actor, v_professor;
     323    END IF;
     324
     325    UPDATE semesters_subjects SET signature = TRUE WHERE id = NEW.enrolled_id;
     326
     327    RETURN NEW;
     328END;
     329$$;
     330
     331CREATE TRIGGER trg_grade_rules
     332    BEFORE INSERT OR UPDATE ON passed_subjects
     333    FOR EACH ROW EXECUTE FUNCTION check_grade_rules();
     334}}}
     335
     336Проверката на овластување се активира '''само''' кога апликацијата ќе ѝ каже на
     337базата кој работи, преку {{{SET app.current_user_id}}}. Така скриптите за
     338одржување и вчитување податоци не се блокирани, а вистинските барања од
     339апликацијата се проверени и на ниво на база.
     340
     341'''Известување на студентот'''
     342
     343{{{#!sql
     344CREATE TABLE notification (
     345    id         INTEGER      GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
     346    user_id    INTEGER      NOT NULL REFERENCES users (id),
     347    kind       VARCHAR(40)  NOT NULL,
     348    message    TEXT         NOT NULL,
     349    is_read    BOOLEAN      NOT NULL DEFAULT FALSE,
     350    created_at TIMESTAMP    NOT NULL DEFAULT now()
     351);
     352
     353CREATE OR REPLACE FUNCTION notify_student_graded()
     354    RETURNS TRIGGER
     355    LANGUAGE plpgsql
     356AS $$
     357DECLARE
     358    v_user    INTEGER;
     359    v_subject TEXT;
     360BEGIN
     361    SELECT es.user_id, s.name
     362      INTO v_user, v_subject
     363    FROM semesters_subjects ss
     364    JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
     365    JOIN subjects s            ON s.id  = ss.subjects_id
     366    WHERE ss.id = NEW.enrolled_id;
     367
     368    INSERT INTO notification (user_id, kind, message)
     369    VALUES (v_user, 'grade',
     370            format('Добивте оценка %s по предметот %s.', NEW.grade, v_subject));
     371    RETURN NEW;
     372END;
     373$$;
     374
     375CREATE TRIGGER trg_notify_graded
     376    AFTER INSERT ON passed_subjects
     377    FOR EACH ROW EXECUTE FUNCTION notify_student_graded();
     378}}}
     379
     380== 4. Прегледи (views)
     381
     382=== Опис на барањето
     383
     384Најчестите извештаи во апликацијата бараат спојување на пет до шест табели.
     385Наместо истото барање да се повторува на повеќе места во кодот, тие се дефинирани
     386како прегледи во базата.
     387
     388=== Имплементација
     389
     390'''Уверение за положени испити'''
     391
     392{{{#!sql
     393CREATE OR REPLACE VIEW v_student_transcript AS
     394SELECT es.user_id,
     395       u."index"                    AS student_index,
     396       u.name || ' ' || u.surname   AS student,
     397       s.code                       AS subject_code,
     398       s.name                       AS subject,
     399       s.awarded_credits,
     400       ps.grade,
     401       ps.date_passed,
     402       p.name || ' ' || p.surname   AS professor,
     403       a.year,
     404       a.type                       AS semester_type
     405FROM passed_subjects ps
     406JOIN semesters_subjects ss ON ss.id = ps.enrolled_id
     407JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
     408JOIN users u               ON u.id  = es.user_id
     409JOIN subjects s            ON s.id  = ss.subjects_id
     410JOIN users p               ON p.id  = ss.professor_id
     411JOIN active_semesters a    ON a.id  = es.semester_id;
     412}}}
     413
     414'''Состојба на студентот - кредити и просек'''
     415
     416{{{#!sql
     417CREATE OR REPLACE VIEW v_student_standing AS
     418SELECT u.id                                          AS user_id,
     419       u."index"                                     AS student_index,
     420       u.name || ' ' || u.surname                    AS student,
     421       count(ps.id)                                  AS passed_subjects,
     422       coalesce(sum(s.awarded_credits), 0)           AS credits,
     423       round(avg(ps.grade::TEXT::INT)::NUMERIC, 2)   AS average_grade
     424FROM users u
     425LEFT JOIN enrolled_semesters es ON es.user_id = u.id
     426LEFT JOIN semesters_subjects ss ON ss.enrolled_semesters_id = es.id
     427LEFT JOIN passed_subjects ps    ON ps.enrolled_id = ss.id
     428LEFT JOIN subjects s            ON s.id = ss.subjects_id AND ps.id IS NOT NULL
     429WHERE u.role = 'student'
     430GROUP BY u.id, u."index", u.name, u.surname;
     431}}}
     432
     433Двојното претворање {{{ps.grade::TEXT::INT}}} е потребно бидејќи grade е
     434набројувачки тип, па не може директно да влезе во avg().
     435
     436'''Дневник на професорот''' и '''покриеност на семестарот'''
     437
     438{{{#!sql
     439CREATE OR REPLACE VIEW v_professor_gradebook AS
     440SELECT ss.professor_id,
     441       p.name || ' ' || p.surname   AS professor,
     442       s.code                       AS subject_code,
     443       s.name                       AS subject,
     444       a.year, a.type               AS semester_type,
     445       u.id                         AS student_id,
     446       u."index"                    AS student_index,
     447       u.name || ' ' || u.surname   AS student,
     448       ss.signature,
     449       ps.grade
     450FROM semesters_subjects ss
     451JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
     452JOIN users u               ON u.id  = es.user_id
     453JOIN users p               ON p.id  = ss.professor_id
     454JOIN subjects s            ON s.id  = ss.subjects_id
     455JOIN active_semesters a    ON a.id  = es.semester_id
     456LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id;
     457
     458CREATE OR REPLACE VIEW v_semester_coverage AS
     459SELECT a.id AS semester_id, a.year, a.type AS semester_type,
     460       s.id AS subject_id, s.code AS subject_code, s.name AS subject
     461FROM active_semesters a
     462CROSS JOIN subjects s
     463WHERE NOT EXISTS (SELECT 1 FROM professour_subjects ps
     464                   WHERE ps.active_semester_id = a.id AND ps.subject_id = s.id);
     465}}}
     466
     467Прегледот v_semester_coverage покажува кои предмети никој не ги предава во даден
     468семестар. Тоа е точно списокот кој администраторот мора да го исчисти, бидејќи
     469студент не може да запише предмет без професор.
     470
     471== 5. Статистика по предмети (материјализиран преглед)
     472
     473=== Опис на барањето
     474
     475Статистиката за проодност по предмет се пресметува преку сите запишувања и сите
     476оценки. Тоа е скапо барање кое не се менува од минута во минута, па се чува
     477материјализирано и се освежува еднаш дневно.
     478
     479=== Имплементација
     480
     481{{{#!sql
     482CREATE MATERIALIZED VIEW mv_subject_statistics AS
     483SELECT s.id                                                     AS subject_id,
     484       s.code                                                   AS subject_code,
     485       s.name                                                   AS subject,
     486       count(ss.id)                                             AS enrolled_count,
     487       count(ps.id)                                             AS passed_count,
     488       round(100.0 * count(ps.id) / nullif(count(ss.id), 0), 1) AS pass_rate,
     489       round(avg(ps.grade::TEXT::INT)::NUMERIC, 2)              AS average_grade
     490FROM subjects s
     491LEFT JOIN semesters_subjects ss ON ss.subjects_id = s.id
     492LEFT JOIN passed_subjects ps    ON ps.enrolled_id = ss.id
     493GROUP BY s.id, s.code, s.name;
     494
     495CREATE UNIQUE INDEX mv_subject_statistics_pk ON mv_subject_statistics (subject_id);
     496}}}
     497
     498Уникатниот индекс не е само оптимизација - тој е услов за да може прегледот да се
     499освежува со REFRESH MATERIALIZED VIEW CONCURRENTLY, односно без да се заклучи за
     500читање додека трае освежувањето.
     501
     502== 6. Ноќно одржување (background jobs)
     503
     504=== Опис на барањето
     505
     506Четири работи треба да се случуваат периодично, без некој да ги повика рачно:
     507
     508 * истечените токени за освежување да се поништат
     509 * запишување во кое сите предмети се положени да се затвори
     510 * студентите да добијат потсетник за предмети без потпис
     511 * статистиката да се пресмета одново
     512
     513=== Имплементација - процедури
     514
     515{{{#!sql
     516CREATE OR REPLACE PROCEDURE expire_old_tokens()
     517    LANGUAGE plpgsql
     518AS $$
     519DECLARE
     520    v_expired INTEGER;
     521BEGIN
     522    UPDATE token SET is_valid = FALSE
     523    WHERE is_valid = TRUE AND expires_at < now();
     524
     525    GET DIAGNOSTICS v_expired = ROW_COUNT;
     526    RAISE NOTICE 'expire_old_tokens: invalidated % token(s)', v_expired;
     527END;
     528$$;
     529
     530CREATE OR REPLACE PROCEDURE close_completed_enrolments()
     531    LANGUAGE plpgsql
     532AS $$
     533DECLARE
     534    v_closed INTEGER;
     535BEGIN
     536    WITH finished AS (
     537        SELECT es.id
     538        FROM enrolled_semesters es
     539        JOIN semesters_subjects ss ON ss.enrolled_semesters_id = es.id
     540        LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id
     541        WHERE es.completed IS NULL
     542        GROUP BY es.id
     543        HAVING count(ss.id) > 0 AND count(ss.id) = count(ps.id)
     544    )
     545    UPDATE enrolled_semesters es
     546    SET completed = now(), last_change = now()
     547    FROM finished f
     548    WHERE es.id = f.id;
     549
     550    GET DIAGNOSTICS v_closed = ROW_COUNT;
     551    RAISE NOTICE 'close_completed_enrolments: closed % enrolment(s)', v_closed;
     552END;
     553$$;
     554}}}
     555
     556Процедурата notify_missing_signatures создава известување за секој предмет без
     557потпис, но само ако таков потсетник сè уште не постои, за да не се праќа истото
     558известување секоја ноќ.
     559
     560Сите четири се повикуваат од една влезна точка, која потоа ја стартува
     561распоредувачот (pg_cron или закажана задача на оперативниот систем):
     562
     563{{{#!sql
     564CREATE OR REPLACE PROCEDURE run_nightly_maintenance()
     565    LANGUAGE plpgsql
     566AS $$
     567BEGIN
     568    CALL expire_old_tokens();
     569    CALL close_completed_enrolments();
     570    CALL notify_missing_signatures();
     571    CALL refresh_statistics();
     572    RAISE NOTICE 'run_nightly_maintenance: done';
     573END;
     574$$;
     575}}}
     576
     577== 7. Функции за извештаи
     578
     579Функцијата student_eligible_subjects враќа табела со сите предмети од студиската
     580програма на студентот, и за секој кажува дали е веќе положен, дали предусловите
     581се исполнети и дали некој го предава во бараниот семестар. Екранот за запишување
     582со еден повик добива сè што му треба, наместо да собира три различни барања.
     583
     584{{{#!sql
     585SELECT * FROM student_eligible_subjects(1, 3);
     586}}}
     587
     588Функцијата student_summary враќа еден ред со бројот на положени предмети,
     589вкупните кредити и просекот на еден студент.
     590
     591== 8. Сопствени домени
     592
     593=== Опис на барањето
     594
     595Проверките на формат се повторуваат на повеќе места: е-пошта има две колони
     596(users.email и contact.microsoft_email), износ има две (payment.amount и
     597documents.cost). Наместо истиот CHECK да се пишува на секое место, правилото се
     598дефинира еднаш како домен и потоа се употребува како тип.
     599
     600=== Имплементација
     601
     602{{{#!sql
     603CREATE DOMAIN email_address AS VARCHAR(150)
     604    CHECK (VALUE ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
     605
     606CREATE DOMAIN embg_number AS VARCHAR(13)
     607    CHECK (VALUE ~ '^[0-9]{13}$');
     608
     609CREATE DOMAIN student_index AS VARCHAR(20)
     610    CHECK (VALUE ~ '^[0-9]{6}$');
     611
     612CREATE DOMAIN phone_number AS VARCHAR(20)
     613    CHECK (VALUE ~ '^[0-9+][0-9 /-]{5,19}$');
     614
     615CREATE DOMAIN money_amount AS INTEGER
     616    CHECK (VALUE >= 0);
     617
     618CREATE DOMAIN gpa_value AS REAL
     619    CHECK (VALUE >= 2.0 AND VALUE <= 5.0);
     620
     621CREATE DOMAIN credit_points AS INTEGER
     622    CHECK (VALUE > 0 AND VALUE <= 30);
     623}}}
     624
     625'''Афектирани колони:'''
     626
     627{{{#!sql
     628ALTER TABLE users       ALTER COLUMN email           TYPE email_address;
     629ALTER TABLE users       ALTER COLUMN embg            TYPE embg_number;
     630ALTER TABLE users       ALTER COLUMN "index"         TYPE student_index;
     631ALTER TABLE contact     ALTER COLUMN microsoft_email TYPE email_address;
     632ALTER TABLE contact     ALTER COLUMN number          TYPE phone_number;
     633ALTER TABLE payment     ALTER COLUMN amount          TYPE money_amount;
     634ALTER TABLE documents   ALTER COLUMN cost            TYPE money_amount;
     635ALTER TABLE high_school ALTER COLUMN gpa             TYPE gpa_value;
     636ALTER TABLE subjects    ALTER COLUMN awarded_credits TYPE credit_points;
     637}}}
     638
     639== Користење на вештачка интелигенција
     640
     641 [wiki:AdvancedDatabaseDevelopmentAIUsage AdvancedDatabaseDevelopmentAIUsage]
     642
     643== Историјат
     644
     645 '''Верзија 1''' - Прва верзија: домени, повеќе факултети со политики на ниво на
     646 редица, тригери за правилата на запишување и оценување, прегледи, материјализиран
     647 преглед за статистика и процедури за ноќно одржување.
     648
     649== Статус
     650
     651 ''' [[span(style=color: #FF8000, Во тек )]] '''