up
This commit is contained in:
@@ -454,6 +454,19 @@ func (r *Repo) AddBatch(ctx context.Context, qsos []QSO) (int64, error) {
|
||||
return inserted, nil
|
||||
}
|
||||
|
||||
// Revision returns a cheap fingerprint of the logbook — row count and the
|
||||
// highest id — so a client can poll for changes made by OTHER instances on a
|
||||
// shared MySQL logbook and refresh when it differs. Inserts bump the max id;
|
||||
// deletes change the count.
|
||||
func (r *Repo) Revision(ctx context.Context) (string, error) {
|
||||
var count, maxID int64
|
||||
if err := r.db.QueryRowContext(ctx,
|
||||
`SELECT COUNT(*), COALESCE(MAX(id), 0) FROM qso`).Scan(&count, &maxID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d:%d", count, maxID), nil
|
||||
}
|
||||
|
||||
// GetByID fetches a single QSO by primary key.
|
||||
func (r *Repo) GetByID(ctx context.Context, id int64) (QSO, error) {
|
||||
row := r.db.QueryRowContext(ctx,
|
||||
@@ -1308,6 +1321,106 @@ func (r *Repo) IterateAll(ctx context.Context, fn func(QSO) error) error {
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
// awardCols is the column set the award engine can read — every field reachable
|
||||
// from refField/inScope/confirmed in internal/award plus enrichQSOForAwards:
|
||||
// the reference fields (dxcc, zones, continent, country, state, grid/vucc, iota,
|
||||
// sota, pota, wwff via extras_json, and the free-text fields name/qth/address/
|
||||
// comment/notes a custom award may key on), band/mode/qso_date for scoping,
|
||||
// freq_hz for band recovery, and the qsl/lotw/eqsl rcvd confirmation flags.
|
||||
// This drops ~125 unused columns so award computation over a remote MySQL
|
||||
// backend ships a fraction of each row instead of the whole record.
|
||||
//
|
||||
// IMPORTANT: when a new award references a QSO field not listed here, add the
|
||||
// column to this list AND populate it in scanAwardQSO below, or that award will
|
||||
// silently see an empty value during stats/computation.
|
||||
const awardCols = `id, callsign, qso_date, band, freq_hz, mode, ` +
|
||||
`grid, vucc_grids, country, state, cont, cqz, ituz, dxcc, iota, sota_ref, pota_ref, ` +
|
||||
`name, qth, address, comment, notes, ` +
|
||||
`qsl_rcvd, lotw_rcvd, eqsl_rcvd, extras_json`
|
||||
|
||||
// IterateForAwards streams a lightweight projection of every QSO — only the
|
||||
// fields award computation reads (see awardCols). All other QSO fields are left
|
||||
// zero. Use IterateAll when the full record is needed. Ordered by date/id like
|
||||
// IterateAll for deterministic results.
|
||||
func (r *Repo) IterateForAwards(ctx context.Context, fn func(QSO) error) error {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT `+awardCols+` FROM qso ORDER BY qso_date ASC, id ASC`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("query qso (awards): %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
q, err := scanAwardQSO(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fn(q); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
// scanAwardQSO reads one row produced by awardCols into a QSO, populating only
|
||||
// the award-relevant fields. Column order MUST match awardCols.
|
||||
func scanAwardQSO(s scanner) (QSO, error) {
|
||||
var q QSO
|
||||
var (
|
||||
qsoDateStr string
|
||||
freqHz sql.NullInt64
|
||||
grid, vucc, country, state sql.NullString
|
||||
cont, iotaRef, sota, pota sql.NullString
|
||||
dxcc, cqz, ituz sql.NullInt64
|
||||
name, qth, address sql.NullString
|
||||
comment, notes sql.NullString
|
||||
qslRcvd, lotwRcvd, eqslRcvd sql.NullString
|
||||
extrasJSON sql.NullString
|
||||
)
|
||||
if err := s.Scan(
|
||||
&q.ID, &q.Callsign, &qsoDateStr, &q.Band, &freqHz, &q.Mode,
|
||||
&grid, &vucc, &country, &state, &cont, &cqz, &ituz, &dxcc, &iotaRef, &sota, &pota,
|
||||
&name, &qth, &address, &comment, ¬es,
|
||||
&qslRcvd, &lotwRcvd, &eqslRcvd, &extrasJSON,
|
||||
); err != nil {
|
||||
return QSO{}, fmt.Errorf("scan qso (awards): %w", err)
|
||||
}
|
||||
q.QSODate = parseTimeLoose(qsoDateStr)
|
||||
if freqHz.Valid {
|
||||
v := freqHz.Int64
|
||||
q.FreqHz = &v
|
||||
}
|
||||
q.Grid = grid.String
|
||||
q.VUCCGrids = vucc.String
|
||||
q.Country = country.String
|
||||
q.State = state.String
|
||||
q.Continent = cont.String
|
||||
if cqz.Valid {
|
||||
v := int(cqz.Int64)
|
||||
q.CQZ = &v
|
||||
}
|
||||
if ituz.Valid {
|
||||
v := int(ituz.Int64)
|
||||
q.ITUZ = &v
|
||||
}
|
||||
if dxcc.Valid {
|
||||
v := int(dxcc.Int64)
|
||||
q.DXCC = &v
|
||||
}
|
||||
q.IOTA = iotaRef.String
|
||||
q.SOTARef = sota.String
|
||||
q.POTARef = pota.String
|
||||
q.Name = name.String
|
||||
q.QTH = qth.String
|
||||
q.Address = address.String
|
||||
q.Comment = comment.String
|
||||
q.Notes = notes.String
|
||||
q.QSLRcvd = qslRcvd.String
|
||||
q.LOTWRcvd = lotwRcvd.String
|
||||
q.EQSLRcvd = eqslRcvd.String
|
||||
q.Extras = decodeExtras(extrasJSON.String)
|
||||
return q, nil
|
||||
}
|
||||
|
||||
// EntitySlot bundles every (band, mode) tuple ever worked for a given
|
||||
// DXCC entity name. Used by the cluster spot colouring code to decide
|
||||
// NEW / NEW SLOT / WORKED in constant time after one batched query.
|
||||
|
||||
Reference in New Issue
Block a user