@kysera/migrations 0.3.0 → 0.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/README.md +1409 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,1409 @@
|
|
|
1
|
+
# @kysera/migrations
|
|
2
|
+
|
|
3
|
+
> Lightweight, type-safe database migration management for Kysera ORM with dry-run support and flexible rollback capabilities.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@kysera/migrations)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
|
|
9
|
+
## 📦 Package Information
|
|
10
|
+
|
|
11
|
+
| Property | Value |
|
|
12
|
+
|----------|-------|
|
|
13
|
+
| **Package** | `@kysera/migrations` |
|
|
14
|
+
| **Version** | `0.3.0` |
|
|
15
|
+
| **Bundle Size** | 3.7 KB (minified) |
|
|
16
|
+
| **Dependencies** | None (peer: kysely) |
|
|
17
|
+
| **Test Coverage** | 24 tests, comprehensive |
|
|
18
|
+
| **Supported Databases** | PostgreSQL, MySQL, SQLite |
|
|
19
|
+
| **Type Safety** | ✅ Full TypeScript support |
|
|
20
|
+
|
|
21
|
+
## 🎯 Features
|
|
22
|
+
|
|
23
|
+
### Core Migration Management
|
|
24
|
+
- ✅ **Simple API** - Intuitive migration creation and execution
|
|
25
|
+
- ✅ **Type-safe** - Full TypeScript support with Kysely integration
|
|
26
|
+
- ✅ **State tracking** - Automatic migration history in database
|
|
27
|
+
- ✅ **Sequential execution** - Migrations run in order
|
|
28
|
+
- ✅ **Dry run mode** - Preview changes before execution
|
|
29
|
+
|
|
30
|
+
### Advanced Features
|
|
31
|
+
- ✅ **Rollback support** - Roll back one or multiple migrations
|
|
32
|
+
- ✅ **Partial migration** - Run up to specific migration
|
|
33
|
+
- ✅ **Status reporting** - View executed and pending migrations
|
|
34
|
+
- ✅ **Error handling** - Graceful failure with detailed logging
|
|
35
|
+
- ✅ **Idempotent setup** - Safe to run multiple times
|
|
36
|
+
- ✅ **Zero dependencies** - Only Kysely as peer dependency
|
|
37
|
+
|
|
38
|
+
## 📥 Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# pnpm (recommended)
|
|
42
|
+
pnpm add @kysera/migrations kysely
|
|
43
|
+
|
|
44
|
+
# npm
|
|
45
|
+
npm install @kysera/migrations kysely
|
|
46
|
+
|
|
47
|
+
# yarn
|
|
48
|
+
yarn add @kysera/migrations kysely
|
|
49
|
+
|
|
50
|
+
# bun
|
|
51
|
+
bun add @kysera/migrations kysely
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 🚀 Quick Start
|
|
55
|
+
|
|
56
|
+
### Basic Usage
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { Kysely } from 'kysely'
|
|
60
|
+
import { createMigrationRunner, createMigration } from '@kysera/migrations'
|
|
61
|
+
|
|
62
|
+
// Define your migrations
|
|
63
|
+
const migrations = [
|
|
64
|
+
createMigration(
|
|
65
|
+
'001_create_users',
|
|
66
|
+
async (db) => {
|
|
67
|
+
await db.schema
|
|
68
|
+
.createTable('users')
|
|
69
|
+
.addColumn('id', 'serial', col => col.primaryKey())
|
|
70
|
+
.addColumn('email', 'varchar(255)', col => col.notNull().unique())
|
|
71
|
+
.addColumn('name', 'varchar(255)', col => col.notNull())
|
|
72
|
+
.execute()
|
|
73
|
+
},
|
|
74
|
+
async (db) => {
|
|
75
|
+
await db.schema.dropTable('users').execute()
|
|
76
|
+
}
|
|
77
|
+
),
|
|
78
|
+
|
|
79
|
+
createMigration(
|
|
80
|
+
'002_create_posts',
|
|
81
|
+
async (db) => {
|
|
82
|
+
await db.schema
|
|
83
|
+
.createTable('posts')
|
|
84
|
+
.addColumn('id', 'serial', col => col.primaryKey())
|
|
85
|
+
.addColumn('user_id', 'integer', col =>
|
|
86
|
+
col.notNull().references('users.id').onDelete('cascade')
|
|
87
|
+
)
|
|
88
|
+
.addColumn('title', 'varchar(255)', col => col.notNull())
|
|
89
|
+
.addColumn('content', 'text')
|
|
90
|
+
.execute()
|
|
91
|
+
},
|
|
92
|
+
async (db) => {
|
|
93
|
+
await db.schema.dropTable('posts').execute()
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
// Create migration runner
|
|
99
|
+
const db = new Kysely<Database>({ /* ... */ })
|
|
100
|
+
const runner = createMigrationRunner(db, migrations)
|
|
101
|
+
|
|
102
|
+
// Run all pending migrations
|
|
103
|
+
await runner.up()
|
|
104
|
+
// ✅ All migrations completed successfully
|
|
105
|
+
|
|
106
|
+
// Check status
|
|
107
|
+
await runner.status()
|
|
108
|
+
// 📊 Migration Status:
|
|
109
|
+
// ✅ Executed: 2
|
|
110
|
+
// ⏳ Pending: 0
|
|
111
|
+
|
|
112
|
+
// Rollback last migration
|
|
113
|
+
await runner.down(1)
|
|
114
|
+
// ✅ Rollback completed successfully
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Dry Run Mode
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// Preview what would happen without making changes
|
|
121
|
+
const runner = createMigrationRunner(db, migrations, {
|
|
122
|
+
dryRun: true,
|
|
123
|
+
logger: console.log
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
await runner.up()
|
|
127
|
+
// 🔍 DRY RUN - No changes will be made
|
|
128
|
+
// ↑ Running 001_create_users...
|
|
129
|
+
// ✓ 001_create_users completed
|
|
130
|
+
// ↑ Running 002_create_posts...
|
|
131
|
+
// ✓ 002_create_posts completed
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 📖 Table of Contents
|
|
135
|
+
|
|
136
|
+
- [Installation](#-installation)
|
|
137
|
+
- [Quick Start](#-quick-start)
|
|
138
|
+
- [Core Concepts](#-core-concepts)
|
|
139
|
+
- [Migrations](#migrations)
|
|
140
|
+
- [Migration Runner](#migration-runner)
|
|
141
|
+
- [State Tracking](#state-tracking)
|
|
142
|
+
- [API Reference](#-api-reference)
|
|
143
|
+
- [createMigration()](#createmigration)
|
|
144
|
+
- [createMigrationRunner()](#createmigrationrunner)
|
|
145
|
+
- [MigrationRunner Methods](#migrationrunner-methods)
|
|
146
|
+
- [setupMigrations()](#setupmigrations)
|
|
147
|
+
- [Migration Lifecycle](#-migration-lifecycle)
|
|
148
|
+
- [Advanced Usage](#-advanced-usage)
|
|
149
|
+
- [Partial Migrations](#partial-migrations)
|
|
150
|
+
- [Custom Logger](#custom-logger)
|
|
151
|
+
- [Migration Metadata](#migration-metadata)
|
|
152
|
+
- [Complex Schema Changes](#complex-schema-changes)
|
|
153
|
+
- [Best Practices](#-best-practices)
|
|
154
|
+
- [Multi-Database Support](#-multi-database-support)
|
|
155
|
+
- [Troubleshooting](#-troubleshooting)
|
|
156
|
+
- [Examples](#-examples)
|
|
157
|
+
|
|
158
|
+
## 💡 Core Concepts
|
|
159
|
+
|
|
160
|
+
### Migrations
|
|
161
|
+
|
|
162
|
+
A migration represents a set of database schema changes that can be applied and (optionally) reverted:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
interface Migration {
|
|
166
|
+
/** Unique migration name (e.g., '001_create_users') */
|
|
167
|
+
name: string
|
|
168
|
+
|
|
169
|
+
/** Migration up function - creates/modifies schema */
|
|
170
|
+
up: (db: Kysely<any>) => Promise<void>
|
|
171
|
+
|
|
172
|
+
/** Optional migration down function - reverts changes */
|
|
173
|
+
down?: (db: Kysely<any>) => Promise<void>
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Key points:**
|
|
178
|
+
- **name**: Must be unique, typically versioned (001_, 002_, etc.)
|
|
179
|
+
- **up**: Required, contains schema changes
|
|
180
|
+
- **down**: Optional, should revert the up changes
|
|
181
|
+
- Migrations run sequentially in the order they appear in the array
|
|
182
|
+
|
|
183
|
+
### Migration Runner
|
|
184
|
+
|
|
185
|
+
The `MigrationRunner` class manages migration execution and state:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
class MigrationRunner {
|
|
189
|
+
// Run all pending migrations
|
|
190
|
+
async up(): Promise<void>
|
|
191
|
+
|
|
192
|
+
// Rollback last N migrations
|
|
193
|
+
async down(steps?: number): Promise<void>
|
|
194
|
+
|
|
195
|
+
// Show migration status
|
|
196
|
+
async status(): Promise<MigrationStatus>
|
|
197
|
+
|
|
198
|
+
// Reset all migrations (rollback everything)
|
|
199
|
+
async reset(): Promise<void>
|
|
200
|
+
|
|
201
|
+
// Migrate up to specific migration
|
|
202
|
+
async upTo(targetName: string): Promise<void>
|
|
203
|
+
|
|
204
|
+
// Get list of executed migrations
|
|
205
|
+
async getExecutedMigrations(): Promise<string[]>
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### State Tracking
|
|
210
|
+
|
|
211
|
+
Migrations are tracked in a `migrations` table:
|
|
212
|
+
|
|
213
|
+
```sql
|
|
214
|
+
CREATE TABLE migrations (
|
|
215
|
+
name VARCHAR(255) PRIMARY KEY,
|
|
216
|
+
executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
217
|
+
);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
This table is automatically created when you run migrations and stores which migrations have been executed.
|
|
221
|
+
|
|
222
|
+
## 📚 API Reference
|
|
223
|
+
|
|
224
|
+
### createMigration()
|
|
225
|
+
|
|
226
|
+
Create a migration object with up and optional down functions.
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
function createMigration(
|
|
230
|
+
name: string,
|
|
231
|
+
up: (db: Kysely<any>) => Promise<void>,
|
|
232
|
+
down?: (db: Kysely<any>) => Promise<void>
|
|
233
|
+
): Migration
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Parameters:**
|
|
237
|
+
- `name` - Unique migration identifier (e.g., '001_create_users')
|
|
238
|
+
- `up` - Function that applies the migration
|
|
239
|
+
- `down` - Optional function that reverts the migration
|
|
240
|
+
|
|
241
|
+
**Returns:** Migration object
|
|
242
|
+
|
|
243
|
+
**Example:**
|
|
244
|
+
```typescript
|
|
245
|
+
const migration = createMigration(
|
|
246
|
+
'001_create_users',
|
|
247
|
+
async (db) => {
|
|
248
|
+
await db.schema
|
|
249
|
+
.createTable('users')
|
|
250
|
+
.addColumn('id', 'serial', col => col.primaryKey())
|
|
251
|
+
.addColumn('email', 'varchar(255)', col => col.notNull().unique())
|
|
252
|
+
.execute()
|
|
253
|
+
},
|
|
254
|
+
async (db) => {
|
|
255
|
+
await db.schema.dropTable('users').execute()
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### createMigrationRunner()
|
|
261
|
+
|
|
262
|
+
Create a MigrationRunner instance.
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
function createMigrationRunner(
|
|
266
|
+
db: Kysely<any>,
|
|
267
|
+
migrations: Migration[],
|
|
268
|
+
options?: MigrationRunnerOptions
|
|
269
|
+
): MigrationRunner
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Parameters:**
|
|
273
|
+
- `db` - Kysely database instance
|
|
274
|
+
- `migrations` - Array of migration objects
|
|
275
|
+
- `options` - Optional configuration
|
|
276
|
+
|
|
277
|
+
**Options:**
|
|
278
|
+
```typescript
|
|
279
|
+
interface MigrationRunnerOptions {
|
|
280
|
+
/** Enable dry run mode (preview only, no changes) */
|
|
281
|
+
dryRun?: boolean
|
|
282
|
+
|
|
283
|
+
/** Custom logger function (default: console.log) */
|
|
284
|
+
logger?: (message: string) => void
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Returns:** MigrationRunner instance
|
|
289
|
+
|
|
290
|
+
**Example:**
|
|
291
|
+
```typescript
|
|
292
|
+
const runner = createMigrationRunner(db, migrations, {
|
|
293
|
+
dryRun: false,
|
|
294
|
+
logger: (msg) => console.log(`[MIGRATION] ${msg}`)
|
|
295
|
+
})
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### MigrationRunner Methods
|
|
299
|
+
|
|
300
|
+
#### up()
|
|
301
|
+
|
|
302
|
+
Run all pending migrations sequentially.
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
async up(): Promise<void>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Behavior:**
|
|
309
|
+
- Creates migrations table if it doesn't exist
|
|
310
|
+
- Gets list of already-executed migrations
|
|
311
|
+
- Runs only migrations that haven't been executed
|
|
312
|
+
- Marks each migration as executed after successful completion
|
|
313
|
+
- Stops on first error (does not mark failed migration as executed)
|
|
314
|
+
|
|
315
|
+
**Example:**
|
|
316
|
+
```typescript
|
|
317
|
+
await runner.up()
|
|
318
|
+
// Output:
|
|
319
|
+
// ✓ 001_create_users (already executed)
|
|
320
|
+
// ↑ Running 002_create_posts...
|
|
321
|
+
// ✓ 002_create_posts completed
|
|
322
|
+
// ✅ All migrations completed successfully
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
#### down()
|
|
326
|
+
|
|
327
|
+
Rollback the last N migrations.
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
async down(steps?: number): Promise<void>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Parameters:**
|
|
334
|
+
- `steps` - Number of migrations to rollback (default: 1)
|
|
335
|
+
|
|
336
|
+
**Behavior:**
|
|
337
|
+
- Gets list of executed migrations
|
|
338
|
+
- Selects last N migrations to rollback (in reverse order)
|
|
339
|
+
- Calls down() method for each migration
|
|
340
|
+
- Removes migration from executed list after successful rollback
|
|
341
|
+
- Skips migrations without down() method
|
|
342
|
+
- Stops on first error
|
|
343
|
+
|
|
344
|
+
**Example:**
|
|
345
|
+
```typescript
|
|
346
|
+
// Rollback last migration
|
|
347
|
+
await runner.down(1)
|
|
348
|
+
|
|
349
|
+
// Rollback last 3 migrations
|
|
350
|
+
await runner.down(3)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### status()
|
|
354
|
+
|
|
355
|
+
Display current migration status.
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
async status(): Promise<MigrationStatus>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Returns:**
|
|
362
|
+
```typescript
|
|
363
|
+
interface MigrationStatus {
|
|
364
|
+
/** List of executed migration names */
|
|
365
|
+
executed: string[]
|
|
366
|
+
|
|
367
|
+
/** List of pending migration names */
|
|
368
|
+
pending: string[]
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Example:**
|
|
373
|
+
```typescript
|
|
374
|
+
const status = await runner.status()
|
|
375
|
+
console.log(`Executed: ${status.executed.length}`)
|
|
376
|
+
console.log(`Pending: ${status.pending.length}`)
|
|
377
|
+
|
|
378
|
+
// Output:
|
|
379
|
+
// 📊 Migration Status:
|
|
380
|
+
// ✅ Executed: 2
|
|
381
|
+
// ⏳ Pending: 1
|
|
382
|
+
//
|
|
383
|
+
// Executed migrations:
|
|
384
|
+
// ✓ 001_create_users
|
|
385
|
+
// ✓ 002_create_posts
|
|
386
|
+
//
|
|
387
|
+
// Pending migrations:
|
|
388
|
+
// - 003_add_indexes
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
#### reset()
|
|
392
|
+
|
|
393
|
+
Rollback all migrations (dangerous!).
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
async reset(): Promise<void>
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**Behavior:**
|
|
400
|
+
- Gets count of executed migrations
|
|
401
|
+
- Calls down() to rollback all migrations
|
|
402
|
+
- Only rolls back migrations that have down() methods
|
|
403
|
+
|
|
404
|
+
**Example:**
|
|
405
|
+
```typescript
|
|
406
|
+
await runner.reset()
|
|
407
|
+
// ⚠️ Resetting 3 migrations...
|
|
408
|
+
// ↓ Rolling back 003_add_indexes...
|
|
409
|
+
// ✓ 003_add_indexes rolled back
|
|
410
|
+
// ↓ Rolling back 002_create_posts...
|
|
411
|
+
// ✓ 002_create_posts rolled back
|
|
412
|
+
// ↓ Rolling back 001_create_users...
|
|
413
|
+
// ✓ 001_create_users rolled back
|
|
414
|
+
// ✅ All migrations reset
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### upTo()
|
|
418
|
+
|
|
419
|
+
Run migrations up to (and including) a specific migration.
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
async upTo(targetName: string): Promise<void>
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Parameters:**
|
|
426
|
+
- `targetName` - Name of the target migration
|
|
427
|
+
|
|
428
|
+
**Behavior:**
|
|
429
|
+
- Finds the target migration in the list
|
|
430
|
+
- Runs all migrations up to and including the target
|
|
431
|
+
- Skips already-executed migrations
|
|
432
|
+
- Throws error if target migration not found
|
|
433
|
+
|
|
434
|
+
**Example:**
|
|
435
|
+
```typescript
|
|
436
|
+
await runner.upTo('002_create_posts')
|
|
437
|
+
// ↑ Running 001_create_users...
|
|
438
|
+
// ✓ 001_create_users completed
|
|
439
|
+
// ↑ Running 002_create_posts...
|
|
440
|
+
// ✓ 002_create_posts completed
|
|
441
|
+
// ✅ Migrated up to 002_create_posts
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### setupMigrations()
|
|
445
|
+
|
|
446
|
+
Manually create the migrations tracking table.
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
async function setupMigrations(db: Kysely<any>): Promise<void>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Note:** This is called automatically by the runner, but you can call it manually if needed.
|
|
453
|
+
|
|
454
|
+
**Example:**
|
|
455
|
+
```typescript
|
|
456
|
+
import { setupMigrations } from '@kysera/migrations'
|
|
457
|
+
|
|
458
|
+
await setupMigrations(db)
|
|
459
|
+
// Creates migrations table if it doesn't exist
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
## 🔄 Migration Lifecycle
|
|
463
|
+
|
|
464
|
+
### Complete Migration Flow
|
|
465
|
+
|
|
466
|
+
```
|
|
467
|
+
┌─────────────────────────────────────────────────────────┐
|
|
468
|
+
│ 1. Create Migrations │
|
|
469
|
+
│ Define up/down functions │
|
|
470
|
+
└─────────────────────────────────────────────────────────┘
|
|
471
|
+
↓
|
|
472
|
+
┌─────────────────────────────────────────────────────────┐
|
|
473
|
+
│ 2. Initialize Runner │
|
|
474
|
+
│ createMigrationRunner(db, migrations, options) │
|
|
475
|
+
└─────────────────────────────────────────────────────────┘
|
|
476
|
+
↓
|
|
477
|
+
┌─────────────────────────────────────────────────────────┐
|
|
478
|
+
│ 3. Check Status (Optional) │
|
|
479
|
+
│ await runner.status() │
|
|
480
|
+
│ → Shows executed and pending migrations │
|
|
481
|
+
└─────────────────────────────────────────────────────────┘
|
|
482
|
+
↓
|
|
483
|
+
┌─────────────────────────────────────────────────────────┐
|
|
484
|
+
│ 4. Run Migrations │
|
|
485
|
+
│ await runner.up() │
|
|
486
|
+
│ → Executes pending migrations sequentially │
|
|
487
|
+
│ → Marks each as executed after success │
|
|
488
|
+
└─────────────────────────────────────────────────────────┘
|
|
489
|
+
↓
|
|
490
|
+
┌─────────────────────────────────────────────────────────┐
|
|
491
|
+
│ 5. Rollback (If Needed) │
|
|
492
|
+
│ await runner.down(N) │
|
|
493
|
+
│ → Reverts last N migrations │
|
|
494
|
+
│ → Removes from executed list │
|
|
495
|
+
└─────────────────────────────────────────────────────────┘
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Migration Execution Order
|
|
499
|
+
|
|
500
|
+
Migrations are executed in **array order**:
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
const migrations = [
|
|
504
|
+
createMigration('001_first', ...), // Runs first
|
|
505
|
+
createMigration('002_second', ...), // Runs second
|
|
506
|
+
createMigration('003_third', ...) // Runs third
|
|
507
|
+
]
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
**Important:** Name your migrations with numeric prefixes to ensure proper ordering:
|
|
511
|
+
- ✅ `001_create_users`, `002_create_posts`, `003_add_indexes`
|
|
512
|
+
- ❌ `create_users`, `create_posts` (no guaranteed order)
|
|
513
|
+
|
|
514
|
+
### Error Handling
|
|
515
|
+
|
|
516
|
+
When a migration fails:
|
|
517
|
+
1. Execution stops immediately
|
|
518
|
+
2. Failed migration is **not** marked as executed
|
|
519
|
+
3. Previous successful migrations remain executed
|
|
520
|
+
4. Error is thrown for handling
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
try {
|
|
524
|
+
await runner.up()
|
|
525
|
+
} catch (error) {
|
|
526
|
+
console.error('Migration failed:', error)
|
|
527
|
+
// Handle error: notify admins, rollback, etc.
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
## 🔧 Advanced Usage
|
|
532
|
+
|
|
533
|
+
### Partial Migrations
|
|
534
|
+
|
|
535
|
+
Run migrations incrementally:
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
const runner = createMigrationRunner(db, migrations)
|
|
539
|
+
|
|
540
|
+
// Run only the first two migrations
|
|
541
|
+
await runner.upTo('002_create_posts')
|
|
542
|
+
|
|
543
|
+
// Later, run the rest
|
|
544
|
+
await runner.up()
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Custom Logger
|
|
548
|
+
|
|
549
|
+
Integrate with your logging system:
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
import { createMigrationRunner } from '@kysera/migrations'
|
|
553
|
+
import { logger } from './logger'
|
|
554
|
+
|
|
555
|
+
const runner = createMigrationRunner(db, migrations, {
|
|
556
|
+
logger: (message) => {
|
|
557
|
+
logger.info('[MIGRATIONS]', message)
|
|
558
|
+
}
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
await runner.up()
|
|
562
|
+
// Logs will be sent to your logging system
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Disable Logging
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
const runner = createMigrationRunner(db, migrations, {
|
|
569
|
+
logger: () => {} // No-op logger
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
await runner.up() // Silent execution
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Migration Metadata
|
|
576
|
+
|
|
577
|
+
Add metadata to migrations for documentation:
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
interface MigrationWithMeta extends Migration {
|
|
581
|
+
/** Human-readable description */
|
|
582
|
+
description?: string
|
|
583
|
+
|
|
584
|
+
/** Whether this is a breaking change */
|
|
585
|
+
breaking?: boolean
|
|
586
|
+
|
|
587
|
+
/** Estimated duration in milliseconds */
|
|
588
|
+
estimatedDuration?: number
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const migration: MigrationWithMeta = {
|
|
592
|
+
name: '001_create_users',
|
|
593
|
+
description: 'Create users table with email and name',
|
|
594
|
+
breaking: false,
|
|
595
|
+
estimatedDuration: 500,
|
|
596
|
+
up: async (db) => { /* ... */ },
|
|
597
|
+
down: async (db) => { /* ... */ }
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Complex Schema Changes
|
|
602
|
+
|
|
603
|
+
Handle complex migrations with transactions:
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
createMigration(
|
|
607
|
+
'004_complex_refactor',
|
|
608
|
+
async (db) => {
|
|
609
|
+
// Use transaction for complex operations
|
|
610
|
+
await db.transaction().execute(async (trx) => {
|
|
611
|
+
// 1. Create new table
|
|
612
|
+
await trx.schema
|
|
613
|
+
.createTable('new_users')
|
|
614
|
+
.addColumn('id', 'serial', col => col.primaryKey())
|
|
615
|
+
.addColumn('email', 'varchar(255)', col => col.notNull())
|
|
616
|
+
.addColumn('full_name', 'varchar(255)', col => col.notNull())
|
|
617
|
+
.execute()
|
|
618
|
+
|
|
619
|
+
// 2. Copy data
|
|
620
|
+
await trx
|
|
621
|
+
.insertInto('new_users')
|
|
622
|
+
.columns(['email', 'full_name'])
|
|
623
|
+
.from(
|
|
624
|
+
trx.selectFrom('users')
|
|
625
|
+
.select(['email', 'name as full_name'])
|
|
626
|
+
)
|
|
627
|
+
.execute()
|
|
628
|
+
|
|
629
|
+
// 3. Drop old table
|
|
630
|
+
await trx.schema.dropTable('users').execute()
|
|
631
|
+
|
|
632
|
+
// 4. Rename new table
|
|
633
|
+
await trx.schema.alterTable('new_users').renameTo('users').execute()
|
|
634
|
+
})
|
|
635
|
+
},
|
|
636
|
+
async (db) => {
|
|
637
|
+
// Revert complex changes
|
|
638
|
+
await db.transaction().execute(async (trx) => {
|
|
639
|
+
await trx.schema
|
|
640
|
+
.alterTable('users')
|
|
641
|
+
.renameColumn('full_name', 'name')
|
|
642
|
+
.execute()
|
|
643
|
+
})
|
|
644
|
+
}
|
|
645
|
+
)
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Data Migrations
|
|
649
|
+
|
|
650
|
+
Migrate data along with schema:
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
createMigration(
|
|
654
|
+
'005_migrate_user_roles',
|
|
655
|
+
async (db) => {
|
|
656
|
+
// 1. Add new column
|
|
657
|
+
await db.schema
|
|
658
|
+
.alterTable('users')
|
|
659
|
+
.addColumn('role', 'varchar(50)', col => col.defaultTo('user'))
|
|
660
|
+
.execute()
|
|
661
|
+
|
|
662
|
+
// 2. Migrate data
|
|
663
|
+
await db
|
|
664
|
+
.updateTable('users')
|
|
665
|
+
.set({ role: 'admin' })
|
|
666
|
+
.where('email', 'like', '%@admin.com')
|
|
667
|
+
.execute()
|
|
668
|
+
|
|
669
|
+
// 3. Make column required
|
|
670
|
+
await db.schema
|
|
671
|
+
.alterTable('users')
|
|
672
|
+
.alterColumn('role', col => col.setNotNull())
|
|
673
|
+
.execute()
|
|
674
|
+
},
|
|
675
|
+
async (db) => {
|
|
676
|
+
await db.schema
|
|
677
|
+
.alterTable('users')
|
|
678
|
+
.dropColumn('role')
|
|
679
|
+
.execute()
|
|
680
|
+
}
|
|
681
|
+
)
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
## ✅ Best Practices
|
|
685
|
+
|
|
686
|
+
### 1. Always Use Numeric Prefixes
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
// ✅ Good: Clear ordering
|
|
690
|
+
const migrations = [
|
|
691
|
+
createMigration('001_create_users', ...),
|
|
692
|
+
createMigration('002_create_posts', ...),
|
|
693
|
+
createMigration('003_add_indexes', ...)
|
|
694
|
+
]
|
|
695
|
+
|
|
696
|
+
// ❌ Bad: No guaranteed order
|
|
697
|
+
const migrations = [
|
|
698
|
+
createMigration('create_users', ...),
|
|
699
|
+
createMigration('create_posts', ...),
|
|
700
|
+
createMigration('add_indexes', ...)
|
|
701
|
+
]
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### 2. Keep Migrations Small and Focused
|
|
705
|
+
|
|
706
|
+
```typescript
|
|
707
|
+
// ✅ Good: One table per migration
|
|
708
|
+
createMigration('001_create_users', async (db) => {
|
|
709
|
+
await db.schema.createTable('users')...execute()
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
createMigration('002_create_posts', async (db) => {
|
|
713
|
+
await db.schema.createTable('posts')...execute()
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
// ❌ Bad: Multiple tables in one migration
|
|
717
|
+
createMigration('001_initial_schema', async (db) => {
|
|
718
|
+
await db.schema.createTable('users')...execute()
|
|
719
|
+
await db.schema.createTable('posts')...execute()
|
|
720
|
+
await db.schema.createTable('comments')...execute()
|
|
721
|
+
})
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
### 3. Always Provide down() Methods
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
// ✅ Good: Reversible migration
|
|
728
|
+
createMigration(
|
|
729
|
+
'001_create_users',
|
|
730
|
+
async (db) => {
|
|
731
|
+
await db.schema.createTable('users')...execute()
|
|
732
|
+
},
|
|
733
|
+
async (db) => {
|
|
734
|
+
await db.schema.dropTable('users').execute()
|
|
735
|
+
}
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
// ⚠️ Acceptable but not ideal: No rollback
|
|
739
|
+
createMigration(
|
|
740
|
+
'002_add_column',
|
|
741
|
+
async (db) => {
|
|
742
|
+
await db.schema
|
|
743
|
+
.alterTable('users')
|
|
744
|
+
.addColumn('status', 'varchar(50)')
|
|
745
|
+
.execute()
|
|
746
|
+
}
|
|
747
|
+
// No down() method - can't be rolled back
|
|
748
|
+
)
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### 4. Test Migrations Before Production
|
|
752
|
+
|
|
753
|
+
```typescript
|
|
754
|
+
// Test in development
|
|
755
|
+
const runner = createMigrationRunner(devDb, migrations, {
|
|
756
|
+
logger: console.log
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
// 1. Test up
|
|
760
|
+
await runner.up()
|
|
761
|
+
// Verify schema is correct
|
|
762
|
+
|
|
763
|
+
// 2. Test down
|
|
764
|
+
await runner.down(1)
|
|
765
|
+
// Verify rollback works
|
|
766
|
+
|
|
767
|
+
// 3. Test up again
|
|
768
|
+
await runner.up()
|
|
769
|
+
// Ensure idempotency
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### 5. Use Dry Run for Production
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
// Preview changes in production
|
|
776
|
+
const dryRunner = createMigrationRunner(db, migrations, {
|
|
777
|
+
dryRun: true,
|
|
778
|
+
logger: console.log
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
await dryRunner.up()
|
|
782
|
+
// Review output, ensure it's safe
|
|
783
|
+
|
|
784
|
+
// Then run for real
|
|
785
|
+
const runner = createMigrationRunner(db, migrations)
|
|
786
|
+
await runner.up()
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
### 6. Organize Migrations in Separate Files
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
// migrations/001_create_users.ts
|
|
793
|
+
export const migration_001 = createMigration(
|
|
794
|
+
'001_create_users',
|
|
795
|
+
async (db) => { /* ... */ },
|
|
796
|
+
async (db) => { /* ... */ }
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
// migrations/002_create_posts.ts
|
|
800
|
+
export const migration_002 = createMigration(
|
|
801
|
+
'002_create_posts',
|
|
802
|
+
async (db) => { /* ... */ },
|
|
803
|
+
async (db) => { /* ... */ }
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
// migrations/index.ts
|
|
807
|
+
import { migration_001 } from './001_create_users'
|
|
808
|
+
import { migration_002 } from './002_create_posts'
|
|
809
|
+
|
|
810
|
+
export const migrations = [
|
|
811
|
+
migration_001,
|
|
812
|
+
migration_002
|
|
813
|
+
]
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
### 7. Handle Breaking Changes Carefully
|
|
817
|
+
|
|
818
|
+
```typescript
|
|
819
|
+
// Mark breaking changes
|
|
820
|
+
const migration: MigrationWithMeta = {
|
|
821
|
+
name: '010_remove_deprecated_columns',
|
|
822
|
+
description: 'Remove deprecated user columns',
|
|
823
|
+
breaking: true, // Alert!
|
|
824
|
+
up: async (db) => {
|
|
825
|
+
// Drop columns that apps might still use
|
|
826
|
+
await db.schema
|
|
827
|
+
.alterTable('users')
|
|
828
|
+
.dropColumn('old_column')
|
|
829
|
+
.execute()
|
|
830
|
+
},
|
|
831
|
+
down: async (db) => {
|
|
832
|
+
// Cannot fully revert - data loss!
|
|
833
|
+
await db.schema
|
|
834
|
+
.alterTable('users')
|
|
835
|
+
.addColumn('old_column', 'text')
|
|
836
|
+
.execute()
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
## 🗃️ Multi-Database Support
|
|
842
|
+
|
|
843
|
+
### PostgreSQL
|
|
844
|
+
|
|
845
|
+
```typescript
|
|
846
|
+
import { Kysely, PostgresDialect } from 'kysely'
|
|
847
|
+
import { Pool } from 'pg'
|
|
848
|
+
import { createMigrationRunner, createMigration } from '@kysera/migrations'
|
|
849
|
+
|
|
850
|
+
const db = new Kysely({
|
|
851
|
+
dialect: new PostgresDialect({
|
|
852
|
+
pool: new Pool({
|
|
853
|
+
host: 'localhost',
|
|
854
|
+
database: 'mydb',
|
|
855
|
+
user: 'user',
|
|
856
|
+
password: 'password'
|
|
857
|
+
})
|
|
858
|
+
})
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
const migrations = [
|
|
862
|
+
createMigration(
|
|
863
|
+
'001_create_users',
|
|
864
|
+
async (db) => {
|
|
865
|
+
await db.schema
|
|
866
|
+
.createTable('users')
|
|
867
|
+
.addColumn('id', 'serial', col => col.primaryKey())
|
|
868
|
+
.addColumn('email', 'varchar(255)', col => col.notNull().unique())
|
|
869
|
+
.addColumn('created_at', 'timestamp', col =>
|
|
870
|
+
col.notNull().defaultTo(db.fn('now'))
|
|
871
|
+
)
|
|
872
|
+
.execute()
|
|
873
|
+
|
|
874
|
+
// PostgreSQL-specific: Create index
|
|
875
|
+
await db.schema
|
|
876
|
+
.createIndex('users_email_idx')
|
|
877
|
+
.on('users')
|
|
878
|
+
.column('email')
|
|
879
|
+
.execute()
|
|
880
|
+
},
|
|
881
|
+
async (db) => {
|
|
882
|
+
await db.schema.dropTable('users').cascade().execute()
|
|
883
|
+
}
|
|
884
|
+
)
|
|
885
|
+
]
|
|
886
|
+
|
|
887
|
+
const runner = createMigrationRunner(db, migrations)
|
|
888
|
+
await runner.up()
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
### MySQL
|
|
892
|
+
|
|
893
|
+
```typescript
|
|
894
|
+
import { Kysely, MysqlDialect } from 'kysely'
|
|
895
|
+
import { createPool } from 'mysql2'
|
|
896
|
+
import { createMigrationRunner, createMigration } from '@kysera/migrations'
|
|
897
|
+
|
|
898
|
+
const db = new Kysely({
|
|
899
|
+
dialect: new MysqlDialect({
|
|
900
|
+
pool: createPool({
|
|
901
|
+
host: 'localhost',
|
|
902
|
+
database: 'mydb',
|
|
903
|
+
user: 'user',
|
|
904
|
+
password: 'password'
|
|
905
|
+
})
|
|
906
|
+
})
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
const migrations = [
|
|
910
|
+
createMigration(
|
|
911
|
+
'001_create_users',
|
|
912
|
+
async (db) => {
|
|
913
|
+
await db.schema
|
|
914
|
+
.createTable('users')
|
|
915
|
+
.addColumn('id', 'integer', col =>
|
|
916
|
+
col.primaryKey().autoIncrement()
|
|
917
|
+
)
|
|
918
|
+
.addColumn('email', 'varchar(255)', col => col.notNull().unique())
|
|
919
|
+
.addColumn('created_at', 'datetime', col =>
|
|
920
|
+
col.notNull().defaultTo(db.fn('now'))
|
|
921
|
+
)
|
|
922
|
+
.execute()
|
|
923
|
+
},
|
|
924
|
+
async (db) => {
|
|
925
|
+
await db.schema.dropTable('users').execute()
|
|
926
|
+
}
|
|
927
|
+
)
|
|
928
|
+
]
|
|
929
|
+
|
|
930
|
+
const runner = createMigrationRunner(db, migrations)
|
|
931
|
+
await runner.up()
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
### SQLite
|
|
935
|
+
|
|
936
|
+
```typescript
|
|
937
|
+
import { Kysely, SqliteDialect } from 'kysely'
|
|
938
|
+
import Database from 'better-sqlite3'
|
|
939
|
+
import { createMigrationRunner, createMigration } from '@kysera/migrations'
|
|
940
|
+
|
|
941
|
+
const db = new Kysely({
|
|
942
|
+
dialect: new SqliteDialect({
|
|
943
|
+
database: new Database('mydb.sqlite')
|
|
944
|
+
})
|
|
945
|
+
})
|
|
946
|
+
|
|
947
|
+
const migrations = [
|
|
948
|
+
createMigration(
|
|
949
|
+
'001_create_users',
|
|
950
|
+
async (db) => {
|
|
951
|
+
await db.schema
|
|
952
|
+
.createTable('users')
|
|
953
|
+
.addColumn('id', 'integer', col =>
|
|
954
|
+
col.primaryKey().autoIncrement()
|
|
955
|
+
)
|
|
956
|
+
.addColumn('email', 'text', col => col.notNull().unique())
|
|
957
|
+
.addColumn('created_at', 'text', col =>
|
|
958
|
+
col.notNull().defaultTo(db.fn('datetime', ['now']))
|
|
959
|
+
)
|
|
960
|
+
.execute()
|
|
961
|
+
},
|
|
962
|
+
async (db) => {
|
|
963
|
+
await db.schema.dropTable('users').execute()
|
|
964
|
+
}
|
|
965
|
+
)
|
|
966
|
+
]
|
|
967
|
+
|
|
968
|
+
const runner = createMigrationRunner(db, migrations)
|
|
969
|
+
await runner.up()
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
## 🐛 Troubleshooting
|
|
973
|
+
|
|
974
|
+
### Migration Not Running
|
|
975
|
+
|
|
976
|
+
**Problem**: Migration appears in pending list but doesn't execute
|
|
977
|
+
|
|
978
|
+
**Solutions:**
|
|
979
|
+
```typescript
|
|
980
|
+
// 1. Check migration is in the array
|
|
981
|
+
const migrations = [
|
|
982
|
+
migration_001,
|
|
983
|
+
migration_002,
|
|
984
|
+
migration_003 // Make sure it's included!
|
|
985
|
+
]
|
|
986
|
+
|
|
987
|
+
// 2. Verify migration name is unique
|
|
988
|
+
const migrations = [
|
|
989
|
+
createMigration('001_users', ...),
|
|
990
|
+
createMigration('001_users', ...) // ❌ Duplicate name!
|
|
991
|
+
]
|
|
992
|
+
|
|
993
|
+
// 3. Check for errors in up() function
|
|
994
|
+
createMigration('001_test', async (db) => {
|
|
995
|
+
try {
|
|
996
|
+
await db.schema.createTable('test')...execute()
|
|
997
|
+
} catch (error) {
|
|
998
|
+
console.error('Migration failed:', error)
|
|
999
|
+
throw error
|
|
1000
|
+
}
|
|
1001
|
+
})
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### Rollback Fails
|
|
1005
|
+
|
|
1006
|
+
**Problem**: down() throws error or doesn't work
|
|
1007
|
+
|
|
1008
|
+
**Solutions:**
|
|
1009
|
+
```typescript
|
|
1010
|
+
// 1. Ensure down() method exists
|
|
1011
|
+
const migration = createMigration(
|
|
1012
|
+
'001_test',
|
|
1013
|
+
async (db) => { /* up */ },
|
|
1014
|
+
async (db) => { /* down - required for rollback! */ }
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
// 2. Handle dependent objects (foreign keys, etc.)
|
|
1018
|
+
createMigration(
|
|
1019
|
+
'001_create_posts',
|
|
1020
|
+
async (db) => {
|
|
1021
|
+
await db.schema.createTable('posts')
|
|
1022
|
+
.addColumn('user_id', 'integer', col =>
|
|
1023
|
+
col.references('users.id')
|
|
1024
|
+
)
|
|
1025
|
+
.execute()
|
|
1026
|
+
},
|
|
1027
|
+
async (db) => {
|
|
1028
|
+
// Must drop in correct order
|
|
1029
|
+
await db.schema.dropTable('posts').execute() // ✅ Drop first
|
|
1030
|
+
// DON'T drop users table here if posts references it
|
|
1031
|
+
}
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
// 3. Use CASCADE for PostgreSQL
|
|
1035
|
+
async (db) => {
|
|
1036
|
+
await db.schema.dropTable('users').cascade().execute()
|
|
1037
|
+
}
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
### Migrations Table Not Found
|
|
1041
|
+
|
|
1042
|
+
**Problem**: `Error: Table 'migrations' does not exist`
|
|
1043
|
+
|
|
1044
|
+
**Solution:**
|
|
1045
|
+
```typescript
|
|
1046
|
+
// The migrations table is created automatically,
|
|
1047
|
+
// but you can create it manually if needed:
|
|
1048
|
+
import { setupMigrations } from '@kysera/migrations'
|
|
1049
|
+
|
|
1050
|
+
await setupMigrations(db)
|
|
1051
|
+
await runner.up()
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
### Duplicate Migration Names
|
|
1055
|
+
|
|
1056
|
+
**Problem**: Same migration runs twice or state is inconsistent
|
|
1057
|
+
|
|
1058
|
+
**Solution:**
|
|
1059
|
+
```typescript
|
|
1060
|
+
// Ensure unique names with consistent prefixes
|
|
1061
|
+
const migrations = [
|
|
1062
|
+
createMigration('001_create_users', ...),
|
|
1063
|
+
createMigration('002_create_posts', ...),
|
|
1064
|
+
createMigration('003_add_indexes', ...)
|
|
1065
|
+
// Not: '001_create_users', '001_another_table', ...
|
|
1066
|
+
]
|
|
1067
|
+
|
|
1068
|
+
// Check for duplicates programmatically
|
|
1069
|
+
const names = migrations.map(m => m.name)
|
|
1070
|
+
const duplicates = names.filter((n, i) => names.indexOf(n) !== i)
|
|
1071
|
+
if (duplicates.length > 0) {
|
|
1072
|
+
throw new Error(`Duplicate migrations: ${duplicates.join(', ')}`)
|
|
1073
|
+
}
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
### Migration Stuck in Pending
|
|
1077
|
+
|
|
1078
|
+
**Problem**: Migration marked as executed but database changes not applied
|
|
1079
|
+
|
|
1080
|
+
**Solution:**
|
|
1081
|
+
```typescript
|
|
1082
|
+
// Manually remove from migrations table and re-run
|
|
1083
|
+
await db
|
|
1084
|
+
.deleteFrom('migrations')
|
|
1085
|
+
.where('name', '=', 'problematic_migration_name')
|
|
1086
|
+
.execute()
|
|
1087
|
+
|
|
1088
|
+
await runner.up()
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
## 📝 Examples
|
|
1092
|
+
|
|
1093
|
+
### Complete Blog Application Migration
|
|
1094
|
+
|
|
1095
|
+
```typescript
|
|
1096
|
+
import { createMigration } from '@kysera/migrations'
|
|
1097
|
+
import { sql } from 'kysely'
|
|
1098
|
+
|
|
1099
|
+
export const blogMigrations = [
|
|
1100
|
+
// 001: Create users table
|
|
1101
|
+
createMigration(
|
|
1102
|
+
'001_create_users',
|
|
1103
|
+
async (db) => {
|
|
1104
|
+
await db.schema
|
|
1105
|
+
.createTable('users')
|
|
1106
|
+
.addColumn('id', 'serial', col => col.primaryKey())
|
|
1107
|
+
.addColumn('email', 'varchar(255)', col => col.notNull().unique())
|
|
1108
|
+
.addColumn('username', 'varchar(50)', col => col.notNull().unique())
|
|
1109
|
+
.addColumn('password_hash', 'varchar(255)', col => col.notNull())
|
|
1110
|
+
.addColumn('created_at', 'timestamp', col =>
|
|
1111
|
+
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
|
|
1112
|
+
)
|
|
1113
|
+
.execute()
|
|
1114
|
+
|
|
1115
|
+
await db.schema
|
|
1116
|
+
.createIndex('users_email_idx')
|
|
1117
|
+
.on('users')
|
|
1118
|
+
.column('email')
|
|
1119
|
+
.execute()
|
|
1120
|
+
},
|
|
1121
|
+
async (db) => {
|
|
1122
|
+
await db.schema.dropTable('users').cascade().execute()
|
|
1123
|
+
}
|
|
1124
|
+
),
|
|
1125
|
+
|
|
1126
|
+
// 002: Create posts table
|
|
1127
|
+
createMigration(
|
|
1128
|
+
'002_create_posts',
|
|
1129
|
+
async (db) => {
|
|
1130
|
+
await db.schema
|
|
1131
|
+
.createTable('posts')
|
|
1132
|
+
.addColumn('id', 'serial', col => col.primaryKey())
|
|
1133
|
+
.addColumn('user_id', 'integer', col =>
|
|
1134
|
+
col.notNull().references('users.id').onDelete('cascade')
|
|
1135
|
+
)
|
|
1136
|
+
.addColumn('title', 'varchar(255)', col => col.notNull())
|
|
1137
|
+
.addColumn('slug', 'varchar(255)', col => col.notNull().unique())
|
|
1138
|
+
.addColumn('content', 'text', col => col.notNull())
|
|
1139
|
+
.addColumn('published', 'boolean', col => col.notNull().defaultTo(false))
|
|
1140
|
+
.addColumn('created_at', 'timestamp', col =>
|
|
1141
|
+
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
|
|
1142
|
+
)
|
|
1143
|
+
.addColumn('updated_at', 'timestamp', col =>
|
|
1144
|
+
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
|
|
1145
|
+
)
|
|
1146
|
+
.execute()
|
|
1147
|
+
|
|
1148
|
+
await db.schema
|
|
1149
|
+
.createIndex('posts_user_id_idx')
|
|
1150
|
+
.on('posts')
|
|
1151
|
+
.column('user_id')
|
|
1152
|
+
.execute()
|
|
1153
|
+
|
|
1154
|
+
await db.schema
|
|
1155
|
+
.createIndex('posts_slug_idx')
|
|
1156
|
+
.on('posts')
|
|
1157
|
+
.column('slug')
|
|
1158
|
+
.execute()
|
|
1159
|
+
},
|
|
1160
|
+
async (db) => {
|
|
1161
|
+
await db.schema.dropTable('posts').execute()
|
|
1162
|
+
}
|
|
1163
|
+
),
|
|
1164
|
+
|
|
1165
|
+
// 003: Create comments table
|
|
1166
|
+
createMigration(
|
|
1167
|
+
'003_create_comments',
|
|
1168
|
+
async (db) => {
|
|
1169
|
+
await db.schema
|
|
1170
|
+
.createTable('comments')
|
|
1171
|
+
.addColumn('id', 'serial', col => col.primaryKey())
|
|
1172
|
+
.addColumn('post_id', 'integer', col =>
|
|
1173
|
+
col.notNull().references('posts.id').onDelete('cascade')
|
|
1174
|
+
)
|
|
1175
|
+
.addColumn('user_id', 'integer', col =>
|
|
1176
|
+
col.notNull().references('users.id').onDelete('cascade')
|
|
1177
|
+
)
|
|
1178
|
+
.addColumn('content', 'text', col => col.notNull())
|
|
1179
|
+
.addColumn('created_at', 'timestamp', col =>
|
|
1180
|
+
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
|
|
1181
|
+
)
|
|
1182
|
+
.execute()
|
|
1183
|
+
|
|
1184
|
+
await db.schema
|
|
1185
|
+
.createIndex('comments_post_id_idx')
|
|
1186
|
+
.on('comments')
|
|
1187
|
+
.column('post_id')
|
|
1188
|
+
.execute()
|
|
1189
|
+
},
|
|
1190
|
+
async (db) => {
|
|
1191
|
+
await db.schema.dropTable('comments').execute()
|
|
1192
|
+
}
|
|
1193
|
+
),
|
|
1194
|
+
|
|
1195
|
+
// 004: Add tags
|
|
1196
|
+
createMigration(
|
|
1197
|
+
'004_create_tags',
|
|
1198
|
+
async (db) => {
|
|
1199
|
+
await db.schema
|
|
1200
|
+
.createTable('tags')
|
|
1201
|
+
.addColumn('id', 'serial', col => col.primaryKey())
|
|
1202
|
+
.addColumn('name', 'varchar(50)', col => col.notNull().unique())
|
|
1203
|
+
.addColumn('slug', 'varchar(50)', col => col.notNull().unique())
|
|
1204
|
+
.execute()
|
|
1205
|
+
|
|
1206
|
+
await db.schema
|
|
1207
|
+
.createTable('post_tags')
|
|
1208
|
+
.addColumn('post_id', 'integer', col =>
|
|
1209
|
+
col.notNull().references('posts.id').onDelete('cascade')
|
|
1210
|
+
)
|
|
1211
|
+
.addColumn('tag_id', 'integer', col =>
|
|
1212
|
+
col.notNull().references('tags.id').onDelete('cascade')
|
|
1213
|
+
)
|
|
1214
|
+
.addPrimaryKeyConstraint('post_tags_pk', ['post_id', 'tag_id'])
|
|
1215
|
+
.execute()
|
|
1216
|
+
|
|
1217
|
+
await db.schema
|
|
1218
|
+
.createIndex('post_tags_tag_id_idx')
|
|
1219
|
+
.on('post_tags')
|
|
1220
|
+
.column('tag_id')
|
|
1221
|
+
.execute()
|
|
1222
|
+
},
|
|
1223
|
+
async (db) => {
|
|
1224
|
+
await db.schema.dropTable('post_tags').execute()
|
|
1225
|
+
await db.schema.dropTable('tags').execute()
|
|
1226
|
+
}
|
|
1227
|
+
),
|
|
1228
|
+
|
|
1229
|
+
// 005: Add full-text search
|
|
1230
|
+
createMigration(
|
|
1231
|
+
'005_add_fulltext_search',
|
|
1232
|
+
async (db) => {
|
|
1233
|
+
// PostgreSQL specific
|
|
1234
|
+
await db.schema
|
|
1235
|
+
.alterTable('posts')
|
|
1236
|
+
.addColumn('search_vector', 'tsvector')
|
|
1237
|
+
.execute()
|
|
1238
|
+
|
|
1239
|
+
await sql`
|
|
1240
|
+
CREATE INDEX posts_search_vector_idx
|
|
1241
|
+
ON posts
|
|
1242
|
+
USING gin(search_vector)
|
|
1243
|
+
`.execute(db)
|
|
1244
|
+
|
|
1245
|
+
await sql`
|
|
1246
|
+
CREATE TRIGGER posts_search_vector_update
|
|
1247
|
+
BEFORE INSERT OR UPDATE ON posts
|
|
1248
|
+
FOR EACH ROW EXECUTE FUNCTION
|
|
1249
|
+
tsvector_update_trigger(
|
|
1250
|
+
search_vector, 'pg_catalog.english', title, content
|
|
1251
|
+
)
|
|
1252
|
+
`.execute(db)
|
|
1253
|
+
},
|
|
1254
|
+
async (db) => {
|
|
1255
|
+
await sql`DROP TRIGGER posts_search_vector_update ON posts`.execute(db)
|
|
1256
|
+
await db.schema
|
|
1257
|
+
.alterTable('posts')
|
|
1258
|
+
.dropColumn('search_vector')
|
|
1259
|
+
.execute()
|
|
1260
|
+
}
|
|
1261
|
+
)
|
|
1262
|
+
]
|
|
1263
|
+
|
|
1264
|
+
// Usage
|
|
1265
|
+
const runner = createMigrationRunner(db, blogMigrations)
|
|
1266
|
+
await runner.up()
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
### Migration Script
|
|
1270
|
+
|
|
1271
|
+
```typescript
|
|
1272
|
+
// scripts/migrate.ts
|
|
1273
|
+
import { Kysely, PostgresDialect } from 'kysely'
|
|
1274
|
+
import { Pool } from 'pg'
|
|
1275
|
+
import { createMigrationRunner } from '@kysera/migrations'
|
|
1276
|
+
import { migrations } from '../migrations'
|
|
1277
|
+
|
|
1278
|
+
async function main() {
|
|
1279
|
+
const db = new Kysely({
|
|
1280
|
+
dialect: new PostgresDialect({
|
|
1281
|
+
pool: new Pool({
|
|
1282
|
+
connectionString: process.env.DATABASE_URL
|
|
1283
|
+
})
|
|
1284
|
+
})
|
|
1285
|
+
})
|
|
1286
|
+
|
|
1287
|
+
const command = process.argv[2]
|
|
1288
|
+
const runner = createMigrationRunner(db, migrations)
|
|
1289
|
+
|
|
1290
|
+
try {
|
|
1291
|
+
switch (command) {
|
|
1292
|
+
case 'up':
|
|
1293
|
+
console.log('Running migrations...')
|
|
1294
|
+
await runner.up()
|
|
1295
|
+
break
|
|
1296
|
+
|
|
1297
|
+
case 'down':
|
|
1298
|
+
const steps = parseInt(process.argv[3] || '1')
|
|
1299
|
+
console.log(`Rolling back ${steps} migration(s)...`)
|
|
1300
|
+
await runner.down(steps)
|
|
1301
|
+
break
|
|
1302
|
+
|
|
1303
|
+
case 'status':
|
|
1304
|
+
await runner.status()
|
|
1305
|
+
break
|
|
1306
|
+
|
|
1307
|
+
case 'reset':
|
|
1308
|
+
console.log('⚠️ WARNING: This will rollback ALL migrations!')
|
|
1309
|
+
await runner.reset()
|
|
1310
|
+
break
|
|
1311
|
+
|
|
1312
|
+
default:
|
|
1313
|
+
console.log('Usage: pnpm migrate [up|down|status|reset] [steps]')
|
|
1314
|
+
process.exit(1)
|
|
1315
|
+
}
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
console.error('Migration failed:', error)
|
|
1318
|
+
process.exit(1)
|
|
1319
|
+
} finally {
|
|
1320
|
+
await db.destroy()
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
main()
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
```json
|
|
1328
|
+
// package.json
|
|
1329
|
+
{
|
|
1330
|
+
"scripts": {
|
|
1331
|
+
"migrate:up": "tsx scripts/migrate.ts up",
|
|
1332
|
+
"migrate:down": "tsx scripts/migrate.ts down",
|
|
1333
|
+
"migrate:status": "tsx scripts/migrate.ts status",
|
|
1334
|
+
"migrate:reset": "tsx scripts/migrate.ts reset"
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
```
|
|
1338
|
+
|
|
1339
|
+
## 🔒 Type Safety
|
|
1340
|
+
|
|
1341
|
+
### Fully Typed Migrations
|
|
1342
|
+
|
|
1343
|
+
```typescript
|
|
1344
|
+
import type { Kysely } from 'kysely'
|
|
1345
|
+
import type { Database } from './database'
|
|
1346
|
+
|
|
1347
|
+
// Type-safe migration
|
|
1348
|
+
const migration = createMigration(
|
|
1349
|
+
'001_create_users',
|
|
1350
|
+
async (db: Kysely<Database>) => {
|
|
1351
|
+
await db.schema
|
|
1352
|
+
.createTable('users')
|
|
1353
|
+
.addColumn('id', 'serial', col => col.primaryKey())
|
|
1354
|
+
.addColumn('email', 'varchar(255)', col => col.notNull())
|
|
1355
|
+
.execute()
|
|
1356
|
+
},
|
|
1357
|
+
async (db: Kysely<Database>) => {
|
|
1358
|
+
await db.schema.dropTable('users').execute()
|
|
1359
|
+
}
|
|
1360
|
+
)
|
|
1361
|
+
|
|
1362
|
+
// Type-safe data migration
|
|
1363
|
+
createMigration(
|
|
1364
|
+
'002_migrate_data',
|
|
1365
|
+
async (db: Kysely<Database>) => {
|
|
1366
|
+
// TypeScript knows about 'users' table
|
|
1367
|
+
const users = await db
|
|
1368
|
+
.selectFrom('users')
|
|
1369
|
+
.selectAll()
|
|
1370
|
+
.execute()
|
|
1371
|
+
|
|
1372
|
+
// Type-safe operations
|
|
1373
|
+
for (const user of users) {
|
|
1374
|
+
await db
|
|
1375
|
+
.updateTable('users')
|
|
1376
|
+
.set({ email: user.email.toLowerCase() })
|
|
1377
|
+
.where('id', '=', user.id)
|
|
1378
|
+
.execute()
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
)
|
|
1382
|
+
```
|
|
1383
|
+
|
|
1384
|
+
## 📄 License
|
|
1385
|
+
|
|
1386
|
+
MIT © [Kysera Team](https://github.com/omnitron-dev)
|
|
1387
|
+
|
|
1388
|
+
## 🤝 Contributing
|
|
1389
|
+
|
|
1390
|
+
Contributions are welcome! Please read our [Contributing Guide](../../CONTRIBUTING.md) for details.
|
|
1391
|
+
|
|
1392
|
+
## 📚 Related Packages
|
|
1393
|
+
|
|
1394
|
+
- [`@kysera/core`](../core) - Core utilities and error handling
|
|
1395
|
+
- [`@kysera/repository`](../repository) - Repository pattern implementation
|
|
1396
|
+
- [`@kysera/audit`](../audit) - Audit logging plugin
|
|
1397
|
+
- [`@kysera/soft-delete`](../soft-delete) - Soft delete plugin
|
|
1398
|
+
- [`@kysera/timestamps`](../timestamps) - Automatic timestamp management
|
|
1399
|
+
|
|
1400
|
+
## 🔗 Links
|
|
1401
|
+
|
|
1402
|
+
- [Documentation](https://kysera.dev/docs/migrations)
|
|
1403
|
+
- [GitHub Repository](https://github.com/omnitron-dev/kysera)
|
|
1404
|
+
- [Issue Tracker](https://github.com/omnitron-dev/kysera/issues)
|
|
1405
|
+
- [Kysely Documentation](https://kysely.dev)
|
|
1406
|
+
|
|
1407
|
+
---
|
|
1408
|
+
|
|
1409
|
+
**Built with ❤️ using TypeScript and Kysely**
|