// 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 }