Drizzle ORM logoDrizzle ORMv0.45INTERMEDIATE

Drizzle ORM

Drizzle ORM cheat sheet with schema definition, queries, relations, migrations, and TypeScript-first database management examples.

12 min read
drizzleormtypescriptsqldatabasepostgresqlmysqlsqlite
Loading your progress

Database Setup

Connect Drizzle ORM to PostgreSQL, MySQL, or SQLite using the unified drizzle() API.

Connect to PostgreSQL using node-postgres or postgres.js drivers.

typescript
import { drizzle } from 'drizzle-orm/node-postgres';
import * as schema from './schema';

// Connection string — recommended
const db = drizzle(process.env.DATABASE_URL!, { schema });
💡 Pass schema to drizzle() to enable relational queries
⚡ Use connection pooling (Pool) in production
📌 Install: npm i drizzle-orm pg @types/pg
🟢 postgres.js is recommended for serverless
setuppostgresqlconnection

MySQL Setup

Connect to MySQL using mysql2 driver.

typescript
import { drizzle } from 'drizzle-orm/mysql2';
import * as schema from './schema';

const db = drizzle(process.env.DATABASE_URL!, { schema });
💡 Install: npm i drizzle-orm mysql2
⚡ Use createPool() for connection reuse
📌 MySQL uses mysqlTable instead of pgTable
🟢 Works with PlanetScale serverless driver too
setupmysqlconnection

SQLite Setup

Connect to SQLite using better-sqlite3 or libsql drivers.

typescript
import { drizzle } from 'drizzle-orm/better-sqlite3';
import * as schema from './schema';

const db = drizzle('./sqlite.db', { schema });
💡 Install: npm i drizzle-orm better-sqlite3
⚡ SQLite is great for local dev and embedded apps
📌 Turso uses libSQL driver for edge deployments
🟢 SQLite uses sqliteTable instead of pgTable
setupsqliteconnectionturso

Configure drizzle-kit for migrations and studio.

typescript
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  dialect: 'postgresql',
  schema: './src/db/schema.ts',
  out: './drizzle',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});
💡 Install: npm i -D drizzle-kit
⚡ Use defineConfig() for type-safe configuration
📌 Set dialect to match your database engine
🟢 tablesFilter helps with multi-tenant schemas
configdrizzle-kitsetup

Schema Definition

Define tables, columns, types, and constraints using Drizzle's type-safe schema builders.

Define a table with common column types and constraints.

typescript
import { pgTable, serial, text, integer,
  boolean, timestamp, varchar } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: varchar('email', { length: 255 }).unique().notNull(),
  age: integer('age'),
  isActive: boolean('is_active').default(true).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});
💡 Use $type<T>() to add TS typing to json cols
⚡ $onUpdate is a JS callback, not a DB trigger
📌 Column name string maps to actual DB column name
🟢 Use .notNull() to prevent null — safer types
schemacolumnstypestable

Define PostgreSQL enums and use them in table schemas.

typescript
import { pgTable, serial, text,
  pgEnum } from 'drizzle-orm/pg-core';

export const roleEnum = pgEnum('role', [
  'admin', 'user', 'moderator'
]);

export const statusEnum = pgEnum('status', [
  'active', 'inactive', 'suspended'
]);

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  role: roleEnum('role').default('user').notNull(),
  status: statusEnum('status').default('active').notNull(),
});
💡 pgEnum creates a real PostgreSQL ENUM type
⚡ Enum values are type-safe in TS automatically
📌 MySQL uses mysqlEnum() defined inline on column
🟢 Drizzle infers TS union type from enum values
schemaenumtypes

Add indexes, unique constraints, and composite keys to tables.

