334 lines
12 KiB
C#
334 lines
12 KiB
C#
using Microsoft.Data.Sqlite;
|
||
|
||
namespace MoShiro.Backend.Storage.Sqlite;
|
||
|
||
/// <summary>
|
||
/// 表结构。版本记录在 <c>PRAGMA user_version</c>。版本 1 到 2 只改了 Kind 的名字(见 <see cref="RenameKinds"/>);
|
||
/// 其他版本的数据库(包括更早的构建留下的)不会被迁移,启动时报错,删除 <c>moshiro.db</c> 和 <c>raw/</c> 之后重新开始。以后的结构变更按版本号追加内置 SQL。
|
||
/// </summary>
|
||
internal static class Schema
|
||
{
|
||
public const int CurrentVersion = 2;
|
||
|
||
public static void Migrate(SqliteConnection connection)
|
||
{
|
||
int version = connection.Scalar<int>("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();
|
||
}
|
||
|
||
/// <summary>版本 2:Kind 改成「协议 + 做什么」的名字。任务和原始数据索引跟着改;已经落盘的原始块头里还是旧名字,按旧名字解不出指标,重算不到它们,已经算好的指标不受影响。</summary>
|
||
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"),
|
||
];
|
||
|
||
/// <summary>
|
||
/// 新增 Kind 或指标都不需要改表结构:指标是 <c>metric_*.metrics</c> 里的一个键,Kind 只是 <c>raw_chunks.kind</c> 的一个取值。
|
||
/// 节点只有一张表(<c>agents</c>):监听的节点有地址和端口,不监听的节点只拨出。线路 <c>(agent_id, series)</c> 里的 agent_id 是监听方,series 里是拨号方。
|
||
/// </summary>
|
||
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';
|
||
""";
|
||
}
|