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
This commit is contained in:
370
.kilo/skills/postgresql-patterns/SKILL.md
Normal file
370
.kilo/skills/postgresql-patterns/SKILL.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# 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)
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```sql
|
||||
-- ✅ 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
|
||||
|
||||
```sql
|
||||
-- ✅ 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
|
||||
|
||||
```go
|
||||
// ✅ 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
|
||||
|
||||
```go
|
||||
// ✅ 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
|
||||
|
||||
```sql
|
||||
-- ✅ 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
|
||||
|
||||
```sql
|
||||
-- ✅ 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
|
||||
|
||||
```go
|
||||
// ✅ 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
|
||||
|
||||
```go
|
||||
// ✅ 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
|
||||
```sql
|
||||
-- ❌ 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
|
||||
```sql
|
||||
-- ✅ 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
|
||||
|
||||
```bash
|
||||
# ✅ 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
|
||||
|
||||
```sql
|
||||
-- ✅ 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;
|
||||
```
|
||||
Reference in New Issue
Block a user