Files
DoGaMa-serv/internal/persistence/sqlite/schema.sql
T
codex 4ed3c8ae58
CI / validate (pull_request) Successful in 26m33s
refactor(catalog): make template artwork fully local
2026-08-26 21:58:30 +02:00

295 lines
14 KiB
SQL

CREATE TABLE system_state (
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
bootstrap_completed_at TEXT
);
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
email TEXT UNIQUE COLLATE NOCASE,
password_hash TEXT NOT NULL,
global_role TEXT NOT NULL CHECK (global_role IN ('admin', 'user')),
disabled_at TEXT,
created_at TEXT NOT NULL
, language TEXT NOT NULL DEFAULT 'en' CHECK (language IN ('en', 'fr')),
avatar_path TEXT NOT NULL DEFAULT '',
avatar_content_type TEXT NOT NULL DEFAULT '' CHECK (avatar_content_type IN ('', 'image/png')));
CREATE TABLE sessions (
id_hash BLOB PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
csrf_hash BLOB NOT NULL,
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL
);
CREATE INDEX sessions_user_id_idx ON sessions(user_id);
CREATE INDEX sessions_expires_at_idx ON sessions(expires_at);
CREATE TABLE authentication_attempts (
attempt_key TEXT PRIMARY KEY,
failures INTEGER NOT NULL,
blocked_until TEXT,
updated_at TEXT NOT NULL
);
CREATE TABLE templates (
id TEXT PRIMARY KEY,
origin TEXT NOT NULL,
trust_status TEXT NOT NULL,
active_version TEXT NOT NULL,
available INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE template_repositories (
id TEXT PRIMARY KEY,
name TEXT NOT NULL CHECK (length(name) BETWEEN 1 AND 120),
url TEXT NOT NULL UNIQUE COLLATE NOCASE CHECK (length(url) BETWEEN 1 AND 2048),
created_at TEXT NOT NULL
);
CREATE INDEX template_repositories_name_idx ON template_repositories(name COLLATE NOCASE, id);
CREATE TABLE template_versions (
template_id TEXT NOT NULL REFERENCES templates(id) ON DELETE RESTRICT,
version TEXT NOT NULL,
schema_version INTEGER NOT NULL,
canonical_yaml TEXT NOT NULL,
digest TEXT NOT NULL,
game_id TEXT NOT NULL,
game_name TEXT NOT NULL,
description TEXT NOT NULL,
image TEXT NOT NULL,
asset_bundle BLOB,
module_bundle BLOB,
created_at TEXT NOT NULL,
PRIMARY KEY (template_id, version),
UNIQUE (digest)
);
CREATE TABLE instances (
id TEXT PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
template_id TEXT NOT NULL,
template_version TEXT NOT NULL,
template_digest TEXT NOT NULL,
revision INTEGER NOT NULL CHECK (revision >= 1),
lifecycle_state TEXT NOT NULL CHECK (lifecycle_state IN ('draft', 'installing', 'stopped', 'starting', 'online', 'stopping', 'backup', 'restore', 'update', 'degraded', 'error', 'unknown', 'intervention_required', 'deleting', 'deleted')),
observed_state TEXT NOT NULL DEFAULT 'unknown' CHECK (observed_state IN ('unknown', 'missing', 'stopped', 'running', 'ready', 'degraded')),
preview_json TEXT NOT NULL,
plan_digest TEXT NOT NULL,
container_id TEXT,
desired_running INTEGER NOT NULL DEFAULT 0 CHECK (desired_running IN (0, 1)),
last_error_code TEXT,
deleted_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL, custom_labels_json TEXT NOT NULL DEFAULT '{}', docker_user_mode TEXT NOT NULL DEFAULT 'dogama' CHECK (docker_user_mode IN ('dogama', 'custom', 'image')), docker_uid INTEGER CHECK (docker_uid BETWEEN 0 AND 4294967295), docker_gid INTEGER CHECK (docker_gid BETWEEN 0 AND 4294967295), image_tag_mode TEXT NOT NULL DEFAULT 'tracked' CHECK (image_tag_mode IN ('tracked', 'pinned')), image_tag TEXT NOT NULL DEFAULT 'en', container_config_pending INTEGER NOT NULL DEFAULT 0 CHECK (container_config_pending IN (0, 1)),
FOREIGN KEY (template_id, template_version) REFERENCES template_versions(template_id, version) ON DELETE RESTRICT
);
CREATE INDEX instances_template_idx ON instances(template_id, template_version);
CREATE INDEX instances_lifecycle_idx ON instances(lifecycle_state);
CREATE TABLE instance_secrets (
instance_id TEXT PRIMARY KEY REFERENCES instances(id) ON DELETE CASCADE,
encrypted_values BLOB NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE instance_memberships (
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('user', 'manager')),
created_by TEXT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (instance_id, user_id)
);
CREATE INDEX instance_memberships_user_idx ON instance_memberships(user_id, instance_id);
CREATE TABLE permission_overrides (
instance_id TEXT NOT NULL,
user_id TEXT NOT NULL,
permission TEXT NOT NULL CHECK (permission IN ('instance.view', 'instance.start', 'instance.stop', 'instance.restart', 'instance.update', 'instance.configure', 'instance.delete', 'instance.welcome.edit', 'metrics.view', 'players.view', 'players.kick', 'players.ban', 'players.unban', 'announcements.send', 'logs.view', 'mods.manage', 'backup.create', 'backup.list', 'backup.export', 'backup.restore', 'backup.delete', 'request.create')),
effect TEXT NOT NULL CHECK (effect IN ('allow', 'deny')),
created_by TEXT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (instance_id, user_id, permission),
FOREIGN KEY (instance_id, user_id) REFERENCES instance_memberships(instance_id, user_id) ON DELETE CASCADE
);
CREATE TABLE installation_requests (
id TEXT PRIMARY KEY,
requested_by TEXT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
template_id TEXT NOT NULL,
template_version TEXT NOT NULL,
suggested_name TEXT,
player_estimate INTEGER CHECK (player_estimate IS NULL OR (player_estimate >= 1 AND player_estimate <= 10000)),
desired_schedule TEXT,
mods_requested INTEGER NOT NULL DEFAULT 0 CHECK (mods_requested IN (0, 1)),
message TEXT,
status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'refused', 'cancelled')),
reviewed_by TEXT REFERENCES users(id) ON DELETE RESTRICT,
review_reason TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
reviewed_at TEXT,
FOREIGN KEY (template_id, template_version) REFERENCES template_versions(template_id, version) ON DELETE RESTRICT
);
CREATE UNIQUE INDEX installation_requests_pending_idx ON installation_requests(requested_by, template_id, template_version) WHERE status = 'pending';
CREATE INDEX installation_requests_status_idx ON installation_requests(status, created_at);
CREATE INDEX installation_requests_requester_idx ON installation_requests(requested_by, created_at DESC);
CREATE TABLE instance_operations (
id TEXT PRIMARY KEY,
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE RESTRICT,
kind TEXT NOT NULL CHECK (kind IN ('install', 'start', 'stop', 'restart', 'delete_container', 'reconcile', 'backup', 'restore', 'import')),
state TEXT NOT NULL CHECK (state IN ('running', 'succeeded', 'failed', 'intervention_required')),
phase TEXT NOT NULL,
error_code TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
completed_at TEXT
);
CREATE UNIQUE INDEX instance_operation_active_idx ON instance_operations(instance_id) WHERE state = 'running';
CREATE INDEX instance_operation_history_idx ON instance_operations(instance_id, created_at DESC);
CREATE TABLE backup_policies (
instance_id TEXT PRIMARY KEY REFERENCES instances(id) ON DELETE CASCADE,
enabled INTEGER NOT NULL DEFAULT 0 CHECK (enabled IN (0, 1)),
cron_expression TEXT,
timezone TEXT NOT NULL DEFAULT 'UTC',
retention_count INTEGER NOT NULL DEFAULT 7 CHECK (retention_count BETWEEN 1 AND 1000),
next_run_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE backups (
id TEXT PRIMARY KEY,
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE RESTRICT,
operation_id TEXT REFERENCES instance_operations(id) ON DELETE SET NULL,
origin TEXT NOT NULL CHECK (origin IN ('manual', 'scheduled', 'pre_update', 'pre_restore', 'idle_shutdown', 'imported', 'system')),
status TEXT NOT NULL CHECK (status IN ('creating', 'available', 'failed', 'deleted')),
relative_path TEXT,
size_bytes INTEGER CHECK (size_bytes IS NULL OR size_bytes >= 0),
sha256 TEXT CHECK (sha256 IS NULL OR length(sha256) = 64),
manifest_json TEXT,
error_code TEXT,
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL,
completed_at TEXT,
deleted_at TEXT
);
CREATE INDEX backups_instance_created_idx ON backups(instance_id, created_at DESC);
CREATE INDEX backups_retention_idx ON backups(instance_id, origin, status, created_at);
CREATE TABLE imports (
id TEXT PRIMARY KEY,
requested_by TEXT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
instance_id TEXT REFERENCES instances(id) ON DELETE CASCADE,
template_id TEXT NOT NULL,
template_version TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('staging', 'validated', 'attached', 'failed', 'expired')),
format TEXT NOT NULL,
relative_stage_path TEXT NOT NULL,
data_root TEXT,
detected_type TEXT,
confidence TEXT CHECK (confidence IS NULL OR confidence IN ('confirmed', 'probable', 'recognized_unknown_version', 'unrecognized')),
file_count INTEGER NOT NULL DEFAULT 0,
expanded_size_bytes INTEGER NOT NULL DEFAULT 0,
error_code TEXT,
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
completed_at TEXT,
FOREIGN KEY (template_id, template_version) REFERENCES template_versions(template_id, version) ON DELETE RESTRICT
);
CREATE INDEX imports_expiry_idx ON imports(status, expires_at);
CREATE TABLE module_versions (
module_id TEXT NOT NULL,
version TEXT NOT NULL,
game_id TEXT NOT NULL,
abi TEXT NOT NULL,
manifest_json TEXT NOT NULL,
wasm_sha256 TEXT NOT NULL CHECK (length(wasm_sha256) = 64),
wasm_size_bytes INTEGER NOT NULL CHECK (wasm_size_bytes > 0),
source TEXT NOT NULL CHECK (source IN ('bundled', 'uploaded')),
installed_at TEXT NOT NULL,
PRIMARY KEY (module_id, version)
);
CREATE TABLE instance_module_bindings (
instance_id TEXT PRIMARY KEY REFERENCES instances(id) ON DELETE CASCADE,
module_id TEXT NOT NULL,
module_version TEXT NOT NULL,
port_id TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 0 CHECK (enabled IN (0, 1)),
configuration_json TEXT NOT NULL DEFAULT '{}',
secret_references_json TEXT NOT NULL DEFAULT '{}',
last_health TEXT NOT NULL DEFAULT 'unknown' CHECK (last_health IN ('ready', 'degraded', 'offline', 'unknown')),
last_error_code TEXT,
activated_at TEXT,
updated_at TEXT NOT NULL,
FOREIGN KEY (module_id, module_version) REFERENCES module_versions(module_id, version) ON DELETE RESTRICT
);
CREATE INDEX instance_module_health_idx ON instance_module_bindings(enabled, last_health);
CREATE TABLE system_settings (
key TEXT PRIMARY KEY,
value_json TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1 CHECK (revision >= 1),
updated_at TEXT NOT NULL
);
CREATE TABLE configuration_revisions (
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE CASCADE,
revision INTEGER NOT NULL CHECK (revision >= 1),
redacted_snapshot TEXT NOT NULL,
reason TEXT NOT NULL CHECK (length(reason) BETWEEN 1 AND 100),
created_by TEXT,
created_at TEXT NOT NULL,
PRIMARY KEY (instance_id, revision)
);
CREATE INDEX configuration_revision_history_idx ON configuration_revisions(instance_id, revision DESC);
CREATE TABLE notification_channels (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('email', 'webhook', 'discord', 'gotify')),
enabled INTEGER NOT NULL DEFAULT 0 CHECK (enabled IN (0, 1)),
encrypted_config BLOB NOT NULL,
event_filter_json TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
-- Personal delivery choices are deliberately separate from encrypted global
-- channel configuration. Important failures default to enabled; routine
-- lifecycle events default to disabled.
CREATE TABLE user_notification_preferences (
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
category TEXT NOT NULL CHECK (category IN ('server_start', 'server_stop', 'server_error', 'backup', 'restore', 'update', 'administration')),
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
updated_at TEXT NOT NULL,
PRIMARY KEY (user_id, category)
);
CREATE INDEX user_notification_preferences_user_idx ON user_notification_preferences(user_id);
CREATE TABLE notification_deliveries (
id TEXT PRIMARY KEY,
channel_id TEXT NOT NULL REFERENCES notification_channels(id) ON DELETE CASCADE,
event_type TEXT NOT NULL,
payload_redacted TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'retrying', 'succeeded', 'failed')),
attempt INTEGER NOT NULL DEFAULT 0 CHECK (attempt >= 0),
next_attempt_at TEXT NOT NULL,
last_error_code TEXT NOT NULL DEFAULT 'en',
created_at TEXT NOT NULL,
completed_at TEXT
);
CREATE INDEX notification_deliveries_due_idx ON notification_deliveries(status, next_attempt_at);
CREATE TABLE audit_events (
id TEXT PRIMARY KEY,
occurred_at TEXT NOT NULL,
actor_id TEXT REFERENCES users(id) ON DELETE SET NULL,
actor_label TEXT NOT NULL,
instance_id TEXT REFERENCES instances(id) ON DELETE SET NULL,
action TEXT NOT NULL,
outcome TEXT NOT NULL CHECK (outcome IN ('allowed', 'denied', 'failed')),
summary_json TEXT NOT NULL DEFAULT '{}'
);
CREATE INDEX audit_events_time_idx ON audit_events(occurred_at DESC, id DESC);
CREATE INDEX audit_events_filters_idx ON audit_events(actor_id, instance_id, action, outcome);
CREATE INDEX audit_events_actor_time_idx ON audit_events(actor_id, occurred_at DESC, id DESC);
CREATE INDEX audit_events_instance_time_idx ON audit_events(instance_id, occurred_at DESC, id DESC);
CREATE INDEX audit_events_action_time_idx ON audit_events(action, occurred_at DESC, id DESC);
INSERT INTO system_state (singleton, bootstrap_completed_at) VALUES (1, NULL);
INSERT INTO system_settings(key, value_json, revision, updated_at)
VALUES ('audit_policy', '{"retention_days":30,"maximum_count":10000}', 1, strftime('%Y-%m-%dT%H:%M:%fZ','now'));
INSERT INTO system_settings(key, value_json, revision, updated_at)
VALUES ('game_container_uid', '1000', 1, strftime('%Y-%m-%dT%H:%M:%fZ','now'));
INSERT INTO system_settings(key, value_json, revision, updated_at)
VALUES ('game_container_gid', '1000', 1, strftime('%Y-%m-%dT%H:%M:%fZ','now'));