@ -0,0 +1,759 @@
-- 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 ( 201 ) ;
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 ' ) ;
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-0000000000b1 ' , ' Qxz Hersteller Eins ' ) ,
( ' f9000000-0000-0000-0000-0000000000b2 ' , ' 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 ' ) ;
-- 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 ' ) ;
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-0000000000b1 ' ) ,
( ' f9000000-0000-0000-0000-000000000040 ' , ' f9000000-0000-0000-0000-0000000000b2 ' ) ,
( ' f9000000-0000-0000-0000-000000000040 ' , ' f9000000-0000-0000-0000-0000000000b1 ' ) ,
( ' f9000000-0000-0000-0000-000000000041 ' , ' f9000000-0000-0000-0000-0000000000b2 ' ) ,
( ' 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-0000000000b1 ' ) ,
( ' f9000000-0000-0000-0000-000000000033 ' , ' f9000000-0000-0000-0000-0000000000b1 ' ) ;
-- 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 ' ) ;
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-0000000000b2 ' ) , ' 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-0000000000b2 ' ;
select is ( ( select name_search from public . publishers where id = ' f9000000-0000-0000-0000-0000000000b2 ' ) , ' 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-0000000000b2 ' ;
-- 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 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 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-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 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 ;