-- 0001_core.sql — Vellum core schema. -- -- Structural rule: media items are global; everything user-specific is a join. -- Nothing in this file may hold per-user state on a media row. -- -- Timestamps are ISO-8601 UTC TEXT. Booleans are INTEGER 0/1. -- `kind` columns are intentionally unrestricted TEXT: media modules declare -- their own kinds and must not require a core migration to do so. -- ── Identity ─────────────────────────────────────────────────────────── CREATE TABLE users ( id INTEGER PRIMARY KEY, username TEXT NOT NULL UNIQUE COLLATE NOCASE, display_name TEXT NOT NULL DEFAULT '', password_hash TEXT NOT NULL, is_admin INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) ); CREATE TABLE sessions ( id TEXT PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE, device_label TEXT NOT NULL DEFAULT '', ip TEXT NOT NULL DEFAULT '', user_agent TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), last_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), expires_at TEXT NOT NULL ); CREATE INDEX idx_sessions_user ON sessions(user_id); CREATE INDEX idx_sessions_expires ON sessions(expires_at); -- A library is a root path plus a media kind. Global. CREATE TABLE libraries ( id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, root_path TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) ); -- Per-user access to a library. The ONLY place read permission is expressed. CREATE TABLE library_grants ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, library_id INTEGER NOT NULL REFERENCES libraries(id) ON DELETE CASCADE, can_read INTEGER NOT NULL DEFAULT 1, can_download INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (user_id, library_id) ); -- ── Media (global) ───────────────────────────────────────────────────── CREATE TABLE media_items ( id INTEGER PRIMARY KEY, library_id INTEGER NOT NULL REFERENCES libraries(id) ON DELETE CASCADE, kind TEXT NOT NULL, path TEXT NOT NULL, parent_id INTEGER REFERENCES media_items(id) ON DELETE CASCADE, title TEXT NOT NULL DEFAULT '', sort_title TEXT NOT NULL DEFAULT '', year INTEGER, added_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), UNIQUE (library_id, path) ); CREATE INDEX idx_items_parent ON media_items(parent_id); CREATE INDEX idx_items_kind ON media_items(kind); CREATE INDEX idx_items_library ON media_items(library_id); CREATE TABLE media_files ( id INTEGER PRIMARY KEY, item_id INTEGER NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, path TEXT NOT NULL UNIQUE, size INTEGER NOT NULL DEFAULT 0, mtime TEXT NOT NULL DEFAULT '', inode INTEGER NOT NULL DEFAULT 0, container TEXT NOT NULL DEFAULT '', duration_ms INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX idx_files_item ON media_files(item_id); CREATE TABLE people ( id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE COLLATE NOCASE ); CREATE TABLE item_people ( item_id INTEGER NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE CASCADE, role TEXT NOT NULL, ord INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (item_id, person_id, role) ); -- External metadata identifiers. UNIQUE so a provider record maps to one item. CREATE TABLE provider_ids ( item_id INTEGER NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, provider TEXT NOT NULL, external_id TEXT NOT NULL, PRIMARY KEY (item_id, provider), UNIQUE (provider, external_id) ); -- Covers, thumbnails, extracted comic pages. Global, shared by all users. CREATE TABLE assets ( id INTEGER PRIMARY KEY, item_id INTEGER NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, role TEXT NOT NULL, path TEXT NOT NULL, width INTEGER NOT NULL DEFAULT 0, height INTEGER NOT NULL DEFAULT 0, bytes INTEGER NOT NULL DEFAULT 0, UNIQUE (item_id, role, path) ); -- ── User state (all joins) ───────────────────────────────────────────── -- One polymorphic progress table for every media kind. -- audio/video/music -> position and duration in milliseconds -- ebook -> position is a spine index or CFI, duration 0 -- comic/pdf -> position is the page number, duration 0 -- PK includes file_id so multi-file items (audiobook tracks) resume per track. CREATE TABLE progress ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, item_id INTEGER NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, file_id INTEGER NOT NULL REFERENCES media_files(id) ON DELETE CASCADE, kind TEXT NOT NULL, position INTEGER NOT NULL DEFAULT 0, duration INTEGER NOT NULL DEFAULT 0, percent REAL NOT NULL DEFAULT 0, finished INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), PRIMARY KEY (user_id, item_id, file_id) ); CREATE INDEX idx_progress_user_updated ON progress(user_id, updated_at DESC); CREATE TABLE collections ( id INTEGER PRIMARY KEY, owner_id INTEGER REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, visibility TEXT NOT NULL DEFAULT 'private' ); CREATE TABLE collection_items ( collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE, item_id INTEGER NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, added_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), PRIMARY KEY (collection_id, item_id) ); CREATE TABLE favorites ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, item_id INTEGER NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, added_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), PRIMARY KEY (user_id, item_id) ); -- ── Infrastructure ───────────────────────────────────────────────────── CREATE TABLE jobs ( id INTEGER PRIMARY KEY, kind TEXT NOT NULL, payload TEXT NOT NULL DEFAULT '{}', state TEXT NOT NULL DEFAULT 'queued', -- queued|running|done|failed attempts INTEGER NOT NULL DEFAULT 0, run_after TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), error TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) ); CREATE INDEX idx_jobs_claim ON jobs(state, run_after); CREATE TABLE settings ( k TEXT PRIMARY KEY, v TEXT NOT NULL DEFAULT '' );