From ed5d41e7ed6004625ba638c109d0c4b625f056e7 Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 29 Oct 2026 07:50:02 +0000 Subject: [PATCH] store: run the names migrations after badges --- apps/simplex-store-service/README.md | 5 +++++ .../StoreService/Store/Postgres/Migrations.hs | 12 ++++++------ .../src/StoreService/Store/SQLite/Migrations.hs | 12 ++++++------ tests/Bots/StoreService/NamesStoreTests.hs | 4 ++-- tests/Bots/StoreService/WebTests.hs | 17 +++++++++++++++-- 5 files changed, 34 insertions(+), 16 deletions(-) diff --git a/apps/simplex-store-service/README.md b/apps/simplex-store-service/README.md index 76b4050de7..9548e8b45f 100644 --- a/apps/simplex-store-service/README.md +++ b/apps/simplex-store-service/README.md @@ -531,6 +531,11 @@ running beside the new one, on the same port and database. The database `badge_s `BADGE_DB_CONN`, the `sx_badge_service_` tables and the chat database prefix `simplex_badge_service` are unchanged. +A badge-service database upgrades in place: the store's own migrations run after every badge +migration, `20261012_badge_subscriptions` included. A store database created before the names +migration became `20261013_names` (it was `20261010_names`) does not start, as the service refuses +migrations applied out of the order it lists. Recreate such a database. + ### Two sites The image carries the badges build at `/srv/badges` and the names build at `/srv/names`, the diff --git a/apps/simplex-store-service/src/StoreService/Store/Postgres/Migrations.hs b/apps/simplex-store-service/src/StoreService/Store/Postgres/Migrations.hs index 6aaf633ca7..74014fc6a4 100644 --- a/apps/simplex-store-service/src/StoreService/Store/Postgres/Migrations.hs +++ b/apps/simplex-store-service/src/StoreService/Store/Postgres/Migrations.hs @@ -2,7 +2,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} -module StoreService.Store.Postgres.Migrations (storeServiceSchemaMigrations, servicePrefix, withPrefix) where +module StoreService.Store.Postgres.Migrations (storeServiceSchemaMigrations, schemaMigrations, servicePrefix, withPrefix) where import Data.List (sortOn) import Data.Text (Text) @@ -21,7 +21,7 @@ schemaMigrations = ("20260918_badge_group_ops", m20260918_badge_group_ops, Just down_m20260918_badge_group_ops), ("20260925_badge_store_receipts", m20260925_badge_store_receipts, Just down_m20260925_badge_store_receipts), ("20261012_badge_subscriptions", m20261012_badge_subscriptions, Just down_m20261012_badge_subscriptions), - ("20261010_names", m20261010_names, Just down_m20261010_names), + ("20261013_names", m20261013_names, Just down_m20261013_names), ("20261022_drop_taken_codes", m20261022_drop_taken_codes, Just down_m20261022_drop_taken_codes), ("20261022_name_balances", m20261022_name_balances, Just down_m20261022_name_balances), ("20261022_name_late_wait", m20261022_name_late_wait, Just down_m20261022_name_late_wait), @@ -280,8 +280,8 @@ DROP TABLE @subscription_charges; -- A registration is live as Names.Store's orderLive decides. -- reserved_by is added after the registrations table, which references the codes table. -m20261010_names :: Text -m20261010_names = +m20261013_names :: Text +m20261013_names = withPrefix servicePrefix [r| @@ -417,8 +417,8 @@ CREATE INDEX @idx_name_transactions_status ON @name_transactions(status, registr -- A table another table's foreign key references cannot be dropped, so reserved_by goes first. -- The name orders' invoices go with them, except those with a payment, which stay for the accounts. -down_m20261010_names :: Text -down_m20261010_names = +down_m20261013_names :: Text +down_m20261013_names = withPrefix servicePrefix [r| diff --git a/apps/simplex-store-service/src/StoreService/Store/SQLite/Migrations.hs b/apps/simplex-store-service/src/StoreService/Store/SQLite/Migrations.hs index 86c9532352..922fec7431 100644 --- a/apps/simplex-store-service/src/StoreService/Store/SQLite/Migrations.hs +++ b/apps/simplex-store-service/src/StoreService/Store/SQLite/Migrations.hs @@ -2,7 +2,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} -module StoreService.Store.SQLite.Migrations (storeServiceSchemaMigrations, servicePrefix, withPrefix) where +module StoreService.Store.SQLite.Migrations (storeServiceSchemaMigrations, schemaMigrations, servicePrefix, withPrefix) where import Data.List (sortOn) import Data.Text (Text) @@ -22,7 +22,7 @@ schemaMigrations = ("20260918_badge_group_ops", m20260918_badge_group_ops, Just down_m20260918_badge_group_ops), ("20260925_badge_store_receipts", m20260925_badge_store_receipts, Just down_m20260925_badge_store_receipts), ("20261012_badge_subscriptions", m20261012_badge_subscriptions, Just down_m20261012_badge_subscriptions), - ("20261010_names", m20261010_names, Just down_m20261010_names), + ("20261013_names", m20261013_names, Just down_m20261013_names), ("20261022_drop_taken_codes", m20261022_drop_taken_codes, Just down_m20261022_drop_taken_codes), ("20261022_name_balances", m20261022_name_balances, Just down_m20261022_name_balances), ("20261022_name_late_wait", m20261022_name_late_wait, Just down_m20261022_name_late_wait), @@ -281,8 +281,8 @@ DROP TABLE @subscription_charges; -- A registration is live as Names.Store's orderLive decides. -- reserved_by is added after the registrations table, which references the codes table. -m20261010_names :: Query -m20261010_names = +m20261013_names :: Query +m20261013_names = withPrefix servicePrefix [sql| @@ -419,8 +419,8 @@ CREATE INDEX @idx_name_transactions_status ON @name_transactions(status, registr -- SQLite refuses to drop a column in a foreign key, and dropping a table deletes its rows first, -- so the codes' references to registrations are cleared before the registrations go. -- The name orders' invoices go with them, except those with a payment, which stay for the accounts. -down_m20261010_names :: Query -down_m20261010_names = +down_m20261013_names :: Query +down_m20261013_names = withPrefix servicePrefix [sql| diff --git a/tests/Bots/StoreService/NamesStoreTests.hs b/tests/Bots/StoreService/NamesStoreTests.hs index 996e27a5d6..7650d4a231 100644 --- a/tests/Bots/StoreService/NamesStoreTests.hs +++ b/tests/Bots/StoreService/NamesStoreTests.hs @@ -196,9 +196,9 @@ testNamesColumns = withServiceStore $ \st -> do testNamesDownUp :: IO () testNamesDownUp = withServiceStore $ \st -> do - let names = dropWhile ((/= "20261010_names") . name) storeServiceSchemaMigrations + let names = dropWhile ((/= "20261013_names") . name) storeServiceSchemaMigrations badgeColumns = mapM (fmap sort . columnsOf st) badgeTables - map name names `shouldBe` ["20261010_names", "20261022_drop_taken_codes", "20261022_name_balances", "20261022_name_late_wait", "20261023_name_terms", "20261027_name_invoiced_units"] + map name names `shouldBe` ["20261013_names", "20261022_drop_taken_codes", "20261022_name_balances", "20261022_name_late_wait", "20261023_name_terms", "20261027_name_invoiced_units"] badgeBefore <- badgeColumns code <- issue st 1 usable placeCode st "r1" ownerA (order "alicebakery" 1) code `shouldReturn` Right () diff --git a/tests/Bots/StoreService/WebTests.hs b/tests/Bots/StoreService/WebTests.hs index 921bdafe4a..4422256799 100644 --- a/tests/Bots/StoreService/WebTests.hs +++ b/tests/Bots/StoreService/WebTests.hs @@ -116,12 +116,12 @@ import Text.Read (readMaybe) import UnliftIO.Temporary (withTempDirectory) #if defined(dbPostgres) -import StoreService.Store.Postgres.Migrations (storeServiceSchemaMigrations) +import StoreService.Store.Postgres.Migrations (schemaMigrations, storeServiceSchemaMigrations) import ChatClient (testDBConnstr) import Database.PostgreSQL.Simple (Only (..)) import qualified Simplex.Messaging.Agent.Store.Postgres.Migrations as Migrations #else -import StoreService.Store.SQLite.Migrations (storeServiceSchemaMigrations) +import StoreService.Store.SQLite.Migrations (schemaMigrations, storeServiceSchemaMigrations) import Data.String (fromString) import Database.SQLite.Simple (Only (..)) import qualified Database.SQLite.Simple as SQL @@ -182,6 +182,7 @@ badgeWebTests = do it "refuses a duplicate provider_ref" testProviderRefUnique it "M20260918 adds the multi-use columns and the group table" testGroupOpsColumns it "M20260918 defaults a fresh code to one use with none spent" testGroupOpsRedeemCounts + it "declares its migrations in name order, master's badge migrations before the names ones" testMigrationOrder it "migrates all the way down and up again" testSchemaDownUpCycle it "rolls back only the group migration and re-applies it, keeping a code redeemed twice spent" testGroupOpsDownUp it "rolls back only the subscription migration, restoring the columns it dropped, and re-applies it" testSubscriptionsDownUp @@ -405,6 +406,18 @@ runMigrations st = Migrations.run st Nothing runMigrations st = Migrations.run st Nothing True #endif +-- | Applied migrations must be a prefix of this list by name, so a migration of ours dated +-- before one of master's breaks upgrading a badge-service database. +testMigrationOrder :: IO () +testMigrationOrder = do + let declared = map (\(n, _, _) -> n) schemaMigrations + [(a, b) | (a, b) <- zip declared (drop 1 declared), a >= b] `shouldBe` [] + let (badges, ours) = span (`elem` badgeMigrations) (map name storeServiceSchemaMigrations) + badges `shouldBe` badgeMigrations + ours `shouldBe` ["20261013_names", "20261022_drop_taken_codes", "20261022_name_balances", "20261022_name_late_wait", "20261023_name_terms", "20261027_name_invoiced_units"] + where + badgeMigrations = ["20260915_badge_service_schema", "20260918_badge_group_ops", "20260925_badge_store_receipts", "20261012_badge_subscriptions"] + testSchemaDownUpCycle :: IO () testSchemaDownUpCycle = withServiceStore $ \st -> do let downMigrations = mapMaybe toDownMigration storeServiceSchemaMigrations