296 lines
12 KiB
C#
296 lines
12 KiB
C#
using Microsoft.Data.Sqlite;
|
|
|
|
namespace MoShiro.Backend.Storage.Sqlite;
|
|
|
|
/// <summary>表结构。版本记录在 <c>PRAGMA user_version</c>;以后的迁移是按版本号追加的内置 SQL。</summary>
|
|
internal static class Schema
|
|
{
|
|
public const int CurrentVersion = 7;
|
|
|
|
public static void Migrate(SqliteConnection connection)
|
|
{
|
|
int version = connection.Scalar<int>("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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 新增 Kind 或指标都不需要改表结构:指标是 <c>metric_*.metrics</c> 里的一个键,Kind 只是 <c>raw_chunks.kind</c> 的一个取值。
|
|
/// </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,
|
|
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);
|
|
""";
|
|
}
|