feat: materialize award references into the QSO row (award_refs column)
Award refs are now computed once and stored on the QSO as a JSON column
(award_refs, e.g. {"DDFM":"74","WAJA":"12"}) instead of recomputing the whole
award engine on every grid page load. Written on log/edit/UDP-import and
bulk-recomputed when an award definition or reference list changes; a one-time
per-logbook backfill materializes pre-existing QSOs. The grid reads the column
directly like any other field, so it's fast and available everywhere (including
the shared MySQL logbook).
Also fix per-profile grid column layout: the DB copy was already per-profile,
but the localStorage cache used one global namespace and shadowed it, so a
profile switch kept the previous profile's columns/widths. gridPrefs now scopes
the cache by active profile id and the grids remount on profile:changed.
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
-- Materialised award references per QSO. As soon as a QSO matches an award
|
||||
-- (via the operator's award definitions) or a reference is set by hand, the
|
||||
-- resolved reference(s) are stored here as a compact JSON object keyed by award
|
||||
-- code, e.g. {"DDFM":"74","WAJA":"12"}. The grid's per-award columns then read
|
||||
-- straight from the row like any other column instead of recomputing the whole
|
||||
-- award engine on every page load — much faster, and easy to display anywhere
|
||||
-- (including the shared MySQL logbook). Kept in step by the app: written on log
|
||||
-- / edit / UDP-import and bulk-recomputed when an award definition or reference
|
||||
-- list changes. SQLite ADD COLUMN is metadata-only, fast even on large logbooks.
|
||||
ALTER TABLE qso ADD COLUMN award_refs TEXT;
|
||||
+58
-2
@@ -197,6 +197,14 @@ type QSO struct {
|
||||
// ADIF field names (e.g. "MS_SHOWER"); values are the raw string content.
|
||||
Extras map[string]string `json:"extras,omitempty"`
|
||||
|
||||
// AwardRefs is the materialised award-reference JSON for this QSO — a compact
|
||||
// object keyed by award code, e.g. {"DDFM":"74","WAJA":"12"}. Derived (the app
|
||||
// computes it on log/edit and bulk-recomputes it when awards change) and stored
|
||||
// so the grid's award columns read straight from the row. Written ONLY via
|
||||
// SetAwardRefs, never through the normal insert/update column list, so an edit
|
||||
// that doesn't know about it can't clobber it.
|
||||
AwardRefs string `json:"award_refs,omitempty"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -250,7 +258,10 @@ const columnList = `callsign, qso_date, qso_date_off, band, band_rx, mode, submo
|
||||
credit_granted, credit_submitted, my_arrl_sect, my_vucc_grids,
|
||||
extras_json`
|
||||
|
||||
const selectCols = `id, ` + columnList + `, created_at, updated_at`
|
||||
// award_refs is read here but is NOT part of columnList (the insert/update
|
||||
// write path) — it is a derived cache written only via SetAwardRefs, so a
|
||||
// normal QSO write can never clobber it.
|
||||
const selectCols = `id, ` + columnList + `, award_refs, created_at, updated_at`
|
||||
|
||||
// columnCount is derived from columnList at init so they can never drift.
|
||||
var columnCount = countColumns(columnList)
|
||||
@@ -813,6 +824,49 @@ func (r *Repo) SetExtra(ctx context.Context, id int64, key, value string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAwardRefs stores the materialised award-reference JSON for one QSO. Like
|
||||
// SetExtra it is a targeted single-column UPDATE — never a full-row write — so it
|
||||
// cannot clobber a field another action changed meanwhile. updated_at is left
|
||||
// UNTOUCHED on purpose: award_refs is a derived cache, and bumping updated_at
|
||||
// would masquerade as a real edit (re-triggering uploads / sync that watch it).
|
||||
func (r *Repo) SetAwardRefs(ctx context.Context, id int64, jsonStr string) error {
|
||||
if id == 0 {
|
||||
return fmt.Errorf("missing id")
|
||||
}
|
||||
if _, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET award_refs = ? WHERE id = ?`, jsonStr, id); err != nil {
|
||||
return fmt.Errorf("set award_refs: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAwardRefsBatch stores materialised award refs for many QSOs in a single
|
||||
// transaction — used by the bulk recompute so a large logbook on a remote MySQL
|
||||
// is one round-trip's worth of work, not N. Like SetAwardRefs it touches only the
|
||||
// award_refs column and leaves updated_at alone (derived cache). A nil/empty map
|
||||
// is a no-op.
|
||||
func (r *Repo) SetAwardRefsBatch(ctx context.Context, byID map[int64]string) error {
|
||||
if len(byID) == 0 {
|
||||
return nil
|
||||
}
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
stmt, err := tx.PrepareContext(ctx, `UPDATE qso SET award_refs = ? WHERE id = ?`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare: %w", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
for id, js := range byID {
|
||||
if _, err := stmt.ExecContext(ctx, js, id); err != nil {
|
||||
return fmt.Errorf("set award_refs %d: %w", id, err)
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// Update overwrites all editable fields of an existing QSO. updated_at is bumped.
|
||||
func (r *Repo) Update(ctx context.Context, q QSO) error {
|
||||
if q.ID == 0 {
|
||||
@@ -2374,6 +2428,7 @@ func scanQSO(s scanner) (QSO, error) {
|
||||
creditGranted, creditSubmitted sql.NullString
|
||||
myARRLSect, myVUCCGrids sql.NullString
|
||||
extrasJSON sql.NullString
|
||||
awardRefs sql.NullString
|
||||
createdStr, updatedStr string
|
||||
)
|
||||
if err := s.Scan(
|
||||
@@ -2401,7 +2456,7 @@ func scanQSO(s scanner) (QSO, error) {
|
||||
&skcc, &fists, &tenTen, &contactedOp, &eqCall, &pfx, &myName, &class,
|
||||
&darcDOK, &myDarcDOK, ®ion, &silentKey, &swl, &qsoComplete, &qsoRandom,
|
||||
&creditGranted, &creditSubmitted, &myARRLSect, &myVUCCGrids,
|
||||
&extrasJSON, &createdStr, &updatedStr,
|
||||
&extrasJSON, &awardRefs, &createdStr, &updatedStr,
|
||||
); err != nil {
|
||||
return QSO{}, fmt.Errorf("scan qso: %w", err)
|
||||
}
|
||||
@@ -2598,6 +2653,7 @@ func scanQSO(s scanner) (QSO, error) {
|
||||
q.MyARRLSect = myARRLSect.String
|
||||
q.MyVUCCGrids = myVUCCGrids.String
|
||||
q.Extras = decodeExtras(extrasJSON.String)
|
||||
q.AwardRefs = awardRefs.String
|
||||
return q, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user