typescript
import { pgTable, serial, text, integer,
  timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core';

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  slug: text('slug').unique().notNull(),
  authorId: integer('author_id').notNull(),
  category: text('category'),
  createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => ([
  index('author_idx').on(table.authorId),
  uniqueIndex('slug_idx').on(table.slug),
  index('category_author_idx')
    .on(table.category, table.authorId),
]));
💡 Third arg to pgTable defines indexes/constraints
⚡ Composite indexes speed up multi-column queries
📌 uniqueIndex() creates a UNIQUE index in the DB
🟢 Return an array of indexes from the callback
schemaindexesconstraints

Foreign Keys

Define foreign key references between tables.

typescript
import { pgTable, serial, text, integer,
  timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  authorId: integer('author_id')
    .references(() => users.id, {
      onDelete: 'cascade',
    })
    .notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});
💡 .references() creates a DB-level foreign key
⚡ onDelete: 'cascade' auto-deletes child rows
📌 Options: cascade, restrict, no action, set null
🟢 FK is separate from Drizzle relations (ORM)
schemaforeign-keyreferences

Extract TypeScript types from your Drizzle schema definitions.

typescript
import type { InferSelectModel,
  InferInsertModel } from 'drizzle-orm';
import { users } from './schema';

// Infer the SELECT type (what you read from DB)
type User = InferSelectModel<typeof users>;

// Infer the INSERT type (what you write to DB)
type NewUser = InferInsertModel<typeof users>;
💡 InferSelectModel = what DB returns on select
⚡ InferInsertModel makes defaults optional
📌 Use these types in your app layer for safety
🟢 Also available as users.$inferSelect shorthand
schematypesinference

🔗 Relations

Define relationships between tables for Drizzle's relational query builder.

Define a one-to-many relationship between users and posts.

typescript
import { relations } from 'drizzle-orm';
import { pgTable, serial, text,
  integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  authorId: integer('author_id').notNull(),
});

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));
💡 Relations are ORM-level, not DB constraints
⚡ relations() is the stable API (drizzle-orm@latest)
📌 fields = FK column, references = target PK column
🟢 Pass schema to drizzle() to use relations
relationsone-to-many

Define a one-to-one relationship between users and profiles.

typescript
import { relations } from 'drizzle-orm';
import { pgTable, serial, text,
  integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
});

export const profiles = pgTable('profiles', {
  id: serial('id').primaryKey(),
  bio: text('bio'),
  avatarUrl: text('avatar_url'),
  userId: integer('user_id').unique().notNull(),
});

export const usersRelations = relations(users, ({ one }) => ({
  profile: one(profiles),
}));

export const profilesRelations = relations(profiles, ({ one }) => ({
  user: one(users, {
    fields: [profiles.userId],
    references: [users.id],
  }),
}));
💡 one() on both sides makes it one-to-one
⚡ Add .unique() on FK column to enforce in DB
📌 fields/references go on the FK-owning side only
🟢 The non-FK side just uses one(table)
relationsone-to-one

Define a many-to-many relationship using a junction table.

typescript
import { relations } from 'drizzle-orm';
import { pgTable, serial, text,
  integer, primaryKey } from 'drizzle-orm/pg-core';

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
});

export const tags = pgTable('tags', {
  id: serial('id').primaryKey(),
  name: text('name').unique().notNull(),
});

export const postsToTags = pgTable('posts_to_tags', {
  postId: integer('post_id').notNull()
    .references(() => posts.id),
  tagId: integer('tag_id').notNull()
    .references(() => tags.id),
}, (t) => ([
  primaryKey({ columns: [t.postId, t.tagId] }),
]));

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],
  }),
}));
💡 Define relations() on the junction table too
⚡ Query nests through the junction (postsToTags)
📌 many() on each entity points at the junction
🟢 Junction needs FK references + its own relations
relationsmany-to-manyjunction

Use the relational query builder to fetch nested data.

typescript
// Fetch users with their posts
const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: true,
  },
});
💡 db.query.* requires schema passed to drizzle()
⚡ Many-to-many nests through the junction table
📌 Use columns: {} to select specific fields
🟢 findFirst returns single record or undefined
relationsquerywith

