Back to list
Wania-Kazmi

database-patterns

by Wania-Kazmi

Autonomous project generator for Claude Code. Write requirements, run one command, get a complete project with custom skills, agents, hooks, TDD, 80%+ coverage, and security-reviewed code.

2🍴 0📅 Jan 24, 2026

SKILL.md


name: database-patterns description: | Database design patterns, SQL best practices, ORM usage (Prisma/Drizzle), and migration strategies. Use when designing schemas, writing queries, or optimizing database operations. allowed-tools: Read, Write, Edit, Bash, Grep, Glob

Database Patterns & Best Practices

Schema Design Principles

1. Normalization (3NF minimum)

  • No repeating groups
  • No partial dependencies
  • No transitive dependencies

2. Use UUIDs vs Auto-Increment

-- GOOD: UUID for distributed systems
id UUID PRIMARY KEY DEFAULT gen_random_uuid()

-- OK: Auto-increment for simple cases
id SERIAL PRIMARY KEY

3. Timestamps on Every Table

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name VARCHAR(100) NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

Prisma ORM Patterns

Schema Definition

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

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

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String
  role      Role     @default(USER)
  posts     Post[]
  profile   Profile?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([email])
  @@map("users")
}

model Post {
  id        String   @id @default(uuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  tags      Tag[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([authorId])
  @@index([published])
  @@map("posts")
}

enum Role {
  USER
  ADMIN
}

CRUD Operations

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

// Create
async function createUser(data: { name: string; email: string }) {
  return prisma.user.create({
    data,
    select: {
      id: true,
      name: true,
      email: true,
      createdAt: true
    }
  })
}

// Read with relations
async function getUserWithPosts(id: string) {
  return prisma.user.findUnique({
    where: { id },
    include: {
      posts: {
        where: { published: true },
        orderBy: { createdAt: 'desc' },
        take: 10
      },
      profile: true
    }
  })
}

// Update
async function updateUser(id: string, data: Partial<User>) {
  return prisma.user.update({
    where: { id },
    data
  })
}

// Delete (soft delete pattern)
async function deleteUser(id: string) {
  return prisma.user.update({
    where: { id },
    data: { deletedAt: new Date() }
  })
}

Transactions

// Interactive transaction
async function transferFunds(fromId: string, toId: string, amount: number) {
  return prisma.$transaction(async (tx) => {
    const from = await tx.account.update({
      where: { id: fromId },
      data: { balance: { decrement: amount } }
    })

    if (from.balance < 0) {
      throw new Error('Insufficient funds')
    }

    await tx.account.update({
      where: { id: toId },
      data: { balance: { increment: amount } }
    })

    return tx.transaction.create({
      data: { fromId, toId, amount }
    })
  })
}

// Sequential transaction
async function createUserWithProfile(data: CreateUserInput) {
  return prisma.$transaction([
    prisma.user.create({ data: data.user }),
    prisma.profile.create({ data: data.profile })
  ])
}

Pagination

// Cursor-based (recommended for large datasets)
async function getUsers(cursor?: string, limit = 20) {
  const users = await prisma.user.findMany({
    take: limit + 1,
    cursor: cursor ? { id: cursor } : undefined,
    orderBy: { createdAt: 'desc' },
    skip: cursor ? 1 : 0 // Skip the cursor itself
  })

  const hasMore = users.length > limit
  const data = hasMore ? users.slice(0, -1) : users

  return {
    data,
    nextCursor: hasMore ? data[data.length - 1].id : null
  }
}

// Offset-based
async function getUsersWithOffset(page = 1, limit = 20) {
  const [users, total] = await Promise.all([
    prisma.user.findMany({
      skip: (page - 1) * limit,
      take: limit
    }),
    prisma.user.count()
  ])

  return {
    data: users,
    meta: { total, page, limit, totalPages: Math.ceil(total / limit) }
  }
}

Query Optimization

Avoid N+1 Queries

// BAD: N+1 problem
const users = await prisma.user.findMany()
for (const user of users) {
  const posts = await prisma.post.findMany({
    where: { authorId: user.id }
  })
}

// GOOD: Include in single query
const users = await prisma.user.findMany({
  include: { posts: true }
})

// GOOD: Select only needed fields
const users = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
    posts: {
      select: {
        id: true,
        title: true
      }
    }
  }
})

Use Indexes Properly

model Post {
  id        String @id
  authorId  String
  status    Status
  createdAt DateTime

  // Compound index for common query patterns
  @@index([authorId, status])
  @@index([status, createdAt])
}
-- For frequently queried columns
CREATE INDEX idx_posts_author_status ON posts(author_id, status);

-- For text search
CREATE INDEX idx_posts_title_gin ON posts USING gin(to_tsvector('english', title));

-- For JSON columns
CREATE INDEX idx_metadata_gin ON posts USING gin(metadata);

Batch Operations

// Batch create
await prisma.user.createMany({
  data: users,
  skipDuplicates: true
})

