Claude Skill

api-database-drizzle

Drizzle ORM, queries, migrations

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-database-drizzle_skills_api-database-drizzle-3a51ef5.zip · 20 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-database-drizzle/skills/api-database-drizzle
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Database with Drizzle ORM + Neon

Quick Guide: Use Drizzle ORM for type-safe queries, Neon serverless Postgres for edge-compatible connections. Schema-first design with automatic TypeScript types. Use RQB v2 with defineRelations() and object-based where syntax. Relational queries with .with() avoid N+1 problems. Use transactions for atomic operations.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST set casing: 'snake_case' in Drizzle config to map camelCase JS to snake_case SQL)

(You MUST use tx parameter (NOT db) inside transaction callbacks to ensure atomicity)

(You MUST use .with() for relational queries to avoid N+1 problems - fetches all data in single SQL query)

(You MUST use defineRelations() for RQB v2 - the old relations() per-table syntax is deprecated)

</critical_requirements>


Detailed Resources:


Auto-detection: drizzle-orm, @neondatabase/serverless, neon-http, db.query, db.transaction, drizzle-kit, pgTable, defineRelations, drizzle-seed

When to use:

  • Serverless functions needing type-safe database queries
  • Schema-first development with migrations
  • Building server-rendered apps with API routes

When NOT to use:

  • Simple apps using framework server actions directly (overhead not justified)
  • Apps needing traditional TCP connection pooling only (use standard Postgres clients)
  • Non-TypeScript projects (lose primary benefit of type safety)
  • Edge functions requiring WebSocket connections (not supported in edge runtime)


Additional Patterns

The following patterns are documented with full examples in examples/:

  • Query Builder - Complex filters, dynamic conditions, custom JOINs - see queries.md
  • Transactions - Atomic operations, error handling, rollback - see transactions.md
  • Database Migrations - Drizzle Kit workflow, generate vs push - see migrations.md
  • Database Seeding - Development data, safe cleanup - see seeding.md

Performance optimization (indexes, prepared statements, pagination) is documented in reference.md.


<red_flags>

RED FLAGS

  • ❌ Using db instead of tx inside transactions - Bypasses transaction context, breaking atomicity
  • ❌ N+1 queries with relations - Use .with() to fetch in one query
  • ❌ Not setting casing: 'snake_case' - Field name mismatches between JS and SQL
  • ❌ Using v1 relations() per-table syntax - Deprecated, use defineRelations()
  • ❌ Using callback-based where/orderBy - v1 syntax deprecated, use object-based syntax
  • ⚠️ Queries without soft delete checks (isNull(deletedAt))
  • ⚠️ No pagination limits on list queries

Gotchas & Edge Cases:

  • Neon HTTP has 30-second query timeout - long queries need WebSocket
  • Prepared statements created outside transactions cannot be used inside transactions
  • enableRLS() deprecated in v1.0.0-beta.1 - use pgTable.withRLS() instead
  • Validator packages consolidated: drizzle-zod is now drizzle-orm/zod (since v1 beta)

For the complete list of anti-patterns and gotchas, see reference.md.

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST set casing: 'snake_case' in Drizzle config to map camelCase JS to snake_case SQL)

(You MUST use tx parameter (NOT db) inside transaction callbacks to ensure atomicity)

(You MUST use .with() for relational queries to avoid N+1 problems - fetches all data in single SQL query)

(You MUST use defineRelations() for RQB v2 - the old relations() per-table syntax is deprecated)

Failure to follow these rules will cause field name mismatches, break transaction atomicity, create N+1 performance issues, and use deprecated APIs.

</critical_reminders>

