Files
APAW/.kilo/skills/postgresql-patterns/SKILL.md
¨NW¨ 41eb4c7d4d feat: add database skills for ClickHouse, PostgreSQL, and SQLite
- Add ClickHouse patterns skill
- Add PostgreSQL patterns skill
- Add SQLite patterns skill
- Update backend-developer agent to reference PostgreSQL and SQLite skills
- Update go-developer agent to reference ClickHouse, PostgreSQL, and SQLite skills
- Update capability-index.yaml with database integration capabilities
2026-04-05 05:00:55 +01:00

9.8 KiB

PostgreSQL Patterns Skill

Comprehensive guide to PostgreSQL database patterns and best practices.

Overview

PostgreSQL is a powerful, open-source object-relational database system. This skill covers schema design, indexing, querying, transactions, and performance optimization.

Connection Management

Basic Connection (using pgx driver)

import (
    "context"
    "time"
    
    "github.com/jackc/pgx/v5"
    "github.com/jackc/pgx/v5/pgxpool"
)

// ✅ Good: Connection pool with context
func NewPostgreSQLPool(ctx context.Context, connString string) (*pgxpool.Pool, error) {
    config, err := pgxpool.ParseConfig(connString)
    if err != nil {
        return nil, fmt.Errorf("parse config: %w", err)
    }
    
    // Customize pool configuration
    config.MinConns = 10
    config.MaxConns = 100
    config.MaxConnLifetime = time.Hour
    config.HealthCheckPeriod = time.Minute
    config.ConnConfig.ConnectTimeout = 5 * time.Second
    
    pool, err := pgxpool.NewWithConfig(ctx, config)
    if err != nil {
        return nil, fmt.Errorf("create pool: %w", err)
    }
    
    // Verify connection
    if err := pool.Ping(ctx); err != nil {
        pool.Close()
        return nil, fmt.Errorf("ping pool: %w", err)
    }
    
    return pool, nil
}

Schema Design

Data Types

-- ✅ Good: Appropriate data types
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash TEXT NOT NULL,
    first_name VARCHAR(100),
    last_name VARCHAR(100),
    age INTEGER CHECK (age >= 0 AND age <= 150),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    is_active BOOLEAN DEFAULT TRUE,
    profile JSONB, -- For flexible schema
    settings HSTORE -- Key-value store
);

-- Indexes for common queries
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_users_profile ON users USING GIN (profile);

Constraints

-- ✅ Good: Using constraints for data integrity
CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    order_number VARCHAR(50) UNIQUE NOT NULL,
    status VARCHAR(20) NOT NULL CHECK (status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled')),
    total_amount DECIMAL(10, 2) NOT NULL CHECK (total_amount >= 0),
    order_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    shipped_at TIMESTAMPTZ,
    delivered_at TIMESTAMPTZ,
    metadata JSONB
);

-- Partial index for active orders
CREATE INDEX idx_orders_active ON orders(id) 
WHERE status IN ('pending', 'processing', 'shipped');

-- Expression index for case-insensitive search
CREATE INDEX idx_users_email_lower ON users((LOWER(email)));

Query Patterns

Basic CRUD

// ✅ Good: Using pgx with context and proper error handling
func GetUserByID(ctx context.Context, pool *pgxpool.Pool, id UUID) (*User, error) {
    var user User
    err := pool.QueryRow(ctx, `
        SELECT id, email, first_name, last_name, age, created_at, updated_at, is_active
        FROM users
        WHERE id = $1
    `, id).Scan(
        &user.ID, &user.Email, &user.FirstName, &user.LastName,
        &user.Age, &user.CreatedAt, &user.UpdatedAt, &user.IsActive,
    )
    if err != nil {
        if errors.Is(err, pgx.ErrNoRows) {
            return nil, ErrNotFound
        }
        return nil, fmt.Errorf("get user: %w", err)
    }
    return &user, nil
}

func CreateUser(ctx context.Context, pool *pgxpool.Pool, user *User) (*User, error) {
    err := pool.QueryRow(ctx, `
        INSERT INTO users (email, password_hash, first_name, last_name, age)
        VALUES ($1, $2, $3, $4, $5)
        RETURNING id, created_at, updated_at
    `, user.Email, user.PasswordHash, user.FirstName, user.LastName, user.Age).
        Scan(&user.ID, &user.CreatedAt, &user.UpdatedAt)
    if err != nil {
        return nil, fmt.Errorf("create user: %w", err)
    }
    return user, nil
}

Transactions

