diff --git a/db/queries/queries.sql b/db/queries/queries.sql index 60196e3..3744ed0 100644 --- a/db/queries/queries.sql +++ b/db/queries/queries.sql @@ -236,6 +236,13 @@ ON CONFLICT (channel_hash, key_fingerprint) DO UPDATE SET 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; diff --git a/db/sqlc/queries.sql.go b/db/sqlc/queries.sql.go index 9bae5ac..fa84665 100644 --- a/db/sqlc/queries.sql.go +++ b/db/sqlc/queries.sql.go @@ -1319,6 +1319,21 @@ func (q *Queries) UpsertChannel(ctx context.Context, arg UpsertChannelParams) (C return i, err } +const upsertChannelHashOnly = `-- 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 +` + +func (q *Queries) UpsertChannelHashOnly(ctx context.Context, channelHash []byte) (int32, error) { + row := q.db.QueryRow(ctx, upsertChannelHashOnly, channelHash) + var id int32 + err := row.Scan(&id) + return id, err +} + const upsertIATA = `-- name: UpsertIATA :exec INSERT INTO iata_codes (iata) diff --git a/db/store.go b/db/store.go index d69f2cc..1a65e7c 100644 --- a/db/store.go +++ b/db/store.go @@ -233,6 +233,18 @@ func (s *Store) UpsertChannel(ctx context.Context, channelHash []byte, keyFinger return int(row.ID), nil } +// UpsertChannelHashOnly upserts a hash-only channel row for cases where the +// channel key is unknown. Uses the partial unique index to ensure only one +// hash-only row exists per channel hash. The return value is the channel ID +// but can be safely ignored since unknown-key channels have no messages. +func (s *Store) UpsertChannelHashOnly(ctx context.Context, channelHash []byte) (int, error) { + rowID, err := s.q.UpsertChannelHashOnly(ctx, channelHash) + if err != nil { + return 0, err + } + return int(rowID), nil +} + // ListIATAs returns all known IATA codes with display name and coordinates. // IATAs are auto-created on first packet arrival from that location. func (s *Store) ListIATAs(ctx context.Context) ([]api.IATA, error) { diff --git a/internal/ingest/ingest.go b/internal/ingest/ingest.go index e8a80ec..1997d9c 100644 --- a/internal/ingest/ingest.go +++ b/internal/ingest/ingest.go @@ -100,6 +100,12 @@ type DB interface { // UpsertChannel upserts a channel row by (hash, keyFingerprint) and returns its integer ID. // Pass nil keyFingerprint to record a hash-only row when the key is unknown. UpsertChannel(ctx context.Context, channelHash []byte, keyFingerprint []byte, name string, hashtag string) (int, error) + + // UpsertChannelHashOnly upserts a hash-only channel row for cases where the + // channel key is unknown. Uses the partial unique index to ensure only one + // hash-only row exists per channel hash. The return value is the channel ID + // but can be safely ignored since unknown-key channels have no messages. + UpsertChannelHashOnly(ctx context.Context, channelHash []byte) (int, error) } // UpsertPacketParams mirrors the columns written on packets upsert. @@ -671,7 +677,7 @@ func (w *Worker) handlePayloadTypeSideEffects(ctx context.Context, packet *meshc channelHashBytes := []byte{grpTxt.ChannelHash} // Always upsert a hash-only row so unknown channels are recorded. - _, _ = w.db.UpsertChannel(ctx, channelHashBytes, nil, "", "") + _, _ = w.db.UpsertChannelHashOnly(ctx, channelHashBytes) // Try each known key entry for this hash. entries := w.keys.GetKey(channelHashBytes)