-- §7 Storage. Fully relational, no JSON blobs on the write path: the database -- can only represent what the schema models, so there is physically nowhere for -- an unexpected field or a smuggled string to live (§5a Threat 1). -- -- Portable SQL — runs unchanged on Postgres. Avoid SQLite-specific forms -- (`INSERT OR REPLACE`); use `INSERT ... ON CONFLICT` (§8 deployment notes). CREATE TABLE IF NOT EXISTS contributors ( id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, revoked_at TEXT, accepted_count INTEGER NOT NULL DEFAULT 0, rejected_count INTEGER NOT NULL DEFAULT 0, flagged_count INTEGER NOT NULL DEFAULT 0 ); -- Server-side, TMDB-derived. `name` never comes from an upload (§5a). CREATE TABLE IF NOT EXISTS people ( tmdb_person_id INTEGER PRIMARY KEY, name TEXT NOT NULL, adult INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS titles ( id TEXT PRIMARY KEY, kind TEXT NOT NULL, -- movie | series tmdb_id TEXT, imdb_id TEXT, name TEXT, year INTEGER, adult INTEGER NOT NULL DEFAULT 0, certification TEXT, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS manifests ( id TEXT PRIMARY KEY, title_id TEXT NOT NULL REFERENCES titles(id), season INTEGER, episode INTEGER, runtime_sec REAL NOT NULL, -- No video_hash: withdrawn (§3). It fingerprinted an individual file rather -- than a cut, which is the one thing this schema deliberately cannot record. audio_signature BLOB, -- §3, ~1290 bytes audio_sig_coarse BLOB, -- candidate-generation index key sample_fps REAL, extinction_sec REAL, -- successor to the withdrawn anneal_sec gallery_scope TEXT, -- limited | global; ranking signal (§2, §7) pipeline_version TEXT, contributor_id TEXT REFERENCES contributors(id), status TEXT NOT NULL, -- pending | listed | flagged | rejected reject_reason TEXT, cast_match_ratio REAL, content_id TEXT UNIQUE, -- §9a, sha256 over canonical form origin TEXT, -- server_id of first acceptance ingested_from TEXT, -- peer id, NULL if uploaded directly created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS manifest_actors ( manifest_id TEXT NOT NULL REFERENCES manifests(id) ON DELETE CASCADE, tmdb_person_id INTEGER NOT NULL, PRIMARY KEY (manifest_id, tmdb_person_id) ); -- Integer centiseconds, not floats — the same quantisation used for -- `content_id`, so stored values and hashed values cannot diverge (§7, §9a). CREATE TABLE IF NOT EXISTS scenes ( manifest_id TEXT NOT NULL REFERENCES manifests(id) ON DELETE CASCADE, tmdb_person_id INTEGER NOT NULL, start_cs INTEGER NOT NULL, end_cs INTEGER NOT NULL, -- Per-window provenance (SR-003, extraction AR-017). Deliberately NOT part -- of `content_id`: belief is a producer-side estimate that may differ -- between pipeline versions for identical timings, so hashing it would give -- two servers different ids for the same content (§9a). belief REAL, route TEXT -- live | deferred | pooled ); CREATE TABLE IF NOT EXISTS reports ( id TEXT PRIMARY KEY, manifest_id TEXT NOT NULL REFERENCES manifests(id) ON DELETE CASCADE, reason TEXT NOT NULL, note TEXT, created_at TEXT NOT NULL, source_ip_hash TEXT ); -- The sole JSON column, and it holds TMDB's responses, not users' (§7). CREATE TABLE IF NOT EXISTS tmdb_cache ( tmdb_id TEXT NOT NULL, kind TEXT NOT NULL, credits TEXT NOT NULL, fetched_at TEXT NOT NULL, PRIMARY KEY (tmdb_id, kind) ); -- Background queue as a table rather than an external broker, so pending work -- survives a restart (§7, §8). CREATE TABLE IF NOT EXISTS jobs ( id TEXT PRIMARY KEY, kind TEXT NOT NULL, -- cast_check | federation_pull payload TEXT NOT NULL, run_after TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, last_error TEXT, leased_at TEXT ); -- §9a federation. Peering is trust-by-configuration: a row exists only because -- an operator typed a URL. Nothing a remote server says can create one. CREATE TABLE IF NOT EXISTS peers ( id TEXT PRIMARY KEY, url TEXT NOT NULL UNIQUE, name TEXT, enabled INTEGER NOT NULL DEFAULT 0, pull_interval_sec INTEGER NOT NULL DEFAULT 3600, -- Auto-delist on a peer's legal retraction. Off by default: a retraction -- that delists automatically is a remote delete primitive over your -- catalogue, so it is opt-in per peer between operators who know each other. trust_abuse_retractions INTEGER NOT NULL DEFAULT 0, -- Publishing a peering is opt-in on BOTH sides (§9a): peering with someone -- must not advertise their existence against their wishes. advertise INTEGER NOT NULL DEFAULT 0, max_ingest_per_hour INTEGER NOT NULL DEFAULT 500, peered_since TEXT, last_cursor TEXT, last_pull_at TEXT, last_error TEXT ); -- The change feed. Append-only, so a peer can resume from an opaque cursor and -- the feed is idempotent. A separate table rather than deriving the feed from -- `manifests` because a *retraction* is an event with no surviving row. CREATE TABLE IF NOT EXISTS federation_log ( -- A genuinely monotonic sequence, NOT a ULID. -- -- ULIDs are only monotonic *between* milliseconds: two generated in the same -- millisecond carry independent random components, so they can sort in the -- opposite order to which they were written. A peer resuming from `seq > -- cursor` would then silently skip an entry — replication losing manifests -- with no error anywhere, which is the worst shape a bug can take here. -- -- AUTOINCREMENT (rather than plain rowid) additionally guarantees the value -- never decreases even after deletions. Portability note (§8): Postgres -- spells this `BIGSERIAL PRIMARY KEY`; it is the one place a monotonic -- sequence has no fully portable form, and it is worth the exception. seq INTEGER PRIMARY KEY AUTOINCREMENT, content_id TEXT NOT NULL, op TEXT NOT NULL, -- add | retract reason TEXT, -- retract only; 'abuse' is the one that may auto-delist origin TEXT NOT NULL, -- server_id that first accepted it created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_federation_log_content ON federation_log(content_id); CREATE INDEX IF NOT EXISTS idx_titles_tmdb ON titles(tmdb_id); CREATE INDEX IF NOT EXISTS idx_titles_imdb ON titles(imdb_id); CREATE INDEX IF NOT EXISTS idx_manifests_title_runtime ON manifests(title_id, runtime_sec); CREATE INDEX IF NOT EXISTS idx_manifests_episode ON manifests(title_id, season, episode); CREATE INDEX IF NOT EXISTS idx_scenes_manifest_person ON scenes(manifest_id, tmdb_person_id); -- All read queries filter `status IN ('listed','flagged')`, so a partial index -- on that predicate keeps the hot path small (§7). CREATE INDEX IF NOT EXISTS idx_manifests_served ON manifests(title_id, season, episode) WHERE status IN ('listed', 'flagged'); CREATE INDEX IF NOT EXISTS idx_jobs_ready ON jobs(run_after);