mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-09-01 16:48:19 +00:00
440 lines
17 KiB
SQL
440 lines
17 KiB
SQL
-- Copyright 2026 Beacon Contributors
|
|
-- SPDX-License-Identifier: agpl
|
|
|
|
-- ============================================================
|
|
-- Beacon schema migration
|
|
-- ============================================================
|
|
|
|
-- ============================================================
|
|
-- IATA CODES
|
|
-- ============================================================
|
|
|
|
CREATE TABLE iata_codes (
|
|
iata CHAR(3) PRIMARY KEY,
|
|
display_name TEXT,
|
|
approx_lat DOUBLE PRECISION,
|
|
approx_lng DOUBLE PRECISION,
|
|
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- ============================================================
|
|
-- REGIONS
|
|
-- ============================================================
|
|
|
|
CREATE TABLE regions (
|
|
id SERIAL PRIMARY KEY,
|
|
slug TEXT UNIQUE NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
display_order INT DEFAULT 0,
|
|
center_lat DOUBLE PRECISION,
|
|
center_lng DOUBLE PRECISION,
|
|
zoom_level INT DEFAULT 8,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE region_iatas (
|
|
region_id INT NOT NULL REFERENCES regions(id) ON DELETE CASCADE,
|
|
iata CHAR(3) NOT NULL REFERENCES iata_codes(iata) ON DELETE CASCADE,
|
|
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (region_id, iata)
|
|
);
|
|
|
|
CREATE INDEX idx_region_iatas_iata ON region_iatas(iata);
|
|
|
|
-- ============================================================
|
|
-- TRANSPORT SCOPES
|
|
-- ============================================================
|
|
|
|
CREATE TABLE transport_scopes (
|
|
id SERIAL PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE, -- normalized key string e.g. "#bc"
|
|
display_name TEXT, -- optional friendly name
|
|
transport_key BYTEA NOT NULL, -- 16-byte derived key
|
|
key_fingerprint BYTEA NOT NULL, -- SHA256(transport_key)[:8] for fast lookup
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_transport_scopes_fingerprint ON transport_scopes(key_fingerprint);
|
|
|
|
-- ============================================================
|
|
-- NODES (must come before observers due to observer_owners FK)
|
|
-- ============================================================
|
|
|
|
CREATE TABLE nodes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
public_key BYTEA UNIQUE NOT NULL,
|
|
node_type SMALLINT NOT NULL,
|
|
name TEXT,
|
|
latitude DOUBLE PRECISION,
|
|
longitude DOUBLE PRECISION,
|
|
location_source TEXT,
|
|
last_advert_at TIMESTAMPTZ,
|
|
supports_multibyte_paths BOOLEAN NOT NULL DEFAULT FALSE,
|
|
supports_multibyte_traces BOOLEAN NOT NULL DEFAULT FALSE,
|
|
default_scope_id INT REFERENCES transport_scopes(id),
|
|
min_firmware_version TEXT GENERATED ALWAYS AS (
|
|
CASE
|
|
WHEN supports_multibyte_paths THEN '1.14.0+'
|
|
WHEN supports_multibyte_traces THEN '1.11.0+'
|
|
ELSE NULL
|
|
END
|
|
) STORED,
|
|
first_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
radio_freq_mhz REAL,
|
|
radio_sf SMALLINT,
|
|
radio_bw_khz REAL,
|
|
metadata JSONB
|
|
);
|
|
|
|
CREATE INDEX idx_nodes_type_last_seen ON nodes(node_type, last_seen DESC);
|
|
CREATE INDEX idx_nodes_location ON nodes(latitude, longitude)
|
|
WHERE latitude IS NOT NULL AND longitude IS NOT NULL;
|
|
CREATE INDEX idx_nodes_pubkey ON nodes(public_key);
|
|
CREATE INDEX idx_nodes_multibyte_paths ON nodes(supports_multibyte_paths) WHERE supports_multibyte_paths;
|
|
CREATE INDEX idx_nodes_multibyte_traces ON nodes(supports_multibyte_traces) WHERE supports_multibyte_traces;
|
|
CREATE INDEX idx_nodes_min_firmware ON nodes(min_firmware_version) WHERE min_firmware_version IS NOT NULL;
|
|
|
|
-- ============================================================
|
|
-- OBSERVERS
|
|
-- ============================================================
|
|
|
|
CREATE TABLE observers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
public_key BYTEA UNIQUE NOT NULL,
|
|
display_name TEXT,
|
|
observer_type TEXT,
|
|
software_version TEXT,
|
|
hardware_model TEXT,
|
|
firmware_version TEXT,
|
|
firmware_build TEXT,
|
|
radio_freq_mhz REAL,
|
|
radio_sf SMALLINT,
|
|
radio_bw_khz REAL,
|
|
radio_cr SMALLINT,
|
|
battery_level REAL,
|
|
uptime_seconds BIGINT,
|
|
status_metadata JSONB,
|
|
last_status_at TIMESTAMPTZ,
|
|
first_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
observation_count BIGINT DEFAULT 0,
|
|
metadata JSONB
|
|
);
|
|
|
|
CREATE INDEX idx_observers_last_seen ON observers(last_seen DESC);
|
|
CREATE INDEX idx_observers_pubkey ON observers(public_key);
|
|
CREATE INDEX idx_observers_type ON observers(observer_type) WHERE observer_type IS NOT NULL;
|
|
|
|
CREATE TABLE observer_brokers (
|
|
observer_id UUID NOT NULL REFERENCES observers(id) ON DELETE CASCADE,
|
|
broker_name TEXT NOT NULL,
|
|
first_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_packet_at TIMESTAMPTZ,
|
|
auth_ok BOOLEAN DEFAULT TRUE,
|
|
PRIMARY KEY (observer_id, broker_name)
|
|
);
|
|
|
|
CREATE TABLE observer_locations (
|
|
observer_id UUID NOT NULL REFERENCES observers(id) ON DELETE CASCADE,
|
|
iata CHAR(3) REFERENCES iata_codes(iata),
|
|
latitude DOUBLE PRECISION,
|
|
longitude DOUBLE PRECISION,
|
|
reported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (observer_id, reported_at)
|
|
);
|
|
|
|
CREATE TABLE observer_scopes (
|
|
observer_id UUID NOT NULL REFERENCES observers(id) ON DELETE CASCADE,
|
|
scope_id INT NOT NULL REFERENCES transport_scopes(id) ON DELETE CASCADE,
|
|
first_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (observer_id, scope_id)
|
|
);
|
|
|
|
CREATE INDEX idx_observer_locations_recent ON observer_locations(observer_id, reported_at DESC);
|
|
|
|
CREATE TABLE observer_telemetry (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
observer_id UUID NOT NULL REFERENCES observers(id) ON DELETE CASCADE,
|
|
reported_at TIMESTAMPTZ NOT NULL,
|
|
battery_voltage_mv INT,
|
|
airtime_tx_pct REAL,
|
|
airtime_rx_pct REAL,
|
|
noise_floor_db REAL,
|
|
uptime_seconds BIGINT,
|
|
queue_length INT,
|
|
debug_flags INT,
|
|
receive_errors INT,
|
|
UNIQUE (observer_id, reported_at)
|
|
);
|
|
|
|
CREATE INDEX idx_telemetry_reported_brin ON observer_telemetry USING BRIN (reported_at);
|
|
CREATE INDEX idx_telemetry_observer_recent ON observer_telemetry(observer_id, reported_at DESC);
|
|
|
|
CREATE TABLE observer_owners (
|
|
observer_id UUID PRIMARY KEY REFERENCES observers(id) ON DELETE CASCADE,
|
|
owner_node_id UUID REFERENCES nodes(id),
|
|
owner_pubkey BYTEA,
|
|
contact_name TEXT,
|
|
contact_email TEXT,
|
|
notes TEXT,
|
|
source TEXT,
|
|
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_observer_owners_node ON observer_owners(owner_node_id) WHERE owner_node_id IS NOT NULL;
|
|
|
|
-- ============================================================
|
|
-- NODE IATAS AND SHORT IDS
|
|
-- ============================================================
|
|
|
|
CREATE TABLE node_iatas (
|
|
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
iata CHAR(3) NOT NULL REFERENCES iata_codes(iata) ON DELETE CASCADE,
|
|
first_heard TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_heard TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
observation_count BIGINT DEFAULT 0,
|
|
PRIMARY KEY (node_id, iata)
|
|
);
|
|
|
|
CREATE INDEX idx_node_iatas_iata ON node_iatas(iata, last_heard DESC);
|
|
|
|
CREATE TABLE node_short_ids (
|
|
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
iata CHAR(3) NOT NULL REFERENCES iata_codes(iata) ON DELETE CASCADE,
|
|
prefix_4 BYTEA NOT NULL,
|
|
prefix_1 BYTEA GENERATED ALWAYS AS (substring(prefix_4 from 1 for 1)) STORED,
|
|
prefix_2 BYTEA GENERATED ALWAYS AS (substring(prefix_4 from 1 for 2)) STORED,
|
|
prefix_3 BYTEA GENERATED ALWAYS AS (substring(prefix_4 from 1 for 3)) STORED,
|
|
PRIMARY KEY (node_id, iata)
|
|
);
|
|
|
|
CREATE INDEX idx_short_ids_p1 ON node_short_ids(iata, prefix_1);
|
|
CREATE INDEX idx_short_ids_p2 ON node_short_ids(iata, prefix_2);
|
|
CREATE INDEX idx_short_ids_p3 ON node_short_ids(iata, prefix_3);
|
|
CREATE INDEX idx_short_ids_p4 ON node_short_ids(iata, prefix_4);
|
|
|
|
-- ============================================================
|
|
-- PACKETS
|
|
-- ============================================================
|
|
|
|
CREATE TABLE packets (
|
|
packet_hash BYTEA PRIMARY KEY,
|
|
payload_type SMALLINT NOT NULL,
|
|
payload_version SMALLINT NOT NULL,
|
|
route_type SMALLINT NOT NULL,
|
|
transport_codes_present BOOLEAN DEFAULT FALSE,
|
|
region_code INT,
|
|
sub_region_code INT,
|
|
scope_id INT REFERENCES transport_scopes(id),
|
|
origin_pubkey BYTEA,
|
|
raw_payload BYTEA NOT NULL,
|
|
raw_header BYTEA NOT NULL,
|
|
parsed_payload JSONB,
|
|
decrypted BOOLEAN DEFAULT FALSE,
|
|
channel_hash BYTEA,
|
|
trace_tag BYTEA,
|
|
first_heard_at TIMESTAMPTZ NOT NULL,
|
|
last_heard_at TIMESTAMPTZ NOT NULL
|
|
);
|
|
|
|
CREATE INDEX idx_packets_first_heard_brin ON packets USING BRIN (first_heard_at);
|
|
CREATE INDEX idx_packets_payload_type ON packets(payload_type, first_heard_at DESC);
|
|
CREATE INDEX idx_packets_route_type ON packets(route_type, first_heard_at DESC);
|
|
CREATE INDEX idx_packets_origin ON packets(origin_pubkey, first_heard_at DESC)
|
|
WHERE origin_pubkey IS NOT NULL;
|
|
CREATE INDEX idx_packets_channel ON packets(channel_hash, first_heard_at DESC)
|
|
WHERE channel_hash IS NOT NULL;
|
|
CREATE INDEX idx_packets_trace_tag ON packets(trace_tag) WHERE trace_tag IS NOT NULL;
|
|
|
|
-- ============================================================
|
|
-- PACKET OBSERVATIONS
|
|
-- ============================================================
|
|
|
|
CREATE TABLE packet_observations (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
packet_hash BYTEA NOT NULL REFERENCES packets(packet_hash) ON DELETE CASCADE,
|
|
observer_id UUID NOT NULL REFERENCES observers(id),
|
|
iata CHAR(3) NOT NULL REFERENCES iata_codes(iata),
|
|
heard_at TIMESTAMPTZ NOT NULL,
|
|
path_length_byte SMALLINT NOT NULL,
|
|
hash_size SMALLINT NOT NULL,
|
|
hop_count SMALLINT NOT NULL,
|
|
path_bytes BYTEA,
|
|
rssi SMALLINT,
|
|
snr REAL,
|
|
propagation_time_ms INT,
|
|
radio_freq_mhz REAL,
|
|
spread_factor SMALLINT,
|
|
bandwidth_khz REAL,
|
|
coding_rate SMALLINT,
|
|
source_broker TEXT,
|
|
UNIQUE (packet_hash, observer_id, heard_at)
|
|
);
|
|
|
|
CREATE INDEX idx_observations_heard_brin ON packet_observations USING BRIN (heard_at);
|
|
CREATE INDEX idx_observations_iata_heard ON packet_observations(iata, heard_at DESC);
|
|
CREATE INDEX idx_observations_observer ON packet_observations(observer_id, heard_at DESC);
|
|
CREATE INDEX idx_observations_packet ON packet_observations(packet_hash);
|
|
|
|
-- ============================================================
|
|
-- CHANNELS AND CHAT MESSAGES
|
|
-- ============================================================
|
|
|
|
CREATE TABLE channels (
|
|
id SERIAL PRIMARY KEY,
|
|
channel_hash BYTEA NOT NULL,
|
|
-- key_fingerprint is the first 8 bytes of SHA256(key_bytes).
|
|
-- Combined with channel_hash it uniquely identifies a channel,
|
|
-- since a 1-byte hash has high collision probability.
|
|
-- NULL when the key is unknown (hash-only record).
|
|
key_fingerprint BYTEA,
|
|
name TEXT,
|
|
hashtag TEXT UNIQUE, -- e.g. "meshcore"; only set for hashtag-derived channels
|
|
is_hashtag BOOLEAN DEFAULT FALSE,
|
|
is_public BOOLEAN DEFAULT FALSE,
|
|
key_known BOOLEAN DEFAULT FALSE,
|
|
first_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
message_count BIGINT DEFAULT 0,
|
|
-- Unique on (hash, fingerprint): allows multiple channels with the same
|
|
-- hash as long as they have different keys. NULL fingerprint is allowed
|
|
-- once per hash (the unknown-key record).
|
|
UNIQUE (channel_hash, key_fingerprint)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX idx_channels_hash_no_key
|
|
ON channels (channel_hash)
|
|
WHERE key_fingerprint IS NULL;
|
|
|
|
CREATE INDEX idx_channels_last_seen ON channels(last_seen DESC);
|
|
CREATE INDEX idx_channels_hash ON channels(channel_hash);
|
|
CREATE INDEX idx_channels_hashtag ON channels(hashtag) WHERE hashtag IS NOT NULL;
|
|
|
|
CREATE TABLE channel_keys (
|
|
channel_id INT PRIMARY KEY REFERENCES channels(id) ON DELETE CASCADE,
|
|
key_bytes BYTEA NOT NULL,
|
|
key_fingerprint BYTEA NOT NULL, -- first 8 bytes of SHA256(key_bytes)
|
|
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
added_by TEXT
|
|
);
|
|
|
|
CREATE TABLE channel_messages (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
channel_id INT NOT NULL REFERENCES channels(id),
|
|
packet_hash BYTEA NOT NULL REFERENCES packets(packet_hash),
|
|
sender_name TEXT,
|
|
sender_pubkey BYTEA,
|
|
content TEXT,
|
|
sent_at TIMESTAMPTZ NOT NULL,
|
|
UNIQUE (packet_hash)
|
|
);
|
|
|
|
CREATE INDEX idx_channel_messages_channel ON channel_messages(channel_id, sent_at DESC);
|
|
CREATE INDEX idx_channel_messages_sent_brin ON channel_messages USING BRIN (sent_at);
|
|
|
|
-- ============================================================
|
|
-- ROUTES
|
|
-- ============================================================
|
|
|
|
CREATE TABLE known_routes (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
node_ids UUID[] NOT NULL, -- resolved node UUIDs in hop order
|
|
hash_prefix BYTEA[] NOT NULL, -- raw hash bytes in hop order for prefix matching
|
|
iata CHAR(3) NOT NULL REFERENCES iata_codes(iata) ON DELETE CASCADE,
|
|
hop_count INT NOT NULL,
|
|
first_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
observation_count BIGINT NOt NULL DEFAULT 1,
|
|
UNIQUE (node_ids, iata)
|
|
);
|
|
|
|
CREATE INDEX idx_known_routes_iata ON known_routes(iata);
|
|
CREATE INDEX idx_known_routes_hop_count ON known_routes(iata, hop_count);
|
|
|
|
-- ============================================================
|
|
-- NEIGHBORS
|
|
-- ============================================================
|
|
|
|
CREATE TABLE node_neighbors (
|
|
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
neighbor_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
iata CHAR(3) NOT NULL REFERENCES iata_codes(iata) ON DELETE CASCADE,
|
|
first_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
observation_count BIGINT NOT NULL DEFAULT 1,
|
|
PRIMARY KEY (node_id, neighbor_id, iata)
|
|
);
|
|
|
|
CREATE INDEX idx_node_neighbors_node ON node_neighbors(node_id, iata);
|
|
CREATE INDEX idx_node_neighbors_neighbor ON node_neighbors(neighbor_id, iata);
|
|
|
|
-- ============================================================
|
|
-- MATERIALIZED VIEWS
|
|
-- ============================================================
|
|
|
|
CREATE MATERIALIZED VIEW mv_hourly_iata_stats AS
|
|
SELECT
|
|
iata,
|
|
date_trunc('hour', heard_at)::timestamptz AS hour,
|
|
COUNT(*) AS observation_count,
|
|
COUNT(DISTINCT packet_hash) AS unique_packets,
|
|
COUNT(DISTINCT observer_id) AS active_observers
|
|
FROM packet_observations
|
|
WHERE heard_at > NOW() - INTERVAL '7 days'
|
|
GROUP BY iata, date_trunc('hour', heard_at);
|
|
|
|
CREATE MATERIALIZED VIEW mv_top_nodes_by_iata AS
|
|
SELECT
|
|
ni.iata,
|
|
ni.node_id,
|
|
n.name,
|
|
n.node_type,
|
|
ni.observation_count,
|
|
ni.last_heard
|
|
FROM node_iatas ni
|
|
JOIN nodes n ON n.id = ni.node_id
|
|
WHERE ni.last_heard > NOW() - INTERVAL '7 days';
|
|
|
|
CREATE UNIQUE INDEX idx_mv_top_nodes
|
|
ON mv_top_nodes_by_iata(iata, node_id);
|
|
|
|
CREATE UNIQUE INDEX idx_mv_hourly_iata_stats
|
|
ON mv_hourly_iata_stats(iata, hour);
|
|
|
|
CREATE MATERIALIZED VIEW mv_radio_presets AS
|
|
SELECT
|
|
concat(o.radio_freq_mhz, ',', o.radio_bw_khz, ',', o.radio_sf)::text AS preset,
|
|
(SELECT po.iata FROM packet_observations po WHERE po.observer_id = o.id ORDER BY po.heard_at DESC LIMIT 1) AS iata,
|
|
'observer' AS source_type,
|
|
COUNT(*) AS count
|
|
FROM observers o
|
|
WHERE o.radio_freq_mhz IS NOT NULL
|
|
AND o.radio_sf IS NOT NULL
|
|
AND o.radio_bw_khz IS NOT NULL
|
|
GROUP BY concat(o.radio_freq_mhz, ',', o.radio_bw_khz, ',', o.radio_sf)::text,
|
|
(SELECT po.iata FROM packet_observations po WHERE po.observer_id = o.id ORDER BY po.heard_at DESC LIMIT 1)
|
|
HAVING (SELECT po.iata FROM packet_observations po WHERE po.observer_id = o.id ORDER BY po.heard_at DESC LIMIT 1) IS NOT NULL
|
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
concat(n.radio_freq_mhz, ',', n.radio_bw_khz, ',', n.radio_sf) AS preset,
|
|
ni.iata,
|
|
'node' AS source_type,
|
|
COUNT(*) AS count
|
|
FROM nodes n
|
|
JOIN node_iatas ni ON ni.node_id = n.id
|
|
WHERE n.radio_freq_mhz IS NOT NULL
|
|
AND n.radio_sf IS NOT NULL
|
|
AND n.radio_bw_khz IS NOT NULL
|
|
GROUP BY concat(n.radio_freq_mhz, ',', n.radio_bw_khz, ',', n.radio_sf), ni.iata;
|
|
|
|
CREATE UNIQUE INDEX idx_mv_radio_presets
|
|
ON mv_radio_presets(preset, iata, source_type);
|