package dbx import ( "database/sql" "errors" "gitea.auvem.com/go-toolkit/app" "gitea.auvem.com/go-toolkit/dbx/internal/dbxshared" "github.com/go-jet/jet/v2/qrm" ) const ( // ModuleDBName is the name of the database module. ModuleDBName = "database" // DialectPostgres is the PostgreSQL dialect. DialectPostgres Dialect = "postgres" // DialectMySQL is the MySQL dialect. DialectMySQL Dialect = "mysql" ) // Dialect is the SQL dialect used by the database connection. type Dialect string // String implements the Stringer interface for Dialect. func (d Dialect) String() string { return string(d) } // dbState stores package-level state for the database connection. type dbState struct { sqlDB *sql.DB config *DBConfig dialect Dialect debugLog bool } var ( // ErrNoRows is returned when a query returns no rows. ErrNoRows = qrm.ErrNoRows // ErrValueIsZero is returned when an expected value is missing. ErrValueIsZero = errors.New("value is zero-value for type") state = dbState{} ) // SQLO returns the current SQL database handle. func SQLO() *sql.DB { dbxshared.DBModule.RequireLoaded("dbx.SQLO requires database module") if state.sqlDB == nil { panic("SQL database not initialized") } return state.sqlDB } // ModuleDB returns the database module with the provided configuration. // dialect specifies the SQL dialect to use (e.g., DialectPostgres, DialectMySQL). // config specifies the database connection configuration. // forceDebugLog forces debug logging to be enabled regardless of the config setting. func ModuleDB(dialect Dialect, config *DBConfig, forceDebugLog bool) *app.Module { if dbxshared.DBModule != nil { panic("ModuleDB initialized multiple times") } if config == nil { panic("ModuleDB requires a non-nil DBConfig") } state.config = config state.dialect = dialect state.debugLog = config.DebugLog || forceDebugLog dbxshared.DBModule = app.NewModule(ModuleDBName, app.ModuleOpts{ Setup: setupDB, Teardown: teardownDB, }) return dbxshared.DBModule } func setupDB(m *app.Module) error { if state.sqlDB != nil && state.sqlDB.Ping() == nil { m.Logger().Warn("Database connection already established") return nil } logArgs := []any{ "user", state.config.User, "name", state.config.Name, "uri", state.config.URI, "dialect", state.dialect, } var err error state.sqlDB, err = sql.Open(string(state.dialect), state.config.ConnectionString(state.dialect)) if err != nil { logArgs = append(logArgs, "err", err) m.Logger().Error("Couldn't open SQL database", logArgs...) return err } if err := state.sqlDB.Ping(); err != nil { logArgs = append(logArgs, "err", err) m.Logger().Error("Couldn't ping SQL database", logArgs...) return err } state.sqlDB.SetMaxOpenConns(state.config.MaxConn) stats := state.sqlDB.Stats() m.Logger().Info( "Connected to SQL database", "user", state.config.User, "name", state.config.Name, "uri", state.config.URI, "maxConnections", stats.MaxOpenConnections, "currConnections", stats.OpenConnections, ) if state.debugLog { dbxshared.InitLogger(state.dialect) } return nil } func teardownDB(m *app.Module) error { if state.sqlDB == nil { return nil } if err := state.sqlDB.Close(); err != nil { m.Logger().Error("Couldn't close database", "err", err) return err } m.Logger().Info("Closed database connection") return nil }