package lagoon import ( "context" "database/sql" "fmt" "strings" "git.golem15.com/golem15/summercms/backpack" "git.golem15.com/golem15/summercms/compass" _ "github.com/jackc/pgx/v5/stdlib" "gorm.io/driver/postgres" "gorm.io/gorm" ) const ( requiredLocaleProvider = "i" requiredICULocale = "pl-PL" ) // Open pings dsn through pgx stdlib, requires Postgres 16 ICU pl-PL, and // returns that exact *sql.DB plus a GORM handle opened on it. // // Phase 11 owns a separate pgxpool.Pool for River LISTEN/NOTIFY. Do not // create that listener pool here; application queries share this *sql.DB. func Open(ctx context.Context, dsn string) (*sql.DB, *gorm.DB, error) { dsn = strings.TrimSpace(dsn) if dsn == "" { return nil, nil, fmt.Errorf("lagoon: database.dsn is empty (set SUMMER_DATABASE__DSN)") } sqlDB, err := sql.Open("pgx", dsn) if err != nil { return nil, nil, fmt.Errorf("lagoon: open postgres: %w", err) } if err := sqlDB.PingContext(ctx); err != nil { _ = sqlDB.Close() return nil, nil, fmt.Errorf("lagoon: ping postgres: %w", err) } if err := CheckLocale(ctx, sqlDB); err != nil { _ = sqlDB.Close() return nil, nil, err } gdb, err := gormFromSQL(sqlDB) if err != nil { _ = sqlDB.Close() return nil, nil, err } return sqlDB, gdb, nil } // Use pings an existing pool, requires ICU pl-PL, and returns a GORM handle // opened on that exact *sql.DB. Callers that already hold a pool (tests, // the app boot seam) must not open a second connection. func Use(ctx context.Context, sqlDB *sql.DB) (*gorm.DB, error) { if sqlDB == nil { return nil, fmt.Errorf("lagoon: sql db is nil") } if err := sqlDB.PingContext(ctx); err != nil { return nil, fmt.Errorf("lagoon: ping postgres: %w", err) } if err := CheckLocale(ctx, sqlDB); err != nil { return nil, err } return gormFromSQL(sqlDB) } func gormFromSQL(sqlDB *sql.DB) (*gorm.DB, error) { gdb, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{}) if err != nil { return nil, fmt.Errorf("lagoon: gorm open: %w", err) } got, err := gdb.DB() if err != nil { return nil, fmt.Errorf("lagoon: gorm sql handle: %w", err) } if got != sqlDB { return nil, fmt.Errorf("lagoon: GORM is not using the shared *sql.DB") } return gdb, nil } // OpenFromApp reads database.dsn from app config and opens the shared pool. func OpenFromApp(ctx context.Context, app *backpack.App) (*sql.DB, *gorm.DB, error) { if app == nil || app.Config == nil { return nil, nil, fmt.Errorf("lagoon: app config is missing") } return Open(ctx, DSN(app.Config)) } // DSN returns database.dsn from layered config (env SUMMER_DATABASE__DSN). func DSN(cfg *compass.Config) string { if cfg == nil { return "" } return strings.TrimSpace(cfg.String("database.dsn")) } // Publish stores the shared SQL pool and GORM handle on the app. Both // handles refer to the same *sql.DB. func Publish(app *backpack.App, sqlDB *sql.DB, gdb *gorm.DB) error { if app == nil { return fmt.Errorf("lagoon: app is nil") } if sqlDB == nil || gdb == nil { return fmt.Errorf("lagoon: database handles are nil") } if err := app.Publish(sqlDB); err != nil { return err } return app.Publish(gdb) } // CheckLocale fails unless the connected database uses ICU locale pl-PL. func CheckLocale(ctx context.Context, db *sql.DB) error { if db == nil { return fmt.Errorf("lagoon: sql db is nil") } var provider, icu string err := db.QueryRowContext(ctx, ` SELECT datlocprovider::text, COALESCE(daticulocale, '') FROM pg_database WHERE datname = current_database()`).Scan(&provider, &icu) if err != nil { return fmt.Errorf("lagoon: read database locale: %w", err) } return checkLocale(provider, icu) } func checkLocale(provider, icu string) error { provider = strings.TrimSpace(provider) icu = strings.TrimSpace(icu) if provider == requiredLocaleProvider && icu == requiredICULocale { return nil } return fmt.Errorf("lagoon: database locale must be ICU pl-PL (datlocprovider=%q, daticulocale=%q); got provider %q locale %q. Create the database with: CREATE DATABASE ... TEMPLATE template0 ENCODING 'UTF8' LOCALE_PROVIDER icu ICU_LOCALE 'pl-PL'", requiredLocaleProvider, requiredICULocale, provider, icu) }