# Schema Creation and Modification > **Complete Walkthrough**: This is a self-contained, step-by-step guide with its own numbered phases (Phase 1-6). > Follow each phase in order for schema design, modification, and migration workflows. Guide for creating or modifying database schemas with Drizzle. ## Table of Contents - [Workflow Checklist](#workflow-checklist) - [Phase 1: Schema Design Patterns](#phase-1-schema-design-patterns) - [Phase 2: Common Schema Patterns](#phase-2-common-schema-patterns) - [Phase 3: Schema Modifications](#phase-3-schema-modifications) - [Phase 4: Indexes and Constraints](#phase-4-indexes-and-constraints) - [Phase 5: Generate and Apply Changes](#phase-5-generate-and-apply-changes) - [Phase 6: Advanced Patterns](#phase-6-advanced-patterns) - [Common Issues](#common-issues) - [Next Steps](#next-steps) --- ## Workflow Checklist When following this guide, I will track these high-level tasks: - [ ] Design schema using appropriate patterns (tables, relationships, types) - [ ] Apply common schema patterns (auth, soft deletes, enums, JSON) - [ ] Implement schema modifications (add/rename/drop columns, change types) - [ ] Add indexes and constraints for performance and data integrity - [ ] Generate and apply migrations - [ ] Verify changes and test with queries --- ## Phase 1: Schema Design Patterns ### 1.1. Basic Table Structure ```typescript import { pgTable, serial, text, varchar, timestamp, boolean } from 'drizzle-orm/pg-core'; export const tableName = pgTable('table_name', { id: serial('id').primaryKey(), name: varchar('name', { length: 255 }).notNull(), description: text('description'), isActive: boolean('is_active').default(true), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow(), }); ``` **Key conventions:** - Use `serial` for auto-incrementing IDs - Use `varchar` for short strings (with length limit) - Use `text` for long strings - Use `timestamp` for dates/times - Always add `createdAt` for audit trails ### 1.2. Relationships **One-to-Many:** ```typescript import { pgTable, serial, text, timestamp, index } from 'drizzle-orm/pg-core'; export const authors = pgTable('authors', { id: serial('id').primaryKey(), name: text('name').notNull(), }); export const posts = pgTable('posts', { id: serial('id').primaryKey(), authorId: serial('author_id') .notNull() .references(() => authors.id), title: text('title').notNull(), content: text('content').notNull(), }, (table) => ({ authorIdIdx: index('posts_author_id_idx').on(table.authorId), })); ``` **Important:** Always add index on foreign keys for query performance. **Many-to-Many:** ```typescript 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').notNull(), }); export const postsTags = pgTable('posts_tags', { postId: serial('post_id') .notNull() .references(() => posts.id), tagId: serial('tag_id') .notNull() .references(() => tags.id), }, (table) => ({ pk: index('posts_tags_pk').on(table.postId, table.tagId), })); ``` ### 1.3. Type-Safe Relations Enable relational queries: ```typescript import { relations } from 'drizzle-orm'; export const authorsRelations = relations(authors, ({ many }) => ({ posts: many(posts), })); export const postsRelations = relations(posts, ({ one }) => ({ author: one(authors, { fields: [posts.authorId], references: [authors.id], }), })); ``` **Benefits:** - Type-safe joins - Automatic loading of related data - No manual JOIN queries needed ## Phase 2: Common Schema Patterns ### 2.1. User Authentication ```typescript import { pgTable, serial, varchar, timestamp, boolean } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).notNull().unique(), passwordHash: varchar('password_hash', { length: 255 }), name: varchar('name', { length: 255 }).notNull(), emailVerified: boolean('email_verified').default(false), createdAt: timestamp('created_at').defaultNow(), lastLoginAt: timestamp('last_login_at'), }); ``` ### 2.2. Soft Deletes ```typescript export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: text('title').notNull(), content: text('content').notNull(), deletedAt: timestamp('deleted_at'), createdAt: timestamp('created_at').defaultNow(), }); ``` Query with soft deletes: ```typescript const activePosts = await db .select() .from(posts) .where(isNull(posts.deletedAt)); ``` ### 2.3. Enums ```typescript import { pgEnum, pgTable, serial, text } from 'drizzle-orm/pg-core'; export const statusEnum = pgEnum('status', ['draft', 'published', 'archived']); export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: text('title').notNull(), status: statusEnum('status').default('draft'), }); ``` ### 2.4. JSON Fields ```typescript import { pgTable, serial, jsonb } from 'drizzle-orm/pg-core'; export const products = pgTable('products', { id: serial('id').primaryKey(), name: text('name').notNull(), metadata: jsonb('metadata').$type<{ color?: string; size?: string; tags?: string[]; }>(), }); ``` ## Phase 3: Schema Modifications ### 3.1. Adding Columns **Step 1:** Update schema: ```typescript export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).notNull(), phoneNumber: varchar('phone_number', { length: 20 }), // NEW }); ``` **Step 2:** Generate migration: ```bash [package-manager] drizzle-kit generate ``` **Step 3:** Apply migration: ```bash export DATABASE_URL="$(grep DATABASE_URL .env.local | cut -d '=' -f2)" && \ [package-manager] drizzle-kit migrate ``` ### 3.2. Renaming Columns **Important:** Drizzle sees renames as drop + add. Manual migration required. **Step 1:** Update schema: ```typescript export const users = pgTable('users', { id: serial('id').primaryKey(), fullName: varchar('full_name', { length: 255 }), // was 'name' }); ``` **Step 2:** Generate migration (will create drop + add): ```bash [package-manager] drizzle-kit generate ``` **Step 3:** Edit migration file manually: ```sql -- Change from: -- ALTER TABLE users DROP COLUMN name; -- ALTER TABLE users ADD COLUMN full_name VARCHAR(255); -- To: ALTER TABLE users RENAME COLUMN name TO full_name; ``` **Step 4:** Apply migration: ```bash [package-manager] drizzle-kit migrate ``` ### 3.3. Dropping Columns **Step 1:** Remove from schema: ```typescript export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).notNull(), // removed: phoneNumber }); ``` **Step 2:** Generate and apply: ```bash [package-manager] drizzle-kit generate [package-manager] drizzle-kit migrate ``` **Warning:** This permanently deletes data. Back up first! ### 3.4. Changing Column Types **Step 1:** Update schema: ```typescript export const posts = pgTable('posts', { id: serial('id').primaryKey(), views: bigint('views', { mode: 'number' }), // was: integer }); ``` **Step 2:** Generate migration: ```bash [package-manager] drizzle-kit generate ``` **Step 3:** Review generated SQL - may need data migration if incompatible types. ## Phase 4: Indexes and Constraints ### 4.1. Add Indexes **Single column:** ```typescript import { pgTable, serial, text, index } from 'drizzle-orm/pg-core'; export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: text('title').notNull(), authorId: serial('author_id').notNull(), }, (table) => ({ titleIdx: index('posts_title_idx').on(table.title), authorIdIdx: index('posts_author_id_idx').on(table.authorId), })); ``` **Composite index:** ```typescript export const posts = pgTable('posts', { id: serial('id').primaryKey(), authorId: serial('author_id').notNull(), status: text('status').notNull(), }, (table) => ({ authorStatusIdx: index('posts_author_status_idx').on(table.authorId, table.status), })); ``` ### 4.2. Unique Constraints **Single column:** ```typescript export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).notNull().unique(), }); ``` **Multiple columns:** ```typescript import { pgTable, serial, text, unique } from 'drizzle-orm/pg-core'; export const postsTags = pgTable('posts_tags', { postId: serial('post_id').notNull(), tagId: serial('tag_id').notNull(), }, (table) => ({ unq: unique('posts_tags_unique').on(table.postId, table.tagId), })); ``` ### 4.3. Check Constraints ```typescript import { pgTable, serial, integer, check } from 'drizzle-orm/pg-core'; export const products = pgTable('products', { id: serial('id').primaryKey(), price: integer('price').notNull(), discountedPrice: integer('discounted_price'), }, (table) => ({ priceCheck: check('price_check', 'price >= 0'), discountCheck: check('discount_check', 'discounted_price < price'), })); ``` ## Phase 5: Generate and Apply Changes ### 5.1. Generate Migration After any schema changes: ```bash [package-manager] drizzle-kit generate ``` Review generated SQL in `src/db/migrations/`. ### 5.2. Apply Migration With proper environment loading: ```bash export DATABASE_URL="$(grep DATABASE_URL .env.local | cut -d '=' -f2)" && \ [package-manager] drizzle-kit migrate ``` Or use the migration script: ```bash [package-manager] tsx scripts/run-migration.ts ``` ### 5.3. Verify Changes **Check in database:** ```bash psql $DATABASE_URL -c "\d table_name" ``` **Test with queries:** ```typescript import { db } from './src/db'; import { tableName } from './src/db/schema'; const result = await db.select().from(tableName); console.log('Schema works:', result); ``` ## Phase 6: Advanced Patterns For complex schemas, see: - `templates/schema-example.ts` - Multi-table examples with relations - `references/migrations.md` - Advanced migration patterns ## Common Issues - **Migration conflicts:** See `guides/troubleshooting.md` - **Relationship errors:** Ensure foreign keys reference correct columns - **Type mismatches:** Match TypeScript types with SQL types carefully ## Next Steps After schema creation: 1. Run migrations (see above) 2. Create queries (see `references/query-patterns.md`) 3. Add validation (use Zod or similar) 4. Test thoroughly before production