package db import ( "database/sql" _ "embed" "fmt" _ "modernc.org/sqlite" ) //go:embed schema.sql var schemaSQL string func Open(path string) (*sql.DB, error) { dsn := fmt.Sprintf("file:%s?_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)&_pragma=busy_timeout(5000)", path) conn, err := sql.Open("sqlite", dsn) if err != nil { return nil, fmt.Errorf("open sqlite: %w", err) } if err := conn.Ping(); err != nil { conn.Close() return nil, fmt.Errorf("ping sqlite: %w", err) } if _, err := conn.Exec(schemaSQL); err != nil { conn.Close() return nil, fmt.Errorf("apply schema: %w", err) } if err := addColumnIfMissing(conn, "items", "min_price", "REAL"); err != nil { conn.Close() return nil, err } if err := addColumnIfMissing(conn, "items", "exclude_keywords", "TEXT"); err != nil { conn.Close() return nil, err } if err := addColumnIfMissing(conn, "results", "matched_query", "TEXT"); err != nil { conn.Close() return nil, err } if err := addColumnIfMissing(conn, "items", "condition", "TEXT"); err != nil { conn.Close() return nil, err } if err := addColumnIfMissing(conn, "items", "region", "TEXT"); err != nil { conn.Close() return nil, err } if err := addColumnIfMissing(conn, "items", "best_price_currency", "TEXT"); err != nil { conn.Close() return nil, err } if err := addColumnIfMissing(conn, "results", "ends_at", "DATETIME"); err != nil { conn.Close() return nil, err } return conn, nil } func addColumnIfMissing(conn *sql.DB, table, column, typ string) error { rows, err := conn.Query(fmt.Sprintf(`PRAGMA table_info(%s)`, table)) if err != nil { return fmt.Errorf("inspect %s: %w", table, err) } defer rows.Close() for rows.Next() { var cid int var name, ctype string var notnull, pk int var dflt sql.NullString if err := rows.Scan(&cid, &name, &ctype, ¬null, &dflt, &pk); err != nil { return err } if name == column { return nil } } if _, err := conn.Exec(fmt.Sprintf(`ALTER TABLE %s ADD COLUMN %s %s`, table, column, typ)); err != nil { return fmt.Errorf("add column %s.%s: %w", table, column, err) } return nil }