Files
beacon-server/db/queries/queries.sql
T
Enot (ded) Skelly 286d633da9 add observer broker details
now observer brokers listed include last seen and last
packet times
2026-05-27 10:36:32 -07:00

447 lines
14 KiB
SQL

-- ============================================================
-- IATA CODES
-- ============================================================
-- name: UpsertIATA :exec
INSERT INTO iata_codes (iata)
VALUES ($1)
ON CONFLICT (iata) DO NOTHING;
-- name: GetIATA :one
SELECT * FROM iata_codes WHERE iata = $1;
-- name: ListIATAs :many
SELECT * FROM iata_codes ORDER BY iata;
-- name: UpsertIATADetails :exec
UPDATE iata_codes SET
display_name = $2,
approx_lat = $3,
approx_lng = $4
WHERE iata = $1;
-- ============================================================
-- OBSERVERS
-- ============================================================
-- name: UpsertObserver :one
INSERT INTO observers (public_key, observer_type, last_seen)
VALUES ($1, 'unknown', NOW())
ON CONFLICT (public_key) DO UPDATE SET
last_seen = NOW(),
observation_count = observers.observation_count + 1
RETURNING *;
-- name: UpdateObserverStatus :one
UPDATE observers SET
display_name = COALESCE(NULLIF($2, ''), display_name),
observer_type = COALESCE(NULLIF($3, ''), observer_type),
software_version = COALESCE($4, software_version),
hardware_model = COALESCE($5, hardware_model),
firmware_version = COALESCE($6, firmware_version),
firmware_build = COALESCE($7, firmware_build),
radio_freq_mhz = COALESCE($8, radio_freq_mhz),
radio_sf = COALESCE($9, radio_sf),
radio_bw_khz = COALESCE($10, radio_bw_khz),
radio_cr = COALESCE($11, radio_cr),
battery_level = COALESCE($12, battery_level),
uptime_seconds = COALESCE($13, uptime_seconds),
status_metadata = $14,
last_status_at = NOW(),
last_seen = NOW()
WHERE public_key = $1
RETURNING id;
-- name: GetObserverByPubkey :one
SELECT * FROM observers WHERE public_key = $1;
-- name: GetObserverByID :one
SELECT * FROM observers WHERE id = $1;
-- name: GetObserverBrokers :many
SELECT broker_name, last_seen, last_packet_at
FROM observer_brokers
WHERE observer_id = $1
ORDER BY last_seen DESC;
-- name: ListObservers :many
SELECT
o.id,
o.display_name,
o.observer_type,
o.last_status_at,
COALESCE(CASE
WHEN o.last_status_at > NOW() - INTERVAL '5 minutes' THEN 'online'
ELSE 'offline'
END, 'offline')::text AS status,
COALESCE((
SELECT po.iata
FROM packet_observations po
WHERE po.observer_id = o.id
ORDER BY po.heard_at DESC
LIMIT 1
), '')::text AS iata
FROM observers o
LEFT JOIN observer_brokers ob ON ob.observer_id = o.id
WHERE
($1 = '' OR (
SELECT po.iata FROM packet_observations po
WHERE po.observer_id = o.id
ORDER BY po.heard_at DESC LIMIT 1
) = $1)
AND ($2 = '' OR o.observer_type = $2)
AND ($3 = '' OR ob.broker_name = $3)
AND ($4 = '' OR CASE
WHEN o.last_status_at > NOW() - INTERVAL '5 minutes' THEN 'online'
ELSE 'offline'
END = $4)
GROUP BY o.id
ORDER BY o.last_seen DESC;
-- name: GetObserverLastIATA :one
SELECT iata FROM packet_observations
WHERE observer_id = $1
ORDER BY heard_at DESC
LIMIT 1;
-- name: GetObserverRadio :one
SELECT radio_freq_mhz, radio_bw_khz, radio_sf, radio_cr
FROM observers
WHERE id = $1;
-- ============================================================
-- OBSERVER BROKERS
-- ============================================================
-- name: UpsertObserverBroker :exec
INSERT INTO observer_brokers (observer_id, broker_name, last_seen, last_packet_at)
VALUES ($1, $2, NOW(), NOW())
ON CONFLICT (observer_id, broker_name) DO UPDATE SET
last_seen = NOW(),
last_packet_at = NOW();
-- ============================================================
-- PACKETS
-- ============================================================
-- name: UpsertPacket :one
INSERT INTO packets (
packet_hash,
payload_type,
payload_version,
route_type,
transport_codes_present,
region_code,
sub_region_code,
origin_pubkey,
raw_payload,
parsed_payload,
channel_hash,
first_heard_at,
last_heard_at,
observation_count
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW(), NOW(), 1
)
ON CONFLICT (packet_hash) DO UPDATE SET
last_heard_at = NOW(),
observation_count = packets.observation_count + 1
RETURNING *, (xmax = 0) AS inserted;
-- name: GetPacket :one
SELECT * FROM packets WHERE packet_hash = $1;
-- name: ListPackets :many
SELECT p.*
FROM packets p
WHERE
($1::smallint IS NULL OR p.payload_type = $1)
AND ($2::smallint IS NULL OR p.route_type = $2)
AND ($3::timestamptz IS NULL OR p.first_heard_at >= $3)
AND ($4::timestamptz IS NULL OR p.first_heard_at <= $4)
ORDER BY p.last_heard_at DESC
LIMIT $5;
-- name: ListPacketsAfterID :many
SELECT p.*
FROM packets p
JOIN packet_observations po ON po.packet_hash = p.packet_hash
WHERE po.id > $1
ORDER BY po.id ASC
LIMIT $2;
-- ============================================================
-- PACKET OBSERVATIONS
-- ============================================================
-- name: InsertObservation :one
INSERT INTO packet_observations (
packet_hash,
observer_id,
iata,
heard_at,
path_length_byte,
hash_size,
hop_count,
path_bytes,
rssi,
snr,
propagation_time_ms,
radio_freq_mhz,
spread_factor,
bandwidth_khz,
coding_rate,
source_broker
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16
)
ON CONFLICT (packet_hash, observer_id, heard_at) DO NOTHING
RETURNING *;
-- name: ListObservationsForPacket :many
SELECT * FROM packet_observations
WHERE packet_hash = $1
ORDER BY heard_at ASC;
-- name: ListObservationsForObserver :many
SELECT * FROM packet_observations
WHERE observer_id = $1
AND ($2::timestamptz IS NULL OR heard_at >= $2)
ORDER BY heard_at DESC
LIMIT $3;
-- ============================================================
-- NODES
-- ============================================================
-- name: UpsertNode :one
INSERT INTO nodes (public_key, node_type, name, latitude, longitude, location_source, last_advert_at, last_seen)
VALUES ($1, $2, $3, $4, $5, 'advert', NOW(), NOW())
ON CONFLICT (public_key) DO UPDATE SET
node_type = EXCLUDED.node_type,
name = COALESCE(EXCLUDED.name, nodes.name),
latitude = COALESCE(EXCLUDED.latitude, nodes.latitude),
longitude = COALESCE(EXCLUDED.longitude, nodes.longitude),
location_source = CASE WHEN EXCLUDED.latitude IS NOT NULL THEN 'advert' ELSE nodes.location_source END,
last_advert_at = NOW(),
last_seen = NOW()
RETURNING *;
-- name: SetNodeMultibytePaths :exec
UPDATE nodes SET supports_multibyte_paths = TRUE
WHERE id = $1 AND supports_multibyte_paths = FALSE;
-- name: SetNodeMultibyteTraces :exec
UPDATE nodes SET supports_multibyte_traces = TRUE
WHERE id = $1 AND supports_multibyte_traces = FALSE;
-- name: GetNodeByPubkey :one
SELECT * FROM nodes WHERE public_key = $1;
-- name: ListNodes :many
SELECT * FROM nodes
WHERE
($1::smallint IS NULL OR node_type = $1)
ORDER BY last_seen DESC
LIMIT $2;
-- ============================================================
-- NODE IATAS
-- ============================================================
-- name: UpsertNodeIATA :exec
INSERT INTO node_iatas (node_id, iata, last_heard, observation_count)
VALUES ($1, $2, NOW(), 1)
ON CONFLICT (node_id, iata) DO UPDATE SET
last_heard = NOW(),
observation_count = node_iatas.observation_count + 1;
-- name: UpsertNodeShortID :exec
INSERT INTO node_short_ids (node_id, iata, prefix_4)
VALUES ($1, $2, $3)
ON CONFLICT (node_id, iata) DO NOTHING;
-- ============================================================
-- CHANNELS
-- ============================================================
-- name: UpsertChannel :one
-- Upsert a channel by (hash, key_fingerprint). Pass NULL fingerprint for
-- hash-only records (key unknown). Returns the channel row.
INSERT INTO channels (channel_hash, key_fingerprint, name, hashtag, is_hashtag, key_known, last_seen)
VALUES ($1, $2::bytea, $3, $4, $5, ($2 IS NOT NULL), NOW())
ON CONFLICT (channel_hash, key_fingerprint) DO UPDATE SET
last_seen = NOW(),
name = COALESCE(EXCLUDED.name, channels.name),
message_count = CASE WHEN $6 THEN channels.message_count + 1 ELSE channels.message_count END
RETURNING *;
-- name: UpsertChannelHashOnly :one
INSERT INTO channels (channel_hash, last_seen)
VALUES ($1, NOW())
ON CONFLICT (channel_hash) WHERE key_fingerprint IS NULL DO UPDATE SET
last_seen = NOW()
RETURNING id;
-- name: SetChannelKeyKnown :exec
UPDATE channels SET key_known = TRUE
WHERE channel_hash = $1 AND key_fingerprint = $2;
-- name: ListChannels :many
-- Returns channels ordered by last seen, optionally filtered by hash and/or IATA.
-- Pass NULL for hash to skip hash filtering. Pass empty string for iata to skip IATA filtering.
-- IATA filter returns channels that have been active (have messages heard) in that IATA.
SELECT DISTINCT c.* FROM channels c
WHERE ($1::bytea IS NULL OR c.channel_hash = $1)
AND ($2 = '' OR EXISTS (
SELECT 1 FROM packets p
JOIN packet_observations po ON po.packet_hash = p.packet_hash
WHERE p.channel_hash = c.channel_hash
AND po.iata = $2
))
ORDER BY c.last_seen DESC
LIMIT $3;
-- name: GetChannelsByHash :many
-- Returns all channels for a given hash (may be multiple on hash collision).
SELECT * FROM channels WHERE channel_hash = $1 ORDER BY last_seen DESC LIMIT $2;
-- name: GetChannelByHashAndFingerprint :one
SELECT * FROM channels WHERE channel_hash = $1 AND key_fingerprint = $2;
-- name: GetChannelByHashtag :one
SELECT * FROM channels WHERE hashtag = $1;
-- name: GetChannelByID :one
SELECT * FROM channels WHERE id = $1;
-- ============================================================
-- CHANNEL MESSAGES
-- ============================================================
-- name: InsertChannelMessage :one
INSERT INTO channel_messages (channel_id, packet_hash, sender_name, content, sent_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (packet_hash) DO NOTHING
RETURNING id;
-- name: ListChannelMessages :many
-- Returns messages for a channel identified by integer ID.
-- Pass a zero/null timestamp for since to return all messages up to limit.
-- Pass empty string for iata to skip IATA filtering.
SELECT DISTINCT ON (cm.id) cm.*, encode(cm.packet_hash, 'hex') as packet_hash_hex, c.channel_hash
FROM channel_messages cm
JOIN channels c ON c.id = cm.channel_id
JOIN packet_observations po ON po.packet_hash = cm.packet_hash
WHERE cm.channel_id = $1
AND ($2::timestamptz IS NULL OR cm.sent_at >= $2)
AND ($3 = '' OR po.iata = $3)
ORDER BY cm.id, cm.sent_at DESC
LIMIT $4;
-- name: ListAllChannelMessages :many
-- Returns all messages across all channels with optional time and IATA filters.
-- Pass empty string for iata to skip IATA filtering.
SELECT DISTINCT ON (cm.id) cm.*, encode(cm.packet_hash, 'hex') as packet_hash_hex, c.channel_hash
FROM channel_messages cm
JOIN channels c ON c.id = cm.channel_id
JOIN packet_observations po ON po.packet_hash = cm.packet_hash
WHERE ($1::timestamptz IS NULL OR cm.sent_at >= $1)
AND ($2 = '' OR po.iata = $2)
ORDER BY cm.id, cm.sent_at DESC
LIMIT $3;
-- name: ListChannelMessagesByHash :many
-- Returns messages for all channels matching a hash byte.
-- May return messages from multiple channels if the hash collides across different keys.
-- Pass empty string for iata to skip IATA filtering.
SELECT DISTINCT ON (cm.id) cm.*, c.channel_hash FROM channel_messages cm
JOIN channels c ON c.id = cm.channel_id
JOIN packet_observations po ON po.packet_hash = cm.packet_hash
WHERE c.channel_hash = $1
AND ($2::timestamptz IS NULL OR cm.sent_at >= $2)
AND ($3 = '' OR po.iata = $3)
ORDER BY cm.id, cm.sent_at DESC
LIMIT $4;
-- ============================================================
-- STATS
-- ============================================================
-- name: GetStatsOverview :one
SELECT
COUNT(DISTINCT po.packet_hash) AS total_packets,
COUNT(*) AS total_observations,
COUNT(DISTINCT po.observer_id) AS active_observers,
COUNT(DISTINCT po.iata) AS active_iatas
FROM packet_observations po
WHERE po.heard_at > NOW() - INTERVAL '24 hours'
AND ($1::char(3) IS NULL OR po.iata = $1);
-- name: GetHourlyStats :many
SELECT * FROM mv_hourly_iata_stats
WHERE ($1::char(3) IS NULL OR iata = $1)
AND hour >= NOW() - $2::interval
ORDER BY iata, hour;
-- name: GetTopNodes :many
SELECT * FROM mv_top_nodes_by_iata
WHERE ($1::char(3) IS NULL OR iata = $1)
ORDER BY observation_count DESC
LIMIT $2;
-- ============================================================
-- REGIONS
-- ============================================================
-- name: ListRegions :many
SELECT id, slug, name
FROM regions
ORDER BY display_order, name;
-- name: GetRegion :one
SELECT id, slug, name, description, center_lat, center_lng, zoom_level
FROM regions
WHERE id = $1;
-- name: GetRegionIATAs :many
SELECT iata FROM region_iatas
WHERE region_id = $1
ORDER BY iata;
-- name: UpsertRegion :one
INSERT INTO regions (slug, name, description, display_order, center_lat, center_lng, zoom_level, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
ON CONFLICT (slug) DO UPDATE SET
name = EXCLUDED.name,
description = EXCLUDED.description,
display_order = EXCLUDED.display_order,
center_lat = EXCLUDED.center_lat,
center_lng = EXCLUDED.center_lng,
zoom_level = EXCLUDED.zoom_level,
updated_at = NOW()
RETURNING id;
-- name: UpsertRegionIATA :exec
INSERT INTO region_iatas (region_id, iata)
VALUES ($1, $2)
ON CONFLICT (region_id, iata) DO NOTHING;
-- ============================================================
-- HELPERS
-- ============================================================
-- name: ResolvePathHashes :many
SELECT DISTINCT n.id
FROM node_short_ids ns
JOIN nodes n ON n.id = ns.node_id
WHERE ns.iata = $1
AND CASE
WHEN cardinality($2::bytea[]) > 0 AND length($2[1]) = 1 THEN ns.prefix_1 = ANY($2)
WHEN cardinality($2::bytea[]) > 0 AND length($2[1]) = 2 THEN ns.prefix_2 = ANY($2)
WHEN cardinality($2::bytea[]) > 0 AND length($2[1]) = 3 THEN ns.prefix_3 = ANY($2)
WHEN cardinality($2::bytea[]) > 0 AND length($2[1]) = 4 THEN ns.prefix_4 = ANY($2)
ELSE FALSE
END;