New unified relations builder — replaces per-table `relations()` calls

typescript
import { defineRelations } from 'drizzle-orm'
import * as schema from './schema'

export const relations = defineRelations(schema, (r) => ({
  users: {
    posts: r.many.posts(),
    profile: r.one.profiles({
      from: r.users.id,
      to: r.profiles.userId,
    }),
  },
  posts: {
    author: r.one.users({
      from: r.posts.authorId,
      to: r.users.id,
    }),
    tags: r.many.tags({
      from: r.posts.id.through(r.postTags.postId),
      to: r.tags.id.through(r.postTags.tagId),
    }),
  },
}))

// Query (the v2 relational API)
const db = drizzle(client, { schema, relations })
const user = await db.query.users.findFirst({
  where: { id: 1 },
  with: { posts: { with: { tags: true } } },
})
💡 v1 / RQB v2 API — install with npm i drizzle-orm@rc
📌 r.many / r.one with from/to type-checks against your schema
⚡ .through(joinTable.column) handles many-to-many without a separate model
🎯 Not in stable 0.45.x yet — classic relations() is today's default
relationsv2modern

Select Queries

Build type-safe SELECT queries with filters, ordering, and pagination.

Basic Select

Select all or specific columns from a table.

typescript
import { eq } from 'drizzle-orm';

// Select all columns
const allUsers = await db.select().from(users);

// Select specific columns
const names = await db.select({
  id: users.id,
  name: users.name,
}).from(users);
💡 Empty select() returns all columns typed
⚡ Partial select returns only chosen columns
📌 Use sql tag for raw SQL expressions
🟢 selectDistinct() for unique value queries
selectquerycolumns

Filter queries with comparison and logical operators.

typescript
import { eq, ne, gt, gte, lt, lte, like, ilike,
  and, or, not, inArray, notInArray,
  between, isNull, isNotNull } from 'drizzle-orm';

// Equality
await db.select().from(users).where(eq(users.id, 1));

// Comparison
await db.select().from(users).where(gt(users.age, 18));

// Pattern matching (case-insensitive)
await db.select().from(users)
  .where(ilike(users.name, '%john%'));

// Logical AND / OR
await db.select().from(users).where(
  and(
    eq(users.isActive, true),
    or(
      gte(users.age, 18),
      eq(users.role, 'admin')
    )
  )
);
💡 ilike is PostgreSQL-only (case-insensitive)
⚡ All operators imported from 'drizzle-orm'
📌 and()/or() accept any number of conditions
🟢 Operators return SQL typed — fully composable
selectwhereoperatorsfilter

Sort results and implement pagination with limit/offset.

typescript
import { asc, desc } from 'drizzle-orm';

// Order by single column
await db.select().from(users)
  .orderBy(asc(users.name));

// Order by multiple columns
await db.select().from(users)
  .orderBy(desc(users.createdAt), asc(users.name));

// Pagination with limit and offset
const page = 2;
const pageSize = 20;
await db.select().from(users)
  .orderBy(desc(users.createdAt))
  .limit(pageSize)
  .offset((page - 1) * pageSize);
💡 asc/desc imported from 'drizzle-orm'
⚡ Pass multiple columns for multi-level sorting
📌 Always use orderBy with limit for consistency
🟢 Cursor-based pagination is better at scale
selectorderpaginationlimit

✏️ Insert / Update / Delete

Mutate data with type-safe insert, update, and delete operations.

Insert Rows

Insert single or multiple rows with optional returning clause.

typescript
// Insert single row
await db.insert(users).values({
  name: 'Alice',
  email: 'alice@example.com',
});

// Insert with returning
const [newUser] = await db.insert(users).values({
  name: 'Bob',
  email: 'bob@example.com',
}).returning();
💡 .returning() is PostgreSQL and SQLite only
⚡ Bulk insert with array of values is efficient
📌 Defaults are applied automatically by the DB
🟢 Destructure the array to get a single result
insertreturningbulk

