package database import ( "context" "database/sql" "fmt" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" ) // Config holds database configuration type Config struct { Driver string Host string Port int User string Password string Database string SSLMode string } // Client wraps database connection type Client struct { db *sql.DB config Config } // NewClient creates a new database client func NewClient(config Config) (*Client, error) { var dsn string switch config.Driver { case "sqlite3": dsn = config.Database case "postgres": dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", config.Host, config.Port, config.User, config.Password, config.Database, config.SSLMode) default: return nil, fmt.Errorf("unsupported driver: %s", config.Driver) } db, err := sql.Open(config.Driver, dsn) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } // Test connection if err := db.PingContext(context.Background()); err != nil { return nil, fmt.Errorf("failed to ping database: %w", err) } return &Client{ db: db, config: config, }, nil } // Close closes the database connection func (c *Client) Close() error { if c.db != nil { return c.db.Close() } return nil } // DB returns the underlying sql.DB instance func (c *Client) DB() *sql.DB { return c.db } // Health checks database connection health func (c *Client) Health(ctx context.Context) error { return c.db.PingContext(ctx) } // CreateTables creates the basic table structure func (c *Client) CreateTables(ctx context.Context) error { queries := []string{ `CREATE TABLE IF NOT EXISTS questions ( id TEXT PRIMARY KEY, theme TEXT NOT NULL, text TEXT NOT NULL, answer TEXT NOT NULL, hint TEXT, difficulty TEXT DEFAULT 'medium' CHECK (difficulty IN ('easy', 'medium', 'hard')), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )`, `CREATE INDEX IF NOT EXISTS idx_questions_theme ON questions(theme)`, `CREATE INDEX IF NOT EXISTS idx_questions_difficulty ON questions(difficulty)`, `CREATE INDEX IF NOT EXISTS idx_questions_active ON questions(is_active)`, `CREATE TABLE IF NOT EXISTS game_sessions ( id TEXT PRIMARY KEY, player_name TEXT NOT NULL, user_id TEXT, total_score INTEGER DEFAULT 0, questions_asked INTEGER DEFAULT 0, questions_correct INTEGER DEFAULT 0, hints_used INTEGER DEFAULT 0, start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, end_time TIMESTAMP, status TEXT DEFAULT 'active' CHECK (status IN ('active', 'completed', 'timeout', 'abandoned')), current_question_id TEXT, current_attempts INTEGER DEFAULT 0, session_data TEXT )`, `CREATE INDEX IF NOT EXISTS idx_sessions_user ON game_sessions(user_id)`, `CREATE INDEX IF NOT EXISTS idx_sessions_status ON game_sessions(status)`, `CREATE INDEX IF NOT EXISTS idx_sessions_score ON game_sessions(total_score)`, `CREATE TABLE IF NOT EXISTS question_attempts ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, question_id TEXT NOT NULL, attempt_number INTEGER NOT NULL, submitted_answer TEXT NOT NULL, is_correct BOOLEAN NOT NULL, used_hint BOOLEAN DEFAULT FALSE, points_awarded INTEGER DEFAULT 0, submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, time_taken_ms INTEGER DEFAULT 0, metadata TEXT )`, `CREATE INDEX IF NOT EXISTS idx_attempts_session ON question_attempts(session_id)`, `CREATE INDEX IF NOT EXISTS idx_attempts_question ON question_attempts(question_id)`, } for _, query := range queries { if _, err := c.db.ExecContext(ctx, query); err != nil { return fmt.Errorf("failed to create table: %w", err) } } return nil }