first commit
This commit is contained in:
103
internal/db/db.go
Normal file
103
internal/db/db.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// DB est le wrapper principal autour de la connexion SQLite
|
||||
type DB struct {
|
||||
conn *sql.DB
|
||||
}
|
||||
|
||||
// Open ouvre (ou crée) la base SQLite et applique les migrations
|
||||
func Open(path string) (*DB, error) {
|
||||
conn, err := sql.Open("sqlite3", path+"?_journal_mode=WAL&_busy_timeout=5000")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open db: %w", err)
|
||||
}
|
||||
|
||||
conn.SetMaxOpenConns(1)
|
||||
conn.SetMaxIdleConns(1)
|
||||
|
||||
db := &DB{conn: conn}
|
||||
if err := db.migrate(); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (db *DB) Close() error {
|
||||
return db.conn.Close()
|
||||
}
|
||||
|
||||
func (db *DB) Conn() *sql.DB {
|
||||
return db.conn
|
||||
}
|
||||
|
||||
// migrate crée les tables si elles n'existent pas
|
||||
func (db *DB) migrate() error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := db.conn.ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS spots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dx TEXT NOT NULL,
|
||||
spotter TEXT NOT NULL,
|
||||
frequency_khz REAL NOT NULL,
|
||||
frequency_mhz TEXT NOT NULL,
|
||||
band TEXT NOT NULL,
|
||||
mode TEXT NOT NULL,
|
||||
comment TEXT DEFAULT '',
|
||||
original_comment TEXT DEFAULT '',
|
||||
time TEXT DEFAULT '',
|
||||
timestamp INTEGER NOT NULL,
|
||||
dxcc TEXT DEFAULT '',
|
||||
country_name TEXT DEFAULT '',
|
||||
new_dxcc INTEGER DEFAULT 0,
|
||||
new_band INTEGER DEFAULT 0,
|
||||
new_mode INTEGER DEFAULT 0,
|
||||
new_slot INTEGER DEFAULT 0,
|
||||
callsign_worked INTEGER DEFAULT 0,
|
||||
in_watchlist INTEGER DEFAULT 0,
|
||||
pota_ref TEXT DEFAULT '',
|
||||
sota_ref TEXT DEFAULT '',
|
||||
park_name TEXT DEFAULT '',
|
||||
summit_name TEXT DEFAULT '',
|
||||
flex_spot_number INTEGER DEFAULT 0,
|
||||
command_number INTEGER DEFAULT 0,
|
||||
color TEXT DEFAULT '#ffeaeaea',
|
||||
background_color TEXT DEFAULT '#ff000000',
|
||||
priority TEXT DEFAULT '5',
|
||||
life_time TEXT DEFAULT '900',
|
||||
cluster_name TEXT DEFAULT '',
|
||||
source INTEGER DEFAULT 0
|
||||
)`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create spots table: %w", err)
|
||||
}
|
||||
|
||||
// Index pour les recherches fréquentes
|
||||
_, err = db.conn.ExecContext(ctx, `
|
||||
CREATE INDEX IF NOT EXISTS idx_spots_dx_band ON spots(dx, band);
|
||||
CREATE INDEX IF NOT EXISTS idx_spots_timestamp ON spots(timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_spots_flex_number ON spots(flex_spot_number);
|
||||
CREATE INDEX IF NOT EXISTS idx_spots_command_number ON spots(command_number);
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create indexes: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAll supprime tous les spots (appelé au démarrage comme l'ancien code)
|
||||
func (db *DB) DeleteAll(ctx context.Context) error {
|
||||
_, err := db.conn.ExecContext(ctx, `DELETE FROM spots`)
|
||||
return err
|
||||
}
|
||||
214
internal/db/spots_store.go
Normal file
214
internal/db/spots_store.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/user/flexdxcluster2/internal/spot"
|
||||
)
|
||||
|
||||
type SpotsStore struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
func NewSpotsStore(db *DB) *SpotsStore {
|
||||
return &SpotsStore{db: db}
|
||||
}
|
||||
|
||||
// Create insère un nouveau spot et retourne son ID
|
||||
func (s *SpotsStore) Create(ctx context.Context, sp spot.Spot) (int64, error) {
|
||||
res, err := s.db.conn.ExecContext(ctx, `
|
||||
INSERT INTO spots (
|
||||
dx, spotter, frequency_khz, frequency_mhz, band, mode,
|
||||
comment, original_comment, time, timestamp,
|
||||
dxcc, country_name,
|
||||
new_dxcc, new_band, new_mode, new_slot, callsign_worked, in_watchlist,
|
||||
pota_ref, sota_ref, park_name, summit_name,
|
||||
flex_spot_number, command_number,
|
||||
color, background_color, priority, life_time,
|
||||
cluster_name, source
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
sp.DX, sp.Spotter, sp.FrequencyKHz, sp.FrequencyMHz, sp.Band, sp.Mode,
|
||||
sp.Comment, sp.OriginalComment, sp.Time, sp.Timestamp,
|
||||
sp.DXCC, sp.CountryName,
|
||||
boolToInt(sp.NewDXCC), boolToInt(sp.NewBand), boolToInt(sp.NewMode),
|
||||
boolToInt(sp.NewSlot), boolToInt(sp.CallsignWorked), boolToInt(sp.InWatchlist),
|
||||
sp.POTARef, sp.SOTARef, sp.ParkName, sp.SummitName,
|
||||
sp.FlexSpotNumber, sp.CommandNumber,
|
||||
sp.Color, sp.BackgroundColor, sp.Priority, sp.LifeTime,
|
||||
sp.ClusterName, int(sp.Source),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("create spot: %w", err)
|
||||
}
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
// GetAll retourne tous les spots, optionnellement filtrés par bande
|
||||
func (s *SpotsStore) GetAll(ctx context.Context, band string) ([]spot.Spot, error) {
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
|
||||
if band == "" || band == "0" || band == "ALL" {
|
||||
rows, err = s.db.conn.QueryContext(ctx,
|
||||
`SELECT * FROM spots ORDER BY timestamp DESC`)
|
||||
} else {
|
||||
rows, err = s.db.conn.QueryContext(ctx,
|
||||
`SELECT * FROM spots WHERE band = ? ORDER BY timestamp DESC`, band)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanSpots(rows)
|
||||
}
|
||||
|
||||
// FindByDXAndBand cherche un spot existant pour le même DX sur la même bande
|
||||
func (s *SpotsStore) FindByDXAndBand(ctx context.Context, dx, band string) (*spot.Spot, error) {
|
||||
row := s.db.conn.QueryRowContext(ctx,
|
||||
`SELECT * FROM spots WHERE dx = ? AND band = ? ORDER BY timestamp DESC LIMIT 1`,
|
||||
dx, band)
|
||||
sp, err := scanSpot(row)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return sp, err
|
||||
}
|
||||
|
||||
// FindByCommandNumber cherche un spot par son numéro de commande Flex
|
||||
func (s *SpotsStore) FindByCommandNumber(ctx context.Context, cmdNum int) (*spot.Spot, error) {
|
||||
row := s.db.conn.QueryRowContext(ctx,
|
||||
`SELECT * FROM spots WHERE command_number = ? LIMIT 1`, cmdNum)
|
||||
sp, err := scanSpot(row)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return sp, err
|
||||
}
|
||||
|
||||
// FindByFlexSpotNumber cherche un spot par son numéro de spot Flex
|
||||
func (s *SpotsStore) FindByFlexSpotNumber(ctx context.Context, flexNum int) (*spot.Spot, error) {
|
||||
row := s.db.conn.QueryRowContext(ctx,
|
||||
`SELECT * FROM spots WHERE flex_spot_number = ? LIMIT 1`, flexNum)
|
||||
sp, err := scanSpot(row)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return sp, err
|
||||
}
|
||||
|
||||
// UpdateFlexSpotNumber met à jour le numéro de spot Flex après confirmation du Flex
|
||||
func (s *SpotsStore) UpdateFlexSpotNumber(ctx context.Context, cmdNum, flexNum int) error {
|
||||
_, err := s.db.conn.ExecContext(ctx,
|
||||
`UPDATE spots SET flex_spot_number = ? WHERE command_number = ?`,
|
||||
flexNum, cmdNum)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteByFlexSpotNumber supprime un spot par son numéro Flex
|
||||
func (s *SpotsStore) DeleteByFlexSpotNumber(ctx context.Context, flexNum int) error {
|
||||
_, err := s.db.conn.ExecContext(ctx,
|
||||
`DELETE FROM spots WHERE flex_spot_number = ?`, flexNum)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteByID supprime un spot par son ID
|
||||
func (s *SpotsStore) DeleteByID(ctx context.Context, id int64) error {
|
||||
_, err := s.db.conn.ExecContext(ctx,
|
||||
`DELETE FROM spots WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteExpired supprime les spots expirés selon leur lifetime
|
||||
func (s *SpotsStore) DeleteExpired(ctx context.Context, lifetimeSeconds int64) ([]spot.Spot, error) {
|
||||
cutoff := time.Now().Unix() - lifetimeSeconds
|
||||
rows, err := s.db.conn.QueryContext(ctx,
|
||||
`SELECT * FROM spots WHERE timestamp < ?`, cutoff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expired, err := scanSpots(rows)
|
||||
rows.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = s.db.conn.ExecContext(ctx,
|
||||
`DELETE FROM spots WHERE timestamp < ?`, cutoff)
|
||||
return expired, err
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func boolToInt(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func scanSpots(rows *sql.Rows) ([]spot.Spot, error) {
|
||||
var spots []spot.Spot
|
||||
for rows.Next() {
|
||||
sp, err := scanSpotFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spots = append(spots, *sp)
|
||||
}
|
||||
return spots, rows.Err()
|
||||
}
|
||||
|
||||
func scanSpot(row *sql.Row) (*spot.Spot, error) {
|
||||
var sp spot.Spot
|
||||
var newDXCC, newBand, newMode, newSlot, worked, inWatchlist, source int
|
||||
err := row.Scan(
|
||||
&sp.ID, &sp.DX, &sp.Spotter, &sp.FrequencyKHz, &sp.FrequencyMHz,
|
||||
&sp.Band, &sp.Mode, &sp.Comment, &sp.OriginalComment, &sp.Time, &sp.Timestamp,
|
||||
&sp.DXCC, &sp.CountryName,
|
||||
&newDXCC, &newBand, &newMode, &newSlot, &worked, &inWatchlist,
|
||||
&sp.POTARef, &sp.SOTARef, &sp.ParkName, &sp.SummitName,
|
||||
&sp.FlexSpotNumber, &sp.CommandNumber,
|
||||
&sp.Color, &sp.BackgroundColor, &sp.Priority, &sp.LifeTime,
|
||||
&sp.ClusterName, &source,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sp.NewDXCC = newDXCC == 1
|
||||
sp.NewBand = newBand == 1
|
||||
sp.NewMode = newMode == 1
|
||||
sp.NewSlot = newSlot == 1
|
||||
sp.CallsignWorked = worked == 1
|
||||
sp.InWatchlist = inWatchlist == 1
|
||||
sp.Source = spot.SpotSource(source)
|
||||
return &sp, nil
|
||||
}
|
||||
|
||||
func scanSpotFromRows(rows *sql.Rows) (*spot.Spot, error) {
|
||||
var sp spot.Spot
|
||||
var newDXCC, newBand, newMode, newSlot, worked, inWatchlist, source int
|
||||
err := rows.Scan(
|
||||
&sp.ID, &sp.DX, &sp.Spotter, &sp.FrequencyKHz, &sp.FrequencyMHz,
|
||||
&sp.Band, &sp.Mode, &sp.Comment, &sp.OriginalComment, &sp.Time, &sp.Timestamp,
|
||||
&sp.DXCC, &sp.CountryName,
|
||||
&newDXCC, &newBand, &newMode, &newSlot, &worked, &inWatchlist,
|
||||
&sp.POTARef, &sp.SOTARef, &sp.ParkName, &sp.SummitName,
|
||||
&sp.FlexSpotNumber, &sp.CommandNumber,
|
||||
&sp.Color, &sp.BackgroundColor, &sp.Priority, &sp.LifeTime,
|
||||
&sp.ClusterName, &source,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sp.NewDXCC = newDXCC == 1
|
||||
sp.NewBand = newBand == 1
|
||||
sp.NewMode = newMode == 1
|
||||
sp.NewSlot = newSlot == 1
|
||||
sp.CallsignWorked = worked == 1
|
||||
sp.InWatchlist = inWatchlist == 1
|
||||
sp.Source = spot.SpotSource(source)
|
||||
return &sp, nil
|
||||
}
|
||||
Reference in New Issue
Block a user