An operator's screenshot had it side by side: the Awards panel showed Morocco validated on five bands, and the band/mode matrix two inches above showed the entity as merely worked. ADIF's QSL_Rcvd enumeration has both Y and V — "received" and "verified" — and V is what a LoTW download writes for a confirmation the ARRL has validated. The award engine's isYes accepted "Y" or "V". Everything else in the app compared against 'Y' alone: the worked-before status grid, the slot statistics, the row colouring by QSL status, the awards QSO list, the call history badges. So the confirmations an operator cares most about were the ones that did not count. Now one definition per side, named so the next reader finds the other: qso.ConfirmedValues on the Go side, used by the queries themselves, and isQSLConfirmed in lib/qsl on the frontend, which rowColors and the panels call instead of testing the letter. The Go test drives the real query shape against a real database with a 'V' row — the constant being right is not the point, the queries using it is.
173 lines
4.9 KiB
Go
173 lines
4.9 KiB
Go
// dbdiag inspects HamLog DB state. Optional 2nd arg: "wb CALL DXCC".
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
const migrationsDir = "internal/db/migrations"
|
|
|
|
func main() {
|
|
base, _ := os.UserConfigDir()
|
|
dbPath := filepath.Join(base, "HamLog", "hamlog.db")
|
|
|
|
mode := "summary"
|
|
var call string
|
|
var dxcc int
|
|
if len(os.Args) >= 3 && os.Args[1] == "wb" {
|
|
mode = "wb"
|
|
call = strings.ToUpper(os.Args[2])
|
|
if len(os.Args) >= 4 {
|
|
dxcc, _ = strconv.Atoi(os.Args[3])
|
|
}
|
|
}
|
|
|
|
fmt.Println("DB path:", dbPath)
|
|
dsn := "file:" + dbPath + "?_pragma=foreign_keys(on)"
|
|
conn, err := sql.Open("sqlite", dsn)
|
|
must("open", err)
|
|
defer conn.Close()
|
|
must("ping", conn.Ping())
|
|
|
|
if mode == "wb" {
|
|
probeWB(conn, call, dxcc)
|
|
probeCache(conn, call)
|
|
return
|
|
}
|
|
summary(conn)
|
|
}
|
|
|
|
func probeCache(conn *sql.DB, call string) {
|
|
fmt.Println("\nCache row for", call, ":")
|
|
row := conn.QueryRow(`SELECT name, qth, country, grid, dxcc, source, fetched_at
|
|
FROM callsign_cache WHERE callsign = ?`, call)
|
|
var name, qth, country, grid, src, fetched sql.NullString
|
|
var dxcc sql.NullInt64
|
|
if err := row.Scan(&name, &qth, &country, &grid, &dxcc, &src, &fetched); err != nil {
|
|
fmt.Println(" (no cache row:", err, ")")
|
|
return
|
|
}
|
|
fmt.Printf(" name=%q qth=%q country=%q grid=%q\n", name.String, qth.String, country.String, grid.String)
|
|
fmt.Printf(" dxcc=%v(valid=%v) source=%s fetched_at=%s\n", dxcc.Int64, dxcc.Valid, src.String, fetched.String)
|
|
}
|
|
|
|
func probeWB(conn *sql.DB, call string, dxcc int) {
|
|
fmt.Printf("\nProbing WorkedBefore for call=%q dxcc=%d\n", call, dxcc)
|
|
|
|
var count int
|
|
must("count call", conn.QueryRow(`SELECT COUNT(*) FROM qso WHERE callsign = ?`, call).Scan(&count))
|
|
fmt.Printf(" count(callsign=%s) = %d\n", call, count)
|
|
|
|
if dxcc == 0 {
|
|
var d sql.NullInt64
|
|
_ = conn.QueryRow(`SELECT dxcc FROM qso
|
|
WHERE callsign = ? AND dxcc IS NOT NULL
|
|
ORDER BY qso_date DESC LIMIT 1`, call).Scan(&d)
|
|
if d.Valid {
|
|
dxcc = int(d.Int64)
|
|
fmt.Printf(" inferred dxcc from prior QSO: %d\n", dxcc)
|
|
}
|
|
}
|
|
if dxcc == 0 {
|
|
fmt.Println(" no DXCC available — skipping entity stats")
|
|
return
|
|
}
|
|
|
|
var dxccCount int
|
|
must("count dxcc", conn.QueryRow(`SELECT COUNT(*) FROM qso WHERE dxcc = ?`, dxcc).Scan(&dxccCount))
|
|
fmt.Printf(" count(dxcc=%d) = %d\n", dxcc, dxccCount)
|
|
|
|
// Distinct (band, mode) for this DXCC
|
|
rows, err := conn.Query(`SELECT band, mode, COUNT(*) FROM qso WHERE dxcc = ? GROUP BY band, mode ORDER BY band, mode`, dxcc)
|
|
must("group by band/mode", err)
|
|
defer rows.Close()
|
|
fmt.Printf(" distinct (band, mode) groups for dxcc=%d:\n", dxcc)
|
|
var n int
|
|
for rows.Next() {
|
|
var band, mode string
|
|
var c int
|
|
rows.Scan(&band, &mode, &c)
|
|
fmt.Printf(" %-8s %-12s %d QSOs\n", band, mode, c)
|
|
n++
|
|
}
|
|
if n == 0 {
|
|
fmt.Println(" (none)")
|
|
}
|
|
|
|
// Same query as backend
|
|
fmt.Println("\n --- exact backend query ---")
|
|
rows2, err := conn.Query(`
|
|
SELECT band, mode,
|
|
MAX(CASE WHEN callsign = ? THEN 1 ELSE 0 END),
|
|
MAX(CASE WHEN callsign = ?
|
|
AND (lotw_rcvd IN ('Y','V') OR qsl_rcvd IN ('Y','V') OR eqsl_rcvd IN ('Y','V'))
|
|
THEN 1 ELSE 0 END),
|
|
MAX(CASE WHEN lotw_rcvd IN ('Y','V') OR qsl_rcvd IN ('Y','V') OR eqsl_rcvd IN ('Y','V')
|
|
THEN 1 ELSE 0 END)
|
|
FROM qso WHERE dxcc = ?
|
|
GROUP BY band, mode
|
|
ORDER BY band, mode`, call, call, dxcc)
|
|
must("backend query", err)
|
|
defer rows2.Close()
|
|
n = 0
|
|
for rows2.Next() {
|
|
var band, mode string
|
|
var cw, cc, dc int
|
|
rows2.Scan(&band, &mode, &cw, &cc, &dc)
|
|
fmt.Printf(" %-8s %-12s callW=%d callC=%d dxccC=%d\n", band, mode, cw, cc, dc)
|
|
n++
|
|
}
|
|
fmt.Printf(" → %d rows\n", n)
|
|
}
|
|
|
|
func summary(conn *sql.DB) {
|
|
if st, err := os.Stat(filepath.Join(filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir("a")))), "")); err == nil {
|
|
fmt.Printf("(size info skipped) %v\n", st)
|
|
}
|
|
|
|
fmt.Println("Applied migrations:")
|
|
rows, err := conn.Query(`SELECT name FROM schema_migrations ORDER BY name`)
|
|
if err == nil {
|
|
for rows.Next() {
|
|
var n string
|
|
rows.Scan(&n)
|
|
fmt.Println(" -", n)
|
|
}
|
|
rows.Close()
|
|
}
|
|
|
|
var n int64
|
|
conn.QueryRow(`SELECT COUNT(*) FROM qso`).Scan(&n)
|
|
fmt.Println("\nQSO rows:", n)
|
|
|
|
var withDxcc int64
|
|
conn.QueryRow(`SELECT COUNT(*) FROM qso WHERE dxcc IS NOT NULL`).Scan(&withDxcc)
|
|
fmt.Printf("Rows with non-null dxcc: %d (%.1f%%)\n", withDxcc, float64(withDxcc)/float64(n)*100)
|
|
|
|
var nDistinctDXCC int
|
|
conn.QueryRow(`SELECT COUNT(DISTINCT dxcc) FROM qso WHERE dxcc IS NOT NULL`).Scan(&nDistinctDXCC)
|
|
fmt.Println("Distinct DXCC entities in log:", nDistinctDXCC)
|
|
|
|
fmt.Println("\nTry: go run ./cmd/dbdiag wb 4X6TT")
|
|
fmt.Println(" go run ./cmd/dbdiag wb 4X6TT 336")
|
|
_ = context.Background
|
|
_ = sort.Strings
|
|
must("dummy", nil)
|
|
}
|
|
|
|
func must(label string, err error) {
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, label+":", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|