- 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
148 lines
3.9 KiB
Go
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
|
|
}
|