Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25eda98612 | ||
|
|
0b909a4d63 | ||
|
|
37298afd77 |
@@ -1,4 +1,18 @@
|
||||
[
|
||||
{
|
||||
"version": "0.27.2",
|
||||
"date": "",
|
||||
"en": [
|
||||
"Bulk operations work on any size of selection — setting a field, fixing frequencies, deleting, marking uploads and exporting the selection all failed with “too many SQL variables” past a few tens of thousands of QSOs. Statements are now issued in slices.",
|
||||
"Elecraft console: the power meter reads in real watts. The K3’s bargraph is relative to a range that flips at 12 W — calibrated against a real radio’s full table, the PC setting picks the range and the bar converts to watts.",
|
||||
"Watchlist: a visual pass toward DXHunter’s look — pink callsigns, counter pills, quieter cards with a hover, the ⚡ back on the DXpedition badge."
|
||||
],
|
||||
"fr": [
|
||||
"Les opérations groupées fonctionnent quelle que soit la taille de la sélection — définir un champ, corriger des fréquences, supprimer, marquer les uploads et exporter la sélection échouaient avec « too many SQL variables » au-delà de quelques dizaines de milliers de QSO. Les requêtes sont désormais émises par tranches.",
|
||||
"Console Elecraft : le wattmètre lit en vrais watts. Le bargraph du K3 est relatif à une gamme qui bascule à 12 W — calibré sur la table complète d’une vraie radio, le réglage PC choisit la gamme et la barre se convertit en watts.",
|
||||
"Watchlist : une passe visuelle vers le look DXHunter — indicatifs roses, compteurs en pastilles, cartes plus feutrées avec survol, le ⚡ de retour sur le badge DXpedition."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.27.1",
|
||||
"date": "",
|
||||
|
||||
@@ -18,7 +18,7 @@ type KenwoodState = {
|
||||
available: boolean; model?: string; elecraft: boolean; mode?: string; data_sub?: string;
|
||||
transmitting: boolean; split: boolean; split_tx_hz?: number;
|
||||
s_meter: number; s_meter_raw: number;
|
||||
power_meter: number; swr: number; swr_raw: number;
|
||||
power_meter: number; power_w?: number; swr: number; swr_raw: number;
|
||||
rf_power: number; af_gain: number; rf_gain: number; mic_gain: number; squelch: number;
|
||||
preamp: boolean; att: boolean; nb: boolean; nr: boolean; agc?: string;
|
||||
filter_hz: number; antenna: number; rit: boolean; xit: boolean; rit_offset: number; key_speed: number;
|
||||
@@ -222,7 +222,8 @@ export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) =>
|
||||
onReportRST(sMeterRST(sp.s, sp.over, view.mode));
|
||||
}}
|
||||
title={t('k3.sMeterHint', { raw: String(view.s_meter_raw) })} />
|
||||
<MeterBar label="PWR" value={view.transmitting ? view.power_meter : 0} lo={0} hi={100} accent="#0ea5e9" />
|
||||
<MeterBar label="PWR" value={view.transmitting ? view.power_meter : 0} lo={0} hi={100} accent="#0ea5e9"
|
||||
display={view.transmitting && view.elecraft ? `${view.power_w ?? 0} W` : undefined} />
|
||||
{/* 0 means "not measured", and it must not render as a perfect 1.0:
|
||||
a match that looks ideal on an antenna nobody has measured is the one
|
||||
reading that can cost a radio. */}
|
||||
|
||||
@@ -247,15 +247,18 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
|
||||
<div className="flex flex-col min-h-0 flex-1 gap-2 p-2 w-full max-w-5xl mx-auto">
|
||||
{/* Header: the counters alone, centred — they are the tab's headline.
|
||||
Everything one INTERACTS with lives on the second row. */}
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<div className="flex items-center justify-center gap-2.5 text-xs text-muted-foreground">
|
||||
<Eye className="size-4 text-primary shrink-0" />
|
||||
<span>
|
||||
{t('wl.cTotal')} <b className="text-foreground">{counters.total}</b>
|
||||
<span className="mx-1.5 opacity-50">|</span>
|
||||
{t('wl.cActive')} <b className="text-info">{counters.active}</b>
|
||||
<span className="mx-1.5 opacity-50">|</span>
|
||||
{t('wl.cNeeded')} <b className="text-warning">{counters.needed}</b>
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">{t('wl.cTotal')}
|
||||
<b className="px-2 py-0.5 rounded bg-muted text-foreground">{counters.total}</b></span>
|
||||
<span className="opacity-40">|</span>
|
||||
<span className="flex items-center gap-1.5">{t('wl.cActive')}
|
||||
<b className="px-2 py-0.5 rounded text-info border border-info/30 bg-info/10">{counters.active}</b></span>
|
||||
<span className="opacity-40">|</span>
|
||||
<span className="flex items-center gap-1.5">{t('wl.cNeeded')}
|
||||
<b className={cn('px-2 py-0.5 rounded border', counters.needed > 0
|
||||
? 'text-warning border-warning/40 bg-warning/10'
|
||||
: 'text-muted-foreground border-border bg-muted/40')}>{counters.needed}</b></span>
|
||||
</div>
|
||||
{/* toolbar */}
|
||||
<div className="flex items-start justify-between gap-x-3 gap-y-1.5 flex-wrap">
|
||||
@@ -328,11 +331,11 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
|
||||
const list = neededOnly ? all.filter((s) => !workedFor(e, s)) : all;
|
||||
return (
|
||||
<div key={e.callsign}
|
||||
className={cn('rounded-lg border bg-card p-3',
|
||||
needed > 0 ? 'border-warning/50' : 'border-border',
|
||||
className={cn('rounded-lg border bg-card/70 p-3 transition-colors hover:bg-accent/20',
|
||||
needed > 0 ? 'border-warning/40' : 'border-border/70',
|
||||
e.isContest && 'border-l-4 border-l-warning')}>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-lg font-bold font-mono text-primary">{e.callsign}</span>
|
||||
<span className="text-lg font-bold font-mono" style={{ color: '#f472b6' }}>{e.callsign}</span>
|
||||
{isOnAir(e) && chip('var(--danger)', t('wl.onAir'), 'animate-pulse')}
|
||||
{e.isContest && (
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-bold bg-warning-muted text-warning-muted-foreground border border-warning-border"
|
||||
@@ -340,7 +343,7 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
|
||||
<Trophy className="size-3" /> {t('wl.contest')}
|
||||
</span>
|
||||
)}
|
||||
{e.isExpedition && chip('var(--chart-5)', t('wl.expedition'))}
|
||||
{e.isExpedition && chip('var(--chart-5)', '⚡ ' + t('wl.expedition'))}
|
||||
{e.clubLogTotalQSOs > 0 && <span className="text-[11px] text-muted-foreground">{e.clubLogTotalQSOs.toLocaleString()} QSOs{e.clubLogQSOs24h > 0 ? ` · ${e.clubLogQSOs24h}/24h` : ''}</span>}
|
||||
{e.clubLogHasOQRS && chip('var(--success)', 'OQRS')}
|
||||
{e.clubLogLiveStream && (
|
||||
@@ -385,8 +388,8 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
|
||||
onClick={() => onSpotSelect?.(s)}
|
||||
onDoubleClick={() => onSpotClick?.(s)}
|
||||
title={t('wl.spotTip')}
|
||||
className={cn('w-full flex items-center gap-2 px-2 py-1.5 rounded text-[11px] bg-muted/40 hover:bg-muted text-left',
|
||||
!done && 'border-l-2 border-warning')}>
|
||||
className={cn('w-full flex items-center gap-2 px-2 py-1.5 rounded text-[11px] bg-muted/25 hover:bg-muted/70 transition-colors text-left',
|
||||
!done && 'border-l-[3px] border-warning')}>
|
||||
{done && <span className='font-bold shrink-0 text-success'>✓</span>}
|
||||
<span className="font-mono font-bold text-info shrink-0">{s.dx_call}</span>
|
||||
<span className="text-muted-foreground truncate flex-1 min-w-0 max-w-56">{(s as any).country ?? ''}</span>
|
||||
|
||||
@@ -1083,6 +1083,7 @@ export namespace cat {
|
||||
s_meter: number;
|
||||
s_meter_raw: number;
|
||||
power_meter: number;
|
||||
power_w: number;
|
||||
swr: number;
|
||||
swr_raw: number;
|
||||
rf_power: number;
|
||||
@@ -1120,6 +1121,7 @@ export namespace cat {
|
||||
this.s_meter = source["s_meter"];
|
||||
this.s_meter_raw = source["s_meter_raw"];
|
||||
this.power_meter = source["power_meter"];
|
||||
this.power_w = source["power_w"];
|
||||
this.swr = source["swr"];
|
||||
this.swr_raw = source["swr_raw"];
|
||||
this.rf_power = source["rf_power"];
|
||||
|
||||
@@ -52,9 +52,15 @@ type KenwoodTXState struct {
|
||||
SMeterRaw int `json:"s_meter_raw"`
|
||||
// PowerMeter is 0-100 while transmitting. SWR is the ratio; 0 means "not
|
||||
// measured", NOT a perfect match.
|
||||
PowerMeter int `json:"power_meter"`
|
||||
SWR float64 `json:"swr"`
|
||||
SWRRaw int `json:"swr_raw"`
|
||||
PowerMeter int `json:"power_meter"`
|
||||
// PowerW is the transmit power in WATTS, derived from the bargraph and the
|
||||
// meter's RANGE. The K3's bar is relative to a range that flips at 12 W —
|
||||
// calibrated against a real one: 10 W showed 83 (10/12), 100 W showed 83
|
||||
// too (100/120). The bar alone never was watts; with the PC setting to
|
||||
// pick the range, it converts. 0 while receiving.
|
||||
PowerW int `json:"power_w"`
|
||||
SWR float64 `json:"swr"`
|
||||
SWRRaw int `json:"swr_raw"`
|
||||
|
||||
RFPower int `json:"rf_power"` // watts, the PC setting
|
||||
AFGain int `json:"af_gain"` // 0-100
|
||||
@@ -162,6 +168,7 @@ func (k *Kenwood) readPanel(mode string, split bool, txHz int64, txNow bool) {
|
||||
// Cleared, not frozen: a power bar left standing after the carrier drops
|
||||
// reads as a live transmission.
|
||||
k.panel.PowerMeter = 0
|
||||
k.panel.PowerW = 0
|
||||
k.panel.SWR, k.panel.SWRRaw = 0, 0
|
||||
k.powerPeak, k.swrPeak = meterPeak{}, meterPeak{}
|
||||
// The S-meter only means anything while receiving.
|
||||
@@ -341,6 +348,13 @@ func (k *Kenwood) readTXMeters() {
|
||||
defer func() { k.noLatch = false }()
|
||||
if v, ok := k.askNum("BG;", "BG", 2); ok {
|
||||
k.panel.PowerMeter = k.powerPeak.update(kenwoodBargraphPercent(v), now)
|
||||
if k.elecraft {
|
||||
scale := 120
|
||||
if k.panel.RFPower > 0 && k.panel.RFPower <= 12 {
|
||||
scale = 12 // the K3's QRP range
|
||||
}
|
||||
k.panel.PowerW = k.panel.PowerMeter * scale / 100
|
||||
}
|
||||
}
|
||||
// SW; — SETTLED, from Elecraft's own release note: three digits, tenths of a
|
||||
// ratio. "SW023;" is 2.3:1, and "SW999;" is the 99.9:1 it reports instead of
|
||||
|
||||
+119
-79
@@ -722,15 +722,14 @@ func (r *Repo) MarkUploadedBatch(ctx context.Context, statusCol, dateCol, date s
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
ph := strings.TrimSuffix(strings.Repeat("?,", len(ids)), ",")
|
||||
args := make([]any, 0, len(ids)+2)
|
||||
args = append(args, date, db.NowISO())
|
||||
for _, id := range ids {
|
||||
args = append(args, id)
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET `+statusCol+` = 'Y', `+dateCol+` = ?, updated_at = ? WHERE id IN (`+ph+`)`,
|
||||
args...)
|
||||
now := db.NowISO()
|
||||
_, err := bulkByIDChunks(ctx, ids, func(ph string, idArgs []any) (int64, error) {
|
||||
args := append([]any{date, now}, idArgs...)
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET `+statusCol+` = 'Y', `+dateCol+` = ?, updated_at = ? WHERE id IN (`+ph+`)`,
|
||||
args...)
|
||||
return 0, err
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("mark uploaded batch (%d): %w", len(ids), err)
|
||||
}
|
||||
@@ -872,6 +871,35 @@ var bulkEditableCols = map[string]bool{
|
||||
// own path, not the text one: the columns are nullable integers, and while
|
||||
// SQLite would coerce "14" quietly, a shared MySQL logbook would not — and an
|
||||
// empty string is NULL here, never "".
|
||||
// bulkByIDChunks runs one UPDATE per slice of ids, small enough for SQLite's
|
||||
// bound-variable cap: the single IN (…) with one placeholder per id worked at
|
||||
// 10 000 QSOs and failed at 168 000 with "too many SQL variables". Each call
|
||||
// gets the placeholder string and the id arguments for its slice; affected
|
||||
// rows are summed. 500 per statement keeps every backend far from any limit
|
||||
// while costing a few hundred statements on the largest logs.
|
||||
func bulkByIDChunks(ctx context.Context, ids []int64, run func(ph string, idArgs []any) (int64, error)) (int64, error) {
|
||||
const chunk = 500
|
||||
var total int64
|
||||
for start := 0; start < len(ids); start += chunk {
|
||||
end := start + chunk
|
||||
if end > len(ids) {
|
||||
end = len(ids)
|
||||
}
|
||||
part := ids[start:end]
|
||||
ph := strings.Repeat("?,", len(part)-1) + "?"
|
||||
args := make([]any, len(part))
|
||||
for i, id := range part {
|
||||
args[i] = id
|
||||
}
|
||||
n, err := run(ph, args)
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
total += n
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
var bulkEditableIntCols = map[string]bool{
|
||||
"my_dxcc": true,
|
||||
"my_cq_zone": true,
|
||||
@@ -886,23 +914,23 @@ func (r *Repo) BulkSetIntField(ctx context.Context, ids []int64, column string,
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
ph := make([]string, len(ids))
|
||||
args := make([]any, 0, len(ids)+2)
|
||||
var val any
|
||||
if v != nil {
|
||||
val = *v
|
||||
}
|
||||
args = append(args, val, db.NowISO())
|
||||
for i, id := range ids {
|
||||
ph[i] = "?"
|
||||
args = append(args, id)
|
||||
}
|
||||
res, err := r.db.ExecContext(ctx,
|
||||
"UPDATE qso SET "+column+" = ?, updated_at = ? WHERE id IN ("+strings.Join(ph, ",")+")", args...)
|
||||
now := db.NowISO()
|
||||
n, err := bulkByIDChunks(ctx, ids, func(ph string, idArgs []any) (int64, error) {
|
||||
args := append([]any{val, now}, idArgs...)
|
||||
res, err := r.db.ExecContext(ctx,
|
||||
"UPDATE qso SET "+column+" = ?, updated_at = ? WHERE id IN ("+ph+")", args...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("bulk set %s: %w", column, err)
|
||||
return n, fmt.Errorf("bulk set %s: %w", column, err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -913,13 +941,6 @@ func (r *Repo) BulkSetField(ctx context.Context, ids []int64, column, value stri
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
ph := make([]string, len(ids))
|
||||
args := make([]any, 0, len(ids)+2)
|
||||
args = append(args, value, db.NowISO())
|
||||
for i, id := range ids {
|
||||
ph[i] = "?"
|
||||
args = append(args, id)
|
||||
}
|
||||
set := column + " = ?, updated_at = ?"
|
||||
if column == "mode" {
|
||||
// A submode belongs to the mode it was recorded under. Left behind, it
|
||||
@@ -928,13 +949,19 @@ func (r *Repo) BulkSetField(ctx context.Context, ids []int64, column, value stri
|
||||
// only outcome that leaves the row meaning what the operator asked for.
|
||||
set += ", submode = ''"
|
||||
}
|
||||
res, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET `+set+` WHERE id IN (`+strings.Join(ph, ",")+`)`,
|
||||
args...)
|
||||
now := db.NowISO()
|
||||
n, err := bulkByIDChunks(ctx, ids, func(ph string, idArgs []any) (int64, error) {
|
||||
args := append([]any{value, now}, idArgs...)
|
||||
res, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET `+set+` WHERE id IN (`+ph+`)`, args...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("bulk set %s: %w", column, err)
|
||||
return n, fmt.Errorf("bulk set %s: %w", column, err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -995,26 +1022,26 @@ func (r *Repo) BulkSetExtra(ctx context.Context, ids []int64, adifKey, value str
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
ph := make([]string, len(ids))
|
||||
args := make([]any, 0, len(ids)+2)
|
||||
head := []any{}
|
||||
expr := `json_set(COALESCE(extras_json, '{}'), '$.` + adifKey + `', ?)`
|
||||
if value == "" {
|
||||
expr = `json_remove(COALESCE(extras_json, '{}'), '$.` + adifKey + `')`
|
||||
} else {
|
||||
args = append(args, value)
|
||||
head = append(head, value)
|
||||
}
|
||||
args = append(args, db.NowISO())
|
||||
for i, id := range ids {
|
||||
ph[i] = "?"
|
||||
args = append(args, id)
|
||||
}
|
||||
res, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET extras_json = `+expr+`, updated_at = ? WHERE id IN (`+strings.Join(ph, ",")+`)`,
|
||||
args...)
|
||||
head = append(head, db.NowISO())
|
||||
n, err := bulkByIDChunks(ctx, ids, func(ph string, idArgs []any) (int64, error) {
|
||||
args := append(append([]any{}, head...), idArgs...)
|
||||
res, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET extras_json = `+expr+`, updated_at = ? WHERE id IN (`+ph+`)`, args...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("bulk set extra %s: %w", adifKey, err)
|
||||
return n, fmt.Errorf("bulk set extra %s: %w", adifKey, err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -1026,20 +1053,19 @@ func (r *Repo) BulkSetFrequency(ctx context.Context, ids []int64, freqHz int64,
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
ph := make([]string, len(ids))
|
||||
args := make([]any, 0, len(ids)+3)
|
||||
args = append(args, freqHz, band, db.NowISO())
|
||||
for i, id := range ids {
|
||||
ph[i] = "?"
|
||||
args = append(args, id)
|
||||
}
|
||||
res, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET freq_hz = ?, band = ?, updated_at = ? WHERE id IN (`+strings.Join(ph, ",")+`)`,
|
||||
args...)
|
||||
now := db.NowISO()
|
||||
n, err := bulkByIDChunks(ctx, ids, func(ph string, idArgs []any) (int64, error) {
|
||||
args := append([]any{freqHz, band, now}, idArgs...)
|
||||
res, err := r.db.ExecContext(ctx,
|
||||
`UPDATE qso SET freq_hz = ?, band = ?, updated_at = ? WHERE id IN (`+ph+`)`, args...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("bulk set frequency: %w", err)
|
||||
return n, fmt.Errorf("bulk set frequency: %w", err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -1201,17 +1227,16 @@ func (r *Repo) DeleteMany(ctx context.Context, ids []int64) (int64, error) {
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
ph := make([]string, len(ids))
|
||||
args := make([]any, len(ids))
|
||||
for i, id := range ids {
|
||||
ph[i] = "?"
|
||||
args[i] = id
|
||||
}
|
||||
res, err := r.db.ExecContext(ctx, `DELETE FROM qso WHERE id IN (`+strings.Join(ph, ",")+`)`, args...)
|
||||
n, err := bulkByIDChunks(ctx, ids, func(ph string, idArgs []any) (int64, error) {
|
||||
res, err := r.db.ExecContext(ctx, `DELETE FROM qso WHERE id IN (`+ph+`)`, idArgs...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("delete qsos: %w", err)
|
||||
return n, fmt.Errorf("delete qsos: %w", err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -1701,27 +1726,42 @@ func (r *Repo) IterateByIDs(ctx context.Context, ids []int64, fn func(QSO) error
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
ph := strings.TrimSuffix(strings.Repeat("?,", len(ids)), ",")
|
||||
args := make([]any, len(ids))
|
||||
for i, id := range ids {
|
||||
args[i] = id
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT `+selectCols+` FROM qso WHERE id IN (`+ph+`) ORDER BY qso_date ASC, id ASC`, args...)
|
||||
// Chunked like every other by-ids statement (the one-placeholder-per-id IN
|
||||
// died at 168k with "too many SQL variables") — and because each chunk is
|
||||
// only locally ordered, the rows are collected and sorted once at the end
|
||||
// so the chronological contract holds across chunks.
|
||||
var all []QSO
|
||||
_, err := bulkByIDChunks(ctx, ids, func(ph string, idArgs []any) (int64, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT `+selectCols+` FROM qso WHERE id IN (`+ph+`)`, idArgs...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
q, err := scanQSO(rows)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
all = append(all, q)
|
||||
}
|
||||
return 0, rows.Err()
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("query qso: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
q, err := scanQSO(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
sort.Slice(all, func(i, j int) bool {
|
||||
if !all[i].QSODate.Equal(all[j].QSODate) {
|
||||
return all[i].QSODate.Before(all[j].QSODate)
|
||||
}
|
||||
return all[i].ID < all[j].ID
|
||||
})
|
||||
for _, q := range all {
|
||||
if err := fn(q); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return rows.Err()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GridKey builds the lookup key for the worked-grid index.
|
||||
|
||||
Reference in New Issue
Block a user