[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.
This commit is contained in:
Lukas Brübach 2026-10-11 14:09:05 +02:00
parent e08402ac94
commit 67a36b9828
6 changed files with 70 additions and 2 deletions

View file

@ -14,4 +14,7 @@ message Command_DeckUpload {
reserved 5, 6, 8; reserved 5, 6, 8;
reserved "banner_card_name", "banner_card_provider", "tags"; reserved "banner_card_name", "banner_card_provider", "tags";
optional string color_identity = 7; 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;
} }

View file

@ -6,4 +6,9 @@ message Response_DeckDownload {
optional Response_DeckDownload ext = 1007; optional Response_DeckDownload ext = 1007;
} }
optional string deck = 1; 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;
} }

View file

@ -11,6 +11,15 @@ message ServerInfo_DeckStorage_File {
// Tag names associated with the deck. Empty for decks uploaded before the // Tag names associated with the deck. Empty for decks uploaded before the
// tags column existed. // tags column existed.
repeated string tags = 6; 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 { message ServerInfo_DeckStorage_Folder {

View file

@ -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;

View file

@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` (
PRIMARY KEY (`version`) PRIMARY KEY (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; ) 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 -- users and user data tables
CREATE TABLE IF NOT EXISTS `cockatrice_users` ( 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, `banner_card_provider` varchar(32) NULL,
`color_identity` varchar(5) NULL, `color_identity` varchar(5) NULL,
`tags` text 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`), PRIMARY KEY (`id`),
KEY `FolderPlusUser` (`id_folder`,`id_user`), KEY `FolderPlusUser` (`id_folder`,`id_user`),
FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; ) 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 -- Temporary deck shares: a named bundle of decks that can be fetched by
-- anyone who knows the (unguessable) token, until the share expires. -- anyone who knows the (unguessable) token, until the share expires.
CREATE TABLE IF NOT EXISTS `cockatrice_deck_share` ( CREATE TABLE IF NOT EXISTS `cockatrice_deck_share` (

View file

@ -21,7 +21,7 @@
class QSqlQuery; class QSqlQuery;
#define DATABASE_SCHEMA_VERSION 40 #define DATABASE_SCHEMA_VERSION 41
class Servatrice; class Servatrice;