AS
AgSkills.dev
MARKETPLACE

drizzle

Drizzle ORM schema and database guide. Use when working with database schemas (src/database/schemas/*), defining tables, creating migrations, or database model code. Triggers on Drizzle schema definition, database migrations, or ORM usage questions.

72.6k
14.6k

Preview

SKILL.md
name
drizzle
description
Drizzle ORM schema and database guide. Use when working with database schemas (src/database/schemas/*), defining tables, creating migrations, or database model code. Triggers on Drizzle schema definition, database migrations, or ORM usage questions.

Drizzle ORM Schema Style Guide

Configuration

  • Config: drizzle.config.ts
  • Schemas: src/database/schemas/
  • Migrations: src/database/migrations/
  • Dialect: postgresql with strict: true

Helper Functions

Location: src/database/schemas/_helpers.ts

  • timestamptz(name): Timestamp with timezone
  • createdAt(), updatedAt(), accessedAt(): Standard timestamp columns
  • timestamps: Object with all three for easy spread

Naming Conventions

  • Tables: Plural snake_case (users, session_groups)
  • Columns: snake_case (user_id, created_at)

Column Definitions

Primary Keys

id: text('id') .primaryKey() .$defaultFn(() => idGenerator('agents')) .notNull(),

ID prefixes make entity types distinguishable. For internal tables, use uuid.

Foreign Keys

userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(),

Timestamps

...timestamps, // Spread from _helpers.ts

Indexes

// Return array (object style deprecated) (t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],

Type Inference

export const insertAgentSchema = createInsertSchema(agents); export type NewAgent = typeof agents.$inferInsert; export type AgentItem = typeof agents.$inferSelect;

Example Pattern

export const agents = pgTable( 'agents', { id: text('id') .primaryKey() .$defaultFn(() => idGenerator('agents')) .notNull(), slug: varchar('slug', { length: 100 }) .$defaultFn(() => randomSlug(4)) .unique(), userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), clientId: text('client_id'), chatConfig: jsonb('chat_config').$type<LobeAgentChatConfig>(), ...timestamps, }, (t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)], );

Common Patterns

Junction Tables (Many-to-Many)

export const agentsKnowledgeBases = pgTable( 'agents_knowledge_bases', { agentId: text('agent_id') .references(() => agents.id, { onDelete: 'cascade' }) .notNull(), knowledgeBaseId: text('knowledge_base_id') .references(() => knowledgeBases.id, { onDelete: 'cascade' }) .notNull(), userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), enabled: boolean('enabled').default(true), ...timestamps, }, (t) => [primaryKey({ columns: [t.agentId, t.knowledgeBaseId] })], );

Query Style

Always use db.select() builder API. Never use db.query.* relational API (findMany, findFirst, with:).

The relational API generates complex lateral joins with json_build_array that are fragile and hard to debug.

Select Single Row

// βœ… Good const [result] = await this.db .select() .from(agents) .where(eq(agents.id, id)) .limit(1); return result; // ❌ Bad: relational API return this.db.query.agents.findFirst({ where: eq(agents.id, id), });

Select with JOIN

// βœ… Good: explicit select + leftJoin const rows = await this.db .select({ runId: agentEvalRunTopics.runId, score: agentEvalRunTopics.score, testCase: agentEvalTestCases, topic: topics, }) .from(agentEvalRunTopics) .leftJoin(agentEvalTestCases, eq(agentEvalRunTopics.testCaseId, agentEvalTestCases.id)) .leftJoin(topics, eq(agentEvalRunTopics.topicId, topics.id)) .where(eq(agentEvalRunTopics.runId, runId)) .orderBy(asc(agentEvalRunTopics.createdAt)); // ❌ Bad: relational API with `with:` return this.db.query.agentEvalRunTopics.findMany({ where: eq(agentEvalRunTopics.runId, runId), with: { testCase: true, topic: true }, });

Select with Aggregation

// βœ… Good: select + leftJoin + groupBy const rows = await this.db .select({ id: agentEvalDatasets.id, name: agentEvalDatasets.name, testCaseCount: count(agentEvalTestCases.id).as('testCaseCount'), }) .from(agentEvalDatasets) .leftJoin(agentEvalTestCases, eq(agentEvalDatasets.id, agentEvalTestCases.datasetId)) .groupBy(agentEvalDatasets.id);

One-to-Many (Separate Queries)

When you need a parent record with its children, use two queries instead of relational with::

// βœ… Good: two simple queries const [dataset] = await this.db .select() .from(agentEvalDatasets) .where(eq(agentEvalDatasets.id, id)) .limit(1); if (!dataset) return undefined; const testCases = await this.db .select() .from(agentEvalTestCases) .where(eq(agentEvalTestCases.datasetId, id)) .orderBy(asc(agentEvalTestCases.sortOrder)); return { ...dataset, testCases };

Database Migrations

See references/db-migrations.md for detailed migration guide.

# Generate migrations bun run db:generate # After modifying SQL (e.g., adding IF NOT EXISTS) bun run db:generate:client

Migration Best Practices

All migration SQL must be idempotent (safe to re-run):

-- βœ… Tables: IF NOT EXISTS CREATE TABLE IF NOT EXISTS "agent_eval_runs" (...); -- βœ… Columns: IF NOT EXISTS / IF EXISTS ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "avatar" text; ALTER TABLE "users" DROP COLUMN IF EXISTS "old_field"; -- βœ… Foreign keys: DROP IF EXISTS + ADD (no IF NOT EXISTS for constraints) ALTER TABLE "t" DROP CONSTRAINT IF EXISTS "t_fk"; ALTER TABLE "t" ADD CONSTRAINT "t_fk" FOREIGN KEY ("col") REFERENCES "ref"("id") ON DELETE cascade; -- βœ… Indexes: IF NOT EXISTS CREATE INDEX IF NOT EXISTS "users_email_idx" ON "users" ("email"); -- ❌ Non-idempotent (will fail on re-run) CREATE TABLE "agent_eval_runs" (...); ALTER TABLE "users" ADD COLUMN "avatar" text; ALTER TABLE "t" ADD CONSTRAINT "t_fk" FOREIGN KEY ...;

Rename migration files meaningfully: 0046_meaningless.sql β†’ 0046_user_add_avatar.sql

GitHub Repository
lobehub/lobehub
Stars
72,604
Forks
14,665
Open Repository
Install Skill
Download ZIP2 files