package db import ( "context" "log" "strings" "time" "github.com/glebarez/sqlite" "gorm.io/driver/postgres" "gorm.io/gorm" ) // DBClient wraps gorm.DB for testability. type DBClient struct { *gorm.DB } // InitDB initializes configured SQL storage for chat, moderation, and admin metadata (ADR-006). // Returns the client and the driver name. func InitDB(databaseURL string) (*DBClient, string, error) { var ( gormDB *gorm.DB err error driver = "sqlite" ) switch { case databaseURL == "": gormDB, err = gorm.Open(sqlite.Open("chat.db"), &gorm.Config{}) case strings.HasPrefix(databaseURL, "sqlite://"): sqlitePath := strings.TrimPrefix(databaseURL, "sqlite://") if sqlitePath == "" { sqlitePath = "chat.db" } gormDB, err = gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{}) case strings.HasPrefix(databaseURL, "postgres://") || strings.HasPrefix(databaseURL, "postgresql://"): driver = "postgres" gormDB, err = gorm.Open(postgres.Open(databaseURL), &gorm.Config{}) default: log.Fatalf("Unsupported DATABASE_URL scheme: %s", databaseURL) } if err != nil { return nil, driver, err } gormDB.AutoMigrate(&Message{}, &ModerationLog{}, &Media{}, &ScheduleOverride{}) log.Printf("%s database initialized (chat history, moderation log, media metadata)", strings.Title(driver)) return &DBClient{DB: gormDB}, driver, nil } // RedisClient wraps the Redis client for testability. // Interface defined for mocking in tests. type RedisClient interface { Ping(ctx context.Context) error Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error Get(ctx context.Context, key string) (string, error) Del(ctx context.Context, keys ...string) error Incr(ctx context.Context, key string) (int64, error) Decr(ctx context.Context, key string) (int64, error) Expire(ctx context.Context, key string, expiration time.Duration) error Exists(ctx context.Context, keys ...string) (int64, error) Publish(ctx context.Context, channel string, message interface{}) error PSubscribe(ctx context.Context, channels ...string) PubSub Scan(ctx context.Context, cursor uint64, match string, count int64) ([]string, uint64, error) Info(ctx context.Context, sections ...string) (string, error) Close() error } // PubSub wraps Redis pub/sub subscription. type PubSub interface { ReceiveMessage(ctx context.Context) (*PubSubMessage, error) Close() error } // PubSubMessage represents a received pub/sub message. type PubSubMessage struct { Channel string Payload string }