Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/schema-upgrade.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ jobs:
- name: Apply 013 then verify the army-family compatibility upgrade
working-directory: database
run: bash ../scripts/with-test-timescale.sh --profile baseline-013 -- go test ./schema -run '^TestArmyCodeFamilyCompatibilityMigration$' -count=1 -v
- name: Apply 018 then verify the populated personal-base upgrade
working-directory: database
run: bash ../scripts/with-test-timescale.sh --profile baseline-018 -- go test ./schema -run '^TestPersonalBaseStorageMigration$' -count=1 -v
4 changes: 2 additions & 2 deletions RETAINED_API_FIXTURE.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Retained API database fixture

The canonical Goose sequence is `database/timescale/001` through `015`.
The canonical Goose sequence is `database/timescale/001` through `016`.
Production was confirmed by the user to be at 006 before consolidation; files
001–006 are unchanged. `007_worker_api.sql` combines the unapplied Worker/API
migrations formerly numbered 007, 008, 009, 010, 012, 013, 020, 022, 023, 027 and
028 into one transaction, excluding the user-rejected player-link mutex, global
AI-budget mutex, subject mutex, resource ownership ledger and Discord delivery receipt tables. Its Down operation is intentionally irreversible.

`008_ranked_battle_history.sql` adds the one-year farming and two-perspective Ranked/Legend histories, immutable normalized army compositions, and the final `ranked_league_group_members` source-counter shape. `009_league_army_analytics.sql` adds permanent league/Legend rollups and immutable army-family assignments. Both have structural Down paths, but 008 cannot recreate stale duplicate season memberships removed before it enforces one group per player and season. `010_cwl_season_statistics.sql` remains the independent, reversible CWL population summary, and 011 adds active verified-player state. See `docs/ranked-battle-history.md` for every column and lifecycle.
`008_ranked_battle_history.sql` adds the one-year farming and two-perspective Ranked/Legend histories, immutable normalized army compositions, and the final `ranked_league_group_members` source-counter shape. `009_league_army_analytics.sql` adds permanent league/Legend rollups and immutable army-family assignments. Both have structural Down paths, but 008 cannot recreate stale duplicate season memberships removed before it enforces one group per player and season. Migration 010 remains immutable history, 011 adds active verified-player state, and 016 removes the rejected CWL-only population summary. See `docs/ranked-battle-history.md` for every retained analytics column and lifecycle, and `docs/cwl-season-statistics-removal.md` for the consumer cutover.

`bash scripts/with-test-timescale.sh --profile retained-api -- COMMAND [ARG ...]`
applies the complete canonical sequence to a fresh local tmpfs Timescale
Expand Down
14 changes: 0 additions & 14 deletions contracts/army-family-v1.json

This file was deleted.

25 changes: 0 additions & 25 deletions contracts/army-hash-v2.json

This file was deleted.

123 changes: 0 additions & 123 deletions database/migrations/cleanup_army_compositions.go

This file was deleted.

93 changes: 0 additions & 93 deletions database/migrations/cleanup_army_compositions_test.go

This file was deleted.

25 changes: 0 additions & 25 deletions database/migrations/legend_composition_scope_schema_test.go

This file was deleted.

