Agent skill

PostgreSQL

Use PostgreSQL for relational data storage with ACID compliance, transactions, and advanced features.

Stars 10
Forks 1

Install this agent skill to your Project

npx add-skill https://github.com/hivellm/rulebook/tree/main/templates/skills/services/postgresql

SKILL.md

PostgreSQL Database Instructions

CRITICAL: Use PostgreSQL for relational data storage with ACID compliance, transactions, and advanced features.

Core Features

Connection

typescript
// Using pg (Node.js)
import { Pool } from 'pg'
const pool = new Pool({
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || '5432'),
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
})

// Using Prisma
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

Basic Queries

typescript
// SELECT
const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId])
const users = result.rows

// INSERT
const result = await pool.query(
  'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
  ['John Doe', 'john@example.com']
)
const newUser = result.rows[0]

// UPDATE
const result = await pool.query(
  'UPDATE users SET name = $1 WHERE id = $2 RETURNING *',
  ['Jane Doe', userId]
)

// DELETE
await pool.query('DELETE FROM users WHERE id = $1', [userId])

Transactions

typescript
const client = await pool.connect()
try {
  await client.query('BEGIN')
  
  await client.query('INSERT INTO accounts (user_id, balance) VALUES ($1, $2)', [userId, 1000])
  await client.query('INSERT INTO transactions (account_id, amount) VALUES ($1, $2)', [accountId, 1000])
  
  await client.query('COMMIT')
} catch (error) {
  await client.query('ROLLBACK')
  throw error
} finally {
  client.release()
}

Advanced Features

typescript
// JSONB queries
await pool.query(
  'SELECT * FROM products WHERE metadata @> $1',
  [JSON.stringify({ category: 'electronics' })]
)

// Full-text search
await pool.query(
  "SELECT * FROM articles WHERE to_tsvector('english', content) @@ to_tsquery('english', $1)",
  ['search term']
)

// Array operations
await pool.query('SELECT * FROM posts WHERE tags && $1', [['javascript', 'typescript']])

// Window functions
await pool.query(`
  SELECT 
    name,
    salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) as rank
  FROM employees
`)

Common Patterns

Connection Pooling

typescript
// Reuse connection pool
let pool: Pool | null = null

export function getPool(): Pool {
  if (!pool) {
    pool = new Pool({
      // ... config
    })
    
    pool.on('error', (err) => {
      console.error('Unexpected error on idle client', err)
      process.exit(-1)
    })
  }
  return pool
}

// Graceful shutdown
process.on('SIGINT', async () => {
  if (pool) {
    await pool.end()
  }
  process.exit(0)
})

Prepared Statements

typescript
// Always use parameterized queries to prevent SQL injection
// ❌ WRONG
await pool.query(`SELECT * FROM users WHERE email = '${email}'`)

// ✅ CORRECT
await pool.query('SELECT * FROM users WHERE email = $1', [email])

Error Handling

typescript
try {
  const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId])
  if (result.rows.length === 0) {
    throw new Error('User not found')
  }
  return result.rows[0]
} catch (error) {
  if (error.code === '23505') { // Unique violation
    throw new Error('Duplicate entry')
  }
  if (error.code === '23503') { // Foreign key violation
    throw new Error('Referenced record does not exist')
  }
  throw error
}

Migrations

typescript
// Using node-pg-migrate
import { migrate } from 'node-pg-migrate'

await migrate({
  databaseUrl: process.env.DATABASE_URL,
  dir: 'migrations',
  direction: 'up',
  migrationsTable: 'pgmigrations',
  dryRun: false,
})

Best Practices

DO:

  • Use connection pooling (max 20-30 connections)
  • Always use parameterized queries ($1, $2, etc.)
  • Use transactions for multi-step operations
  • Create indexes on frequently queried columns
  • Use EXPLAIN ANALYZE to optimize queries
  • Enable connection pooling (PgBouncer for production)
  • Use SSL in production
  • Set appropriate connection timeouts
  • Monitor connection pool usage
  • Use prepared statements for repeated queries

DON'T:

  • Use string concatenation for queries (SQL injection risk)
  • Create too many connections (exhaust pool)
  • Skip error handling
  • Ignore connection pool limits
  • Use SELECT * in production (specify columns)
  • Skip indexes on foreign keys
  • Hardcode connection strings
  • Skip SSL in production
  • Ignore query performance
  • Use synchronous queries

Configuration

Environment Variables

bash
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DB_USER=myuser
DB_PASSWORD=securepassword
DATABASE_URL=postgresql://user:password@host:port/database

Docker Compose

yaml
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: securepassword
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

Prisma Schema

prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
  posts     Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
}

Performance Optimization

Indexing

sql
-- Single column index
CREATE INDEX idx_users_email ON users(email);

-- Composite index
CREATE INDEX idx_posts_author_created ON posts(author_id, created_at DESC);

-- Partial index
CREATE INDEX idx_active_users ON users(email) WHERE active = true;

-- GIN index for JSONB
CREATE INDEX idx_products_metadata ON products USING GIN(metadata);

Query Optimization

typescript
// Use LIMIT and OFFSET for pagination
const result = await pool.query(
  'SELECT * FROM posts ORDER BY created_at DESC LIMIT $1 OFFSET $2',
  [limit, offset]
)

// Use EXISTS instead of COUNT for existence checks
const result = await pool.query(
  'SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)',
  [email]
)

Integration with Development

Testing

typescript
// Use test database
const testPool = new Pool({
  database: 'myapp_test',
  // ... config
})

// Clean up after tests
afterEach(async () => {
  await testPool.query('TRUNCATE TABLE users, posts CASCADE')
})

// Use transactions for test isolation
beforeEach(async () => {
  await testPool.query('BEGIN')
})

afterEach(async () => {
  await testPool.query('ROLLBACK')
})

Health Checks

typescript
async function checkDatabaseHealth(): Promise<boolean> {
  try {
    const result = await pool.query('SELECT 1')
    return result.rows.length > 0
  } catch {
    return false
  }
}

Didn't find tool you were looking for?

Be as detailed as possible for better results