skyview/internal/database/database.go
Ole-Morten Duesund 41c7758f69 feat: Eliminate CGO dependency by switching to pure Go SQLite
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>
2025-09-01 19:53:07 +02:00

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)
}