go-bananas/internal/store/migrations/0001_init.sql

144 lines
6 KiB
MySQL
Raw Permalink Normal View History

2026-10-07 15:11:35 +02:00
-- go-bananas schema.
--
-- Design notes
-- ------------
-- * Timestamps are TEXT in RFC 3339 (see domain.TimeFormat). SQLite has no
-- native date type, and storing a sortable string removes every date-parsing
-- ambiguity from ORDER BY and range filters.
-- * JSON columns are TEXT holding a JSON document. The token document is read
-- and written as a whole value, never queried into, so a JSON column is the
-- correct shape: it avoids a join table whose only purpose would be to
-- reassemble a tree the application always wants whole.
-- * Every table a consumer might read carries an explicit covering index.
PRAGMA foreign_keys = ON;
-- ---------------------------------------------------------------- themes ----
CREATE TABLE IF NOT EXISTS themes (
id TEXT PRIMARY KEY,
slug TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft', 'published', 'archived')),
default_appearance TEXT NOT NULL DEFAULT 'light'
CHECK (default_appearance IN ('light', 'dark')),
tokens_json TEXT NOT NULL,
meta_json TEXT NOT NULL DEFAULT '{}',
is_system INTEGER NOT NULL DEFAULT 0 CHECK (is_system IN (0, 1)),
version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
created_by TEXT NOT NULL DEFAULT '',
updated_by TEXT NOT NULL DEFAULT ''
);
-- The runtime API resolves by slug on every page load, so this index is the
-- single most important one in the schema.
CREATE UNIQUE INDEX IF NOT EXISTS idx_themes_slug ON themes (slug);
-- Listing is always ordered by recency within a status filter.
CREATE INDEX IF NOT EXISTS idx_themes_status_updated
ON themes (status, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_themes_updated ON themes (updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_themes_created ON themes (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_themes_name ON themes (name COLLATE NOCASE);
-- Free-text search over slug, name and description. SQLite's LIKE with a
-- leading wildcard cannot use an index, so this stays a scan; the table is
-- small by construction (themes are hand-authored, not machine-generated) and
-- the scan is served from the page cache.
CREATE INDEX IF NOT EXISTS idx_themes_search
ON themes (name COLLATE NOCASE, slug, description);
-- ---------------------------------------------------------- theme_versions --
CREATE TABLE IF NOT EXISTS theme_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
theme_id TEXT NOT NULL REFERENCES themes (id) ON DELETE CASCADE,
version INTEGER NOT NULL,
slug TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL,
default_appearance TEXT NOT NULL,
tokens_json TEXT NOT NULL,
message TEXT NOT NULL DEFAULT '',
author TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
UNIQUE (theme_id, version)
);
-- Rollback resolves a theme's history newest-first.
CREATE INDEX IF NOT EXISTS idx_versions_theme
ON theme_versions (theme_id, version DESC);
-- ---------------------------------------------------------------- api_keys --
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
-- prefix is the non-secret leading segment shown in the UI so a key can be
-- identified after its secret has been forgotten.
prefix TEXT NOT NULL,
-- secret_hash is a SHA-256 of the full key. The plaintext is never stored,
-- so a database disclosure does not yield usable credentials.
secret_hash TEXT NOT NULL,
scopes TEXT NOT NULL,
identity TEXT NOT NULL DEFAULT '',
created_by TEXT NOT NULL,
created_at TEXT NOT NULL,
last_used_at TEXT,
expires_at TEXT,
revoked_at TEXT
);
-- Lookup by secret hash happens on every machine request.
CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys (secret_hash);
-- The UI lists live keys newest-first and hides revoked ones by default.
CREATE INDEX IF NOT EXISTS idx_api_keys_live ON api_keys (revoked_at, created_at DESC);
-- ----------------------------------------------------------------- users ----
CREATE TABLE IF NOT EXISTS users (
subject TEXT PRIMARY KEY,
email TEXT NOT NULL DEFAULT '',
display_name TEXT NOT NULL DEFAULT '',
roles TEXT NOT NULL DEFAULT '[]',
method TEXT NOT NULL DEFAULT '',
first_seen TEXT NOT NULL,
last_login TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
-- ------------------------------------------------------------- audit_log ----
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
at TEXT NOT NULL,
actor TEXT NOT NULL DEFAULT '',
actor_email TEXT NOT NULL DEFAULT '',
action TEXT NOT NULL,
entity_type TEXT NOT NULL DEFAULT '',
entity_id TEXT NOT NULL DEFAULT '',
summary TEXT NOT NULL DEFAULT '',
detail_json TEXT NOT NULL DEFAULT '{}',
remote_ip TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_audit_at ON audit_log (at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_log (actor, at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log (entity_type, entity_id, at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log (action, at DESC);
-- -------------------------------------------------------------- settings ----
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL,
updated_by TEXT NOT NULL DEFAULT ''
);