-- KAT-01 (PR 3a): Katalogsuche. search_normalize, generierte Spalten, game_alt_titles, RPC -- search_catalog_games, Rechte, Idempotenz der alternativen Titel im Startbestand-Werkzeug und die -- SQL-Bausteine aus docs/katalog-pflege.md (Abschnitte 3.1, 5 und 8; Kommentare mit Abschnittsverweis). -- Läuft auch mit eingespieltem Startbestand: eigene Fixtures mit festen IDs (Präfix f9) und dem -- Kunstwort "qxz" in allen Titeln und Verlagen, damit Suchen nie Startbestandsspiele treffen. -- Ohne Suchbegriff wird nur auf Fixture-IDs geprüft (Hilfsfunktion pg_temp.hits blättert durch). -- Die Include-Dateien in ../support/ (\ir) erzeugt supabase/catalog/build_startbestand.ts. begin; select plan(211); insert into auth.users (id, email, raw_user_meta_data) values ('11111111-1111-1111-1111-111111111111', 'anna@example.com', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"Anna"}'), ('22222222-2222-2222-2222-222222222222', 'ben@example.com', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"Ben"}'); -- --------------------------------------------------------------------------------------------- -- Erweiterungen und Rechte (Sicherheit Kriterium 2, Spec 3.2) -- --------------------------------------------------------------------------------------------- select is((select string_agg(extname || ':' || extnamespace::regnamespace::text, ',' order by extname) from pg_extension where extname in ('pg_trgm', 'unaccent')), 'pg_trgm:extensions,unaccent:extensions', 'KAT-01: pg_trgm und unaccent liegen im Schema extensions'); select ok(has_function_privilege('anon', 'extensions.unaccent(regdictionary, text)', 'execute') and has_function_privilege('authenticated', 'extensions.unaccent(regdictionary, text)', 'execute') and has_function_privilege('anon', 'extensions.similarity(text, text)', 'execute') and has_function_privilege('authenticated', 'extensions.similarity(text, text)', 'execute') and has_schema_privilege('anon', 'extensions', 'usage') and has_schema_privilege('authenticated', 'extensions', 'usage'), 'KAT-01: anon und authenticated dürfen unaccent und similarity im Schema extensions ausführen (B5)'); select ok(has_function_privilege('anon', 'public.search_normalize(text)', 'execute') and has_function_privilege('authenticated', 'public.search_normalize(text)', 'execute') and has_function_privilege('service_role', 'public.search_normalize(text)', 'execute'), 'KAT-01: search_normalize ist für anon, authenticated und service_role ausführbar'); select is((select p.proacl is not null and not exists (select 1 from aclexplode(p.proacl) a where a.grantee = 0) from pg_proc p where p.oid = 'public.search_normalize(text)'::regprocedure), true, 'KAT-01: search_normalize ist für public (alle) nicht ausführbar (Rechteliste ausdrücklich gesetzt)'); select ok(has_function_privilege('anon', 'public.search_catalog_games(text, smallint, smallint, smallint, uuid[], uuid[], integer, integer)', 'execute') and has_function_privilege('authenticated', 'public.search_catalog_games(text, smallint, smallint, smallint, uuid[], uuid[], integer, integer)', 'execute'), 'KAT-01 Sicherheit Kriterium 2: anon und authenticated dürfen search_catalog_games ausführen'); select ok(not has_function_privilege('service_role', 'public.search_catalog_games(text, smallint, smallint, smallint, uuid[], uuid[], integer, integer)', 'execute'), 'KAT-01: service_role darf search_catalog_games nicht ausführen'); select is((select p.proacl is not null and not exists (select 1 from aclexplode(p.proacl) a where a.grantee = 0) from pg_proc p where p.oid = 'public.search_catalog_games(text, smallint, smallint, smallint, uuid[], uuid[], integer, integer)'::regprocedure), true, 'KAT-01: search_catalog_games ist für public (alle) nicht ausführbar (Rechteliste ausdrücklich gesetzt)'); select is((select count(*) from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = 'search_normalize' and p.provolatile = 'i' and p.proisstrict and p.proparallel = 's' and not p.prosecdef and p.proconfig = array['search_path=""']), 1::bigint, 'KAT-01: search_normalize ist immutable, strict, parallel safe, security invoker mit leerem search_path'); select is((select count(*) from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = 'search_catalog_games' and p.provolatile = 's' and not p.prosecdef and p.proconfig = array['search_path=""']), 1::bigint, 'KAT-01: search_catalog_games ist stable, security invoker mit leerem search_path'); select is((select count(*) from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = 'search_catalog_games'), 1::bigint, 'KAT-01: es gibt genau eine Überladung von search_catalog_games'); select ok(exists (select 1 from pg_roles where rolname = 'anon' and exists (select 1 from unnest(rolconfig) c where c like 'statement_timeout=%')) and exists (select 1 from pg_roles where rolname = 'authenticated' and exists (select 1 from unnest(rolconfig) c where c like 'statement_timeout=%')), 'KAT-01 Sicherheit (R4): anon und authenticated haben ein statement_timeout (schützt die Suche ohne Anmeldung)'); -- --------------------------------------------------------------------------------------------- -- search_normalize: Pflichtfälle (Spec 3.2 Nr. 2) und weitere Fälle -- --------------------------------------------------------------------------------------------- select is(public.search_normalize('Die Siedler von Catan'), 'die siedler von catan', 'KAT-01 Kriterium 1: Pflichtfall Die Siedler von Catan'); select is(public.search_normalize('CATAN®'), 'catan', 'KAT-01 Kriterium 1: Pflichtfall CATAN® (Schutzzeichen entfernt, kein "catan r")'); select is(public.search_normalize('Spiel™ ½'), 'spiel 1 2', 'KAT-01 Kriterium 1: Pflichtfall Spiel™ ½ (Bruch wie von unaccent umgeschrieben)'); select is(public.search_normalize('Æon’s End'), 'aeon s end', 'KAT-01 Kriterium 1: Pflichtfall Æon’s End (Apostroph wird Leerzeichen)'); select is(public.search_normalize('7 Wonders: Duel'), '7 wonders duel', 'KAT-01 Kriterium 1: Pflichtfall 7 Wonders: Duel'); select is(public.search_normalize('Café Größe'), 'cafe grosse', 'KAT-01 Kriterium 1: Pflichtfall Café Größe (é, ö, ß)'); select is(public.search_normalize('Ticket to Ride – Europe'), 'ticket to ride europe', 'KAT-01 Kriterium 1: Pflichtfall Gedankenstrich'); select is(public.search_normalize('!!!'), null, 'KAT-01 Kriterium 1: nur Sonderzeichen ergibt null'); select is(public.search_normalize(''), null, 'KAT-01 Kriterium 1: leerer Text ergibt null'); select is(public.search_normalize(null), null, 'KAT-01: null ergibt null (strict)'); select is(public.search_normalize(' ©Ärger℠ ℗ Ü '), 'arger u', 'KAT-01 Kriterium 1: alle Schutzzeichen, Umlaute und Ränder'); select is(public.search_normalize('Die Siedler, von Catan'), 'die siedler von catan', 'KAT-01: mehrere Trenner werden zu einem Leerzeichen'); -- --------------------------------------------------------------------------------------------- -- Generierte Spalten, Indizes, Tabelle game_alt_titles (Kriterium 2) -- --------------------------------------------------------------------------------------------- select is((select string_agg(attrelid::regclass::text || '.' || attname, ',' order by attrelid::regclass::text) from pg_attribute where attgenerated = 's' and attname in ('title_search', 'name_search') and attrelid in ('public.games'::regclass, 'public.publishers'::regclass, 'public.game_alt_titles'::regclass)), 'game_alt_titles.title_search,games.title_search,publishers.name_search', 'KAT-01: title_search und name_search sind gespeicherte generierte Spalten'); select is((select count(*) from pg_indexes where schemaname = 'public' and indexdef like '%USING gin (title_search %gin_trgm_ops)%' and indexname in ('games_title_search_trgm_idx', 'game_alt_titles_title_search_trgm_idx')), 2::bigint, 'KAT-01: Trigramm-Indizes auf games.title_search und game_alt_titles.title_search'); select is((select count(*) from pg_indexes where schemaname = 'public' and indexname = 'games_title_search_trgm_idx' and indexdef like '%WHERE%visibility%catalog%hidden_at IS NULL%merged_into_id IS NULL%'), 1::bigint, 'KAT-01: der Index auf games.title_search ist ein Partial-Index mit den Bedingungen der View catalog_games'); select is((select count(*) from pg_indexes where schemaname = 'public' and indexname = 'publishers_name_search_trgm_idx' and indexdef like '%USING gin (name_search %gin_trgm_ops)%'), 1::bigint, 'KAT-01: Trigramm-Index auf publishers.name_search'); select is((select confdeltype::text from pg_constraint where conrelid = 'public.game_alt_titles'::regclass and contype = 'f' and confrelid = 'public.games'::regclass), 'c', 'KAT-01 Kriterium 2: game_alt_titles.game_id löscht mit dem Spiel (on delete cascade)'); select ok(exists (select 1 from pg_trigger where tgrelid = 'public.game_alt_titles'::regclass and tgname = 'game_links_mark_manual'), 'KAT-01: game_alt_titles hat den Trigger game_links_mark_manual'); -- Rechte der Tabelle: nur Lesen für API-Rollen select ok(has_table_privilege('anon', 'public.game_alt_titles', 'select') and has_table_privilege('authenticated', 'public.game_alt_titles', 'select'), 'KAT-01 Kriterium 2: anon und authenticated dürfen game_alt_titles lesen'); select is_empty($$select r, p from unnest(array['anon', 'authenticated']) r, unnest(array['insert', 'update', 'delete', 'truncate', 'references', 'trigger']) p where has_table_privilege(r, 'public.game_alt_titles', p)$$, 'KAT-01 Kriterium 2: anon und authenticated haben kein Schreibrecht und keine weiteren Rechte auf game_alt_titles'); select is_empty($$select r, c from unnest(array['anon', 'authenticated']) r, unnest(array['game_id', 'title', 'locale', 'id']) c where has_column_privilege(r, 'public.game_alt_titles', c, 'insert') or has_column_privilege(r, 'public.game_alt_titles', c, 'update')$$, 'KAT-01 Kriterium 2: auch auf Spaltenebene kein Schreibrecht auf game_alt_titles'); select results_eq($$select polcmd::text, array(select rolname from pg_roles where oid = any(polroles) order by 1) from pg_policy where polrelid = 'public.game_alt_titles'::regclass$$, $$values ('r'::text, array['anon', 'authenticated']::name[])$$, 'KAT-01 Kriterium 2: game_alt_titles hat genau eine Policy, nur select für anon und authenticated'); -- --------------------------------------------------------------------------------------------- -- Fixtures (alle Titel und Verlage mit "qxz") -- --------------------------------------------------------------------------------------------- insert into public.genres (id, name_de, name_en) values ('f9000000-0000-0000-0000-0000000000e1', 'Qxz Genre A', 'Qxz genre A'), ('f9000000-0000-0000-0000-0000000000e2', 'Qxz Genre B', 'Qxz genre B'), ('f9000000-0000-0000-0000-0000000000e3', 'Qxz Genre Q', 'Qxz genre Q'), ('f9000000-0000-0000-0000-0000000000e4', 'Qxz Genre R', 'Qxz genre R'); insert into public.mechanics (id, name_de, name_en) values ('f9000000-0000-0000-0000-0000000000d1', 'Qxz Mechanik A', 'Qxz mechanic A'), ('f9000000-0000-0000-0000-0000000000d2', 'Qxz Mechanik B', 'Qxz mechanic B'); insert into public.publishers (id, name) values ('f9000000-0000-0000-0000-0000000000b2', 'Qxz Hersteller Eins'), ('f9000000-0000-0000-0000-0000000000b1', 'Qxz Hersteller Zwei'), ('f9000000-0000-0000-0000-0000000000b3', 'Qxz Zorb Verlag'); insert into public.games (id, title, visibility) values ('f9000000-0000-0000-0000-000000000001', 'QXZ Zorb', 'catalog'), ('f9000000-0000-0000-0000-000000000002', 'Qxz Zorb Deluxe', 'catalog'), ('f9000000-0000-0000-0000-000000000003', 'Das Qxz Zorb', 'catalog'), ('f9000000-0000-0000-0000-000000000004', 'Zorb of Qxz', 'catalog'), ('f9000000-0000-0000-0000-000000000005', 'Qxz Great Zorb', 'catalog'), ('f9000000-0000-0000-0000-000000000006', 'Qxz Spiel', 'catalog'), -- Gleichstand: erst die größere ID eingefügt, damit die Heap-Reihenfolge nicht zufällig die ID-Reihenfolge ist ('f9000000-0000-0000-0000-000000000008', 'Qxz Twin', 'catalog'), ('f9000000-0000-0000-0000-000000000007', 'QXZ TWIN', 'catalog'), ('f9000000-0000-0000-0000-000000000010', 'Qxz Hauptspiel Alpha', 'catalog'), ('f9000000-0000-0000-0000-000000000011', 'Alpha Qxz Bird', 'catalog'), ('f9000000-0000-0000-0000-000000000012', 'Qxz Einzigartig', 'catalog'), ('f9000000-0000-0000-0000-000000000013', 'Qxz Käse Größe', 'catalog'), ('f9000000-0000-0000-0000-000000000014', 'Qxz Æon’s End', 'catalog'), ('f9000000-0000-0000-0000-000000000015', 'Qxz 7 Wonders', 'catalog'), ('f9000000-0000-0000-0000-000000000016', 'QXZ CATAN®', 'catalog'), ('f9000000-0000-0000-0000-000000000040', 'Qxz Doppelspiel', 'catalog'), ('f9000000-0000-0000-0000-000000000041', 'Qxz Einzelspiel', 'catalog'), ('f9000000-0000-0000-0000-000000000060', 'Qxz Cabin', 'catalog'), -- Sortierung ist binär (collate "C"): "qxz b" kommt vor "qxzab". Hinweis: Mit ICU-Sortierung (wie lokal) gilt das auch -- ohne collate "C"; der Test ist dort nicht mutationsempfindlich und schützt vor libc-Sortierungen, die Leerzeichen ignorieren. ('f9000000-0000-0000-0000-000000000081', 'Qxzab', 'catalog'), ('f9000000-0000-0000-0000-000000000095', 'Qxz Zwilling', 'catalog'), ('f9000000-0000-0000-0000-000000000093', 'Qxz Zwilling', 'catalog'), ('f9000000-0000-0000-0000-000000000091', 'Qxz Zwilling', 'catalog'), ('f9000000-0000-0000-0000-000000000094', 'Qxz Zwilling', 'catalog'), ('f9000000-0000-0000-0000-000000000092', 'Qxz Zwilling', 'catalog'), ('f9000000-0000-0000-0000-000000000090', 'Qxz Zwilling', 'catalog'), ('f9000000-0000-0000-0000-000000000017', 'Qxz Wanderer', 'catalog'), ('f9000000-0000-0000-0000-000000000080', 'Qxz B', 'catalog'); -- Filter-Fixtures (20-25); 22 ohne jede Angabe, 23 nur max_minutes, 24 nur min_minutes insert into public.games (id, title, visibility, min_players, max_players, min_minutes, max_minutes, min_age) values ('f9000000-0000-0000-0000-000000000020', 'Qxz Filter 20', 'catalog', 2, 4, 30, 60, 8), ('f9000000-0000-0000-0000-000000000021', 'Qxz Filter 21', 'catalog', 3, 6, 60, 90, 12), ('f9000000-0000-0000-0000-000000000022', 'Qxz Filter 22', 'catalog', null, null, null, null, null), ('f9000000-0000-0000-0000-000000000023', 'Qxz Filter 23', 'catalog', 2, 2, null, 45, 6), ('f9000000-0000-0000-0000-000000000024', 'Qxz Filter 24', 'catalog', 1, 8, 20, null, 0), ('f9000000-0000-0000-0000-000000000025', 'Qxz Filter 25', 'catalog', 2, 5, 30, 30, 10); insert into public.game_genres (game_id, genre_id) values ('f9000000-0000-0000-0000-000000000020', 'f9000000-0000-0000-0000-0000000000e1'), ('f9000000-0000-0000-0000-000000000020', 'f9000000-0000-0000-0000-0000000000e2'), ('f9000000-0000-0000-0000-000000000021', 'f9000000-0000-0000-0000-0000000000e1'), ('f9000000-0000-0000-0000-000000000022', 'f9000000-0000-0000-0000-0000000000e1'), ('f9000000-0000-0000-0000-000000000025', 'f9000000-0000-0000-0000-0000000000e2'), ('f9000000-0000-0000-0000-000000000005', 'f9000000-0000-0000-0000-0000000000e3'), ('f9000000-0000-0000-0000-000000000060', 'f9000000-0000-0000-0000-0000000000e3'), ('f9000000-0000-0000-0000-000000000080', 'f9000000-0000-0000-0000-0000000000e4'), ('f9000000-0000-0000-0000-000000000081', 'f9000000-0000-0000-0000-0000000000e4'); insert into public.game_mechanics (game_id, mechanic_id) values ('f9000000-0000-0000-0000-000000000020', 'f9000000-0000-0000-0000-0000000000d1'), ('f9000000-0000-0000-0000-000000000021', 'f9000000-0000-0000-0000-0000000000d1'), ('f9000000-0000-0000-0000-000000000021', 'f9000000-0000-0000-0000-0000000000d2'), ('f9000000-0000-0000-0000-000000000025', 'f9000000-0000-0000-0000-0000000000d2'); -- Verlage: 01 und 30 bei Eins; 40 bei Eins und Zwei; 41 bei Zwei; 06 bei "Qxz Zorb Verlag" insert into public.game_publishers (game_id, publisher_id) values ('f9000000-0000-0000-0000-000000000001', 'f9000000-0000-0000-0000-0000000000b2'), ('f9000000-0000-0000-0000-000000000040', 'f9000000-0000-0000-0000-0000000000b1'), ('f9000000-0000-0000-0000-000000000040', 'f9000000-0000-0000-0000-0000000000b2'), ('f9000000-0000-0000-0000-000000000041', 'f9000000-0000-0000-0000-0000000000b1'), ('f9000000-0000-0000-0000-000000000006', 'f9000000-0000-0000-0000-0000000000b3'); -- Ausgeblendet, zusammengeführt, privat insert into public.games (id, title, visibility, hidden_at) values ('f9000000-0000-0000-0000-000000000030', 'Qxz Verborgen', 'catalog', now()); insert into public.games (id, title, visibility, hidden_at, merged_into_id) values ('f9000000-0000-0000-0000-000000000031', 'Qxz Verschmolzen', 'catalog', now(), 'f9000000-0000-0000-0000-000000000001'); insert into public.games (id, title, visibility, owner_id) values ('f9000000-0000-0000-0000-000000000032', 'Qxz Privat', 'private', '11111111-1111-1111-1111-111111111111'), ('f9000000-0000-0000-0000-000000000033', 'QXZ ZORB', 'private', '22222222-2222-2222-2222-222222222222'); insert into public.game_publishers (game_id, publisher_id) values ('f9000000-0000-0000-0000-000000000030', 'f9000000-0000-0000-0000-0000000000b2'), ('f9000000-0000-0000-0000-000000000033', 'f9000000-0000-0000-0000-0000000000b2'); -- Bilder: 50 mit Quelle ohne Erlaubnis (maskiert), 51 manuell gepflegt, 52 ohne Quelle insert into public.games (id, title, visibility, image_path, catalog_source_id, source_ref) values ('f9000000-0000-0000-0000-000000000050', 'Qxz Cover Quelle', 'catalog', 'covers/quelle.png', (select id from public.catalog_sources where key = 'bgg'), '501'), ('f9000000-0000-0000-0000-000000000051', 'Qxz Cover Manuell', 'catalog', 'covers/manuell.png', (select id from public.catalog_sources where key = 'bgg'), '502'), ('f9000000-0000-0000-0000-000000000052', 'Qxz Cover Eigen', 'catalog', 'covers/eigen.png', null, null); update public.games set manual_fields = '{}' where id = 'f9000000-0000-0000-0000-000000000050'; update public.games set manual_fields = array['image_path'] where id = 'f9000000-0000-0000-0000-000000000051'; -- 40 Spiele für das Nachladen (Kriterium 5) insert into public.games (id, title, visibility) select ('f9000000-0000-0000-0000-0000000001' || lpad(n::text, 2, '0'))::uuid, 'Qxz Seite ' || lpad(n::text, 2, '0'), 'catalog' from generate_series(1, 40) n; -- Alternative Titel (als postgres, wie im Studio) insert into public.game_alt_titles (id, game_id, locale, title) values ('f9000000-0000-0000-0000-0000000000a1', 'f9000000-0000-0000-0000-000000000010', 'de', 'Qxz Zugvogel'), ('f9000000-0000-0000-0000-0000000000a2', 'f9000000-0000-0000-0000-000000000010', 'en', 'Qxz Migrant Bird'), ('f9000000-0000-0000-0000-0000000000a3', 'f9000000-0000-0000-0000-000000000011', 'en', 'Qxz Bird'), ('f9000000-0000-0000-0000-0000000000a4', 'f9000000-0000-0000-0000-000000000012', 'de', 'Qxz Einzigartig Alt'), ('f9000000-0000-0000-0000-0000000000a5', 'f9000000-0000-0000-0000-000000000012', 'en', 'QXZ EINZIGARTIG ALT'), ('f9000000-0000-0000-0000-0000000000a6', 'f9000000-0000-0000-0000-000000000030', 'de', 'Qxz Verborgen Alt'), ('f9000000-0000-0000-0000-0000000000a7', 'f9000000-0000-0000-0000-000000000031', 'de', 'Qxz Verschmolzen Alt'), ('f9000000-0000-0000-0000-0000000000a8', 'f9000000-0000-0000-0000-000000000032', 'de', 'Qxz Privat Alt'), ('f9000000-0000-0000-0000-0000000000a9', 'f9000000-0000-0000-0000-000000000033', 'en', 'Qxz Bens Alt'), -- drei Treffer-Titel für "qxz nomad": Stufe 2, Stufe 1 (länger, weniger ähnlich), Stufe 1 (kürzer, ähnlicher) ('f9000000-0000-0000-0000-0000000000c1', 'f9000000-0000-0000-0000-000000000017', 'de', 'Stern Qxz Nomad'), ('f9000000-0000-0000-0000-0000000000c2', 'f9000000-0000-0000-0000-000000000017', 'en', 'Qxz Nomad Sternenwanderer'), ('f9000000-0000-0000-0000-0000000000c3', 'f9000000-0000-0000-0000-000000000017', 'de', 'Qxz Nomadisch'); select is((select title_search from public.games where id = 'f9000000-0000-0000-0000-000000000014'), 'qxz aeon s end', 'KAT-01 Kriterium 1: games.title_search wird beim Anlegen gefüllt'); select is((select title_search from public.game_alt_titles where id = 'f9000000-0000-0000-0000-0000000000a5'), 'qxz einzigartig alt', 'KAT-01 Kriterium 2: game_alt_titles.title_search wird gefüllt'); select is((select name_search from public.publishers where id = 'f9000000-0000-0000-0000-0000000000b1'), 'qxz hersteller zwei', 'KAT-01 Kriterium 3: publishers.name_search wird gefüllt'); update public.publishers set name = 'Qxz Hersteller Zwei & Söhne' where id = 'f9000000-0000-0000-0000-0000000000b1'; select is((select name_search from public.publishers where id = 'f9000000-0000-0000-0000-0000000000b1'), 'qxz hersteller zwei sohne', 'KAT-01: publishers.name_search folgt einer Namensänderung'); update public.publishers set name = 'Qxz Hersteller Zwei' where id = 'f9000000-0000-0000-0000-0000000000b1'; -- Hilfsfunktion: alle Fixture-Treffer durchblättern (nur als postgres, für Abfragen ohne Suchbegriff) create function pg_temp.hits(p_query text default null, p_players smallint default null, p_max_minutes smallint default null, p_age smallint default null, p_genre_ids uuid[] default null, p_mechanic_ids uuid[] default null) returns setof uuid language sql as $$ select r.id from generate_series(0, (select coalesce(max(t.total_count), 0) from public.search_catalog_games(p_query, p_players, p_max_minutes, p_age, p_genre_ids, p_mechanic_ids, 1, 0) t)::int, 50) o, lateral public.search_catalog_games(p_query, p_players, p_max_minutes, p_age, p_genre_ids, p_mechanic_ids, 50, o) r where r.id::text like 'f9000000%' $$; -- --------------------------------------------------------------------------------------------- -- Kriterium 1: Teilzeichenkette, Groß/Klein, Sonderzeichen, Relevanzstufen -- --------------------------------------------------------------------------------------------- select results_eq($$select id, match_rank::int, total_count::int from public.search_catalog_games('qxz zorb')$$, $$values ('f9000000-0000-0000-0000-000000000001'::uuid, 0, 6), ('f9000000-0000-0000-0000-000000000002', 1, 6), ('f9000000-0000-0000-0000-000000000003', 2, 6), ('f9000000-0000-0000-0000-000000000004', 3, 6), ('f9000000-0000-0000-0000-000000000005', 3, 6), ('f9000000-0000-0000-0000-000000000006', 4, 6)$$, 'KAT-01 Kriterium 1: Relevanzstufen 0 bis 4 in der erwarteten Reihenfolge; gleiche Stufe nach Ähnlichkeit (nicht nach Titel); Verlagstreffer zuletzt'); select results_eq($$select id from public.search_catalog_games('QXZ: ZORB!')$$, $$select id from public.search_catalog_games('qxz zorb')$$, 'KAT-01 Kriterium 1: Groß-/Kleinschreibung und Sonderzeichen ändern das Ergebnis nicht'); select results_eq($$select id, match_rank::int from public.search_catalog_games('qxz zo')$$, $$values ('f9000000-0000-0000-0000-000000000001'::uuid, 1), ('f9000000-0000-0000-0000-000000000002', 1), ('f9000000-0000-0000-0000-000000000003', 2), ('f9000000-0000-0000-0000-000000000004', 3), ('f9000000-0000-0000-0000-000000000005', 3), ('f9000000-0000-0000-0000-000000000006', 4)$$, 'KAT-01 Kriterium 1: ein Teil des Titels genügt (Wort ab 2 Zeichen als Teilzeichenkette)'); select results_eq($$select id from public.search_catalog_games('qxz twin')$$, $$values ('f9000000-0000-0000-0000-000000000007'::uuid), ('f9000000-0000-0000-0000-000000000008')$$, 'KAT-01 Kriterium 5: bei völligem Gleichstand entscheidet die ID (stabile Reihenfolge)'); select results_eq($$select id from public.search_catalog_games('qxz zorb', null, null, null, null, null, 4, 0)$$, $$values ('f9000000-0000-0000-0000-000000000001'::uuid), ('f9000000-0000-0000-0000-000000000002'), ('f9000000-0000-0000-0000-000000000003'), ('f9000000-0000-0000-0000-000000000004')$$, 'KAT-01 Kriterium 5: die erste Seite enthält die relevantesten Treffer (Stufe, dann Ähnlichkeit), nicht die ersten nach Titel'); select results_eq($$select id from public.search_catalog_games('qxz zorb', null, null, null, null, null, 2, 0) union all select id from public.search_catalog_games('qxz zorb', null, null, null, null, null, 2, 2) union all select id from public.search_catalog_games('qxz zorb', null, null, null, null, null, 2, 4)$$, $$select id from public.search_catalog_games('qxz zorb')$$, 'KAT-01 Kriterium 5: Seiten zu je zwei ergeben dieselbe Reihenfolge wie die ganze Liste'); select results_eq($$select id from public.search_catalog_games('qxz twin', null, null, null, null, null, 1, 0) union all select id from public.search_catalog_games('qxz twin', null, null, null, null, null, 1, 1)$$, $$values ('f9000000-0000-0000-0000-000000000007'::uuid), ('f9000000-0000-0000-0000-000000000008')$$, 'KAT-01 Kriterium 5: bei völligem Gleichstand entscheidet die ID auch über die Seitengrenze hinweg'); select results_eq($$select id from public.search_catalog_games('qxz zwilling', null, null, null, null, null, 3, 0) union all select id from public.search_catalog_games('qxz zwilling', null, null, null, null, null, 3, 3)$$, $$select id from public.games where title = 'Qxz Zwilling' order by id$$, 'KAT-01 Kriterium 5: sechs gleich gute Treffer erscheinen seitenübergreifend in ID-Reihenfolge'); select results_eq($$select id, match_rank::int, matched_alt_title from public.search_catalog_games('qxz nomad')$$, $$values ('f9000000-0000-0000-0000-000000000017'::uuid, 1, 'Qxz Nomadisch'::text)$$, 'KAT-01 Kriterium 2: von mehreren treffenden alternativen Titeln gewinnt die beste Stufe, dann die größte Ähnlichkeit'); select results_eq($$select id, match_rank::int, matched_alt_title from public.search_catalog_games('qxz einzig')$$, $$values ('f9000000-0000-0000-0000-000000000012'::uuid, 1, null::text)$$, 'KAT-01 Kriterium 2: bei gleicher Stufe von Haupttitel und alternativem Titel gilt der Haupttitel (matched_alt_title null)'); select is((select count(*) from public.search_catalog_games('qxz käse')), 1::bigint, 'KAT-01 Kriterium 1: Umlaut im Suchbegriff findet den Umlaut-Titel'); select is((select count(*) from public.search_catalog_games('QXZ KASE GROSSE')), 1::bigint, 'KAT-01 Kriterium 1: ohne Umlaut und ß wird derselbe Titel gefunden'); select is((select count(*) from public.search_catalog_games('qxz grösse')), 1::bigint, 'KAT-01 Kriterium 1: ö und ß in der Eingabe'); select is((select count(*) from public.search_catalog_games('qxz kaese')), 0::bigint, 'KAT-01 Spec-F5: "ae" findet "ä" in R1 nicht'); select results_eq($$select id, match_rank::int from public.search_catalog_games('qxz aeons end')$$, $$values ('f9000000-0000-0000-0000-000000000014'::uuid, 0)$$, 'KAT-01 Kriterium 1: "aeons end" findet "Æon’s End" über die Schreibweise ohne Leerzeichen'); select is((select count(*) from public.search_catalog_games('qxzaeonsend')), 1::bigint, 'KAT-01 Kriterium 1: ohne jedes Leerzeichen eingegeben'); select is((select count(*) from public.search_catalog_games('qxz æon’s end')), 1::bigint, 'KAT-01 Kriterium 1: Æ und Apostroph in der Eingabe'); select results_eq($$select id, match_rank::int from public.search_catalog_games('qxz 7wonders')$$, $$values ('f9000000-0000-0000-0000-000000000015'::uuid, 0)$$, 'KAT-01 Kriterium 1: "7wonders" findet "7 Wonders"'); select results_eq($$select id, match_rank::int from public.search_catalog_games('Qxz-Catan!')$$, $$values ('f9000000-0000-0000-0000-000000000016'::uuid, 0)$$, 'KAT-01 Kriterium 1: "CATAN®" wird über "catan" gefunden'); select is((select count(*) from public.search_catalog_games('!!!')), 0::bigint, 'KAT-01 Kriterium 1: nur Sonderzeichen ergibt keine Treffer'); select is((select count(*) from public.search_catalog_games('zzznichtvorhanden')), 0::bigint, 'KAT-01 Kriterium 6: Suche ohne Treffer liefert keine Zeilen'); select results_eq($$select id from public.search_catalog_games('qxz 9 zorb') order by id$$, $$values ('f9000000-0000-0000-0000-000000000001'::uuid), ('f9000000-0000-0000-0000-000000000002'), ('f9000000-0000-0000-0000-000000000003'), ('f9000000-0000-0000-0000-000000000004'), ('f9000000-0000-0000-0000-000000000005'), ('f9000000-0000-0000-0000-000000000006')$$, 'KAT-01 Kriterium 1: Wörter mit einem Zeichen werden verworfen ("qxz 9 zorb" = "qxz zorb")'); select results_eq($$select id, match_rank::int from public.search_catalog_games('a b', null, null, null, array['f9000000-0000-0000-0000-0000000000e3']::uuid[])$$, $$values ('f9000000-0000-0000-0000-000000000060'::uuid, 3)$$, 'KAT-01 Kriterium 1: "a b" (nur Wörter mit einem Zeichen) trifft nur über die Schreibweise ohne Leerzeichen'); select results_eq($$select id, match_rank::int from public.search_catalog_games('É', null, null, null, array['f9000000-0000-0000-0000-0000000000e3']::uuid[])$$, $$values ('f9000000-0000-0000-0000-000000000060'::uuid, null::int), ('f9000000-0000-0000-0000-000000000005', null)$$, 'KAT-01 Kriterium 1: ein normalisierter Begriff unter 2 Zeichen gilt als kein Suchbegriff (alles, match_rank null)'); select results_eq($$select id, match_rank::int from public.search_catalog_games(' ', null, null, null, array['f9000000-0000-0000-0000-0000000000e3']::uuid[])$$, $$values ('f9000000-0000-0000-0000-000000000060'::uuid, null::int), ('f9000000-0000-0000-0000-000000000005', null)$$, 'KAT-01 Kriterium 8: nur Leerzeichen gelten als kein Suchbegriff'); select results_eq($$select id, match_rank::int from public.search_catalog_games(null, null, null, null, array['f9000000-0000-0000-0000-0000000000e3']::uuid[])$$, $$values ('f9000000-0000-0000-0000-000000000060'::uuid, null::int), ('f9000000-0000-0000-0000-000000000005', null)$$, 'KAT-01 Kriterium 8: null ist kein Suchbegriff, Sortierung nach Titel'); -- --------------------------------------------------------------------------------------------- -- Kriterium 2: alternative Titel -- --------------------------------------------------------------------------------------------- select results_eq($$select id, matched_alt_title, match_rank::int from public.search_catalog_games('qxz zugvogel')$$, $$values ('f9000000-0000-0000-0000-000000000010'::uuid, 'Qxz Zugvogel'::text, 0)$$, 'KAT-01 Kriterium 2: der deutsche alternative Titel findet das Spiel, matched_alt_title nennt ihn'); select results_eq($$select id, matched_alt_title, match_rank::int from public.search_catalog_games('qxz migrant bird')$$, $$values ('f9000000-0000-0000-0000-000000000010'::uuid, 'Qxz Migrant Bird'::text, 0)$$, 'KAT-01 Kriterium 2: der englische alternative Titel findet dasselbe Spiel'); select results_eq($$select id, matched_alt_title, match_rank::int from public.search_catalog_games('qxz hauptspiel')$$, $$values ('f9000000-0000-0000-0000-000000000010'::uuid, null::text, 1)$$, 'KAT-01 Kriterium 2: Treffer über den Haupttitel hat matched_alt_title null'); select results_eq($$select id, matched_alt_title, match_rank::int, total_count::int from public.search_catalog_games('qxz bird')$$, $$values ('f9000000-0000-0000-0000-000000000011'::uuid, 'Qxz Bird'::text, 0, 2), ('f9000000-0000-0000-0000-000000000010', 'Qxz Migrant Bird', 3, 2)$$, 'KAT-01 Kriterium 2: der alternative Titel zählt, wenn er besser trifft als der Haupttitel (Stufe 0 statt 2)'); select results_eq($$select id, matched_alt_title, match_rank::int, total_count::int from public.search_catalog_games('qxz einzigartig')$$, $$values ('f9000000-0000-0000-0000-000000000012'::uuid, null::text, 0, 1)$$, 'KAT-01 Kriterium 2: ein Spiel erscheint nur einmal, auch wenn Haupt- und alternative Titel treffen; bei besserem Haupttitel null'); select results_eq($$select id, lower(matched_alt_title), total_count::int from public.search_catalog_games('qxz einzigartig alt')$$, $$values ('f9000000-0000-0000-0000-000000000012'::uuid, 'qxz einzigartig alt'::text, 1)$$, 'KAT-01 Kriterium 2: zwei alternative Titel (de, en) mit gleicher Stufe ergeben eine Zeile'); -- --------------------------------------------------------------------------------------------- -- Kriterium 3: Verlage -- --------------------------------------------------------------------------------------------- select results_eq($$select id, match_rank::int, publisher_names from public.search_catalog_games('qxz hersteller zwei')$$, $$values ('f9000000-0000-0000-0000-000000000040'::uuid, 4, array['Qxz Hersteller Eins', 'Qxz Hersteller Zwei']::text[]), ('f9000000-0000-0000-0000-000000000041', 4, array['Qxz Hersteller Zwei'])$$, 'KAT-01 Kriterium 3: der Verlag findet alle seine Spiele (Stufe 4); ein Spiel mit zwei Verlagen über beide; publisher_names sortiert und vollständig'); select results_eq($$select id from public.search_catalog_games('qxz hersteller eins') order by id$$, $$values ('f9000000-0000-0000-0000-000000000001'::uuid), ('f9000000-0000-0000-0000-000000000040')$$, 'KAT-01 Kriterium 3: der andere Verlag des Spiels findet es ebenfalls; ausgeblendete Spiele des Verlags fehlen'); select results_eq($$select id, match_rank::int from public.search_catalog_games('qxz zorb zorb eins')$$, $$values ('f9000000-0000-0000-0000-000000000001'::uuid, 4)$$, 'KAT-01 Kriterium 3: ein doppelt eingegebenes Wort zählt einfach (Verlagstreffer bleibt auffindbar)'); select results_eq($$select id, match_rank::int from public.search_catalog_games('qxz zorb eins')$$, $$values ('f9000000-0000-0000-0000-000000000001'::uuid, 4)$$, 'KAT-01 Kriterium 3: ein Wort aus dem Titel plus ein Wort aus dem Verlag (UND)'); select results_eq($$select id, publisher_names from public.search_catalog_games('qxz cabin')$$, $$values ('f9000000-0000-0000-0000-000000000060'::uuid, '{}'::text[])$$, 'KAT-01 Kriterium 3: publisher_names ist leer statt null'); -- --------------------------------------------------------------------------------------------- -- Kriterium 4 und 8: Filter, mit und ohne Suchbegriff -- --------------------------------------------------------------------------------------------- select results_eq($$select h from pg_temp.hits(p_players := 3::smallint) h order by 1$$, $$values ('f9000000-0000-0000-0000-000000000020'::uuid), ('f9000000-0000-0000-0000-000000000021'), ('f9000000-0000-0000-0000-000000000024'), ('f9000000-0000-0000-0000-000000000025')$$, 'KAT-01 Kriterium 8: Spielerzahl 3 ohne Suchbegriff (Spiele ohne Angabe fehlen, 2-2 passt nicht)'); select results_eq($$select h from pg_temp.hits('qxz filter', p_players := 3::smallint) h order by 1$$, $$values ('f9000000-0000-0000-0000-000000000020'::uuid), ('f9000000-0000-0000-0000-000000000021'), ('f9000000-0000-0000-0000-000000000024'), ('f9000000-0000-0000-0000-000000000025')$$, 'KAT-01 Kriterium 4: Spielerzahl 3 mit Suchbegriff'); select results_eq($$select h from pg_temp.hits(p_players := 2::smallint) h order by 1$$, $$values ('f9000000-0000-0000-0000-000000000020'::uuid), ('f9000000-0000-0000-0000-000000000023'), ('f9000000-0000-0000-0000-000000000024'), ('f9000000-0000-0000-0000-000000000025')$$, 'KAT-01 Kriterium 8: Spielerzahl 2 (untere und obere Grenze zählen mit)'); select results_eq($$select h from pg_temp.hits(p_max_minutes := 45::smallint) h order by 1$$, $$values ('f9000000-0000-0000-0000-000000000023'::uuid), ('f9000000-0000-0000-0000-000000000024'), ('f9000000-0000-0000-0000-000000000025')$$, 'KAT-01 Spec-F2: höchstens 45 Minuten; nur max_minutes oder nur min_minutes bekannt zählt, ohne Angabe fehlt'); select results_eq($$select h from pg_temp.hits('qxz filter', p_max_minutes := 60::smallint) h order by 1$$, $$values ('f9000000-0000-0000-0000-000000000020'::uuid), ('f9000000-0000-0000-0000-000000000023'), ('f9000000-0000-0000-0000-000000000024'), ('f9000000-0000-0000-0000-000000000025')$$, 'KAT-01 Kriterium 4: höchstens 60 Minuten (Grenze zählt mit) mit Suchbegriff'); select results_eq($$select h from pg_temp.hits(p_age := 8::smallint) h order by 1$$, $$values ('f9000000-0000-0000-0000-000000000020'::uuid), ('f9000000-0000-0000-0000-000000000023'), ('f9000000-0000-0000-0000-000000000024')$$, 'KAT-01 Kriterium 8: Alter 8 (Mindestalter bis einschließlich 8)'); select results_eq($$select h from pg_temp.hits(p_age := 0::smallint) h$$, $$values ('f9000000-0000-0000-0000-000000000024'::uuid)$$, 'KAT-01 Kriterium 4: Alter 0 findet nur Mindestalter 0'); select results_eq($$select h from pg_temp.hits(p_genre_ids := array['f9000000-0000-0000-0000-0000000000e1']::uuid[]) h order by 1$$, $$values ('f9000000-0000-0000-0000-000000000020'::uuid), ('f9000000-0000-0000-0000-000000000021'), ('f9000000-0000-0000-0000-000000000022')$$, 'KAT-01 Kriterium 4: Genre allein'); select results_eq($$select h from pg_temp.hits(p_genre_ids := array['f9000000-0000-0000-0000-0000000000e1', 'f9000000-0000-0000-0000-0000000000e2']::uuid[]) h$$, $$values ('f9000000-0000-0000-0000-000000000020'::uuid)$$, 'KAT-01 Spec-F1: zwei Genres: nur Spiele mit beiden'); select results_eq($$select h from pg_temp.hits(p_genre_ids := array['f9000000-0000-0000-0000-0000000000e1', 'f9000000-0000-0000-0000-0000000000e1']::uuid[]) h order by 1$$, $$select h from pg_temp.hits(p_genre_ids := array['f9000000-0000-0000-0000-0000000000e1']::uuid[]) h order by 1$$, 'KAT-01 Kriterium 4: Duplikate im Array zählen einfach'); select results_eq($$select h from pg_temp.hits(p_mechanic_ids := array['f9000000-0000-0000-0000-0000000000d2']::uuid[]) h order by 1$$, $$values ('f9000000-0000-0000-0000-000000000021'::uuid), ('f9000000-0000-0000-0000-000000000025')$$, 'KAT-01 Kriterium 4: Mechanik allein'); select results_eq($$select h from pg_temp.hits(p_mechanic_ids := array['f9000000-0000-0000-0000-0000000000d2', 'f9000000-0000-0000-0000-0000000000d2']::uuid[]) h order by 1$$, $$select h from pg_temp.hits(p_mechanic_ids := array['f9000000-0000-0000-0000-0000000000d2']::uuid[]) h order by 1$$, 'KAT-01 Kriterium 4: Duplikate im Mechanik-Array zählen einfach'); select results_eq($$select h from pg_temp.hits(p_mechanic_ids := array['f9000000-0000-0000-0000-0000000000d1', 'f9000000-0000-0000-0000-0000000000d2']::uuid[]) h$$, $$values ('f9000000-0000-0000-0000-000000000021'::uuid)$$, 'KAT-01 Spec-F1: zwei Mechaniken: nur Spiele mit beiden'); select results_eq($$select h from pg_temp.hits(p_players := 3::smallint, p_age := 12::smallint, p_genre_ids := array['f9000000-0000-0000-0000-0000000000e1']::uuid[], p_mechanic_ids := array['f9000000-0000-0000-0000-0000000000d2']::uuid[]) h$$, $$values ('f9000000-0000-0000-0000-000000000021'::uuid)$$, 'KAT-01 Kriterium 4: alle Filter zusammen sind UND-verknüpft'); select results_eq($$select h from pg_temp.hits('qxz filter', p_players := 2::smallint, p_max_minutes := 60::smallint, p_genre_ids := array['f9000000-0000-0000-0000-0000000000e2']::uuid[]) h order by 1$$, $$values ('f9000000-0000-0000-0000-000000000020'::uuid), ('f9000000-0000-0000-0000-000000000025')$$, 'KAT-01 Kriterium 4: Suchbegriff plus Spielerzahl, Dauer und Genre'); select is((select count(*) from public.search_catalog_games(null, null, null, null, array[gen_random_uuid()])), 0::bigint, 'KAT-01 Kriterium 4: unbekannte Genre-ID ergibt 0 Treffer'); select is((select count(*) from public.search_catalog_games(null, null, null, null, null, array[gen_random_uuid()])), 0::bigint, 'KAT-01 Kriterium 4: unbekannte Mechanik-ID ergibt 0 Treffer'); select results_eq($$select id, match_rank::int, matched_alt_title, total_count::int from public.search_catalog_games(null, null, null, null, array['f9000000-0000-0000-0000-0000000000e3']::uuid[])$$, $$values ('f9000000-0000-0000-0000-000000000060'::uuid, null::int, null::text, 2), ('f9000000-0000-0000-0000-000000000005', null, null, 2)$$, 'KAT-01 Kriterium 8: ohne Suchbegriff nach Titel sortiert, match_rank und matched_alt_title null'); select results_eq($$select id from public.search_catalog_games(null, null, null, null, array['f9000000-0000-0000-0000-0000000000e3']::uuid[], null, 1, 0) union all select id from public.search_catalog_games(null, null, null, null, array['f9000000-0000-0000-0000-0000000000e3']::uuid[], null, 1, 1)$$, $$values ('f9000000-0000-0000-0000-000000000060'::uuid), ('f9000000-0000-0000-0000-000000000005')$$, 'KAT-01 Kriterium 5: ohne Suchbegriff bestimmt der Titel (nicht die ID) auch die Seitengrenze'); select results_eq($$select id from public.search_catalog_games(null, null, null, null, array['f9000000-0000-0000-0000-0000000000e4']::uuid[])$$, $$values ('f9000000-0000-0000-0000-000000000080'::uuid), ('f9000000-0000-0000-0000-000000000081')$$, 'KAT-01 Kriterium 8: ohne Suchbegriff wird binär nach dem normalisierten Titel sortiert (Leerzeichen vor Buchstaben)'); select results_eq($$select h from pg_temp.hits() h order by 1$$, $$select id from public.games where id::text like 'f9000000%' and visibility = 'catalog' and hidden_at is null and merged_into_id is null order by id$$, 'KAT-01 Kriterium 8: ohne Suchbegriff und ohne Filter erscheinen alle sichtbaren Fixture-Spiele (ohne ausgeblendete, zusammengeführte, private)'); -- --------------------------------------------------------------------------------------------- -- Kriterium 5: Nachladen -- --------------------------------------------------------------------------------------------- select results_eq($$select id from public.search_catalog_games('qxz seite', null, null, null, null, null, 15, 0) union all select id from public.search_catalog_games('qxz seite', null, null, null, null, null, 15, 15) union all select id from public.search_catalog_games('qxz seite', null, null, null, null, null, 15, 30)$$, $$select id from public.games where title like 'Qxz Seite %' order by title$$, 'KAT-01 Kriterium 5: drei Seiten ergeben ohne Überschneidung genau die 40 Treffer in stabiler Reihenfolge'); select results_eq($$select count(*), count(distinct id), count(distinct total_count), min(total_count) from (select * from public.search_catalog_games('qxz seite', null, null, null, null, null, 15, 0) union all select * from public.search_catalog_games('qxz seite', null, null, null, null, null, 15, 15) union all select * from public.search_catalog_games('qxz seite', null, null, null, null, null, 15, 30)) s$$, $$values (40::bigint, 40::bigint, 1::bigint, 40::bigint)$$, 'KAT-01 Kriterium 5: total_count ist in jeder Zeile jeder Seite gleich (40)'); select is((select count(*) from public.search_catalog_games('qxz seite', null, null, null, null, null, 15, 45)), 0::bigint, 'KAT-01 Kriterium 5: hinter dem Ende keine Zeilen (hasMore false)'); select is((select count(*) from public.search_catalog_games('qxz seite')), 30::bigint, 'KAT-01 Kriterium 5: Standard-Seitengröße 30'); select is((select count(*) from public.search_catalog_games('qxz seite', p_limit := 50)), 40::bigint, 'KAT-01 Kriterium 5: Seitengröße bis 50 erlaubt'); select results_eq($$select id from public.search_catalog_games('qxz seite', null, null, null, null, null, 10, 0)$$, $$select id from public.search_catalog_games('qxz seite', null, null, null, null, null, 10, 0)$$, 'KAT-01 Kriterium 5: wiederholter Aufruf liefert dieselbe Reihenfolge'); -- --------------------------------------------------------------------------------------------- -- KAT-05 Kriterium 4 und 10: ausgeblendet, zusammengeführt, privat, Bild -- --------------------------------------------------------------------------------------------- select is((select count(*) from public.search_catalog_games('qxz verborgen')), 0::bigint, 'KAT-05 Kriterium 4: ein ausgeblendetes Spiel erscheint nicht über den Titel'); select is((select count(*) from public.search_catalog_games('qxz verborgen alt')), 0::bigint, 'KAT-05 Kriterium 4: ... nicht über den alternativen Titel'); select is((select count(*) from public.search_catalog_games('qxz verschmolzen alt')), 0::bigint, 'KAT-05 Kriterium 4: ein zusammengeführtes Spiel erscheint nicht über seinen alternativen Titel'); select is((select count(*) from public.search_catalog_games('qxz verschmolzen')), 0::bigint, 'KAT-05 Kriterium 4: ... und nicht über den Titel'); select is((select count(*) from public.search_catalog_games('qxz privat')), 0::bigint, 'KAT-01 Sicherheit: ein privates Spiel erscheint nie (Titel)'); select is((select count(*) from public.search_catalog_games('qxz bens alt')), 0::bigint, 'KAT-01 Sicherheit: ... auch nicht über einen alternativen Titel'); select is((select count(*) from public.search_catalog_games(null, null, null, null, null, null, 50, 0) where id::text in ('f9000000-0000-0000-0000-000000000030', 'f9000000-0000-0000-0000-000000000031', 'f9000000-0000-0000-0000-000000000032', 'f9000000-0000-0000-0000-000000000033')), 0::bigint, 'KAT-05 Kriterium 4: ohne Suchbegriff fehlen ausgeblendete, zusammengeführte und private Spiele ebenso (erste Seite)'); select is((select count(*) from pg_temp.hits() h where h in ('f9000000-0000-0000-0000-000000000030', 'f9000000-0000-0000-0000-000000000031', 'f9000000-0000-0000-0000-000000000032', 'f9000000-0000-0000-0000-000000000033')), 0::bigint, 'KAT-05 Kriterium 4: ohne Suchbegriff fehlen ausgeblendete, zusammengeführte und private Spiele auch beim Durchblättern'); select results_eq($$select id, image_path from public.search_catalog_games('qxz cover') order by id$$, $$values ('f9000000-0000-0000-0000-000000000050'::uuid, null::text), ('f9000000-0000-0000-0000-000000000051', 'covers/manuell.png'), ('f9000000-0000-0000-0000-000000000052', 'covers/eigen.png')$$, 'KAT-05 Kriterium 10: image_path kommt aus catalog_games (maskiert bei Quelle ohne Erlaubnis, sichtbar bei manueller Pflege und ohne Quelle)'); select results_eq($$select title, year, min_players, max_players, min_minutes, max_minutes, min_age from public.search_catalog_games('qxz filter 20')$$, $$values ('Qxz Filter 20'::text, null::smallint, 2::smallint, 4::smallint, 30::smallint, 60::smallint, 8::smallint)$$, 'KAT-01: die Ausgabe enthält Titel und Kurzangaben'); -- --------------------------------------------------------------------------------------------- -- Eingabegrenzen -- --------------------------------------------------------------------------------------------- select throws_ok($$select * from public.search_catalog_games(repeat('a', 101))$$, 'P0001', 'search_query_too_long', 'KAT-01: 101 Zeichen sind zu lang'); select lives_ok($$select * from public.search_catalog_games(repeat('a', 100))$$, 'KAT-01: 100 Zeichen sind erlaubt'); select throws_ok($$select * from public.search_catalog_games('aa bb cc dd ee ff gg hh ii jj kk')$$, 'P0001', 'search_query_too_long', 'KAT-01: 11 Wörter sind zu viel'); select throws_ok($$select * from public.search_catalog_games('a b c d e f g h i j k')$$, 'P0001', 'search_query_too_long', 'KAT-01: 11 Wörter zählen auch, wenn sie nur ein Zeichen haben'); select lives_ok($$select * from public.search_catalog_games('aa bb cc dd ee ff gg hh ii jj')$$, 'KAT-01: 10 Wörter sind erlaubt'); select throws_ok($$select * from public.search_catalog_games(null, 0::smallint)$$, 'P0001', 'search_filter_invalid', 'KAT-01: Spielerzahl 0'); select throws_ok($$select * from public.search_catalog_games(null, 100::smallint)$$, 'P0001', 'search_filter_invalid', 'KAT-01: Spielerzahl 100'); select lives_ok($$select * from public.search_catalog_games(null, 1::smallint); select * from public.search_catalog_games(null, 99::smallint)$$, 'KAT-01: Spielerzahl 1 und 99 sind erlaubt'); select throws_ok($$select * from public.search_catalog_games(null, null, 0::smallint)$$, 'P0001', 'search_filter_invalid', 'KAT-01: Dauer 0'); select throws_ok($$select * from public.search_catalog_games(null, null, 1441::smallint)$$, 'P0001', 'search_filter_invalid', 'KAT-01: Dauer 1441'); select lives_ok($$select * from public.search_catalog_games(null, null, 1::smallint); select * from public.search_catalog_games(null, null, 1440::smallint)$$, 'KAT-01: Dauer 1 und 1440 sind erlaubt'); select throws_ok($$select * from public.search_catalog_games(null, null, null, (-1)::smallint)$$, 'P0001', 'search_filter_invalid', 'KAT-01: Alter -1'); select throws_ok($$select * from public.search_catalog_games(null, null, null, 100::smallint)$$, 'P0001', 'search_filter_invalid', 'KAT-01: Alter 100'); select lives_ok($$select * from public.search_catalog_games(null, null, null, 0::smallint); select * from public.search_catalog_games(null, null, null, 99::smallint)$$, 'KAT-01: Alter 0 und 99 sind erlaubt'); select throws_ok($$select * from public.search_catalog_games(null, null, null, null, (select array_agg(gen_random_uuid()) from generate_series(1, 21)))$$, 'P0001', 'search_filter_invalid', 'KAT-01: 21 Genre-IDs'); select throws_ok($$select * from public.search_catalog_games(null, null, null, null, null, (select array_agg(gen_random_uuid()) from generate_series(1, 21)))$$, 'P0001', 'search_filter_invalid', 'KAT-01: 21 Mechanik-IDs'); select lives_ok($$select * from public.search_catalog_games(null, null, null, null, (select array_agg(gen_random_uuid()) from generate_series(1, 20)), (select array_agg(gen_random_uuid()) from generate_series(1, 20)))$$, 'KAT-01: je 20 IDs sind erlaubt'); select throws_ok($$select * from public.search_catalog_games(null, null, null, null, null, null, 0, 0)$$, 'P0001', 'search_paging_invalid', 'KAT-01: Seitengröße 0'); select throws_ok($$select * from public.search_catalog_games(null, null, null, null, null, null, 51, 0)$$, 'P0001', 'search_paging_invalid', 'KAT-01: Seitengröße 51'); select throws_ok($$select * from public.search_catalog_games(null, null, null, null, null, null, 30, -1)$$, 'P0001', 'search_paging_invalid', 'KAT-01: Versatz -1'); select throws_ok($$select * from public.search_catalog_games(null, null, null, null, null, null, 30, 10001)$$, 'P0001', 'search_paging_invalid', 'KAT-01: Versatz 10001'); select lives_ok($$select * from public.search_catalog_games(null, null, null, null, null, null, 1, 0); select * from public.search_catalog_games(null, null, null, null, null, null, 50, 10000)$$, 'KAT-01: Seitengröße 1 und 50, Versatz 0 und 10000 sind erlaubt'); -- --------------------------------------------------------------------------------------------- -- Tabelle game_alt_titles: Checks, Unique, manual_fields, Kaskade -- --------------------------------------------------------------------------------------------- select throws_ok($$insert into public.game_alt_titles (game_id, locale, title) values ('f9000000-0000-0000-0000-000000000013', 'fr', 'Titre')$$, '23514', null, 'KAT-01 Kriterium 2: Sprache nur de oder en (Check)'); select throws_ok($$insert into public.game_alt_titles (game_id, locale, title) values ('f9000000-0000-0000-0000-000000000013', 'de', repeat('x', 201))$$, '23514', null, 'KAT-01 Kriterium 2: Titel höchstens 200 Zeichen (Check)'); select throws_ok($$insert into public.game_alt_titles (game_id, locale, title) values ('f9000000-0000-0000-0000-000000000013', 'de', '')$$, '23514', null, 'KAT-01 Kriterium 2: Titel nicht leer (Check)'); select throws_ok($$insert into public.game_alt_titles (game_id, locale, title) values ('f9000000-0000-0000-0000-000000000013', 'de', '!!! ®')$$, '23514', null, 'KAT-01 Kriterium 2: ein Titel nur aus Sonderzeichen ist nicht zulässig (title_search nicht null)'); select lives_ok($$insert into public.game_alt_titles (game_id, locale, title) values ('f9000000-0000-0000-0000-000000000013', 'de', repeat('x', 200))$$, 'KAT-01 Kriterium 2: 200 Zeichen sind erlaubt'); select throws_ok($$insert into public.game_alt_titles (game_id, locale, title) values ('f9000000-0000-0000-0000-000000000012', 'de', 'QXZ einzigartig alt!')$$, '23505', null, 'KAT-01 Kriterium 2: derselbe Titel in derselben Sprache nur einmal, unabhängig von der Schreibweise (Unique)'); select lives_ok($$insert into public.game_alt_titles (game_id, locale, title) values ('f9000000-0000-0000-0000-000000000013', 'de', 'Qxz Zweitname')$$, 'KAT-01 Kriterium 2: weiterer Titel in einer Sprache'); select lives_ok($$insert into public.game_alt_titles (game_id, locale, title) values ('f9000000-0000-0000-0000-000000000013', 'en', 'QXZ zweitname!')$$, 'KAT-01 Kriterium 2: derselbe Titel in der anderen Sprache ist zulässig'); select lives_ok($$insert into public.game_alt_titles (game_id, locale, title) values ('f9000000-0000-0000-0000-000000000013', 'de', 'Qxz Dritter Name')$$, 'KAT-01 Kriterium 2: mehrere Titel je Sprache sind erlaubt'); select throws_ok($$insert into public.game_alt_titles (game_id, locale, title) values (gen_random_uuid(), 'de', 'Qxz Waise')$$, '23503', null, 'KAT-01 Kriterium 2: alternative Titel brauchen ein vorhandenes Spiel (Fremdschlüssel)'); select throws_ok($$insert into public.game_alt_titles (game_id, locale, title, title_search) values ('f9000000-0000-0000-0000-000000000013', 'de', 'Qxz Eigen', 'x')$$, '428C9', null, 'KAT-01: title_search ist generiert und nicht beschreibbar (auch nicht als postgres)'); select throws_ok($$update public.games set title_search = 'x' where id = 'f9000000-0000-0000-0000-000000000013'$$, '428C9', null, 'KAT-01: games.title_search ist nicht beschreibbar'); -- manual_fields: Einfügen, Ändern und Löschen markieren alt_titles (nur Katalogspiele) select ok((select 'alt_titles' = any(manual_fields) from public.games where id = 'f9000000-0000-0000-0000-000000000010'), 'KAT-01 Kriterium 2: Einfügen alternativer Titel markiert alt_titles in manual_fields (Katalogspiel)'); select ok(not ((select 'alt_titles' = any(manual_fields) from public.games where id = 'f9000000-0000-0000-0000-000000000040')), 'KAT-01: ein Spiel ohne alternative Titel ist nicht als alt_titles markiert'); select is((select manual_fields from public.games where id = 'f9000000-0000-0000-0000-000000000032'), '{}'::text[], 'KAT-01: alternative Titel eines privaten Spiels markieren nichts'); insert into public.games (id, title, visibility) values ('f9000000-0000-0000-0000-000000000071', 'Qxz Markierung', 'catalog'); select ok(not ((select 'alt_titles' = any(manual_fields) from public.games where id = 'f9000000-0000-0000-0000-000000000071')), 'Ausgangslage: Markierungsspiel ohne alt_titles'); insert into public.game_alt_titles (id, game_id, locale, title) values ('f9000000-0000-0000-0000-0000000000b9', 'f9000000-0000-0000-0000-000000000071', 'de', 'Qxz Mark Alt'); select ok((select 'alt_titles' = any(manual_fields) from public.games where id = 'f9000000-0000-0000-0000-000000000071'), 'KAT-01: insert markiert alt_titles'); update public.games set manual_fields = array_remove(manual_fields, 'alt_titles') where id = 'f9000000-0000-0000-0000-000000000071'; update public.game_alt_titles set title = 'Qxz Mark Alt Zwei' where id = 'f9000000-0000-0000-0000-0000000000b9'; select ok((select 'alt_titles' = any(manual_fields) from public.games where id = 'f9000000-0000-0000-0000-000000000071'), 'KAT-01: update markiert alt_titles'); update public.games set manual_fields = array_remove(manual_fields, 'alt_titles') where id = 'f9000000-0000-0000-0000-000000000071'; delete from public.game_alt_titles where id = 'f9000000-0000-0000-0000-0000000000b9'; select ok((select 'alt_titles' = any(manual_fields) from public.games where id = 'f9000000-0000-0000-0000-000000000071'), 'KAT-01: delete markiert alt_titles'); -- manual_fields-Check: vollständige Liste von main plus alt_titles select lives_ok($$update public.games set manual_fields = array['title', 'description', 'description_locale', 'year', 'min_players', 'max_players', 'min_minutes', 'max_minutes', 'min_age', 'image_path', 'genres', 'mechanics', 'categories', 'publishers', 'alt_titles'] where id = 'f9000000-0000-0000-0000-000000000071'$$, 'KAT-01: der Check akzeptiert alle 14 bisherigen Schlüssel und alt_titles'); select throws_ok($$update public.games set manual_fields = array['alt_titles', 'unbekannt'] where id = 'f9000000-0000-0000-0000-000000000071'$$, '23514', null, 'KAT-01: der Check lehnt unbekannte Schlüssel weiterhin ab'); -- Kaskade: Spiel löschen löscht seine alternativen Titel insert into public.games (id, title, visibility) values ('f9000000-0000-0000-0000-000000000070', 'Qxz Löschtest', 'catalog'); insert into public.game_alt_titles (game_id, locale, title) values ('f9000000-0000-0000-0000-000000000070', 'de', 'Qxz Löschtest Alt'), ('f9000000-0000-0000-0000-000000000070', 'en', 'Qxz Delete test'); delete from public.games where id = 'f9000000-0000-0000-0000-000000000070'; select is((select count(*) from public.game_alt_titles where game_id = 'f9000000-0000-0000-0000-000000000070'), 0::bigint, 'KAT-01 Kriterium 2: Spiel löschen löscht seine alternativen Titel'); -- updated_at: Trigger setzt ihn bei Änderung (ctid statt now(), die Transaktion hat eine feste Zeit) select ok(exists (select 1 from pg_trigger where tgrelid = 'public.game_alt_titles'::regclass and tgname = 'set_updated_at'), 'KAT-01: game_alt_titles setzt updated_at per Trigger'); -- --------------------------------------------------------------------------------------------- -- Rollen: anon, authenticated (Sicherheit Kriterium 2) -- --------------------------------------------------------------------------------------------- set local role anon; select lives_ok($$select * from public.search_catalog_games('qxz zorb')$$, 'KAT-01 Sicherheit Kriterium 2: die Suche als anon läuft ohne Rechtefehler'); select results_eq($$select id, match_rank::int from public.search_catalog_games('qxz zorb')$$, $$values ('f9000000-0000-0000-0000-000000000001'::uuid, 0), ('f9000000-0000-0000-0000-000000000002', 1), ('f9000000-0000-0000-0000-000000000003', 2), ('f9000000-0000-0000-0000-000000000004', 3), ('f9000000-0000-0000-0000-000000000005', 3), ('f9000000-0000-0000-0000-000000000006', 4)$$, 'KAT-01 Sicherheit Kriterium 2: anon sieht dieselben Treffer; das private Spiel "QXZ ZORB" eines Nutzers fehlt'); select results_eq($$select id, matched_alt_title from public.search_catalog_games('qxz zugvogel')$$, $$values ('f9000000-0000-0000-0000-000000000010'::uuid, 'Qxz Zugvogel'::text)$$, 'KAT-01 Kriterium 2: alternative Titel funktionieren für anon'); select results_eq($$select id, publisher_names from public.search_catalog_games('qxz doppelspiel')$$, $$values ('f9000000-0000-0000-0000-000000000040'::uuid, array['Qxz Hersteller Eins', 'Qxz Hersteller Zwei']::text[])$$, 'KAT-01 Kriterium 3: Verlage funktionieren für anon'); select results_eq($$select id, image_path from public.search_catalog_games('qxz cover') order by id$$, $$values ('f9000000-0000-0000-0000-000000000050'::uuid, null::text), ('f9000000-0000-0000-0000-000000000051', 'covers/manuell.png'), ('f9000000-0000-0000-0000-000000000052', 'covers/eigen.png')$$, 'KAT-05 Kriterium 10: Maskierung des Bildes gilt für anon'); select is((select count(*) from public.search_catalog_games(null, null, null, null, array['f9000000-0000-0000-0000-0000000000e1', 'f9000000-0000-0000-0000-0000000000e2']::uuid[])), 1::bigint, 'KAT-01 Kriterium 4: Filter wirken auch für anon'); select is((select count(*) from public.search_catalog_games('qxz seite', p_limit := 50)), 40::bigint, 'KAT-01 Kriterium 5: Nachladen für anon'); select is((select count(*) from public.game_alt_titles where game_id = 'f9000000-0000-0000-0000-000000000010'), 2::bigint, 'KAT-01 Kriterium 2: anon liest alternative Titel von Katalogspielen'); select is((select count(*) from public.game_alt_titles where game_id = 'f9000000-0000-0000-0000-000000000032'), 0::bigint, 'KAT-01 Kriterium 2: anon liest keine alternativen Titel eines privaten Spiels'); select throws_ok($$insert into public.game_alt_titles (game_id, locale, title) values ('f9000000-0000-0000-0000-000000000013', 'de', 'Qxz Anon')$$, '42501', null, 'KAT-01 Kriterium 2: anon kann keine alternativen Titel anlegen'); select throws_ok($$update public.game_alt_titles set title = 'Qxz Anon' where id = 'f9000000-0000-0000-0000-0000000000a1'$$, '42501', null, 'KAT-01 Kriterium 2: anon kann keine alternativen Titel ändern'); select throws_ok($$delete from public.game_alt_titles where id = 'f9000000-0000-0000-0000-0000000000a1'$$, '42501', null, 'KAT-01 Kriterium 2: anon kann keine alternativen Titel löschen'); reset role; -- Anna (authentifiziert): legt ein privates Spiel an und ändert es (title_search wird mit den Rechten des Nutzers berechnet) select set_config('request.jwt.claims', '{"sub":"11111111-1111-1111-1111-111111111111","role":"authenticated"}', true); set local role authenticated; select lives_ok($$insert into public.games (owner_id, title, visibility) values ('11111111-1111-1111-1111-111111111111', 'Qxz Mölkky ®', 'private')$$, 'KAT-01 Sicherheit: authenticated legt ein privates Spiel an (search_normalize ausführbar, kein 42501)'); select is((select title_search from public.games where title = 'Qxz Mölkky ®'), 'qxz molkky', 'KAT-01 Sicherheit: title_search ist nach dem Anlegen gefüllt'); select is((select count(*) from public.search_catalog_games('qxz molkky')), 0::bigint, 'KAT-01 Sicherheit: Anna findet das gerade angelegte private Spiel nicht über die Katalogsuche'); select lives_ok($$update public.games set title = 'Spiel™ ½' where title = 'Qxz Mölkky ®'$$, 'KAT-01 Sicherheit: authenticated ändert den Titel seines privaten Spiels (kein 42501)'); select is((select title_search from public.games where title = 'Spiel™ ½'), 'spiel 1 2', 'KAT-01 Sicherheit: title_search ist nach dem Ändern aktualisiert'); select throws_ok($$insert into public.games (owner_id, title, visibility, title_search) values ('11111111-1111-1111-1111-111111111111', 'Qxz X', 'private', 'x')$$, '428C9', null, 'KAT-01: authenticated kann title_search nicht setzen (generierte Spalte)'); select ok(not has_column_privilege('authenticated', 'public.games', 'title_search', 'insert') and not has_column_privilege('authenticated', 'public.games', 'title_search', 'update') and not has_column_privilege('anon', 'public.games', 'title_search', 'update') and has_column_privilege('anon', 'public.games', 'title_search', 'select'), 'KAT-01: title_search ist für API-Rollen nur lesbar (Spaltenrechte)'); select lives_ok($$select * from public.search_catalog_games('qxz zorb')$$, 'KAT-01: die Suche als authenticated läuft'); select results_eq($$select id from public.search_catalog_games('qxz zorb')$$, $$values ('f9000000-0000-0000-0000-000000000001'::uuid), ('f9000000-0000-0000-0000-000000000002'), ('f9000000-0000-0000-0000-000000000003'), ('f9000000-0000-0000-0000-000000000004'), ('f9000000-0000-0000-0000-000000000005'), ('f9000000-0000-0000-0000-000000000006')$$, 'KAT-01 Sicherheit: Annas Suche zeigt nur Katalogspiele (Bens privates "QXZ ZORB" fehlt)'); select is((select count(*) from public.search_catalog_games('qxz privat')), 0::bigint, 'KAT-01 Sicherheit: Anna findet ihr eigenes privates Spiel nicht über die Katalogsuche'); select is((select count(*) from public.game_alt_titles where game_id = 'f9000000-0000-0000-0000-000000000032'), 1::bigint, 'KAT-01 Kriterium 2: Anna liest die alternativen Titel ihres privaten Spiels'); select is((select count(*) from public.game_alt_titles where game_id = 'f9000000-0000-0000-0000-000000000033'), 0::bigint, 'KAT-01 Kriterium 2: Anna liest nicht die alternativen Titel von Bens privatem Spiel'); select throws_ok($$insert into public.game_alt_titles (game_id, locale, title) values ('f9000000-0000-0000-0000-000000000032', 'de', 'Qxz Anna Neu')$$, '42501', null, 'KAT-01 Kriterium 2: auch am eigenen privaten Spiel kann Anna keine alternativen Titel anlegen'); select throws_ok($$update public.game_alt_titles set title = 'Qxz Anna' where id = 'f9000000-0000-0000-0000-0000000000a8'$$, '42501', null, 'KAT-01 Kriterium 2: ... nicht ändern'); select throws_ok($$delete from public.game_alt_titles where id = 'f9000000-0000-0000-0000-0000000000a8'$$, '42501', null, 'KAT-01 Kriterium 2: ... nicht löschen'); reset role; -- Ben: die eigene Suche zeigt Annas privates Spiel nicht und sieht keine fremden alternativen Titel select set_config('request.jwt.claims', '{"sub":"22222222-2222-2222-2222-222222222222","role":"authenticated"}', true); set local role authenticated; select is((select count(*) from public.game_alt_titles where game_id = 'f9000000-0000-0000-0000-000000000032'), 0::bigint, 'KAT-01 Kriterium 2: Ben liest keine alternativen Titel von Annas privatem Spiel'); select is((select count(*) from public.search_catalog_games('qxz privat')), 0::bigint, 'KAT-01 Sicherheit: Ben findet Annas privates Spiel nicht'); select is((select count(*) from public.search_catalog_games('qxz zorb')), 6::bigint, 'KAT-01 Sicherheit: Ben findet sein eigenes privates "QXZ ZORB" nicht, nur die sechs Katalogspiele'); reset role; -- --------------------------------------------------------------------------------------------- -- Startbestand-Werkzeug: alternative Titel, Idempotenz (Spec 4.3, Krit. 2) -- Testspiel B hat laut testdata/startbestand.csv zwei alternative Titel, Testspiel A keinen. -- --------------------------------------------------------------------------------------------- \ir ../support/vocabulary_fixture.inc \ir ../support/startbestand_fixture.inc select results_eq($$select a.locale, a.title from public.game_alt_titles a join public.games g on g.id = a.game_id where g.title = 'Testspiel B' order by a.locale$$, $$values ('de'::text, 'Testspiel Bravo'::text), ('en', 'Test Game Bravo')$$, 'KAT-01 Kriterium 2: Testspiel B bekommt aus der CSV einen deutschen und einen englischen alternativen Titel'); select is((select count(*) from public.game_alt_titles a join public.games g on g.id = a.game_id where g.title = 'Testspiel A'), 0::bigint, 'KAT-01 Kriterium 2: Testspiel A hat keine alternativen Titel'); select ok((select 'alt_titles' = any(manual_fields) from public.games where title = 'Testspiel B'), 'KAT-01 Kriterium 2: das Einspielen markiert alt_titles in manual_fields (SB8)'); select results_eq($$select id, match_rank::int, matched_alt_title from public.search_catalog_games('test game bravo') where id = extensions.uuid_generate_v5('6f1d9a52-3c4e-4b8a-9f27-5d0e8c71a3b4', 'game:testspiel-b')$$, $$values (extensions.uuid_generate_v5('6f1d9a52-3c4e-4b8a-9f27-5d0e8c71a3b4', 'game:testspiel-b'), 0, 'Test Game Bravo'::text)$$, 'KAT-01 Kriterium 2: ein eingespielter alternativer Titel ist über die Suche auffindbar'); create temp table snap_alt on commit drop as select a.id, a.game_id, a.locale, a.title, a.ctid::text as c, a.updated_at as u from public.game_alt_titles a where a.game_id in (select id from public.games where title like 'Testspiel %'); \ir ../support/startbestand_fixture.inc select is((select count(*) from snap_alt), 2::bigint, 'Schnappschuss der alternativen Titel enthält die zwei Zeilen von Testspiel B'); select is_empty($$select 1 from snap_alt s where not exists ( select 1 from public.game_alt_titles n where n.id = s.id and n.game_id = s.game_id and n.locale = s.locale and n.title = s.title and n.ctid::text = s.c and n.updated_at = s.u)$$, 'KAT-01 Kriterium 2: erneutes Einspielen ändert keine Zeile von game_alt_titles (ctid und updated_at gleich)'); select is((select count(*) from public.game_alt_titles a join public.games g on g.id = a.game_id where g.title like 'Testspiel %'), 2::bigint, 'KAT-01 Kriterium 2: erneutes Einspielen legt keine Duplikate an'); -- Ein in der CSV nicht (mehr) stehender Titel wird gelöscht, auch bei Testspiel A ohne alt_titles; eine geänderte -- Schreibweise stellt nur den Titel wieder her (gleiche ID), unveränderte Zeilen bleiben unberührt. insert into public.game_alt_titles (game_id, locale, title) select id, 'en', 'Entfernter Titel' from public.games where title = 'Testspiel B'; insert into public.game_alt_titles (game_id, locale, title) select id, 'de', 'Fremder Titel' from public.games where title = 'Testspiel A'; update public.game_alt_titles set title = 'TESTSPIEL BRAVO' where locale = 'de' and game_id = (select id from public.games where title = 'Testspiel B'); create temp table snap_alt2 on commit drop as select a.id, a.locale, a.title, a.ctid::text as c from public.game_alt_titles a where a.game_id in (select id from public.games where title like 'Testspiel %'); \ir ../support/startbestand_fixture.inc select results_eq($$select a.locale, a.title from public.game_alt_titles a where a.game_id in (select id from public.games where title like 'Testspiel %') order by a.locale$$, $$values ('de'::text, 'Testspiel Bravo'::text), ('en', 'Test Game Bravo')$$, 'KAT-01 Kriterium 2: ein in der CSV entfernter alternativer Titel wird gelöscht (auch bei einem Spiel ohne alt_titles); geänderte Schreibweise wird wiederhergestellt'); select is((select count(*) from public.game_alt_titles a join snap_alt2 s on s.id = a.id and s.title = 'TESTSPIEL BRAVO'), 1::bigint, 'KAT-01 Kriterium 2: die geänderte Schreibweise behält ihre ID (nur title aktualisiert)'); select is((select count(*) from public.game_alt_titles a join snap_alt2 s on s.id = a.id and s.locale = 'en' and s.title = 'Test Game Bravo' and s.c = a.ctid::text), 1::bigint, 'KAT-01 Kriterium 2: die unveränderte Zeile bleibt unberührt (gleiche ctid)'); select is((select count(*) from public.game_alt_titles a join snap_alt2 s on s.id = a.id and s.locale = 'de' and s.c <> a.ctid::text), 1::bigint, 'KAT-01 Kriterium 2: nur die geänderte Zeile wurde geschrieben'); -- --------------------------------------------------------------------------------------------- -- SQL-Bausteine aus docs/katalog-pflege.md (unverändert, ohne begin/commit) -- Abschnitt 3.1: Alternative Titel pflegen (Spiel 7a1b...0001 "Beispielspiel Runbook") -- --------------------------------------------------------------------------------------------- insert into public.games (id, title, year, min_players, max_players, min_minutes, max_minutes, min_age, description, description_locale, visibility) values ('7a1b2c3d-0000-4000-8000-000000000001', 'Beispielspiel Runbook', 2020, 2, 4, 30, 60, 8, 'Eigener Kurztext.', 'de', 'catalog'); insert into public.game_alt_titles (game_id, locale, title) values ('7a1b2c3d-0000-4000-8000-000000000001', 'de', 'Beispielspiel Deutsch'), ('7a1b2c3d-0000-4000-8000-000000000001', 'en', 'Example game') on conflict (game_id, locale, title_search) do update set title = excluded.title where public.game_alt_titles.title is distinct from excluded.title; select results_eq($$select locale, title from public.game_alt_titles where game_id = '7a1b2c3d-0000-4000-8000-000000000001' order by locale, title$$, $$values ('de'::text, 'Beispielspiel Deutsch'::text), ('en', 'Example game')$$, 'Runbook 3.1: Alternative Titel anlegen'); insert into public.game_alt_titles (game_id, locale, title) values ('7a1b2c3d-0000-4000-8000-000000000001', 'de', 'BEISPIELSPIEL DEUTSCH'), ('7a1b2c3d-0000-4000-8000-000000000001', 'en', 'Example game') on conflict (game_id, locale, title_search) do update set title = excluded.title where public.game_alt_titles.title is distinct from excluded.title; select results_eq($$select locale, title from public.game_alt_titles where game_id = '7a1b2c3d-0000-4000-8000-000000000001' order by locale, title$$, $$values ('de'::text, 'BEISPIELSPIEL DEUTSCH'::text), ('en', 'Example game')$$, 'Runbook 3.1: derselbe Baustein mit geänderter Schreibweise aktualisiert den Titel statt einen zweiten anzulegen'); select results_eq($$select id, match_rank::int, matched_alt_title from public.search_catalog_games('example game')$$, $$values ('7a1b2c3d-0000-4000-8000-000000000001'::uuid, 0, 'Example game'::text)$$, 'Runbook 3.1: der angelegte Titel ist sofort über die Suche auffindbar'); delete from public.game_alt_titles where game_id = '7a1b2c3d-0000-4000-8000-000000000001' and locale = 'en' and title = 'Example game'; select results_eq($$select locale from public.game_alt_titles where game_id = '7a1b2c3d-0000-4000-8000-000000000001'$$, $$values ('de'::text)$$, 'Runbook 3.1: Alternativen Titel löschen'); -- Abschnitt 5, Schritt 2b: alternative Titel der Quelle am Ziel ergänzen, danach zusammenführen insert into public.games (id, title, year, min_players, max_players, min_minutes, max_minutes, min_age, description, description_locale, visibility) values ('7a1b2c3d-0000-4000-8000-000000000002', 'Duplikat Runbook', 2020, 2, 4, 30, 60, 8, 'Eigener Kurztext.', 'de', 'catalog'), ('7a1b2c3d-0000-4000-8000-000000000003', 'Ziel Runbook', 2020, 2, 4, 30, 60, 8, 'Eigener Kurztext.', 'de', 'catalog'); insert into public.game_alt_titles (game_id, locale, title) values ('7a1b2c3d-0000-4000-8000-000000000002', 'de', 'Runbook Quellname'), ('7a1b2c3d-0000-4000-8000-000000000002', 'en', 'Runbook source name'), ('7a1b2c3d-0000-4000-8000-000000000003', 'en', 'RUNBOOK SOURCE NAME'); select locale, title from public.game_alt_titles where game_id = '7a1b2c3d-0000-4000-8000-000000000002' order by locale, title; insert into public.game_alt_titles (game_id, locale, title) select '7a1b2c3d-0000-4000-8000-000000000003', a.locale, a.title from public.game_alt_titles a where a.game_id = '7a1b2c3d-0000-4000-8000-000000000002' on conflict (game_id, locale, title_search) do nothing; select results_eq($$select locale, title from public.game_alt_titles where game_id = '7a1b2c3d-0000-4000-8000-000000000003' order by locale, title$$, $$values ('de'::text, 'Runbook Quellname'::text), ('en', 'RUNBOOK SOURCE NAME')$$, 'Runbook 5 Schritt 2b: Titel der Quelle werden am Ziel ergänzt, vorhandene (auch in anderer Schreibweise) bleiben unverändert'); select results_eq($$select id from public.search_catalog_games('runbook quellname')$$, $$values ('7a1b2c3d-0000-4000-8000-000000000002'::uuid), ('7a1b2c3d-0000-4000-8000-000000000003')$$, 'Runbook 5 Schritt 2b: das Ziel ist jetzt über den Titel der Quelle auffindbar (vor dem Zusammenführen zusammen mit der Quelle)'); select public.admin_merge_games('7a1b2c3d-0000-4000-8000-000000000002', '7a1b2c3d-0000-4000-8000-000000000003'); select is((select count(*) from public.game_alt_titles where game_id = '7a1b2c3d-0000-4000-8000-000000000002'), 2::bigint, 'Spec-F6 A: nach dem Zusammenführen bleiben die alternativen Titel am ausgeblendeten Quell-Eintrag'); select is((select count(*) from public.game_alt_titles where game_id = '7a1b2c3d-0000-4000-8000-000000000003'), 2::bigint, 'Spec-F6 A: das Ziel behält seine alternativen Titel'); select results_eq($$select id from public.search_catalog_games('runbook quellname')$$, $$values ('7a1b2c3d-0000-4000-8000-000000000003'::uuid)$$, 'Spec-F6 A: nach dem Zusammenführen findet die Suche nur noch das Ziel, nie die Quelle'); select is((select count(*) from public.search_catalog_games('duplikat runbook')), 0::bigint, 'KAT-05 Kriterium 4: die zusammengeführte Quelle erscheint nicht mehr'); -- --------------------------------------------------------------------------------------------- -- Kaskade beim Konto-Löschen (QUE-04): alternative Titel privater Spiele verschwinden mit dem Konto -- --------------------------------------------------------------------------------------------- select is((select count(*) from public.game_alt_titles where game_id = 'f9000000-0000-0000-0000-000000000033'), 1::bigint, 'Ausgangslage: Bens privates Spiel hat einen alternativen Titel'); delete from auth.users where id = '22222222-2222-2222-2222-222222222222'; select is((select count(*) from public.game_alt_titles where game_id = 'f9000000-0000-0000-0000-000000000033'), 0::bigint, 'KAT-01: Konto löschen löscht auch die alternativen Titel seiner privaten Spiele'); -- --------------------------------------------------------------------------------------------- -- Abschnitt 8 des Runbooks: Normalisierung nach einem Image-Upgrade prüfen (R6) -- --------------------------------------------------------------------------------------------- select results_eq($$select 'games' as tabelle, count(*) as abweichend from public.games where title_search is distinct from public.search_normalize(title) union all select 'game_alt_titles', count(*) from public.game_alt_titles where title_search is distinct from public.search_normalize(title) union all select 'publishers', count(*) from public.publishers where name_search is distinct from public.search_normalize(name)$$, $$values ('games'::text, 0::bigint), ('game_alt_titles', 0), ('publishers', 0)$$, 'Runbook 8: die Prüfabfrage meldet bei unverändertem Stand überall 0 Abweichungen'); -- Drift simulieren: Die Funktion wird geändert, die gespeicherten Spalten bleiben (wie nach einem Image-Upgrade) create or replace function public.search_normalize(p text) returns text language sql immutable strict parallel safe set search_path = '' as $$ select upper(p) $$; select results_eq($$select 'games' as tabelle, count(*) > 0 as abweichend from public.games where title_search is distinct from public.search_normalize(title) union all select 'game_alt_titles', count(*) > 0 from public.game_alt_titles where title_search is distinct from public.search_normalize(title) union all select 'publishers', count(*) > 0 from public.publishers where name_search is distinct from public.search_normalize(name)$$, $$values ('games'::text, true), ('game_alt_titles', true), ('publishers', true)$$, 'Runbook 8: die Prüfabfrage erkennt eine geänderte Normalisierung in allen drei Tabellen'); select * from finish(); rollback;