Insert or update on conflict using onConflictDoUpdate or onConflictDoNothing.

typescript
// Upsert — update on conflict
await db.insert(users).values({
  email: 'alice@example.com',
  name: 'Alice Updated',
}).onConflictDoUpdate({
  target: users.email,
  set: { name: 'Alice Updated' },
});
💡 target must be a unique or PK column(s)
⚡ sql`excluded.*` references incoming values
📌 onConflictDoNothing silently skips duplicates
🟢 Combine with .returning() to get final row
insertupsertconflict

Update Rows

Update existing rows with type-safe set and where clauses.

typescript
import { eq } from 'drizzle-orm';

// Update single field
await db.update(users)
  .set({ name: 'Alice Smith' })
  .where(eq(users.id, 1));

// Update with returning
const [updated] = await db.update(users)
  .set({ isActive: false })
  .where(eq(users.id, 1))
  .returning();
💡 .set() accepts partial object — only named cols
⚡ Use sql tag for increment/decrement operations
📌 Always add .where() to avoid updating all rows
🟢 .returning() gives you the updated row(s)
updatesetreturning

Delete Rows

Delete rows from tables with type-safe conditions.

typescript
import { eq, lt, and } from 'drizzle-orm';

// Delete by ID
await db.delete(users).where(eq(users.id, 1));

// Delete with returning
const [deleted] = await db.delete(users)
  .where(eq(users.id, 1))
  .returning();

// Delete with complex condition
await db.delete(users).where(
  and(
    eq(users.isActive, false),
    lt(users.createdAt, new Date('2024-01-01'))
  )
);
💡 Always use .where() to prevent deleting all!
⚡ .returning() gets deleted rows (PG/SQLite)
📌 Cascade deletes depend on FK onDelete setting
🟢 Combine with and()/or() for precise targeting
deletereturning

🔗 Joins

Combine data from multiple tables using SQL-level joins.

Inner Join

Return rows that have matching values in both tables.

typescript
import { eq } from 'drizzle-orm';

const result = await db.select({
  postId: posts.id,
  postTitle: posts.title,
  authorName: users.name,
}).from(posts)
  .innerJoin(users, eq(posts.authorId, users.id));
💡 innerJoin excludes rows with no match
⚡ Specify columns in select() to avoid clashes
📌 Join condition uses eq() like where clauses
🟢 Result type is inferred from selected columns
joininner-join

Return all rows from one or both tables, even without matches.

typescript
import { eq } from 'drizzle-orm';

// Left join — all users, even without posts
const result = await db.select({
  userName: users.name,
  postTitle: posts.title, // nullable!
}).from(users)
  .leftJoin(posts, eq(users.id, posts.authorId));
💡 Left join makes right-side columns nullable
⚡ Chain multiple joins for complex queries
📌 Full join makes both sides nullable
🟢 Drizzle adjusts TS types per join type
joinleft-joinright-joinfull-join

📊 Aggregations

Perform aggregate calculations with groupBy and having clauses.

Use count, sum, avg, min, and max for data aggregation.

typescript
import { eq, count, sum, avg, min, max } from 'drizzle-orm';

// Count all rows
const [{ total }] = await db.select({
  total: count(),
}).from(users);

// Sum
const [{ totalRevenue }] = await db.select({
  totalRevenue: sum(orders.amount),
}).from(orders);
💡 count() with no args counts all rows
⚡ count(column) counts non-null values only
📌 sum/avg return string — cast if needed
🟢 Use sql tag for count(distinct ...) queries
aggregationcountsumavg

Group results and filter aggregated data.

typescript
import { eq, gt, count } from 'drizzle-orm';

