diff options
Diffstat (limited to 'sql/func.sql')
-rw-r--r-- | sql/func.sql | 1192 |
1 files changed, 838 insertions, 354 deletions
diff --git a/sql/func.sql b/sql/func.sql index 6cb3735d..de0a45c3 100644 --- a/sql/func.sql +++ b/sql/func.sql @@ -4,83 +4,272 @@ -- notify_* -> functions creating entries in the notifications table -- user_* -> functions to manage users and sessions -- update_* -> functions to update a cache --- *_update ^ (I should probably rename these to --- *_calc ^ the update_* scheme for consistency) +-- *_calc ^ (same, should prolly rename to the update_* scheme for consistency) -- I like to keep the nouns in functions singular, in contrast to the table -- naming scheme where nouns are always plural. But I'm not very consistent -- with that, either. --- strip_bb_tags(text) - simple utility function to aid full-text searching -CREATE OR REPLACE FUNCTION strip_bb_tags(t text) RETURNS text AS $$ - SELECT regexp_replace(t, '\[(?:url=[^\]]+|/?(?:spoiler|quote|raw|code|url))\]', ' ', 'gi'); -$$ LANGUAGE sql IMMUTABLE; +-- Handy function to format an ipinfo type for human consumption. +CREATE OR REPLACE FUNCTION fmtip(n ipinfo) RETURNS text AS $$ + SELECT COALESCE(COALESCE((n).country, 'X')||':'||(n).asn||COALESCE(':'||(n).as_name,'')||'/', (n).country||'/', '') + || abbrev((n).ip) + || CASE WHEN (n).anonymous_proxy THEN ' ANON' ELSE '' END + || CASE WHEN (n).sattelite_provider THEN ' SAT' ELSE '' END + || CASE WHEN (n).anycast THEN ' ANY' ELSE '' END + || CASE WHEN (n).drop THEN ' DROP' ELSE '' END +$$ LANGUAGE SQL IMMUTABLE; --- Wrapper around to_tsvector() and strip_bb_tags(), implemented in plpgsql and --- with an associated cost function to make it opaque to the query planner and --- ensure the query planner realizes that this function is _slow_. -CREATE OR REPLACE FUNCTION bb_tsvector(t text) RETURNS tsvector AS $$ + + +-- Helper function for `update_search()` +CREATE OR REPLACE FUNCTION update_search_terms(objid vndbid) RETURNS SETOF record AS $$ +DECLARE + e int; -- because I'm too lazy to write out 'NULL::int' every time. BEGIN - RETURN to_tsvector('english', public.strip_bb_tags(t)); -END; -$$ LANGUAGE plpgsql IMMUTABLE COST 500; - --- BUG: Since this isn't a full bbcode parser, [spoiler] tags inside [raw] or [code] are still considered spoilers. -CREATE OR REPLACE FUNCTION strip_spoilers(t text) RETURNS text AS $$ - -- The website doesn't require the [spoiler] tag to be closed, the outer replace catches that case. - SELECT regexp_replace(regexp_replace(t, '\[spoiler\].*?\[/spoiler\]', ' ', 'ig'), '\[spoiler\].*', ' ', 'i'); -$$ LANGUAGE sql IMMUTABLE; - - --- Assigns a score to the relevance of a substring match, intended for use in --- an ORDER BY clause. Exact matches are ordered first, prefix matches after --- that, and finally a normal substring match. Not particularly fast, but --- that's to be expected of naive substring searches. --- Pattern must be escaped for use as a LIKE pattern. -CREATE OR REPLACE FUNCTION substr_score(str text, pattern text) RETURNS integer AS $$ -SELECT CASE - WHEN str ILIKE pattern THEN 0 - WHEN str ILIKE pattern||'%' THEN 1 - WHEN str ILIKE '%'||pattern||'%' THEN 2 - ELSE 3 + CASE vndbid_type(objid) + WHEN 'v' THEN RETURN QUERY + SELECT e, 3, search_norm_term(title) FROM vn_titles WHERE id = objid + UNION ALL SELECT e, 3, search_norm_term(latin) FROM vn_titles WHERE id = objid + UNION ALL SELECT e, 2, search_norm_term(a) FROM vn, regexp_split_to_table(alias, E'\n') a(a) WHERE objid = id + -- Remove the various editions/version strings from release titles, + -- this reduces the index size and makes VN search more relevant. + -- People looking for editions should be using the release search. + UNION ALL SELECT e, 1, regexp_replace(search_norm_term(t), '(?: + 体験|ダウンロド|初回限定|初回|限定|通常|廉価|豪華|追加|コレクション + |パッケージ|ダウンロード|ベスト|復刻|新装|7対応|版|生産|リメイク + |first|press|limited|regular|standard|full|remake + |pack|package|boxed|download|complete|popular|premium|deluxe|collectors?|collection + |lowprice|price|free|best|thebest|cheap|budget|reprint|bundle|set|renewal|extended + |special|trial|demo|allages|voiced?|uncensored|web|patch|port|r18|18|earlyaccess + |cd|cdr|cdrom|dvdrom|dvd|dvdpg|disk|disc|steam|for + |(?:win|windows)(?:7|10|95)?|vista|pc9821|support(?:ed)? + |(?:parts?|vol|volumes?|chapters?|v|ver|versions?)(?:[0-9]+) + |editions?|version|production|thebest|append|scenario|dlc)+$', '', 'xg') + FROM ( + SELECT title FROM releases r JOIN releases_vn rv ON rv.id = r.id JOIN releases_titles rt ON rt.id = r.id WHERE NOT r.hidden AND rv.vid = objid + UNION ALL + SELECT latin FROM releases r JOIN releases_vn rv ON rv.id = r.id JOIN releases_titles rt ON rt.id = r.id WHERE NOT r.hidden AND rv.vid = objid + ) r(t); + + WHEN 'r' THEN RETURN QUERY + SELECT e, 3, search_norm_term(title) FROM releases_titles WHERE id = objid + UNION ALL SELECT e, 3, search_norm_term(latin) FROM releases_titles WHERE id = objid + UNION ALL SELECT e, 1, gtin::text FROM releases WHERE id = objid AND gtin <> 0 + UNION ALL SELECT e, 1, search_norm_term(catalog) FROM releases WHERE id = objid AND catalog <> ''; + + WHEN 'c' THEN RETURN QUERY + SELECT e, 3, search_norm_term(name) FROM chars WHERE id = objid + UNION ALL SELECT e, 3, search_norm_term(latin) FROM chars WHERE id = objid + UNION ALL SELECT e, 2, search_norm_term(a) FROM chars, regexp_split_to_table(alias, E'\n') a(a) WHERE id = objid; + + WHEN 'p' THEN RETURN QUERY + SELECT e, 3, search_norm_term(name) FROM producers WHERE id = objid + UNION ALL SELECT e, 3, search_norm_term(latin) FROM producers WHERE id = objid + UNION ALL SELECT e, 2, search_norm_term(a) FROM producers, regexp_split_to_table(alias, E'\n') a(a) WHERE id = objid; + + WHEN 's' THEN RETURN QUERY + SELECT aid, 3, search_norm_term(name) FROM staff_alias WHERE id = objid + UNION ALL SELECT aid, 3, search_norm_term(latin) FROM staff_alias WHERE id = objid; + + WHEN 'g' THEN RETURN QUERY + SELECT e, 3, search_norm_term(name) FROM tags WHERE id = objid + UNION ALL SELECT e, 2, search_norm_term(a) FROM tags, regexp_split_to_table(alias, E'\n') a(a) WHERE objid = id; + + WHEN 'i' THEN RETURN QUERY + SELECT e, 3, search_norm_term(name) FROM traits WHERE id = objid + UNION ALL SELECT e, 2, search_norm_term(a) FROM traits, regexp_split_to_table(alias, E'\n') a(a) WHERE objid = id; + + ELSE RAISE 'unknown objid type'; + END CASE; END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION update_search(objid vndbid) RETURNS void AS $$ + WITH excluded(excluded) AS ( + -- VN, tag & trait search needs to support finding 'hidden' items, but for + -- other entry types we can safely exclude those from the search cache. + SELECT 1 + WHERE (vndbid_type(objid) = 'r' AND EXISTS(SELECT 1 FROM releases WHERE hidden AND id = objid)) + OR (vndbid_type(objid) = 'c' AND EXISTS(SELECT 1 FROM chars WHERE hidden AND id = objid)) + OR (vndbid_type(objid) = 'p' AND EXISTS(SELECT 1 FROM producers WHERE hidden AND id = objid)) + OR (vndbid_type(objid) = 's' AND EXISTS(SELECT 1 FROM staff WHERE hidden AND id = objid)) + ), uniq(subid, prio, label) AS ( + SELECT subid, MAX(prio)::smallint, label + FROM update_search_terms(objid) x (subid int, prio int, label text) + WHERE label IS NOT NULL AND label <> '' AND NOT EXISTS(SELECT 1 FROM excluded) + GROUP BY subid, label + ), terms(subid, prio, label) AS ( + -- It's possible for some entries to have no searchable terms at all, e.g. + -- when their titles only consists of characters that are normalized away. + -- In that case we still need to have at least one row in the search_cache + -- table for the id-based search to work. (Would be nicer to support + -- non-normalized search in those cases, but these cases aren't too common) + SELECT * FROM uniq + UNION ALL + SELECT NULL::int, 1, '' WHERE NOT EXISTS(SELECT 1 FROM excluded) AND NOT EXISTS(SELECT 1 FROM uniq) + ), n(subid, prio, label) AS ( + SELECT COALESCE(t.subid, o.subid), t.prio, COALESCE(t.label, o.label) + FROM terms t + FULL OUTER JOIN (SELECT subid, label FROM search_cache WHERE id = objid) o ON o.subid IS NOT DISTINCT FROM t.subid AND o.label = t.label + ) MERGE INTO search_cache o USING n ON o.id = objid AND (o.subid, o.label) IS NOT DISTINCT FROM (n.subid, n.label) + WHEN NOT MATCHED THEN INSERT (id, subid, prio, label) VALUES (objid, subid, n.prio, n.label) + WHEN MATCHED AND n.prio IS NULL THEN DELETE + WHEN MATCHED AND n.prio <> o.prio THEN UPDATE SET prio = n.prio; $$ LANGUAGE SQL; + +-- Helper function for the titleprefs functions below. +CREATE OR REPLACE FUNCTION titleprefs_swap(p titleprefs, lang language, title text, latin text) RETURNS text[] AS $$ + SELECT ARRAY[lang::text, CASE WHEN ( + CASE WHEN p.t1_lang = lang THEN p.t1_latin + WHEN p.t2_lang = lang THEN p.t2_latin + WHEN p.t3_lang = lang THEN p.t3_latin + WHEN p.t4_lang = lang THEN p.t4_latin ELSE p IS NULL OR p.to_latin END + ) THEN COALESCE(latin, title) ELSE title END, lang::text, CASE WHEN ( + CASE WHEN p.a1_lang = lang THEN p.a1_latin + WHEN p.a2_lang = lang THEN p.a2_latin + WHEN p.a3_lang = lang THEN p.a3_latin + WHEN p.a4_lang = lang THEN p.a4_latin ELSE p.ao_latin END + ) THEN COALESCE(latin, title) ELSE title END] +$$ LANGUAGE SQL STABLE; + + +-- This is a pure-SQL implementation of the title preference selection +-- algorithm in VNWeb::TitlePrefs. Given a preferences object, this function +-- returns a copy of the 'vn' table with two additional columns: +-- * title - Array of: main title language, main title, secondary title language, secondary title +-- * sorttitle - title to be used in ORDER BY clause +-- +-- The 'title' array format is (supposed to be) used pervasively through the +-- back-end code to order to easily pass around titles as a single object and +-- to support proper rendering of both the main & secondary title of each +-- entry. +-- +-- This function looks slow and messy, but it's been specifically written to be +-- transparent to the query planner and so that unused joins can be fully +-- optimized out during query execution. Even with that, it's better to avoid +-- this function in complex queries when possible because you may run into +-- bad query plans by hitting join_collapse_limit or from_collapse_limit. +-- (More info at https://dev.yorhel.nl/doc/vndbtitles) +CREATE OR REPLACE FUNCTION vnt(p titleprefs) RETURNS SETOF vnt AS $$ + -- The language selection logic below is specially written so that the planner can remove references to joined tables corresponding to NULL languages. + SELECT v.*, (CASE + WHEN p.t1_lang = t1.lang AND (NOT p.t1_official OR t1.official) AND (p.t1_official IS NOT NULL OR p.t1_lang = v.olang) THEN ARRAY[t1.lang::text, COALESCE(CASE WHEN p.t1_latin THEN t1.latin ELSE NULL END, t1.title)] + WHEN p.t2_lang = t2.lang AND (NOT p.t2_official OR t2.official) AND (p.t2_official IS NOT NULL OR p.t2_lang = v.olang) THEN ARRAY[t2.lang::text, COALESCE(CASE WHEN p.t2_latin THEN t2.latin ELSE NULL END, t2.title)] + WHEN p.t3_lang = t3.lang AND (NOT p.t3_official OR t3.official) AND (p.t3_official IS NOT NULL OR p.t3_lang = v.olang) THEN ARRAY[t3.lang::text, COALESCE(CASE WHEN p.t3_latin THEN t3.latin ELSE NULL END, t3.title)] + WHEN p.t4_lang = t4.lang AND (NOT p.t4_official OR t4.official) AND (p.t4_official IS NOT NULL OR p.t4_lang = v.olang) THEN ARRAY[t4.lang::text, COALESCE(CASE WHEN p.t4_latin THEN t4.latin ELSE NULL END, t4.title)] + ELSE ARRAY[v.olang::text, COALESCE(CASE WHEN p IS NULL OR p.to_latin THEN ol.latin ELSE NULL END, ol.title)] END + ) || (CASE + WHEN p.a1_lang = a1.lang AND (NOT p.a1_official OR a1.official) AND (p.a1_official IS NOT NULL OR p.a1_lang = v.olang) THEN ARRAY[a1.lang::text, COALESCE(CASE WHEN p.a1_latin THEN a1.latin ELSE NULL END, a1.title)] + WHEN p.a2_lang = a2.lang AND (NOT p.a2_official OR a2.official) AND (p.a2_official IS NOT NULL OR p.a2_lang = v.olang) THEN ARRAY[a2.lang::text, COALESCE(CASE WHEN p.a2_latin THEN a2.latin ELSE NULL END, a2.title)] + WHEN p.a3_lang = a3.lang AND (NOT p.a3_official OR a3.official) AND (p.a3_official IS NOT NULL OR p.a3_lang = v.olang) THEN ARRAY[a3.lang::text, COALESCE(CASE WHEN p.a3_latin THEN a3.latin ELSE NULL END, a3.title)] + WHEN p.a4_lang = a4.lang AND (NOT p.a4_official OR a4.official) AND (p.a4_official IS NOT NULL OR p.a4_lang = v.olang) THEN ARRAY[a4.lang::text, COALESCE(CASE WHEN p.a4_latin THEN a4.latin ELSE NULL END, a4.title)] + ELSE ARRAY[v.olang::text, COALESCE(CASE WHEN p.ao_latin THEN ol.latin ELSE NULL END, ol.title)] END) + , CASE + WHEN p.t1_lang = t1.lang AND (NOT p.t1_official OR t1.official) AND (p.t1_official IS NOT NULL OR p.t1_lang = v.olang) THEN COALESCE(t1.latin, t1.title) + WHEN p.t2_lang = t2.lang AND (NOT p.t2_official OR t2.official) AND (p.t2_official IS NOT NULL OR p.t2_lang = v.olang) THEN COALESCE(t2.latin, t2.title) + WHEN p.t3_lang = t3.lang AND (NOT p.t3_official OR t3.official) AND (p.t3_official IS NOT NULL OR p.t3_lang = v.olang) THEN COALESCE(t3.latin, t3.title) + WHEN p.t4_lang = t4.lang AND (NOT p.t4_official OR t4.official) AND (p.t4_official IS NOT NULL OR p.t4_lang = v.olang) THEN COALESCE(t4.latin, t4.title) + ELSE COALESCE(ol.latin, ol.title) END + FROM vn v + JOIN vn_titles ol ON ol.id = v.id AND ol.lang = v.olang + -- The COALESCE() below is kind of meaningless, but apparently the query planner can't optimize out JOINs with NULL conditions. + LEFT JOIN vn_titles t1 ON t1.id = v.id AND t1.lang = COALESCE(p.t1_lang, 'en') + LEFT JOIN vn_titles t2 ON t2.id = v.id AND t2.lang = COALESCE(p.t2_lang, 'en') + LEFT JOIN vn_titles t3 ON t3.id = v.id AND t3.lang = COALESCE(p.t3_lang, 'en') + LEFT JOIN vn_titles t4 ON t4.id = v.id AND t4.lang = COALESCE(p.t4_lang, 'en') + LEFT JOIN vn_titles a1 ON a1.id = v.id AND a1.lang = COALESCE(p.a1_lang, 'en') + LEFT JOIN vn_titles a2 ON a2.id = v.id AND a2.lang = COALESCE(p.a2_lang, 'en') + LEFT JOIN vn_titles a3 ON a3.id = v.id AND a3.lang = COALESCE(p.a3_lang, 'en') + LEFT JOIN vn_titles a4 ON a4.id = v.id AND a4.lang = COALESCE(p.a4_lang, 'en') +$$ LANGUAGE SQL STABLE; + + + +-- Same thing as vnt() +CREATE OR REPLACE FUNCTION releasest(p titleprefs) RETURNS SETOF releasest AS $$ + SELECT r.*, (CASE + WHEN p.t1_lang = t1.lang AND (p.t1_official IS NOT NULL OR p.t1_lang = r.olang) THEN ARRAY[t1.lang::text, COALESCE(CASE WHEN p.t1_latin THEN t1.latin ELSE NULL END, t1.title)] + WHEN p.t2_lang = t2.lang AND (p.t2_official IS NOT NULL OR p.t2_lang = r.olang) THEN ARRAY[t2.lang::text, COALESCE(CASE WHEN p.t2_latin THEN t2.latin ELSE NULL END, t2.title)] + WHEN p.t3_lang = t3.lang AND (p.t3_official IS NOT NULL OR p.t3_lang = r.olang) THEN ARRAY[t3.lang::text, COALESCE(CASE WHEN p.t3_latin THEN t3.latin ELSE NULL END, t3.title)] + WHEN p.t4_lang = t4.lang AND (p.t4_official IS NOT NULL OR p.t4_lang = r.olang) THEN ARRAY[t4.lang::text, COALESCE(CASE WHEN p.t4_latin THEN t4.latin ELSE NULL END, t4.title)] + ELSE ARRAY[r.olang::text, COALESCE(CASE WHEN p IS NULL OR p.to_latin THEN ol.latin ELSE NULL END, ol.title)] END + ) || (CASE + WHEN p.a1_lang = a1.lang AND (p.a1_official IS NOT NULL OR p.a1_lang = r.olang) THEN ARRAY[a1.lang::text, COALESCE(CASE WHEN p.a1_latin THEN a1.latin ELSE NULL END, a1.title)] + WHEN p.a2_lang = a2.lang AND (p.a2_official IS NOT NULL OR p.a2_lang = r.olang) THEN ARRAY[a2.lang::text, COALESCE(CASE WHEN p.a2_latin THEN a2.latin ELSE NULL END, a2.title)] + WHEN p.a3_lang = a3.lang AND (p.a3_official IS NOT NULL OR p.a3_lang = r.olang) THEN ARRAY[a3.lang::text, COALESCE(CASE WHEN p.a3_latin THEN a3.latin ELSE NULL END, a3.title)] + WHEN p.a4_lang = a4.lang AND (p.a4_official IS NOT NULL OR p.a4_lang = r.olang) THEN ARRAY[a4.lang::text, COALESCE(CASE WHEN p.a4_latin THEN a4.latin ELSE NULL END, a4.title)] + ELSE ARRAY[r.olang::text, COALESCE(CASE WHEN p.ao_latin THEN ol.latin ELSE NULL END, ol.title)] END) + , CASE + WHEN p.t1_lang = t1.lang AND (p.t1_official IS NOT NULL OR p.t1_lang = r.olang) THEN COALESCE(t1.latin, t1.title) + WHEN p.t2_lang = t2.lang AND (p.t2_official IS NOT NULL OR p.t2_lang = r.olang) THEN COALESCE(t2.latin, t2.title) + WHEN p.t3_lang = t3.lang AND (p.t3_official IS NOT NULL OR p.t3_lang = r.olang) THEN COALESCE(t3.latin, t3.title) + WHEN p.t4_lang = t4.lang AND (p.t4_official IS NOT NULL OR p.t4_lang = r.olang) THEN COALESCE(t4.latin, t4.title) + ELSE COALESCE(ol.latin, ol.title) END + FROM releases r + JOIN releases_titles ol ON ol.id = r.id AND ol.lang = r.olang + LEFT JOIN releases_titles t1 ON t1.id = r.id AND t1.lang = COALESCE(p.t1_lang, 'en') AND t1.title IS NOT NULL + LEFT JOIN releases_titles t2 ON t2.id = r.id AND t2.lang = COALESCE(p.t2_lang, 'en') AND t2.title IS NOT NULL + LEFT JOIN releases_titles t3 ON t3.id = r.id AND t3.lang = COALESCE(p.t3_lang, 'en') AND t3.title IS NOT NULL + LEFT JOIN releases_titles t4 ON t4.id = r.id AND t4.lang = COALESCE(p.t4_lang, 'en') AND t4.title IS NOT NULL + LEFT JOIN releases_titles a1 ON a1.id = r.id AND a1.lang = COALESCE(p.a1_lang, 'en') AND a1.title IS NOT NULL + LEFT JOIN releases_titles a2 ON a2.id = r.id AND a2.lang = COALESCE(p.a2_lang, 'en') AND a2.title IS NOT NULL + LEFT JOIN releases_titles a3 ON a3.id = r.id AND a3.lang = COALESCE(p.a3_lang, 'en') AND a3.title IS NOT NULL + LEFT JOIN releases_titles a4 ON a4.id = r.id AND a4.lang = COALESCE(p.a4_lang, 'en') AND a4.title IS NOT NULL +$$ LANGUAGE SQL STABLE; + + + +-- This one just flips the name/original columns around depending on +-- preferences, so is fast enough to use directly. +CREATE OR REPLACE FUNCTION producerst(p titleprefs) RETURNS SETOF producerst AS $$ + SELECT *, titleprefs_swap(p, lang, name, latin), COALESCE(latin, name) FROM producers +$$ LANGUAGE SQL STABLE; + + + +-- Same for charst +CREATE OR REPLACE FUNCTION charst(p titleprefs) RETURNS SETOF charst AS $$ + SELECT *, titleprefs_swap(p, c_lang, name, latin), COALESCE(latin, name) FROM chars +$$ LANGUAGE SQL STABLE; + + + +-- Same for staff_aliast +CREATE OR REPLACE FUNCTION staff_aliast(p titleprefs) RETURNS SETOF staff_aliast AS $$ + SELECT s.*, sa.aid, sa.name, sa.latin + , titleprefs_swap(p, s.lang, sa.name, sa.latin), COALESCE(sa.latin, sa.name) + FROM staff s + JOIN staff_alias sa ON sa.id = s.id +$$ LANGUAGE SQL STABLE; + + + -- update_vncache(id) - updates some c_* columns in the vn table -CREATE OR REPLACE FUNCTION update_vncache(integer) RETURNS void AS $$ +CREATE OR REPLACE FUNCTION update_vncache(vndbid) RETURNS void AS $$ UPDATE vn SET c_released = COALESCE(( SELECT MIN(r.released) FROM releases r JOIN releases_vn rv ON r.id = rv.id WHERE rv.vid = $1 - AND r.type <> 'trial' + AND rv.rtype <> 'trial' AND r.hidden = FALSE AND r.released <> 0 + AND r.official GROUP BY rv.vid ), 0), - c_olang = ARRAY( - SELECT lang - FROM releases_lang - WHERE id = ( - SELECT r.id - FROM releases_vn rv - JOIN releases r ON rv.id = r.id - WHERE r.released > 0 - AND NOT r.hidden - AND rv.vid = $1 - ORDER BY r.released - LIMIT 1 - ) - ), c_languages = ARRAY( SELECT rl.lang - FROM releases_lang rl + FROM releases_titles rl JOIN releases r ON r.id = rl.id JOIN releases_vn rv ON r.id = rv.id WHERE rv.vid = $1 - AND r.type <> 'trial' + AND rv.rtype <> 'trial' + AND NOT rl.mtl AND r.released <= TO_CHAR('today'::timestamp, 'YYYYMMDD')::integer AND r.hidden = FALSE GROUP BY rl.lang @@ -92,48 +281,76 @@ CREATE OR REPLACE FUNCTION update_vncache(integer) RETURNS void AS $$ JOIN releases r ON rp.id = r.id JOIN releases_vn rv ON rp.id = rv.id WHERE rv.vid = $1 - AND r.type <> 'trial' + AND rv.rtype <> 'trial' AND r.released <= TO_CHAR('today'::timestamp, 'YYYYMMDD')::integer AND r.hidden = FALSE GROUP BY rp.platform ORDER BY rp.platform + ), + c_developers = ARRAY( + SELECT rp.pid + FROM releases_producers rp + JOIN releases r ON rp.id = r.id + JOIN releases_vn rv ON rv.id = r.id + WHERE rv.vid = $1 + AND r.official AND rp.developer + AND r.hidden = FALSE + GROUP BY rp.pid + ORDER BY rp.pid ) WHERE id = $1; $$ LANGUAGE sql; --- Update vn.c_popularity, c_rating, c_votecount, c_pop_rank and c_rat_rank +-- Update c_rating, c_votecount, c_pop_rank, c_rat_rank and c_average CREATE OR REPLACE FUNCTION update_vnvotestats() RETURNS void AS $$ WITH votes(vid, uid, vote) AS ( -- List of all non-ignored VN votes SELECT vid, uid, vote FROM ulist_vns WHERE vote IS NOT NULL AND uid NOT IN(SELECT id FROM users WHERE ign_votes) - ), avgcount(avgcount) AS ( -- Average number of votes per VN - SELECT COUNT(vote)::real/COUNT(DISTINCT vid)::real FROM votes ), avgavg(avgavg) AS ( -- Average vote average SELECT AVG(a)::real FROM (SELECT AVG(vote) FROM votes GROUP BY vid) x(a) - ), ratings(vid, count, rating) AS ( -- Ratings and vote counts - SELECT vid, COALESCE(COUNT(uid), 0), - COALESCE( - ((SELECT avgcount FROM avgcount) * (SELECT avgavg FROM avgavg) + SUM(vote)::real) / - ((SELECT avgcount FROM avgcount) + COUNT(uid)::real), - 0) + ), ratings(vid, count, average, rating) AS ( -- Ratings and vote counts + SELECT vid, COUNT(uid), (AVG(vote)*10)::smallint, + -- Bayesian average B(a,p,votes) = (p * a + sum(votes)) / (p + count(votes)) + -- p = (1 - min(1, count(votes)/100)) * 7 i.e. linear interpolation from 7 to 0 for vote counts from 0 to 100. + -- a = Average vote average + ( (1 - LEAST(1, COUNT(uid)::real/100))*7 * (SELECT avgavg FROM avgavg) + SUM(vote) ) / + ( (1 - LEAST(1, COUNT(uid)::real/100))*7 + COUNT(uid) ) FROM votes GROUP BY vid - ), popularities(vid, win) AS ( -- Popularity scores (before normalization) - SELECT vid, SUM(rank) - FROM ( - SELECT uid, vid, ((rank() OVER (PARTITION BY uid ORDER BY vote))::real - 1) ^ 0.36788 FROM votes - ) x(uid, vid, rank) - GROUP BY vid - ), stats(vid, rating, count, popularity, pop_rank, rat_rank) AS ( -- Combined stats - SELECT v.id, COALESCE(r.rating, 0), COALESCE(r.count, 0) - , p.win/(SELECT MAX(win) FROM popularities) - , CASE WHEN p.win IS NULL THEN NULL ELSE rank() OVER(ORDER BY hidden, p.win DESC NULLS LAST) END + ), capped(vid, count, average, rating) AS ( -- Ratings, but capped + SELECT vid, count, average, CASE + WHEN count < 5 THEN NULL + WHEN count < 50 THEN LEAST(rating, (SELECT rating FROM ratings WHERE count >= 50 ORDER BY rating DESC LIMIT 1 OFFSET 101)) + WHEN count < 100 THEN LEAST(rating, (SELECT rating FROM ratings WHERE count >= 100 ORDER BY rating DESC LIMIT 1 OFFSET 51)) + ELSE rating END + FROM ratings + ), stats(vid, count, average, rating, rat_rank, pop_rank) AS ( -- Combined stats + SELECT v.id, COALESCE(r.count, 0), r.average, (r.rating*10)::smallint , CASE WHEN r.rating IS NULL THEN NULL ELSE rank() OVER(ORDER BY hidden, r.rating DESC NULLS LAST) END + , rank() OVER(ORDER BY hidden, r.count DESC NULLS LAST) FROM vn v - LEFT JOIN ratings r ON r.vid = v.id - LEFT JOIN popularities p ON p.vid = v.id AND p.win > 0 + LEFT JOIN capped r ON r.vid = v.id ) - UPDATE vn SET c_rating = rating, c_votecount = count, c_popularity = popularity, c_pop_rank = pop_rank, c_rat_rank = rat_rank FROM stats WHERE id = vid; + UPDATE vn SET c_rating = rating, c_votecount = count, c_pop_rank = pop_rank, c_rat_rank = rat_rank, c_average = average + FROM stats + WHERE id = vid AND (c_rating, c_votecount, c_pop_rank, c_rat_rank, c_average) IS DISTINCT FROM (rating, count, pop_rank, rat_rank, average); +$$ LANGUAGE SQL; + + + +-- Updates vn.c_length and vn.c_lengthnum +CREATE OR REPLACE FUNCTION update_vn_length_cache(vndbid) RETURNS void AS $$ + WITH s (vid, cnt, len) AS ( + SELECT v.id, count(l.vid) FILTER (WHERE u.id IS NOT NULL AND l.vid IS NOT NULL AND v.devstatus <> 1) + , percentile_cont(0.5) WITHIN GROUP (ORDER BY l.length + (l.length/4 * (l.speed-1))) FILTER (WHERE u.id IS NOT NULL AND l.vid IS NOT NULL AND v.devstatus <> 1) + FROM vn v + LEFT JOIN vn_length_votes l ON l.vid = v.id AND l.speed IS NOT NULL AND NOT l.private + LEFT JOIN users u ON u.id = l.uid AND u.perm_lengthvote + WHERE ($1 IS NULL OR v.id = $1) + GROUP BY v.id + ) UPDATE vn SET c_lengthnum = cnt, c_length = len + FROM s + WHERE s.vid = id AND (c_lengthnum, c_length) IS DISTINCT FROM (cnt, len) $$ LANGUAGE SQL; @@ -152,20 +369,26 @@ CREATE OR REPLACE FUNCTION update_images_cache(vndbid) RETURNS void AS $$ BEGIN UPDATE images SET c_votecount = votecount, c_sexual_avg = sexual_avg, c_sexual_stddev = sexual_stddev - , c_violence_avg = violence_avg, c_violence_stddev = violence_stddev, c_weight = weight + , c_violence_avg = violence_avg, c_violence_stddev = violence_stddev, c_weight = weight, c_uids = uids FROM ( - SELECT s.*, - CASE WHEN EXISTS( + SELECT s.id, s.votecount, s.uids + , COALESCE(s.sexual_avg *100, 200) AS sexual_avg, COALESCE(s.sexual_stddev *100, 0) AS sexual_stddev + , COALESCE(s.violence_avg*100, 200) AS violence_avg, COALESCE(s.violence_stddev*100, 0) AS violence_stddev + , CASE WHEN s.votecount >= 15 THEN 1 -- Lock the weight at 1 at 15 votes, collecting more votes is just inefficient + WHEN EXISTS( SELECT 1 FROM vn v WHERE s.id BETWEEN 'cv1' AND vndbid_max('cv') AND NOT v.hidden AND v.image = s.id UNION ALL SELECT 1 FROM vn_screenshots vs JOIN vn v ON v.id = vs.id WHERE s.id BETWEEN 'sf1' AND vndbid_max('sf') AND NOT v.hidden AND vs.scr = s.id UNION ALL SELECT 1 FROM chars c WHERE s.id BETWEEN 'ch1' AND vndbid_max('ch') AND NOT c.hidden AND c.image = s.id ) - THEN pow(2, greatest(0, 14 - s.votecount)) + coalesce(pow(s.sexual_stddev, 2), 0)*100 + coalesce(pow(s.violence_stddev, 2), 0)*100 + THEN ceil(pow(2, greatest(0, 14 - s.votecount)) + coalesce(pow(s.sexual_stddev, 2), 0)*100 + coalesce(pow(s.violence_stddev, 2), 0)*100) ELSE 0 END AS weight FROM ( SELECT i.id, count(iv.id) AS votecount - , avg(sexual) FILTER(WHERE NOT iv.ignore) AS sexual_avg, stddev_pop(sexual) FILTER(WHERE NOT iv.ignore) AS sexual_stddev - , avg(violence) FILTER(WHERE NOT iv.ignore) AS violence_avg, stddev_pop(violence) FILTER(WHERE NOT iv.ignore) AS violence_stddev + , round(avg(sexual) FILTER(WHERE NOT iv.ignore), 2) AS sexual_avg + , round(avg(violence) FILTER(WHERE NOT iv.ignore), 2) AS violence_avg + , round(stddev_pop(sexual) FILTER(WHERE NOT iv.ignore), 2) AS sexual_stddev + , round(stddev_pop(violence) FILTER(WHERE NOT iv.ignore), 2) AS violence_stddev + , coalesce(array_agg(u.id) FILTER(WHERE u.id IS NOT NULL), '{}') AS uids FROM images i LEFT JOIN image_votes iv ON iv.id = i.id LEFT JOIN users u ON u.id = iv.uid @@ -174,7 +397,8 @@ BEGIN GROUP BY i.id ) s ) weights - WHERE weights.id = images.id; + WHERE weights.id = images.id AND (c_votecount, c_sexual_avg, c_sexual_stddev, c_violence_avg, c_violence_stddev, c_weight, c_uids) + IS DISTINCT FROM (votecount, sexual_avg, sexual_stddev, violence_avg, violence_stddev, weight, uids); END; $$ LANGUAGE plpgsql; @@ -182,15 +406,10 @@ END; $$ LANGUAGE plpgsql; -- Update reviews.c_up, c_down and c_flagged CREATE OR REPLACE FUNCTION update_reviews_votes_cache(vndbid) RETURNS void AS $$ BEGIN - WITH stats(id,up,down,flag) AS ( + WITH stats(id,up,down) AS ( SELECT r.id - , COUNT(*) FILTER(WHERE rv.vote AND NOT u.ign_votes AND r2.id IS NULL) - , COUNT(*) FILTER(WHERE NOT rv.vote AND NOT u.ign_votes AND r2.id IS NULL) - -- flag score = up-down < -10, overrule votes count for 10000 (this algorithm is subject to tuning) - , COALESCE( - SUM((CASE WHEN rv.vote THEN 1 ELSE -1 END)*(CASE WHEN rv.overrule THEN 10000 ELSE 1 END)) - FILTER(WHERE NOT u.ign_votes AND (r2.id IS NULL OR rv.overrule)), - 0) < -1000 + , COALESCE(SUM(CASE WHEN rv.overrule THEN 100000 WHEN rv.ip IS NULL THEN 100 ELSE 1 END) FILTER(WHERE rv.vote AND u.ign_votes IS DISTINCT FROM true AND (rv.overrule OR r2.id IS NULL)), 0) + , COALESCE(SUM(CASE WHEN rv.overrule THEN 100000 WHEN rv.ip IS NULL THEN 100 ELSE 1 END) FILTER(WHERE NOT rv.vote AND u.ign_votes IS DISTINCT FROM true AND (rv.overrule OR r2.id IS NULL)), 0) FROM reviews r LEFT JOIN reviews_votes rv ON rv.id = r.id LEFT JOIN users u ON u.id = rv.uid @@ -198,125 +417,189 @@ BEGIN WHERE $1 IS NULL OR r.id = $1 GROUP BY r.id ) - UPDATE reviews SET c_up = up, c_down = down, c_flagged = flag - FROM stats WHERE reviews.id = stats.id AND (reviews.c_up,reviews.c_down,reviews.c_flagged) <> (stats.up,stats.down,stats.flag); + UPDATE reviews SET c_up = up, c_down = down, c_flagged = up-down<-10000 + FROM stats WHERE reviews.id = stats.id AND (c_up,c_down,c_flagged) <> (up,down,up-down<10000); END; $$ LANGUAGE plpgsql; -- Update users.c_vns, c_votes and c_wish for one user (when given an id) or all users (when given NULL) -CREATE OR REPLACE FUNCTION update_users_ulist_stats(integer) RETURNS void AS $$ +CREATE OR REPLACE FUNCTION update_users_ulist_stats(vndbid) RETURNS void AS $$ BEGIN WITH cnt(uid, votes, vns, wish) AS ( SELECT u.id - , COUNT(DISTINCT uvl.vid) FILTER (WHERE NOT ul.private AND uv.vote IS NOT NULL) -- Voted - , COUNT(DISTINCT uvl.vid) FILTER (WHERE NOT ul.private AND ul.id NOT IN(5,6)) -- Labelled, but not wishlish/blacklist - , COUNT(DISTINCT uvl.vid) FILTER (WHERE NOT ul.private AND ul.id = 5) -- Wishlist + , COUNT(uv.vid) FILTER (WHERE NOT uv.c_private AND uv.vote IS NOT NULL) -- Voted + , COUNT(uv.vid) FILTER (WHERE NOT uv.c_private AND NOT (uv.labels <@ ARRAY[5,6]::smallint[])) -- Labelled, but not wishlish/blacklist + , COUNT(uv.vid) FILTER (WHERE uwish.private IS NOT DISTINCT FROM false AND uv.labels && ARRAY[5::smallint]) -- Wishlist FROM users u - LEFT JOIN ulist_vns_labels uvl ON uvl.uid = u.id - LEFT JOIN ulist_labels ul ON ul.id = uvl.lbl AND ul.uid = u.id - LEFT JOIN ulist_vns uv ON uv.uid = u.id AND uv.vid = uvl.vid + LEFT JOIN ulist_vns uv ON uv.uid = u.id + LEFT JOIN ulist_labels uwish ON uwish.uid = u.id AND uwish.id = 5 WHERE $1 IS NULL OR u.id = $1 GROUP BY u.id - ) UPDATE users SET c_votes = votes, c_vns = vns, c_wish = wish FROM cnt WHERE id = uid; + ) UPDATE users SET c_votes = votes, c_vns = vns, c_wish = wish + FROM cnt WHERE id = uid AND (c_votes, c_vns, c_wish) IS DISTINCT FROM (votes, vns, wish); END; $$ LANGUAGE plpgsql; -- Don't use "LANGUAGE SQL" here; Make sure to generate a new query plan at invocation time. --- Recalculate tags_vn_inherit. +-- Update ulist_vns.c_private for a particular (user, vid). vid can be null to +-- update the cache for the all VNs in the user's list, user can also be null +-- to update the cache for everyone. +CREATE OR REPLACE FUNCTION update_users_ulist_private(vndbid, vndbid) RETURNS void AS $$ +BEGIN + WITH p(uid,vid,private) AS ( + SELECT uv.uid, uv.vid, COALESCE(bool_and(l.private), true) + FROM ulist_vns uv + LEFT JOIN unnest(uv.labels) x(id) ON true + LEFT JOIN ulist_labels l ON l.id = x.id AND l.uid = uv.uid + WHERE ($1 IS NULL OR uv.uid = $1) + AND ($2 IS NULL OR uv.vid = $2) + GROUP BY uv.uid, uv.vid + ) UPDATE ulist_vns SET c_private = p.private FROM p + WHERE ulist_vns.uid = p.uid AND ulist_vns.vid = p.vid AND ulist_vns.c_private <> p.private; +END; +$$ LANGUAGE plpgsql; + + + +-- Update tags_vn_direct & tags_vn_inherit. -- When a vid is given, only the tags for that vid will be updated. These -- incremental updates do not affect tags.c_items, so that may still get -- out-of-sync. -CREATE OR REPLACE FUNCTION tag_vn_calc(uvid integer) RETURNS void AS $$ +CREATE OR REPLACE FUNCTION tag_vn_calc(uvid vndbid) RETURNS void AS $$ BEGIN - IF uvid IS NULL THEN - DROP INDEX IF EXISTS tags_vn_inherit_tag_vid; - TRUNCATE tags_vn_inherit; - ELSE - DELETE FROM tags_vn_inherit WHERE vid = uvid; - END IF; - - INSERT INTO tags_vn_inherit (tag, vid, rating, spoiler) - -- Group votes to generate a list of directly-upvoted (vid, tag) pairs. - -- This is essentually the same as the tag listing on VN pages. - WITH RECURSIVE t_avg(tag, vid, vote, spoiler) AS ( - SELECT tv.tag, tv.vid, AVG(tv.vote)::real, CASE WHEN COUNT(tv.spoiler) = 0 THEN MIN(t.defaultspoil) ELSE AVG(tv.spoiler)::real END - FROM tags_vn tv - JOIN tags t ON t.id = tv.tag - LEFT JOIN users u ON u.id = tv.uid - WHERE NOT tv.ignore AND t.state = 2 - AND (u.id IS NULL OR u.perm_tag) - AND vid NOT IN(SELECT id FROM vn WHERE hidden) - AND (uvid IS NULL OR vid = uvid) - GROUP BY tv.tag, tv.vid - HAVING AVG(tv.vote) > 0 - -- Add parent tags - ), t_all(lvl, tag, vid, vote, spoiler) AS ( - SELECT 15, * FROM t_avg - UNION ALL - SELECT ta.lvl-1, tp.parent, ta.vid, ta.vote, ta.spoiler - FROM t_all ta - JOIN tags_parents tp ON tp.tag = ta.tag - WHERE ta.lvl > 0 - ) - -- Merge - SELECT tag, vid, AVG(vote) - , (CASE WHEN MIN(spoiler) > 1.3 THEN 2 WHEN MIN(spoiler) > 0.4 THEN 1 ELSE 0 END)::smallint - FROM t_all - WHERE tag IN(SELECT id FROM tags WHERE searchable) - GROUP BY tag, vid; + -- tags_vn_direct + WITH new (tag, vid, rating, count, spoiler, lie) AS ( + -- Rows that we want + SELECT tv.tag, tv.vid + -- https://vndb.org/t13470.28 -> (z || 3) * ((x-y) / (x+y)) + -- No exception is made for the x==y case, a score of 0 seems better to me. + , (COALESCE(AVG(tv.vote) filter (where tv.vote > 0), 3) * SUM(sign(tv.vote)) / COUNT(tv.vote))::real + , LEAST( COUNT(tv.vote) filter (where tv.vote > 0), 32000 )::smallint + , CASE WHEN COUNT(spoiler) = 0 THEN MIN(t.defaultspoil) WHEN AVG(spoiler) > 1.3 THEN 2 WHEN AVG(spoiler) > 0.4 THEN 1 ELSE 0 END + , count(lie) filter(where lie) > 0 AND count(lie) filter (where lie) >= count(lie) filter(where not lie) + FROM tags_vn tv + JOIN tags t ON t.id = tv.tag + LEFT JOIN users u ON u.id = tv.uid + WHERE NOT t.hidden + AND NOT tv.ignore AND (u.id IS NULL OR u.perm_tag) + AND vid NOT IN(SELECT id FROM vn WHERE hidden) + AND (uvid IS NULL OR vid = uvid) + GROUP BY tv.tag, tv.vid + HAVING SUM(sign(tv.vote)) > 0 + ), n AS ( + -- Add existing rows from tags_vn_direct as NULLs, so we can delete them during merge + SELECT coalesce(a.tag, b.tag) AS tag, coalesce(a.vid, b.vid) AS vid, a.rating, a.count, a.spoiler, a.lie + FROM new a + FULL OUTER JOIN (SELECT tag, vid FROM tags_vn_direct WHERE uvid IS NULL OR vid = uvid) b on (a.tag, a.vid) = (b.tag, b.vid) + -- Now merge + ) MERGE INTO tags_vn_direct o USING n ON (n.tag, n.vid) = (o.tag, o.vid) + WHEN NOT MATCHED THEN INSERT (tag, vid, rating, count, spoiler, lie) VALUES (n.tag, n.vid, n.rating, (n)."count", n.spoiler, n.lie) + WHEN MATCHED AND n.rating IS NULL THEN DELETE + WHEN MATCHED AND (o.rating, o.count, o.spoiler, o.lie) IS DISTINCT FROM (n.rating, n.count, n.spoiler, n.lie) THEN + UPDATE SET rating = n.rating, count = n.count, spoiler = n.spoiler, lie = n.lie; + + -- tags_vn_inherit, based on the data from tags_vn_direct + WITH new (tag, vid, rating, spoiler, lie) AS ( + -- Add parent tags to tags_vn_direct + WITH RECURSIVE t_all(lvl, tag, vid, vote, spoiler, lie) AS ( + SELECT 15, tag, vid, rating, spoiler, lie + FROM tags_vn_direct + WHERE (uvid IS NULL OR vid = uvid) + UNION ALL + SELECT ta.lvl-1, tp.parent, ta.vid, ta.vote, ta.spoiler, ta.lie + FROM t_all ta + JOIN tags_parents tp ON tp.id = ta.tag + WHERE ta.lvl > 0 + -- Merge duplicates + ) SELECT tag, vid, AVG(vote)::real, MIN(spoiler), bool_and(lie) + FROM t_all + WHERE tag IN(SELECT id FROM tags WHERE searchable) + GROUP BY tag, vid + ), n AS ( + -- Add existing rows from tags_vn_inherit as NULLs, so we can delete them during merge + SELECT coalesce(a.tag, b.tag) AS tag, coalesce(a.vid, b.vid) AS vid, a.rating, a.spoiler, a.lie + FROM new a + FULL OUTER JOIN (SELECT tag, vid FROM tags_vn_inherit WHERE uvid IS NULL OR vid = uvid) b on (a.tag, a.vid) = (b.tag, b.vid) + -- Now merge + ) MERGE INTO tags_vn_inherit o USING n ON (n.tag, n.vid) = (o.tag, o.vid) + WHEN NOT MATCHED THEN INSERT (tag, vid, rating, spoiler, lie) VALUES (n.tag, n.vid, n.rating, n.spoiler, n.lie) + WHEN MATCHED AND n.rating IS NULL THEN DELETE + WHEN MATCHED AND (o.rating, o.spoiler, o.lie) IS DISTINCT FROM (n.rating, n.spoiler, n.lie) THEN + UPDATE SET rating = n.rating, spoiler = n.spoiler, lie = n.lie; IF uvid IS NULL THEN - CREATE INDEX tags_vn_inherit_tag_vid ON tags_vn_inherit (tag, vid); UPDATE tags SET c_items = (SELECT COUNT(*) FROM tags_vn_inherit WHERE tag = id); END IF; - RETURN; END; -$$ LANGUAGE plpgsql SECURITY DEFINER; +$$ LANGUAGE plpgsql; + -- Recalculate traits_chars. Pretty much same thing as tag_vn_calc(). -CREATE OR REPLACE FUNCTION traits_chars_calc(ucid integer) RETURNS void AS $$ +CREATE OR REPLACE FUNCTION traits_chars_calc(ucid vndbid) RETURNS void AS $$ BEGIN - IF ucid IS NULL THEN - DROP INDEX IF EXISTS traits_chars_tid; - TRUNCATE traits_chars; - ELSE - DELETE FROM traits_chars WHERE cid = ucid; - END IF; - - INSERT INTO traits_chars (tid, cid, spoil) + WITH new (tid, cid, spoil, lie) AS ( -- all char<->trait links of the latest revisions, including chars inherited from child traits. -- (also includes non-searchable traits, because they could have a searchable trait as parent) - WITH RECURSIVE traits_chars_all(lvl, tid, cid, spoiler) AS ( - SELECT 15, tid, ct.id, spoil + WITH RECURSIVE t_all(lvl, tid, cid, spoiler, lie) AS ( + SELECT 15, tid, ct.id, spoil, lie FROM chars_traits ct WHERE id NOT IN(SELECT id from chars WHERE hidden) AND (ucid IS NULL OR ct.id = ucid) + AND NOT EXISTS (SELECT 1 FROM traits t WHERE t.id = ct.tid AND t.hidden) UNION ALL - SELECT lvl-1, tp.parent, tc.cid, tc.spoiler - FROM traits_chars_all tc - JOIN traits_parents tp ON tp.trait = tc.tid + SELECT lvl-1, tp.parent, tc.cid, tc.spoiler, tc.lie + FROM t_all tc + JOIN traits_parents tp ON tp.id = tc.tid JOIN traits t ON t.id = tp.parent - WHERE t.state = 2 + WHERE NOT t.hidden AND tc.lvl > 0 ) -- now grouped by (tid, cid), with non-searchable traits filtered out SELECT tid, cid - , (CASE WHEN MIN(spoiler) > 1.3 THEN 2 WHEN MIN(spoiler) > 0.7 THEN 1 ELSE 0 END)::smallint AS spoiler - FROM traits_chars_all + , (CASE WHEN MIN(spoiler) > 1.3 THEN 2 WHEN MIN(spoiler) > 0.7 THEN 1 ELSE 0 END)::smallint + , bool_and(lie) + FROM t_all WHERE tid IN(SELECT id FROM traits WHERE searchable) - GROUP BY tid, cid; + GROUP BY tid, cid + ), n AS ( + -- Add existing rows from traits_chars as NULLs, so we can delete them during merge + SELECT coalesce(a.tid, b.tid) AS tid, coalesce(a.cid, b.cid) AS cid, a.spoil, a.lie + FROM new a + FULL OUTER JOIN (SELECT tid, cid FROM traits_chars WHERE ucid IS NULL OR cid = ucid) b on (a.tid, a.cid) = (b.tid, b.cid) + -- Now merge + ) MERGE INTO traits_chars o USING n ON (n.tid, n.cid) = (o.tid, o.cid) + WHEN NOT MATCHED THEN INSERT (tid, cid, spoil, lie) VALUES (n.tid, n.cid, n.spoil, n.lie) + WHEN MATCHED AND n.spoil IS NULL THEN DELETE + WHEN MATCHED AND (o.spoil, o.lie) IS DISTINCT FROM (n.spoil, n.lie) THEN + UPDATE SET spoil = n.spoil, lie = n.lie; IF ucid IS NULL THEN - CREATE INDEX traits_chars_tid ON traits_chars (tid); UPDATE traits SET c_items = (SELECT COUNT(*) FROM traits_chars WHERE tid = id); END IF; RETURN; END; -$$ LANGUAGE plpgsql SECURITY DEFINER; +$$ LANGUAGE plpgsql; + + + +CREATE OR REPLACE FUNCTION quotes_rand_calc() RETURNS void AS $$ + WITH q(id, vid, score) AS ( + SELECT id, vid, score FROM quotes q WHERE score > 0 AND NOT hidden AND EXISTS(SELECT 1 FROM vn v WHERE v.id = q.vid AND NOT v.hidden) + ), r(id,rand) AS ( + SELECT id, -- 'rand' is chosen such that each VN has an equal probability to be selected, regardless of how many quotes it has. + ( ((dense_rank() OVER (ORDER BY vid)) - 1)::real -- [0..n-1] cumulative count of distinct VNs + + ((sum(score) OVER (PARTITION BY vid ORDER BY id) - score)::float / (sum(score) OVER (PARTITION BY vid))) -- [0,1) cumulative normalized score of this quote + ) / (SELECT count(DISTINCT vid) FROM q) + FROM q + ), u AS ( + UPDATE quotes SET rand = NULL WHERE rand IS NOT NULL AND NOT EXISTS(SELECT 1 FROM r WHERE quotes.id = r.id) + ) UPDATE quotes SET rand = r.rand FROM r WHERE quotes.rand IS DISTINCT FROM r.rand AND r.id = quotes.id; +$$ LANGUAGE SQL; + -- Fully recalculate all rows in stats_cache @@ -327,14 +610,14 @@ BEGIN UPDATE stats_cache SET count = (SELECT COUNT(*) FROM producers WHERE hidden = FALSE) WHERE section = 'producers'; UPDATE stats_cache SET count = (SELECT COUNT(*) FROM chars WHERE hidden = FALSE) WHERE section = 'chars'; UPDATE stats_cache SET count = (SELECT COUNT(*) FROM staff WHERE hidden = FALSE) WHERE section = 'staff'; - UPDATE stats_cache SET count = (SELECT COUNT(*) FROM tags WHERE state = 2) WHERE section = 'tags'; - UPDATE stats_cache SET count = (SELECT COUNT(*) FROM traits WHERE state = 2) WHERE section = 'traits'; + UPDATE stats_cache SET count = (SELECT COUNT(*) FROM tags WHERE hidden = FALSE) WHERE section = 'tags'; + UPDATE stats_cache SET count = (SELECT COUNT(*) FROM traits WHERE hidden = FALSE) WHERE section = 'traits'; END; $$ LANGUAGE plpgsql; -- Create ulist labels for new users. -CREATE OR REPLACE FUNCTION ulist_labels_create(integer) RETURNS void AS $$ +CREATE OR REPLACE FUNCTION ulist_labels_create(vndbid) RETURNS void AS $$ INSERT INTO ulist_labels (uid, id, label, private) VALUES ($1, 1, 'Playing', false), ($1, 2, 'Finished', false), @@ -347,6 +630,59 @@ CREATE OR REPLACE FUNCTION ulist_labels_create(integer) RETURNS void AS $$ $$ LANGUAGE SQL; +-- Returns generic information for almost every supported vndbid + num. +-- Not currently supported: ch#, cv#, sf# +-- Some oddities: +-- * The given user title preferences are not used for explicit revisions. +-- * Trait names are prefixed with their group name ("Group > Trait"), but only for non-revisions. +-- +-- Returned fields: +-- * title - Titles array, same format as returned by vnt(). +-- For users this is their username, not displayname. +-- * uid - User who created/initiated this entry. Used in notification listings and reports +-- * hidden - Whether this entry is 'hidden' or private. Used for the reporting function & framework_ object. +-- For edits this info comes from the revision itself, not the final entry. +-- Interpretation of this field is dependent on the entry type, For most database entries, +-- 'hidden' means "partially visible if you know the ID, but not shown in regular listings". +-- For threads it means "totally invisible, does not exist". +-- * locked - Whether this entry is 'locked'. Used for the framework_ object. +CREATE OR REPLACE FUNCTION item_info(titleprefs, vndbid, int, out ret item_info_type) AS $$ +BEGIN + -- x# + IF $3 IS NULL THEN CASE vndbid_type($2) + WHEN 'v' THEN SELECT v.title, NULL::vndbid, v.hidden, v.locked INTO ret FROM vnt($1) v WHERE v.id = $2; + WHEN 'r' THEN SELECT r.title, NULL::vndbid, r.hidden, r.locked INTO ret FROM releasest($1) r WHERE r.id = $2; + WHEN 'p' THEN SELECT p.title, NULL::vndbid, p.hidden, p.locked INTO ret FROM producerst($1) p WHERE p.id = $2; + WHEN 'c' THEN SELECT c.title, NULL::vndbid, c.hidden, c.locked INTO ret FROM charst($1) c WHERE c.id = $2; + WHEN 'd' THEN SELECT ARRAY[NULL, d.title, NULL, d.title], NULL::vndbid, d.hidden, d.locked INTO ret FROM docs d WHERE d.id = $2; + WHEN 'g' THEN SELECT ARRAY[NULL, g.name, NULL, g.name], NULL::vndbid, g.hidden, g.locked INTO ret FROM tags g WHERE g.id = $2; + WHEN 'i' THEN SELECT ARRAY[NULL, COALESCE(g.name||' > ', '')||i.name, NULL, COALESCE(g.name||' > ', '')||i.name], NULL::vndbid, i.hidden, i.locked INTO ret FROM traits i LEFT JOIN traits g ON g.id = i.gid WHERE i.id = $2; + WHEN 's' THEN SELECT s.title, NULL::vndbid, s.hidden, s.locked INTO ret FROM staff_aliast($1) s WHERE s.id = $2 AND s.aid = s.main; + WHEN 't' THEN SELECT ARRAY[NULL, t.title, NULL, t.title], NULL::vndbid, t.hidden OR t.private, t.locked INTO ret FROM threads t WHERE t.id = $2; + WHEN 'w' THEN SELECT v.title, w.uid, w.c_flagged, w.locked INTO ret FROM reviews w JOIN vnt v ON v.id = w.vid WHERE w.id = $2; + WHEN 'u' THEN SELECT ARRAY[NULL, COALESCE(u.username, u.id::text), NULL, COALESCE(u.username, u.id::text)], NULL::vndbid, u.username IS NULL, FALSE INTO ret FROM users u WHERE u.id = $2; + ELSE NULL; + END CASE; + -- x#.# + ELSE CASE vndbid_type($2) + WHEN 'v' THEN SELECT ARRAY[v.olang::text, COALESCE(vo.latin, vo.title), v.olang::text, CASE WHEN vo.latin IS NULL THEN '' ELSE vo.title END], h.requester, h.ihid, h.ilock INTO ret + FROM changes h JOIN vn_hist v ON h.id = v.chid JOIN vn_titles_hist vo ON h.id = vo.chid AND vo.lang = v.olang WHERE h.itemid = $2 AND h.rev = $3; + WHEN 'r' THEN SELECT ARRAY[r.olang::text, COALESCE(ro.latin, ro.title), r.olang::text, CASE WHEN ro.latin IS NULL THEN '' ELSE ro.title END], h.requester, h.ihid, h.ilock INTO ret + FROM changes h JOIN releases_hist r ON h.id = r.chid JOIN releases_titles_hist ro ON h.id = ro.chid AND ro.lang = r.olang WHERE h.itemid = $2 AND h.rev = $3; + WHEN 'p' THEN SELECT ARRAY[p.lang::text, COALESCE(p.latin, p.name), p.lang::text, p.name], h.requester, h.ihid, h.ilock INTO ret FROM changes h JOIN producers_hist p ON h.id = p.chid WHERE h.itemid = $2 AND h.rev = $3; + WHEN 'c' THEN SELECT ARRAY[cm.c_lang::text, COALESCE(c.latin, c.name), cm.c_lang::text, c.name], h.requester, h.ihid, h.ilock INTO ret FROM changes h JOIN chars cm ON cm.id = h.itemid JOIN chars_hist c ON h.id = c.chid WHERE h.itemid = $2 AND h.rev = $3; + WHEN 'd' THEN SELECT ARRAY[NULL, d.title, NULL, d.title ], h.requester, h.ihid, h.ilock INTO ret FROM changes h JOIN docs_hist d ON h.id = d.chid WHERE h.itemid = $2 AND h.rev = $3; + WHEN 'g' THEN SELECT ARRAY[NULL, g.name, NULL, g.name ], h.requester, h.ihid, h.ilock INTO ret FROM changes h JOIN tags_hist g ON h.id = g.chid WHERE h.itemid = $2 AND h.rev = $3; + WHEN 'i' THEN SELECT ARRAY[NULL, i.name, NULL, i.name ], h.requester, h.ihid, h.ilock INTO ret FROM changes h JOIN traits_hist i ON h.id = i.chid WHERE h.itemid = $2 AND h.rev = $3; + WHEN 's' THEN SELECT ARRAY[s.lang::text, COALESCE(sa.latin, sa.name), s.lang::text, sa.name], h.requester, h.ihid, h.ilock INTO ret FROM changes h JOIN staff_hist s ON h.id = s.chid JOIN staff_alias_hist sa ON sa.chid = s.chid AND sa.aid = s.main WHERE h.itemid = $2 AND h.rev = $3; + WHEN 't' THEN SELECT ARRAY[NULL, t.title, NULL, t.title], tp.uid, t.hidden OR t.private OR tp.hidden IS NOT NULL, t.locked INTO ret FROM threads t JOIN threads_posts tp ON tp.tid = t.id WHERE t.id = $2 AND tp.num = $3; + WHEN 'w' THEN SELECT v.title, wp.uid, w.c_flagged OR wp.hidden IS NOT NULL, w.locked INTO ret FROM reviews w JOIN vnt($1) v ON v.id = w.vid JOIN reviews_posts wp ON wp.id = w.id WHERE w.id = $2 AND wp.num = $3; + ELSE NULL; + END CASE; + END IF; +END; +$$ LANGUAGE plpgsql STABLE; + ---------------------------------------------------------- @@ -356,17 +692,14 @@ $$ LANGUAGE SQL; -- The two functions below are utility functions used by the item-specific functions in editfunc.sql -- create temporary table for generic revision info, and returns the chid of the revision being edited (or NULL). -CREATE OR REPLACE FUNCTION edit_revtable(xtype dbentry_type, xitemid integer, xrev integer) RETURNS integer AS $$ +CREATE OR REPLACE FUNCTION edit_revtable(xitemid vndbid, xrev integer) RETURNS integer AS $$ DECLARE - ret integer; x record; BEGIN BEGIN CREATE TEMPORARY TABLE edit_revision ( - type dbentry_type NOT NULL, - itemid integer, - requester integer, - ip inet, + itemid vndbid, + requester vndbid, comments text, ihid boolean, ilock boolean @@ -374,136 +707,109 @@ BEGIN EXCEPTION WHEN duplicate_table THEN TRUNCATE edit_revision; END; - SELECT INTO x id, ihid, ilock FROM changes c WHERE type = xtype AND itemid = xitemid AND rev = xrev; - INSERT INTO edit_revision (type, itemid, ihid, ilock) VALUES (xtype, xitemid, COALESCE(x.ihid, FALSE), COALESCE(x.ilock, FALSE)); + SELECT INTO x id, ihid, ilock FROM changes c WHERE itemid = xitemid AND rev = xrev; + INSERT INTO edit_revision (itemid, ihid, ilock) VALUES (xitemid, COALESCE(x.ihid, FALSE), COALESCE(x.ilock, FALSE)); RETURN x.id; END; $$ LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION edit_commit() RETURNS edit_rettype AS $$ -DECLARE - ret edit_rettype; - xtype dbentry_type; -BEGIN - SELECT type INTO xtype FROM edit_revision; - SELECT itemid INTO ret.itemid FROM edit_revision; - -- figure out revision number - SELECT MAX(rev)+1 INTO ret.rev FROM changes WHERE type = xtype AND itemid = ret.itemid; - SELECT COALESCE(ret.rev, 1) INTO ret.rev; - -- insert DB item - IF ret.itemid IS NULL THEN - CASE xtype - WHEN 'v' THEN INSERT INTO vn DEFAULT VALUES RETURNING id INTO ret.itemid; - WHEN 'r' THEN INSERT INTO releases DEFAULT VALUES RETURNING id INTO ret.itemid; - WHEN 'p' THEN INSERT INTO producers DEFAULT VALUES RETURNING id INTO ret.itemid; - WHEN 'c' THEN INSERT INTO chars DEFAULT VALUES RETURNING id INTO ret.itemid; - WHEN 's' THEN INSERT INTO staff DEFAULT VALUES RETURNING id INTO ret.itemid; - WHEN 'd' THEN INSERT INTO docs DEFAULT VALUES RETURNING id INTO ret.itemid; - END CASE; - END IF; - -- insert change - INSERT INTO changes (type, itemid, rev, requester, ip, comments, ihid, ilock) - SELECT type, ret.itemid, ret.rev, requester, ip, comments, ihid, ilock FROM edit_revision RETURNING id INTO ret.chid; - RETURN ret; -END; -$$ LANGUAGE plpgsql; - - - -- Check for stuff to be done when an item has been changed -CREATE OR REPLACE FUNCTION edit_committed(xtype dbentry_type, xedit edit_rettype) RETURNS void AS $$ +CREATE OR REPLACE FUNCTION edit_committed(nchid integer, nitemid vndbid, nrev integer) RETURNS void AS $$ DECLARE xoldchid integer; BEGIN - SELECT id INTO xoldchid FROM changes WHERE type = xtype AND itemid = xedit.itemid AND rev = xedit.rev-1; + SELECT id INTO xoldchid FROM changes WHERE itemid = nitemid AND rev = nrev-1; - -- Set c_search to NULL and notify when - -- 1. A new VN entry is created - -- 2. The vn title/original/alias has changed - IF xtype = 'v' THEN - IF -- 1. - xoldchid IS NULL OR - -- 2. - EXISTS(SELECT 1 FROM vn_hist v1, vn_hist v2 WHERE (v2.title <> v1.title OR v2.original <> v1.original OR v2.alias <> v1.alias) AND v1.chid = xoldchid AND v2.chid = xedit.chid) - THEN - UPDATE vn SET c_search = NULL WHERE id = xedit.itemid; - NOTIFY vnsearch; - END IF; + -- Update search_cache + IF vndbid_type(nitemid) IN('v','r','c','p','s','g','i') THEN + PERFORM update_search(nitemid); END IF; - -- Set related vn.c_search columns to NULL and notify when + -- Update search_cache for related VNs when -- 1. A new release is created -- 2. A release has been hidden or unhidden - -- 3. The release title/original has changed + -- 3. The releases_titles have changed -- 4. The releases_vn table differs from a previous revision - IF xtype = 'r' THEN + IF vndbid_type(nitemid) = 'r' THEN IF -- 1. xoldchid IS NULL OR -- 2. - EXISTS(SELECT 1 FROM changes c1, changes c2 WHERE c1.ihid IS DISTINCT FROM c2.ihid AND c1.id = xedit.chid AND c2.id = xoldchid) OR + EXISTS(SELECT 1 FROM changes c1, changes c2 WHERE c1.ihid IS DISTINCT FROM c2.ihid AND c1.id = nchid AND c2.id = xoldchid) OR -- 3. - EXISTS(SELECT 1 FROM releases_hist r1, releases_hist r2 WHERE (r2.title <> r1.title OR r2.original <> r1.original) AND r1.chid = xoldchid AND r2.chid = xedit.chid) OR + EXISTS(SELECT title, latin FROM releases_titles_hist WHERE chid = xoldchid EXCEPT SELECT title, latin FROM releases_titles_hist WHERE chid = nchid) OR + EXISTS(SELECT title, latin FROM releases_titles_hist WHERE chid = nchid EXCEPT SELECT title, latin FROM releases_titles_hist WHERE chid = xoldchid) OR -- 4. - EXISTS(SELECT vid FROM releases_vn_hist WHERE chid = xoldchid EXCEPT SELECT vid FROM releases_vn_hist WHERE chid = xedit.chid) OR - EXISTS(SELECT vid FROM releases_vn_hist WHERE chid = xedit.chid EXCEPT SELECT vid FROM releases_vn_hist WHERE chid = xoldchid) + EXISTS(SELECT vid FROM releases_vn_hist WHERE chid = xoldchid EXCEPT SELECT vid FROM releases_vn_hist WHERE chid = nchid) OR + EXISTS(SELECT vid FROM releases_vn_hist WHERE chid = nchid EXCEPT SELECT vid FROM releases_vn_hist WHERE chid = xoldchid) THEN - UPDATE vn SET c_search = NULL WHERE id IN(SELECT vid FROM releases_vn_hist WHERE chid IN(xedit.chid, xoldchid)); - NOTIFY vnsearch; + PERFORM update_search(vid) FROM releases_vn_hist WHERE chid IN(nchid, xoldchid); END IF; END IF; + -- Update drm.c_ref + IF vndbid_type(nitemid) = 'r' THEN + WITH + old (id) AS (SELECT r.drm FROM releases_drm_hist r, changes c WHERE r.chid = xoldchid AND c.id = xoldchid AND NOT c.ihid), + new (id) AS (SELECT r.drm FROM releases_drm_hist r, changes c WHERE r.chid = nchid AND c.id = nchid AND NOT c.ihid), + ins AS (UPDATE drm SET c_ref = c_ref + 1 WHERE id IN(SELECT id FROM new EXCEPT SELECT id FROM old)) + UPDATE drm SET c_ref = c_ref - 1 WHERE id IN(SELECT id FROM old EXCEPT SELECT id FROM new); + END IF; + + -- Update tags_vn_* when the VN's hidden flag is changed + IF vndbid_type(nitemid) = 'v' AND EXISTS(SELECT 1 FROM changes c1, changes c2 WHERE c1.ihid IS DISTINCT FROM c2.ihid AND c1.id = nchid AND c2.id = xoldchid) THEN + PERFORM tag_vn_calc(nitemid); + END IF; + + -- Ensure chars.c_lang is updated when the related VN or char has been edited + -- (the cache also depends on vn.c_released but isn't run when that is updated; + -- not an issue, the c_released is only there as rare fallback) + IF vndbid_type(nitemid) IN('c','v') THEN + WITH x(id,lang) AS ( + SELECT DISTINCT ON (cv.id) cv.id, v.olang + FROM chars_vns cv + JOIN vn v ON v.id = cv.vid + WHERE cv.vid = nitemid OR cv.id = nitemid + ORDER BY cv.id, v.hidden, v.c_released + ) UPDATE chars c SET c_lang = x.lang FROM x WHERE c.id = x.id AND c.c_lang <> x.lang; + END IF; + -- Call update_vncache() for related VNs when a release has been created or edited -- (This could be made more specific, but update_vncache() is fast enough that it's not worth the complexity) - IF xtype = 'r' THEN + IF vndbid_type(nitemid) = 'r' THEN PERFORM update_vncache(vid) FROM ( - SELECT DISTINCT vid FROM releases_vn_hist WHERE chid IN(xedit.chid, xoldchid) + SELECT DISTINCT vid FROM releases_vn_hist WHERE chid IN(nchid, xoldchid) ) AS v(vid); END IF; -- Call traits_chars_calc() for characters to update the traits cache - IF xtype = 'c' THEN - PERFORM traits_chars_calc(xedit.itemid); - END IF; - - -- Call notify_dbdel() if an entry has been deleted - -- Call notify_listdel() if a vn/release entry has been deleted - IF xoldchid IS NOT NULL - AND EXISTS(SELECT 1 FROM changes WHERE id = xoldchid AND NOT ihid) - AND EXISTS(SELECT 1 FROM changes WHERE id = xedit.chid AND ihid) - THEN - PERFORM notify_dbdel(xtype, xedit); - IF xtype = 'v' OR xtype = 'r' THEN - PERFORM notify_listdel(xtype, xedit); - END IF; + IF vndbid_type(nitemid) = 'c' THEN + PERFORM traits_chars_calc(nitemid); END IF; - -- Call notify_dbedit() if a non-hidden entry has been edited - IF xoldchid IS NOT NULL AND EXISTS(SELECT 1 FROM changes WHERE id = xedit.chid AND NOT ihid) - THEN - PERFORM notify_dbedit(xtype, xedit); - END IF; + -- Create edit notifications + INSERT INTO notifications (uid, ntype, iid, num) + SELECT n.uid, n.ntype, n.iid, n.num FROM changes c, notify(nitemid, c.rev, c.requester) n WHERE c.id = nchid; -- Make sure all visual novels linked to a release have a corresponding entry -- in ulist_vns for users who have the release in rlists. This is action (3) in -- update_vnlist_rlist(). - IF xtype = 'r' AND xoldchid IS NOT NULL + IF vndbid_type(nitemid) = 'r' AND xoldchid IS NOT NULL THEN INSERT INTO ulist_vns (uid, vid) - SELECT rl.uid, rv.vid FROM rlists rl JOIN releases_vn rv ON rv.id = rl.rid WHERE rl.rid = xedit.itemid + SELECT rl.uid, rv.vid FROM rlists rl JOIN releases_vn rv ON rv.id = rl.rid WHERE rl.rid = nitemid ON CONFLICT (uid, vid) DO NOTHING; END IF; -- Call update_images_cache() where appropriate - IF xtype = 'c' + IF vndbid_type(nitemid) = 'c' THEN - PERFORM update_images_cache(image) FROM chars_hist WHERE chid IN(xoldchid,xedit.chid) AND image IS NOT NULL; + PERFORM update_images_cache(image) FROM chars_hist WHERE chid IN(xoldchid,nchid) AND image IS NOT NULL; END IF; - IF xtype = 'v' + IF vndbid_type(nitemid) = 'v' THEN - PERFORM update_images_cache(image) FROM vn_hist WHERE chid IN(xoldchid,xedit.chid) AND image IS NOT NULL; - PERFORM update_images_cache(scr) FROM vn_screenshots_hist WHERE chid IN(xoldchid,xedit.chid); + PERFORM update_images_cache(image) FROM vn_hist WHERE chid IN(xoldchid,nchid) AND image IS NOT NULL; + PERFORM update_images_cache(scr) FROM vn_screenshots_hist WHERE chid IN(xoldchid,nchid); END IF; END; $$ LANGUAGE plpgsql; @@ -516,70 +822,129 @@ $$ LANGUAGE plpgsql; ---------------------------------------------------------- --- called when an entry has been deleted -CREATE OR REPLACE FUNCTION notify_dbdel(xtype dbentry_type, xedit edit_rettype) RETURNS void AS $$ - INSERT INTO notifications (ntype, uid, iid, num, c_title, c_byuser) - SELECT DISTINCT 'dbdel'::notification_ntype, h.requester, vndbid(xtype::text, xedit.itemid), xedit.rev, x.title, h2.requester - FROM changes h - -- join info about the deletion itself - JOIN changes h2 ON h2.id = xedit.chid - -- Fetch the latest name/title of the entry - -- this method may look a bit unintuitive, but it's way faster than doing LEFT JOINs - JOIN ( SELECT v.title FROM vn v WHERE xtype = 'v' AND v.id = xedit.itemid - UNION SELECT r.title FROM releases r WHERE xtype = 'r' AND r.id = xedit.itemid - UNION SELECT p.name FROM producers p WHERE xtype = 'p' AND p.id = xedit.itemid - UNION SELECT c.name FROM chars c WHERE xtype = 'c' AND c.id = xedit.itemid - UNION SELECT d.title FROM docs d WHERE xtype = 'd' AND d.id = xedit.itemid - UNION SELECT sa.name FROM staff s JOIN staff_alias sa ON sa.aid = s.aid WHERE xtype = 's' AND s.id = xedit.itemid - ) x(title) ON true - WHERE h.type = xtype AND h.itemid = xedit.itemid - AND h.requester <> 1 -- exclude Multi - AND h.requester <> h2.requester; -- exclude the user who deleted the entry -$$ LANGUAGE sql; - - - --- Called when a non-deleted item has been edited. -CREATE OR REPLACE FUNCTION notify_dbedit(xtype dbentry_type, xedit edit_rettype) RETURNS void AS $$ - INSERT INTO notifications (ntype, uid, iid, num, c_title, c_byuser) - SELECT DISTINCT 'dbedit'::notification_ntype, h.requester, vndbid(xtype::text, xedit.itemid), xedit.rev, x.title, h2.requester - FROM changes h - -- join info about the edit itself - JOIN changes h2 ON h2.id = xedit.chid - -- Fetch the latest name/title of the entry - JOIN ( SELECT v.title FROM vn v WHERE xtype = 'v' AND v.id = xedit.itemid - UNION SELECT r.title FROM releases r WHERE xtype = 'r' AND r.id = xedit.itemid - UNION SELECT p.name FROM producers p WHERE xtype = 'p' AND p.id = xedit.itemid - UNION SELECT c.name FROM chars c WHERE xtype = 'c' AND c.id = xedit.itemid - UNION SELECT d.title FROM docs d WHERE xtype = 'd' AND d.id = xedit.itemid - UNION SELECT sa.name FROM staff s JOIN staff_alias sa ON sa.aid = s.aid WHERE xtype = 's' AND s.id = xedit.itemid - ) x(title) ON true - WHERE h.type = xtype AND h.itemid = xedit.itemid - AND h.requester <> h2.requester -- exclude the user who edited the entry - AND h2.requester <> 1 -- exclude edits by Multi - -- exclude users who don't want this notify - AND EXISTS(SELECT 1 FROM users u WHERE u.id = h.requester AND notify_dbedit); -$$ LANGUAGE sql; - - +-- Called after a certain event has occurred (new edit, post, etc). +-- 'iid' and 'num' identify the item that has been created. +-- 'uid' indicates who created the item, providing an easy method of not creating a notification for that user. +-- (can technically be fetched with a DB lookup, too) +CREATE OR REPLACE FUNCTION notify(iid vndbid, num integer, uid vndbid) RETURNS TABLE (uid vndbid, ntype notification_ntype[], iid vndbid, num int) AS $$ + SELECT uid, array_agg(ntype), $1, $2 + FROM ( --- called when a VN/release entry has been deleted -CREATE OR REPLACE FUNCTION notify_listdel(xtype dbentry_type, xedit edit_rettype) RETURNS void AS $$ - INSERT INTO notifications (ntype, uid, iid, num, c_title, c_byuser) - SELECT DISTINCT 'listdel'::notification_ntype, u.uid, vndbid(xtype::text, xedit.itemid), xedit.rev, x.title, c.requester - -- look for users who should get this notify - FROM ( - SELECT uid FROM ulist_vns WHERE xtype = 'v' AND vid = xedit.itemid - UNION SELECT uid FROM rlists WHERE xtype = 'r' AND rid = xedit.itemid - ) u - -- fetch info about this edit - JOIN changes c ON c.id = xedit.chid - JOIN ( - SELECT title FROM vn WHERE xtype = 'v' AND id = xedit.itemid - UNION SELECT title FROM releases WHERE xtype = 'r' AND id = xedit.itemid - ) x ON true - WHERE c.requester <> u.uid; -$$ LANGUAGE sql; + -- pm + SELECT 'pm'::notification_ntype, u.id + FROM threads_boards tb + JOIN users u ON u.id = tb.iid + WHERE vndbid_type($1) = 't' AND tb.tid = $1 AND tb.type = 'u' + AND NOT EXISTS(SELECT 1 FROM notification_subs ns WHERE ns.iid = $1 AND ns.uid = tb.iid AND ns.subnum = false) + + -- dbdel + UNION + SELECT 'dbdel', c_all.requester + FROM changes c_cur, changes c_all, changes c_pre + WHERE c_cur.itemid = $1 AND c_cur.rev = $2 -- Current edit + AND c_pre.itemid = $1 AND c_pre.rev = $2-1 -- Previous edit, to check if .ihid changed + AND c_all.itemid = $1 -- All edits on this entry, to see whom to notify + AND c_cur.ihid AND NOT c_pre.ihid + AND $2 > 1 AND vndbid_type($1) IN('v', 'r', 'p', 'c', 's', 'd', 'g', 'i') + + -- listdel + UNION + SELECT 'listdel', u.uid + FROM changes c_cur, changes c_pre, + ( SELECT uid FROM ulist_vns WHERE vndbid_type($1) = 'v' AND vid = $1 -- TODO: Could use an index on ulist_vns.vid + UNION ALL + SELECT uid FROM rlists WHERE vndbid_type($1) = 'r' AND rid = $1 -- TODO: Could also use an index, but the rlists table isn't that large so it's still okay + ) u(uid) + WHERE c_cur.itemid = $1 AND c_cur.rev = $2 -- Current edit + AND c_pre.itemid = $1 AND c_pre.rev = $2-1 -- Previous edit, to check if .ihid changed + AND c_cur.ihid AND NOT c_pre.ihid + AND $2 > 1 AND vndbid_type($1) IN('v','r') + + -- dbedit + UNION + SELECT 'dbedit', c.requester + FROM changes c + JOIN users u ON u.id = c.requester + WHERE c.itemid = $1 + AND $2 > 1 AND vndbid_type($1) IN('v', 'r', 'p', 'c', 's', 'd', 'g', 'i') + AND $3 <> 'u1' -- Exclude edits by Multi + AND u.notify_dbedit + AND NOT EXISTS(SELECT 1 FROM notification_subs ns WHERE ns.iid = $1 AND ns.uid = c.requester AND ns.subnum = false) + + -- subedit + UNION + SELECT 'subedit', ns.uid + FROM notification_subs ns + WHERE $2 > 1 AND vndbid_type($1) IN('v', 'r', 'p', 'c', 's', 'd', 'g', 'i') + AND $3 <> 'u1' -- Exclude edits by Multi + AND ns.iid = $1 AND ns.subnum + + -- announce + UNION + SELECT 'announce', u.id + FROM threads t + JOIN threads_boards tb ON tb.tid = t.id + JOIN users u ON u.notify_announce + WHERE vndbid_type($1) = 't' AND $2 = 1 AND t.id = $1 AND tb.type = 'an' + + -- post (threads_posts) + UNION + SELECT 'post', u.id + FROM threads t, threads_posts tp + JOIN users u ON tp.uid = u.id + WHERE t.id = $1 AND tp.tid = $1 AND vndbid_type($1) = 't' AND $2 > 1 AND NOT t.private AND NOT t.hidden AND u.notify_post + AND NOT EXISTS(SELECT 1 FROM notification_subs ns WHERE ns.iid = $1 AND ns.uid = tp.uid AND ns.subnum = false) + + -- post (reviews_posts) + UNION + SELECT 'post', u.id + FROM reviews_posts wp + JOIN users u ON wp.uid = u.id + WHERE wp.id = $1 AND vndbid_type($1) = 'w' AND $2 IS NOT NULL AND u.notify_post + AND NOT EXISTS(SELECT 1 FROM notification_subs ns WHERE ns.iid = $1 AND ns.uid = wp.uid AND ns.subnum = false) + + -- subpost (threads_posts) + UNION + SELECT 'subpost', ns.uid + FROM threads t, notification_subs ns + WHERE t.id = $1 AND ns.iid = $1 AND vndbid_type($1) = 't' AND $2 > 1 AND NOT t.private AND NOT t.hidden AND ns.subnum + + -- subpost (reviews_posts) + UNION + SELECT 'subpost', ns.uid + FROM notification_subs ns + WHERE ns.iid = $1 AND vndbid_type($1) = 'w' AND $2 IS NOT NULL AND ns.subnum + + -- comment + UNION + SELECT 'comment', u.id + FROM reviews w + JOIN users u ON w.uid = u.id + WHERE w.id = $1 AND vndbid_type($1) = 'w' AND $2 IS NOT NULL AND u.notify_comment + AND NOT EXISTS(SELECT 1 FROM notification_subs ns WHERE ns.iid = $1 AND ns.uid = w.uid AND NOT ns.subnum) + + -- subreview + UNION + SELECT 'subreview', ns.uid + FROM reviews w, notification_subs ns + WHERE w.id = $1 AND vndbid_type($1) = 'w' AND $2 IS NULL AND ns.iid = w.vid AND ns.subreview + + -- subapply + UNION + SELECT 'subapply', uid + FROM notification_subs + WHERE subapply AND vndbid_type($1) = 'c' AND $2 IS NOT NULL + AND iid IN( + WITH new(tid) AS (SELECT tid FROM chars_traits_hist WHERE chid = (SELECT id FROM changes WHERE itemid = $1 AND rev = $2)), + old(tid) AS (SELECT tid FROM chars_traits_hist WHERE chid = (SELECT id FROM changes WHERE itemid = $1 AND $2 > 1 AND rev = $2-1)) + (SELECT tid FROM old EXCEPT SELECT tid FROM new) UNION (SELECT tid FROM new EXCEPT SELECT tid FROM old) + ) + + ) AS noti(ntype, uid) + WHERE uid <> $3 + AND uid <> 'u1' -- No announcements for Multi + GROUP BY uid; +$$ LANGUAGE SQL; @@ -593,60 +958,66 @@ $$ LANGUAGE sql; -- Returns the raw scrypt parameters (N, r, p and salt) for this user, in order -- to create an encrypted pass. Returns NULL if this user does not have a valid -- password. -CREATE OR REPLACE FUNCTION user_getscryptargs(integer) RETURNS bytea AS $$ +CREATE OR REPLACE FUNCTION user_getscryptargs(vndbid) RETURNS bytea AS $$ SELECT CASE WHEN length(passwd) = 46 THEN substring(passwd from 1 for 14) ELSE NULL END - FROM users WHERE id = $1 + FROM users_shadow WHERE id = $1 $$ LANGUAGE SQL SECURITY DEFINER; --- Create a new web session for this user (uid, scryptpass, token) -CREATE OR REPLACE FUNCTION user_login(integer, bytea, bytea) RETURNS boolean AS $$ - INSERT INTO sessions (uid, token, expires, type) SELECT $1, $3, NOW() + '1 month', 'web' FROM users - WHERE length($2) = 46 AND length($3) = 20 - AND id = $1 AND passwd = $2 +-- Create a new session for this user (uid, type, scryptpass, token) +CREATE OR REPLACE FUNCTION user_login(vndbid, session_type, bytea, bytea) RETURNS boolean AS $$ + INSERT INTO sessions (uid, token, expires, type) SELECT $1, $4, NOW() + '1 month', $2 FROM users_shadow + WHERE length($3) = 46 AND length($4) = 20 + AND id = $1 AND passwd = $3 AND $2 IN('web', 'api') RETURNING true $$ LANGUAGE SQL SECURITY DEFINER; -CREATE OR REPLACE FUNCTION user_logout(integer, bytea) RETURNS void AS $$ - DELETE FROM sessions WHERE uid = $1 AND token = $2 AND type = 'web' +CREATE OR REPLACE FUNCTION user_logout(vndbid, bytea) RETURNS void AS $$ + DELETE FROM sessions WHERE uid = $1 AND token = $2 AND type IN('web','api') $$ LANGUAGE SQL SECURITY DEFINER; --- Returns true if the given session token is valid. --- As a side effect, this also extends the expiration time of web sessions. -CREATE OR REPLACE FUNCTION user_isvalidsession(integer, bytea, session_type) RETURNS bool AS $$ +-- BIG WARNING: Do not use "IS NOT NULL" on the return value, it'll always +-- evaluate to false. Use 'IS DISTINCT FROM NULL' instead. +CREATE OR REPLACE FUNCTION user_validate_session(vndbid, bytea, session_type) RETURNS sessions AS $$ + -- Extends the expiration time of web and api sessions. UPDATE sessions SET expires = NOW() + '1 month' - WHERE uid = $1 AND token = $2 AND type = $3 AND $3 = 'web' + WHERE uid = $1 AND token = $2 AND type = $3 AND $3 IN('web', 'api') AND expires < NOW() + '1 month'::interval - '6 hours'::interval; - SELECT true FROM sessions WHERE uid = $1 AND token = $2 AND type = $3 AND expires > NOW(); + -- Update last use date for api2 sessions + UPDATE sessions SET expires = NOW() + WHERE uid = $1 AND token = $2 AND type = $3 AND $3 = 'api2' + AND (expires = added OR expires::date < 'today'::date); + SELECT * FROM sessions WHERE uid = $1 AND token = $2 AND type = $3 $$ LANGUAGE SQL SECURITY DEFINER; -CREATE OR REPLACE FUNCTION user_emailexists(text, integer) RETURNS boolean AS $$ - SELECT true FROM users WHERE lower(mail) = lower($1) AND ($2 IS NULL OR id <> $2) LIMIT 1 -$$ LANGUAGE SQL SECURITY DEFINER; +-- Used for duplicate email checks and user-by-email lookup for usermods. +CREATE OR REPLACE FUNCTION user_emailtoid(text) RETURNS TABLE (uid vndbid, mail text) AS $$ + SELECT id, mail FROM users_shadow WHERE hash_email(mail) = hash_email($1) +$$ LANGUAGE SQL SECURITY DEFINER ROWS 1; --- Create a password reset token. args: email, token. Returns: user id. +-- Store a password reset token. args: email, token. Returns: user id, actual email. -- Doesn't work for usermods, otherwise an attacker could use this function to -- gain access to all user's emails by obtaining a reset token of a usermod. -- Ideally Postgres itself would send the user an email so that the application -- calling this function doesn't even get the token, and thus can't get access -- to someone's account. But alas, that'd require a separate process. -CREATE OR REPLACE FUNCTION user_resetpass(text, bytea) RETURNS integer AS $$ +CREATE OR REPLACE FUNCTION user_resetpass(text, bytea, OUT vndbid, OUT text) AS $$ INSERT INTO sessions (uid, token, expires, type) - SELECT id, $2, NOW()+'1 week', 'pass' FROM users - WHERE lower(mail) = lower($1) AND length($2) = 20 AND NOT perm_usermod - RETURNING uid + SELECT id, $2, NOW()+'1 week', 'pass' FROM users_shadow + WHERE hash_email(mail) = hash_email($1) AND length($2) = 20 AND NOT perm_usermod + RETURNING uid, mail $$ LANGUAGE SQL SECURITY DEFINER; -- Changes the user's password and invalidates all existing sessions. args: uid, old_pass_or_reset_token, new_pass -CREATE OR REPLACE FUNCTION user_setpass(integer, bytea, bytea) RETURNS boolean AS $$ +CREATE OR REPLACE FUNCTION user_setpass(vndbid, bytea, bytea) RETURNS boolean AS $$ WITH upd(id) AS ( - UPDATE users SET passwd = $3 + UPDATE users_shadow SET passwd = $3 WHERE id = $1 AND length($3) = 46 AND ( (passwd = $2 AND length($2) = 46) @@ -654,7 +1025,7 @@ CREATE OR REPLACE FUNCTION user_setpass(integer, bytea, bytea) RETURNS boolean A ) RETURNING id ), del AS( -- Not referenced, but still guaranteed to run - DELETE FROM sessions WHERE uid IN(SELECT id FROM upd) + DELETE FROM sessions WHERE uid IN(SELECT id FROM upd) AND type <> 'api2' ) SELECT true FROM upd $$ LANGUAGE SQL SECURITY DEFINER; @@ -662,24 +1033,33 @@ $$ LANGUAGE SQL SECURITY DEFINER; -- Internal function, used to verify whether user ($2 with session $3) is -- allowed to access sensitive data from user $1. -CREATE OR REPLACE FUNCTION user_isauth(integer, integer, bytea) RETURNS boolean AS $$ - SELECT true FROM users +CREATE OR REPLACE FUNCTION user_isauth(vndbid, vndbid, bytea) RETURNS boolean AS $$ + SELECT true FROM users_shadow WHERE id = $2 AND EXISTS(SELECT 1 FROM sessions WHERE uid = $2 AND token = $3 AND type = 'web') - AND ($2 = $1 OR perm_usermod) + AND ($2 IS NOT DISTINCT FROM $1 OR perm_usermod) $$ LANGUAGE SQL; -- uid of user email to get, uid currently logged in, session token of currently logged in. -- Ensures that only the user itself or a useradmin can get someone's email address. -CREATE OR REPLACE FUNCTION user_getmail(integer, integer, bytea) RETURNS text AS $$ - SELECT mail FROM users WHERE id = $1 AND user_isauth($1, $2, $3) +CREATE OR REPLACE FUNCTION user_getmail(vndbid, vndbid, bytea) RETURNS text AS $$ + SELECT mail FROM users_shadow WHERE id = $1 AND user_isauth($1, $2, $3) +$$ LANGUAGE SQL SECURITY DEFINER; + + +-- Set or unset delete_at for this user. +-- Args: uid, web-token, delete?. +CREATE OR REPLACE FUNCTION user_setdelete(vndbid, bytea, boolean) RETURNS void AS $$ + UPDATE users_shadow + SET delete_at = CASE WHEN $3 THEN NOW() + '1 week'::interval ELSE NULL END + WHERE id = $1 AND user_isauth($1, $1, $2) $$ LANGUAGE SQL SECURITY DEFINER; -- Set a token to change a user's email address. -- Args: uid, web-token, new-email-token, email -CREATE OR REPLACE FUNCTION user_setmail_token(integer, bytea, bytea, text) RETURNS void AS $$ +CREATE OR REPLACE FUNCTION user_setmail_token(vndbid, bytea, bytea, text) RETURNS void AS $$ INSERT INTO sessions (uid, token, expires, type, mail) SELECT id, $3, NOW()+'1 week', 'mail', $4 FROM users WHERE id = $1 AND user_isauth($1, $1, $2) AND length($3) = 20 @@ -687,27 +1067,131 @@ $$ LANGUAGE SQL SECURITY DEFINER; -- Actually change a user's email address, given a valid token. -CREATE OR REPLACE FUNCTION user_setmail_confirm(integer, bytea) RETURNS boolean AS $$ +CREATE OR REPLACE FUNCTION user_setmail_confirm(vndbid, bytea) RETURNS boolean AS $$ WITH u(mail) AS ( DELETE FROM sessions WHERE uid = $1 AND token = $2 AND type = 'mail' AND expires > NOW() RETURNING mail ) - UPDATE users SET mail = (SELECT mail FROM u) WHERE id = $1 AND EXISTS(SELECT 1 FROM u) RETURNING true; + UPDATE users_shadow SET mail = (SELECT mail FROM u) WHERE id = $1 AND EXISTS(SELECT 1 FROM u) RETURNING true; $$ LANGUAGE SQL SECURITY DEFINER; -CREATE OR REPLACE FUNCTION user_setperm_usermod(integer, integer, bytea, boolean) RETURNS void AS $$ - UPDATE users SET perm_usermod = $4 WHERE id = $1 AND user_isauth(-1, $2, $3) +CREATE OR REPLACE FUNCTION user_setperm_usermod(vndbid, vndbid, bytea, boolean) RETURNS void AS $$ + UPDATE users_shadow SET perm_usermod = $4 WHERE id = $1 AND user_isauth(NULL, $2, $3) $$ LANGUAGE SQL SECURITY DEFINER; -CREATE OR REPLACE FUNCTION user_admin_setpass(integer, integer, bytea, bytea) RETURNS void AS $$ +CREATE OR REPLACE FUNCTION user_admin_setpass(vndbid, vndbid, bytea, bytea) RETURNS void AS $$ WITH upd(id) AS ( - UPDATE users SET passwd = $4 WHERE id = $1 AND user_isauth(-1, $2, $3) AND length($4) = 46 RETURNING id + UPDATE users_shadow SET passwd = $4 WHERE id = $1 AND user_isauth(NULL, $2, $3) AND length($4) = 46 RETURNING id ) DELETE FROM sessions WHERE uid IN(SELECT id FROM upd) $$ LANGUAGE SQL SECURITY DEFINER; -CREATE OR REPLACE FUNCTION user_admin_setmail(integer, integer, bytea, text) RETURNS void AS $$ - UPDATE users SET mail = $4 WHERE id = $1 AND user_isauth(-1, $2, $3) +CREATE OR REPLACE FUNCTION user_admin_setmail(vndbid, vndbid, bytea, text) RETURNS void AS $$ + UPDATE users_shadow SET mail = $4 WHERE id = $1 AND user_isauth(NULL, $2, $3) +$$ LANGUAGE SQL SECURITY DEFINER; + + +CREATE OR REPLACE FUNCTION user_api2_tokens(vndbid, vndbid, bytea) RETURNS SETOF sessions AS $$ + SELECT * FROM sessions WHERE uid = $1 AND type = 'api2' AND user_isauth($1, $2, $3) +$$ LANGUAGE SQL SECURITY DEFINER; + + +CREATE OR REPLACE FUNCTION user_api2_set_token(vndbid, vndbid, bytea, bytea, text, boolean, boolean) RETURNS void AS $$ + INSERT INTO sessions (uid, type, expires, token, notes, listread, listwrite) + SELECT $1, 'api2', NOW(), $4, $5, $6, $7 + WHERE user_isauth($1, $2, $3) AND length($4) = 20 + ON CONFLICT (uid, token) DO UPDATE SET notes = $5, listread = $6, listwrite = $7 +$$ LANGUAGE SQL SECURITY DEFINER; + + +CREATE OR REPLACE FUNCTION user_api2_del_token(vndbid, vndbid, bytea, bytea) RETURNS void AS $$ + DELETE FROM sessions WHERE uid = $1 AND token = $4 AND user_isauth($1, $2, $3) +$$ LANGUAGE SQL SECURITY DEFINER; + + +CREATE OR REPLACE FUNCTION email_optout_check(text) RETURNS boolean AS $$ + SELECT EXISTS(SELECT 1 FROM email_optout WHERE mail = hash_email($1)) +$$ LANGUAGE SQL SECURITY DEFINER; + + +-- Delete a user account. +-- A 'hard' delete means that the row in the 'users' table is also deleted and +-- any database contributions referring to this user will refer to NULL +-- instead. +-- A non-'hard' delete still deletes all account information but keeps the row +-- in the users table, so that we are still able to audit their database +-- contributions. +-- 'hard' can be set to NULL to do a hard delete when the user has not made any +-- relevant contributions and a soft delete otherwise. +CREATE OR REPLACE FUNCTION user_delete(userid vndbid, hard boolean) RETURNS void AS $$ +BEGIN + -- References can be audited with: grep 'REFERENCES users' sql/tableattrs.sql + IF hard IS NULL THEN + SELECT INTO hard NOT ( + EXISTS(SELECT 1 FROM changes WHERE userid = requester) + OR EXISTS(SELECT 1 FROM changes_patrolled WHERE userid = uid) + OR EXISTS(SELECT 1 FROM images WHERE userid = uploader) + OR EXISTS(SELECT 1 FROM image_votes WHERE userid = uid) + OR EXISTS(SELECT 1 FROM quotes WHERE userid = addedby) + OR EXISTS(SELECT 1 FROM reports_log WHERE userid = uid) + OR EXISTS(SELECT 1 FROM reviews WHERE userid = uid) + OR EXISTS(SELECT 1 FROM reviews_posts WHERE userid = uid) + OR EXISTS(SELECT 1 FROM tags_vn WHERE userid = uid) + OR EXISTS(SELECT 1 FROM threads_posts WHERE userid = uid) + OR EXISTS(SELECT 1 FROM vn_length_votes WHERE userid = uid)); + END IF; + INSERT INTO email_optout (mail) + SELECT hash_email(mail) FROM users_shadow WHERE id = userid + ON CONFLICT (mail) DO NOTHING; + -- Account-related data. + -- (This is unnecessary for a hard delete due to the ON DELETE CASCADE + -- constraint actions, but we need this code anyway for the soft deletes) + DELETE FROM notification_subs WHERE uid = userid; + DELETE FROM notifications WHERE uid = userid; + DELETE FROM rlists WHERE uid = userid; + DELETE FROM saved_queries WHERE uid = userid; + DELETE FROM sessions WHERE uid = userid; + DELETE FROM ulist_labels WHERE uid = userid; + DELETE FROM ulist_vns WHERE uid = userid; + DELETE FROM users_prefs WHERE id = userid; + DELETE FROM users_prefs_tags WHERE id = userid; + DELETE FROM users_prefs_traits WHERE id = userid; + DELETE FROM users_shadow WHERE id = userid; + DELETE FROM users_traits WHERE id = userid; + DELETE FROM users_username_hist WHERE id = userid; + DELETE FROM vn_length_votes WHERE private AND uid = userid; + IF hard THEN + -- Delete votes that have been invalidated by a moderator, otherwise they will suddenly start counting again + DELETE FROM reviews_votes WHERE uid = userid AND NOT EXISTS(SELECT 1 FROM users WHERE id = userid AND ign_votes); + DELETE FROM threads_poll_votes WHERE uid = userid AND NOT EXISTS(SELECT 1 FROM users WHERE id = userid AND ign_votes); + DELETE FROM quotes_votes WHERE uid = userid AND NOT EXISTS(SELECT 1 FROM users WHERE id = userid AND ign_votes); + DELETE FROM tags_vn WHERE uid = userid AND NOT EXISTS(SELECT 1 FROM users WHERE id = userid AND perm_tag); + DELETE FROM vn_length_votes WHERE uid = userid AND NOT EXISTS(SELECT 1 FROM users WHERE id = userid AND perm_lengthvote); + DELETE FROM image_votes WHERE uid = userid AND NOT EXISTS(SELECT 1 FROM users WHERE id = userid AND perm_imgvote); + DELETE FROM users WHERE id = userid; + INSERT INTO audit_log (affected_uid, action) VALUES (userid, 'hard delete'); + ELSE + UPDATE users SET + notify_dbedit = DEFAULT, + notify_announce = DEFAULT, + notify_post = DEFAULT, + notify_comment = DEFAULT, + nodistract_noads = DEFAULT, + nodistract_nofancy = DEFAULT, + support_enabled = DEFAULT, + pubskin_enabled = DEFAULT, + username = DEFAULT, + uniname = DEFAULT + WHERE id = userid; + INSERT INTO audit_log (affected_uid, action) VALUES (userid, 'soft delete'); + END IF; +END +$$ LANGUAGE plpgsql; + + +-- Should be called from a cron, deletes user accounts with a delete_at in the past. +CREATE OR REPLACE FUNCTION user_delete() RETURNS int AS $$ + SELECT COUNT(*) FROM (SELECT user_delete(id, null) FROM users_shadow WHERE delete_at < NOW()) x $$ LANGUAGE SQL SECURITY DEFINER; |