Rework library sync around a JSON document; add card filters
Sync
The previous design put the merge inside SQL: timestamp-stamping triggers,
a tombstone table, per-device SQLite snapshots, a device id in the database,
and row merges written as correlated-subquery UPDATEs. All of that schema
was inherited by desktop, and the merge was very hard to verify.
Replace it with a single JSON document plus a three-way merge in plain Rust.
Storing the last-read remote document as the merge base makes "who changed
what" and "what was deleted" derivable, which removes the tombstone table,
the updated_at columns and all eight triggers. It also makes a single shared
file safe: if a publish is lost the remote is unchanged, so the next merge
re-detects the local edits and republishes rather than reading their absence
as a deletion. That removes the need for per-device files and for device
identity entirely. Schema impact drops to one column, decks.uuid.
Conflicts resolve to "theirs", which is required for convergence rather than
chosen for taste — preferring ours has each device re-asserting its own value
forever. Every such choice is reported to the user.
SQLite becomes invisible: the working copy lives in the app data dir and the
user only ever names a .json file. There is always a document, defaulting to
private storage, so edits are never stranded with nowhere to be saved.
Pointing at a file that doesn't exist yet moves the current library there;
pointing at one that does adopts it. A converter brings a pre-document
library.db across.
Android reaches the folder through the Storage Access Framework rather than
MANAGE_EXTERNAL_STORAGE, so no storage permission is declared at all.
Filters
Track card origin in reference.db: Universes Beyond, digital-only and
Un-set cards, plus numeric power/toughness. Each origin flag is derived as
"every printing", not "any printing" — ordinary cards get reprinted into
those sets, and Lightning Bolt appearing in Marvel Commander must not hide
it. Non-numeric power ("*", "1+*") stays NULL so it drops out of range
filters instead of being compared as zero.
Also wire up filters over data already present: price range, format legality
(restricted counts as legal) and "can be a commander". All of these degrade
silently against an older reference.db rather than failing every query.
The shared criteria live in one component used by both filter panels, as a
compact row of icon toggles.
Card Search gains the Library tab's bulk selection: checkboxes in both views,
select-all, and a bulk add-to-deck with a single undo for the whole batch.
This commit is contained in:
21
crates/mtgc-core/src/sql/library/0005_deck_uuid.sql
Normal file
21
crates/mtgc-core/src/sql/library/0005_deck_uuid.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
-- v5 — a stable, device-independent identity for decks.
|
||||
--
|
||||
-- Sync merges two copies of the library by matching entities across them.
|
||||
-- `library_cards` and `deck_cards` already have global keys (`oracle_id`, and
|
||||
-- `(deck, oracle_id)`), but `decks.id` is per-device autoincrement: two devices
|
||||
-- both hand out id 3 to different decks. Matching on `name` instead would work
|
||||
-- until someone renames a deck, at which point the merge sees a delete plus an
|
||||
-- unrelated create and the deck's contents are stranded.
|
||||
--
|
||||
-- This is the *only* schema sync needs. Change tracking is not stored here:
|
||||
-- the merge derives what changed (and what was deleted) by comparing against
|
||||
-- the last-synced document, so there are no timestamp columns, no tombstones,
|
||||
-- and no triggers.
|
||||
|
||||
ALTER TABLE decks ADD COLUMN uuid TEXT;
|
||||
|
||||
-- Existing decks predate the column. `randomblob` is SQLite's own CSPRNG, so
|
||||
-- this needs no help from Rust and no new dependency.
|
||||
UPDATE decks SET uuid = lower(hex(randomblob(16))) WHERE uuid IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX idx_decks_uuid ON decks(uuid);
|
||||
@@ -24,7 +24,27 @@ CREATE TABLE IF NOT EXISTS oracle_cards (
|
||||
-- absent (falls back to `printings`).
|
||||
image_uri TEXT NOT NULL DEFAULT '',
|
||||
back_image_uri TEXT NOT NULL DEFAULT '',
|
||||
default_scryfall_id TEXT NOT NULL DEFAULT ''
|
||||
default_scryfall_id TEXT NOT NULL DEFAULT '',
|
||||
-- Boolean: this card exists *only* as Universes Beyond (Lord of the Rings,
|
||||
-- Warhammer 40,000, Marvel, ...). Derived after the printings pass from
|
||||
-- `printings.universes_beyond`, deliberately as "every printing is UB"
|
||||
-- rather than "some printing is UB" — ordinary cards get reprinted into UB
|
||||
-- sets (Lightning Bolt is in Marvel Commander) and must not be caught by
|
||||
-- a filter meant to exclude other IPs' characters.
|
||||
universes_beyond INTEGER NOT NULL DEFAULT 0,
|
||||
-- Boolean: exists only on Arena/MTGO (Alchemy rebalances and the like), so
|
||||
-- it can never be part of a physical collection. Same every-printing
|
||||
-- rollup as `universes_beyond`.
|
||||
digital_only INTEGER NOT NULL DEFAULT 0,
|
||||
-- Boolean: exists only in "funny" sets — Unglued, Unhinged, Unstable,
|
||||
-- Unfinity. Not legal in any normal format.
|
||||
funny INTEGER NOT NULL DEFAULT 0,
|
||||
-- Printed power/toughness, NULL when the card isn't a creature *or* when
|
||||
-- the value isn't a plain number ("*", "1+*", "X"). NULL therefore means
|
||||
-- "can't be compared numerically", and such cards drop out of a
|
||||
-- power/toughness range filter rather than being coerced to 0.
|
||||
power REAL,
|
||||
toughness REAL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_oracle_name_lower ON oracle_cards(name_lower);
|
||||
CREATE INDEX IF NOT EXISTS idx_oracle_ci ON oracle_cards(color_identity);
|
||||
@@ -62,7 +82,13 @@ CREATE TABLE IF NOT EXISTS printings (
|
||||
rarity TEXT NOT NULL DEFAULT '',
|
||||
image_uri TEXT NOT NULL DEFAULT '',
|
||||
back_image_uri TEXT NOT NULL DEFAULT '', -- transform/MDFC/reversible back face
|
||||
price_usd REAL NOT NULL DEFAULT 0 -- Scryfall "prices.usd", 0 if unknown
|
||||
price_usd REAL NOT NULL DEFAULT 0, -- Scryfall "prices.usd", 0 if unknown
|
||||
-- Boolean: Scryfall lists "universesbeyond" in this printing's promo_types.
|
||||
universes_beyond INTEGER NOT NULL DEFAULT 0,
|
||||
-- Boolean: this printing isn't available on paper (Scryfall `games`).
|
||||
digital INTEGER NOT NULL DEFAULT 0,
|
||||
-- Boolean: this printing comes from a "funny" set (Scryfall `set_type`).
|
||||
funny INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_printings_oid ON printings(oracle_id);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user