// Group by with count
const postsByAuthor = await db.select({
  authorId: posts.authorId,
  postCount: count(),
}).from(posts)
  .groupBy(posts.authorId);

// Having — filter groups
const prolificAuthors = await db.select({
  authorId: posts.authorId,
  postCount: count(),
}).from(posts)
  .groupBy(posts.authorId)
  .having(gt(count(), 5));
💡 groupBy groups rows before aggregation
⚡ having filters after aggregation (vs where)
📌 groupBy all non-aggregate selected columns
🟢 Combine with joins for cross-table aggregates
aggregationgroup-byhaving

🔒 Transactions

Execute multiple operations atomically with transaction support.

Wrap multiple queries in an ACID transaction that auto-rolls back on error.

typescript
const result = await db.transaction(async (tx) => {
  const [user] = await tx.insert(users).values({
    name: 'Alice',
    email: 'alice@example.com',
  }).returning();

  await tx.insert(profiles).values({
    userId: user.id,
    bio: 'Hello world',
  });

  return user;
});
💡 Use tx instead of db inside transactions
⚡ Throwing an error auto-triggers rollback
📌 tx.rollback() for manual abort when needed
🟢 Transaction return value becomes result
transactionrollbackatomic

Use nested transactions that create savepoints for partial rollback.

typescript
await db.transaction(async (tx) => {
  await tx.insert(users).values({
    name: 'Alice',
    email: 'alice@example.com',
  });

  // Nested transaction — creates a SAVEPOINT
  try {
    await tx.transaction(async (tx2) => {
      await tx2.insert(posts).values({
        title: 'First Post',
        authorId: 1,
      });
      // This will rollback only the nested tx
      throw new Error('Oops');
    });
  } catch {
    // Nested tx rolled back, outer tx continues
    console.log('Nested transaction failed');
  }

  // This still commits — Alice is created
  await tx.insert(profiles).values({
    userId: 1,
    bio: 'No posts yet',
  });
});
💡 Nested tx.transaction() creates a SAVEPOINT
⚡ Only nested changes roll back on failure
📌 Outer transaction continues after nested fail
🟢 Wrap nested tx in try/catch to handle errors
transactionnestedsavepoint

Prepared Statements & Raw SQL

Optimize query performance with prepared statements and raw SQL expressions.

Pre-compile queries for repeated execution with different parameters.

typescript
import { eq, placeholder } from 'drizzle-orm';

// Create a prepared statement
const getUserById = db.select()
  .from(users)
  .where(eq(users.id, placeholder('id')))
  .prepare('get_user_by_id');

// Execute with different params
const user1 = await getUserById.execute({ id: 1 });
const user2 = await getUserById.execute({ id: 2 });
💡 Prepared stmts skip query planning on reuse
⚡ Use placeholder() for dynamic parameter slots
📌 Name the prepared stmt for DB-level caching
🟢 Works with select, insert, update, and delete
preparedplaceholderperformance

Use raw SQL expressions and build dynamic queries conditionally.

typescript
import { sql, eq } from 'drizzle-orm';

// Raw SQL query
const result = await db.execute(
  sql`SELECT * FROM users WHERE id = ${userId}`
);
💡 sql tag auto-parameterizes to prevent injection
⚡ Use sql<Type> to type raw SQL results
📌 $dynamic() enables conditional query building
🟢 Drizzle escapes all interpolated values safely
raw-sqldynamicperformance

📦 Migrations

Manage database schema changes with drizzle-kit CLI commands.

Generate SQL migration files from schema changes and apply them.

bash
# Generate migration from schema diff
npx drizzle-kit generate

# Apply pending migrations
npx drizzle-kit migrate

# Push schema directly (no migration files)
npx drizzle-kit push

# Open Drizzle Studio (DB GUI)
npx drizzle-kit studio
💡 generate creates SQL files in your out dir
⚡ push is great for prototyping — skips files
📌 migrate runs pending SQL migration files
🟢 studio opens a GUI at https://local.drizzle.studio
migrationsdrizzle-kitcli

