This commit is contained in:
2026-05-28 11:09:07 +02:00
parent a8b7622667
commit d3c9982c66
8 changed files with 380 additions and 200 deletions
+28 -14
View File
@@ -832,34 +832,48 @@ type EntitySlot struct {
Slots map[string]map[string]struct{} // band → modes worked
}
// EntitySlotMap returns slot data for every QSO grouped by lowercase
// country name (cty.dat-style key). Cheap on a 25k-row table: one
// scan, no joins. Callers can compare a spot's entity to this map to
// decide if it's NEW / NEW SLOT / WORKED.
func (r *Repo) EntitySlotMap(ctx context.Context) (map[string]*EntitySlot, error) {
// EntitySlotMap returns slot data for every QSO, grouping by entity.
//
// `resolveEntity` maps a callsign to its canonical entity name (we use
// cty.dat for this). When non-nil, the resolved name wins over the
// stored `country` column — that's important because QRZ's "Turkey"
// disagrees with cty.dat's "Asiatic Turkey" and the cluster status
// comparison would otherwise miss past QSOs. When nil, we fall back to
// the stored country (useful for tests).
//
// One DB scan regardless of input size. Cheap to call per cluster batch.
func (r *Repo) EntitySlotMap(ctx context.Context, resolveEntity func(callsign string) string) (map[string]*EntitySlot, error) {
rows, err := r.db.QueryContext(ctx,
`SELECT lower(country), lower(band), upper(mode) FROM qso
WHERE country IS NOT NULL AND country != ''
AND band IS NOT NULL AND band != ''
AND mode IS NOT NULL AND mode != ''`)
`SELECT callsign, lower(coalesce(country,'')), lower(band), upper(mode) FROM qso
WHERE band IS NOT NULL AND band != ''
AND mode IS NOT NULL AND mode != ''`)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[string]*EntitySlot, 256)
for rows.Next() {
var country, band, mode string
if err := rows.Scan(&country, &band, &mode); err != nil {
var call, country, band, mode string
if err := rows.Scan(&call, &country, &band, &mode); err != nil {
return nil, err
}
e, ok := out[country]
key := country
if resolveEntity != nil {
if name := strings.ToLower(strings.TrimSpace(resolveEntity(call))); name != "" {
key = name
}
}
if key == "" {
continue
}
e, ok := out[key]
if !ok {
e = &EntitySlot{
Country: country,
Country: key,
Bands: make(map[string]struct{}),
Slots: make(map[string]map[string]struct{}),
}
out[country] = e
out[key] = e
}
e.Bands[band] = struct{}{}
bandSlots, ok := e.Slots[band]