Skip to main content

## MIGRATION_024

# Constant MIGRATION_024

Source

    const MIGRATION_024: &str = r#"
    CREATE TABLE IF NOT EXISTS user_pins (
        user_id      TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
        pin_hash     TEXT NOT NULL,
        failed_count INTEGER NOT NULL DEFAULT 0,
        locked_until TEXT,
        updated_at   TEXT DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS user_item_visibility (
        user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        item_id TEXT NOT NULL,
        seen_at TEXT DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (user_id, item_id)
    );

    CREATE INDEX IF NOT EXISTS idx_visibility_user ON user_item_visibility(user_id);

    CREATE TABLE IF NOT EXISTS user_libraries (
        user_id    TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        library_id TEXT NOT NULL,
        seen_at    TEXT DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (user_id, library_id)
    );

    CREATE TABLE IF NOT EXISTS download_grants (
        user_id     TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        download_id INTEGER NOT NULL REFERENCES downloads(id) ON DELETE CASCADE,
        granted_at  TEXT DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (user_id, download_id)
    );

    CREATE INDEX IF NOT EXISTS idx_download_grants_download ON download_grants(download_id);

    -- Backfill: the active user has seen everything already cached on this device.
    INSERT OR IGNORE INTO user_item_visibility (user_id, item_id)
    SELECT u.id, i.id
    FROM users u CROSS JOIN items i
    WHERE u.is_active = 1
       OR (SELECT COUNT(*) FROM users WHERE is_active = 1) = 0;

    INSERT OR IGNORE INTO user_libraries (user_id, library_id)
    SELECT u.id, l.id
    FROM users u CROSS JOIN libraries l
    WHERE u.is_active = 1
       OR (SELECT COUNT(*) FROM users WHERE is_active = 1) = 0;

    -- Downloads already record who asked for them, so every existing row becomes
    -- exactly one grant held by its original requester.
    INSERT OR IGNORE INTO download_grants (user_id, download_id)
    SELECT d.user_id, d.id FROM downloads d;
    "#;

Expand description

Multi-user profiles: PIN gate, per-user cache visibility, and download grants.

Three tables, one purpose each:

- `user_pins` holds the switching gate. The PIN hash lives here rather than wrapping the access token, because a wrapped token would leave a locked profile unable to resume its own downloads or drain its own sync queue until someone typed the code. See DR-268 for why that trade was taken.
- `user_item_visibility` records what the server has actually shown to each user. It is written as a byproduct of the cache write path, never rebuilt, so it cannot disagree with what the server returned.
- `download_grants` separates the bytes from the claim on them, so one file can serve several profiles and is unlinked only when the last claim goes.

The backfill is not optional. Every existing cache row and download predates the concept of a user; without it an upgrading install’s library goes blank. It grants the active user only — other pre-existing rows re-populate from the server on next browse, which is strictly safer than handing every profile the whole cache. The `OR (SELECT COUNT(*) ...) = 0` arm covers an install whose single user somehow has `is_active = 0`.

TRACES: UR-082, UR-083 | DR-268, DR-271, DR-272