Compare commits

...

2 commits

Author SHA1 Message Date
cschlaefke
9a63985d74 Merge origin/feature/2b-katalog-zusammenfuehren into feature/2c-katalog-meldungen
All checks were successful
CI / flutter (pull_request) Successful in 1m22s
CI / web-build (pull_request) Successful in 58s
CI / supabase (pull_request) Successful in 8s
CI / supabase-tests (pull_request) Successful in 32s
CI / e2e (pull_request) Successful in 2m54s
2026-11-04 00:25:32 +01:00
cschlaefke
500763d5ca docs(db): late references to merged source as known limitation, with test (KAT-05, 2b)
All checks were successful
CI / flutter (pull_request) Successful in 1m34s
CI / web-build (pull_request) Successful in 58s
CI / supabase (pull_request) Successful in 9s
CI / supabase-tests (pull_request) Successful in 36s
CI / e2e (pull_request) Successful in 2m55s
Refs #2

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 00:24:49 +01:00
3 changed files with 110 additions and 1 deletions

View file

@ -262,6 +262,35 @@ Die Quelle ist ausgeblendet und verweist auf das Ziel. Das Ziel bleibt sichtbar;
select id from public.games where merged_into_id = '7a1b2c3d-0000-4000-8000-000000000002';
```
### Bekannte Einschränkung: nachträgliche Verweise auf eine zusammengeführte Quelle
Nach dem Zusammenführen kann sich noch Neues an die ausgeblendete Quelle hängen, zum Beispiel durch einen veralteten Client oder eine offline erfasste Partie (`plays.client_id`), die erst nach dem Merge synchronisiert wird. Das ist eine bekannte Einschränkung, kein Fehler: Die Quelle wird nie gelöscht und bleibt als Fremdschlüsselziel gültig, die App leitet nur die Anzeige auf das Ziel um. Die Verweise verschwinden also nicht, sie hängen nur am falschen Eintrag.
**Erkennung** (nach einem Merge, bei Verdacht oder als gelegentliche Kontrolle; die Zahlen müssen überall `0` sein):
```sql
select 'collection_items' as tabelle, count(*) as anzahl from public.collection_items t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null
union all select 'favorite_games', count(*) from public.favorite_games t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null
union all select 'plays', count(*) from public.plays t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null
union all select 'score_sheets', count(*) from public.score_sheets t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null
union all select 'game_variants', count(*) from public.game_variants t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null;
```
**Behebung:** als `postgres` die interne Umhänge-Funktion erneut aufrufen, in einer Transaktion. `admin_merge_games` geht hier nicht mehr (`merge_source_already_merged`):
```sql
select public.repoint_game_references('7a1b2c3d-0000-4000-8000-000000000002'::uuid, '7a1b2c3d-0000-4000-8000-000000000003'::uuid);
```
Die Funktion arbeitet wie beim Merge (Kollisionsregeln, Favoriten, Editionen). Da die Quelle ein Katalogspiel ist, werden Varianten, die erst nachträglich an der Quelle entstanden sind, am Ziel kopiert beziehungsweise auf gleichnamige abgebildet und danach an der Quelle gelöscht. Die Quelle bleibt ausgeblendet, `merged_into_id` und `hidden_at` ändern sich nicht. Das Ergebnis-JSON hat dieselben Schlüssel wie beim Merge (ohne die Felder von `admin_merge_games`).
**Nachprüfung:** den Erkennungs-Baustein erneut ausführen; er muss überall `0` liefern.
**Was die Funktion macht**
| Was | Regel |

View file

@ -722,6 +722,8 @@ Umgesetzt: B5 (Migration `20261103193928_catalog_merge.sql`), B6 (pgTAP `07_cata
- **Nachtrag Review 2b:** Tests ergänzt (Spec-F1-Edition am Ziel, `variants_cleared` im Kollisionszweig, gleichnamige Quellvarianten, zweiter Fremdschlüssel-Wächter auf `game_variants`, unbeteiligte Spiele, Ziel `null`); Runbook um Trockenlauf (`begin; …; rollback;` in einem Lauf, nur dieser Baustein läuft nicht wörtlich in pgTAP), Ketten-Nachprüfung und Nachschlagen von `merged_into_id` ergänzt. Der Null-Check in `admin_merge_games` ist ein äquivalenter Mutant (das spätere `not found` liefert denselben Fehler) und bleibt als ausdrückliche Absicherung. Offen für Chris: Verweise, die nach dem Merge noch auf die Quelle entstehen (W3).
- **Nachtrag W3 (Chris):** Nachträgliche Verweise auf eine bereits zusammengeführte Quelle sind eine bekannte Einschränkung, kein neuer Code. Runbook 5 beschreibt Erkennung, Behebung (`repoint_game_references` erneut als `postgres`) und Nachprüfung; `07` testet das (Quelle bleibt ausgeblendet, Kollisionen, nachträglich entstandene Variante wird am Ziel kopiert).
### 2026-11-03 – Umsetzung PR 2c (backend-dev)
Umgesetzt: B8 (Migration `20261103195151_game_reports.sql`), B9 (pgTAP `08_game_reports.test.sql`, 160 Tests; `07` und `00` angepasst), B10 (Runbook Abschnitt 6). Abweichungen und Ergänzungen, jeweils zur Prüfung im PR:

