-- Migration: Initial schema with TIMESTAMPTZ -- Extracted from inline SCHEMA_SQL in db.rs, with TEXT timestamps converted to TIMESTAMPTZ. CREATE TABLE IF NOT EXISTS saas_schema_version ( version INTEGER PRIMARY KEY ); CREATE TABLE IF NOT EXISTS accounts ( id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, display_name TEXT NOT NULL DEFAULT '', avatar_url TEXT, role TEXT NOT NULL DEFAULT 'user', status TEXT NOT NULL DEFAULT 'active', totp_secret TEXT, totp_enabled BOOLEAN NOT NULL DEFAULT FALSE, last_login_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_accounts_email ON accounts(email); CREATE INDEX IF NOT EXISTS idx_accounts_role ON accounts(role); CREATE TABLE IF NOT EXISTS api_tokens ( id TEXT PRIMARY KEY, account_id TEXT NOT NULL, name TEXT NOT NULL, token_hash TEXT NOT NULL, token_prefix TEXT NOT NULL, permissions TEXT NOT NULL DEFAULT '[]', last_used_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), revoked_at TIMESTAMPTZ, FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_api_tokens_account ON api_tokens(account_id); CREATE INDEX IF NOT EXISTS idx_api_tokens_hash ON api_tokens(token_hash); CREATE TABLE IF NOT EXISTS roles ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, permissions TEXT NOT NULL DEFAULT '[]', is_system BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS permission_templates ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, permissions TEXT NOT NULL DEFAULT '[]', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS operation_logs ( id BIGSERIAL PRIMARY KEY, account_id TEXT, action TEXT NOT NULL, target_type TEXT, target_id TEXT, details TEXT, ip_address TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_op_logs_account ON operation_logs(account_id); CREATE INDEX IF NOT EXISTS idx_op_logs_action ON operation_logs(action); CREATE INDEX IF NOT EXISTS idx_op_logs_time ON operation_logs(created_at); CREATE TABLE IF NOT EXISTS providers ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, api_key TEXT, base_url TEXT NOT NULL, api_protocol TEXT NOT NULL DEFAULT 'openai', enabled BOOLEAN NOT NULL DEFAULT TRUE, rate_limit_rpm INTEGER, rate_limit_tpm INTEGER, config_json TEXT DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS models ( id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, model_id TEXT NOT NULL, alias TEXT NOT NULL, context_window BIGINT NOT NULL DEFAULT 8192, max_output_tokens BIGINT NOT NULL DEFAULT 4096, supports_streaming BOOLEAN NOT NULL DEFAULT TRUE, supports_vision BOOLEAN NOT NULL DEFAULT FALSE, enabled BOOLEAN NOT NULL DEFAULT TRUE, pricing_input DOUBLE PRECISION DEFAULT 0, pricing_output DOUBLE PRECISION DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(provider_id, model_id), FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_models_provider ON models(provider_id); CREATE TABLE IF NOT EXISTS account_api_keys ( id TEXT PRIMARY KEY, account_id TEXT NOT NULL, provider_id TEXT NOT NULL, key_value TEXT NOT NULL, key_label TEXT, permissions TEXT NOT NULL DEFAULT '[]', enabled BOOLEAN NOT NULL DEFAULT TRUE, last_used_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), revoked_at TIMESTAMPTZ, FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE, FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_account_api_keys_account ON account_api_keys(account_id); CREATE TABLE IF NOT EXISTS usage_records ( id BIGSERIAL PRIMARY KEY, account_id TEXT NOT NULL, provider_id TEXT NOT NULL, model_id TEXT NOT NULL, input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0, latency_ms INTEGER, status TEXT NOT NULL DEFAULT 'success', error_message TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_usage_account ON usage_records(account_id); CREATE INDEX IF NOT EXISTS idx_usage_time ON usage_records(created_at); -- idx_usage_day: Skipping because ::date on TIMESTAMPTZ is not IMMUTABLE -- CREATE INDEX IF NOT EXISTS idx_usage_day ON usage_records((created_at::date)); CREATE TABLE IF NOT EXISTS relay_tasks ( id TEXT PRIMARY KEY, account_id TEXT NOT NULL, provider_id TEXT NOT NULL, model_id TEXT NOT NULL, request_hash TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'queued', priority INTEGER NOT NULL DEFAULT 0, attempt_count INTEGER NOT NULL DEFAULT 0, max_attempts INTEGER NOT NULL DEFAULT 3, request_body TEXT NOT NULL, response_body TEXT, input_tokens INTEGER DEFAULT 0, output_tokens INTEGER DEFAULT 0, error_message TEXT, queued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_relay_status ON relay_tasks(status); CREATE INDEX IF NOT EXISTS idx_relay_account ON relay_tasks(account_id); CREATE INDEX IF NOT EXISTS idx_relay_provider ON relay_tasks(provider_id); CREATE INDEX IF NOT EXISTS idx_relay_time ON relay_tasks(created_at); -- idx_relay_day: Skipping because ::date on TIMESTAMPTZ is not IMMUTABLE -- CREATE INDEX IF NOT EXISTS idx_relay_day ON relay_tasks((created_at::date)); CREATE TABLE IF NOT EXISTS config_items ( id TEXT PRIMARY KEY, category TEXT NOT NULL, key_path TEXT NOT NULL, value_type TEXT NOT NULL, current_value TEXT, default_value TEXT, source TEXT NOT NULL DEFAULT 'local', description TEXT, requires_restart BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(category, key_path) ); CREATE INDEX IF NOT EXISTS idx_config_category ON config_items(category); CREATE TABLE IF NOT EXISTS config_sync_log ( id BIGSERIAL PRIMARY KEY, account_id TEXT NOT NULL, client_fingerprint TEXT NOT NULL, action TEXT NOT NULL, config_keys TEXT NOT NULL, client_values TEXT, saas_values TEXT, resolution TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_sync_account ON config_sync_log(account_id); CREATE TABLE IF NOT EXISTS devices ( id TEXT PRIMARY KEY, account_id TEXT NOT NULL, device_id TEXT NOT NULL, device_name TEXT, platform TEXT, app_version TEXT, last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_devices_account ON devices(account_id); CREATE INDEX IF NOT EXISTS idx_devices_device_id ON devices(device_id); CREATE UNIQUE INDEX IF NOT EXISTS idx_devices_unique ON devices(account_id, device_id); -- Prompt template master table CREATE TABLE IF NOT EXISTS prompt_templates ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, category TEXT NOT NULL, description TEXT, source TEXT NOT NULL DEFAULT 'builtin', current_version INTEGER NOT NULL DEFAULT 1, status TEXT NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_prompt_status ON prompt_templates(status); -- Prompt versions table (immutable) CREATE TABLE IF NOT EXISTS prompt_versions ( id TEXT PRIMARY KEY, template_id TEXT NOT NULL, version INTEGER NOT NULL, system_prompt TEXT, user_prompt_template TEXT, variables TEXT NOT NULL DEFAULT '[]', changelog TEXT, min_app_version TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(template_id, version) ); CREATE INDEX IF NOT EXISTS idx_prompt_ver_template ON prompt_versions(template_id); -- Client prompt sync status CREATE TABLE IF NOT EXISTS prompt_sync_status ( device_id TEXT NOT NULL, template_id TEXT NOT NULL, synced_version INTEGER NOT NULL, synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY(device_id, template_id) ); -- Provider Key Pool table CREATE TABLE IF NOT EXISTS provider_keys ( id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, key_label TEXT NOT NULL, key_value TEXT NOT NULL, priority INTEGER NOT NULL DEFAULT 0, max_rpm INTEGER, max_tpm INTEGER, quota_reset_interval TEXT, is_active BOOLEAN NOT NULL DEFAULT TRUE, last_429_at TIMESTAMPTZ, cooldown_until TIMESTAMPTZ, total_requests BIGINT NOT NULL DEFAULT 0, total_tokens BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_pkeys_provider ON provider_keys(provider_id); CREATE INDEX IF NOT EXISTS idx_pkeys_active ON provider_keys(provider_id, is_active); -- Key usage sliding window CREATE TABLE IF NOT EXISTS key_usage_window ( key_id TEXT NOT NULL, window_minute TEXT NOT NULL, request_count INTEGER NOT NULL DEFAULT 0, token_count BIGINT NOT NULL DEFAULT 0, PRIMARY KEY(key_id, window_minute) ); -- Agent config template table CREATE TABLE IF NOT EXISTS agent_templates ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, category TEXT NOT NULL DEFAULT 'general', source TEXT NOT NULL DEFAULT 'builtin', model TEXT, system_prompt TEXT, tools TEXT NOT NULL DEFAULT '[]'::text, capabilities TEXT NOT NULL DEFAULT '[]'::text, temperature DOUBLE PRECISION, max_tokens INTEGER, visibility TEXT NOT NULL DEFAULT 'public', status TEXT NOT NULL DEFAULT 'active', current_version INTEGER NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_agent_tmpl_status ON agent_templates(status); CREATE INDEX IF NOT EXISTS idx_agent_tmpl_visibility ON agent_templates(visibility); -- Desktop telemetry report table (token usage statistics, no content) CREATE TABLE IF NOT EXISTS telemetry_reports ( id TEXT PRIMARY KEY, account_id TEXT NOT NULL, device_id TEXT NOT NULL, app_version TEXT, model_id TEXT NOT NULL, input_tokens BIGINT NOT NULL DEFAULT 0, output_tokens BIGINT NOT NULL DEFAULT 0, latency_ms INTEGER, success BOOLEAN NOT NULL DEFAULT TRUE, error_type TEXT, connection_mode TEXT NOT NULL DEFAULT 'tauri', reported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_telemetry_account ON telemetry_reports(account_id); CREATE INDEX IF NOT EXISTS idx_telemetry_time ON telemetry_reports(reported_at); CREATE INDEX IF NOT EXISTS idx_telemetry_model ON telemetry_reports(model_id); -- idx_telemetry_day: Skipping because ::date on TIMESTAMPTZ is not IMMUTABLE -- CREATE INDEX IF NOT EXISTS idx_telemetry_day ON telemetry_reports((reported_at::date)); -- Refresh Token storage (single-use, JWT jti tracking) CREATE TABLE IF NOT EXISTS refresh_tokens ( id TEXT PRIMARY KEY, account_id TEXT NOT NULL, jti TEXT NOT NULL UNIQUE, token_hash TEXT NOT NULL, expires_at TIMESTAMPTZ NOT NULL, used_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_refresh_account ON refresh_tokens(account_id); CREATE INDEX IF NOT EXISTS idx_refresh_jti ON refresh_tokens(jti); CREATE INDEX IF NOT EXISTS idx_refresh_expires ON refresh_tokens(expires_at);