Files (skills)
  • examples
    • core.md 9.7 KB
      # Core Database Examples
      
      Essential patterns for Drizzle ORM setup and schema definition. These are foundational patterns needed for any database work.
      
      ---
      
      ## Database Connection
      
      ### Neon HTTP Setup (Edge-Compatible)
      
      ```typescript
      // Good Example - Proper Neon HTTP setup
      import { neon } from "@neondatabase/serverless";
      import { drizzle } from "drizzle-orm/neon-http";
      import * as schema from "./db/schema";
      
      if (!process.env.DATABASE_URL) {
        throw new Error("DATABASE_URL environment variable is not set");
      }
      
      // Use neon() for HTTP-based queries (edge-compatible)
      export const sql = neon(process.env.DATABASE_URL);
      
      // Initialize Drizzle with schema and snake_case mapping
      export const db = drizzle(sql, {
        schema,
        casing: "snake_case", // Maps camelCase JS to snake_case SQL
      });
      
      // Export tables for direct query access
      export const { jobs, companies, companyLocations, skills, jobSkills } = schema;
      ```
      
      **Why good:** Environment variable validation prevents runtime errors, `casing: "snake_case"` ensures JS camelCase maps correctly to SQL snake_case preventing field name mismatches, HTTP connection works in all serverless environments
      
      ```typescript
      // Bad Example - Missing critical configuration
      import { neon } from "@neondatabase/serverless";
      import { drizzle } from "drizzle-orm/neon-http";
      
      const sql = neon(process.env.DATABASE_URL!); // No validation
      const db = drizzle(sql); // Missing schema and casing
      
      export default db; // Default export
      ```
      
      **Why bad:** No env validation = runtime crashes, missing `casing` config = field name mismatches between JS and SQL, missing schema = no relational queries available, default export violates project conventions
      
      ---
      
      ### WebSocket Connection (Long-Running Queries)
      
      For queries taking > 30 seconds or LISTEN/NOTIFY.
      
      ```typescript
      // Good Example - WebSocket for long queries
      import { Pool, neonConfig } from "@neondatabase/serverless";
      import ws from "ws";
      
      // Enable WebSocket support (Node.js only, not edge)
      neonConfig.webSocketConstructor = ws;
      
      const QUERY_TIMEOUT_MS = 60000;
      const pool = new Pool({
        connectionString: process.env.DATABASE_URL,
        connectionTimeoutMillis: QUERY_TIMEOUT_MS,
      });
      
      async function longRunningQuery() {
        const client = await pool.connect();
        try {
          const result = await client.query("SELECT * FROM large_table WHERE ...");
          return result.rows;
        } finally {
          client.release(); // Always release connection
        }
      }
      
      export { longRunningQuery };
      ```
      
      **Why good:** WebSocket enables queries > 30 seconds without timeout, connection pooling reuses connections efficiently, `finally` ensures connections are released preventing memory leaks, named constant for timeout improves maintainability
      
      ```typescript
      // Bad Example - Missing connection cleanup
      import { Pool } from "@neondatabase/serverless";
      
      const pool = new Pool({ connectionString: process.env.DATABASE_URL });
      
      async function query() {
        const client = await pool.connect();
        const result = await client.query("SELECT * FROM table");
        return result.rows; // Missing client.release()
      }
      ```
      
      **Why bad:** Missing `client.release()` causes connection leaks, connection pool exhaustion, memory leaks in serverless environments
      
      ---
      
      ### Drizzle Kit Configuration
      
      ```typescript
      // drizzle.config.ts
      import { defineConfig } from "drizzle-kit";
      
      export default defineConfig({
        schema: "./lib/db/schema.ts",
        out: "./drizzle", // Migration files output
        dialect: "postgresql",
        dbCredentials: {
          url: process.env.DATABASE_URL!,
        },
      });
      ```
      
      ---
      
      ## Schema Definition
      
      ### Complete Table Definitions
      
      ```typescript
      // Good Example - Well-structured table with constraints
      export const companies = pgTable("companies", {
        id: uuid("id").primaryKey().defaultRandom(),
        name: varchar("name", { length: 255 }).notNull(),
        slug: varchar("slug", { length: 255 }).unique(),
        description: text("description"),
        logoUrl: text("logo_url"),
        websiteUrl: text("website_url"),
        createdAt: timestamp("created_at").defaultNow(),
        updatedAt: timestamp("updated_at").defaultNow(),
        deletedAt: timestamp("deleted_at"), // Soft delete
      });
      
      export const companyLocations = pgTable("company_locations", {
        id: uuid("id").primaryKey().defaultRandom(),
        companyId: uuid("company_id")
          .references(() => companies.id, { onDelete: "cascade" })
          .notNull(),
        name: varchar("name", { length: 255 }),
        country: varchar("country", { length: 100 }).notNull(),
        city: varchar("city", { length: 100 }),
        isHeadquarters: boolean("is_headquarters").default(false),
        createdAt: timestamp("created_at").defaultNow(),
      });
      
      export const jobs = pgTable("jobs", {
        id: uuid("id").primaryKey().defaultRandom(),
        companyId: uuid("company_id")
          .references(() => companies.id, { onDelete: "cascade" })
          .notNull(),
        title: varchar("title", { length: 255 }).notNull(),
        description: text("description").notNull(),
        externalUrl: text("external_url").notNull(),
        employmentType: employmentTypeEnum("employment_type"),
        seniorityLevel: seniorityLevelEnum("seniority_level"),
        salaryMin: integer("salary_min"),
        salaryMax: integer("salary_max"),
        salaryCurrency: varchar("salary_currency", { length: 3 }).default("EUR"),
        showSalary: boolean("show_salary").default(false),
        isActive: boolean("is_active").default(true),
        createdAt: timestamp("created_at").defaultNow(),
        updatedAt: timestamp("updated_at").defaultNow(),
        deletedAt: timestamp("deleted_at"),
      });
      
      export const skills = pgTable("skills", {
        id: uuid("id").primaryKey().defaultRandom(),
        name: varchar("name", { length: 100 }).notNull().unique(),
        slug: varchar("slug", { length: 100 }).unique(),
        popularityScore: integer("popularity_score").notNull().default(0),
        createdAt: timestamp("created_at").defaultNow(),
      });
      
      // Junction table for many-to-many
      export const jobSkills = pgTable("job_skills", {
        id: uuid("id").primaryKey().defaultRandom(),
        jobId: uuid("job_id")
          .references(() => jobs.id, { onDelete: "cascade" })
          .notNull(),
        skillId: uuid("skill_id")
          .references(() => skills.id, { onDelete: "cascade" })
          .notNull(),
        isRequired: boolean("is_required").default(true),
        createdAt: timestamp("created_at").defaultNow(),
      });
      ```
      
      **Why good:** `uuid().defaultRandom()` generates secure unique IDs, `.notNull()` enforces required fields preventing null errors, enums constrain values to valid options, `onDelete: 'cascade'` prevents orphaned records, `deletedAt` enables soft deletes preserving data history, `createdAt`/`updatedAt` track record lifecycle
      
      ```typescript
      // Bad Example - Missing constraints and best practices
      export const companies = pgTable("companies", {
        id: uuid("id").primaryKey(), // Missing defaultRandom()
        name: varchar("name", { length: 255 }), // Missing .notNull()
        // Missing timestamps
      });
      
      export const jobs = pgTable("jobs", {
        id: uuid("id").primaryKey(),
        companyId: uuid("company_id").references(() => companies.id), // Missing onDelete
        status: varchar("status", { length: 50 }), // Should use enum
      });
      ```
      
      **Why bad:** Missing `defaultRandom()` requires manual ID generation, missing `.notNull()` allows nulls in required fields causing runtime errors, missing `onDelete` leaves orphaned records on deletion, no timestamps prevents tracking creation/updates, varchar for status instead of enum allows invalid values
      
      ---
      
      ### Identity Columns (Alternative to UUID)
      
      PostgreSQL recommends identity columns over serial for auto-incrementing integer primary keys. Use when you prefer integer IDs over UUIDs.
      
      ```typescript
      import { pgTable, integer, text } from "drizzle-orm/pg-core";
      
      // Good Example - Identity column for auto-increment
      export const products = pgTable("products", {
        // GENERATED ALWAYS - database always generates value
        id: integer("id").primaryKey().generatedAlwaysAsIdentity({ startWith: 1000 }),
        name: text("name").notNull(),
        description: text("description"),
        createdAt: timestamp("created_at").defaultNow(),
      });
      
      // Alternative: GENERATED BY DEFAULT - allows manual override
      export const categories = pgTable("categories", {
        id: integer("id").primaryKey().generatedByDefaultAsIdentity(),
        name: text("name").notNull(),
      });
      ```
      
      **Why use identity columns:** PostgreSQL recommended over deprecated `serial` type, customizable sequence options (startWith, increment), allows multiple identity columns per table, standard SQL compliance
      
      **When to use UUID vs Identity:**
      
      - **UUID** (`uuid().defaultRandom()`): Distributed systems, client-generated IDs, prevent ID enumeration
      - **Identity** (`integer().generatedAlwaysAsIdentity()`): Simple auto-increment, smaller storage, faster joins
      
      ---
      
      ### Relations Definition
      
      Use `defineRelations()` (RQB v2) for centralized relation definitions. See [relations-v2.md](relations-v2.md) for full examples.
      
      ```typescript
      // RQB v2 - Centralized relation definitions
      import { defineRelations } from "drizzle-orm";
      import * as schema from "./schema";
      
      export const relations = defineRelations(schema, (r) => ({
        companies: {
          jobs: r.many.jobs({ from: r.companies.id, to: r.jobs.companyId }),
          locations: r.many.companyLocations({
            from: r.companies.id,
            to: r.companyLocations.companyId,
          }),
        },
        jobs: {
          company: r.one.companies({ from: r.jobs.companyId, to: r.companies.id }),
          jobSkills: r.many.jobSkills({ from: r.jobs.id, to: r.jobSkills.jobId }),
        },
        jobSkills: {
          job: r.one.jobs({ from: r.jobSkills.jobId, to: r.jobs.id }),
          skill: r.one.skills({ from: r.jobSkills.skillId, to: r.skills.id }),
        },
        skills: {
          jobSkills: r.many.jobSkills({ from: r.skills.id, to: r.jobSkills.skillId }),
        },
      }));
      ```
      
      ---
      
      ## See Also
      
      - [queries.md](queries.md) - Relational queries with `.with()` and query builder patterns
      - [transactions.md](transactions.md) - Atomic operations and error handling
      - [migrations.md](migrations.md) - Drizzle Kit workflow and best practices
      - [seeding.md](seeding.md) - Development data population
      
    • migrations.md 4 KB
      # Migration Examples
      
      Patterns for managing database schema changes with Drizzle Kit.
      
      > **Version Note:** Drizzle Kit v1.0.0-beta.2 restructured migrations - journal.json removed, SQL files and snapshots now in separate folders per migration. Run `drizzle-kit up` to upgrade existing migrations.
      
      ---
      
      ## Migration Workflow
      
      ```bash
      # 1. Make schema changes in your schema file
      # 2. Generate migration
      bun run drizzle-kit generate
      
      # 3. Review generated SQL in drizzle/ directory
      # 4. Apply migration (development)
      bun run drizzle-kit migrate
      
      # 5. For serverless/production, use push for direct schema sync
      bun run drizzle-kit push
      
      # 6. (v1.0.0-beta.2+) Upgrade existing migrations to new folder structure
      bun run drizzle-kit up
      ```
      
      ---
      
      ## Package.json Scripts
      
      ```json
      {
        "scripts": {
          "db:generate": "drizzle-kit generate",
          "db:migrate": "drizzle-kit migrate",
          "db:push": "drizzle-kit push",
          "db:pull": "drizzle-kit pull",
          "db:up": "drizzle-kit up",
          "db:studio": "drizzle-kit studio"
        }
      }
      ```
      
      ---
      
      ## When to Use `generate` vs `push`
      
      | Command                | Use Case          | Notes                                     |
      | ---------------------- | ----------------- | ----------------------------------------- |
      | `generate` + `migrate` | Development       | Trackable migration files                 |
      | `push`                 | Serverless/CI     | Direct schema sync                        |
      | `pull`                 | Existing DB       | Introspect schema from database           |
      | `up`                   | Migration upgrade | Upgrade to v1.0.0-beta.2 folder structure |
      
      **Critical:** Never use `push` in production with existing data without backup
      
      **Note:** `drizzle-kit drop` was removed in v1.0.0-beta.2. Delete migration folders manually if needed.
      
      ---
      
      ## Migration Naming Convention
      
      Follow this pattern for migration file names (auto-generated use timestamp prefix):
      
      - `20242409125510_init.sql` - Timestamp-prefixed (default)
      - Custom naming with `--name` flag: `drizzle-kit generate --name=add_users_table`
      
      ---
      
      ## Custom Migrations
      
      For data migrations or unsupported DDL:
      
      ```bash
      # Generate empty migration file for custom SQL
      bun run drizzle-kit generate --custom --name=seed-initial-data
      ```
      
      Then edit the generated file with your custom SQL.
      
      ---
      
      ## Migration Table Configuration
      
      Configure where Drizzle stores migration history:
      
      ```typescript
      // drizzle.config.ts
      import { defineConfig } from "drizzle-kit";
      
      export default defineConfig({
        schema: "./lib/db/schema.ts",
        out: "./drizzle",
        dialect: "postgresql",
        dbCredentials: {
          url: process.env.DATABASE_URL!,
        },
        migrations: {
          table: "__drizzle_migrations", // Custom table name (default)
          schema: "public", // PostgreSQL only - schema for migrations table
        },
      });
      ```
      
      ---
      
      ## v1.0.0-beta.2 Migration Structure
      
      New folder structure (eliminates Git conflicts with journal.json):
      
      ```
      drizzle/
        0001_migration_name/
          migration.sql      # SQL statements
          snapshot.json      # Schema snapshot
        0002_another_migration/
          migration.sql
          snapshot.json
      ```
      
      **Upgrading from older structure:**
      
      ```bash
      # Run once to upgrade existing migrations
      bun run drizzle-kit up
      ```
      
      ---
      
      ## Production Migration Patterns
      
      ### Runtime Migrations (Monolith)
      
      Apply migrations during application startup:
      
      ```typescript
      import { migrate } from "drizzle-orm/neon-http/migrator";
      import { db } from "./db";
      
      // Run during deployment/startup
      await migrate(db, { migrationsFolder: "./drizzle" });
      ```
      
      ### Serverless Migrations
      
      For serverless deployments, apply migrations as a separate step:
      
      ```bash
      # In CI/CD pipeline
      bun run drizzle-kit migrate
      
      # Or use push for direct sync
      bun run drizzle-kit push
      ```
      
      ---
      
      ## See Also
      
      - [core.md](core.md) - Drizzle Kit configuration
      - [seeding.md](seeding.md) - Populating development data after migrations
      - [Drizzle Kit Docs](https://orm.drizzle.team/docs/kit-overview) - Official documentation
      - [v1.0.0-beta.2 Release](https://orm.drizzle.team/docs/latest-releases/drizzle-orm-v1beta2) - Migration structure changes
      
    • queries.md 4.9 KB
      # Query Examples
      
      Patterns for relational queries with `.with()` and the Drizzle query builder for complex filtering.
      
      ---
      
      ## Relational Queries
      
      ### Efficient Relational Query with `.with()`
      
      ```typescript
      // Good Example - Efficient relational query
      import { and, eq, desc, asc, isNull } from "drizzle-orm";
      
      const job = await db.query.jobs.findFirst({
        where: and(
          eq(jobs.id, jobId),
          eq(jobs.isActive, true),
          isNull(jobs.deletedAt),
        ),
        with: {
          company: {
            with: {
              locations: {
                orderBy: [
                  desc(companyLocations.isHeadquarters),
                  asc(companyLocations.name),
                ],
              },
            },
          },
          jobSkills: {
            where: eq(jobSkills.isRequired, true),
            with: {
              skill: true,
            },
          },
        },
      });
      
      // Result is fully typed and nested
      if (job) {
        console.log(job.title); // string
        console.log(job.company.name); // string
        console.log(job.company.locations); // CompanyLocation[]
        console.log(job.jobSkills[0].skill.name); // string
      }
      ```
      
      **Why good:** Single SQL query eliminates N+1 problem, nested `.with()` loads deep relations efficiently, soft delete check prevents returning deleted records, ordering nested results provides predictable output, fully typed results catch errors at compile time
      
      ```typescript
      // Bad Example - N+1 query problem
      const job = await db.query.jobs.findFirst({
        where: eq(jobs.id, jobId), // Missing soft delete check
      });
      
      // Separate queries cause N+1 problem
      const company = await db.query.companies.findFirst({
        where: eq(companies.id, job.companyId),
      });
      
      const locations = await db.query.companyLocations.findMany({
        where: eq(companyLocations.companyId, company.id),
      });
      
      const jobSkills = await db.query.jobSkills.findMany({
        where: eq(jobSkills.jobId, job.id),
      });
      ```
      
      **Why bad:** Multiple queries create N+1 problem degrading performance, missing soft delete check returns deleted records, manual relation loading is error-prone, no ordering means unpredictable results, more database round-trips = slower response times
      
      ---
      
      ## Query Builder
      
      ### Basic Query Builder
      
      ```typescript
      // Good Example - Complex filtered query
      import { and, eq, desc, isNull, sql } from "drizzle-orm";
      
      const MAX_RESULTS = 100;
      
      const results = await db
        .select({
          id: jobs.id,
          title: jobs.title,
          companyName: companies.name,
          companyLogo: companies.logoUrl,
        })
        .from(jobs)
        .leftJoin(companies, eq(jobs.companyId, companies.id))
        .where(
          and(
            eq(jobs.isActive, true),
            isNull(jobs.deletedAt),
            sql`LOWER(${jobs.country}) = ${country.toLowerCase()}`,
          ),
        )
        .orderBy(desc(jobs.createdAt))
        .limit(MAX_RESULTS);
      ```
      
      **Why good:** Custom column selection reduces data transfer, named constant for limit improves maintainability, soft delete filter prevents returning deleted records, `LOWER()` makes country comparison case-insensitive, explicit ordering provides predictable results
      
      ```typescript
      // Bad Example - Missing filters and magic numbers
      const results = await db
        .select()
        .from(jobs)
        .leftJoin(companies, eq(jobs.companyId, companies.id))
        .where(sql`country = ${country}`) // Case-sensitive, no soft delete
        .limit(1000); // Magic number
      ```
      
      **Why bad:** Missing soft delete check returns deleted records, case-sensitive comparison misses matches, magic number makes limit unclear and hard to change, selecting all columns wastes bandwidth, no ordering = unpredictable results
      
      ---
      
      ### Dynamic Filtering
      
      ```typescript
      // Good Example - Dynamic filters with proper handling
      import { inArray } from "drizzle-orm";
      
      const conditions = [eq(jobs.isActive, true), isNull(jobs.deletedAt)];
      
      // Country filter: support comma-separated values
      if (country) {
        const countries = country.split(",").map((c) => c.trim().toLowerCase());
        if (countries.length === 1) {
          conditions.push(sql`LOWER(${jobs.country}) = ${countries[0]}`);
        } else {
          conditions.push(
            sql`LOWER(${jobs.country}) IN (${sql.join(
              countries.map((c) => sql`${c}`),
              sql`, `,
            )})`,
          );
        }
      }
      
      // Single value filter
      if (employment_type) {
        conditions.push(eq(jobs.employmentType, employment_type as any));
      }
      
      // Multiple value filter with enum
      if (seniority_level) {
        const seniorities = seniority_level.split(",");
        if (seniorities.length === 1) {
          conditions.push(eq(jobs.seniorityLevel, seniorities[0] as any));
        } else {
          conditions.push(inArray(jobs.seniorityLevel, seniorities as any));
        }
      }
      
      const results = await db
        .select()
        .from(jobs)
        .where(and(...conditions))
        .limit(MAX_RESULTS);
      ```
      
      **Why good:** Conditions array allows dynamic filter building, handles both single and multiple values correctly, `trim()` cleans whitespace from input, case-insensitive country matching prevents missed results, `inArray()` optimizes multiple value queries
      
      ---
      
      ## See Also
      
      - [core.md](core.md) - Database connection and schema definition
      - [transactions.md](transactions.md) - Atomic operations for complex queries
      
    • relations-v2.md 11.3 KB
      # Relational Queries v2 (RQB v2) Examples
      
      RQB v2 introduces `defineRelations()` for centralized relation definitions and object-based `where`/`orderBy` syntax. Available in Drizzle ORM v1.0.0-beta.1+.
      
      ---
      
      ## Defining Relations with defineRelations()
      
      The v2 API consolidates all relations into a single location using `defineRelations()`:
      
      ```typescript
      // Good Example - RQB v2 with defineRelations()
      import { defineRelations } from "drizzle-orm";
      import { users, posts, comments, groups, usersToGroups } from "./schema";
      
      // Define ALL relations in one centralized location
      export const relations = defineRelations(
        { users, posts, comments, groups, usersToGroups },
        (r) => ({
          // Relations for the 'users' table
          users: {
            posts: r.many.posts({
              from: r.users.id,
              to: r.posts.authorId,
            }),
            comments: r.many.comments({
              from: r.users.id,
              to: r.comments.userId,
            }),
            // Many-to-many with .through() - eliminates junction table boilerplate
            groups: r.many.groups({
              from: r.users.id.through(r.usersToGroups.userId),
              to: r.groups.id.through(r.usersToGroups.groupId),
            }),
          },
          // Relations for the 'posts' table
          posts: {
            author: r.one.users({
              from: r.posts.authorId,
              to: r.users.id,
            }),
            comments: r.many.comments({
              from: r.posts.id,
              to: r.comments.postId,
            }),
          },
          // Relations for the 'comments' table
          comments: {
            user: r.one.users({
              from: r.comments.userId,
              to: r.users.id,
            }),
            post: r.one.posts({
              from: r.comments.postId,
              to: r.posts.id,
            }),
          },
          // Relations for the 'groups' table
          groups: {
            members: r.many.users({
              from: r.groups.id.through(r.usersToGroups.groupId),
              to: r.users.id.through(r.usersToGroups.userId),
            }),
          },
        }),
      );
      ```
      
      **Why good:** Single location for all relations enables full autocomplete through `r` parameter, `through()` handles many-to-many junction tables automatically, centralized definition prevents inconsistencies
      
      ```typescript
      // Bad Example - Old v1 syntax (DEPRECATED)
      import { relations } from "drizzle-orm";
      
      // ❌ Per-table relations() calls are deprecated in RQB v2
      export const usersRelations = relations(users, ({ many }) => ({
        posts: many(posts),
      }));
      
      export const postsRelations = relations(posts, ({ one }) => ({
        author: one(users, {
          fields: [posts.authorId],
          references: [users.id],
        }),
      }));
      ```
      
      **Why bad:** Scattered relation definitions across multiple files, harder to maintain, `fields`/`references` syntax replaced by `from`/`to` in v2
      
      ---
      
      ## Database Initialization (v2)
      
      ```typescript
      // Good Example - v2 initialization with relations object
      import { drizzle } from "drizzle-orm/neon-http";
      import { neon } from "@neondatabase/serverless";
      import { relations } from "./relations";
      
      const sql = neon(process.env.DATABASE_URL!);
      
      // Pass relations object (NOT schema)
      export const db = drizzle(sql, {
        relations,
        casing: "snake_case",
      });
      ```
      
      **Why good:** v2 uses `relations` object instead of `schema`, enables RQB v2 query syntax
      
      ```typescript
      // Bad Example - v1 initialization (still works but outdated)
      import { drizzle } from "drizzle-orm/neon-http";
      import * as schema from "./schema";
      
      // ❌ v1 syntax - passing schema with mode
      const db = drizzle(sql, { schema, mode: "default" });
      ```
      
      **Why bad:** v1 syntax doesn't enable RQB v2 features, `mode` parameter removed for most dialects
      
      ---
      
      ## Object-Based Where Syntax
      
      RQB v2 replaces callback-based `where` with simpler object syntax:
      
      ```typescript
      // Good Example - v2 object-based where (simple equality)
      const activeUsers = await db.query.users.findMany({
        where: {
          isActive: true,
          role: "admin",
        },
        with: {
          posts: true,
        },
      });
      
      // Good Example - v2 object-based where with operators
      const filteredUsers = await db.query.users.findMany({
        where: {
          isActive: true,
          age: { gt: 18 }, // Greater than
          email: { like: "%@company.com" }, // LIKE pattern
          status: { in: ["active", "pending"] }, // IN array
          deletedAt: { isNull: true }, // NULL check
        },
        orderBy: { createdAt: "desc" },
      });
      
      // Available operators: eq, ne, gt, gte, lt, lte, in, notIn, like, ilike, isNull, isNotNull
      
      // Complex conditions with logical operators
      const complexQuery = await db.query.users.findMany({
        where: {
          OR: [{ role: "admin" }, { AND: [{ role: "user" }, { age: { gte: 21 } }] }],
          NOT: { status: "banned" },
        },
      });
      ```
      
      **Why good:** Object syntax is cleaner and more readable, still supports full operator set (AND, OR, NOT, gt, lt, like, etc.), orderBy uses object notation
      
      ```typescript
      // Bad Example - v1 callback-based where (DEPRECATED)
      const users = await db.query.users.findMany({
        // ❌ Callback syntax deprecated in RQB v2
        where: (users, { eq }) => eq(users.isActive, true),
        orderBy: (users, { desc }) => [desc(users.createdAt)],
      });
      ```
      
      **Why bad:** Callback syntax is verbose and deprecated, object syntax is cleaner and better typed
      
      ---
      
      ## Many-to-Many with through()
      
      The `through()` method eliminates manual junction table mapping:
      
      ```typescript
      // Good Example - Direct many-to-many access
      // Define the relation with through()
      groups: r.many.groups({
        from: r.users.id.through(r.usersToGroups.userId),
        to: r.groups.id.through(r.usersToGroups.groupId),
      }),
      
      // Query directly - no junction table in results
      const userWithGroups = await db.query.users.findFirst({
        where: { id: userId },
        with: {
          groups: true, // Returns Group[] directly, not UsersToGroups[]
        },
      });
      
      console.log(userWithGroups.groups); // Group[] - junction table is abstracted away
      ```
      
      **Why good:** `through()` abstracts junction table completely, query returns `Group[]` directly instead of `{ groupId, userId, group: Group }[]`
      
      ```typescript
      // Bad Example - v1 manual junction table mapping
      // ❌ v1 required selecting through junction table
      const userWithGroups = await db.query.users.findFirst({
        where: eq(users.id, userId),
        with: {
          usersToGroups: {
            with: {
              group: true,
            },
          },
        },
      });
      
      // Then manually map the results
      const groups = userWithGroups.usersToGroups.map((utg) => utg.group);
      ```
      
      **Why bad:** Requires manual mapping, junction table pollutes type, more verbose queries
      
      ---
      
      ## Filtering on Relations
      
      Filter parent records based on related entities:
      
      ```typescript
      // Good Example - Filter by related entity
      const usersWithRecentPosts = await db.query.users.findMany({
        where: {
          posts: {
            // Filter users who have posts created in last 7 days
            createdAt: gt(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)),
          },
        },
        with: {
          posts: {
            limit: 5,
            orderBy: { createdAt: "desc" },
          },
        },
      });
      ```
      
      **Why good:** Can filter parent by related entity attributes, nested limit/orderBy on relations
      
      ---
      
      ## Predefined Where in Relations
      
      Add default filters directly in relation definitions:
      
      ```typescript
      // Good Example - Predefined filter in relation definition
      const relations = defineRelations({ users, posts }, (r) => ({
        users: {
          // Only return published posts by default
          publishedPosts: r.many.posts({
            from: r.users.id,
            to: r.posts.authorId,
            where: {
              status: "published",
              deletedAt: null,
            },
          }),
          // Separate relation for drafts
          draftPosts: r.many.posts({
            from: r.users.id,
            to: r.posts.authorId,
            where: {
              status: "draft",
            },
          }),
        },
      }));
      
      // Query uses the predefined filter automatically
      const user = await db.query.users.findFirst({
        where: { id: userId },
        with: {
          publishedPosts: true, // Already filtered to status='published'
        },
      });
      ```
      
      **Why good:** Encapsulates common filtering logic, prevents accidental exposure of unpublished/deleted content
      
      **Note:** Predefined `where` can only filter on the target (`to`) table, not the source table.
      
      ---
      
      ## Optional Relations
      
      Control nullability of one-to-one relations:
      
      ```typescript
      // Good Example - Required vs optional relations
      const relations = defineRelations({ users, profiles, settings }, (r) => ({
        users: {
          // Optional (default) - user may not have a profile
          profile: r.one.profiles({
            from: r.users.id,
            to: r.profiles.userId,
            optional: true, // This is the default
          }),
          // Required - throws if user doesn't have settings
          settings: r.one.settings({
            from: r.users.id,
            to: r.settings.userId,
            optional: false, // TypeScript type is non-nullable
          }),
        },
      }));
      ```
      
      **Why good:** `optional: false` provides non-nullable types when relation is guaranteed to exist
      
      ---
      
      ## Splitting Relations with defineRelationsPart
      
      For large schemas, split relations across files:
      
      ```typescript
      // Good Example - Split relations across files
      // relations/users.ts
      import { defineRelationsPart } from "drizzle-orm";
      import * as schema from "../schema";
      
      export const userRelations = defineRelationsPart(schema, (r) => ({
        users: {
          posts: r.many.posts({ from: r.users.id, to: r.posts.authorId }),
        },
      }));
      
      // relations/posts.ts
      export const postRelations = defineRelationsPart(schema, (r) => ({
        posts: {
          author: r.one.users({ from: r.posts.authorId, to: r.users.id }),
        },
      }));
      
      // relations/index.ts - Combine all parts
      import { defineRelations } from "drizzle-orm";
      import * as schema from "../schema";
      import { userRelations } from "./users";
      import { postRelations } from "./posts";
      
      // CRITICAL: Main relations must spread FIRST for TypeScript inference
      export const relations = defineRelations(schema, (r) => ({
        ...userRelations,
        ...postRelations,
      }));
      ```
      
      **Why good:** Keeps large codebases organized, each domain owns its relations
      
      **Critical:** Main `defineRelations()` must spread first to ensure proper TypeScript inference.
      
      ---
      
      ## Migration from v1 to v2
      
      Quick migration steps:
      
      1. Run `npx drizzle-kit pull` to auto-generate v2 relations (optional)
      2. Create `relations.ts` with `defineRelations()` consolidating all relations
      3. Replace `drizzle(url, { schema })` with `drizzle(url, { relations })`
      4. Update queries: convert callback `where`/`orderBy` to object syntax
      5. Update field references: `fields` → `from`, `references` → `to`
      6. Update relation names: `relationName` → `alias`
      
      | v1 Syntax                                 | v2 Syntax                                      |
      | ----------------------------------------- | ---------------------------------------------- |
      | `relations(table, ...)` per table         | `defineRelations({ tables }, ...)` centralized |
      | `fields: [table.column]`                  | `from: r.table.column`                         |
      | `references: [other.id]`                  | `to: r.other.id`                               |
      | `relationName: "author"`                  | `alias: "author"`                              |
      | `where: (t, { eq }) => eq(t.col, val)`    | `where: { col: val }`                          |
      | `orderBy: (t, { desc }) => [desc(t.col)]` | `orderBy: { col: "desc" }`                     |
      | Manual junction table mapping             | `through()` for many-to-many                   |
      
      ---
      
      ## See Also
      
      - [core.md](core.md) - Database connection and schema definition
      - [queries.md](queries.md) - Query builder patterns (still valid for complex queries)
      - [Drizzle Relations v2 Docs](https://orm.drizzle.team/docs/relations-v2)
      - [RQB v2 Docs](https://orm.drizzle.team/docs/rqb-v2)
      - [v1 to v2 Migration](https://orm.drizzle.team/docs/relations-v1-v2)
      
    • seeding.md 5.8 KB
      # Seeding Examples
      
      Patterns for populating development databases with test data. Includes manual seeding and the official `drizzle-seed` package for deterministic fake data generation.
      
      ---
      
      ## drizzle-seed Package (Recommended)
      
      The official `drizzle-seed` package generates deterministic, realistic fake data. Requires `drizzle-orm@0.36.4+`.
      
      ### Installation
      
      ```bash
      bun add drizzle-seed
      ```
      
      ### Basic Usage
      
      ```typescript
      // Good Example - drizzle-seed basic usage
      import { seed, reset } from "drizzle-seed";
      import { drizzle } from "drizzle-orm/neon-http";
      import * as schema from "./db/schema";
      
      const db = drizzle(process.env.DATABASE_URL!, { schema });
      
      const DEFAULT_SEED_COUNT = 10;
      const SEED_VALUE = 42; // Deterministic seed for reproducibility
      
      async function seedDatabase() {
        // Clear all tables (respects foreign keys)
        await reset(db, schema);
      
        // Seed with default options (10 rows per table)
        await seed(db, schema);
      
        // Or with custom count and deterministic seed
        await seed(db, schema, {
          count: DEFAULT_SEED_COUNT,
          seed: SEED_VALUE, // Same seed = same data every run
          version: 2, // Generator version (default: 2) - pin for stability
        });
      
        console.log("Database seeded");
      }
      
      seedDatabase().catch(console.error);
      ```
      
      **Why good:** Deterministic seeding ensures reproducible data across test runs, `reset()` handles foreign key constraints automatically, respects schema relationships
      
      ### Refinements API
      
      Customize data generation per table:
      
      ```typescript
      // Good Example - drizzle-seed with refinements
      import { seed } from "drizzle-seed";
      
      const USERS_COUNT = 20;
      const POSTS_PER_USER = 5;
      
      await seed(db, schema).refine((f) => ({
        users: {
          columns: {
            name: f.fullName(),
            email: f.email(),
            avatar: f.valuesFromArray({ values: ["avatar1.png", "avatar2.png"] }),
          },
          count: USERS_COUNT,
          with: {
            posts: POSTS_PER_USER, // Create 5 posts per user
          },
        },
        posts: {
          columns: {
            title: f.loremIpsum({ sentencesCount: 1 }),
            content: f.loremIpsum({ sentencesCount: 10 }),
            publishedAt: f.date({ minDate: "2024-01-01", maxDate: "2024-12-31" }),
          },
        },
      }));
      ```
      
      **Why good:** Fine-grained control per column, built-in generators (`fullName`, `email`, `loremIpsum`, `date`), `with` creates related records automatically
      
      ### Available Generators
      
      | Generator                                     | Description           |
      | --------------------------------------------- | --------------------- |
      | `f.fullName()`                                | Realistic full names  |
      | `f.email()`                                   | Valid email addresses |
      | `f.companyName()`                             | Company names         |
      | `f.city()`                                    | City names            |
      | `f.date({ minDate, maxDate })`                | Date range            |
      | `f.int({ minValue, maxValue })`               | Integer range         |
      | `f.number({ minValue, maxValue, precision })` | Decimal numbers       |
      | `f.loremIpsum({ sentencesCount })`            | Lorem ipsum text      |
      | `f.valuesFromArray({ values })`               | Pick from array       |
      | `f.weightedRandom([{ weight, value }])`       | Weighted distribution |
      
      ### Reset Behavior
      
      The `reset()` function uses database-specific strategies:
      
      - **PostgreSQL/CockroachDB:** `TRUNCATE ... CASCADE`
      - **MySQL:** `DELETE` with foreign key check disable
      - **SQLite:** `DELETE` with pragma foreign_keys off
      
      ```typescript
      // Reset specific tables only
      await reset(db, { users: schema.users, posts: schema.posts });
      ```
      
      ---
      
      ## Manual Seeding (Simple Cases)
      
      For simple cases or when you need full control over seed data.
      
      ### Safe Seeding with Cleanup
      
      ```typescript
      // Good Example - Safe seeding with cleanup
      import { db } from "./db";
      import { companies, jobs, skills } from "./db/schema";
      
      async function seed() {
        console.log("Seeding database...");
      
        // Clear existing data (development only!)
        await db.delete(jobs);
        await db.delete(companies);
      
        // Seed companies
        const [acme] = await db
          .insert(companies)
          .values({
            name: "Acme Corp",
            slug: "acme-corp",
            websiteUrl: "https://acme.com",
          })
          .returning();
      
        // Seed jobs
        await db.insert(jobs).values([
          {
            companyId: acme.id,
            title: "Senior Engineer",
            description: "Build amazing products",
            externalUrl: "https://acme.com/jobs/1",
            employmentType: "full_time",
          },
          {
            companyId: acme.id,
            title: "Product Designer",
            description: "Design user experiences",
            externalUrl: "https://acme.com/jobs/2",
            employmentType: "full_time",
          },
        ]);
      
        console.log("Database seeded");
      }
      
      seed().catch(console.error);
      ```
      
      **Why good:** Cleanup ensures clean slate, `.returning()` gets IDs for dependent records, error handling with `.catch()` surfaces failures, uses enum values for type safety
      
      ```typescript
      // Bad Example - Unsafe seeding
      async function seed() {
        // No cleanup - causes duplicate errors on re-run
        await db.insert(companies).values({
          name: "Acme Corp",
        }); // Missing .returning()
      
        // Hard-coded UUID - fragile
        await db.insert(jobs).values({
          companyId: "12345",
        });
      }
      ```
      
      **Why bad:** No cleanup causes errors on re-run, missing `.returning()` prevents getting generated IDs, hard-coded UUIDs are fragile and break easily, no error handling hides failures
      
      ---
      
      ## Package.json Script
      
      ```json
      {
        "scripts": {
          "db:seed": "bun run scripts/seed.ts"
        }
      }
      ```
      
      ---
      
      ## See Also
      
      - [core.md](core.md) - Schema definition for seeding
      - [migrations.md](migrations.md) - Apply migrations before seeding
      - [transactions.md](transactions.md) - Wrap seeding in transaction for atomicity
      - [drizzle-seed Docs](https://orm.drizzle.team/docs/seed-overview) - Official documentation
      - [drizzle-seed Generators](https://orm.drizzle.team/docs/seed-functions) - All available generators
      
    • transactions.md 2.4 KB
      # Transaction Examples
      
      Patterns for atomic database operations with proper error handling and rollback.
      
      ---
      
      ## Basic Transaction
      
      ```typescript
      // Good Example - Proper transaction usage
      await db.transaction(async (tx) => {
        // Insert parent
        const [company] = await tx
          .insert(companies)
          .values({
            name: "Acme Corp",
            slug: "acme-corp",
            websiteUrl: "https://acme.com",
          })
          .returning({ id: companies.id });
      
        // Insert children
        await tx.insert(jobs).values([
          {
            companyId: company.id,
            title: "Senior Engineer",
            description: "Build things",
            externalUrl: "https://acme.com/jobs/1",
          },
          {
            companyId: company.id,
            title: "Junior Designer",
            description: "Design things",
            externalUrl: "https://acme.com/jobs/2",
          },
        ]);
      
        // All succeed or all fail together
      });
      ```
      
      **Why good:** Uses `tx` parameter (not `db`) ensuring atomicity, `.returning()` gets inserted ID for child records, all operations succeed or fail together preventing partial data, transaction keeps related data consistent
      
      ```typescript
      // Bad Example - Not using transaction parameter
      await db.transaction(async (tx) => {
        const [company] = await db.insert(companies).values({...}); // Using db instead of tx
        await db.insert(jobs).values({...}); // Using db instead of tx
      });
      ```
      
      **Why bad:** Using `db` instead of `tx` bypasses transaction context breaking atomicity, operations can succeed/fail independently leaving inconsistent data, defeats entire purpose of transaction
      
      ---
      
      ## Transaction with Error Handling
      
      ```typescript
      // Good Example - Transaction with proper error handling
      try {
        await db.transaction(async (tx) => {
          const [job] = await tx
            .update(jobs)
            .set({ isActive: false })
            .where(eq(jobs.id, jobId))
            .returning();
      
          if (!job) {
            throw new Error("Job not found");
          }
      
          await tx.insert(auditLogs).values({
            entityType: "job",
            entityId: job.id,
            action: "deactivated",
            userId: currentUserId,
          });
        });
      } catch (error) {
        console.error("Transaction failed:", error);
        // Nothing was changed in the database
      }
      ```
      
      **Why good:** Try-catch handles transaction failures, throwing error inside transaction rolls back all changes, validation ensures data exists before proceeding, audit log and update are atomic
      
      ---
      
      ## See Also
      
      - [core.md](core.md) - Database connection setup
      - [queries.md](queries.md) - Query patterns for reading data
      
  • reference.md 10.3 KB
    # Database Reference
    
    Decision frameworks, anti-patterns, red flags, and performance optimization for Drizzle ORM + Neon.
    
    ---
    
    <performance>
    
    ## Performance Optimization
    
    ### Query Optimization with Indexes
    
    Add indexes for commonly filtered columns:
    
    ```typescript
    import { index } from "drizzle-orm/pg-core";
    
    export const jobs = pgTable(
      "jobs",
      {
        // ... columns
      },
      (table) => ({
        // Composite index for common filter combinations
        countryEmploymentIdx: index("jobs_country_employment_idx").on(
          table.country,
          table.employmentType,
        ),
        // Partial index for active jobs only
        activeIdx: index("jobs_active_idx")
          .on(table.isActive)
          .where(sql`${table.deletedAt} IS NULL`),
        // Text search index
        titleIdx: index("jobs_title_idx").on(table.title),
      }),
    );
    ```
    
    ### Prepared Statements for Repeated Queries
    
    ```typescript
    const DEFAULT_LIMIT = 50;
    
    // Define once, reuse many times
    const getActiveJobsByCountry = db
      .select()
      .from(jobs)
      .where(
        and(eq(jobs.country, sql.placeholder("country")), eq(jobs.isActive, true)),
      )
      .prepare("get_active_jobs_by_country");
    
    // Execute with parameters (faster than building query each time)
    const results = await getActiveJobsByCountry.execute({ country: "germany" });
    ```
    
    **Caveat:** Prepared statements created outside a transaction cannot be used inside transactions. If you need prepared statements within transactions, create them inside each transaction callback.
    
    ---
    
    ### Batch API (Neon)
    
    Execute multiple statements in a single network round-trip:
    
    ```typescript
    // Batch multiple operations - reduces latency significantly
    const batchResponse = await db.batch([
      db
        .insert(companies)
        .values({ name: "Acme Corp", slug: "acme-corp" })
        .returning(),
      db.insert(jobs).values({ companyId: companyId, title: "Engineer" }),
      db.query.jobs.findMany({ where: eq(jobs.isActive, true) }),
    ]);
    
    // Results are typed and ordered
    const [insertedCompany, insertedJob, activeJobs] = batchResponse;
    ```
    
    **Why use batch:** Single network round-trip instead of multiple, all statements execute in implicit transaction (all succeed or all fail), significantly reduces latency in serverless environments.
    
    **When to use:** Multiple independent operations that should be atomic, bulk inserts/updates, reducing network overhead in Neon serverless.
    
    ### Pagination
    
    Implement offset-based pagination:
    
    ```typescript
    const DEFAULT_PAGE_LIMIT = 50;
    const DEFAULT_PAGE_OFFSET = 0;
    
    export const parsePagination = (limit?: string, offset?: string) => ({
      limit: parseInt(limit || String(DEFAULT_PAGE_LIMIT), 10),
      offset: parseInt(offset || String(DEFAULT_PAGE_OFFSET), 10),
    });
    
    const { limit, offset } = parsePagination(query.limit, query.offset);
    
    const results = await db.select().from(jobs).limit(limit).offset(offset);
    
    // Get total count for pagination
    const [{ count }] = await db
      .select({ count: sql<number>`count(*)::int` })
      .from(jobs)
      .where(and(...conditions));
    
    return { jobs: results, total: count };
    ```
    
    **When NOT to use offset pagination:**
    
    - Real-time feeds (use cursor-based pagination)
    - Large datasets (offset is slow on large tables - use keyset pagination)
    
    </performance>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Relational Query API vs Query Builder?
    
    **Use `db.query` (Relational API) when:**
    
    - Fetching related data defined in schema relations
    - Want simple, readable queries
    - Need nested relations with `.with()`
    - Don't need custom column selection
    
    **Use Query Builder when:**
    
    - Need custom column selection
    - Complex WHERE conditions
    - JOINs not defined in relations
    - Aggregations (COUNT, SUM, etc.)
    
    ### When to use Transactions?
    
    **Use transactions when:**
    
    - Creating parent + child records together
    - Updating related tables that must stay consistent
    - Operations must be atomic (all or nothing)
    
    **Don't use transactions when:**
    
    - Read-only queries
    - Independent operations
    - Long-running operations
    
    ### Neon HTTP vs WebSocket?
    
    **Use HTTP (`neon()`) when:**
    
    - Short-lived serverless functions
    - Edge runtime (serverless edge environments)
    - Simple queries
    
    **Use WebSocket when:**
    
    - Need persistent connections
    - Long-running queries (> 30 seconds)
    - LISTEN/NOTIFY for real-time updates
    - NOT available in all edge environments
    
    </decision_framework>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - ❌ **Using `db` instead of `tx` inside transactions** - Bypasses transaction context, breaking atomicity and causing inconsistent data
    - ❌ **Using `Pool` with edge runtime** - Not compatible with serverless environments, causes runtime errors
    - ❌ **Not setting `casing: 'snake_case'`** - Causes field name mismatches between JS camelCase and SQL snake_case
    - ❌ **Long transactions** - Locks rows, blocks other queries, degrades performance
    - ❌ **N+1 queries with relations** - Multiple database round-trips instead of single query, use `.with()` to fetch in one query
    - ❌ **Using v1 `relations()` per-table syntax** - Deprecated, use `defineRelations()` for RQB v2
    
    **Medium Priority Issues:**
    
    - ⚠️ Queries without soft delete checks - Returns deleted records to users
    - ⚠️ No pagination limits - Can return massive datasets causing memory issues
    - ⚠️ Not using prepared statements - Slower performance for repeated queries
    - ⚠️ Mixing relational queries and query builder - Inconsistent patterns, harder to maintain
    - ⚠️ Using callback-based `where`/`orderBy` syntax - v1 syntax deprecated, use object-based syntax
    
    **Common Mistakes:**
    
    - Forgetting `.returning()` after inserts - Can't access inserted IDs for dependent records
    - Not handling `null` in queries - Causes type errors and runtime crashes
    - Using `parseInt()` without fallback - Returns NaN on invalid input
    - Not cleaning up database connections - Memory leaks in serverless functions
    - Foreign keys without `onDelete` - Orphaned records possible after deletions
    - No timestamps - Can't track creation/updates
    - No soft deletes - Data loss risk
    - Manual junction table mapping - Use `through()` for many-to-many in RQB v2
    
    **Gotchas & Edge Cases:**
    
    - Neon HTTP has 30-second query timeout - long queries need WebSocket connection
    - WebSocket connections not available in edge runtime - must use HTTP
    - `casing: 'snake_case'` config must match actual database column names
    - Relations must be defined separately from tables in Drizzle schema
    - Transaction callbacks receive `tx` parameter - using `db` bypasses transaction
    - Prepared statements created outside transactions cannot be used inside transactions
    - Identity columns: `generatedAlwaysAsIdentity` prevents manual ID insertion (use `generatedByDefaultAsIdentity` if needed)
    - RQB v2 `defineRelations()` must spread main relations first for TypeScript inference when using `defineRelationsPart()`
    - RQB v2 predefined `where` in relations can only filter on target (`to`) table, not source
    - v1.0.0-beta.2 removed journal.json - run `drizzle-kit up` to migrate existing migrations
    - DrizzleQueryError wraps all driver errors (v0.44.0+) - check `error.cause` for original error
    - MSSQL/CockroachDB now supported but RQB v2 not yet available for these dialects
    - `drizzle-kit drop` was removed in v1.0.0-beta.2 - delete migration folders manually
    - Validator packages consolidated (since v1 beta): `drizzle-zod` is now `drizzle-orm/zod`, `drizzle-valibot` is now `drizzle-orm/valibot`
    - `enableRLS()` deprecated in v1.0.0-beta.1 - use `pgTable.withRLS()` instead
    
    </red_flags>
    
    ---
    
    <anti_patterns>
    
    ## Anti-Patterns to Avoid
    
    ### Using `db` Instead of `tx` in Transactions
    
    ```typescript
    // ❌ ANTI-PATTERN: Bypassing transaction context
    await db.transaction(async (tx) => {
      const [company] = await db.insert(companies).values({...}); // Using db!
      await db.insert(jobs).values({ companyId: company.id }); // Using db!
    });
    ```
    
    **Why it's wrong:** Using `db` instead of `tx` bypasses transaction context, operations succeed/fail independently, data becomes inconsistent.
    
    **What to do instead:** Always use the `tx` parameter passed to the callback.
    
    ---
    
    ### N+1 Query Problem
    
    ```typescript
    // ❌ ANTI-PATTERN: Separate queries for related data
    const job = await db.query.jobs.findFirst({ where: eq(jobs.id, jobId) });
    const company = await db.query.companies.findFirst({
      where: eq(companies.id, job.companyId),
    });
    const locations = await db.query.companyLocations.findMany({
      where: eq(companyLocations.companyId, company.id),
    });
    ```
    
    **Why it's wrong:** Multiple database round-trips degrade performance, each query adds latency, doesn't scale with data size.
    
    **What to do instead:** Use `.with()` to fetch all related data in a single query.
    
    ---
    
    ### Missing casing Configuration
    
    ```typescript
    // ❌ ANTI-PATTERN: No casing config
    const db = drizzle(sql, { schema });
    // camelCase JS fields won't match snake_case SQL columns
    ```
    
    **Why it's wrong:** Field name mismatches between JavaScript camelCase and SQL snake_case cause silent failures.
    
    **What to do instead:** Always set `casing: 'snake_case'` in Drizzle config.
    
    ---
    
    ### Queries Without Soft Delete Checks
    
    ```typescript
    // ❌ ANTI-PATTERN: No soft delete filter
    const results = await db
      .select()
      .from(jobs)
      .where(eq(jobs.companyId, companyId));
    // Returns deleted records!
    ```
    
    **Why it's wrong:** Returns records that were soft-deleted, users see data that should be hidden.
    
    **What to do instead:** Always include `isNull(jobs.deletedAt)` in WHERE conditions.
    
    ---
    
    ## When to Use Each Pattern
    
    | Scenario                 | Pattern                      | Reason                    |
    | ------------------------ | ---------------------------- | ------------------------- |
    | Fetch job with company   | `db.query` with `.with()`    | Single query, type-safe   |
    | Custom column selection  | Query builder                | More control over output  |
    | Create company + jobs    | Transaction                  | Atomic operation          |
    | List jobs with filters   | Query builder                | Dynamic conditions        |
    | Paginated list           | Query builder + offset/limit | Standard pagination       |
    | Real-time updates        | WebSocket connection         | LISTEN/NOTIFY support     |
    | Edge runtime             | HTTP connection              | WebSocket not available   |
    | Repeated query           | Prepared statement           | Performance optimization  |
    | Multiple Neon operations | Batch API                    | Single round-trip, atomic |
    
    </anti_patterns>
    
  • SKILL.md 7 KB
    ---
    name: api-database-drizzle
    description: Drizzle ORM, queries, migrations
    ---
    
    # Database with Drizzle ORM + Neon
    
    > **Quick Guide:** Use Drizzle ORM for type-safe queries, Neon serverless Postgres for edge-compatible connections. Schema-first design with automatic TypeScript types. Use RQB v2 with `defineRelations()` and object-based `where` syntax. Relational queries with `.with()` avoid N+1 problems. Use transactions for atomic operations.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST set `casing: 'snake_case'` in Drizzle config to map camelCase JS to snake_case SQL)**
    
    **(You MUST use `tx` parameter (NOT `db`) inside transaction callbacks to ensure atomicity)**
    
    **(You MUST use `.with()` for relational queries to avoid N+1 problems - fetches all data in single SQL query)**
    
    **(You MUST use `defineRelations()` for RQB v2 - the old `relations()` per-table syntax is deprecated)**
    
    </critical_requirements>
    
    ---
    
    **Detailed Resources:**
    
    - For code examples, see [examples/](examples/) folder:
      - [core.md](examples/core.md) - Connection setup and schema definition (always loaded)
      - [queries.md](examples/queries.md) - Relational queries and query builder
      - [relations-v2.md](examples/relations-v2.md) - RQB v2 with defineRelations() (NEW)
      - [transactions.md](examples/transactions.md) - Atomic operations
      - [migrations.md](examples/migrations.md) - Drizzle Kit workflow
      - [seeding.md](examples/seeding.md) - Development data population (includes drizzle-seed)
    - For decision frameworks and anti-patterns, see [reference.md](reference.md)
    
    ---
    
    **Auto-detection:** drizzle-orm, @neondatabase/serverless, neon-http, db.query, db.transaction, drizzle-kit, pgTable, defineRelations, drizzle-seed
    
    **When to use:**
    
    - Serverless functions needing type-safe database queries
    - Schema-first development with migrations
    - Building server-rendered apps with API routes
    
    **When NOT to use:**
    
    - Simple apps using framework server actions directly (overhead not justified)
    - Apps needing traditional TCP connection pooling only (use standard Postgres clients)
    - Non-TypeScript projects (lose primary benefit of type safety)
    - Edge functions requiring WebSocket connections (not supported in edge runtime)
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Database Connection (Neon HTTP)
    
    Configure Drizzle with Neon for serverless/edge compatibility. Key setup requirements:
    
    ```typescript
    export const db = drizzle(sql, {
      schema,
      casing: "snake_case", // Maps camelCase JS to snake_case SQL
    });
    ```
    
    - Validate `DATABASE_URL` before use (throw on missing)
    - Always set `casing: "snake_case"` to prevent field name mismatches
    - Use `neon()` for HTTP (edge-compatible) or `Pool` for WebSocket (long queries)
    
    Full connection setup, WebSocket config, and Drizzle Kit config in [examples/core.md](examples/core.md).
    
    ---
    
    ### Pattern 2: Schema Definition
    
    Define tables with TypeScript types using Drizzle's schema builder:
    
    ```typescript
    export const companies = pgTable("companies", {
      id: uuid("id").primaryKey().defaultRandom(),
      name: varchar("name", { length: 255 }).notNull(),
      slug: varchar("slug", { length: 255 }).unique(),
      deletedAt: timestamp("deleted_at"), // Soft delete
      createdAt: timestamp("created_at").defaultNow(),
    });
    ```
    
    - Use `pgEnum()` for constrained values instead of varchar
    - Always include `createdAt`/`updatedAt` timestamps
    - Add `deletedAt` for soft deletes
    - Set `onDelete: "cascade"` on foreign keys to prevent orphaned records
    - Use `uuid().defaultRandom()` or `integer().generatedAlwaysAsIdentity()` for primary keys
    
    Full schema examples (enums, relations, junction tables, identity columns) in [examples/core.md](examples/core.md).
    
    ---
    
    ### Pattern 3: Relational Queries with `.with()`
    
    Fetch related data efficiently in a single SQL query using `.with()`:
    
    ```typescript
    const job = await db.query.jobs.findFirst({
      where: and(eq(jobs.id, jobId), isNull(jobs.deletedAt)),
      with: {
        company: { with: { locations: true } },
        jobSkills: { with: { skill: true } },
      },
    });
    // Result is fully typed: job.company.name, job.jobSkills[0].skill.name
    ```
    
    - **Use `db.query` with `.with()`** when fetching related data -- single SQL query, no N+1
    - **Use query builder (`db.select()`)** for custom column selection, complex JOINs, aggregations
    - Always include `isNull(deletedAt)` in WHERE conditions for soft-deleted tables
    
    Full relational query examples, N+1 anti-patterns, and dynamic filtering in [examples/queries.md](examples/queries.md).
    
    </patterns>
    
    ---
    
    ## Additional Patterns
    
    The following patterns are documented with full examples in [examples/](examples/):
    
    - **Query Builder** - Complex filters, dynamic conditions, custom JOINs - see [queries.md](examples/queries.md)
    - **Transactions** - Atomic operations, error handling, rollback - see [transactions.md](examples/transactions.md)
    - **Database Migrations** - Drizzle Kit workflow, `generate` vs `push` - see [migrations.md](examples/migrations.md)
    - **Database Seeding** - Development data, safe cleanup - see [seeding.md](examples/seeding.md)
    
    Performance optimization (indexes, prepared statements, pagination) is documented in [reference.md](reference.md#performance-optimization).
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    - ❌ **Using `db` instead of `tx` inside transactions** - Bypasses transaction context, breaking atomicity
    - ❌ **N+1 queries with relations** - Use `.with()` to fetch in one query
    - ❌ **Not setting `casing: 'snake_case'`** - Field name mismatches between JS and SQL
    - ❌ **Using v1 `relations()` per-table syntax** - Deprecated, use `defineRelations()`
    - ❌ **Using callback-based `where`/`orderBy`** - v1 syntax deprecated, use object-based syntax
    - ⚠️ Queries without soft delete checks (`isNull(deletedAt)`)
    - ⚠️ No pagination limits on list queries
    
    **Gotchas & Edge Cases:**
    
    - Neon HTTP has 30-second query timeout - long queries need WebSocket
    - Prepared statements created outside transactions cannot be used inside transactions
    - `enableRLS()` deprecated in v1.0.0-beta.1 - use `pgTable.withRLS()` instead
    - Validator packages consolidated: `drizzle-zod` is now `drizzle-orm/zod` (since v1 beta)
    
    For the complete list of anti-patterns and gotchas, see [reference.md](reference.md#red-flags).
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md**
    
    **(You MUST set `casing: 'snake_case'` in Drizzle config to map camelCase JS to snake_case SQL)**
    
    **(You MUST use `tx` parameter (NOT `db`) inside transaction callbacks to ensure atomicity)**
    
    **(You MUST use `.with()` for relational queries to avoid N+1 problems - fetches all data in single SQL query)**
    
    **(You MUST use `defineRelations()` for RQB v2 - the old `relations()` per-table syntax is deprecated)**
    
    **Failure to follow these rules will cause field name mismatches, break transaction atomicity, create N+1 performance issues, and use deprecated APIs.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related