4 changes: 4 additions & 0 deletions database/migrations/war_archive_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ func TestTimescaleMigrationsAreExplicitlyNumbered(t *testing.T) {
"013_battle_player_time_identity.sql",
"014_ranked_defense_loot_nullable.sql",
"015_army_code_family_compatibility.sql",
"016_remove_cwl_season_statistics.sql",
"017_final_operational_contract.sql",
"018_personal_base_library.sql",
"019_unlimited_personal_bases.sql",
}
if len(files) != len(want) {
t.Fatalf("Timescale migrations = %v, want %v", files, want)
Expand Down
72 changes: 72 additions & 0 deletions database/schema/army_code_family_migration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,76 @@ func TestArmyCodeFamilyCompatibilityMigration(t *testing.T) {
t.Fatal("rollback discarded v2-only data")
}
check(`SELECT max(version_id)=15 FROM goose_db_version WHERE is_applied`)

// Complete the code-only representative so the final FK can preserve it,
// then prove a data-bearing 015 -> 019 conversion across every contract.
run(`INSERT INTO army_compositions(army_hash,normalized_share_code)
VALUES(decode(repeat('dd',32),'hex'),'u3x0');
INSERT INTO auth_users(user_id,provider) VALUES('100','discord');
INSERT INTO player_links(tag,is_verified,source,user_id) VALUES('#P0Y',true,'api_token','100');
INSERT INTO mobile_push_devices(
user_id,device_id,platform,provider,environment,token_ciphertext,token_hash,
war_attacks_enabled,war_state_enabled,war_reminders_enabled,raid_reminders_enabled,
events_enabled,announcements_enabled,monthly_support_enabled,reminder_timings,raid_reminder_timings
) VALUES('100','iphone','ios','fcm','production','cipher','hash',true,false,true,true,true,false,true,'{60}','{120}');
INSERT INTO mobile_notification_accounts(user_id,player_tag) VALUES('100','#P0Y');
INSERT INTO mobile_notification_deliveries(user_id,notification_key) VALUES('100','old-delivery');
INSERT INTO bases(id,message_id,base_link,downloaders,images,description,upvoter_ids,downvoter_ids)
VALUES('00000000-0000-4000-8000-000000000001','123456789012345678',
'https://link.clashofclans.com/en?action=OpenLayout&id=TH17%3AHV%3AAAAA',
'{100}','{https://api.clashk.ing/v2/media/base.png}','Legacy','{100}','{}');
INSERT INTO basic_clan(tag,name,public_war_log,war_wins,member_count,badge_token,troops_donated,troops_received)
VALUES('#2PP','Clan',true,1,1,'badge',0,0);
INSERT INTO basic_player(tag,name,league_id,clan_tag,townhall_level,trophies)
VALUES('#P0Y','Player',105000036,'#2PP',18,6500);
INSERT INTO leaderboard_history_player_home(
location_id,date,player_tag,player_name,exp_level,trophies,attack_wins,defense_wins,
rank,previous_rank,clan_tag,clan_name,clan_badge_token,league_id
) VALUES
('global','2026-09-10','#P0Y','Historical Player',250,6400,21,7,12,15,
'#2PP','Historical Clan','historical-badge',29000022),
('32000006','2026-09-10','#P0L','Local Player',175,5900,11,9,3,4,
NULL,NULL,NULL,29000021);`)

if err := migrate("up-to", "19"); err != nil {
t.Fatal("final contract upgrade:", err)
}
check(`SELECT max(version_id)=19 FROM goose_db_version WHERE is_applied`)
check(`SELECT duration_seconds=0 AND direction=1 AND battle_mode=2
FROM battles_ranked WHERE share_code='u3x0'`)
check(`SELECT EXISTS(SELECT 1 FROM army_compositions WHERE share_code='u3x0')
AND NOT EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema='public' AND column_name IN ('army_hash','anchor_army_hash'))`)
check(`SELECT war_attacks_enabled AND NOT war_state_enabled AND war_reminders_enabled
AND raid_reminders_enabled AND events_enabled AND NOT announcements_enabled
AND monthly_support_enabled AND NOT legend_defenses_enabled
AND reminder_timings='{60}'::integer[] AND raid_reminder_timings='{120}'::integer[]
FROM mobile_notification_preferences WHERE user_id='100'`)
check(`SELECT to_regclass('public.mobile_notification_deliveries') IS NULL`)
check(`SELECT base.id=1 AND image.image_url='https://api.clashk.ing/v2/media/base.png'
AND counts.download_count=1 AND counts.upvote_count=1 AND counts.downvote_count=0
FROM bases base JOIN base_images image ON image.base_id=base.id
JOIN base_public_counts counts ON counts.base_id=base.id
WHERE base.message_id='123456789012345678'`)
check(`SELECT to_regclass('public.user_saved_bases') IS NOT NULL
AND to_regclass('public.user_base_slots') IS NULL
AND to_regclass('public.base_downloaders') IS NULL
AND (SELECT downloads ? '100' FROM bases WHERE message_id='123456789012345678')`)
check(`SELECT name='Player' AND trophies=6500 AND global_rank=1
AND clan_tag='#2PP' AND clan_name='Clan' FROM legend_rankings_current WHERE tag='#P0Y'`)
check(`SELECT count(*)=2 FROM leaderboard_history_player_home WHERE date='2026-09-10'`)
check(`SELECT player_name='Historical Player' AND exp_level=250 AND trophies=6400
AND attack_wins=21 AND defense_wins=7 AND rank=12 AND previous_rank=15
AND clan_tag='#2PP' AND clan_name='Historical Clan'
AND clan_badge_token='historical-badge' AND league_id=29000022
FROM leaderboard_history_player_home
WHERE location_id='global' AND date='2026-09-10' AND player_tag='#P0Y'`)
check(`SELECT player_name='Local Player' AND rank=3 AND clan_tag IS NULL
FROM leaderboard_history_player_home
WHERE location_id='32000006' AND date='2026-09-10' AND player_tag='#P0L'`)
run(`INSERT INTO legend_rankings_history(day,tag,global_rank,trophies)
VALUES('2026-09-10','#P0Y',1,6500)`)
check(`SELECT global_rank=1 AND trophies=6500
FROM legend_rankings_history WHERE day='2026-09-10' AND tag='#P0Y'`)
check(`SELECT count(*)=2 FROM leaderboard_history_player_home WHERE date='2026-09-10'`)
}
Loading
Loading