// ✅ Good: Using tx with context and proper rollback
func TransferFunds(ctx context.Context, pool *pgxpool.Pool, fromID, toID UUID, amount Decimal) error {
    tx, err := pool.Begin(ctx)
    if err != nil {
        return fmt.Errorf("begin transaction: %w", err)
    }
    // Ensure rollback on error
    defer func() {
        if err != nil {
            tx.Rollback(ctx)
        }
    }()
    
    // Check sender balance
    var fromBalance Decimal
    err := tx.QueryRow(ctx, `
        SELECT balance FROM accounts WHERE user_id = $1 FOR UPDATE
    `, fromID).Scan(&fromBalance)
    if err != nil {
        return fmt.Errorf("get sender balance: %w", err)
    }
    if fromBalance < amount {
        return ErrInsufficientFunds
    }
    
    // Update sender
    _, err = tx.Exec(ctx, `
        UPDATE accounts SET balance = balance - $1 WHERE user_id = $2
    `, amount, fromID)
    if err != nil {
        return fmt.Errorf("update sender: %w", err)
    }
    
    // Update receiver
    _, err = tx.Exec(ctx, `
        UPDATE accounts SET balance = balance + $1 WHERE user_id = $2
    `, amount, toID)
    if err != nil {
        return fmt.Errorf("update receiver: %w", err)
    }
    
    // Commit transaction
    if err := tx.Commit(ctx); err != nil {
        return fmt.Errorf("commit transaction: %w", err)
    }
    
    return nil
}

Performance Optimization

Indexing Strategies

-- ✅ Good: B-tree index for equality and range queries
CREATE INDEX idx_orders_date ON orders(order_date);

-- ✅ Good: GIN index for JSONB containment queries
CREATE INDEX idx_orders_metadata ON orders USING GIN (metadata);

-- ✅ Good: BRIN index for large tables with natural ordering
CREATE INDEX idx_large_table_time ON large_table USING BRIN (created_at);

-- ✅ Good: Partial index for infrequent values
CREATE INDEX idx_orders_cancelled ON orders(id) 
WHERE status = 'cancelled';

-- ✅ Good: Covering index (include columns)
CREATE INDEX idx_orders_covering ON orders(user_id) 
INCLUDE (order_number, total_amount, order_date);

Query Optimization

-- ✅ Good: Using EXPLAIN ANALYZE
EXPLAIN ANALYZE
SELECT o.id, o.total_amount, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.order_date >= CURRENT_DATE - INTERVAL '30 days'
  AND o.status = 'shipped'
ORDER BY o.order_date DESC
LIMIT 100;

-- ✅ Good: Using CTE for readability and optimization
WITH recent_orders AS (
    SELECT id, user_id, total_amount, order_date
    FROM orders
    WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'
      AND status = 'shipped'
)
SELECT o.id, o.total_amount, u.email
FROM recent_orders o
JOIN users u ON o.user_id = u.id
ORDER BY o.order_date DESC
LIMIT 100;

Connection Pooling

// ✅ Good: Using pgxpool for connection pooling
// Already demonstrated in NewPostgreSQLPool function

// ✅ Good: Using prepared statements implicitly through pgx
// pgx automatically prepares and caches statements

Testing

Testcontainers for PostgreSQL

// ✅ Good: Using testcontrollers for integration tests
func setupPostgreSQL(t *testing.T) *pgxpool.Pool {
    ctx := context.Background()
    
    req := testcontainers.ContainerRequest{
        Image: "postgres:15-alpine",
        ExposedPorts: []string{"5432/tcp"},
        Env: map[string]string{
            "POSTGRES_USER": "test",
            "POSTGRES_PASSWORD": "test",
            "POSTGRES_DB": "test",
        },
        WaitingFor: wait.ForLog("database system is ready to accept connections"),
    }
    
    container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: req,
        Started: true,
    })
    require.NoError(t, err)
    
    t.Cleanup(func() {
        container.Terminate(ctx)
    })
    
    host, err := container.Host(ctx)
    require.NoError(t, err)
    
    port, err := container.MappedPort(ctx, "5432")
    require.NoError(t, err)
    
    connString := fmt.Sprintf("host=%s port=%s user=test password=test dbname=test sslmode=disable",
        host, port.Port())
    
    pool, err := pgxpool.New(ctx, connString)
    require.NoError(t, err)
    
    return pool
}

Best Practices

Bad Patterns

-- ❌ Bad: Using SELECT * in production
SELECT * FROM orders WHERE order_date > NOW() - INTERVAL '7 days'

-- ❌ Bad: Not using parameterized queries (SQL injection risk)
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)

-- ❌ Bad: No connection pooling
// Creating new connection for every query

Good Patterns

-- ✅ Good: Explicit column selection
SELECT id, order_number, total_amount, order_date
FROM orders
WHERE order_date > NOW() - INTERVAL '7 days'

-- ✅ Good: Using parameterized queries
pool.QueryRow(ctx, "SELECT * FROM users WHERE email = $1", email)

-- ✅ Good: Using connection pool
// Reuse pool across multiple requests

Common Operations

Backup and Restore

# ✅ Good: Logical backup with pg_dump
pg_dump -h localhost -U test -d test > backup.sql

# Restore
psql -h localhost -U test -d test < backup.sql

# ✅ Good: Custom format with compression
pg_dump -Fc -z 9 -f backup.dump -U test test
pg_restore -U test -d test backup.dump

Monitoring

-- ✅ Good: Checking active queries
SELECT pid, age(clock_timestamp(), query_start), state, query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start DESC;

-- ✅ Good: Index usage statistics
SELECT 
    schemaname,
    tablename,
    indexname,
    idx_tup_read,
    idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_tup_read DESC;

-- ✅ Good: Table size
SELECT 
    schemaname,
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;