-- UOMysticmoon database schema (MariaDB) -- Run automatically by the MariaDB container (docker-entrypoint-initdb.d) on a -- fresh volume, and idempotently by ensureSchema() on every server boot. CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) NOT NULL UNIQUE, password_hash VARCHAR(72) NOT NULL, role ENUM('admin','editor') NOT NULL DEFAULT 'admin', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, last_login_at DATETIME NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS posts ( id INT AUTO_INCREMENT PRIMARY KEY, category ENUM('news','five_on_friday','newsletter','screenshot') NOT NULL, title VARCHAR(200) NOT NULL, slug VARCHAR(220) NULL, excerpt VARCHAR(400) NULL, body MEDIUMTEXT NULL, image_url VARCHAR(500) NULL, published TINYINT(1) NOT NULL DEFAULT 0, author_id INT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, published_at DATETIME NULL, CONSTRAINT fk_posts_author FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL, INDEX idx_posts_feed (category, published, published_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Wiki categories / sections. Defined before wiki_pages so the FK resolves on a -- fresh install. Pages reference a category (nullable = "Uncategorized"). CREATE TABLE IF NOT EXISTS wiki_categories ( id INT AUTO_INCREMENT PRIMARY KEY, slug VARCHAR(120) NOT NULL UNIQUE, title VARCHAR(200) NOT NULL, description VARCHAR(400) NULL, sort_order INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS wiki_pages ( id INT AUTO_INCREMENT PRIMARY KEY, slug VARCHAR(120) NOT NULL UNIQUE, title VARCHAR(200) NOT NULL, body MEDIUMTEXT NULL, excerpt VARCHAR(400) NULL, category_id INT NULL, published TINYINT(1) NOT NULL DEFAULT 1, sort_order INT NOT NULL DEFAULT 0, updated_by INT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, published_at DATETIME NULL, CONSTRAINT fk_wiki_user FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT fk_wiki_category FOREIGN KEY (category_id) REFERENCES wiki_categories(id) ON DELETE SET NULL, FULLTEXT INDEX idx_wiki_search (title, body) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Wiki tags (many-to-many with pages). CREATE TABLE IF NOT EXISTS wiki_tags ( id INT AUTO_INCREMENT PRIMARY KEY, slug VARCHAR(120) NOT NULL UNIQUE, label VARCHAR(120) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS wiki_page_tags ( page_id INT NOT NULL, tag_id INT NOT NULL, PRIMARY KEY (page_id, tag_id), CONSTRAINT fk_wpt_page FOREIGN KEY (page_id) REFERENCES wiki_pages(id) ON DELETE CASCADE, CONSTRAINT fk_wpt_tag FOREIGN KEY (tag_id) REFERENCES wiki_tags(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Internal-link index, rebuilt on each save. target_slug may point at a page -- that does not exist yet (a "red link"). CREATE TABLE IF NOT EXISTS wiki_links ( source_page_id INT NOT NULL, target_slug VARCHAR(120) NOT NULL, CONSTRAINT fk_wiki_links_src FOREIGN KEY (source_page_id) REFERENCES wiki_pages(id) ON DELETE CASCADE, INDEX idx_wiki_links_target (target_slug) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Per-save content snapshots for history / diff / restore. CREATE TABLE IF NOT EXISTS wiki_revisions ( id INT AUTO_INCREMENT PRIMARY KEY, page_id INT NOT NULL, title VARCHAR(200) NOT NULL, body MEDIUMTEXT NULL, excerpt VARCHAR(400) NULL, category_id INT NULL, editor_id INT NULL, change_note VARCHAR(280) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_wiki_rev_page FOREIGN KEY (page_id) REFERENCES wiki_pages(id) ON DELETE CASCADE, CONSTRAINT fk_wiki_rev_editor FOREIGN KEY (editor_id) REFERENCES users(id) ON DELETE SET NULL, INDEX idx_wiki_rev_page (page_id, id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS settings ( `key` VARCHAR(64) PRIMARY KEY, value TEXT NULL, updated_by INT NULL, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_settings_user FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS activity_log ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NULL, action VARCHAR(64) NOT NULL, detail TEXT NULL, ip VARCHAR(45) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_activity_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, INDEX idx_activity_created (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Migrations for databases created before the wiki upgrade. Each statement uses -- IF NOT EXISTS so re-running on every boot is a harmless no-op. New installs get -- these columns from the CREATE TABLE above; existing installs get them here. -- (The category foreign key is only added on fresh installs; on upgraded databases -- referential integrity for category_id is enforced in application code.) ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS excerpt VARCHAR(400) NULL; ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS category_id INT NULL; ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS published TINYINT(1) NOT NULL DEFAULT 1; ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS sort_order INT NOT NULL DEFAULT 0; ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS published_at DATETIME NULL; ALTER TABLE wiki_pages ADD FULLTEXT INDEX IF NOT EXISTS idx_wiki_search (title, body);