up
This commit is contained in:
+100
-37
@@ -10,6 +10,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hamlog/internal/db"
|
||||
)
|
||||
|
||||
// MergeNonZero copies every non-zero field of src onto dst. Zero-value src
|
||||
@@ -255,6 +257,19 @@ var columnCount = countColumns(columnList)
|
||||
// insertPlaceholders returns "?,?,?,..." matching columnCount.
|
||||
var insertPlaceholders = buildInsertPlaceholders()
|
||||
|
||||
// insertCols/insertVals append created_at + updated_at so they're set from Go
|
||||
// (NowISO) on every backend — MySQL has no strftime default, and binding the
|
||||
// timestamp keeps a single backend-agnostic INSERT. insertArgs pairs with them.
|
||||
const insertCols = columnList + `, created_at, updated_at`
|
||||
|
||||
var insertVals = insertPlaceholders + ",?,?"
|
||||
|
||||
// insertArgs returns the column values plus the two timestamps for an INSERT.
|
||||
func (q *QSO) insertArgs() []any {
|
||||
now := db.NowISO()
|
||||
return append(q.args(), now, now)
|
||||
}
|
||||
|
||||
func countColumns(s string) int {
|
||||
n := 1
|
||||
for i := 0; i < len(s); i++ {
|
||||
@@ -349,34 +364,30 @@ func (r *Repo) Add(ctx context.Context, q QSO) (int64, error) {
|
||||
q.QSODate = time.Now().UTC()
|
||||
}
|
||||
res, err := r.db.ExecContext(ctx,
|
||||
`INSERT INTO qso (`+columnList+`) VALUES (`+insertPlaceholders+`)`,
|
||||
q.args()...)
|
||||
`INSERT INTO qso (`+insertCols+`) VALUES (`+insertVals+`)`,
|
||||
q.insertArgs()...)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("insert qso: %w", err)
|
||||
}
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
// AddBatch inserts many QSOs inside a single transaction using a prepared
|
||||
// statement. Empty-callsign records are skipped. Returns rows inserted.
|
||||
// batchInsertRows is how many QSOs go into one multi-row INSERT on MySQL. Each
|
||||
// row carries ~135 columns, so 200 rows ≈ 27k bound parameters — well under
|
||||
// MySQL's 65535-placeholder limit and a modest packet — while cutting network
|
||||
// round-trips ~200× versus one INSERT per row (critical for a remote server).
|
||||
const batchInsertRows = 200
|
||||
|
||||
// AddBatch inserts many QSOs inside a single transaction. Empty-callsign records
|
||||
// are skipped. On MySQL it uses chunked multi-row INSERTs so a 27k-record import
|
||||
// over a remote link takes seconds, not many minutes; on local SQLite a prepared
|
||||
// statement per row is already fast.
|
||||
func (r *Repo) AddBatch(ctx context.Context, qsos []QSO) (int64, error) {
|
||||
if len(qsos) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
stmt, err := tx.PrepareContext(ctx,
|
||||
`INSERT INTO qso (`+columnList+`) VALUES (`+insertPlaceholders+`)`)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("prepare batch insert: %w", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
var inserted int64
|
||||
// Normalise and drop empty-callsign records up front.
|
||||
rows := make([]QSO, 0, len(qsos))
|
||||
for _, q := range qsos {
|
||||
q.Callsign = strings.ToUpper(strings.TrimSpace(q.Callsign))
|
||||
if q.Callsign == "" {
|
||||
@@ -385,10 +396,57 @@ func (r *Repo) AddBatch(ctx context.Context, qsos []QSO) (int64, error) {
|
||||
if q.QSODate.IsZero() {
|
||||
q.QSODate = time.Now().UTC()
|
||||
}
|
||||
if _, err := stmt.ExecContext(ctx, q.args()...); err != nil {
|
||||
return inserted, fmt.Errorf("insert qso %q: %w", q.Callsign, err)
|
||||
rows = append(rows, q)
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
var inserted int64
|
||||
if db.IsMySQL() {
|
||||
rowPlaceholder := "(" + insertVals + ")"
|
||||
for start := 0; start < len(rows); start += batchInsertRows {
|
||||
end := start + batchInsertRows
|
||||
if end > len(rows) {
|
||||
end = len(rows)
|
||||
}
|
||||
chunk := rows[start:end]
|
||||
var sb strings.Builder
|
||||
sb.WriteString("INSERT INTO qso (")
|
||||
sb.WriteString(insertCols)
|
||||
sb.WriteString(") VALUES ")
|
||||
args := make([]any, 0, len(chunk)*(columnCount+2))
|
||||
for j := range chunk {
|
||||
if j > 0 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
sb.WriteString(rowPlaceholder)
|
||||
args = append(args, chunk[j].insertArgs()...)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, sb.String(), args...); err != nil {
|
||||
return inserted, fmt.Errorf("batch insert: %w", err)
|
||||
}
|
||||
inserted += int64(len(chunk))
|
||||
}
|
||||
} else {
|
||||
stmt, err := tx.PrepareContext(ctx,
|
||||
`INSERT INTO qso (`+insertCols+`) VALUES (`+insertVals+`)`)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("prepare batch insert: %w", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
for i := range rows {
|
||||
if _, err := stmt.ExecContext(ctx, rows[i].insertArgs()...); err != nil {
|
||||
return inserted, fmt.Errorf("insert qso %q: %w", rows[i].Callsign, err)
|
||||
}
|
||||
inserted++
|
||||
}
|
||||
inserted++
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, fmt.Errorf("commit batch: %w", err)
|
||||
@@ -455,8 +513,8 @@ func (r *Repo) ListForUpload(ctx context.Context, column, value string) ([]Uploa
|
||||
func (r *Repo) MarkQRZUploaded(ctx context.Context, id int64, date string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET qrzcom_qso_upload_status = 'Y', qrzcom_qso_upload_date = ?,
|
||||
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
|
||||
date, id)
|
||||
updated_at = ? WHERE id = ?`,
|
||||
date, db.NowISO(), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mark qrz uploaded %d: %w", id, err)
|
||||
}
|
||||
@@ -468,8 +526,8 @@ func (r *Repo) MarkQRZUploaded(ctx context.Context, id int64, date string) error
|
||||
func (r *Repo) MarkClublogUploaded(ctx context.Context, id int64, date string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET clublog_qso_upload_status = 'Y', clublog_qso_upload_date = ?,
|
||||
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
|
||||
date, id)
|
||||
updated_at = ? WHERE id = ?`,
|
||||
date, db.NowISO(), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mark clublog uploaded %d: %w", id, err)
|
||||
}
|
||||
@@ -481,8 +539,8 @@ func (r *Repo) MarkClublogUploaded(ctx context.Context, id int64, date string) e
|
||||
func (r *Repo) MarkLoTWUploaded(ctx context.Context, id int64, date string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET lotw_sent = 'Y', lotw_sent_date = ?,
|
||||
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
|
||||
date, id)
|
||||
updated_at = ? WHERE id = ?`,
|
||||
date, db.NowISO(), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mark lotw uploaded %d: %w", id, err)
|
||||
}
|
||||
@@ -494,8 +552,8 @@ func (r *Repo) MarkLoTWUploaded(ctx context.Context, id int64, date string) erro
|
||||
func (r *Repo) MarkEQSLSent(ctx context.Context, id int64, date string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET eqsl_sent = 'Y', eqsl_sent_date = ?,
|
||||
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
|
||||
date, id)
|
||||
updated_at = ? WHERE id = ?`,
|
||||
date, db.NowISO(), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mark eqsl sent %d: %w", id, err)
|
||||
}
|
||||
@@ -515,9 +573,9 @@ func (r *Repo) Update(ctx context.Context, q QSO) error {
|
||||
q.QSODate = time.Now().UTC()
|
||||
}
|
||||
setClause := buildUpdateSetClause()
|
||||
args := append(q.args(), q.ID)
|
||||
args := append(q.args(), db.NowISO(), q.ID)
|
||||
res, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET `+setClause+`, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
|
||||
`UPDATE qso SET `+setClause+`, updated_at = ? WHERE id = ?`,
|
||||
args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update qso %d: %w", q.ID, err)
|
||||
@@ -714,6 +772,11 @@ func columnExpr(field string) (string, bool) {
|
||||
return f, true
|
||||
}
|
||||
if key, ok := filterableExtras[f]; ok {
|
||||
if db.IsMySQL() {
|
||||
// JSON_EXTRACT errors on an invalid/empty document, so guard with
|
||||
// NULLIF; JSON_UNQUOTE strips the quotes MySQL adds around strings.
|
||||
return "JSON_UNQUOTE(JSON_EXTRACT(NULLIF(extras_json,''), '$." + key + "'))", true
|
||||
}
|
||||
return "json_extract(extras_json, '$." + key + "')", true
|
||||
}
|
||||
return "", false
|
||||
@@ -1348,7 +1411,7 @@ func (r *Repo) Count(ctx context.Context) (int64, error) {
|
||||
// far cheaper than N exists-queries during the import loop.
|
||||
func (r *Repo) ExistingDedupeKeys(ctx context.Context) (map[string]struct{}, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT callsign, strftime('%Y-%m-%dT%H:%M', qso_date), band, mode
|
||||
SELECT callsign, substr(qso_date, 1, 16), band, mode
|
||||
FROM qso`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1376,7 +1439,7 @@ func DedupeKey(callsign, qsoDateMinute, band, mode string) string {
|
||||
// confirmations back to local QSOs.
|
||||
func (r *Repo) DedupeKeyIDs(ctx context.Context) (map[string]int64, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, callsign, strftime('%Y-%m-%dT%H:%M', qso_date), band, mode
|
||||
SELECT id, callsign, substr(qso_date, 1, 16), band, mode
|
||||
FROM qso`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1463,8 +1526,8 @@ func (r *Repo) ConfirmedSlots(ctx context.Context, cols []string) (ConfirmedSets
|
||||
func (r *Repo) MarkQRZConfirmed(ctx context.Context, id int64, date string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET qrzcom_qso_download_status = 'Y', qrzcom_qso_download_date = ?,
|
||||
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
|
||||
date, id)
|
||||
updated_at = ? WHERE id = ?`,
|
||||
date, db.NowISO(), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mark qrz confirmed %d: %w", id, err)
|
||||
}
|
||||
@@ -1476,8 +1539,8 @@ func (r *Repo) MarkQRZConfirmed(ctx context.Context, id int64, date string) erro
|
||||
func (r *Repo) MarkLoTWConfirmed(ctx context.Context, id int64, date string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET lotw_rcvd = 'Y', lotw_rcvd_date = ?,
|
||||
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
|
||||
date, id)
|
||||
updated_at = ? WHERE id = ?`,
|
||||
date, db.NowISO(), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mark lotw confirmed %d: %w", id, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user