diff --git a/cmd/server/loadchunk_observer_iata_test.go b/cmd/server/loadchunk_observer_iata_test.go new file mode 100644 index 00000000..715518ca --- /dev/null +++ b/cmd/server/loadchunk_observer_iata_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "database/sql" + "os" + "path/filepath" + "testing" + "time" +) + +// The background loader (loadChunk) fills in days 2-7 of the retention +// window after the hot startup window. Its query left out the observer's +// IATA code, so packets older than the hot window lost observer_iata in the +// packets API and the iata filter (#1188), while Load and ingest kept it. +func TestLoadChunkKeepsObserverIATA(t *testing.T) { + dir := t.TempDir() + dbPath := filepath.Join(dir, "iata.db") + createTestDBAt(t, dbPath, 20) + t.Cleanup(func() { os.RemoveAll(dir) }) + conn, err := sql.Open("sqlite3", dbPath) + if err != nil { + t.Fatal(err) + } + if _, err := conn.Exec(`INSERT INTO observers (id, name, iata) VALUES ('obs1', 'Obs1', 'SJC')`); err != nil { + t.Fatal(err) + } + conn.Close() + + db, err := OpenDB(dbPath) + if err != nil { + t.Fatalf("OpenDB: %v", err) + } + t.Cleanup(func() { db.Close() }) + store := NewPacketStore(db, &PacketStoreConfig{}) + from := time.Date(2025, 12, 31, 0, 0, 0, 0, time.UTC) + if err := store.loadChunk(from, from.Add(72*time.Hour)); err != nil { + t.Fatal(err) + } + store.mu.RLock() + defer store.mu.RUnlock() + if len(store.packets) == 0 { + t.Fatal("loadChunk loaded nothing") + } + for _, tx := range store.packets { + for _, o := range tx.Observations { + if o.ObserverIATA != "SJC" { + t.Fatalf("tx %d: observation IATA = %q, want SJC", tx.ID, o.ObserverIATA) + } + } + if tx.ObserverIATA != "SJC" { + t.Fatalf("tx %d: IATA = %q, want SJC", tx.ID, tx.ObserverIATA) + } + } +} diff --git a/cmd/server/store.go b/cmd/server/store.go index b6b3d898..d0f2fae5 100644 --- a/cmd/server/store.go +++ b/cmd/server/store.go @@ -1203,7 +1203,7 @@ func (s *PacketStore) loadChunk(from, to time.Time) error { if s.db.isV3 { chunkSQL = `SELECT t.id, t.raw_hex, t.hash, t.first_seen, t.route_type, t.payload_type, t.payload_version, t.decoded_json, - o.id, obs.id, obs.name, o.direction, + o.id, obs.id, obs.name, COALESCE(obs.iata, ''), o.direction, o.snr, o.rssi, o.score, o.path_json, strftime('%Y-%m-%dT%H:%M:%fZ', o.timestamp, 'unixepoch')` + obsRawHexCol + rpCol + scopeNameCol + ` FROM transmissions t LEFT JOIN observations o ON o.transmission_id = t.id @@ -1212,10 +1212,11 @@ func (s *PacketStore) loadChunk(from, to time.Time) error { } else { chunkSQL = `SELECT t.id, t.raw_hex, t.hash, t.first_seen, t.route_type, t.payload_type, t.payload_version, t.decoded_json, - o.id, o.observer_id, o.observer_name, o.direction, + o.id, o.observer_id, o.observer_name, COALESCE(obs.iata, ''), o.direction, o.snr, o.rssi, o.score, o.path_json, o.timestamp` + obsRawHexCol + rpCol + scopeNameCol + ` FROM transmissions t - LEFT JOIN observations o ON o.transmission_id = t.id` + filterClause + ` + LEFT JOIN observations o ON o.transmission_id = t.id + LEFT JOIN observers obs ON obs.id = o.observer_id` + filterClause + ` ORDER BY t.first_seen ASC, o.timestamp DESC` } @@ -1268,7 +1269,7 @@ func (s *PacketStore) loadChunk(from, to time.Time) error { var rawHex, hash, firstSeen, decodedJSON sql.NullString var routeType, payloadType, payloadVersion sql.NullInt64 var obsID sql.NullInt64 - var observerID, observerName, direction, pathJSON, obsTimestamp sql.NullString + var observerID, observerName, observerIATA, direction, pathJSON, obsTimestamp sql.NullString var snr, rssi sql.NullFloat64 var score sql.NullInt64 var obsRawHex sql.NullString @@ -1277,7 +1278,7 @@ func (s *PacketStore) loadChunk(from, to time.Time) error { scanArgs := []interface{}{&txID, &rawHex, &hash, &firstSeen, &routeType, &payloadType, &payloadVersion, &decodedJSON, - &obsID, &observerID, &observerName, &direction, + &obsID, &observerID, &observerName, &observerIATA, &direction, &snr, &rssi, &score, &pathJSON, &obsTimestamp} if s.db.hasObsRawHex { scanArgs = append(scanArgs, &obsRawHex) @@ -1330,6 +1331,7 @@ func (s *PacketStore) loadChunk(from, to time.Time) error { TransmissionID: txID, ObserverID: obsIDStr, ObserverName: nullStrVal(observerName), + ObserverIATA: nullStrVal(observerIATA), Direction: nullStrVal(direction), SNR: nullFloatPtr(snr), RSSI: nullFloatPtr(rssi),