-- pages.elijah.run — D1 schema
--
-- Mirrors docs/plans/2026-07-12-pages-design.md §4.2 exactly. All statements
-- use `IF NOT EXISTS` so this file can be re-applied safely (e.g. re-running
-- `wrangler d1 execute` against a database that already has these objects).
--
-- Apply with:
--   wrangler d1 execute <DB_NAME> --file=migrations/schema.sql          (remote)
--   wrangler d1 execute <DB_NAME> --local --file=migrations/schema.sql  (local dev)

-- Registered users, upserted on every successful GitHub OAuth login
-- (src/db.rs::Db::upsert_user_on_login). `github_id` is the stable GitHub
-- account id (not the login, which can change) and is the natural
-- conflict key for the upsert. `can_upload` and `is_admin` are booleans
-- stored as INTEGER 0/1 (SQLite/D1 has no native boolean type) — see the
-- `UserRow` -> `User` conversion in src/db.rs for how these are turned
-- into real `bool`s on read. Both default to 0: new OAuth users can log
-- in but cannot publish until an admin grants `can_upload` (design §2.4).
-- `ADMIN_GITHUB_LOGIN` logins are auto-promoted (both flags forced to 1)
-- on every login by the upsert, independent of this default. `trusted` is a
-- separate, admin-only-granted flag (pages-shqc): it is never touched by
-- admin auto-promotion or by any login upsert — see the "Trust escape
-- hatch" addendum in docs/plans/2026-07-12-pages-design.md §2.1. A user's
-- `trusted` flag contributes to a page's *effective* trust (alongside the
-- page's own `trusted` flag below) via `p.trusted OR u.trusted`.
CREATE TABLE IF NOT EXISTS users (
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  github_id  INTEGER NOT NULL UNIQUE,
  login      TEXT    NOT NULL,
  avatar_url TEXT,
  can_upload INTEGER NOT NULL DEFAULT 0,
  is_admin   INTEGER NOT NULL DEFAULT 0,
  trusted    INTEGER NOT NULL DEFAULT 0,
  created_at TEXT    NOT NULL DEFAULT (datetime('now'))
);

-- Server-side session records. The cookie sent to the browser holds the raw
-- session token; only its SHA-256 hex hash is ever persisted here, so a D1
-- read leak does not yield a usable session token (design §2.4).
-- `ON DELETE CASCADE` means deleting a user row (none currently does this in
-- v0, but future admin tooling might) also removes their sessions.
CREATE TABLE IF NOT EXISTS sessions (
  token_hash TEXT PRIMARY KEY,          -- SHA-256 hex of the cookie token
  user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  expires_at TEXT    NOT NULL,
  created_at TEXT    NOT NULL DEFAULT (datetime('now'))
);

-- One row per published page. `slug` is the primary key, so a duplicate
-- upload name surfaces as a UNIQUE/PK constraint violation, which src/db.rs
-- maps to a typed `DbError::NameTaken` (HTTP 409 at the routes layer,
-- design §2.3). `file_count`/`total_bytes` are denormalized summaries of the
-- R2 objects stored under the `<slug>/` prefix (design §4.3), populated at
-- upload time so the page list and per-user quota check (design §2.5) don't
-- need to touch R2. `trusted` is an admin-only-granted flag (pages-shqc,
-- design §2.1 addendum): a page's *effective* trust is
-- `pages.trusted OR <owner's> users.trusted`, computed at serve time —
-- see src/db.rs's `PageRow::effective_trust`.
CREATE TABLE IF NOT EXISTS pages (
  slug        TEXT PRIMARY KEY,
  owner_id    INTEGER NOT NULL REFERENCES users(id),
  file_count  INTEGER NOT NULL,
  total_bytes INTEGER NOT NULL,
  trusted     INTEGER NOT NULL DEFAULT 0,
  created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
);

-- Per-user page quota (design §2.5) is enforced by counting
-- `pages` rows WHERE owner_id = ? before an upload; without this index that
-- scan is a full table scan on every upload.
CREATE INDEX IF NOT EXISTS idx_pages_owner_id ON pages(owner_id);

-- get_user_by_session filters `WHERE expires_at > datetime('now')` on every
-- authenticated request; this index keeps that lookup (and any future
-- expired-session cleanup job) from scanning the whole table.
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);

-- Not required by any current query pattern (session lookups go through
-- `token_hash`, the primary key) but cheap to maintain and speeds up the
-- `ON DELETE CASCADE` scan when a user row is deleted, plus any future
-- "list my sessions" / "log out everywhere" feature keyed on user_id.
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
