-- Copyright 2026 Beacon Contributors -- SPDX-License-Identifier: agpl -- ============================================================ -- IATA CODES -- ============================================================ -- name: UpsertIATA :exec INSERT INTO iata_codes (iata) VALUES ($1) ON CONFLICT (iata) DO NOTHING; -- name: AddIATAs :exec INSERT INTO iata_codes (iata) SELECT unnest(@iatas::bpchar[]) 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: ListHeardIATAs :many SELECT DISTINCT last_iata::text AS iata FROM observers WHERE last_iata IS NOT NULL ORDER BY 1; -- name: UpsertIATADetails :exec INSERT INTO iata_codes (iata, display_name, approx_lat, approx_lng) VALUES ($1, $2, $3, $4) ON CONFLICT (iata) DO UPDATE SET display_name = EXCLUDED.display_name, approx_lat = EXCLUDED.approx_lat, approx_lng = EXCLUDED.approx_lng; -- name: GetIATABorder :one -- An imported MeshMapper boundary overrides the configured one. border is NULL -- when neither exists; a missing row (unknown IATA) is sql.ErrNoRows, same -- not-found distinction GetIATA already makes. SELECT COALESCE(z.feature, i.border)::jsonb AS border FROM iata_codes i LEFT JOIN meshmapper_zone_boundaries z ON z.iata = i.iata WHERE i.iata = $1; -- name: UpsertIATABorder :exec -- Written by the config-file-driven seeder (internal/config/seed.go), not a -- runtime HTTP path. border is a full, pre-validated GeoJSON Feature with -- bbox already computed -- see internal/config/border.go. INSERT INTO iata_codes (iata, border) VALUES ($1, $2) ON CONFLICT (iata) DO UPDATE SET border = EXCLUDED.border; -- ============================================================ -- TRANSPORT CODES -- ============================================================ -- name: UpsertTransportScope :exec INSERT INTO transport_scopes (name, display_name, transport_key, key_fingerprint) VALUES ($1, $2, $3, $4) ON CONFLICT (name) DO UPDATE SET display_name = EXCLUDED.display_name, transport_key = EXCLUDED.transport_key, key_fingerprint = EXCLUDED.key_fingerprint, imported_only = FALSE; -- name: GetTransportScopes :many SELECT name, transport_key, key_fingerprint FROM transport_scopes WHERE NOT imported_only ORDER BY name; -- name: GetTransportScopeByName :one SELECT id FROM transport_scopes WHERE name = $1; -- name: GetScopeNames :many SELECT name FROM transport_scopes ORDER BY name; -- name: GetScopeByName :one -- Packets and IATAs cover every retained rollup hour; IATAs are those that heard the -- scope's own packets. Observer and node counts are current memberships. SELECT ts.name, COALESCE((SELECT SUM(s.packets) FROM analytics_hourly_scope_sets s WHERE s.scope_id = ts.id), 0)::bigint AS packet_count, (SELECT COUNT(*) FROM observer_scopes os WHERE os.scope_id = ts.id)::bigint AS observer_count, (SELECT COUNT(*) FROM nodes n WHERE n.default_scope_id = ts.id)::bigint AS node_count, cardinality(i.iatas)::bigint AS iata_count, i.iatas FROM transport_scopes ts CROSS JOIN LATERAL ( SELECT COALESCE(array_agg(DISTINCT x.iata::text ORDER BY x.iata::text), '{}')::text[] AS iatas FROM analytics_hourly_scope_sets s CROSS JOIN LATERAL unnest(s.iatas) AS x(iata) WHERE s.scope_id = ts.id ) i WHERE ts.name = $1; -- ============================================================ -- OBSERVERS -- ============================================================ -- name: UpsertObserver :one -- Empty iata (status/neighbors) leaves last_iata alone; otherwise the newest iata_at wins. INSERT INTO observers (public_key, observer_type, last_seen, last_iata, last_iata_at) VALUES ($1, 'unknown', NOW(), NULLIF(sqlc.arg(iata)::text, ''), CASE WHEN sqlc.arg(iata)::text <> '' THEN sqlc.arg(iata_at)::timestamptz END) ON CONFLICT (public_key) DO UPDATE SET last_seen = NOW(), observation_count = observers.observation_count + 1, last_iata = CASE WHEN EXCLUDED.last_iata IS NOT NULL AND (observers.last_iata_at IS NULL OR EXCLUDED.last_iata_at > observers.last_iata_at) THEN EXCLUDED.last_iata ELSE observers.last_iata END, last_iata_at = GREATEST(observers.last_iata_at, EXCLUDED.last_iata_at) 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: TouchObservers :exec -- Batched flush of coalesced presence bumps. GREATEST keeps a late flush -- from regressing a newer write-through (e.g. a status update). UPDATE observers o SET last_seen = GREATEST(o.last_seen, v.seen), observation_count = COALESCE(o.observation_count, 0) + v.delta, last_iata = CASE WHEN v.iata <> '' AND (o.last_iata_at IS NULL OR v.iata_at > o.last_iata_at) THEN v.iata ELSE o.last_iata END, last_iata_at = GREATEST(o.last_iata_at, v.iata_at) FROM ( SELECT unnest($1::uuid[]) AS id, unnest($2::timestamptz[]) AS seen, unnest($3::int[]) AS delta, unnest($4::text[]) AS iata, unnest($5::timestamptz[]) AS iata_at ) v WHERE o.id = v.id; -- name: UpsertObserverScope :exec INSERT INTO observer_scopes (observer_id, scope_id, last_seen) VALUES ($1, $2, NOW()) ON CONFLICT (observer_id, scope_id) DO UPDATE SET last_seen = NOW(); -- name: GetObserverScopes :many SELECT ts.name FROM observer_scopes os JOIN transport_scopes ts ON ts.id = os.scope_id WHERE os.observer_id = $1 ORDER BY ts.name; -- 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 -- Pass cursor=0 to start from the beginning, or the last seen observer's rownum for pagination. -- Note: observers use UUID PKs so we order by last_seen and use a keyset on last_seen+id. SELECT o.id, o.display_name, o.observer_type, o.last_status_at, o.radio_freq_mhz, o.radio_sf, o.radio_bw_khz, array_remove(array_agg(DISTINCT ts.name ORDER BY ts.name), NULL)::text[] AS scopes, COALESCE(CASE WHEN GREATEST(COALESCE(o.last_status_at, o.last_seen), o.last_seen) > NOW() - INTERVAL '5 minutes' THEN 'online' ELSE 'offline' END, 'offline')::text AS status, COALESCE(o.last_iata, '')::text AS iata FROM observers o LEFT JOIN observer_brokers ob ON ob.observer_id = o.id LEFT JOIN observer_scopes os ON os.observer_id = o.id LEFT JOIN transport_scopes ts ON ts.id = os.scope_id WHERE (COALESCE(cardinality($1::bpchar[]), 0) = 0 OR o.last_iata = ANY($1::bpchar[])) AND ($2 = '' OR o.observer_type = $2) AND ($3 = '' OR ob.broker_name = $3) AND ($4 = '' OR CASE WHEN GREATEST(COALESCE(o.last_status_at, o.last_seen), o.last_seen) > NOW() - INTERVAL '5 minutes' THEN 'online' ELSE 'offline' END = $4) AND ($5 = '' OR o.display_name ILIKE '%' || $5 || '%') AND ($6::timestamptz IS NULL OR o.last_seen < $6) AND ($8::text = '' OR EXISTS ( SELECT 1 FROM observer_scopes os2 JOIN transport_scopes ts2 ON ts2.id = os2.scope_id WHERE os2.observer_id = o.id AND ts2.name = $8::text )) GROUP BY o.id ORDER BY o.last_seen DESC LIMIT $7; -- name: GetObserverLastIATA :one SELECT COALESCE(last_iata, '')::text FROM observers WHERE id = $1; -- name: GetObserverRadio :one SELECT radio_freq_mhz, radio_bw_khz, radio_sf, radio_cr FROM observers WHERE id = $1; -- name: InsertObserverTelemetry :exec -- Inserts a telemetry snapshot for an observer. The reported_at timestamp should -- be truncated to the configured resolution before calling to ensure deduplication. INSERT INTO observer_telemetry ( observer_id, reported_at, battery_voltage_mv, airtime_tx_secs, airtime_rx_secs, noise_floor_db, uptime_seconds, queue_length, debug_flags, receive_errors ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (observer_id, reported_at) DO NOTHING; -- name: GetObserverTelemetry :many SELECT id, reported_at, battery_voltage_mv, airtime_tx_secs, airtime_rx_secs, noise_floor_db, uptime_seconds, queue_length, debug_flags, receive_errors FROM observer_telemetry WHERE observer_id = $1 AND ($2::timestamptz IS NULL OR reported_at >= $2) AND ($3::timestamptz IS NULL OR reported_at <= $3) AND ($4 = 0 OR id > $4) ORDER BY reported_at ASC; -- name: GetObserverActivityRaw :many -- Sub-hour activity buckets straight off idx_observations_observer; no join to packets. -- Aggregates are COALESCEd and paired with a count column: sqlc types a cast expression as -- NOT NULL, so the counts are what tell the store a bucket had no costed or no signal rows. SELECT date_bin($3::interval, heard_at, TIMESTAMPTZ 'epoch')::timestamptz AS bucket, COUNT(*)::bigint AS observations, COALESCE(SUM(airtime_ms), 0)::real AS airtime_ms, COUNT(airtime_ms)::bigint AS airtime_n, COALESCE(AVG(snr) FILTER (WHERE NOT (COALESCE(rssi, 0) = 0 AND COALESCE(snr, 0) = 0)), 0)::real AS snr_avg, COALESCE(MIN(snr) FILTER (WHERE NOT (COALESCE(rssi, 0) = 0 AND COALESCE(snr, 0) = 0)), 0)::real AS snr_min, COUNT(snr) FILTER (WHERE NOT (COALESCE(rssi, 0) = 0 AND COALESCE(snr, 0) = 0))::bigint AS snr_n, COALESCE(AVG(rssi) FILTER (WHERE NOT (COALESCE(rssi, 0) = 0 AND COALESCE(snr, 0) = 0)), 0)::real AS rssi_avg, COUNT(rssi) FILTER (WHERE NOT (COALESCE(rssi, 0) = 0 AND COALESCE(snr, 0) = 0))::bigint AS rssi_n FROM packet_observations WHERE observer_id = $1 AND heard_at >= $2::timestamptz AND heard_at < @until::timestamptz GROUP BY bucket ORDER BY bucket; -- name: GetObserverActivityRawPayloadTypes :many SELECT payload_type, COUNT(*)::bigint AS count FROM packet_observations WHERE observer_id = $1 AND heard_at >= $2::timestamptz AND heard_at < @until::timestamptz GROUP BY payload_type ORDER BY count DESC; -- name: GetObserverActivityHourly :many -- Hour-or-coarser buckets summed from the hourly rollup; same COALESCE-plus-count shape as the raw query. -- Hours from @tail_start on aren't rolled yet and read raw rows. WITH src AS ( SELECT a.hour AS t, a.observations, a.airtime_ms, a.airtime_n, a.snr_sum, a.snr_n, a.snr_min, a.rssi_sum, a.rssi_n FROM analytics_hourly_observer_activity a WHERE a.observer_id = $1 AND a.hour >= $2::timestamptz AND a.hour < LEAST(@until::timestamptz, @tail_start::timestamptz) UNION ALL SELECT o.heard_at, 1::bigint, o.airtime_ms, (o.airtime_ms IS NOT NULL)::int::bigint, s.snr, (s.snr IS NOT NULL)::int::bigint, s.snr, s.rssi::bigint, (s.rssi IS NOT NULL)::int::bigint FROM packet_observations o CROSS JOIN LATERAL (SELECT CASE WHEN NOT (COALESCE(o.rssi, 0) = 0 AND COALESCE(o.snr, 0) = 0) THEN o.snr END AS snr, CASE WHEN NOT (COALESCE(o.rssi, 0) = 0 AND COALESCE(o.snr, 0) = 0) THEN o.rssi END AS rssi) s WHERE o.observer_id = $1 AND o.heard_at >= GREATEST($2::timestamptz, @tail_start::timestamptz) AND o.heard_at < @until::timestamptz ) SELECT date_bin($3::interval, t, TIMESTAMPTZ 'epoch')::timestamptz AS bucket, SUM(observations)::bigint AS observations, COALESCE(SUM(airtime_ms), 0)::real AS airtime_ms, SUM(airtime_n)::bigint AS airtime_n, COALESCE(SUM(snr_sum), 0)::real AS snr_sum, SUM(snr_n)::bigint AS snr_n, COALESCE(MIN(snr_min), 0)::real AS snr_min, COALESCE(SUM(rssi_sum), 0)::bigint AS rssi_sum, SUM(rssi_n)::bigint AS rssi_n FROM src GROUP BY 1 ORDER BY 1; -- name: GetObserverActivityHourlyPayloadTypes :many -- Same rollup/raw-tail split as GetObserverActivityHourly. WITH src AS ( SELECT a.payload_type, a.observations AS n FROM analytics_hourly_observer_activity a WHERE a.observer_id = $1 AND a.hour >= $2::timestamptz AND a.hour < LEAST(@until::timestamptz, @tail_start::timestamptz) UNION ALL SELECT COALESCE(o.payload_type, -1)::smallint, 1::bigint FROM packet_observations o WHERE o.observer_id = $1 AND o.heard_at >= GREATEST($2::timestamptz, @tail_start::timestamptz) AND o.heard_at < @until::timestamptz ) SELECT payload_type, SUM(n)::bigint AS count FROM src GROUP BY payload_type ORDER BY count DESC; -- name: ListObserverAdverts :many -- Returns advert packets (payload_type=4) heard by a specific observer. -- Pass cursor=0 to start from the beginning, or the last seen id for pagination. -- Keep missing-origin adverts; the generated key field expects a string, not NULL. SELECT po.id, encode(po.packet_hash, 'hex') AS packet_hash_hex, p.payload_type, po.iata, po.heard_at, po.rssi, po.snr, po.hop_count, n.name AS node_name, COALESCE(encode(p.origin_pubkey, 'hex'), '')::text AS node_public_key FROM packet_observations po JOIN packets p ON p.packet_hash = po.packet_hash LEFT JOIN nodes n ON n.public_key = p.origin_pubkey WHERE po.observer_id = $1 AND p.payload_type = 4 AND ($2 = 0 OR po.id > $2) ORDER BY po.id ASC LIMIT $3; -- name: DeleteOldTelemetry :exec -- Deletes telemetry rows older than the given cutoff. Called by the cleanup goroutine. DELETE FROM observer_telemetry WHERE reported_at < $1; -- name: DeleteOldObservers :many -- Opt-in age-out: preserve retained history and manually recorded ownership. -- Bound deletions per cleanup tick and skip observers being updated by ingest. WITH expired AS ( SELECT o.id FROM observers o WHERE o.last_seen < $1 AND (o.last_status_at IS NULL OR o.last_status_at < $1) AND NOT EXISTS (SELECT 1 FROM packet_observations po WHERE po.observer_id = o.id) AND NOT EXISTS (SELECT 1 FROM observer_telemetry ot WHERE ot.observer_id = o.id) AND NOT EXISTS (SELECT 1 FROM observer_owners oo WHERE oo.observer_id = o.id) ORDER BY o.last_seen, o.id LIMIT 1000 FOR UPDATE OF o SKIP LOCKED ) DELETE FROM observers o USING expired e WHERE o.id = e.id RETURNING o.id; -- ============================================================ -- OBSERVER BROKERS -- ============================================================ -- name: UpsertObserverBroker :exec INSERT INTO observer_brokers (observer_id, broker_name, last_seen, last_packet_at) VALUES ($1, $2, NOW(), CASE WHEN @is_packet::boolean THEN NOW() END) ON CONFLICT (observer_id, broker_name) DO UPDATE SET last_seen = NOW(), last_packet_at = COALESCE(EXCLUDED.last_packet_at, observer_brokers.last_packet_at); -- name: TouchObserverBrokers :exec UPDATE observer_brokers ob SET last_seen = GREATEST(ob.last_seen, v.seen), last_packet_at = GREATEST(ob.last_packet_at, v.packet) FROM ( SELECT unnest($1::uuid[]) AS observer_id, unnest($2::text[]) AS broker_name, unnest($3::timestamptz[]) AS seen, unnest($4::timestamptz[]) AS packet ) v WHERE ob.observer_id = v.observer_id AND ob.broker_name = v.broker_name; -- ============================================================ -- PACKETS -- ============================================================ -- name: UpsertPacket :one -- A new TRACE packet adds itself to its tag summary in the same statement, so the summary -- can't miss a stored packet. 'TRACE' > 'PING'; the longest path keeps the best payload. WITH up AS ( INSERT INTO packets ( packet_hash, payload_type, payload_version, route_type, transport_codes_present, region_code, sub_region_code, origin_pubkey, raw_payload, raw_header, parsed_payload, channel_hash, scope_id, trace_tag, first_heard_at, last_heard_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, NOW(), NOW() ) ON CONFLICT (packet_hash) DO UPDATE SET last_heard_at = NOW() RETURNING packet_hash, payload_type, payload_version, route_type, transport_codes_present, region_code, sub_region_code, origin_pubkey, raw_payload, raw_header, parsed_payload, decrypted, channel_hash, first_heard_at, last_heard_at, trace_tag, scope_id, (xmax = 0) AS inserted ), trace AS ( INSERT INTO trace_tags (trace_tag, first_heard_at, last_heard_at, packet_count, trace_type, scope_id, best_payload, best_path_len) SELECT u.trace_tag, u.first_heard_at, u.last_heard_at, 1, u.parsed_payload->>'type', u.scope_id, u.parsed_payload, CASE WHEN jsonb_typeof(u.parsed_payload->'pathHashes') = 'array' THEN jsonb_array_length(u.parsed_payload->'pathHashes') ELSE 0 END FROM up u WHERE u.inserted AND u.trace_tag IS NOT NULL ON CONFLICT (trace_tag) DO UPDATE SET packet_count = trace_tags.packet_count + 1, trace_type = GREATEST(trace_tags.trace_type, EXCLUDED.trace_type), scope_id = COALESCE(trace_tags.scope_id, EXCLUDED.scope_id), best_payload = CASE WHEN EXCLUDED.best_path_len > trace_tags.best_path_len THEN EXCLUDED.best_payload ELSE trace_tags.best_payload END, best_path_len = GREATEST(trace_tags.best_path_len, EXCLUDED.best_path_len) ) SELECT packet_hash, payload_type, payload_version, route_type, transport_codes_present, region_code, sub_region_code, origin_pubkey, raw_payload, raw_header, parsed_payload, decrypted, channel_hash, first_heard_at, last_heard_at, inserted FROM up; -- name: SetPacketDecrypted :exec UPDATE packets SET decrypted = true WHERE packet_hash = $1; -- name: TouchPackets :exec UPDATE packets p SET last_heard_at = GREATEST(p.last_heard_at, v.heard) FROM ( SELECT unnest($1::bytea[]) AS packet_hash, unnest($2::timestamptz[]) AS heard ) v WHERE p.packet_hash = v.packet_hash; -- name: GetPacketByHash :one SELECT p.*, ts.name AS scope_name, cm.sender_name AS cm_sender_name, cm.content AS cm_content, cm.sent_at AS cm_sent_at FROM packets p LEFT JOIN transport_scopes ts ON ts.id = p.scope_id LEFT JOIN channel_messages cm ON cm.packet_hash = p.packet_hash WHERE p.packet_hash = $1; -- name: GetPacketsByTraceTag :many -- Return distinct observation IATAs in first-heard order for path resolution, -- without fetching full observations separately for every trace packet. SELECT encode(p.packet_hash, 'hex') AS packet_hash_hex, p.route_type, p.first_heard_at, p.last_heard_at, p.parsed_payload, p.scope_id, ts.name AS scope_name, ARRAY( SELECT po.iata FROM packet_observations po WHERE po.packet_hash = p.packet_hash GROUP BY po.iata ORDER BY MIN(po.heard_at), po.iata )::bpchar[] AS iatas FROM packets p LEFT JOIN transport_scopes ts ON ts.id = p.scope_id WHERE p.trace_tag = decode($1, 'hex') ORDER BY p.first_heard_at ASC; -- name: ListPackets :many -- Returns packets with the latest observation rolled in for display. -- Pass cursor=0 to start from the beginning. IATA-filtered requests are -- served by ListPacketsByIATAs instead. SELECT p.packet_hash, p.payload_type, COALESCE(CASE WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' THEN p.parsed_payload #>> '{appData,name}' WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') END, '')::text AS summary, p.route_type, p.first_heard_at, p.last_heard_at, p.scope_id, ts.name AS scope_name, p.observation_count AS observation_count, -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. po.observer_id AS latest_observer_id, o.display_name AS latest_observer_name, COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, po.path_bytes AS latest_observer_path_bytes, p.raw_payload, p.origin_pubkey FROM packets p LEFT JOIN LATERAL ( SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes FROM packet_observations WHERE packet_hash = p.packet_hash ORDER BY heard_at DESC LIMIT 1 ) po ON true LEFT JOIN observers o ON o.id = po.observer_id LEFT JOIN transport_scopes ts ON ts.id = p.scope_id WHERE (COALESCE(cardinality($1::smallint[]), 0) = 0 OR p.payload_type = ANY($1::smallint[])) AND (COALESCE(cardinality($2::smallint[]), 0) = 0 OR p.route_type = ANY($2::smallint[])) AND ($3::timestamptz IS NULL OR p.first_heard_at >= $3) AND ($4::timestamptz IS NULL OR p.first_heard_at <= $4) AND ($5::timestamptz IS NULL OR p.last_heard_at < $5) AND (COALESCE(cardinality($7::text[]), 0) = 0 OR ts.name = ANY($7::text[])) ORDER BY p.last_heard_at DESC LIMIT $6; -- name: ListPacketsByIATAs :many -- IATA-filtered packet list, driven from idx_observations_iata_heard. -- Walking packets newest-first and probing for the site probes ~589k packets -- to fill a page for a quiet site; walking the site's own observation log is -- proportional to the page size instead. Results are ordered by when the -- requested sites heard the packet (site-local recency) and the cursor -- follows that ordering. scan_depth caps how deep each site's observation -- log is walked. A packet repeats once per observer that heard it, so a -- page can collapse to fewer distinct packets than were asked for without -- the site being exhausted. scan_saturated reports whether any site hit -- that cap and scan_floor the oldest heard_at they all cover, so a short -- page can keep paging instead of reading as the end of the data. WITH scanned AS ( SELECT req.iata AS req_iata, hits.packet_hash, hits.heard_at FROM unnest(@iatas::bpchar[]) AS req(iata) CROSS JOIN LATERAL ( SELECT po3.packet_hash, po3.heard_at FROM packet_observations po3 JOIN packets p2 ON p2.packet_hash = po3.packet_hash WHERE po3.iata = req.iata AND po3.heard_at < COALESCE(@cursor_ts::timestamptz, 'infinity'::timestamptz) AND (COALESCE(cardinality(@payload_types::smallint[]), 0) = 0 OR p2.payload_type = ANY(@payload_types::smallint[])) AND (COALESCE(cardinality(@route_types::smallint[]), 0) = 0 OR p2.route_type = ANY(@route_types::smallint[])) AND (@since_ts::timestamptz IS NULL OR p2.first_heard_at >= @since_ts) AND (@until_ts::timestamptz IS NULL OR p2.first_heard_at <= @until_ts) AND (COALESCE(cardinality(@scope_names::text[]), 0) = 0 OR EXISTS ( SELECT 1 FROM transport_scopes ts2 WHERE ts2.id = p2.scope_id AND ts2.name = ANY(@scope_names::text[]))) ORDER BY po3.heard_at DESC LIMIT @scan_depth ) hits ), -- A site that filled scan_depth still has unread history below its floor. -- The newest such floor is the point above which every site is covered. saturation AS ( SELECT COUNT(*) > 0 AS scan_saturated, MAX(floor_ts)::timestamptz AS scan_floor FROM ( SELECT MIN(heard_at) AS floor_ts FROM scanned GROUP BY req_iata HAVING COUNT(*) >= @scan_depth ) filled ), page AS ( SELECT scanned.packet_hash, MAX(scanned.heard_at)::timestamptz AS site_heard_at FROM scanned GROUP BY scanned.packet_hash HAVING (@cursor_ts::timestamptz IS NULL OR NOT EXISTS ( SELECT 1 FROM packet_observations px WHERE px.packet_hash = scanned.packet_hash AND px.iata = ANY(@iatas::bpchar[]) AND px.heard_at >= @cursor_ts)) ORDER BY site_heard_at DESC LIMIT @page_limit ) SELECT p.packet_hash, p.payload_type, COALESCE(CASE WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' THEN p.parsed_payload #>> '{appData,name}' WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') END, '')::text AS summary, p.route_type, p.first_heard_at, p.last_heard_at, p.scope_id, ts.name AS scope_name, sh.site_heard_at, sat.scan_saturated, sat.scan_floor, p.observation_count AS observation_count, -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. po.observer_id AS latest_observer_id, o.display_name AS latest_observer_name, COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, po.path_bytes AS latest_observer_path_bytes, p.raw_payload, p.origin_pubkey FROM page sh CROSS JOIN saturation sat JOIN packets p ON p.packet_hash = sh.packet_hash LEFT JOIN LATERAL ( SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes FROM packet_observations WHERE packet_hash = p.packet_hash ORDER BY heard_at DESC LIMIT 1 ) po ON true LEFT JOIN observers o ON o.id = po.observer_id LEFT JOIN transport_scopes ts ON ts.id = p.scope_id ORDER BY sh.site_heard_at DESC; -- name: ListPacketsAfterID :many -- Returns packets with observations after the given observation ID, ordered oldest first. -- Used for WS reconnect backfill. Pass afterObservationId=0 to start from the beginning. SELECT p.packet_hash, p.payload_type, COALESCE(CASE WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' THEN p.parsed_payload #>> '{appData,name}' WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') END, '')::text AS summary, p.route_type, p.first_heard_at, p.last_heard_at, p.observation_count AS observation_count, po.observer_id AS latest_observer_id, o.display_name AS latest_observer_name, po.iata AS latest_observer_iata, po.path_length_byte AS latest_observer_path_length_byte, po.hash_size AS latest_observer_hash_size, po.hop_count AS latest_observer_hop_count, po.path_bytes AS latest_observer_path_bytes, p.raw_payload, p.origin_pubkey, ts.name AS scope_name FROM packets p JOIN packet_observations po ON po.packet_hash = p.packet_hash LEFT JOIN observers o ON o.id = po.observer_id LEFT JOIN transport_scopes ts ON ts.id = p.scope_id WHERE po.id > $1 AND ($2::smallint = -1 OR p.payload_type = $2::smallint) AND ($3::smallint = -1 OR p.route_type = $3::smallint) AND (COALESCE(cardinality($4::bpchar[]), 0) = 0 OR po.iata = ANY($4::bpchar[])) AND ($5::text = '' OR ts.name = $5::text) ORDER BY po.id ASC LIMIT $6; -- name: DeleteOldPackets :one -- Deletes one bounded cohort (observations cascade). SKIP LOCKED leaves packets an in-flight -- observation insert holds. raw_deleted_before tells the rollup which hours lost raw rows. WITH victims AS MATERIALIZED ( SELECT vp.packet_hash, vp.last_heard_at FROM packets vp WHERE vp.last_heard_at < @cutoff::timestamptz ORDER BY vp.last_heard_at, vp.packet_hash LIMIT @batch_size::integer FOR UPDATE OF vp SKIP LOCKED ), deleted AS ( DELETE FROM packets dp USING victims v WHERE dp.packet_hash = v.packet_hash RETURNING dp.last_heard_at ), mark AS ( UPDATE analytics_raw_state SET raw_deleted_before = GREATEST(raw_deleted_before, (SELECT max(last_heard_at) FROM deleted)) WHERE EXISTS (SELECT 1 FROM deleted) ) SELECT count(*) FROM deleted; -- name: DeleteOldNodes :exec -- Deletes nodes not seen since the given cutoff. node_iatas and node_neighbors cascade- -- delete via FK. Excludes nodes referenced by observer_owners.owner_node_id -- that FK has -- no ON DELETE action, so deleting one directly would fail the whole statement anyway, and -- an operator manually recorded ownership for that node, so leave it alone even if stale. -- known_routes.node_ids is a plain UUID[] with no FK; a deleted node's id can be left -- dangling in old routes there, but ReconfirmTask already prunes stale/ambiguous routes -- periodically and will clean those up on its own schedule. DELETE FROM nodes WHERE last_seen < $1 AND id NOT IN (SELECT owner_node_id FROM observer_owners WHERE owner_node_id IS NOT NULL); -- name: DeleteOldRoutes :execrows -- One batch of routes past retention, or past grace with too few observations. -- GREATEST keeps the scan on idx_known_routes_last_seen. WITH expired AS ( SELECT r.iata, r.path_key FROM known_routes r WHERE r.last_seen < GREATEST(@retention_cutoff::timestamptz, @grace_cutoff::timestamptz) AND (r.last_seen < @retention_cutoff OR (r.observation_count < @min_observations AND r.last_seen < @grace_cutoff)) LIMIT @batch_size FOR UPDATE OF r SKIP LOCKED ) DELETE FROM known_routes kr USING expired e WHERE kr.iata = e.iata AND kr.path_key = e.path_key; -- name: DeleteOldChannelIATAs :exec -- Keeps the channel IATA filter in step with packet retention. DELETE FROM channel_iatas WHERE last_heard < $1; -- name: DeleteOldTraceIATAs :exec -- Keeps the trace IATA filter in step with packet retention. DELETE FROM trace_iatas WHERE last_heard < $1; -- ============================================================ -- PACKET OBSERVATIONS -- ============================================================ -- name: InsertObservation :one -- Bumps packets.observation_count only for a new row; a duplicate returns the current count. WITH ins AS ( 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, payload_type, airtime_ms ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18 ) ON CONFLICT (packet_hash, observer_id) DO NOTHING RETURNING packet_hash ), bump AS ( UPDATE packets p SET observation_count = p.observation_count + 1 FROM ins WHERE p.packet_hash = ins.packet_hash RETURNING p.observation_count ) SELECT EXISTS (SELECT 1 FROM ins) AS inserted, COALESCE((SELECT b.observation_count FROM bump b), (SELECT pk.observation_count FROM packets pk WHERE pk.packet_hash = $1), 0)::bigint AS observation_count; -- name: ListObservationsForPacket :many SELECT po.*, o.display_name AS observer_name FROM packet_observations po LEFT JOIN observers o ON o.id = po.observer_id WHERE po.packet_hash = $1 ORDER BY po.heard_at ASC; -- ============================================================ -- NODES -- ============================================================ -- name: UpsertNode :one INSERT INTO nodes (public_key, node_type, name, latitude, longitude, location_source, last_advert_at, last_seen, radio_freq_mhz, radio_sf, radio_bw_khz, device_clock_drift_seconds) VALUES (@public_key, @node_type, @name, @latitude, @longitude, CASE WHEN @latitude::double precision IS NOT NULL THEN 'advert' END, NOW(), NOW(), @radio_freq_mhz, @radio_sf, @radio_bw_khz, @device_clock_drift_seconds) ON CONFLICT (public_key) DO UPDATE SET node_type = EXCLUDED.node_type, name = COALESCE(EXCLUDED.name, nodes.name), latitude = CASE WHEN @clear_location::bool THEN NULL ELSE COALESCE(EXCLUDED.latitude, nodes.latitude) END, longitude = CASE WHEN @clear_location::bool THEN NULL ELSE COALESCE(EXCLUDED.longitude, nodes.longitude) END, location_source = CASE WHEN @clear_location::bool THEN NULL WHEN EXCLUDED.latitude IS NOT NULL THEN 'advert' ELSE nodes.location_source END, last_advert_at = NOW(), last_seen = NOW(), radio_freq_mhz = EXCLUDED.radio_freq_mhz, radio_sf = EXCLUDED.radio_sf, radio_bw_khz = EXCLUDED.radio_bw_khz, device_clock_drift_seconds = EXCLUDED.device_clock_drift_seconds 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: SetNodeDefaultScope :exec UPDATE nodes SET default_scope_id = $2 WHERE id = $1; -- name: GetNodeByID :one SELECT n.*, ts.name AS default_scope_name, EXISTS (SELECT 1 FROM observers o WHERE o.public_key = n.public_key) AS is_observer, (SELECT o.id FROM observers o WHERE o.public_key = n.public_key LIMIT 1) AS observer_id, (SELECT json_agg(json_build_object('iata', ni.iata, 'lastHeard', (extract(epoch from ni.last_heard) * 1000)::bigint) ORDER BY ni.last_heard DESC) FROM node_iatas ni WHERE ni.node_id = n.id) AS iatas, (SELECT COUNT(DISTINCT nn.neighbor_id) FROM node_neighbors nn WHERE nn.node_id = n.id)::bigint AS known_neighbor_count FROM nodes n LEFT JOIN transport_scopes ts ON ts.id = n.default_scope_id WHERE n.id = $1; -- name: GetNodesByIDs :many SELECT id, public_key, name, latitude, longitude FROM nodes WHERE id = ANY($1::uuid[]); -- name: GetNodeByPubkey :one SELECT id FROM nodes WHERE public_key = $1; -- name: GetNodesByPubkeys :many SELECT id, public_key, name, latitude, longitude FROM nodes WHERE public_key = ANY(@pubkeys::bytea[]); -- name: ListNodes :many -- Limit the filtered node page before enriching IATA membership and neighbours. WITH page AS ( SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_seen, n.radio_freq_mhz, n.radio_sf, n.radio_bw_khz, ts.name AS default_scope_name FROM nodes n LEFT JOIN transport_scopes ts ON ts.id = n.default_scope_id WHERE ($1 = 0 OR n.node_type = $1) AND (COALESCE(cardinality($2::bpchar[]), 0) = 0 OR n.id IN (SELECT node_id FROM node_iatas WHERE iata = ANY($2::bpchar[]))) AND ( $3::text = 'any' OR ($3::text = 'true' AND n.supports_multibyte_paths = TRUE) OR ($3::text = 'false' AND n.supports_multibyte_paths = FALSE) ) AND ( $4::text = 'any' OR ($4::text = 'true' AND n.supports_multibyte_traces = TRUE) OR ($4::text = 'false' AND n.supports_multibyte_traces = FALSE) ) AND ($5::bytea IS NULL OR n.public_key = $5) AND ($6 = '' OR n.name ILIKE '%' || $6 || '%') AND ($7::timestamptz IS NULL OR n.last_seen < $7) AND ($9::text = '' OR ts.name = $9::text) AND ($11::text = '' OR encode(n.public_key, 'hex') ILIKE $11 || '%') ORDER BY n.last_seen DESC LIMIT $8 ) SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_seen, n.radio_freq_mhz, n.radio_sf, n.radio_bw_khz, n.default_scope_name, (SELECT json_agg(json_build_object('iata', ni.iata, 'lastHeard', (extract(epoch from ni.last_heard) * 1000)::bigint) ORDER BY ni.last_heard DESC) FROM node_iatas ni WHERE ni.node_id = n.id) AS iatas, EXISTS (SELECT 1 FROM observers o WHERE o.public_key = n.public_key) AS is_observer, (SELECT o.id FROM observers o WHERE o.public_key = n.public_key LIMIT 1) AS observer_id, (SELECT COUNT(DISTINCT nn.neighbor_id) FROM node_neighbors nn WHERE nn.node_id = n.id)::bigint AS known_neighbor_count, (CASE WHEN $10::bool THEN (SELECT COALESCE(array_agg(DISTINCT nn.neighbor_id), '{}'::uuid[]) FROM node_neighbors nn WHERE nn.node_id = n.id) ELSE NULL END)::uuid[] AS neighbor_ids FROM page n ORDER BY n.last_seen DESC; -- name: ListNodeObservations :many SELECT po.id, encode(po.packet_hash, 'hex') AS packet_hash_hex, p.payload_type, po.iata, po.heard_at, po.rssi, po.snr, po.hop_count FROM packet_observations po JOIN packets p ON p.packet_hash = po.packet_hash JOIN nodes n ON n.public_key = p.origin_pubkey WHERE n.id = $1 AND ($2 = 0 OR po.id < $2) ORDER BY po.id DESC LIMIT $3; -- ============================================================ -- 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) 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: ListUndecryptedGroupTextPackets :many -- Returns GRP_TXT packets (payload_type=5) never successfully decrypted. Used at boot to -- retry decryption against the current keystore for packets whose channel key was only added -- to the config after they'd already been ingested -- see -- internal/ingest.BackfillChannelMessages. SELECT packet_hash, raw_payload FROM packets WHERE payload_type = 5 AND decrypted IS NOT TRUE; -- name: ListUndecryptedGroupTextPacketsByHash :many -- Like ListUndecryptedGroupTextPackets, limited to channels that just gained a key. SELECT packet_hash, raw_payload FROM packets WHERE payload_type = 5 AND decrypted IS NOT TRUE AND channel_hash = ANY(@hashes::bytea[]); -- name: DeleteChannelConfigScopes :exec DELETE FROM channel_config_scopes; -- name: AddChannelConfigScopes :exec -- An empty region places the channel Beacon-wide. INSERT INTO channel_config_scopes (key_fingerprint, region_slug) SELECT unnest(@fingerprints::bytea[]), NULLIF(unnest(@regions::text[]), ''); -- name: UpsertChannelIATA :exec -- Refreshes at most hourly so repeat hears don't churn the row. INSERT INTO channel_iatas (channel_hash, iata, last_heard) VALUES ($1, $2, $3) ON CONFLICT (channel_hash, iata) DO UPDATE SET last_heard = EXCLUDED.last_heard WHERE EXCLUDED.last_heard > channel_iatas.last_heard + INTERVAL '1 hour'; -- name: RecordTrace :exec -- One hearing of a TRACE packet: trace_iatas refreshes at most hourly and the tag's heard -- window widens (the first hearing replaces the packet's provisional times). Packet counts -- and payloads come from UpsertPacket. WITH iata AS ( INSERT INTO trace_iatas (trace_tag, iata, last_heard) VALUES (@trace_tag, @iata, @heard_at) ON CONFLICT (trace_tag, iata) DO UPDATE SET last_heard = EXCLUDED.last_heard WHERE EXCLUDED.last_heard > trace_iatas.last_heard + INTERVAL '1 hour' ) INSERT INTO trace_tags (trace_tag, first_heard_at, last_heard_at, heard) VALUES (@trace_tag, @heard_at, @heard_at, true) ON CONFLICT (trace_tag) DO UPDATE SET first_heard_at = CASE WHEN trace_tags.heard THEN LEAST(trace_tags.first_heard_at, EXCLUDED.first_heard_at) ELSE EXCLUDED.first_heard_at END, last_heard_at = CASE WHEN trace_tags.heard THEN GREATEST(trace_tags.last_heard_at, EXCLUDED.last_heard_at) ELSE EXCLUDED.last_heard_at END, heard = true; -- name: DeleteOldTraceTags :exec DELETE FROM trace_tags WHERE last_heard_at < $1; -- name: ListChannels :many -- Channels ordered by last seen, optionally filtered by hash and/or IATAs. -- A channel belongs to an IATA when MeshMapper lists it there or config scopes it -- to a region containing it (or Beacon-wide). NULL hash / empty array / NULL key_known skip those filters. -- Pass cursor=0 to start from the beginning (cursor is last_seen epoch ms). SELECT c.* FROM channels c WHERE (@channel_hash::bytea IS NULL OR c.channel_hash = @channel_hash) AND (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR EXISTS (SELECT 1 FROM meshmapper_channel_members m WHERE m.key_fingerprint = c.key_fingerprint AND m.iata::bpchar = ANY(@iatas::bpchar[])) OR EXISTS (SELECT 1 FROM channel_config_scopes s WHERE s.key_fingerprint = c.key_fingerprint AND (s.region_slug IS NULL OR s.region_slug IN ( SELECT r.slug FROM regions r JOIN region_iatas ri ON ri.region_id = r.id WHERE ri.iata = ANY(@iatas::bpchar[]))))) AND (sqlc.narg(key_known)::boolean IS NULL OR COALESCE(c.key_known, false) = sqlc.narg(key_known)) AND (@cursor_ts::timestamptz IS NULL OR c.last_seen < @cursor_ts) ORDER BY c.last_seen DESC, c.id DESC LIMIT @page_limit; -- name: ListChannelsAfter :many -- Keep the non-null tuple boundary separate from the legacy optional cursor so -- generic prepared plans can seek directly into the composite ordered index. SELECT c.* FROM channels c WHERE (c.last_seen, c.id) < (@cursor_ts::timestamptz, @cursor_id::integer) AND (@channel_hash::bytea IS NULL OR c.channel_hash = @channel_hash) AND (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR EXISTS (SELECT 1 FROM meshmapper_channel_members m WHERE m.key_fingerprint = c.key_fingerprint AND m.iata::bpchar = ANY(@iatas::bpchar[])) OR EXISTS (SELECT 1 FROM channel_config_scopes s WHERE s.key_fingerprint = c.key_fingerprint AND (s.region_slug IS NULL OR s.region_slug IN ( SELECT r.slug FROM regions r JOIN region_iatas ri ON ri.region_id = r.id WHERE ri.iata = ANY(@iatas::bpchar[]))))) AND (sqlc.narg(key_known)::boolean IS NULL OR COALESCE(c.key_known, false) = sqlc.narg(key_known)) ORDER BY c.last_seen DESC, c.id DESC LIMIT @page_limit; -- name: GetChannelByID :one SELECT * FROM channels WHERE id = $1; -- ============================================================ -- CHANNEL MESSAGES -- ============================================================ -- name: InsertChannelMessage :one -- Read the immutable first-packet scope in the same statement as insertion. -- A later reception's transport code must not give live and historical messages different tags. -- message_count is a lifetime count, bumped only for a new message. A historical (backfilled) -- message queues its observation hours for a re-roll in the same statement, so the two can't diverge. WITH inserted AS ( 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, packet_hash, channel_id ), bump AS ( UPDATE channels c SET message_count = c.message_count + 1 FROM inserted WHERE c.id = inserted.channel_id ), dirty AS ( INSERT INTO analytics_dirty_hours (hour) SELECT DISTINCT date_trunc('hour', po.heard_at, 'UTC') FROM packet_observations po JOIN inserted i ON i.packet_hash = po.packet_hash WHERE $6::boolean ON CONFLICT (hour) DO UPDATE SET enqueued_at = now() ) SELECT inserted.id, ts.name AS scope_name, p.transport_codes_present FROM inserted JOIN packets p ON p.packet_hash = inserted.packet_hash LEFT JOIN transport_scopes ts ON ts.id = p.scope_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. -- Pass cursor=0 to start from the beginning. SELECT DISTINCT ON (cm.id) cm.*, encode(cm.packet_hash, 'hex') as packet_hash_hex, c.channel_hash, ts.name AS scope_name, p.transport_codes_present, p.observation_count AS observation_count FROM channel_messages cm JOIN channels c ON c.id = cm.channel_id JOIN packet_observations po ON po.packet_hash = cm.packet_hash JOIN packets p ON p.packet_hash = cm.packet_hash LEFT JOIN transport_scopes ts ON ts.id = p.scope_id WHERE cm.channel_id = $1 AND ($2::timestamptz IS NULL OR cm.sent_at >= $2) AND (COALESCE(cardinality($3::bpchar[]), 0) = 0 OR po.iata = ANY($3::bpchar[])) AND ($4::text = '' OR ts.name = $4::text) AND ($5::bigint = 0 OR cm.id < $5::bigint) ORDER BY cm.id DESC LIMIT $6; -- name: ListAllChannelMessages :many -- Returns all messages across all channels with optional time, IATA, scope and cursor filters. -- Pass empty string for iata or scope to skip those filters. -- Pass cursor=0 to start from the beginning. SELECT DISTINCT ON (cm.id) cm.*, encode(cm.packet_hash, 'hex') as packet_hash_hex, c.channel_hash, ts.name AS scope_name, p.transport_codes_present, p.observation_count AS observation_count FROM channel_messages cm JOIN channels c ON c.id = cm.channel_id JOIN packet_observations po ON po.packet_hash = cm.packet_hash JOIN packets p ON p.packet_hash = cm.packet_hash LEFT JOIN transport_scopes ts ON ts.id = p.scope_id WHERE ($1::timestamptz IS NULL OR cm.sent_at >= $1) AND (COALESCE(cardinality($2::bpchar[]), 0) = 0 OR po.iata = ANY($2::bpchar[])) AND ($3::text = '' OR ts.name = $3::text) AND ($4 = 0 OR cm.id < $4) ORDER BY cm.id DESC LIMIT $5; -- 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 or scope to skip those filters. -- Pass cursor=0 to start from the beginning. SELECT DISTINCT ON (cm.id) cm.*, c.channel_hash, ts.name AS scope_name, p.transport_codes_present, p.observation_count AS observation_count FROM channel_messages cm JOIN channels c ON c.id = cm.channel_id JOIN packet_observations po ON po.packet_hash = cm.packet_hash JOIN packets p ON p.packet_hash = cm.packet_hash LEFT JOIN transport_scopes ts ON ts.id = p.scope_id WHERE c.channel_hash = $1 AND ($2::timestamptz IS NULL OR cm.sent_at >= $2) AND (COALESCE(cardinality($3::bpchar[]), 0) = 0 OR po.iata = ANY($3::bpchar[])) AND ($4::text = '' OR ts.name = $4::text) AND ($5::bigint = 0 OR cm.id < $5::bigint) ORDER BY cm.id DESC LIMIT $6; -- name: ListMessagesAfterID :many -- Returns messages after the given message ID, ordered oldest first. -- Used for WS reconnect backfill. SELECT DISTINCT ON (cm.id) cm.*, encode(cm.packet_hash, 'hex') as packet_hash_hex, c.channel_hash, ts.name AS scope_name, p.transport_codes_present, p.observation_count AS observation_count FROM channel_messages cm JOIN channels c ON c.id = cm.channel_id JOIN packet_observations po ON po.packet_hash = cm.packet_hash JOIN packets p ON p.packet_hash = cm.packet_hash LEFT JOIN transport_scopes ts ON ts.id = p.scope_id WHERE cm.id > $1 AND (COALESCE(cardinality($2::bpchar[]), 0) = 0 OR po.iata = ANY($2::bpchar[])) AND ($3::text = '' OR ts.name = $3::text) ORDER BY cm.id ASC LIMIT $4; -- ============================================================ -- STATS -- ============================================================ -- name: GetHourlyStats :many SELECT iata, hour, observation_count FROM analytics_hourly_iata_observations WHERE (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR iata = ANY(@iatas::bpchar[])) AND hour >= @since::timestamptz ORDER BY iata, hour; -- name: GetTopNodes :many -- Nodes by ADVERT hearings since the given hour. Live names win over the rolled snapshot; -- node_id is NULL once the node row is gone. iata is a representative one. WITH ranked AS ( SELECT origin_pubkey, SUM(observations)::bigint AS observation_count, MAX(last_heard) AS last_heard, MAX(iata) AS iata, MAX(name) AS name, MAX(node_type) AS node_type FROM analytics_hourly_advert_hearings WHERE hour >= @since::timestamptz AND (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR iata = ANY(@iatas::bpchar[])) GROUP BY origin_pubkey ORDER BY observation_count DESC, origin_pubkey LIMIT @row_limit ) SELECT n.id AS node_id, encode(r.origin_pubkey, 'hex') AS public_key, COALESCE(n.name, r.name, '')::text AS name, COALESCE(n.node_type, r.node_type, 0)::smallint AS node_type, r.iata::bpchar AS iata, r.observation_count, r.last_heard::timestamptz AS last_heard FROM ranked r LEFT JOIN nodes n ON n.public_key = r.origin_pubkey ORDER BY r.observation_count DESC, r.origin_pubkey; -- name: GetStatsPayloadBreakdown :many -- Payload-type observation counts since the given hour, from the hourly rollup. SELECT payload_type, SUM(count)::bigint AS count FROM analytics_hourly_payload_breakdown WHERE (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR iata = ANY(@iatas::bpchar[])) AND hour >= @since::timestamptz GROUP BY payload_type ORDER BY count DESC; -- name: GetStatsNodeTypes :many -- Returns node counts grouped by type, optionally filtered by IATA. SELECT n.node_type, COUNT(DISTINCT n.id)::bigint AS count FROM nodes n LEFT JOIN node_iatas ni ON ni.node_id = n.id WHERE (COALESCE(cardinality($1::bpchar[]), 0) = 0 OR ni.iata = ANY($1::bpchar[])) GROUP BY n.node_type ORDER BY count DESC; -- name: GetStatsTopObservers :many -- Top N observers since the given hour. Counts sum across matched IATAs; iata is a -- representative one. Live names win over the rolled snapshot. WITH ranked AS ( SELECT observer_id, SUM(observation_count)::bigint AS observation_count, MAX(iata) AS iata, MAX(display_name) AS display_name, MAX(observer_type) AS observer_type FROM analytics_hourly_observer_identity WHERE hour >= @since::timestamptz AND (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR iata = ANY(@iatas::bpchar[])) GROUP BY observer_id ORDER BY observation_count DESC, observer_id LIMIT @row_limit ) SELECT r.observer_id AS id, COALESCE(o.display_name, r.display_name, '')::text AS display_name, COALESCE(o.observer_type, r.observer_type, '')::text AS observer_type, r.observation_count, r.iata::bpchar AS iata FROM ranked r LEFT JOIN observers o ON o.id = r.observer_id ORDER BY r.observation_count DESC, r.observer_id; -- name: GetStatsTopAdvertisers :many -- Top N advertisers since the given hour. Each ADVERT packet counts once per hour heard, -- however many of the requested IATAs heard it (IATA-set rollup). Live names win. WITH ranked AS ( SELECT origin_pubkey, SUM(advert_packets)::bigint AS advert_count, SUM(flood_packets)::bigint AS flood_advert_count, SUM(direct_packets)::bigint AS direct_advert_count FROM analytics_hourly_advert_sets WHERE hour >= @since::timestamptz AND (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR iatas && @iatas::bpchar[]) GROUP BY origin_pubkey ORDER BY advert_count DESC, origin_pubkey LIMIT @row_limit ), heard AS ( SELECT h.origin_pubkey, MAX(h.last_heard) AS last_heard, MAX(h.iata) AS iata, MAX(h.name) AS name, MAX(h.node_type) AS node_type FROM analytics_hourly_advert_hearings h WHERE h.hour >= @since::timestamptz AND h.origin_pubkey IN (SELECT origin_pubkey FROM ranked) AND (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR h.iata = ANY(@iatas::bpchar[])) GROUP BY h.origin_pubkey ) SELECT n.id AS node_id, encode(r.origin_pubkey, 'hex') AS public_key, COALESCE(n.name, h.name, '')::text AS name, COALESCE(n.node_type, h.node_type, 0)::smallint AS node_type, r.advert_count, r.flood_advert_count, r.direct_advert_count, h.last_heard::timestamptz AS last_heard, COALESCE(h.iata, '')::bpchar AS iata FROM ranked r LEFT JOIN heard h ON h.origin_pubkey = r.origin_pubkey LEFT JOIN nodes n ON n.public_key = r.origin_pubkey ORDER BY r.advert_count DESC, r.origin_pubkey; -- name: GetStatsClockDrift :many -- Repeaters/room servers (node_type 2/3) whose current advert-derived clock drift exceeds -- the given threshold in magnitude, worst first. Not time-windowed -- reflects each node's -- latest measured drift, not an aggregate over a period. -- Select the filtered page before aggregating its IATA memberships. WITH page AS ( SELECT n.id, n.name, n.node_type, n.device_clock_drift_seconds, n.last_advert_at FROM nodes n WHERE n.node_type IN (2, 3) AND n.device_clock_drift_seconds IS NOT NULL AND ABS(n.device_clock_drift_seconds) > $1::int AND (COALESCE(cardinality($2::bpchar[]), 0) = 0 OR n.id IN (SELECT node_id FROM node_iatas WHERE iata = ANY($2::bpchar[]))) ORDER BY ABS(n.device_clock_drift_seconds) DESC LIMIT $3 ) SELECT n.id, n.name, n.node_type, n.device_clock_drift_seconds, n.last_advert_at, (SELECT json_agg(json_build_object('iata', ni.iata, 'lastHeard', (extract(epoch from ni.last_heard) * 1000)::bigint) ORDER BY ni.last_heard DESC) FROM node_iatas ni WHERE ni.node_id = n.id) AS iatas FROM page n ORDER BY ABS(n.device_clock_drift_seconds) DESC; -- name: GetStatsTopTalkers :many -- Top N senders since the given hour. Each message counts once per hour heard, however many -- of the requested IATAs heard it (IATA-set rollup). SELECT sender_name, SUM(messages)::bigint AS message_count, MAX(last_sent)::timestamptz AS last_sent FROM analytics_hourly_talker_sets WHERE hour >= @since::timestamptz AND (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR iatas && @iatas::bpchar[]) GROUP BY sender_name ORDER BY message_count DESC, sender_name LIMIT @row_limit; -- name: GetRadioPresets :many SELECT preset, iata, source_type, count FROM mv_radio_presets WHERE ($1::text = '' OR preset = $1::text) AND (COALESCE(cardinality($2::bpchar[]), 0) = 0 OR iata = ANY($2::bpchar[])) ORDER BY preset, iata, source_type; -- name: GetScopeStatsHourly :many -- GetScopeStats packet counts split by hour (same window and IATA-set filter), with the -- distinct observers and advertising nodes active in each scope that hour. Empty hours omitted. WITH pk AS ( SELECT s.scope_id, s.hour, SUM(s.packets)::bigint AS n FROM analytics_hourly_scope_sets s WHERE s.hour >= @since::timestamptz AND (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR s.iatas && @iatas::bpchar[]) GROUP BY s.scope_id, s.hour ), ob AS ( SELECT o.scope_id, o.hour, COUNT(DISTINCT o.observer_id)::bigint AS n FROM analytics_hourly_scope_observers o WHERE o.hour >= @since::timestamptz AND (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR o.iata = ANY(@iatas::bpchar[])) GROUP BY o.scope_id, o.hour ), nd AS ( SELECT d.scope_id, d.hour, COUNT(DISTINCT d.origin_pubkey)::bigint AS n FROM analytics_hourly_scope_nodes d WHERE d.hour >= @since::timestamptz AND (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR d.iata = ANY(@iatas::bpchar[])) GROUP BY d.scope_id, d.hour ), k AS ( SELECT scope_id, hour FROM pk UNION SELECT scope_id, hour FROM ob UNION SELECT scope_id, hour FROM nd ) SELECT ts.name, k.hour, COALESCE(pk.n, 0)::bigint AS packets, COALESCE(ob.n, 0)::bigint AS observers, COALESCE(nd.n, 0)::bigint AS nodes FROM k JOIN transport_scopes ts ON ts.id = k.scope_id LEFT JOIN pk ON pk.scope_id = k.scope_id AND pk.hour = k.hour LEFT JOIN ob ON ob.scope_id = k.scope_id AND ob.hour = k.hour LEFT JOIN nd ON nd.scope_id = k.scope_id AND nd.hour = k.hour WHERE COALESCE(pk.n, 0) > 0 OR COALESCE(ob.n, 0) > 0 OR COALESCE(nd.n, 0) > 0 ORDER BY ts.name, k.hour; -- name: GetScopeStats :many -- Packets since the given hour come from the IATA-set rollup (counted once per hour heard). -- Observer and node counts are current memberships; observers filter by their latest IATA. WITH packet_counts AS ( SELECT s.scope_id, SUM(s.packets)::bigint AS packet_count FROM analytics_hourly_scope_sets s WHERE s.hour >= @since::timestamptz AND (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR s.iatas && @iatas::bpchar[]) GROUP BY s.scope_id ), observer_counts AS ( SELECT os.scope_id, COUNT(*)::bigint AS observer_count FROM observer_scopes os JOIN observers o ON o.id = os.observer_id WHERE COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR o.last_iata = ANY(@iatas::bpchar[]) GROUP BY os.scope_id ), node_counts AS ( SELECT n.default_scope_id AS scope_id, COUNT(*)::bigint AS node_count FROM nodes n WHERE n.default_scope_id IS NOT NULL AND (COALESCE(cardinality(@iatas::bpchar[]), 0) = 0 OR n.id IN (SELECT node_id FROM node_iatas WHERE iata = ANY(@iatas::bpchar[]))) GROUP BY n.default_scope_id ) SELECT ts.name, COALESCE(pc.packet_count, 0)::bigint AS packet_count, COALESCE(oc.observer_count, 0)::bigint AS observer_count, COALESCE(nc.node_count, 0)::bigint AS node_count FROM transport_scopes ts LEFT JOIN packet_counts pc ON pc.scope_id = ts.id LEFT JOIN observer_counts oc ON oc.scope_id = ts.id LEFT JOIN node_counts nc ON nc.scope_id = ts.id ORDER BY ts.name; -- ============================================================ -- 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: GetRegionBySlug :one SELECT id, slug, name, description, center_lat, center_lng, zoom_level FROM regions WHERE slug = $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, imported = FALSE, -- config owns the slug from now on updated_at = NOW() RETURNING id; -- name: DeleteRegionIATAsNotIn :exec DELETE FROM region_iatas WHERE region_id = @region_id AND NOT (iata = ANY(@keep::bpchar[])); -- name: AddRegionIATAs :exec INSERT INTO region_iatas (region_id, iata) SELECT @region_id, unnest(@iatas::bpchar[]) ON CONFLICT (region_id, iata) DO NOTHING; -- name: ListRegionState :many -- Every region with its members, for reconciling imported MeshMapper groups. SELECT r.slug, r.name, COALESCE(r.display_order, 0)::int AS display_order, r.imported, r.center_lat, r.center_lng, COALESCE(array_agg(ri.iata::text ORDER BY ri.iata) FILTER (WHERE ri.iata IS NOT NULL), '{}')::text[] AS iatas FROM regions r LEFT JOIN region_iatas ri ON ri.region_id = r.id GROUP BY r.id ORDER BY r.slug; -- name: UpsertImportedRegion :one -- A hand-written region owns its slug: the WHERE turns a clash into no row. INSERT INTO regions (slug, name, display_order, center_lat, center_lng, zoom_level, imported, updated_at) VALUES (@slug, @name, @display_order, sqlc.narg(center_lat), sqlc.narg(center_lng), NULL, TRUE, NOW()) ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, display_order = EXCLUDED.display_order, center_lat = EXCLUDED.center_lat, center_lng = EXCLUDED.center_lng, updated_at = NOW() WHERE regions.imported RETURNING id; -- name: PruneImportedRegions :many DELETE FROM regions WHERE imported AND NOT (slug = ANY(@keep::text[])) RETURNING slug; -- ============================================================ -- TRACES -- ============================================================ -- name: ListTraceTags :many -- Tags, most recent first. Filters match the tag summary (see RecordTrace). SELECT encode(t.trace_tag, 'hex') AS trace_tag, t.first_heard_at, t.last_heard_at, t.packet_count, (SELECT COUNT(*) FROM trace_iatas ti WHERE ti.trace_tag = t.trace_tag AND (COALESCE(cardinality($1::bpchar[]), 0) = 0 OR ti.iata = ANY($1::bpchar[]))) AS iata_count, COALESCE(t.trace_type, '')::text AS trace_type, t.best_payload FROM trace_tags t WHERE (COALESCE(cardinality($1::bpchar[]), 0) = 0 OR EXISTS ( SELECT 1 FROM trace_iatas ti WHERE ti.trace_tag = t.trace_tag AND ti.iata = ANY($1::bpchar[]))) AND ($2::text = '' OR t.scope_id = (SELECT id FROM transport_scopes WHERE name = $2)) AND ($3::timestamptz IS NULL OR t.first_heard_at >= $3) AND ($4::timestamptz IS NULL OR t.first_heard_at <= $4) -- Keyset on the millisecond clients see, tie-broken by tag; matches idx_trace_tags_keyset. AND ($5::timestamptz IS NULL OR date_trunc('milliseconds', t.last_heard_at, 'UTC') < $5 OR ($8::bytea IS NOT NULL AND date_trunc('milliseconds', t.last_heard_at, 'UTC') = $5 AND t.trace_tag < $8)) AND ($7::text = '' OR t.trace_type = $7) ORDER BY date_trunc('milliseconds', t.last_heard_at, 'UTC') DESC, t.trace_tag DESC LIMIT $6; -- ============================================================ -- ROUTES -- ============================================================ -- name: UpsertKnownRoute :exec -- Route identity is path_key, an md5 of node_ids computed by the caller. -- On conflict, observation_count and last_seen are bumped and hash_prefix follows the -- latest hearing, so evidence matches the hash width the route uses now. INSERT INTO known_routes (path_key, node_ids, hash_prefix, iata, hop_count) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (iata, path_key) DO UPDATE SET hash_prefix = EXCLUDED.hash_prefix, last_seen = NOW(), observation_count = known_routes.observation_count + 1; -- name: ListKnownRoutes :many -- Only one branch runs. Keep the IATA range ordered by the composite index: -- generic plans can otherwise prefer scanning the global timestamp index. -- The text equality preserves exact input matching, including trailing spaces. -- Pages by (last_seen to the ms, id) so routes sharing the cursor's millisecond -- aren't skipped; cursor id 0 keeps the plain timestamp cursor. SELECT r.id, r.node_ids, r.hash_prefix, r.iata, r.hop_count, r.first_seen, r.last_seen, r.observation_count FROM (( SELECT id, node_ids, hash_prefix, iata, hop_count, first_seen, last_seen, observation_count FROM known_routes WHERE $1 = '' AND ($2 = 0 OR hop_count = $2) AND ($3::timestamptz IS NULL OR date_trunc('milliseconds', last_seen, 'UTC') < $3 OR ($5::bigint > 0 AND date_trunc('milliseconds', last_seen, 'UTC') = $3 AND id < $5)) ORDER BY date_trunc('milliseconds', last_seen, 'UTC') DESC, id DESC LIMIT $4 ) UNION ALL ( SELECT id, node_ids, hash_prefix, iata, hop_count, first_seen, last_seen, observation_count FROM known_routes WHERE $1 <> '' AND iata::text = $1 AND iata >= $1::bpchar AND iata <= $1::bpchar AND ($2 = 0 OR hop_count = $2) AND ($3::timestamptz IS NULL OR date_trunc('milliseconds', last_seen, 'UTC') < $3 OR ($5::bigint > 0 AND date_trunc('milliseconds', last_seen, 'UTC') = $3 AND id < $5)) ORDER BY iata, date_trunc('milliseconds', last_seen, 'UTC') DESC, id DESC LIMIT $4 )) r ORDER BY date_trunc('milliseconds', r.last_seen, 'UTC') DESC, r.id DESC LIMIT $4; -- name: GetKnownRoutesByNodes :many -- Routes containing @nodes[i] in IATA @iatas[i] (aligned, one node per IATA). The && -- drives the node_ids GIN index; the indexed check keeps LIMIT counting real matches. SELECT id, node_ids, hash_prefix, iata, hop_count, first_seen, last_seen, observation_count FROM known_routes kr WHERE kr.node_ids && @nodes::uuid[] AND kr.iata = ANY(@iatas::bpchar[]) AND (@nodes::uuid[])[array_position(@iatas::bpchar[], kr.iata)] = ANY(kr.node_ids) ORDER BY hop_count ASC, last_seen DESC LIMIT @row_limit; -- name: SearchKnownRoutesByNodes :many -- Routes holding a source and a destination candidate; callers check order and IATA. -- A route's hops resolve in its own IATA, so candidates from other IATAs can't match. -- Both && predicates use the node_ids GIN index. SELECT id, node_ids, hash_prefix, iata, hop_count, first_seen, last_seen, observation_count FROM known_routes kr WHERE kr.node_ids && @from_nodes::uuid[] AND kr.node_ids && @to_nodes::uuid[] AND kr.iata = ANY(@iatas::bpchar[]) ORDER BY hop_count ASC, last_seen DESC LIMIT @row_limit; -- ============================================================ -- NEIGHBORS -- ============================================================ -- name: UpsertNodeNeighbor :exec -- Records or updates a neighbor relationship between two nodes observed in the same IATA. -- node_id is the advertising node, neighbor_id is the first-hop forwarder. -- snr is optional; pass NULL when no signal reading is available (the -- common case). regionScope is optional too; pass NULL whenever the OTA -- scope query for this neighbor didn't succeed (status != "responded"), -- so a failed/timed-out query doesn't erase a previously known scope. -- On conflict, snr and region_scope are only overwritten when a new -- non-null value is supplied. INSERT INTO node_neighbors (node_id, neighbor_id, iata, observation_count, snr, region_scope) VALUES ($1, $2, $3, 1, $4, $5) ON CONFLICT (node_id, neighbor_id, iata) DO UPDATE SET last_seen = NOW(), observation_count = node_neighbors.observation_count + 1, snr = COALESCE(EXCLUDED.snr, node_neighbors.snr), region_scope = COALESCE(EXCLUDED.region_scope, node_neighbors.region_scope); -- name: UpdateObserverRegionScope :exec -- Records the observer's own OTA-reported region scope, from the "self" -- field of a /neighbors report. Always known (not queried OTA), so this -- unconditionally overwrites, unlike the neighbor-side region_scope. UPDATE observers SET region_scope = $2 WHERE id = $1; -- name: GetNodeNeighbors :many -- Returns the neighbors of a node with details, ordered by most recently seen. SELECT n.id, n.public_key, n.name, n.node_type, n.latitude, n.longitude, nn.iata, nn.observation_count, nn.first_seen, nn.last_seen, nn.snr FROM node_neighbors nn JOIN nodes n ON n.id = nn.neighbor_id WHERE nn.node_id = $1 ORDER BY nn.last_seen DESC; -- name: GetNeighborLinks :many -- Neighbor rows between @a_nodes and @b_nodes in either orientation: ingest writes -- adverts as (sender, forwarder) but traces as (receiver, sender). Callers normalise. SELECT node_id, neighbor_id, last_seen FROM node_neighbors WHERE (node_id = ANY(@a_nodes::uuid[]) AND neighbor_id = ANY(@b_nodes::uuid[])) OR (node_id = ANY(@b_nodes::uuid[]) AND neighbor_id = ANY(@a_nodes::uuid[])); -- ============================================================ -- HELPERS -- ============================================================ -- Path hash resolution is split per prefix width so each query gets a -- cacheable generic plan on its (iata, prefix_N) index; a single CASE -- predicate forced a fresh custom plan on every call. -- name: ResolvePathHashesP1 :many SELECT ns.prefix_4 AS hash, n.id AS node_id, n.name, n.latitude, n.longitude, n.public_key FROM node_short_ids ns JOIN nodes n ON n.id = ns.node_id WHERE ns.iata = $1 AND n.node_type IN (2, 3) AND ns.prefix_1 = ANY($2::bytea[]); -- name: ResolveEndpointHashes :many -- Logical endpoints can be any advertised role, unlike intermediate relay hops. -- Endpoint hashes are always one byte; use the existing (iata, prefix_1) index. -- LIMIT 1 keeps generic plans on a node PK lookup per candidate instead of -- flattening the join into a scan of all nodes. The PK already guarantees one row. SELECT ns.prefix_1 AS hash, n.id AS node_id, n.name, n.latitude, n.longitude, n.public_key FROM node_short_ids ns CROSS JOIN LATERAL ( SELECT id, name, latitude, longitude, public_key FROM nodes WHERE id = ns.node_id LIMIT 1 ) n WHERE ns.iata = $1 AND ns.prefix_1 = ANY($2::bytea[]); -- name: ResolveEndpointHashPairs :many -- Batch form of ResolveEndpointHashes for a page of packets. Matches the cross -- product of IATAs and hashes; callers pick out the pairs they asked for. SELECT ns.iata, ns.prefix_1 AS hash, n.id AS node_id, n.name, n.latitude, n.longitude, n.public_key FROM node_short_ids ns CROSS JOIN LATERAL ( SELECT id, name, latitude, longitude, public_key FROM nodes WHERE id = ns.node_id LIMIT 1 ) n WHERE ns.iata = ANY(@iatas::bpchar[]) AND ns.prefix_1 = ANY(@hashes::bytea[]); -- name: ResolvePathHashesP2 :many SELECT ns.prefix_4 AS hash, n.id AS node_id, n.name, n.latitude, n.longitude, n.public_key FROM node_short_ids ns JOIN nodes n ON n.id = ns.node_id WHERE ns.iata = $1 AND n.node_type IN (2, 3) AND ns.prefix_2 = ANY($2::bytea[]); -- name: ResolvePathHashesP3 :many SELECT ns.prefix_4 AS hash, n.id AS node_id, n.name, n.latitude, n.longitude, n.public_key FROM node_short_ids ns JOIN nodes n ON n.id = ns.node_id WHERE ns.iata = $1 AND n.node_type IN (2, 3) AND ns.prefix_3 = ANY($2::bytea[]); -- name: ResolvePathHashesP4 :many SELECT ns.prefix_4 AS hash, n.id AS node_id, n.name, n.latitude, n.longitude, n.public_key FROM node_short_ids ns JOIN nodes n ON n.id = ns.node_id WHERE ns.iata = $1 AND n.node_type IN (2, 3) AND ns.prefix_4 = ANY($2::bytea[]); -- name: ResolveRelayHashPairsP1 :many -- Batch form of ResolvePathHashesP1 across IATAs; callers check uniqueness per IATA. SELECT ns.iata, ns.prefix_4 AS hash, ns.node_id FROM node_short_ids ns JOIN nodes n ON n.id = ns.node_id WHERE ns.iata = ANY(@iatas::bpchar[]) AND n.node_type IN (2, 3) AND ns.prefix_1 = ANY(@hashes::bytea[]); -- name: ResolveRelayHashPairsP2 :many -- Batch form of ResolvePathHashesP2 across IATAs; callers check uniqueness per IATA. SELECT ns.iata, ns.prefix_4 AS hash, ns.node_id FROM node_short_ids ns JOIN nodes n ON n.id = ns.node_id WHERE ns.iata = ANY(@iatas::bpchar[]) AND n.node_type IN (2, 3) AND ns.prefix_2 = ANY(@hashes::bytea[]); -- name: ResolveRelayHashPairsP3 :many -- Batch form of ResolvePathHashesP3 across IATAs; callers check uniqueness per IATA. SELECT ns.iata, ns.prefix_4 AS hash, ns.node_id FROM node_short_ids ns JOIN nodes n ON n.id = ns.node_id WHERE ns.iata = ANY(@iatas::bpchar[]) AND n.node_type IN (2, 3) AND ns.prefix_3 = ANY(@hashes::bytea[]); -- name: ResolveRelayHashPairsP4 :many -- Batch form of ResolvePathHashesP4 across IATAs; callers check uniqueness per IATA. SELECT ns.iata, ns.prefix_4 AS hash, ns.node_id FROM node_short_ids ns JOIN nodes n ON n.id = ns.node_id WHERE ns.iata = ANY(@iatas::bpchar[]) AND n.node_type IN (2, 3) AND ns.prefix_4 = ANY(@hashes::bytea[]); -- name: RefreshRadioPresets :exec REFRESH MATERIALIZED VIEW CONCURRENTLY mv_radio_presets; -- name: AmbiguousPrefixes :many -- Hop prefixes that match >1 node in an IATA, per width. Computed once per reconfirm run. SELECT iata::text AS iata, 1::int AS len, prefix_1 AS prefix FROM node_short_ids GROUP BY iata, prefix_1 HAVING COUNT(*) > 1 UNION ALL SELECT iata::text, 2::int, prefix_2 FROM node_short_ids GROUP BY iata, prefix_2 HAVING COUNT(*) > 1 UNION ALL SELECT iata::text, 3::int, prefix_3 FROM node_short_ids GROUP BY iata, prefix_3 HAVING COUNT(*) > 1 UNION ALL SELECT iata::text, 4::int, prefix_4 FROM node_short_ids GROUP BY iata, prefix_4 HAVING COUNT(*) > 1; -- name: ReconfirmRoutes :one -- Checks one batch of least-recently-reconfirmed routes: deletes those with a departed -- hop node or a hop prefix now matching >1 node in that IATA (length-aware: -- 1/2/3/4-byte hop prefixes check prefix_1/2/3/4; ambiguity set supplied by AmbiguousPrefixes), -- and stamps the survivors. WITH batch AS MATERIALIZED ( SELECT r.iata, r.path_key, r.node_ids, r.hash_prefix FROM known_routes r WHERE r.last_reconfirmed_at < @before ORDER BY r.last_reconfirmed_at LIMIT @batch_size FOR UPDATE OF r SKIP LOCKED ), amb AS ( SELECT a.iata::char(3) AS iata, a.len, a.p FROM ROWS FROM (unnest(@amb_iata::text[]), unnest(@amb_len::int[]), unnest(@amb_prefix::bytea[])) AS a(iata, len, p) ), dead AS ( SELECT b.iata, b.path_key FROM batch b WHERE EXISTS ( SELECT 1 FROM unnest(b.node_ids) AS hop_node_id WHERE NOT EXISTS ( SELECT 1 FROM node_short_ids ns WHERE ns.node_id = hop_node_id AND ns.iata = b.iata ) ) UNION SELECT DISTINCT b.iata, b.path_key FROM batch b CROSS JOIN LATERAL unnest(b.hash_prefix) AS hp JOIN amb a ON a.iata = b.iata AND a.len = length(hp) AND a.p = hp ), deleted AS ( DELETE FROM known_routes kr USING dead d WHERE kr.iata = d.iata AND kr.path_key = d.path_key ), updated AS ( UPDATE known_routes kr SET last_reconfirmed_at = GREATEST(NOW(), @before::timestamptz) FROM batch b WHERE kr.iata = b.iata AND kr.path_key = b.path_key AND NOT EXISTS ( SELECT 1 FROM dead d WHERE d.iata = b.iata AND d.path_key = b.path_key ) ) SELECT count(*) FROM batch; -- name: ReconfirmNeighbors :exec -- Delete node_neighbors where the neighbor has departed from node_short_ids -- for that IATA, or where its prefix_4 is now ambiguous. DELETE FROM node_neighbors nn WHERE NOT EXISTS ( SELECT 1 FROM node_short_ids ns WHERE ns.node_id = nn.neighbor_id AND ns.iata = nn.iata ) OR ( SELECT COUNT(*) FROM node_short_ids ns WHERE ns.iata = nn.iata AND ns.prefix_4 = ( SELECT prefix_4 FROM node_short_ids WHERE node_id = nn.neighbor_id AND iata = nn.iata ) ) > 1; -- name: CreateAccount :one INSERT INTO accounts (name) VALUES (sqlc.arg(name)) ON CONFLICT (name) WHERE deactivated_at IS NULL DO NOTHING RETURNING id, name, created_at, deactivated_at; -- name: ListAccounts :many SELECT id, name, created_at, deactivated_at FROM accounts ORDER BY created_at DESC, id DESC; -- name: GetAccount :one SELECT id, name, created_at, deactivated_at FROM accounts WHERE id = $1; -- name: DeactivateAccount :one -- Lock the current row before deciding the outcome, including when another -- deactivation commits while this statement is waiting for its row lock. WITH target AS MATERIALIZED ( SELECT a.id, a.deactivated_at FROM accounts a WHERE a.id = $1 FOR UPDATE ), changed AS ( UPDATE accounts a SET deactivated_at = NOW() FROM target t WHERE a.id = t.id AND t.deactivated_at IS NULL RETURNING a.id ) SELECT EXISTS(SELECT 1 FROM target) AS found, EXISTS(SELECT 1 FROM changed) AS deactivated; -- name: GetObserverActivityLiveSummary :one -- Two indexed ranges, bounded to one observer; no legacy presence counters. WITH latest AS ( SELECT heard_at FROM packet_observations WHERE observer_id = @observer_id::uuid AND heard_at <= @generated_at::timestamptz ORDER BY heard_at DESC LIMIT 1 ), hourly AS ( SELECT COUNT(*)::bigint AS n FROM packet_observations WHERE observer_id = @observer_id::uuid AND heard_at >= @hour_start::timestamptz AND heard_at < @hour_end::timestamptz ) SELECT (SELECT heard_at FROM latest)::timestamptz AS latest_recorded_at, hourly.n AS last_complete_hour FROM hourly; -- name: GetScopeCatalogue :one SELECT * FROM meshmapper_scope_catalogues WHERE iata = $1 AND url = $2; -- name: ListScopeCatalogues :many SELECT * FROM meshmapper_scope_catalogues ORDER BY iata, attempted_at; -- name: SaveScopeCatalogue :exec -- One statement commits the validated snapshot and its lookup identities together. -- Empty arrays insert nothing. NULL payload/checked_at retain last-known-good data -- after an error or 304. Imported names never replace existing manual metadata. WITH inserted AS ( INSERT INTO transport_scopes (name, transport_key, key_fingerprint, imported_only) SELECT entry.name, entry.key, entry.fingerprint, TRUE FROM (SELECT unnest(@names::text[]) AS name, unnest(@keys::bytea[]) AS key, unnest(@fingerprints::bytea[]) AS fingerprint) AS entry ON CONFLICT (name) DO NOTHING ) INSERT INTO meshmapper_scope_catalogues (iata, url, payload, etag, checked_at, attempted_at, next_attempt, last_error) VALUES (@iata, @url, sqlc.narg(payload)::jsonb, sqlc.narg(etag)::text, sqlc.narg(checked_at)::timestamptz, @attempted_at, @next_attempt, @last_error) ON CONFLICT (iata, url) DO UPDATE SET payload = COALESCE(EXCLUDED.payload, meshmapper_scope_catalogues.payload), etag = COALESCE(EXCLUDED.etag, meshmapper_scope_catalogues.etag), checked_at = COALESCE(EXCLUDED.checked_at, meshmapper_scope_catalogues.checked_at), attempted_at = EXCLUDED.attempted_at, next_attempt = EXCLUDED.next_attempt, last_error = EXCLUDED.last_error; -- name: ListChannelCatalogues :many SELECT * FROM meshmapper_channel_catalogues ORDER BY iata, attempted_at; -- name: SaveChannelCatalogue :exec -- NULL payload/etag/checked_at retain the last good list after an error or 304. INSERT INTO meshmapper_channel_catalogues (iata, url, payload, etag, checked_at, attempted_at, next_attempt, last_error) VALUES (@iata, @url, sqlc.narg(payload)::jsonb, sqlc.narg(etag)::text, sqlc.narg(checked_at)::timestamptz, @attempted_at, @next_attempt, @last_error) ON CONFLICT (iata, url) DO UPDATE SET payload = COALESCE(EXCLUDED.payload, meshmapper_channel_catalogues.payload), etag = COALESCE(EXCLUDED.etag, meshmapper_channel_catalogues.etag), checked_at = COALESCE(EXCLUDED.checked_at, meshmapper_channel_catalogues.checked_at), attempted_at = EXCLUDED.attempted_at, next_attempt = EXCLUDED.next_attempt, last_error = EXCLUDED.last_error; -- name: DeleteChannelMembersNotIn :exec DELETE FROM meshmapper_channel_members WHERE iata = @iata AND NOT (key_fingerprint = ANY(@keep::bytea[])); -- name: AddChannelMembers :exec INSERT INTO meshmapper_channel_members (iata, key_fingerprint) SELECT @iata, unnest(@fingerprints::bytea[]) ON CONFLICT (iata, key_fingerprint) DO NOTHING; -- name: DeleteAllChannelMembers :exec DELETE FROM meshmapper_channel_members; -- name: ListZoneBoundaries :many SELECT * FROM meshmapper_zone_boundaries ORDER BY iata; -- name: SaveZoneBoundary :exec -- NULL feature/etag/checked_at retain the last good boundary after an error or 304. INSERT INTO meshmapper_zone_boundaries (iata, url, feature, etag, checked_at, attempted_at, next_attempt, last_error) VALUES (@iata, @url, sqlc.narg(feature)::jsonb, sqlc.narg(etag)::text, sqlc.narg(checked_at)::timestamptz, @attempted_at, @next_attempt, @last_error) ON CONFLICT (iata) DO UPDATE SET url = EXCLUDED.url, feature = COALESCE(EXCLUDED.feature, meshmapper_zone_boundaries.feature), etag = COALESCE(EXCLUDED.etag, meshmapper_zone_boundaries.etag), checked_at = COALESCE(EXCLUDED.checked_at, meshmapper_zone_boundaries.checked_at), attempted_at = EXCLUDED.attempted_at, next_attempt = EXCLUDED.next_attempt, last_error = EXCLUDED.last_error; -- name: ListZoneLists :many SELECT * FROM meshmapper_zone_lists ORDER BY country; -- name: SaveZoneList :exec -- NULL payload/etag/fetched_at retain the last good list after an error or 304. INSERT INTO meshmapper_zone_lists (country, payload, etag, fetched_at, attempted_at, next_attempt, last_error) VALUES (@country, sqlc.narg(payload)::jsonb, sqlc.narg(etag)::text, sqlc.narg(fetched_at)::timestamptz, @attempted_at, @next_attempt, @last_error) ON CONFLICT (country) DO UPDATE SET payload = COALESCE(EXCLUDED.payload, meshmapper_zone_lists.payload), etag = COALESCE(EXCLUDED.etag, meshmapper_zone_lists.etag), fetched_at = COALESCE(EXCLUDED.fetched_at, meshmapper_zone_lists.fetched_at), attempted_at = EXCLUDED.attempted_at, next_attempt = EXCLUDED.next_attempt, last_error = EXCLUDED.last_error; -- name: PruneZoneBoundaries :many -- Drops imports for IATAs no longer configured, so their manual border returns. DELETE FROM meshmapper_zone_boundaries WHERE NOT (iata = ANY(@keep::text[])) RETURNING iata;