Files
goplt/internal/infra/database/client.go
0x1d 767654f257
Some checks failed
CI / Test (pull_request) Failing after 22s
CI / Lint (pull_request) Failing after 19s
CI / Build (pull_request) Failing after 6s
CI / Format Check (pull_request) Successful in 2s
fix(lint): resolve golangci-lint errors
- Fix errcheck: explicitly ignore tx.Rollback() error in defer
  - When transaction commits successfully, Rollback() returns an error (expected)
  - Use defer func() with explicit error assignment to satisfy linter

- Remove unused connectToService function
  - Function is not currently used (proto files not yet generated)
  - Commented out with TODO for future implementation
  - Prevents unused function lint error
2025-11-06 10:28:48 +01:00

148 lines
3.9 KiB
Go

// Package database provides database client and connection management.
package database
import (
"context"
"database/sql"
"fmt"
"time"
"entgo.io/ent/dialect"
entsql "entgo.io/ent/dialect/sql"
"git.dcentral.systems/toolz/goplt/internal/ent"
_ "github.com/lib/pq" // PostgreSQL driver
)
// Client wraps the Ent client with additional functionality.
type Client struct {
*ent.Client
db *sql.DB
}
// Config holds database configuration.
type Config struct {
DSN string
Schema string // Schema name for schema isolation (e.g., "identity", "auth", "authz", "audit")
MaxConnections int
MaxIdleConns int
ConnMaxLifetime time.Duration
ConnMaxIdleTime time.Duration
}
// NewClient creates a new Ent client with connection pooling and schema isolation support.
// If schema is provided, it will be created if it doesn't exist and set as the search path.
func NewClient(cfg Config) (*Client, error) {
// Open database connection
db, err := sql.Open("postgres", cfg.DSN)
if err != nil {
return nil, fmt.Errorf("failed to open database connection: %w", err)
}
// Configure connection pool
db.SetMaxOpenConns(cfg.MaxConnections)
db.SetMaxIdleConns(cfg.MaxIdleConns)
db.SetConnMaxLifetime(cfg.ConnMaxLifetime)
db.SetConnMaxIdleTime(cfg.ConnMaxIdleTime)
// Test connection
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
_ = db.Close()
return nil, fmt.Errorf("failed to ping database: %w", err)
}
// Create schema if provided
if cfg.Schema != "" {
if err := createSchemaIfNotExists(ctx, db, cfg.Schema); err != nil {
_ = db.Close()
return nil, fmt.Errorf("failed to create schema %s: %w", cfg.Schema, err)
}
// Set search path to the schema
if _, err := db.ExecContext(ctx, fmt.Sprintf("SET search_path TO %s", cfg.Schema)); err != nil {
_ = db.Close()
return nil, fmt.Errorf("failed to set search path to schema %s: %w", cfg.Schema, err)
}
}
// Create Ent driver
drv := entsql.OpenDB(dialect.Postgres, db)
// Create Ent client
entClient := ent.NewClient(ent.Driver(drv))
return &Client{
Client: entClient,
db: db,
}, nil
}
// NewClientWithSchema is a convenience function that creates a client with a specific schema.
func NewClientWithSchema(dsn string, schema string) (*Client, error) {
return NewClient(Config{
DSN: dsn,
Schema: schema,
MaxConnections: 25,
MaxIdleConns: 5,
ConnMaxLifetime: 5 * time.Minute,
ConnMaxIdleTime: 10 * time.Minute,
})
}
// createSchemaIfNotExists creates a PostgreSQL schema if it doesn't exist.
func createSchemaIfNotExists(ctx context.Context, db *sql.DB, schemaName string) error {
// Use a transaction to ensure atomicity
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback() // Ignore error - if commit succeeded, rollback will error (expected)
}()
// Check if schema exists
var exists bool
err = tx.QueryRowContext(ctx,
"SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)",
schemaName,
).Scan(&exists)
if err != nil {
return err
}
// Create schema if it doesn't exist
if !exists {
// Use fmt.Sprintf for schema name since it's a configuration value, not user input
_, err = tx.ExecContext(ctx, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", schemaName))
if err != nil {
return err
}
}
return tx.Commit()
}
// Close closes the database connection.
func (c *Client) Close() error {
if err := c.Client.Close(); err != nil {
return err
}
return c.db.Close()
}
// Migrate runs database migrations.
func (c *Client) Migrate(ctx context.Context) error {
return c.Schema.Create(ctx)
}
// Ping checks database connectivity.
func (c *Client) Ping(ctx context.Context) error {
return c.db.PingContext(ctx)
}
// DB returns the underlying *sql.DB for advanced operations.
func (c *Client) DB() *sql.DB {
return c.db
}