Replaced github.com/mattn/go-sqlite3 (CGO-based) with modernc.org/sqlite (pure Go implementation) to enable CGO-free builds while maintaining full SQLite functionality. Benefits: - ✅ Cross-compilation without C compiler requirements - ✅ Simplified build environment (no CGO_ENABLED=1) - ✅ Reduced deployment dependencies - ✅ Easier container builds and static linking - ✅ Full SQLite compatibility maintained Changes: - Updated go.mod to use modernc.org/sqlite v1.34.4 - Updated database import to use pure Go SQLite driver - Removed CGO_ENABLED=1 from all Makefile build targets - Verified all binaries (skyview, beast-dump, skyview-data) build successfully The modernc.org/sqlite package is a cgo-free port of SQLite that transpiles the original C code to Go, providing identical functionality without CGO. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
260 lines
7.4 KiB
Go
260 lines
7.4 KiB
Go
// Package database provides persistent storage for aircraft data and callsign enhancement
|
|
// using SQLite with versioned schema migrations and comprehensive error handling.
|
|
//
|
|
// The database system supports:
|
|
// - Aircraft position history with configurable retention
|
|
// - Embedded OpenFlights airline and airport data
|
|
// - External API result caching with TTL
|
|
// - Schema migrations with rollback support
|
|
// - Privacy mode for complete offline operation
|
|
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite" // Pure Go SQLite driver (CGO-free)
|
|
)
|
|
|
|
// Database represents the main database connection and operations
|
|
type Database struct {
|
|
conn *sql.DB
|
|
config *Config
|
|
migrator *Migrator
|
|
callsign *CallsignManager
|
|
history *HistoryManager
|
|
}
|
|
|
|
// Config holds database configuration options
|
|
type Config struct {
|
|
// Database file path (auto-resolved if empty)
|
|
Path string `json:"path"`
|
|
|
|
// Data retention settings
|
|
MaxHistoryDays int `json:"max_history_days"` // 0 = unlimited
|
|
BackupOnUpgrade bool `json:"backup_on_upgrade"`
|
|
|
|
// Connection settings
|
|
MaxOpenConns int `json:"max_open_conns"` // Default: 10
|
|
MaxIdleConns int `json:"max_idle_conns"` // Default: 5
|
|
ConnMaxLifetime time.Duration `json:"conn_max_lifetime"` // Default: 1 hour
|
|
|
|
// Maintenance settings
|
|
VacuumInterval time.Duration `json:"vacuum_interval"` // Default: 24 hours
|
|
CleanupInterval time.Duration `json:"cleanup_interval"` // Default: 1 hour
|
|
|
|
// Compression settings
|
|
EnableCompression bool `json:"enable_compression"` // Enable automatic compression
|
|
CompressionLevel int `json:"compression_level"` // Compression level (1-9, default: 6)
|
|
PageSize int `json:"page_size"` // SQLite page size (default: 4096)
|
|
}
|
|
|
|
// AircraftHistoryRecord represents a stored aircraft position update
|
|
type AircraftHistoryRecord struct {
|
|
ID int64 `json:"id"`
|
|
ICAO string `json:"icao"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Latitude *float64 `json:"latitude,omitempty"`
|
|
Longitude *float64 `json:"longitude,omitempty"`
|
|
Altitude *int `json:"altitude,omitempty"`
|
|
Speed *int `json:"speed,omitempty"`
|
|
Track *int `json:"track,omitempty"`
|
|
VerticalRate *int `json:"vertical_rate,omitempty"`
|
|
Squawk *string `json:"squawk,omitempty"`
|
|
Callsign *string `json:"callsign,omitempty"`
|
|
SourceID string `json:"source_id"`
|
|
SignalStrength *float64 `json:"signal_strength,omitempty"`
|
|
}
|
|
|
|
// CallsignInfo represents enriched callsign information
|
|
type CallsignInfo struct {
|
|
OriginalCallsign string `json:"original_callsign"`
|
|
AirlineCode string `json:"airline_code"`
|
|
FlightNumber string `json:"flight_number"`
|
|
AirlineName string `json:"airline_name"`
|
|
AirlineCountry string `json:"airline_country"`
|
|
DisplayName string `json:"display_name"`
|
|
IsValid bool `json:"is_valid"`
|
|
LastUpdated time.Time `json:"last_updated"`
|
|
}
|
|
|
|
// AirlineRecord represents embedded airline data from OpenFlights
|
|
type AirlineRecord struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
Alias string `json:"alias"`
|
|
IATACode string `json:"iata_code"`
|
|
ICAOCode string `json:"icao_code"`
|
|
Callsign string `json:"callsign"`
|
|
Country string `json:"country"`
|
|
Active bool `json:"active"`
|
|
}
|
|
|
|
// AirportRecord represents embedded airport data from OpenFlights
|
|
type AirportRecord struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
City string `json:"city"`
|
|
Country string `json:"country"`
|
|
IATA string `json:"iata"`
|
|
ICAO string `json:"icao"`
|
|
Latitude float64 `json:"latitude"`
|
|
Longitude float64 `json:"longitude"`
|
|
Altitude int `json:"altitude"`
|
|
TimezoneOffset float64 `json:"timezone_offset"`
|
|
DST string `json:"dst"`
|
|
Timezone string `json:"timezone"`
|
|
}
|
|
|
|
// DatabaseError represents database operation errors
|
|
type DatabaseError struct {
|
|
Operation string
|
|
Err error
|
|
Query string
|
|
Retryable bool
|
|
}
|
|
|
|
func (e *DatabaseError) Error() string {
|
|
if e.Query != "" {
|
|
return fmt.Sprintf("database %s error: %v (query: %s)", e.Operation, e.Err, e.Query)
|
|
}
|
|
return fmt.Sprintf("database %s error: %v", e.Operation, e.Err)
|
|
}
|
|
|
|
func (e *DatabaseError) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
// NewDatabase creates a new database connection with the given configuration
|
|
func NewDatabase(config *Config) (*Database, error) {
|
|
if config == nil {
|
|
config = DefaultConfig()
|
|
}
|
|
|
|
// Resolve database path
|
|
dbPath, err := ResolveDatabasePath(config.Path)
|
|
if err != nil {
|
|
return nil, &DatabaseError{
|
|
Operation: "path_resolution",
|
|
Err: err,
|
|
Retryable: false,
|
|
}
|
|
}
|
|
config.Path = dbPath
|
|
|
|
// Open database connection
|
|
conn, err := sql.Open("sqlite3", buildConnectionString(dbPath))
|
|
if err != nil {
|
|
return nil, &DatabaseError{
|
|
Operation: "connect",
|
|
Err: err,
|
|
Retryable: true,
|
|
}
|
|
}
|
|
|
|
// Configure connection pool
|
|
conn.SetMaxOpenConns(config.MaxOpenConns)
|
|
conn.SetMaxIdleConns(config.MaxIdleConns)
|
|
conn.SetConnMaxLifetime(config.ConnMaxLifetime)
|
|
|
|
// Test connection
|
|
if err := conn.Ping(); err != nil {
|
|
conn.Close()
|
|
return nil, &DatabaseError{
|
|
Operation: "ping",
|
|
Err: err,
|
|
Retryable: true,
|
|
}
|
|
}
|
|
|
|
db := &Database{
|
|
conn: conn,
|
|
config: config,
|
|
}
|
|
|
|
// Initialize components
|
|
db.migrator = NewMigrator(conn)
|
|
db.callsign = NewCallsignManager(conn)
|
|
db.history = NewHistoryManager(conn, config.MaxHistoryDays)
|
|
|
|
return db, nil
|
|
}
|
|
|
|
// Initialize runs database migrations and sets up embedded data
|
|
func (db *Database) Initialize() error {
|
|
// Run schema migrations
|
|
if err := db.migrator.MigrateToLatest(); err != nil {
|
|
return &DatabaseError{
|
|
Operation: "migration",
|
|
Err: err,
|
|
Retryable: false,
|
|
}
|
|
}
|
|
|
|
// Load embedded OpenFlights data if not already loaded
|
|
if err := db.callsign.LoadEmbeddedData(); err != nil {
|
|
return &DatabaseError{
|
|
Operation: "load_embedded_data",
|
|
Err: err,
|
|
Retryable: false,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetConfig returns the database configuration
|
|
func (db *Database) GetConfig() *Config {
|
|
return db.config
|
|
}
|
|
|
|
// GetConnection returns the underlying database connection
|
|
func (db *Database) GetConnection() *sql.DB {
|
|
return db.conn
|
|
}
|
|
|
|
// GetHistoryManager returns the history manager
|
|
func (db *Database) GetHistoryManager() *HistoryManager {
|
|
return db.history
|
|
}
|
|
|
|
// GetCallsignManager returns the callsign manager
|
|
func (db *Database) GetCallsignManager() *CallsignManager {
|
|
return db.callsign
|
|
}
|
|
|
|
// Close closes the database connection and stops background tasks
|
|
func (db *Database) Close() error {
|
|
if db.conn != nil {
|
|
return db.conn.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Health returns the database health status
|
|
func (db *Database) Health() error {
|
|
if db.conn == nil {
|
|
return fmt.Errorf("database connection not initialized")
|
|
}
|
|
return db.conn.Ping()
|
|
}
|
|
|
|
// DefaultConfig returns the default database configuration
|
|
func DefaultConfig() *Config {
|
|
return &Config{
|
|
Path: "", // Auto-resolved
|
|
MaxHistoryDays: 7,
|
|
BackupOnUpgrade: true,
|
|
MaxOpenConns: 10,
|
|
MaxIdleConns: 5,
|
|
ConnMaxLifetime: time.Hour,
|
|
VacuumInterval: 24 * time.Hour,
|
|
CleanupInterval: time.Hour,
|
|
}
|
|
}
|
|
|
|
// buildConnectionString creates SQLite connection string with optimizations
|
|
func buildConnectionString(path string) string {
|
|
return fmt.Sprintf("%s?_journal_mode=WAL&_synchronous=NORMAL&_cache_size=-64000&_temp_store=MEMORY&_foreign_keys=ON", path)
|
|
}
|