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';
""";
}