Files
silo/internal/db/db.go
Forbes d08b178466 test: add comprehensive test suite for backend
Add 56 tests covering the core backend packages:

Unit tests (no database required):
- internal/partnum: 7 tests for part number generation logic
  (sequence, format templates, enum validation, constants)
- internal/schema: 8 tests for YAML schema loading, property
  merging, validation, and default application

Integration tests (require TEST_DATABASE_URL):
- internal/db/items: 10 tests for item CRUD, archive/unarchive,
  revisions, and thumbnail operations
- internal/db/relationships: 10 tests for BOM CRUD, cycle detection,
  self-reference blocking, where-used, expanded/flat BOM
- internal/db/projects: 5 tests for project CRUD and item association
- internal/api/bom_handlers: 6 HTTP handler tests for BOM endpoints
  including flat BOM, cost calculation, add/delete entries
- internal/api/items: 5 HTTP handler tests for item CRUD endpoints

Infrastructure:
- internal/testutil: shared helpers for test DB pool setup,
  migration runner, and table truncation
- internal/db/helpers_test.go: DB wrapper for integration tests
- internal/db/db.go: add NewFromPool constructor
- Makefile: add test-integration target with default DSN

Integration tests skip gracefully when TEST_DATABASE_URL is unset.
Dev-mode auth (nil authConfig) used for API handler tests.

Fixes: fmt.Errorf Go vet warning in partnum/generator.go

Closes #2
2026-02-07 01:57:10 -06:00

108 lines
2.6 KiB
Go

// Package db provides PostgreSQL database access.
package db
import (
"context"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// Config holds database connection settings.
type Config struct {
Host string
Port int
Name string
User string
Password string
SSLMode string
MaxConnections int
}
// DB wraps the connection pool.
type DB struct {
pool *pgxpool.Pool
}
// Connect establishes a database connection pool.
func Connect(ctx context.Context, cfg Config) (*DB, error) {
dsn := fmt.Sprintf(
"host=%s port=%d dbname=%s user=%s password=%s sslmode=%s pool_max_conns=%d",
cfg.Host, cfg.Port, cfg.Name, cfg.User, cfg.Password, cfg.SSLMode, cfg.MaxConnections,
)
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
return nil, fmt.Errorf("creating connection pool: %w", err)
}
if err := pool.Ping(ctx); err != nil {
return nil, fmt.Errorf("pinging database: %w", err)
}
return &DB{pool: pool}, nil
}
// ConnectDSN establishes a database connection pool from a DSN string.
func ConnectDSN(ctx context.Context, dsn string) (*DB, error) {
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
return nil, fmt.Errorf("creating connection pool: %w", err)
}
if err := pool.Ping(ctx); err != nil {
return nil, fmt.Errorf("pinging database: %w", err)
}
return &DB{pool: pool}, nil
}
// NewFromPool wraps an existing connection pool in a DB handle.
func NewFromPool(pool *pgxpool.Pool) *DB {
return &DB{pool: pool}
}
// Close closes the connection pool.
func (db *DB) Close() {
db.pool.Close()
}
// Pool returns the underlying connection pool for direct access.
func (db *DB) Pool() *pgxpool.Pool {
return db.pool
}
// Tx executes a function within a transaction.
func (db *DB) Tx(ctx context.Context, fn func(tx pgx.Tx) error) error {
tx, err := db.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("beginning transaction: %w", err)
}
if err := fn(tx); err != nil {
if rbErr := tx.Rollback(ctx); rbErr != nil {
return fmt.Errorf("rollback failed: %v (original error: %w)", rbErr, err)
}
return err
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("committing transaction: %w", err)
}
return nil
}
// NextSequenceValue atomically increments and returns the next sequence value.
// Uses schema name (not UUID) for simpler operation.
func (db *DB) NextSequenceValue(ctx context.Context, schemaName string, scope string) (int, error) {
var val int
err := db.pool.QueryRow(ctx,
"SELECT next_sequence_by_name($1, $2)",
schemaName, scope,
).Scan(&val)
if err != nil {
return 0, fmt.Errorf("getting next sequence: %w", err)
}
return val, nil
}