// 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 = 'Y' OR qsl_rcvd = 'Y' OR eqsl_rcvd = 'Y') THEN 1 ELSE 0 END), MAX(CASE WHEN lotw_rcvd = 'Y' OR qsl_rcvd = 'Y' OR eqsl_rcvd = 'Y' 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) } }