== Напредни PostgreSQL компоненти **1) Интегритетни ограничувања (CHECK/UNIQUE)** **1.1 Reaction: различни елементи** Цел: да не се креира реакција со ист елемент двапати. {{{ ALTER TABLE reaction ADD CONSTRAINT reaction_distinct_elements CHECK (element1_id <> element2_id); }}} Каде се користи: DatabaseManager.add_reaction(...) и update_reaction(...) (backend): пред-валидација и фаќање CheckViolation. **1.2 Elements: физички валидни вредности** Цел: основна научна валидност за елементите. {{{ ALTER TABLE elements ADD CONSTRAINT elements_atomic_number_positive CHECK (atomic_number > 0); ALTER TABLE elements ADD CONSTRAINT elements_mass_positive CHECK (atomic_weight > 0); ALTER TABLE elements ADD CONSTRAINT elements_melting_before_boiling CHECK (boiling_point IS NULL OR melting_point IS NULL OR melting_point < boiling_point); }}} Каде се користи: DatabaseManager.add_element(...) / update_element(...): фаќа CheckViolation и враќа јасна порака. **1.3 Партиципација: без дупликати** Цел: еден корисник да не „учествува“ двапати во истиот експеримент. {{{ ALTER TABLE userparticipatesinexperiment ADD CONSTRAINT uq_user_experiment UNIQUE (user_id, experiment_id); }}} Каде се користи: DatabaseManager.track_experiment_participation(...): ON CONFLICT (user_id, experiment_id) DO NOTHING. **1.4 Уникатни бизнис-клучеви (email/symbol/equipment_name)** Цел: да нема дупликат email/симбол/име на опрема. {{{ ALTER TABLE "User" ADD CONSTRAINT uq_user_email UNIQUE (email); ALTER TABLE elements ADD CONSTRAINT uq_elements_symbol UNIQUE (symbol); ALTER TABLE labequipment ADD CONSTRAINT uq_equipment_name UNIQUE (equipment_name); }}} Каде се користи: Регистрација (DatabaseManager.register_user): фаќа UniqueViolation за email. Додавање/уредување елемент (add_element/update_element): _norm_symbol() → UniqueViolation ако постои. Додавање опрема (add_lab_equipment): UniqueViolation ако постои. **1.5 Уникатна тројка за Reaction (element1, element2, conditions)** Цел: иста реакција под исти услови да не се дуплира. {{{ CREATE UNIQUE INDEX IF NOT EXISTS uq_reaction_e1_e2_cond ON reaction (element1_id, element2_id, COALESCE(conditions, '')); }}} Каде се користи: add_reaction/update_reaction: фаќа UniqueViolation и враќа None/False со лог порака „duplicate (element1, element2, conditions)“. **2) Временско печатење на учество (TIMESTAMPTZ)** Цел: да се бележи точниот момент (со временска зона) кога студентот учествувал. {{{ ALTER TABLE userparticipatesinexperiment ADD COLUMN IF NOT EXISTS participation_timestamp TIMESTAMPTZ; UPDATE userparticipatesinexperiment SET participation_timestamp = COALESCE(participation_timestamp, NOW()); ALTER TABLE userparticipatesinexperiment ALTER COLUMN participation_timestamp SET NOT NULL, ALTER COLUMN participation_timestamp SET DEFAULT NOW(); }}} Каде се користи: get_user_experiments(...), get_student_participation_experiments(...): ORDER BY participation_timestamp DESC. Извештаи /reports/... што групираат/редат по участие. **3) Тригер: default safety_warning (на experiment)** Цел: ако UI не прати безбедносно предупредување, базата сама да пополни разумен default. {{{ CREATE OR REPLACE FUNCTION trg_experiment_default_safety() RETURNS TRIGGER AS $$ BEGIN IF NEW.safety_warning IS NULL OR NEW.safety_warning = '' THEN NEW.safety_warning := 'Стандардни безбедносни мерки'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS set_default_safety ON experiment; CREATE TRIGGER set_default_safety BEFORE INSERT ON experiment FOR EACH ROW EXECUTE FUNCTION trg_experiment_default_safety(); }}} Каде се користи: DatabaseManager.insert_experiment(...): во кодот праќаме NULL ако корисникот остави празно → тригерот пополнува. **4) Погледи (Views)** **4.1 vw_students_experiments_detailed** Цел: брз извештај за студент, експеримент и време на учество. {{{ CREATE OR REPLACE VIEW vw_students_experiments_detailed AS SELECT s.student_id, (u.user_name || ' ' || u.user_surname) AS full_name, e.experiment_id, e.result, up.participation_timestamp::timestamp AS participation_time FROM student s JOIN "User" u ON s.student_id = u.user_id JOIN userparticipatesinexperiment up ON s.student_id = up.user_id JOIN experiment e ON up.experiment_id = e.experiment_id ORDER BY u.user_name, up.participation_timestamp DESC; }}} **4.2 vw_user_activity_summary** Цел: консолидиран преглед на активност по корисник. {{{ CREATE OR REPLACE VIEW vw_user_activity_summary AS SELECT u.user_id, (u.user_name || ' ' || u.user_surname) AS full_name, u.role, COUNT(DISTINCT uve.element_id) AS elements_viewed, COUNT(DISTINCT uvl.equipment_id) AS equipment_viewed, COUNT(DISTINCT upe.experiment_id) AS experiments_participated FROM "User" u LEFT JOIN userviewselement uve ON u.user_id = uve.user_id LEFT JOIN userviewslabequipment uvl ON u.user_id = uvl.user_id LEFT JOIN userparticipatesinexperiment upe ON u.user_id = upe.user_id GROUP BY u.user_id, full_name, u.role ORDER BY full_name; }}} **4.3 vw_students_experiments_for_teacher** Цел: на наставник да му се прикажат експериментите на неговите студенти. {{{ CREATE OR REPLACE VIEW vw_students_experiments_for_teacher AS SELECT (u.user_name || ' ' || u.user_surname) AS student_name, s.student_id, e.experiment_id, e.result, e.time_stamp, r.product, el1.symbol AS element1_symbol, el2.symbol AS element2_symbol, up.participation_timestamp AS participation_date, s.teacher_id FROM student s JOIN "User" u ON s.student_id = u.user_id JOIN userparticipatesinexperiment up ON s.student_id = up.user_id JOIN experiment e ON up.experiment_id = e.experiment_id JOIN reaction r ON e.reaction_id = r.reaction_id JOIN elements el1 ON r.element1_id = el1.element_id JOIN elements el2 ON r.element2_id = el2.element_id ORDER BY up.participation_timestamp DESC, student_name; }}} Каде се користи: Backend методи: vw_students_experiments_detailed(), vw_students_experiments_for_teacher(teacher_id), get_user_activity_summary(). **5) Складирана функција: create_reaction_and_experiment_fn(...)** Цел: атомично креирање Reaction → Experiment → (N:M) Equipment во една транскација на ниво на база. Код (скратено): {{{ CREATE OR REPLACE FUNCTION create_reaction_and_experiment_fn( p_teacher_id INT, p_element1_id INT, p_element2_id INT, p_product TEXT, p_conditions TEXT, p_exp_result TEXT, p_safety TEXT, p_equipment_ids INT[] ) RETURNS TABLE(reaction_id INT, experiment_id INT) LANGUAGE plpgsql AS $$ DECLARE v_reaction_id INT; v_experiment_id INT; v_result TEXT; BEGIN INSERT INTO reaction(...) RETURNING reaction_id INTO v_reaction_id; IF p_exp_result IS NULL OR p_exp_result = '' THEN -- авто-опис: „Експеримент со X и Y под услови...“ SELECT 'Експеримент со ' || e1.symbol || ' и ' || e2.symbol || CASE WHEN p_conditions IS NULL OR p_conditions = '' THEN ' под услови: стандардни. ' ELSE ' под услови: ' || p_conditions || '. ' END || 'Очекуван производ: ' || COALESCE(p_product, 'непознат') || '.' INTO v_result FROM elements e1, elements e2 WHERE e1.element_id = p_element1_id AND e2.element_id = p_element2_id; ELSE v_result := p_exp_result; END IF; INSERT INTO experiment(...) VALUES (...) RETURNING experiment_id INTO v_experiment_id; IF p_equipment_ids IS NOT NULL AND array_length(p_equipment_ids,1) > 0 THEN INSERT INTO experimentlabequipment(experiment_id, equipment_id) SELECT v_experiment_id, unnest(p_equipment_ids) ON CONFLICT DO NOTHING; END IF; RETURN QUERY SELECT v_reaction_id, v_experiment_id; END; $$; }}} Каде се користи: DatabaseManager.create_reaction_and_experiment(...): прво повикува DB-функција; ако не постои → Python fallback со транскација. Route /reactions/add: UI формата креира Reaction + Experiment + Equipment во еден чекор. **6) Индекси (перформанси)** Цел: побрзи пребарувања и сортирања. {{{ -- филтрирање по E1/E2: CREATE INDEX IF NOT EXISTS idx_reaction_elements ON reaction (element1_id, element2_id); -- експерименти по реакција (JOIN/филтер): CREATE INDEX IF NOT EXISTS idx_exp_reaction ON experiment (reaction_id); -- учества по корисник/експеримент: CREATE INDEX IF NOT EXISTS idx_up_user ON userparticipatesinexperiment (user_id); CREATE INDEX IF NOT EXISTS idx_up_experiment ON userparticipatesinexperiment (experiment_id); -- реални ORDER BY по време: CREATE INDEX IF NOT EXISTS idx_up_user_time ON userparticipatesinexperiment (user_id, participation_timestamp DESC); CREATE INDEX IF NOT EXISTS idx_exp_reaction_time ON experiment (reaction_id, time_stamp DESC); }}} Каде се користат: get_experiments_by_reaction(reaction_id) → idx_exp_reaction(_time) get_user_experiments(user_id) и get_student_participation_experiments(student_id) → idx_up_user(_time) get_reaction_by_symbols(sym1,sym2) → elements(symbol) + idx_reaction_elements (бидејќи симболите се нормализирани во кодот, индексот е употреблив).