docs(db): late references to merged source as known limitation, with test (KAT-05, 2b)
All checks were successful
All checks were successful
Refs #2 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
127f765d85
commit
500763d5ca
3 changed files with 110 additions and 1 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -720,3 +720,5 @@ Umgesetzt: B5 (Migration `20261103193928_catalog_merge.sql`), B6 (pgTAP `07_cata
|
|||
- **Runbook:** Alle SQL-Bausteine aus Abschnitt 5 laufen wörtlich in `07` (einmalige Prüfung per Skript; keine CI-Prüfung). Abschnitt 5 enthält die Duplikatsuche, eine Vorschau (Verweise, Doppelbesitz), den Aufruf, die Nachprüfung, die Regeln, die Schlüssel des Ergebnis-JSON und die Fehlertexte.
|
||||
|
||||
- **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).
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
-- Hinweis für 2c: Der Fremdschlüssel-Wächter am Ende muss um game_reports.target_game_id (behandelt) und
|
||||
-- game_reports.provisional_game_id (bewusst ausgelassen) erweitert werden, sonst wird er rot.
|
||||
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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue