Agent skill
schema-design
Design or modify Drizzle ORM schemas with proper relationships, constraints, and indexes. Use when adding new tables, modifying existing schemas, or optimizing database structure.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/schema-design
SKILL.md
Schema Design Skill
This skill helps you design and modify database schemas using Drizzle ORM in packages/database/.
When to Use This Skill
- Creating new database tables
- Adding columns to existing tables
- Defining relationships between tables
- Creating indexes for query optimization
- Adding constraints (unique, not null, default values)
- Renaming or dropping tables/columns
- Optimizing schema for performance
Database Architecture
packages/database/
├── src/
│ ├── db/
│ │ └── schema/
│ │ ├── cars.ts # Car registration data
│ │ ├── coe.ts # COE bidding results
│ │ ├── pqp.ts # PQP data
│ │ ├── posts.ts # Blog posts
│ │ ├── analytics.ts # Analytics events
│ │ └── index.ts # Schema exports
│ ├── index.ts # Database client export
│ └── migrate.ts # Migration runner
├── migrations/ # Migration files
└── drizzle.config.ts # Drizzle configuration
Naming Conventions
The project uses camelCase for column names:
// ✅ Correct
export const cars = pgTable("cars", {
vehicleClass: text("vehicle_class"),
fuelType: text("fuel_type"),
registrationDate: timestamp("registration_date"),
});
// ❌ Wrong
export const cars = pgTable("cars", {
vehicle_class: text("vehicle_class"), // snake_case
FuelType: text("fuel_type"), // PascalCase
});
Basic Schema Patterns
Simple Table
// packages/database/src/db/schema/example.ts
import { pgTable, text, integer, timestamp, boolean } from "drizzle-orm/pg-core";
export const examples = pgTable("examples", {
id: text("id").primaryKey(),
name: text("name").notNull(),
description: text("description"),
count: integer("count").default(0).notNull(),
isActive: boolean("is_active").default(true).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
Table with Relationships
import { pgTable, text, integer, timestamp } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
import { users } from "./users";
export const posts = pgTable("posts", {
id: text("id").primaryKey(),
title: text("title").notNull(),
content: text("content").notNull(),
authorId: text("author_id").notNull().references(() => users.id),
publishedAt: timestamp("published_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// Define relations
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
Table with Indexes
import { pgTable, text, integer, timestamp, index, uniqueIndex } from "drizzle-orm/pg-core";
export const cars = pgTable("cars", {
id: text("id").primaryKey(),
make: text("make").notNull(),
model: text("model").notNull(),
year: integer("year").notNull(),
registrationDate: timestamp("registration_date").notNull(),
}, (table) => ({
// Single column index
makeIdx: index("cars_make_idx").on(table.make),
// Composite index
makeModelIdx: index("cars_make_model_idx").on(table.make, table.model),
// Unique index
registrationIdx: uniqueIndex("cars_registration_idx").on(table.registrationDate),
}));
Existing Schema Examples
Cars Table
// packages/database/src/db/schema/cars.ts
import { pgTable, text, integer, timestamp, index } from "drizzle-orm/pg-core";
export const cars = pgTable("cars", {
id: text("id").primaryKey(),
make: text("make").notNull(),
model: text("model"),
vehicleClass: text("vehicle_class"),
fuelType: text("fuel_type"),
month: text("month").notNull(),
number: integer("number").default(0).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
}, (table) => ({
monthIdx: index("cars_month_idx").on(table.month),
makeIdx: index("cars_make_idx").on(table.make),
}));
COE Table
// packages/database/src/db/schema/coe.ts
import { pgTable, text, integer, timestamp, numeric, index } from "drizzle-orm/pg-core";
export const coe = pgTable("coe", {
id: text("id").primaryKey(),
biddingNo: integer("bidding_no").notNull(),
month: text("month").notNull(),
vehicleClass: text("vehicle_class").notNull(),
quota: integer("quota").default(0).notNull(),
bidsReceived: integer("bids_received").default(0).notNull(),
premium: numeric("premium", { precision: 10, scale: 2 }).default("0").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
}, (table) => ({
biddingNoIdx: index("coe_bidding_no_idx").on(table.biddingNo),
monthIdx: index("coe_month_idx").on(table.month),
}));
Posts Table
// packages/database/src/db/schema/posts.ts
import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
export const posts = pgTable("posts", {
id: text("id").primaryKey(),
title: text("title").notNull(),
slug: text("slug").notNull().unique(),
content: text("content").notNull(),
excerpt: text("excerpt"),
published: boolean("published").default(false).notNull(),
publishedAt: timestamp("published_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
}, (table) => ({
slugIdx: index("posts_slug_idx").on(table.slug),
publishedAtIdx: index("posts_published_at_idx").on(table.publishedAt),
}));
Column Types
Text Types
import { pgTable, text, varchar, char } from "drizzle-orm/pg-core";
export const examples = pgTable("examples", {
// Unlimited text
description: text("description"),
// Limited varchar
email: varchar("email", { length: 255 }),
// Fixed length
code: char("code", { length: 10 }),
});
Numeric Types
import { pgTable, integer, bigint, numeric, real, doublePrecision } from "drizzle-orm/pg-core";
export const examples = pgTable("examples", {
// Integer types
count: integer("count"),
bigCount: bigint("big_count", { mode: "number" }), // or "bigint" for BigInt
// Decimal types
price: numeric("price", { precision: 10, scale: 2 }), // 10 digits, 2 decimal
// Floating point
rating: real("rating"),
coordinate: doublePrecision("coordinate"),
});
Date/Time Types
import { pgTable, timestamp, date, time } from "drizzle-orm/pg-core";
export const examples = pgTable("examples", {
// Timestamp with timezone
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
// Timestamp without timezone
scheduledAt: timestamp("scheduled_at", { withTimezone: false }),
// Date only
birthDate: date("birth_date"),
// Time only
openingTime: time("opening_time"),
});
Boolean and JSON
import { pgTable, boolean, json, jsonb } from "drizzle-orm/pg-core";
export const examples = pgTable("examples", {
// Boolean
isActive: boolean("is_active").default(true),
// JSON (slower, stores as text)
settings: json("settings"),
// JSONB (faster, binary format)
metadata: jsonb("metadata").$type<{ key: string; value: any }>(),
});
Array Types
import { pgTable, text } from "drizzle-orm/pg-core";
export const examples = pgTable("examples", {
tags: text("tags").array(),
emails: text("emails").array().notNull().default([]),
});
Relationships
One-to-Many
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
// Users table
export const users = pgTable("users", {
id: text("id").primaryKey(),
name: text("name").notNull(),
});
// Posts table (many posts belong to one user)
export const posts = pgTable("posts", {
id: text("id").primaryKey(),
title: text("title").notNull(),
authorId: text("author_id").notNull().references(() => users.id),
});
// Define relations
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
Many-to-Many
import { pgTable, text, primaryKey } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
// Posts table
export const posts = pgTable("posts", {
id: text("id").primaryKey(),
title: text("title").notNull(),
});
// Tags table
export const tags = pgTable("tags", {
id: text("id").primaryKey(),
name: text("name").notNull(),
});
// Junction table
export const postsToTags = pgTable("posts_to_tags", {
postId: text("post_id").notNull().references(() => posts.id),
tagId: text("tag_id").notNull().references(() => tags.id),
}, (table) => ({
pk: primaryKey({ columns: [table.postId, table.tagId] }),
}));
// Define relations
export const postsRelations = relations(posts, ({ many }) => ({
postsToTags: many(postsToTags),
}));
export const tagsRelations = relations(tags, ({ many }) => ({
postsToTags: many(postsToTags),
}));
export const postsToTagsRelations = relations(postsToTags, ({ one }) => ({
post: one(posts, {
fields: [postsToTags.postId],
references: [posts.id],
}),
tag: one(tags, {
fields: [postsToTags.tagId],
references: [tags.id],
}),
}));
Constraints
Primary Keys
import { pgTable, text, integer, primaryKey } from "drizzle-orm/pg-core";
// Single column primary key
export const users = pgTable("users", {
id: text("id").primaryKey(),
});
// Composite primary key
export const userRoles = pgTable("user_roles", {
userId: text("user_id").notNull(),
roleId: text("role_id").notNull(),
}, (table) => ({
pk: primaryKey({ columns: [table.userId, table.roleId] }),
}));
Unique Constraints
import { pgTable, text, unique } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: text("id").primaryKey(),
email: text("email").notNull().unique(), // Column-level unique
username: text("username").notNull(),
}, (table) => ({
// Table-level unique constraint
uniqueUsername: unique("users_username_unique").on(table.username),
}));
Foreign Keys
import { pgTable, text, foreignKey } from "drizzle-orm/pg-core";
export const posts = pgTable("posts", {
id: text("id").primaryKey(),
authorId: text("author_id").notNull(),
}, (table) => ({
// Inline foreign key
authorFk: foreignKey({
columns: [table.authorId],
foreignColumns: [users.id],
}).onDelete("cascade"), // Options: cascade, set null, restrict, no action
}));
// Or use references() shorthand
export const posts2 = pgTable("posts", {
id: text("id").primaryKey(),
authorId: text("author_id").notNull().references(() => users.id, { onDelete: "cascade" }),
});
Check Constraints
import { pgTable, integer, check, sql } from "drizzle-orm/pg-core";
export const products = pgTable("products", {
id: text("id").primaryKey(),
price: integer("price").notNull(),
discount: integer("discount").notNull(),
}, (table) => ({
// Ensure discount is less than price
priceCheck: check("price_check", sql`${table.price} > ${table.discount}`),
}));
Indexes
Single Column Index
import { pgTable, text, index } from "drizzle-orm/pg-core";
export const cars = pgTable("cars", {
id: text("id").primaryKey(),
make: text("make").notNull(),
}, (table) => ({
makeIdx: index("cars_make_idx").on(table.make),
}));
Composite Index
export const cars = pgTable("cars", {
id: text("id").primaryKey(),
make: text("make").notNull(),
model: text("model").notNull(),
}, (table) => ({
makeModelIdx: index("cars_make_model_idx").on(table.make, table.model),
}));
Unique Index
import { uniqueIndex } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: text("id").primaryKey(),
email: text("email").notNull(),
}, (table) => ({
emailIdx: uniqueIndex("users_email_idx").on(table.email),
}));
Partial Index
import { sql } from "drizzle-orm";
export const posts = pgTable("posts", {
id: text("id").primaryKey(),
published: boolean("published").default(false),
publishedAt: timestamp("published_at"),
}, (table) => ({
// Index only published posts
publishedIdx: index("posts_published_idx")
.on(table.publishedAt)
.where(sql`${table.published} = true`),
}));
Schema Workflow
1. Create Schema File
// packages/database/src/db/schema/my-table.ts
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
export const myTable = pgTable("my_table", {
id: text("id").primaryKey(),
name: text("name").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
2. Export from Index
// packages/database/src/db/schema/index.ts
export * from "./cars";
export * from "./coe";
export * from "./posts";
export * from "./my-table"; // Add new export
3. Generate Migration
cd packages/database
# Generate migration from schema changes
pnpm db:generate
# This creates a new migration file in migrations/
4. Review Migration
Check generated SQL in migrations/XXXX_migration_name.sql:
CREATE TABLE IF NOT EXISTS "my_table" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
5. Run Migration
# Apply migration to database
pnpm db:migrate
Common Schema Patterns
Soft Delete
export const posts = pgTable("posts", {
id: text("id").primaryKey(),
title: text("title").notNull(),
deletedAt: timestamp("deleted_at"), // null = not deleted
});
// Query only non-deleted posts
const activePosts = await db.query.posts.findMany({
where: isNull(posts.deletedAt),
});
Timestamps
export const posts = pgTable("posts", {
id: text("id").primaryKey(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
// Update updatedAt on every change
await db.update(posts)
.set({
title: "New Title",
updatedAt: new Date(),
})
.where(eq(posts.id, postId));
Enum Types
import { pgTable, text, pgEnum } from "drizzle-orm/pg-core";
// Define enum
export const roleEnum = pgEnum("role", ["admin", "user", "guest"]);
export const users = pgTable("users", {
id: text("id").primaryKey(),
role: roleEnum("role").default("user").notNull(),
});
UUID Primary Keys
import { pgTable, uuid, text } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: uuid("id").defaultRandom().primaryKey(), // Auto-generate UUID
name: text("name").notNull(),
});
Performance Optimization
Choose Appropriate Indexes
// ✅ Index frequently queried columns
export const cars = pgTable("cars", {
make: text("make").notNull(),
registrationDate: timestamp("registration_date").notNull(),
}, (table) => ({
makeIdx: index().on(table.make), // For: WHERE make = 'Toyota'
dateIdx: index().on(table.registrationDate), // For: WHERE registrationDate > '2024-01-01'
}));
// ❌ Don't index every column
// Only index columns used in WHERE, JOIN, ORDER BY
Use Appropriate Data Types
// ✅ Use smallest appropriate type
count: integer("count"), // -2B to 2B
price: numeric("price", { precision: 10, scale: 2 }), // $99,999,999.99
// ❌ Don't use text for everything
count: text("count"), // Wastes space, slower queries
Denormalization for Performance
// Store computed values to avoid expensive joins
export const posts = pgTable("posts", {
id: text("id").primaryKey(),
authorId: text("author_id").notNull(),
authorName: text("author_name").notNull(), // Denormalized from users table
commentsCount: integer("comments_count").default(0), // Denormalized count
});
Testing Schemas
// packages/database/src/db/schema/__tests__/cars.test.ts
import { describe, it, expect } from "vitest";
import { db } from "../../index";
import { cars } from "../cars";
describe("Cars Schema", () => {
it("inserts and queries car data", async () => {
const [car] = await db.insert(cars).values({
id: "test-1",
make: "Toyota",
model: "Camry",
month: "2024-01",
number: 100,
}).returning();
expect(car.make).toBe("Toyota");
expect(car.number).toBe(100);
});
});
References
- Drizzle ORM Documentation: Use Context7 for latest docs
- Related files:
packages/database/src/db/schema/- All schema filespackages/database/drizzle.config.ts- Drizzle configurationpackages/database/CLAUDE.md- Database package documentation
Best Practices
- Naming: Use camelCase for columns, snake_case for table names
- Not Null: Use .notNull() for required fields
- Defaults: Provide sensible defaults where appropriate
- Indexes: Index columns used in WHERE, JOIN, ORDER BY
- Relationships: Define relations for type-safe queries
- Timestamps: Always include createdAt/updatedAt
- Constraints: Use unique, foreign key constraints
- Migrations: Always review generated migrations before running
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?