using Microsoft.Data.Sqlite; namespace MoShiro.Backend.Storage.Sqlite; /// /// 表结构。版本记录在 PRAGMA user_version。版本 1 到 2 只改了 Kind 的名字(见 ); /// 其他版本的数据库(包括更早的构建留下的)不会被迁移,启动时报错,删除 moshiro.db 和 raw/ 之后重新开始。以后的结构变更按版本号追加内置 SQL。 /// internal static class Schema { public const int CurrentVersion = 2; public static void Migrate(SqliteConnection connection) { int version = connection.Scalar("PRAGMA user_version"); if (version == CurrentVersion) { return; } if (version == 1) { using SqliteTransaction upgrade = connection.BeginTransaction(); foreach ((string from, string to) in RenameKinds) { connection.Exec($"UPDATE tasks SET kind = '{to}' WHERE kind = '{from}'"); connection.Exec($"UPDATE raw_chunks SET kind = '{to}' WHERE kind = '{from}'"); connection.Exec($"UPDATE tests SET plan = REPLACE(plan, '\"{from}\"', '\"{to}\"')"); } connection.Exec("PRAGMA user_version = 2"); upgrade.Commit(); return; } if (version != 0) { throw new InvalidOperationException($"database schema version {version} is not supported by this build (expected {CurrentVersion}); delete moshiro.db and the raw/ directory to start over"); } using SqliteTransaction transaction = connection.BeginTransaction(); connection.Exec(V1); connection.Exec("PRAGMA user_version = 2"); transaction.Commit(); } /// 版本 2:Kind 改成「协议 + 做什么」的名字。任务和原始数据索引跟着改;已经落盘的原始块头里还是旧名字,按旧名字解不出指标,重算不到它们,已经算好的指标不受影响。 private static readonly (string From, string To)[] RenameKinds = [ ("udp_echo", "quic_ping"), ("tcp_echo", "tcp_ping"), ("udp_train", "quic_stream"), ("tcp_bulk", "tcp_transfer"), ("connect", "tcp_connect"), ("hold", "tls_hold"), ("icmp_echo", "icmp_ping"), ]; /// /// 新增 Kind 或指标都不需要改表结构:指标是 metric_*.metrics 里的一个键,Kind 只是 raw_chunks.kind 的一个取值。 /// 节点只有一张表(agents):监听的节点有地址和端口,不监听的节点只拨出。线路 (agent_id, series) 里的 agent_id 是监听方,series 里是拨号方。 /// 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, tls_pubkey BLOB NOT NULL, listens INTEGER NOT NULL DEFAULT 0, public_host TEXT NOT NULL DEFAULT '', tcp_port INTEGER NOT NULL DEFAULT 0, quic_ports TEXT NOT NULL DEFAULT '[]', ipv6_host TEXT NOT NULL DEFAULT '', ipv6_supported INTEGER NOT NULL DEFAULT 0, ipv6_diagnostics TEXT, public_host_manual INTEGER NOT NULL DEFAULT 0, ipv6_host_manual INTEGER NOT NULL DEFAULT 0, reported_addresses TEXT NOT NULL DEFAULT '[]', 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, added_by TEXT, created_at INTEGER NOT NULL, last_seen_at INTEGER, row_version INTEGER NOT NULL DEFAULT 0 ); 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 TABLE api_tokens ( id TEXT PRIMARY KEY, name TEXT NOT NULL, capabilities TEXT NOT NULL, token_sha256 BLOB NOT NULL UNIQUE, created_at INTEGER NOT NULL, last_used_at INTEGER, expires_at INTEGER, revoked_at INTEGER ); CREATE TABLE audit_log ( id INTEGER PRIMARY KEY, at INTEGER NOT NULL, user_id TEXT, actor TEXT NOT NULL, method TEXT NOT NULL, action TEXT NOT NULL, target TEXT, detail TEXT, remote TEXT, success INTEGER NOT NULL ); CREATE TABLE events ( id INTEGER PRIMARY KEY, agent_id TEXT, kind TEXT NOT NULL, at INTEGER NOT NULL, detail TEXT ); 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; 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, kind TEXT NOT NULL DEFAULT '', created_by TEXT ); 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_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 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_versions ( metric_id TEXT PRIMARY KEY, fingerprint TEXT NOT NULL, recompute_from INTEGER, recompute_cursor INTEGER ); CREATE TABLE node_meta ( node_id TEXT PRIMARY KEY, os TEXT, os_name TEXT, arch TEXT, country TEXT, isp_key TEXT, isp_name TEXT, isp_manual INTEGER NOT NULL DEFAULT 0, geo_host TEXT, geo_at INTEGER NOT NULL DEFAULT 0, version TEXT, tag TEXT, commit_id TEXT, built_at TEXT, updated_at INTEGER NOT NULL DEFAULT 0 ) WITHOUT ROWID; CREATE TABLE operator_sets ( version INTEGER PRIMARY KEY, statement TEXT NOT NULL, credential_id TEXT NOT NULL, authenticator_data TEXT NOT NULL, client_data_json TEXT NOT NULL, signature TEXT NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE passkeys ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, public_key BLOB NOT NULL, sign_count INTEGER NOT NULL DEFAULT 0, transports TEXT NOT NULL DEFAULT '[]', created_at INTEGER NOT NULL, last_used_at INTEGER ); 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 TABLE raw_segments ( id INTEGER PRIMARY KEY, day TEXT NOT NULL, path TEXT NOT NULL UNIQUE, bytes INTEGER NOT NULL DEFAULT 0 ); 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 sessions ( id_sha256 BLOB PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, csrf_token BLOB NOT NULL, auth_method TEXT NOT NULL DEFAULT 'password', created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL ); 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 test_results ( test_id TEXT PRIMARY KEY, metrics BLOB NOT NULL, series BLOB NOT NULL ) WITHOUT ROWID; CREATE TABLE tests ( id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, dialer_id TEXT NOT NULL, ip_version TEXT NOT NULL DEFAULT 'ipv4', 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 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 INDEX ix_audit_time ON audit_log (at); CREATE INDEX ix_events_agent ON events (agent_id, at); CREATE INDEX ix_events_time ON events (at); 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 INDEX ix_passkeys_user ON passkeys (user_id); CREATE INDEX ix_queue_due ON analysis_queue (not_before); CREATE INDEX ix_raw_segment ON raw_chunks (segment_id); 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_tests_agent ON tests (agent_id, requested_at); CREATE INDEX ix_tests_path ON tests (agent_id, dialer_id, ip_version, requested_at); CREATE UNIQUE INDEX ux_agents_name ON agents (name) WHERE status <> 'pending'; """; }