View file

@ -5,7 +5,7 @@
-- Ab 2c kennt der Fremdschlüssel-Wächter am Ende auch game_reports.target_game_id (behandelt) und
-- game_reports.provisional_game_id (bewusst ausgelassen); admin_merge_games meldet reports_repointed.
begin;
select plan(107);
select plan(118);
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"}'),
@ -490,6 +490,84 @@ select is((select v.name || '/' || v.game_id::text from public.collection_items
where c.id = 'f7000000-0000-0000-0000-0000000003c2'),
'Jubiläum/f7000000-0000-0000-0000-0000000003b1', 'Spec-F1: Kollision: die Edition der Quelle kommt über die Zuordnung (Kopie am Ziel) in den Zieleintrag');
-- Nachträgliche Verweise auf eine bereits zusammengeführte Quelle (Runbook 5, bekannte Einschränkung; Entscheidung Chris, W3).
-- Die Quelle ...0002 ist im Hauptfall schon zusammengeführt. Ein veralteter Client bzw. eine offline erfasste Partie
-- hängt danach noch Daten an sie: Variante (neu am Quell-Eintrag), Partie, Sammlungseintrag mit Kollision, Favoriten.
create temp table rb_late_state as
select hidden_at, merged_into_id from public.games where id = '7a1b2c3d-0000-4000-8000-000000000002';
insert into public.game_variants (id, game_id, name) values
('f7000000-0000-0000-0000-0000000006e1', '7a1b2c3d-0000-4000-8000-000000000002', 'Nachzügler');
insert into public.plays (id, owner_id, game_id, variant_id, played_at, client_id) values
('f7000000-0000-0000-0000-0000000006b1', '11111111-1111-1111-1111-111111111111', '7a1b2c3d-0000-4000-8000-000000000002',
'f7000000-0000-0000-0000-0000000006e1', '2026-05-01', 'f7000000-0000-0000-0000-0000000006b1');
insert into public.collection_items (id, owner_id, game_id, list) values
('f7000000-0000-0000-0000-0000000006c1', '33333333-3333-3333-3333-333333333333', '7a1b2c3d-0000-4000-8000-000000000002', 'owned');
insert into public.favorite_games (id, profile_id, game_id, position) values
('f7000000-0000-0000-0000-0000000006d1', '33333333-3333-3333-3333-333333333333', '7a1b2c3d-0000-4000-8000-000000000003', 4),
('f7000000-0000-0000-0000-0000000006d2', '33333333-3333-3333-3333-333333333333', '7a1b2c3d-0000-4000-8000-000000000002', 5),
('f7000000-0000-0000-0000-0000000006d3', '44444444-4444-4444-4444-444444444444', '7a1b2c3d-0000-4000-8000-000000000002', 2);
-- Runbook 5, nachträgliche Verweise: Erkennung
create temp table rb_late_vorher as
select 'collection_items' as tabelle, count(*) as anzahl from public.collection_items t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null
union all select 'favorite_games', count(*) from public.favorite_games t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null
union all select 'plays', count(*) from public.plays t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null
union all select 'score_sheets', count(*) from public.score_sheets t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null
union all select 'game_variants', count(*) from public.game_variants t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null;
select set_eq($$select tabelle, anzahl from rb_late_vorher$$,
$$values ('collection_items'::text, 1::bigint), ('favorite_games', 2), ('plays', 1), ('score_sheets', 0), ('game_variants', 1)$$,
'KAT-05 Kriterium 8: Runbook 5, Erkennung nachträglicher Verweise auf die zusammengeführte Quelle');
select throws_ok($$select public.admin_merge_games('7a1b2c3d-0000-4000-8000-000000000002', '7a1b2c3d-0000-4000-8000-000000000003')$$,
'P0001', 'merge_source_already_merged', 'Runbook 5: ein zweiter admin_merge_games scheitert (merge_source_already_merged), deshalb repoint');
-- Runbook 5, nachträgliche Verweise: Behebung
create temp table rb_late_fix as
select public.repoint_game_references('7a1b2c3d-0000-4000-8000-000000000002'::uuid, '7a1b2c3d-0000-4000-8000-000000000003'::uuid);
select is((select repoint_game_references from rb_late_fix),
'{"collection_items":0,"collection_conflicts":1,"favorite_games":1,"favorite_conflicts":1,"plays":1,"score_sheets":0,"variants_copied":1,"variants_mapped":0,"variants_cleared":0}'::jsonb,
'KAT-05 Kriterium 8: Runbook 5, repoint auf eine zusammengeführte Quelle hängt nachträgliche Verweise um (Katalog→Katalog: Variante kopiert)');
-- Runbook 5, nachträgliche Verweise: Nachprüfung (Erkennung erneut)
create temp table rb_late_nachher as
select 'collection_items' as tabelle, count(*) as anzahl from public.collection_items t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null
union all select 'favorite_games', count(*) from public.favorite_games t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null
union all select 'plays', count(*) from public.plays t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null
union all select 'score_sheets', count(*) from public.score_sheets t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null
union all select 'game_variants', count(*) from public.game_variants t join public.games g on g.id = t.game_id
where g.id = '7a1b2c3d-0000-4000-8000-000000000002' and g.merged_into_id is not null;
select set_eq($$select tabelle, anzahl from rb_late_nachher$$,
$$values ('collection_items'::text, 0::bigint), ('favorite_games', 0), ('plays', 0), ('score_sheets', 0), ('game_variants', 0)$$,
'KAT-05 Kriterium 8: Runbook 5, nach dem erneuten repoint hängt nichts mehr an der Quelle');
select is((select count(*) from public.games g, rb_late_state s where g.id = '7a1b2c3d-0000-4000-8000-000000000002'
and g.hidden_at is not distinct from s.hidden_at and g.merged_into_id is not distinct from s.merged_into_id
and g.merged_into_id = '7a1b2c3d-0000-4000-8000-000000000003'), 1::bigint,
'Die Quelle bleibt ausgeblendet mit unverändertem merged_into_id und hidden_at');
select is((select row(list, note)::text from public.collection_items where id = 'f7000000-0000-0000-0000-0000000000c5'),
'(owned,"Caras Zielnotiz")', 'Kollision: Caras nachträglicher Eintrag fließt in den Zieleintrag (owned vor wishlist), der Quell-Eintrag ist weg');
select is((select count(*) from public.collection_items where id = 'f7000000-0000-0000-0000-0000000006c1'), 0::bigint, 'Kollision: nachträglicher Quell-Eintrag gelöscht');
select is((select array_agg(f.id::text order by f.id) from public.favorite_games f where f.id in
('f7000000-0000-0000-0000-0000000006d1', 'f7000000-0000-0000-0000-0000000006d2', 'f7000000-0000-0000-0000-0000000006d3')),
array['f7000000-0000-0000-0000-0000000006d1', 'f7000000-0000-0000-0000-0000000006d3'],
'Favoriten: Caras Quell-Favorit entfällt (Ziel-Favorit bleibt), Doras wechselt auf das Ziel');
select is((select game_id from public.favorite_games where id = 'f7000000-0000-0000-0000-0000000006d3'), '7a1b2c3d-0000-4000-8000-000000000003'::uuid,
'Favoriten: Doras nachträglicher Favorit hängt am Ziel');
select is((select p.game_id::text || ':' || v.game_id::text || ':' || v.name from public.plays p join public.game_variants v on v.id = p.variant_id
where p.id = 'f7000000-0000-0000-0000-0000000006b1'),
'7a1b2c3d-0000-4000-8000-000000000003:7a1b2c3d-0000-4000-8000-000000000003:Nachzügler',
'Partie: die nachträgliche Edition ist eine Kopie am Ziel');
select is((select count(*) from public.game_variants where id = 'f7000000-0000-0000-0000-0000000006e1'), 0::bigint,
'Die nachträgliche Variante der Quelle ist gelöscht (kopiert)');
-- Spec-F1 Kernregel (W1): Doppelbesitz, beide Einträge haben eine eigene Edition -> der Zieleintrag behält seine
insert into public.games (id, title, visibility) values
('f7000000-0000-0000-0000-000000000401', 'Edition Quelle', 'catalog'), ('f7000000-0000-0000-0000-000000000402', 'Edition Ziel', 'catalog');