// Batch update with raw SQL (when needed)
await prisma.$executeRaw`
  UPDATE posts 
  SET status = 'archived' 
  WHERE created_at < NOW() - INTERVAL '1 year'
`

// Batch delete
await prisma.user.deleteMany({
  where: {
    deletedAt: { not: null },
    deletedAt: { lt: thirtyDaysAgo }
  }
})

Migration Patterns

Prisma Migrations

# Create migration
npx prisma migrate dev --name add_user_role

# Apply in production
npx prisma migrate deploy

# Reset database (dev only)
npx prisma migrate reset

Safe Schema Changes

// Step 1: Add nullable column
model User {
  newField String?  // nullable first
}

// Step 2: Backfill data
await prisma.$executeRaw`
  UPDATE users SET new_field = 'default' WHERE new_field IS NULL
`

// Step 3: Make non-nullable
model User {
  newField String @default("default")
}

Rollback Strategy

-- Always create down migrations
-- up.sql
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- down.sql
ALTER TABLE users DROP COLUMN phone;

Connection Management

Connection Pooling

// Singleton pattern for Prisma
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient({
  log: ['error', 'warn'],
  datasources: {
    db: {
      url: process.env.DATABASE_URL
    }
  }
})

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

Serverless Configuration

// For serverless (Vercel, AWS Lambda)
import { PrismaClient } from '@prisma/client'
import { Pool } from '@neondatabase/serverless'
import { PrismaNeon } from '@prisma/adapter-neon'

const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const adapter = new PrismaNeon(pool)
const prisma = new PrismaClient({ adapter })

Soft Delete Pattern

model User {
  id        String    @id @default(uuid())
  email     String    @unique
  deletedAt DateTime?

  @@index([deletedAt])
}
// Middleware for automatic filtering
prisma.$use(async (params, next) => {
  if (params.model === 'User') {
    if (params.action === 'findMany' || params.action === 'findFirst') {
      params.args.where = {
        ...params.args.where,
        deletedAt: null
      }
    }
  }
  return next(params)
})

// Soft delete
async function softDelete(id: string) {
  return prisma.user.update({
    where: { id },
    data: { deletedAt: new Date() }
  })
}

// Hard delete (permanent)
async function hardDelete(id: string) {
  return prisma.user.delete({ where: { id } })
}

Audit Trail Pattern

model AuditLog {
  id         String   @id @default(uuid())
  entityType String
  entityId   String
  action     String   // CREATE, UPDATE, DELETE
  changes    Json?
  userId     String?
  createdAt  DateTime @default(now())

  @@index([entityType, entityId])
  @@index([userId])
  @@index([createdAt])
}
// Middleware for automatic audit logging
prisma.$use(async (params, next) => {
  const result = await next(params)
  
  if (['create', 'update', 'delete'].includes(params.action)) {
    await prisma.auditLog.create({
      data: {
        entityType: params.model!,
        entityId: result.id,
        action: params.action.toUpperCase(),
        changes: params.args.data,
        userId: getCurrentUserId()
      }
    })
  }
  
  return result
})

Multi-Tenant Pattern

model Organization {
  id    String @id @default(uuid())
  name  String
  users User[]
  posts Post[]
}

model User {
  id             String       @id @default(uuid())
  organization   Organization @relation(fields: [organizationId], references: [id])
  organizationId String

  @@index([organizationId])
}
// Row-level security with Prisma extension
const prismaWithTenant = prisma.$extends({
  query: {
    $allModels: {
      async $allOperations({ args, query, model }) {
        const tenantId = getTenantId()
        
        if (tenantId && hasTenantField(model)) {
          args.where = { ...args.where, organizationId: tenantId }
        }
        
        return query(args)
      }
    }
  }
})

Raw SQL When Needed

// Complex aggregations
const stats = await prisma.$queryRaw<Stats[]>`
  SELECT 
    DATE_TRUNC('day', created_at) as date,
    COUNT(*) as count,
    SUM(amount) as total
  FROM transactions
  WHERE created_at >= ${startDate}
  GROUP BY DATE_TRUNC('day', created_at)
  ORDER BY date DESC
`

// Full-text search
const results = await prisma.$queryRaw<Post[]>`
  SELECT * FROM posts
  WHERE to_tsvector('english', title || ' ' || content) 
    @@ plainto_tsquery('english', ${searchTerm})
  ORDER BY ts_rank(
    to_tsvector('english', title || ' ' || content),
    plainto_tsquery('english', ${searchTerm})
  ) DESC
  LIMIT ${limit}
`

Checklist

  • UUIDs for distributed systems
  • Timestamps on all tables
  • Proper indexes for query patterns
  • N+1 queries avoided (use include/join)
  • Transactions for multi-step operations
  • Soft delete where appropriate
  • Connection pooling configured
  • Migrations tested (up and down)
  • Audit logging for sensitive data
  • Query performance monitored

Score

Total Score

65/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon