using Microsoft.Data.Sqlite; namespace MoShiro.Backend.Storage.Sqlite; /// 表结构。版本记录在 PRAGMA user_version;以后的迁移是按版本号追加的内置 SQL。 internal static class Schema { public const int CurrentVersion = 7; public static void Migrate(SqliteConnection connection) { int version = connection.Scalar("PRAGMA user_version"); if (version > CurrentVersion) { throw new InvalidOperationException($"database schema version {version} is newer than this build supports ({CurrentVersion})"); } if (version < 1) { using SqliteTransaction transaction = connection.BeginTransaction(); connection.Exec(V1); connection.Exec("PRAGMA user_version = 1"); transaction.Commit(); } if (version < 2) { // 测试属于一条线路(Probe,Agent):旧测试没有 Probe,留空 using SqliteTransaction transaction = connection.BeginTransaction(); connection.Exec("ALTER TABLE tests ADD COLUMN probe_id TEXT NOT NULL DEFAULT ''"); connection.Exec("CREATE INDEX ix_tests_line ON tests (agent_id, probe_id, requested_at)"); connection.Exec("PRAGMA user_version = 2"); transaction.Commit(); } if (version < 3) { // 主机资源占用的历史(每个 Agent 每分钟一行) using SqliteTransaction transaction = connection.BeginTransaction(); connection.Exec(""" CREATE TABLE host_samples ( agent_id TEXT NOT NULL, at INTEGER NOT NULL, cpu REAL, mem_used INTEGER, mem_total INTEGER, disk_used INTEGER, disk_total INTEGER, net_rx_bps INTEGER, net_tx_bps INTEGER, uptime INTEGER, PRIMARY KEY (agent_id, at) ) WITHOUT ROWID; """); connection.Exec("PRAGMA user_version = 3"); transaction.Commit(); } if (version < 4) { // 节点的展示信息:系统、架构、国家、运营商;geo_host 是查询用的地址,只在 Backend 内部用 using SqliteTransaction transaction = connection.BeginTransaction(); connection.Exec(""" CREATE TABLE node_meta ( node_id TEXT PRIMARY KEY, kind TEXT NOT NULL, os TEXT, os_name TEXT, arch TEXT, country TEXT, isp_key TEXT, isp_name TEXT, geo_host TEXT, geo_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 ) WITHOUT ROWID """); connection.Exec("PRAGMA user_version = 4"); transaction.Commit(); } if (version < 5) { // 节点自报的构建信息:版本、tag、提交、构建时间 using SqliteTransaction transaction = connection.BeginTransaction(); connection.Exec("ALTER TABLE node_meta ADD COLUMN version TEXT"); connection.Exec("ALTER TABLE node_meta ADD COLUMN tag TEXT"); connection.Exec("ALTER TABLE node_meta ADD COLUMN commit_id TEXT"); connection.Exec("ALTER TABLE node_meta ADD COLUMN built_at TEXT"); connection.Exec("PRAGMA user_version = 5"); transaction.Commit(); } if (version < 6) { using SqliteTransaction transaction = connection.BeginTransaction(); connection.Exec("ALTER TABLE agents ADD COLUMN ipv6_host TEXT NOT NULL DEFAULT ''"); connection.Exec("ALTER TABLE agents ADD COLUMN ipv6_supported INTEGER NOT NULL DEFAULT 0"); connection.Exec("ALTER TABLE tests ADD COLUMN ip_version TEXT NOT NULL DEFAULT 'ipv4'"); connection.Exec("CREATE INDEX ix_tests_path ON tests (agent_id, probe_id, ip_version, requested_at)"); connection.Exec("PRAGMA user_version = 6"); transaction.Commit(); } if (version < 7) { using SqliteTransaction transaction = connection.BeginTransaction(); connection.Exec("ALTER TABLE agents ADD COLUMN ipv6_diagnostics TEXT"); connection.Exec("ALTER TABLE probes ADD COLUMN ipv6_diagnostics TEXT"); connection.Exec("PRAGMA user_version = 7"); transaction.Commit(); } } /// /// 新增 Kind 或指标都不需要改表结构:指标是 metric_*.metrics 里的一个键,Kind 只是 raw_chunks.kind 的一个取值。 /// private const string V1 = """ CREATE TABLE agents ( id TEXT PRIMARY KEY, name TEXT NOT NULL, status TEXT NOT NULL, pubkey BLOB NOT NULL, tls_spki BLOB NOT NULL, public_host TEXT NOT NULL DEFAULT '', probe_tcp_port INTEGER NOT NULL DEFAULT 7443, probe_udp_ports TEXT NOT NULL DEFAULT '[7443]', labels TEXT NOT NULL DEFAULT '[]', public_visible INTEGER NOT NULL DEFAULT 1, kinds TEXT NOT NULL DEFAULT '[]', agent_version TEXT, local_limits TEXT, task_version INTEGER NOT NULL DEFAULT 0, applied_task_version INTEGER NOT NULL DEFAULT 0, rejected TEXT, task_hash TEXT, meta TEXT, created_at INTEGER NOT NULL, last_seen_at INTEGER, row_version INTEGER NOT NULL DEFAULT 0 ); -- 同名节点只允许一个已建立的(pending 的可以重名,等待管理员接管或改名) CREATE UNIQUE INDEX ux_agents_name ON agents (name) WHERE status <> 'pending'; CREATE TABLE join_tokens ( id TEXT PRIMARY KEY, token_sha256 BLOB NOT NULL UNIQUE, note TEXT, labels TEXT NOT NULL DEFAULT '[]', auto_approve INTEGER NOT NULL, max_uses INTEGER NOT NULL, uses INTEGER NOT NULL DEFAULT 0, expires_at INTEGER NOT NULL, revoked_at INTEGER, created_at INTEGER NOT NULL ); CREATE TABLE probes ( id TEXT PRIMARY KEY, name TEXT NOT NULL, pubkey BLOB NOT NULL, created_at INTEGER NOT NULL, revoked_at INTEGER ); CREATE TABLE users ( id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash BLOB NOT NULL, password_salt BLOB NOT NULL, iterations INTEGER NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE sessions ( id_sha256 BLOB PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, csrf_token BLOB NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL ); CREATE TABLE api_tokens ( id TEXT PRIMARY KEY, name TEXT NOT NULL, scope TEXT NOT NULL, token_sha256 BLOB NOT NULL UNIQUE, created_at INTEGER NOT NULL, last_used_at INTEGER, revoked_at INTEGER ); CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL); CREATE TABLE tasks ( id TEXT PRIMARY KEY, scope TEXT NOT NULL, agent_id TEXT REFERENCES agents(id) ON DELETE CASCADE, base_task_id TEXT REFERENCES tasks(id) ON DELETE CASCADE, kind TEXT NOT NULL, target TEXT, params TEXT NOT NULL DEFAULT '{}', schedule TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, updated_at INTEGER NOT NULL, row_version INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE tests ( id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, plan TEXT NOT NULL, params TEXT NOT NULL, trigger TEXT NOT NULL, status TEXT NOT NULL, error_code TEXT, phase INTEGER, phase_count INTEGER NOT NULL DEFAULT 0, requested_at INTEGER NOT NULL, started_at INTEGER, finished_at INTEGER ); CREATE INDEX ix_tests_agent ON tests (agent_id, requested_at); CREATE TABLE test_results ( test_id TEXT PRIMARY KEY, metrics BLOB NOT NULL, series BLOB NOT NULL ) WITHOUT ROWID; CREATE TABLE raw_segments ( id INTEGER PRIMARY KEY, day TEXT NOT NULL, path TEXT NOT NULL UNIQUE, bytes INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE raw_chunks ( chunk_id TEXT PRIMARY KEY, segment_id INTEGER NOT NULL REFERENCES raw_segments(id), offset INTEGER NOT NULL, length INTEGER NOT NULL, agent_id TEXT NOT NULL, series TEXT NOT NULL, kind TEXT NOT NULL, task_id TEXT, test_id TEXT, phase INTEGER, start_ts INTEGER NOT NULL, duration_ms INTEGER NOT NULL, partial INTEGER NOT NULL ) WITHOUT ROWID; CREATE INDEX ix_raw_series ON raw_chunks (agent_id, series, start_ts); CREATE INDEX ix_raw_test ON raw_chunks (test_id) WHERE test_id IS NOT NULL; CREATE INDEX ix_raw_segment ON raw_chunks (segment_id); CREATE TABLE metric_10s (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID; CREATE TABLE metric_1m (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID; CREATE TABLE metric_5m (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID; CREATE TABLE metric_1h (agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, partial INTEGER NOT NULL, metrics BLOB NOT NULL, PRIMARY KEY (agent_id, series, window_start)) WITHOUT ROWID; CREATE INDEX ix_metric_10s_time ON metric_10s (window_start); CREATE INDEX ix_metric_1m_time ON metric_1m (window_start); CREATE INDEX ix_metric_5m_time ON metric_5m (window_start); CREATE TABLE metric_versions ( metric_id TEXT PRIMARY KEY, fingerprint TEXT NOT NULL, recompute_from INTEGER, recompute_cursor INTEGER ); CREATE TABLE analysis_queue ( agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL, not_before INTEGER NOT NULL, reason TEXT NOT NULL, seq INTEGER NOT NULL DEFAULT 1, PRIMARY KEY (agent_id, series, window_start) ) WITHOUT ROWID; CREATE INDEX ix_queue_due ON analysis_queue (not_before); CREATE TABLE rollup_dirty ( level INTEGER NOT NULL, agent_id TEXT NOT NULL, series TEXT NOT NULL, bucket_start INTEGER NOT NULL, PRIMARY KEY (level, agent_id, series, bucket_start) ) WITHOUT ROWID; CREATE TABLE events ( id INTEGER PRIMARY KEY, agent_id TEXT, kind TEXT NOT NULL, at INTEGER NOT NULL, detail TEXT ); CREATE INDEX ix_events_time ON events (at); CREATE INDEX ix_events_agent ON events (agent_id, at); """; }