@girardmedia/bootspring 3.3.2 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: database-migrations
|
|
3
|
+
description: Database migration patterns for Prisma, Knex, zero-downtime deploys, rollback strategies, and seed data management.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Database Migration Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply migration patterns whenever you change database schema: adding tables, modifying columns, creating indexes, or altering constraints. Migrations provide version-controlled, repeatable, and reversible schema changes. Use these patterns from the first database change to prevent drift between environments. Zero-downtime techniques are essential for production deployments with active traffic.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Prisma Migrate Workflow
|
|
14
|
+
|
|
15
|
+
```prisma
|
|
16
|
+
// prisma/schema.prisma
|
|
17
|
+
generator client {
|
|
18
|
+
provider = "prisma-client-js"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
datasource db {
|
|
22
|
+
provider = "postgresql"
|
|
23
|
+
url = env("DATABASE_URL")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
model User {
|
|
27
|
+
id String @id @default(cuid())
|
|
28
|
+
email String @unique
|
|
29
|
+
name String?
|
|
30
|
+
role Role @default(USER)
|
|
31
|
+
posts Post[]
|
|
32
|
+
createdAt DateTime @default(now())
|
|
33
|
+
updatedAt DateTime @updatedAt
|
|
34
|
+
|
|
35
|
+
@@index([email])
|
|
36
|
+
@@index([createdAt])
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
model Post {
|
|
40
|
+
id String @id @default(cuid())
|
|
41
|
+
title String @db.VarChar(255)
|
|
42
|
+
slug String @unique
|
|
43
|
+
body String
|
|
44
|
+
published Boolean @default(false)
|
|
45
|
+
author User @relation(fields: [authorId], references: [id])
|
|
46
|
+
authorId String
|
|
47
|
+
tags Tag[]
|
|
48
|
+
createdAt DateTime @default(now())
|
|
49
|
+
|
|
50
|
+
@@index([authorId])
|
|
51
|
+
@@index([slug])
|
|
52
|
+
@@index([published, createdAt])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
model Tag {
|
|
56
|
+
id String @id @default(cuid())
|
|
57
|
+
name String @unique
|
|
58
|
+
posts Post[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
enum Role {
|
|
62
|
+
USER
|
|
63
|
+
ADMIN
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Create a new migration
|
|
69
|
+
npx prisma migrate dev --name add_tags_table
|
|
70
|
+
|
|
71
|
+
# Apply migrations in production (no prompt)
|
|
72
|
+
npx prisma migrate deploy
|
|
73
|
+
|
|
74
|
+
# Reset database (dev only)
|
|
75
|
+
npx prisma migrate reset
|
|
76
|
+
|
|
77
|
+
# Check migration status
|
|
78
|
+
npx prisma migrate status
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Knex Migration Pattern
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// migrations/20260526120000_create_users.ts
|
|
85
|
+
import { Knex } from 'knex';
|
|
86
|
+
|
|
87
|
+
export async function up(knex: Knex): Promise<void> {
|
|
88
|
+
await knex.schema.createTable('users', (table) => {
|
|
89
|
+
table.uuid('id').primary().defaultTo(knex.fn.uuid());
|
|
90
|
+
table.string('email', 255).notNullable().unique();
|
|
91
|
+
table.string('name', 255);
|
|
92
|
+
table.enum('role', ['user', 'admin']).defaultTo('user');
|
|
93
|
+
table.timestamps(true, true); // created_at, updated_at with defaults
|
|
94
|
+
|
|
95
|
+
table.index(['email']);
|
|
96
|
+
table.index(['created_at']);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function down(knex: Knex): Promise<void> {
|
|
101
|
+
await knex.schema.dropTableIfExists('users');
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// migrations/20260526130000_add_posts_table.ts
|
|
107
|
+
import { Knex } from 'knex';
|
|
108
|
+
|
|
109
|
+
export async function up(knex: Knex): Promise<void> {
|
|
110
|
+
await knex.schema.createTable('posts', (table) => {
|
|
111
|
+
table.uuid('id').primary().defaultTo(knex.fn.uuid());
|
|
112
|
+
table.string('title', 255).notNullable();
|
|
113
|
+
table.string('slug', 255).notNullable().unique();
|
|
114
|
+
table.text('body').notNullable();
|
|
115
|
+
table.boolean('published').defaultTo(false);
|
|
116
|
+
table.uuid('author_id').notNullable()
|
|
117
|
+
.references('id').inTable('users').onDelete('CASCADE');
|
|
118
|
+
table.timestamps(true, true);
|
|
119
|
+
|
|
120
|
+
table.index(['author_id']);
|
|
121
|
+
table.index(['slug']);
|
|
122
|
+
table.index(['published', 'created_at']);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function down(knex: Knex): Promise<void> {
|
|
127
|
+
await knex.schema.dropTableIfExists('posts');
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Zero-Downtime Column Addition
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// Step 1: Add nullable column (non-breaking)
|
|
135
|
+
export async function up(knex: Knex): Promise<void> {
|
|
136
|
+
await knex.schema.alterTable('users', (table) => {
|
|
137
|
+
table.string('avatar_url', 500).nullable();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Step 2: Backfill data (separate migration, can run while app serves traffic)
|
|
142
|
+
export async function up(knex: Knex): Promise<void> {
|
|
143
|
+
const batchSize = 1000;
|
|
144
|
+
let offset = 0;
|
|
145
|
+
let rows;
|
|
146
|
+
|
|
147
|
+
do {
|
|
148
|
+
rows = await knex('users')
|
|
149
|
+
.whereNull('avatar_url')
|
|
150
|
+
.limit(batchSize)
|
|
151
|
+
.offset(offset)
|
|
152
|
+
.select('id');
|
|
153
|
+
|
|
154
|
+
if (rows.length > 0) {
|
|
155
|
+
await knex('users')
|
|
156
|
+
.whereIn('id', rows.map((r: any) => r.id))
|
|
157
|
+
.update({ avatar_url: 'https://cdn.example.com/default-avatar.png' });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
offset += batchSize;
|
|
161
|
+
} while (rows.length === batchSize);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Step 3: Add NOT NULL constraint (after backfill verified)
|
|
165
|
+
export async function up(knex: Knex): Promise<void> {
|
|
166
|
+
await knex.schema.alterTable('users', (table) => {
|
|
167
|
+
table.string('avatar_url', 500).notNullable().defaultTo('https://cdn.example.com/default-avatar.png').alter();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Zero-Downtime Column Rename (Expand-Contract)
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// Phase 1: Add new column, copy data
|
|
176
|
+
export async function up(knex: Knex): Promise<void> {
|
|
177
|
+
// Add new column
|
|
178
|
+
await knex.schema.alterTable('users', (table) => {
|
|
179
|
+
table.string('display_name', 255);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Copy data
|
|
183
|
+
await knex.raw('UPDATE users SET display_name = name');
|
|
184
|
+
|
|
185
|
+
// Add trigger to keep columns in sync during transition
|
|
186
|
+
await knex.raw(`
|
|
187
|
+
CREATE OR REPLACE FUNCTION sync_user_name() RETURNS TRIGGER AS $$
|
|
188
|
+
BEGIN
|
|
189
|
+
IF TG_OP = 'INSERT' OR NEW.name IS DISTINCT FROM OLD.name THEN
|
|
190
|
+
NEW.display_name = NEW.name;
|
|
191
|
+
END IF;
|
|
192
|
+
IF NEW.display_name IS DISTINCT FROM OLD.display_name THEN
|
|
193
|
+
NEW.name = NEW.display_name;
|
|
194
|
+
END IF;
|
|
195
|
+
RETURN NEW;
|
|
196
|
+
END;
|
|
197
|
+
$$ LANGUAGE plpgsql;
|
|
198
|
+
|
|
199
|
+
CREATE TRIGGER sync_name_trigger BEFORE INSERT OR UPDATE ON users
|
|
200
|
+
FOR EACH ROW EXECUTE FUNCTION sync_user_name();
|
|
201
|
+
`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Phase 2: Deploy app code reading from display_name
|
|
205
|
+
// Phase 3: Remove old column and trigger
|
|
206
|
+
export async function up(knex: Knex): Promise<void> {
|
|
207
|
+
await knex.raw('DROP TRIGGER IF EXISTS sync_name_trigger ON users');
|
|
208
|
+
await knex.raw('DROP FUNCTION IF EXISTS sync_user_name()');
|
|
209
|
+
await knex.schema.alterTable('users', (table) => {
|
|
210
|
+
table.dropColumn('name');
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Seed Data Management
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// prisma/seed.ts
|
|
219
|
+
import { PrismaClient } from '@prisma/client';
|
|
220
|
+
|
|
221
|
+
const prisma = new PrismaClient();
|
|
222
|
+
|
|
223
|
+
async function main() {
|
|
224
|
+
// Upsert to make seeds idempotent
|
|
225
|
+
const admin = await prisma.user.upsert({
|
|
226
|
+
where: { email: 'admin@example.com' },
|
|
227
|
+
update: {},
|
|
228
|
+
create: {
|
|
229
|
+
email: 'admin@example.com',
|
|
230
|
+
name: 'Admin',
|
|
231
|
+
role: 'ADMIN',
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const tags = ['typescript', 'react', 'node', 'database'];
|
|
236
|
+
for (const name of tags) {
|
|
237
|
+
await prisma.tag.upsert({
|
|
238
|
+
where: { name },
|
|
239
|
+
update: {},
|
|
240
|
+
create: { name },
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.log(`Seeded: ${admin.email}, ${tags.length} tags`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
main()
|
|
248
|
+
.then(() => prisma.$disconnect())
|
|
249
|
+
.catch((e) => {
|
|
250
|
+
console.error(e);
|
|
251
|
+
prisma.$disconnect();
|
|
252
|
+
process.exit(1);
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Rollback Strategy
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// scripts/rollback-migration.ts
|
|
260
|
+
import { execSync } from 'child_process';
|
|
261
|
+
|
|
262
|
+
async function rollback(steps: number = 1) {
|
|
263
|
+
console.log(`Rolling back ${steps} migration(s)...`);
|
|
264
|
+
|
|
265
|
+
// For Knex
|
|
266
|
+
execSync(`npx knex migrate:rollback --knexfile knexfile.ts --step ${steps}`, {
|
|
267
|
+
stdio: 'inherit',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Verify state
|
|
271
|
+
execSync('npx knex migrate:status --knexfile knexfile.ts', { stdio: 'inherit' });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// For Prisma, rollback requires resolving migration:
|
|
275
|
+
// npx prisma migrate resolve --rolled-back MIGRATION_NAME
|
|
276
|
+
// Then manually reverse the changes
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Migration Testing
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// __tests__/migrations.test.ts
|
|
283
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
284
|
+
import { Knex, knex } from 'knex';
|
|
285
|
+
|
|
286
|
+
describe('database migrations', () => {
|
|
287
|
+
let db: Knex;
|
|
288
|
+
|
|
289
|
+
beforeAll(async () => {
|
|
290
|
+
db = knex({ client: 'pg', connection: process.env.TEST_DATABASE_URL });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
afterAll(async () => { await db.destroy(); });
|
|
294
|
+
|
|
295
|
+
it('applies all migrations without error', async () => {
|
|
296
|
+
await db.migrate.latest();
|
|
297
|
+
const [, pending] = await db.migrate.status();
|
|
298
|
+
expect(pending).toHaveLength(0);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('rolls back without error', async () => {
|
|
302
|
+
await db.migrate.rollback(undefined, true); // rollback all
|
|
303
|
+
await db.migrate.latest(); // reapply
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('creates expected tables', async () => {
|
|
307
|
+
const tables = await db.raw(`
|
|
308
|
+
SELECT table_name FROM information_schema.tables
|
|
309
|
+
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
310
|
+
`);
|
|
311
|
+
const names = tables.rows.map((r: any) => r.table_name);
|
|
312
|
+
expect(names).toContain('users');
|
|
313
|
+
expect(names).toContain('posts');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('creates expected indexes', async () => {
|
|
317
|
+
const indexes = await db.raw(`
|
|
318
|
+
SELECT indexname FROM pg_indexes WHERE tablename = 'posts'
|
|
319
|
+
`);
|
|
320
|
+
const names = indexes.rows.map((r: any) => r.indexname);
|
|
321
|
+
expect(names).toContain('posts_slug_unique');
|
|
322
|
+
expect(names).toContain('posts_author_id_index');
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Examples
|
|
328
|
+
|
|
329
|
+
| Change | Strategy | Downtime Risk |
|
|
330
|
+
|--------|----------|---------------|
|
|
331
|
+
| Add nullable column | Single migration | None |
|
|
332
|
+
| Add NOT NULL column | 3-step: add nullable, backfill, add constraint | None |
|
|
333
|
+
| Rename column | Expand-contract with sync trigger | None |
|
|
334
|
+
| Drop column | Remove from app code first, then drop | None if app deployed first |
|
|
335
|
+
| Add index | `CREATE INDEX CONCURRENTLY` (PG) | None |
|
|
336
|
+
| Change column type | Add new column, migrate, swap | None |
|
|
337
|
+
|
|
338
|
+
## Checklist
|
|
339
|
+
- [ ] Every schema change has an up and down migration
|
|
340
|
+
- [ ] Migrations are idempotent (re-running does not fail)
|
|
341
|
+
- [ ] Seeds use upsert to be safely re-runnable
|
|
342
|
+
- [ ] Column additions are nullable or have defaults (non-breaking)
|
|
343
|
+
- [ ] Destructive changes use expand-contract pattern over multiple deploys
|
|
344
|
+
- [ ] Indexes created with `CONCURRENTLY` option on production databases
|
|
345
|
+
- [ ] Migration tests verify apply, rollback, and expected schema
|
|
346
|
+
- [ ] Backfill migrations process data in batches to avoid lock contention
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: database-patterns
|
|
3
|
+
description: Design databases for performance and safety — indexes, N+1 prevention, pooling, migrations, and row-level security.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Database Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when designing schemas, writing queries, or operating
|
|
11
|
+
databases in production. Most performance problems are database problems — a
|
|
12
|
+
missing index or N+1 query can make a 10ms page take 10 seconds. Fix these
|
|
13
|
+
at design time, not after users complain.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### 1. Indexing Strategy
|
|
18
|
+
|
|
19
|
+
Create indexes for columns in WHERE, JOIN, and ORDER BY clauses.
|
|
20
|
+
|
|
21
|
+
```sql
|
|
22
|
+
-- Single column: filter by status
|
|
23
|
+
CREATE INDEX idx_orders_status ON orders(status);
|
|
24
|
+
|
|
25
|
+
-- Composite: filter by user + sort by date
|
|
26
|
+
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC);
|
|
27
|
+
|
|
28
|
+
-- Partial: only index active rows
|
|
29
|
+
CREATE INDEX idx_active_users ON users(email) WHERE deleted_at IS NULL;
|
|
30
|
+
|
|
31
|
+
-- Covering: index includes all selected columns (index-only scan)
|
|
32
|
+
CREATE INDEX idx_orders_cover ON orders(user_id, status) INCLUDE (total, created_at);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Column order in composite indexes matters. The leftmost column must appear in
|
|
36
|
+
the query for the index to be used. `(user_id, status)` works for queries
|
|
37
|
+
filtering by `user_id` alone or `user_id + status`, but not `status` alone.
|
|
38
|
+
|
|
39
|
+
Always verify with EXPLAIN:
|
|
40
|
+
|
|
41
|
+
```sql
|
|
42
|
+
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 'usr_123' AND status = 'pending';
|
|
43
|
+
-- Look for: Index Scan, not Seq Scan
|
|
44
|
+
-- Check: actual time, rows examined vs returned
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. N+1 Query Prevention
|
|
48
|
+
|
|
49
|
+
The N+1 problem: 1 query to get 50 users, then 50 queries to get each user's
|
|
50
|
+
orders.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// Bad — N+1
|
|
54
|
+
const users = await db.user.findMany();
|
|
55
|
+
for (const user of users) {
|
|
56
|
+
user.orders = await db.order.findMany({ where: { userId: user.id } });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Good — eager load (2 queries total)
|
|
60
|
+
const users = await db.user.findMany({
|
|
61
|
+
include: { orders: { where: { status: 'active' } } },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Good — manual batch with IN clause
|
|
65
|
+
const users = await db.user.findMany();
|
|
66
|
+
const userIds = users.map((u) => u.id);
|
|
67
|
+
const orders = await db.order.findMany({
|
|
68
|
+
where: { userId: { in: userIds } },
|
|
69
|
+
});
|
|
70
|
+
const ordersByUser = groupBy(orders, 'userId');
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For GraphQL, use DataLoader to batch and deduplicate within a request:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const userLoader = new DataLoader(async (ids: string[]) => {
|
|
77
|
+
const users = await db.user.findMany({ where: { id: { in: ids } } });
|
|
78
|
+
const map = new Map(users.map((u) => [u.id, u]));
|
|
79
|
+
return ids.map((id) => map.get(id) ?? null);
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3. Connection Pooling
|
|
84
|
+
|
|
85
|
+
Every database connection consumes memory. Pool and limit them.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Prisma — configure in connection string
|
|
89
|
+
// postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=30
|
|
90
|
+
|
|
91
|
+
// Node pg pool
|
|
92
|
+
const pool = new Pool({
|
|
93
|
+
max: 20, // max connections
|
|
94
|
+
idleTimeoutMillis: 30000,
|
|
95
|
+
connectionTimeoutMillis: 5000,
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Production sizing rule: start with `max = (2 * CPU cores) + disk spindles`.
|
|
100
|
+
For a 2-core VPS with SSD, 5-10 connections is often optimal.
|
|
101
|
+
|
|
102
|
+
### 4. Migration Best Practices
|
|
103
|
+
|
|
104
|
+
```sql
|
|
105
|
+
-- Always use IF NOT EXISTS / IF EXISTS for safety
|
|
106
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
107
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
108
|
+
action TEXT NOT NULL,
|
|
109
|
+
user_id UUID REFERENCES users(id),
|
|
110
|
+
payload JSONB,
|
|
111
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
-- Add columns as nullable first, backfill, then add NOT NULL
|
|
115
|
+
ALTER TABLE users ADD COLUMN phone TEXT;
|
|
116
|
+
-- Backfill: UPDATE users SET phone = '' WHERE phone IS NULL;
|
|
117
|
+
-- Then: ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Rules:
|
|
121
|
+
- Never rename or drop a column in a single migration — it breaks running code
|
|
122
|
+
- Instead: add new column, deploy code that reads both, migrate data, deploy
|
|
123
|
+
code that reads only new, drop old column
|
|
124
|
+
- Keep migrations small and reversible
|
|
125
|
+
- Test migrations against a production-size dataset before deploying
|
|
126
|
+
|
|
127
|
+
### 5. Read Replicas
|
|
128
|
+
|
|
129
|
+
Route read-heavy queries to replicas, writes to the primary.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
const primary = new Pool({ connectionString: process.env.DATABASE_PRIMARY });
|
|
133
|
+
const replica = new Pool({ connectionString: process.env.DATABASE_REPLICA });
|
|
134
|
+
|
|
135
|
+
function getPool(operation: 'read' | 'write'): Pool {
|
|
136
|
+
return operation === 'write' ? primary : replica;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Be aware of replication lag — after a write, read from primary briefly
|
|
140
|
+
async function createAndReturn(data: CreateInput) {
|
|
141
|
+
const created = await primary.query('INSERT INTO ... RETURNING *', [data]);
|
|
142
|
+
return created.rows[0]; // read from primary, not replica
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 6. Row-Level Security (PostgreSQL)
|
|
147
|
+
|
|
148
|
+
Let the database enforce access control, not just the application.
|
|
149
|
+
|
|
150
|
+
```sql
|
|
151
|
+
-- Enable RLS on the table
|
|
152
|
+
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
|
|
153
|
+
|
|
154
|
+
-- Users can only see their own documents
|
|
155
|
+
CREATE POLICY user_documents ON documents
|
|
156
|
+
USING (owner_id = current_setting('app.current_user_id')::uuid);
|
|
157
|
+
|
|
158
|
+
-- Admins can see everything
|
|
159
|
+
CREATE POLICY admin_all ON documents
|
|
160
|
+
USING (current_setting('app.current_role') = 'admin');
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Set the session variable before queries:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
await pool.query(`SET app.current_user_id = $1`, [userId]);
|
|
167
|
+
await pool.query(`SET app.current_role = $1`, [userRole]);
|
|
168
|
+
const docs = await pool.query('SELECT * FROM documents'); // RLS filters automatically
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 7. Soft Deletes and Archival
|
|
172
|
+
|
|
173
|
+
```sql
|
|
174
|
+
ALTER TABLE orders ADD COLUMN deleted_at TIMESTAMPTZ;
|
|
175
|
+
|
|
176
|
+
-- Partial index for non-deleted rows (most queries)
|
|
177
|
+
CREATE INDEX idx_orders_active ON orders(user_id, created_at)
|
|
178
|
+
WHERE deleted_at IS NULL;
|
|
179
|
+
|
|
180
|
+
-- View for convenience
|
|
181
|
+
CREATE VIEW active_orders AS
|
|
182
|
+
SELECT * FROM orders WHERE deleted_at IS NULL;
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 8. JSONB for Flexible Data
|
|
186
|
+
|
|
187
|
+
```sql
|
|
188
|
+
-- Store semi-structured metadata
|
|
189
|
+
ALTER TABLE events ADD COLUMN metadata JSONB DEFAULT '{}';
|
|
190
|
+
|
|
191
|
+
-- Index for fast key lookups
|
|
192
|
+
CREATE INDEX idx_events_metadata ON events USING GIN (metadata);
|
|
193
|
+
|
|
194
|
+
-- Query nested fields
|
|
195
|
+
SELECT * FROM events WHERE metadata->>'source' = 'api';
|
|
196
|
+
SELECT * FROM events WHERE metadata @> '{"tags": ["urgent"]}';
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Examples
|
|
200
|
+
|
|
201
|
+
| Problem | Solution |
|
|
202
|
+
|---------|----------|
|
|
203
|
+
| Slow list queries | Composite index on filter + sort columns |
|
|
204
|
+
| 50 queries per page load | Eager loading or DataLoader batching |
|
|
205
|
+
| 100 idle connections | Connection pooling with max 10-20 |
|
|
206
|
+
| Column rename breaks prod | Two-phase migration (add, migrate, drop) |
|
|
207
|
+
| Multi-tenant data leaks | Row-level security policies |
|
|
208
|
+
|
|
209
|
+
## Checklist
|
|
210
|
+
|
|
211
|
+
- [ ] Every foreign key column has an index
|
|
212
|
+
- [ ] Composite indexes match the most common query patterns
|
|
213
|
+
- [ ] `EXPLAIN ANALYZE` is run for new queries touching large tables
|
|
214
|
+
- [ ] No N+1 queries — verified with query logging in development
|
|
215
|
+
- [ ] Connection pool max is tuned to server capacity (not default 100)
|
|
216
|
+
- [ ] Migrations are backward-compatible — no column drops or renames in one step
|
|
217
|
+
- [ ] Read replicas handle read-heavy workloads with replication lag awareness
|
|
218
|
+
- [ ] Sensitive tables use row-level security or application-level tenant filtering
|
|
219
|
+
- [ ] JSONB columns have GIN indexes when queried frequently
|