Typical development workflow with schema changes and migrations.

bash
# 1. Edit your schema file (e.g., src/db/schema.ts)
#    Add/modify tables, columns, indexes

# 2. Generate a migration
npx drizzle-kit generate
# Creates: drizzle/0001_add_user_table.sql

# 3. Review the generated SQL
cat drizzle/0001_add_user_table.sql

# 4. Apply the migration
npx drizzle-kit migrate

# 5. Check current schema status
npx drizzle-kit check

# 6. Drop a migration (if needed before applying)
npx drizzle-kit drop
💡 Always review generated SQL before migrating
⚡ check validates migration consistency
📌 drop removes last migration file if unapplied
🟢 Commit migration files to version control
migrationsworkflowdrizzle-kit

Run migrations from your application code at startup.

typescript
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';

const db = drizzle(process.env.DATABASE_URL!);

// Run migrations on app startup
await migrate(db, {
  migrationsFolder: './drizzle',
});

console.log('Migrations complete');
💡 Import migrate from your specific driver path
⚡ Runs all pending migrations in order
📌 Safe to call on every startup — skips applied
🟢 Great for serverless or Docker deployments
migrationsprogrammaticstartup

When to use push (fast iteration) vs generate + migrate (versioned, prod-safe)

bash
# Fast iteration — diff schema and push to the DB directly
npx drizzle-kit push           # no SQL files, no history; great for local/dev

# Versioned migrations — generate SQL, commit it, then apply
npx drizzle-kit generate       # writes drizzle/<timestamp>_<name>.sql
npx drizzle-kit migrate        # applies pending migration files

# Other useful kit commands
npx drizzle-kit studio         # browse + edit data at localhost:4983
npx drizzle-kit check          # verify migration files are consistent
npx drizzle-kit up             # rewrite drizzle metadata after manual edits

# DETAILED_TAB:
# ===== drizzle-kit push =====
# What it does: diffs your schema.ts against the live DB and runs the
# resulting ALTERs immediately. No SQL files written. No history.
#
# Use when:
#   - Local dev where the schema is still in flux
#   - A throwaway preview branch
#   - You don't need an audit trail of how the schema evolved
#
# Don't use in production — there's no replayable record of changes.
npx drizzle-kit push
npx drizzle-kit push --verbose            # show every SQL statement
npx drizzle-kit push --strict             # confirm potentially destructive ops

# ===== drizzle-kit generate + migrate =====
# What it does:
#   generate → diffs schema.ts and writes a numbered .sql file
#              into your migrations folder
#   migrate  → applies all unapplied files in order, tracked in a
#              __drizzle_migrations table
#
# This is the production workflow. Commit the SQL files to git so
# every deploy sees the same migration history.

# drizzle.config.ts
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: { url: process.env.DATABASE_URL! },
})

# Workflow
npx drizzle-kit generate --name=add_posts_table   # writes 0001_add_posts_table.sql
git add drizzle/
git commit -m 'migration: add posts table'
npx drizzle-kit migrate                            # apply locally / in CI

# Programmatic migrate (e.g. in a CI step or app startup)
import { migrate } from 'drizzle-orm/node-postgres/migrator'
await migrate(db, { migrationsFolder: './drizzle' })

# ===== Hybrid pattern that works for most teams =====
#   - Local dev: npx drizzle-kit push for fast iteration
#   - Before PR: npx drizzle-kit generate to capture the final shape
#   - CI / prod: npx drizzle-kit migrate from the generated SQL files
💡 push = no SQL files, fast loops; migrate = versioned, replayable, prod-safe
⚠️ Never use push in production — there is no audit trail of changes
📌 Commit the generated SQL files in /drizzle so every env converges
🎯 Common pattern: push locally → generate before PR → migrate in CI
migrationsworkflowdrizzle-kit