stargazer/db/
migrations.rs1use sqlx::PgPool;
24
25const SCHEMA_SQL: &str = r"
30-- Reflector registry populated by Tier 1 discovery sweeps.
31-- Primary key is the reflector callsign (e.g. 'REF001', 'XLX001', 'DCS001').
32CREATE TABLE IF NOT EXISTS reflectors (
33 callsign TEXT PRIMARY KEY,
34 protocol TEXT NOT NULL,
35 ip_address INET,
36 dashboard_url TEXT,
37 country TEXT,
38 last_seen TIMESTAMPTZ,
39 tier2_available BOOLEAN DEFAULT FALSE,
40 created_at TIMESTAMPTZ DEFAULT now()
41);
42
43-- Timestamped callsign observations from Tier 2 XLX monitors or Tier 1
44-- scrapes. Each row records one callsign heard on a reflector module at a
45-- specific instant, with 'source' indicating the origin (e.g. 'xlx_monitor',
46-- 'ircddb', 'pistar').
47CREATE TABLE IF NOT EXISTS activity_log (
48 id BIGSERIAL PRIMARY KEY,
49 reflector TEXT NOT NULL REFERENCES reflectors(callsign),
50 module CHAR(1),
51 callsign TEXT NOT NULL,
52 source TEXT NOT NULL,
53 observed_at TIMESTAMPTZ NOT NULL
54);
55-- Composite index for the common query pattern: activity on a specific
56-- reflector/module within a time window.
57CREATE INDEX IF NOT EXISTS idx_activity_log_lookup
58 ON activity_log (reflector, module, observed_at);
59
60-- Snapshot of nodes currently linked to each reflector module. Updated by
61-- Tier 2 monitors; stale entries are evicted when a monitor reconnects.
62-- The composite primary key (reflector + node_callsign) means each node can
63-- only appear once per reflector.
64CREATE TABLE IF NOT EXISTS connected_nodes (
65 reflector TEXT NOT NULL REFERENCES reflectors(callsign),
66 node_callsign TEXT NOT NULL,
67 module CHAR(1),
68 protocol TEXT,
69 connected_since TIMESTAMPTZ,
70 last_heard TIMESTAMPTZ,
71 PRIMARY KEY (reflector, node_callsign)
72);
73
74-- Voice transmissions captured by Tier 3 deep connections. Each row is one
75-- D-STAR voice stream (header + N voice frames + optional EOT). The audio_mp3
76-- column holds the decoded PCM->MP3 blob once encoding completes.
77-- upload_status tracks the Rdio upload lifecycle: pending -> uploaded | failed.
78CREATE TABLE IF NOT EXISTS streams (
79 id BIGSERIAL PRIMARY KEY,
80 reflector TEXT NOT NULL,
81 module CHAR(1) NOT NULL,
82 protocol TEXT NOT NULL,
83 stream_id INTEGER NOT NULL,
84 callsign TEXT NOT NULL,
85 suffix TEXT,
86 ur_call TEXT,
87 dstar_text TEXT,
88 dprs_lat DOUBLE PRECISION,
89 dprs_lon DOUBLE PRECISION,
90 started_at TIMESTAMPTZ NOT NULL,
91 ended_at TIMESTAMPTZ,
92 frame_count INTEGER DEFAULT 0,
93 audio_mp3 BYTEA,
94 upload_status TEXT DEFAULT 'pending',
95 upload_attempts INTEGER DEFAULT 0,
96 last_upload_error TEXT,
97 uploaded_at TIMESTAMPTZ,
98 created_at TIMESTAMPTZ DEFAULT now()
99);
100-- Index for the upload processor to find pending streams efficiently.
101CREATE INDEX IF NOT EXISTS idx_streams_upload
102 ON streams (upload_status);
103-- Composite index for querying streams by reflector, module, and time.
104CREATE INDEX IF NOT EXISTS idx_streams_lookup
105 ON streams (reflector, module, started_at);
106";
107
108pub(crate) async fn run(pool: &PgPool) -> Result<(), sqlx::Error> {
119 let _result = sqlx::raw_sql(SCHEMA_SQL).execute(pool).await?;
120 tracing::info!("schema migrations applied successfully");
121 Ok(())
122}