From 67a36b9828694c654d42858901dc02a792e7665e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sun, 11 Oct 2026 14:09:05 +0200 Subject: [PATCH] [Server] Deck sync identity: schema v41 + proto guid/hash fields Bumps the schema to v41: master is already at v40, so the deck-sync migration is servatrice_0040_to_0041.sql. --- .../protocol/pb/command_deck_upload.proto | 3 ++ .../protocol/pb/response_deck_download.proto | 5 +++ .../protocol/pb/serverinfo_deckstorage.proto | 9 ++++++ .../migrations/servatrice_0040_to_0041.sql | 32 +++++++++++++++++++ servatrice/servatrice.sql | 21 +++++++++++- .../src/servatrice_database_interface.h | 2 +- 6 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 servatrice/migrations/servatrice_0040_to_0041.sql diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto index 1a5d44e9e..89ad94899 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto @@ -14,4 +14,7 @@ message Command_DeckUpload { reserved 5, 6, 8; reserved "banner_card_name", "banner_card_provider", "tags"; optional string color_identity = 7; + // Stable per-deck identity for two-way collection sync. Sent on first + // synced upload, echoed back in ServerInfo_DeckStorage_File afterwards. + optional string guid = 9; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_download.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_download.proto index f1839a9e2..11a72763a 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_download.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_download.proto @@ -6,4 +6,9 @@ message Response_DeckDownload { optional Response_DeckDownload ext = 1007; } optional string deck = 1; + // Sync identity echoed so the client can update its sidecar without an + // extra list fetch. + optional string guid = 2; + optional string hash = 3; + optional uint64 last_modified = 4; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto index b04d676d8..04dd37cf5 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto @@ -11,6 +11,15 @@ message ServerInfo_DeckStorage_File { // Tag names associated with the deck. Empty for decks uploaded before the // tags column existed. repeated string tags = 6; + // Stable per-deck identity for two-way collection sync. Empty for decks + // uploaded before the sync migration until they are re-uploaded. + optional string guid = 7; + // Content hash (DeckList::getDeckHash) for change detection without + // downloading the full list. + optional string hash = 8; + // Last modification time, seconds since Unix epoch. Drives newest-wins + // default and conflict-dialog recency highlight. + optional uint64 last_modified = 9; } message ServerInfo_DeckStorage_Folder { diff --git a/servatrice/migrations/servatrice_0040_to_0041.sql b/servatrice/migrations/servatrice_0040_to_0041.sql new file mode 100644 index 000000000..83edf871f --- /dev/null +++ b/servatrice/migrations/servatrice_0040_to_0041.sql @@ -0,0 +1,32 @@ +-- Servatrice db migration from version 40 to version 41 + +-- Deck collection sync + versioning. +-- +-- 1. Stable per-deck identity and change detection for two-way sync: +-- guid links a local file to its server row across renames, hash detects +-- content changes without downloading, last_modified drives newest-wins +-- default and conflict highlighting, deleted implements tombstones so a +-- delete on one device does not resurrect from the other. +-- 2. Limited per-deck version history: every hash-changing upload inserts a +-- row; the server prunes beyond the configured retention (default 20). + +ALTER TABLE `cockatrice_decklist_files` + ADD COLUMN `guid` char(36) NULL AFTER `tags`, + ADD COLUMN `hash` char(40) NULL AFTER `guid`, + ADD COLUMN `last_modified` datetime NULL AFTER `hash`, + ADD COLUMN `deleted` tinyint(1) NOT NULL DEFAULT 0 AFTER `last_modified`; + +CREATE TABLE IF NOT EXISTS `cockatrice_deck_versions` ( + `id` int(7) unsigned zerofill NOT NULL auto_increment, + `deck_id` int(7) unsigned zerofill NOT NULL, + `version_no` int(7) unsigned NOT NULL, + `content` text NOT NULL, + `hash` char(40) NULL, + `created_at` datetime NOT NULL, + `created_by_clientid` varchar(15) NULL, + PRIMARY KEY (`id`), + KEY `deck_id` (`deck_id`), + FOREIGN KEY(`deck_id`) REFERENCES `cockatrice_decklist_files`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; + +UPDATE cockatrice_schema_version SET version=41 WHERE version=40; diff --git a/servatrice/servatrice.sql b/servatrice/servatrice.sql index e942e4e91..a259a7c7d 100644 --- a/servatrice/servatrice.sql +++ b/servatrice/servatrice.sql @@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` ( PRIMARY KEY (`version`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -INSERT INTO cockatrice_schema_version VALUES(40); +INSERT INTO cockatrice_schema_version VALUES(41); -- users and user data tables CREATE TABLE IF NOT EXISTS `cockatrice_users` ( @@ -73,11 +73,30 @@ CREATE TABLE IF NOT EXISTS `cockatrice_decklist_files` ( `banner_card_provider` varchar(32) NULL, `color_identity` varchar(5) NULL, `tags` text NULL, + `guid` char(36) NULL, + `hash` char(40) NULL, + `last_modified` datetime NULL, + `deleted` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `FolderPlusUser` (`id_folder`,`id_user`), FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; +-- Per-deck version history for collection sync. +-- One row per hash-changing upload; pruned beyond retention (default 20). +CREATE TABLE IF NOT EXISTS `cockatrice_deck_versions` ( + `id` int(7) unsigned zerofill NOT NULL auto_increment, + `deck_id` int(7) unsigned zerofill NOT NULL, + `version_no` int(7) unsigned NOT NULL, + `content` text NOT NULL, + `hash` char(40) NULL, + `created_at` datetime NOT NULL, + `created_by_clientid` varchar(15) NULL, + PRIMARY KEY (`id`), + KEY `deck_id` (`deck_id`), + FOREIGN KEY(`deck_id`) REFERENCES `cockatrice_decklist_files`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; + -- Temporary deck shares: a named bundle of decks that can be fetched by -- anyone who knows the (unguessable) token, until the share expires. CREATE TABLE IF NOT EXISTS `cockatrice_deck_share` ( diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index 6b38384e4..d0fd8bd12 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -21,7 +21,7 @@ class QSqlQuery; -#define DATABASE_SCHEMA_VERSION 40 +#define DATABASE_SCHEMA_VERSION 41 class Servatrice;