DatabaseProgramming: procedures_functions_triggers.sql

File procedures_functions_triggers.sql, 14.5 KB (added by 231136, 6 days ago)
Line 
1-- функции, тригери, процедури
2
3
4-- тригер за додавање stream по додавање или ажурирање на сесија која траела подолго од 30 секунди
5
6CREATE OR REPLACE FUNCTION trg_record_stream_from_session()
7 RETURNS TRIGGER AS $$
8BEGIN
9 IF NEW.listened_ms >= 30000
10 AND NOT EXISTS (SELECT 1
11 FROM Song_Streams ss
12 WHERE ss.playback_session_id = NEW.id)
13 THEN
14 INSERT INTO Song_Streams (playback_session_id, song_id, streamed_at, user_id)
15 VALUES (NEW.id, NEW.song_id, NEW.started_at, NEW.user_id);
16 END IF;
17 RETURN NEW;
18END;
19$$ language plpgsql;
20
21
22DROP TRIGGER IF EXISTS record_stream_from_session on playback_sessions;
23
24CREATE TRIGGER record_stream_from_session
25 AFTER INSERT OR UPDATE OF listened_ms ON playback_sessions
26 FOR EACH ROW
27EXECUTE FUNCTION trg_record_stream_from_session();
28
29
30-- тригер за проверка дека една песна не може да има релација (ремикс, препев, ...) со самата себе
31-- и дека дека нема да има дупликати од истата релација
32
33CREATE OR REPLACE FUNCTION trg_check_song_relationship()
34 RETURNS TRIGGER AS $$
35BEGIN
36 IF NEW.source_song_id = NEW.target_song_id THEN
37 RAISE EXCEPTION 'A song cannot have a relationship with itself (song %)',
38 NEW.source_song_id;
39 END IF;
40
41 IF EXISTS (
42 SELECT 1
43 FROM Song_Relationships sr
44 WHERE sr.source_song_id = NEW.source_song_id
45 AND sr.target_song_id = NEW.target_song_id
46 AND sr.relationship_type = NEW.relationship_type
47 AND sr.id <> COALESCE(NEW.id, -1)
48 ) THEN
49 RAISE EXCEPTION 'Duplicate % relationship between songs % and %',
50 NEW.relationship_type, NEW.source_song_id, NEW.target_song_id;
51 END IF;
52
53 RETURN NEW;
54END;
55$$ LANGUAGE plpgsql;
56
57DROP TRIGGER IF EXISTS check_song_relationship ON Song_Relationships;
58CREATE TRIGGER check_song_relationship
59 BEFORE INSERT OR UPDATE ON Song_Relationships
60 FOR EACH ROW
61EXECUTE FUNCTION trg_check_song_relationship();
62
63
64-- функција за броење на бројот на слушања на одредена песна во изминат период
65
66CREATE OR REPLACE FUNCTION song_stream_count(
67 p_song_id BIGINT,
68 p_window INTERVAL DEFAULT INTERVAL '30 days'
69) RETURNS BIGINT AS $$
70SELECT COUNT(*)
71FROM Song_Streams ss
72WHERE ss.song_id = p_song_id
73 AND ss.streamed_at >= CURRENT_TIMESTAMP - p_window;
74$$ LANGUAGE sql STABLE;
75
76
77-- функција за проверување дали даден корисник може да преземе одредена акција - главна авторизациска логика
78
79CREATE OR REPLACE FUNCTION can_user_perform(
80 p_user_id BIGINT,
81 p_action VARCHAR,
82 p_resource_type VARCHAR,
83 p_resource_id BIGINT
84) RETURNS BOOLEAN AS $$
85DECLARE
86 v_scopes TEXT[];
87 v_is_owner BOOLEAN := FALSE;
88 v_visibility TEXT;
89 v_is_shared BOOLEAN := FALSE;
90BEGIN
91 -- site scopes dodeleni na ovoj user za torkata (action, resource_type)
92 SELECT ARRAY_AGG(DISTINCT p.scope)
93 INTO v_scopes
94 FROM User_Roles ur
95 JOIN Role_Permissions rp ON rp.role_id = ur.role_id
96 JOIN Permissions p ON p.id = rp.permission_id
97 WHERE ur.user_id = p_user_id
98 AND p.action = p_action
99 AND p.resource_type = p_resource_type;
100
101 IF v_scopes IS NULL THEN
102 RETURN FALSE;
103 END IF;
104
105 -- opredeli ownership + visibility za sekoj resource_type
106 IF p_resource_type = 'SONG' THEN
107 SELECT s.visibility,
108 EXISTS (SELECT 1 FROM Artists a
109 WHERE a.id = s.owner_artist_id
110 AND a.user_id = p_user_id)
111 INTO v_visibility, v_is_owner
112 FROM Songs s WHERE s.id = p_resource_id;
113
114 ELSIF p_resource_type = 'ALBUM' THEN
115 SELECT al.visibility,
116 EXISTS (SELECT 1 FROM Artists a
117 WHERE a.id = al.owner_artist_id
118 AND a.user_id = p_user_id)
119 INTO v_visibility, v_is_owner
120 FROM Albums al WHERE al.id = p_resource_id;
121
122 ELSIF p_resource_type = 'PLAYLIST' THEN
123 SELECT pl.visibility,
124 (pl.creator_user_id = p_user_id)
125 INTO v_visibility, v_is_owner
126 FROM Playlists pl WHERE pl.id = p_resource_id;
127 END IF;
128
129 -- resursot ne postoi / nepoznat resource_type
130 IF v_visibility IS NULL THEN
131 RETURN FALSE;
132 END IF;
133
134 -- ANY scope dozvoluva se, nema potreba od dopolnitelni proverki (dokolku resursot postoi)
135 IF 'ANY' = ANY (v_scopes) THEN
136 RETURN TRUE;
137 END IF;
138
139 IF v_is_owner AND 'OWN' = ANY (v_scopes) THEN
140 RETURN TRUE;
141 END IF;
142
143 IF v_visibility = 'PUBLIC' AND 'PUBLIC' = ANY (v_scopes) THEN
144 RETURN TRUE;
145 END IF;
146
147 -- SHARED: resursot moze da e spodelen direktno so korisnikot
148 -- ILI so edna od roljite koja korisnikot gi poseduva, I samiot share dozvoluva p_action
149 IF 'SHARED' = ANY (v_scopes) THEN
150 SELECT EXISTS (
151 SELECT 1
152 FROM Resource_Shares rs
153 JOIN Permissions sp ON sp.id = rs.permission_id
154 WHERE (
155 (p_resource_type = 'SONG' AND rs.song_id = p_resource_id) OR
156 (p_resource_type = 'ALBUM' AND rs.album_id = p_resource_id) OR
157 (p_resource_type = 'PLAYLIST' AND rs.playlist_id = p_resource_id)
158 )
159 AND sp.action = p_action
160 AND sp.resource_type = p_resource_type
161 AND (
162 rs.user_id = p_user_id
163 OR rs.role_id IN (SELECT ur.role_id
164 FROM User_Roles ur
165 WHERE ur.user_id = p_user_id)
166 )
167 ) INTO v_is_shared;
168
169 IF v_is_shared THEN
170 RETURN TRUE;
171 END IF;
172 END IF;
173
174 RETURN FALSE;
175END;
176$$ LANGUAGE plpgsql STABLE;
177
178
179
180
181-- тригер за проверка дали артистот и админот се дел од истата издавачка куќа при
182-- објавување на песна/албум
183
184CREATE OR REPLACE FUNCTION check_same_label()
185RETURNS TRIGGER
186LANGUAGE plpgsql
187AS $$
188BEGIN
189 IF NEW.published_by_label_id IS NOT NULL THEN
190 IF NOT EXISTS (
191 SELECT 1
192 FROM artist_labels al
193 WHERE al.label_id = NEW.published_by_label_id
194 AND al.artist_id = NEW.owner_artist_id
195 AND al.active = TRUE
196 ) THEN
197 RAISE EXCEPTION 'Artist does not belong to this label';
198 END IF;
199 END IF;
200
201 RETURN NEW;
202END;
203$$;
204
205
206DROP TRIGGER IF EXISTS check_same_admin_artist_label ON songs;
207DROP TRIGGER IF EXISTS check_same_admin_artist_label ON albums;
208
209CREATE OR REPLACE TRIGGER check_same_admin_artist_label
210 BEFORE INSERT OR UPDATE ON songs
211 FOR EACH ROW
212 EXECUTE FUNCTION check_same_label();
213
214
215CREATE OR REPLACE TRIGGER check_same_admin_artist_label
216 BEFORE INSERT OR UPDATE ON albums
217 FOR EACH ROW
218 EXECUTE FUNCTION check_same_label();
219
220
221
222-- тригер за автоматско ажурирање на припадноста на артистот кон издавачка куќа при негово префрлање во нова издавачка куќа
223
224CREATE OR REPLACE FUNCTION handle_new_artist_in_label()
225RETURNS TRIGGER
226LANGUAGE plpgsql
227AS $$
228BEGIN
229 UPDATE artist_labels
230 SET end_date = NOW(),
231 active = FALSE
232 WHERE artist_id = NEW.artist_id
233 AND active = TRUE;
234
235 NEW.active := TRUE;
236 NEW.start_date := NOW();
237
238 RETURN NEW;
239END;
240$$;
241
242DROP TRIGGER IF EXISTS handle_new_artist_in_label ON artist_labels;
243
244CREATE TRIGGER handle_new_artist_in_label
245BEFORE INSERT ON artist_labels
246FOR EACH ROW
247EXECUTE FUNCTION handle_new_artist_in_label();
248
249
250-- процедура за објавување на албум со песни
251
252CREATE OR REPLACE PROCEDURE upload_album_with_songs(
253 p_album_title VARCHAR,
254 p_album_visibility VARCHAR,
255 p_owner_artist_id BIGINT,
256 p_published_by_artist_id BIGINT,
257 p_published_by_label_id BIGINT,
258 p_song_ids BIGINT[],
259 INOUT p_album_id BIGINT DEFAULT NULL
260)
261LANGUAGE plpgsql
262AS $$
263BEGIN
264 IF p_song_ids IS NULL
265 OR array_length(p_song_ids, 1) IS NULL THEN
266 RAISE EXCEPTION 'Album must contain at least one song';
267 END IF;
268
269 IF EXISTS (
270 SELECT 1
271 FROM albums
272 WHERE title = p_album_title
273 ) THEN
274 RAISE EXCEPTION 'Album with that title already exists';
275 END IF;
276
277 INSERT INTO albums (
278 title,
279 visibility,
280 owner_artist_id,
281 published_by_artist_id,
282 published_by_label_id
283 )
284 VALUES (
285 p_album_title,
286 p_album_visibility,
287 p_owner_artist_id,
288 p_published_by_artist_id,
289 p_published_by_label_id
290 )
291 RETURNING id INTO p_album_id;
292
293 INSERT INTO album_tracks (
294 album_id,
295 song_id,
296 track_number
297 )
298 SELECT
299 p_album_id,
300 song_id,
301 ordinality
302 FROM unnest(p_song_ids)
303 WITH ORDINALITY AS t(song_id, ordinality);
304END;
305$$;
306
307
308
309-- процедура за ажурирање на сите материјализирани погледи
310
311CREATE OR REPLACE PROCEDURE refresh_all_materialized_views()
312LANGUAGE plpgsql
313AS $$
314DECLARE
315 v_mv RECORD;
316BEGIN
317 FOR v_mv IN
318 SELECT schemaname,
319 matviewname
320 FROM pg_matviews
321 WHERE schemaname = 'public'
322 LOOP
323 RAISE NOTICE 'Refreshing %.%',
324 v_mv.schemaname,
325 v_mv.matviewname;
326
327 EXECUTE format(
328 'REFRESH MATERIALIZED VIEW %I.%I',
329 v_mv.schemaname,
330 v_mv.matviewname
331 );
332 END LOOP;
333END;
334$$;
335
336
337-- функција за агрегирање на податоци од изминати song_streams партиции во song_stream_counts_archive
338-- и маркирање на тие месеци како затворени (податоците се користат во view #7)
339
340-- помошни табели
341
342CREATE TABLE IF NOT EXISTS song_stream_sealed_partitions (
343 partition_month date PRIMARY KEY, -- prviot den od zatvoreniot mesecot
344 sealed_at timestamptz NOT NULL DEFAULT now()
345);
346
347-- kumulativni brojaci po pesna za site do sega zatvoreni meseci
348CREATE TABLE IF NOT EXISTS song_stream_counts_archive (
349 song_id bigint PRIMARY KEY,
350 streams bigint NOT NULL
351);
352
353CREATE OR REPLACE FUNCTION seal_closed_song_streams_months()
354RETURNS void
355LANGUAGE plpgsql
356AS $$
357DECLARE
358 current_month date := date_trunc('month', current_date)::date;
359 r record;
360 yr int;
361 mo int;
362 part_month date;
363BEGIN
364 FOR r IN -- iterirame niz sekoja particija
365 SELECT c.relname
366 FROM pg_inherits i
367 JOIN pg_class c ON c.oid = i.inhrelid
368 JOIN pg_class p ON p.oid = i.inhparent
369 WHERE p.relname = 'song_streams'
370 AND c.relname ~ '^song_streams_y\d{4}m\d{2}$' -- isklucuva default particija
371 ORDER BY c.relname -- pocnuva od najstarata
372 LOOP
373 yr := substring(r.relname from 'y(\d{4})m')::int;
374 mo := substring(r.relname from 'm(\d{2})$')::int;
375 part_month := make_date(yr, mo, 1);
376
377 -- zatvarame (seal-nuvame) samo meseci koi se celosno pominati i ne se zatvoreni do sega
378 CONTINUE WHEN part_month >= current_month;
379 CONTINUE WHEN EXISTS (
380 SELECT 1 FROM song_stream_sealed_partitions s
381 WHERE s.partition_month = part_month
382 );
383
384 -- generira per-song brojaci za segasniot mesec i gi vnesuva so stream count arhivata
385 -- koristenjeto na particiite tuka e eksplicitno (r e imeto na particioniranata tabela)
386 EXECUTE format(
387 'INSERT INTO song_stream_counts_archive AS a (song_id, streams) '
388 'SELECT song_id, count(*) FROM %I GROUP BY song_id '
389 'ON CONFLICT (song_id) DO UPDATE SET streams = a.streams + EXCLUDED.streams',
390 r.relname
391 );
392
393 -- markiraj go kako sealed
394 INSERT INTO song_stream_sealed_partitions (partition_month)
395 VALUES (part_month);
396
397 RAISE NOTICE 'sealed month %', to_char(part_month, 'YYYY-MM');
398 END LOOP;
399END;
400$$;
401
402
403-- функции за овозможување стриминг на песни
404
405-- помошна функција за земање големина на песната во бајти.
406-- backend-от ја користи за да го пресмета Content-Range header-от и да валидира опсези
407CREATE OR REPLACE FUNCTION song_content_size(p_song_id bigint)
408RETURNS bigint
409LANGUAGE sql
410STABLE
411AS $$
412 SELECT octet_length(content)
413 FROM song_contents
414 WHERE song_id = p_song_id
415 LIMIT 1;
416$$;
417
418
419-- функција која враќа сегмент бајти од песната - главната (core) функција корисна за самиот стриминг
420-- p_start - 0-indexed offset (како во HTTP Range), inclusive
421-- p_length - колку бајти да се вратат од таа позиција
422
423CREATE OR REPLACE FUNCTION song_content_chunk(
424 p_song_id bigint,
425 p_start bigint,
426 p_length integer
427)
428RETURNS bytea
429LANGUAGE sql
430STABLE
431AS $$
432 SELECT substring(content FROM (p_start + 1)::int FOR p_length) -- substring() е 1-indexed па додаваме +1
433 FROM song_contents
434 WHERE song_id = p_song_id
435 LIMIT 1;
436$$;
437
438
439-- функции за менаџирање на сесии (playback_session)
440
441-- функција за започнување нова сесија (listened_ms = 0). враќа id-то на сесијата.
442
443CREATE OR REPLACE FUNCTION start_playback_session(
444 p_user_id bigint,
445 p_song_id bigint
446)
447RETURNS bigint
448LANGUAGE sql
449AS $$
450 INSERT INTO playback_sessions (user_id, song_id, started_at, listened_ms, last_position_ms)
451 VALUES (p_user_id, p_song_id, now(), 0, 0)
452 RETURNING id;
453$$;
454
455-- функција за update на сесијата согласно испратениот heartbeat од backend-от.
456-- доколку listened_ms надмине 30_000 поставениот тригер ќе направи запис во song_streams
457
458CREATE OR REPLACE FUNCTION update_playback_progress(
459 p_session_id bigint,
460 p_listened_ms integer,
461 p_last_position_ms integer
462)
463RETURNS void
464LANGUAGE sql
465AS $$
466 UPDATE playback_sessions
467 SET listened_ms = p_listened_ms,
468 last_position_ms = p_last_position_ms
469 WHERE id = p_session_id;
470$$;