@kysera/migrations 0.4.1 → 0.6.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 +443 -1046
- package/dist/index.d.ts +234 -24
- package/dist/index.js +1 -14
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.ts +138 -0
- package/dist/schemas.js +2 -0
- package/dist/schemas.js.map +1 -0
- package/package.json +24 -11
package/README.md
CHANGED
|
@@ -1,41 +1,53 @@
|
|
|
1
1
|
# @kysera/migrations
|
|
2
2
|
|
|
3
|
-
> Lightweight, type-safe database migration management for Kysera ORM with dry-run support
|
|
3
|
+
> Lightweight, type-safe database migration management for Kysera ORM with dry-run support, flexible rollback capabilities, and plugin system.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@kysera/migrations)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Package Information
|
|
10
10
|
|
|
11
11
|
| Property | Value |
|
|
12
12
|
|----------|-------|
|
|
13
13
|
| **Package** | `@kysera/migrations` |
|
|
14
|
-
| **Version** | `0.
|
|
15
|
-
| **Bundle Size** |
|
|
16
|
-
| **Dependencies** |
|
|
17
|
-
| **
|
|
14
|
+
| **Version** | `0.5.1` |
|
|
15
|
+
| **Bundle Size** | ~12 KB (minified) |
|
|
16
|
+
| **Dependencies** | @kysera/core (workspace) |
|
|
17
|
+
| **Peer Dependencies** | kysely >=0.28.8, zod ^4.1.13 |
|
|
18
|
+
| **Test Coverage** | 64 tests, comprehensive |
|
|
18
19
|
| **Supported Databases** | PostgreSQL, MySQL, SQLite |
|
|
19
|
-
| **Type Safety** |
|
|
20
|
+
| **Type Safety** | Full TypeScript support |
|
|
20
21
|
|
|
21
|
-
##
|
|
22
|
+
## Features
|
|
22
23
|
|
|
23
24
|
### Core Migration Management
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
25
|
+
- **Simple API** - Intuitive migration creation and execution
|
|
26
|
+
- **Type-safe** - Full TypeScript support with Kysely integration
|
|
27
|
+
- **State tracking** - Automatic migration history in database
|
|
28
|
+
- **Sequential execution** - Migrations run in order
|
|
29
|
+
- **Dry run mode** - Preview changes before execution
|
|
29
30
|
|
|
30
31
|
### Advanced Features
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
- **Rollback support** - Roll back one or multiple migrations
|
|
33
|
+
- **Partial migration** - Run up to specific migration
|
|
34
|
+
- **Status reporting** - View executed and pending migrations
|
|
35
|
+
- **Error handling** - Typed errors with `MigrationError` class
|
|
36
|
+
- **Transaction support** - Optional transaction wrapping per migration
|
|
37
|
+
- **Duplicate detection** - Validates unique migration names
|
|
38
|
+
|
|
39
|
+
### Developer Experience (v0.5.0+)
|
|
40
|
+
- **`defineMigrations()`** - Object-based migration definition
|
|
41
|
+
- **`runMigrations()`** - One-liner to run pending migrations
|
|
42
|
+
- **`rollbackMigrations()`** - One-liner for rollbacks
|
|
43
|
+
- **Migration metadata** - Description, breaking flag, tags, timing
|
|
44
|
+
|
|
45
|
+
### Plugin System (v0.5.0+)
|
|
46
|
+
- **Plugin hooks** - Before/after migration events
|
|
47
|
+
- **Built-in plugins** - Logging and metrics plugins
|
|
48
|
+
- **Extensible** - Create custom plugins for your needs
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
39
51
|
|
|
40
52
|
```bash
|
|
41
53
|
# pnpm (recommended)
|
|
@@ -51,7 +63,7 @@ yarn add @kysera/migrations kysely
|
|
|
51
63
|
bun add @kysera/migrations kysely
|
|
52
64
|
```
|
|
53
65
|
|
|
54
|
-
##
|
|
66
|
+
## Quick Start
|
|
55
67
|
|
|
56
68
|
### Basic Usage
|
|
57
69
|
|
|
@@ -86,7 +98,6 @@ const migrations = [
|
|
|
86
98
|
col.notNull().references('users.id').onDelete('cascade')
|
|
87
99
|
)
|
|
88
100
|
.addColumn('title', 'varchar(255)', col => col.notNull())
|
|
89
|
-
.addColumn('content', 'text')
|
|
90
101
|
.execute()
|
|
91
102
|
},
|
|
92
103
|
async (db) => {
|
|
@@ -101,1221 +112,576 @@ const runner = createMigrationRunner(db, migrations)
|
|
|
101
112
|
|
|
102
113
|
// Run all pending migrations
|
|
103
114
|
await runner.up()
|
|
104
|
-
// ✅ All migrations completed successfully
|
|
105
115
|
|
|
106
116
|
// Check status
|
|
107
117
|
await runner.status()
|
|
108
|
-
// 📊 Migration Status:
|
|
109
|
-
// ✅ Executed: 2
|
|
110
|
-
// ⏳ Pending: 0
|
|
111
118
|
|
|
112
119
|
// Rollback last migration
|
|
113
120
|
await runner.down(1)
|
|
114
|
-
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Minimalist API (v0.5.0+)
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { Kysely } from 'kysely'
|
|
127
|
+
import { defineMigrations, runMigrations } from '@kysera/migrations'
|
|
128
|
+
|
|
129
|
+
// Define migrations with object syntax
|
|
130
|
+
const migrations = defineMigrations({
|
|
131
|
+
'001_create_users': {
|
|
132
|
+
description: 'Create users table with email and name',
|
|
133
|
+
up: async (db) => {
|
|
134
|
+
await db.schema
|
|
135
|
+
.createTable('users')
|
|
136
|
+
.addColumn('id', 'serial', col => col.primaryKey())
|
|
137
|
+
.addColumn('email', 'varchar(255)', col => col.notNull().unique())
|
|
138
|
+
.execute()
|
|
139
|
+
},
|
|
140
|
+
down: async (db) => {
|
|
141
|
+
await db.schema.dropTable('users').execute()
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
'002_create_posts': {
|
|
146
|
+
description: 'Create posts table',
|
|
147
|
+
breaking: false,
|
|
148
|
+
tags: ['schema'],
|
|
149
|
+
up: async (db) => {
|
|
150
|
+
await db.schema
|
|
151
|
+
.createTable('posts')
|
|
152
|
+
.addColumn('id', 'serial', col => col.primaryKey())
|
|
153
|
+
.addColumn('title', 'varchar(255)', col => col.notNull())
|
|
154
|
+
.execute()
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// One-liner to run all migrations
|
|
160
|
+
await runMigrations(db, migrations)
|
|
115
161
|
```
|
|
116
162
|
|
|
117
163
|
### Dry Run Mode
|
|
118
164
|
|
|
119
165
|
```typescript
|
|
166
|
+
import { runMigrations } from '@kysera/migrations'
|
|
167
|
+
|
|
120
168
|
// Preview what would happen without making changes
|
|
121
|
-
const
|
|
122
|
-
dryRun: true,
|
|
123
|
-
logger: console.log
|
|
124
|
-
})
|
|
169
|
+
const result = await runMigrations(db, migrations, { dryRun: true })
|
|
125
170
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
// ✓ 001_create_users completed
|
|
130
|
-
// ↑ Running 002_create_posts...
|
|
131
|
-
// ✓ 002_create_posts completed
|
|
171
|
+
console.log('Would execute:', result.executed)
|
|
172
|
+
console.log('Would skip:', result.skipped)
|
|
173
|
+
// No actual changes made to database
|
|
132
174
|
```
|
|
133
175
|
|
|
134
|
-
##
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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:
|
|
176
|
+
## API Reference
|
|
177
|
+
|
|
178
|
+
### Types
|
|
179
|
+
|
|
180
|
+
#### `Migration`
|
|
163
181
|
|
|
164
182
|
```typescript
|
|
165
183
|
interface Migration {
|
|
166
|
-
/** Unique migration name (e.g., '001_create_users') */
|
|
167
184
|
name: string
|
|
168
|
-
|
|
169
|
-
/** Migration up function - creates/modifies schema */
|
|
170
185
|
up: (db: Kysely<any>) => Promise<void>
|
|
171
|
-
|
|
172
|
-
/** Optional migration down function - reverts changes */
|
|
173
186
|
down?: (db: Kysely<any>) => Promise<void>
|
|
174
187
|
}
|
|
175
188
|
```
|
|
176
189
|
|
|
177
|
-
|
|
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
|
|
190
|
+
#### `MigrationWithMeta`
|
|
182
191
|
|
|
183
|
-
|
|
192
|
+
```typescript
|
|
193
|
+
interface MigrationWithMeta extends Migration {
|
|
194
|
+
description?: string // Shown in logs
|
|
195
|
+
breaking?: boolean // Shows warning
|
|
196
|
+
estimatedDuration?: number // In milliseconds
|
|
197
|
+
tags?: string[] // For categorization
|
|
198
|
+
}
|
|
199
|
+
```
|
|
184
200
|
|
|
185
|
-
|
|
201
|
+
#### `MigrationStatus`
|
|
186
202
|
|
|
187
203
|
```typescript
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
204
|
+
interface MigrationStatus {
|
|
205
|
+
executed: string[]
|
|
206
|
+
pending: string[]
|
|
207
|
+
total: number
|
|
208
|
+
}
|
|
209
|
+
```
|
|
194
210
|
|
|
195
|
-
|
|
196
|
-
async status(): Promise<MigrationStatus>
|
|
211
|
+
#### `MigrationResult`
|
|
197
212
|
|
|
198
|
-
|
|
199
|
-
|
|
213
|
+
```typescript
|
|
214
|
+
interface MigrationResult {
|
|
215
|
+
executed: string[] // Successfully executed
|
|
216
|
+
skipped: string[] // Already executed or no down()
|
|
217
|
+
failed: string[] // Failed migrations
|
|
218
|
+
duration: number // Total time in ms
|
|
219
|
+
dryRun: boolean // Whether dry run mode
|
|
220
|
+
}
|
|
221
|
+
```
|
|
200
222
|
|
|
201
|
-
|
|
202
|
-
async upTo(targetName: string): Promise<void>
|
|
223
|
+
#### `MigrationRunnerOptions`
|
|
203
224
|
|
|
204
|
-
|
|
205
|
-
|
|
225
|
+
```typescript
|
|
226
|
+
interface MigrationRunnerOptions {
|
|
227
|
+
dryRun?: boolean // Preview only (default: false)
|
|
228
|
+
logger?: KyseraLogger // Logger instance from @kysera/core (default: silentLogger)
|
|
229
|
+
useTransactions?: boolean // Wrap in transactions (default: false)
|
|
230
|
+
stopOnError?: boolean // Stop on first error (default: true)
|
|
231
|
+
verbose?: boolean // Show metadata (default: true)
|
|
206
232
|
}
|
|
207
233
|
```
|
|
208
234
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
Migrations are tracked in a `migrations` table:
|
|
235
|
+
#### `MigrationDefinition`
|
|
212
236
|
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
237
|
+
```typescript
|
|
238
|
+
interface MigrationDefinition {
|
|
239
|
+
up: (db: Kysely<any>) => Promise<void>
|
|
240
|
+
down?: (db: Kysely<any>) => Promise<void>
|
|
241
|
+
description?: string
|
|
242
|
+
breaking?: boolean
|
|
243
|
+
estimatedDuration?: number
|
|
244
|
+
tags?: string[]
|
|
245
|
+
}
|
|
218
246
|
```
|
|
219
247
|
|
|
220
|
-
|
|
248
|
+
#### `MigrationDefinitions`
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
type MigrationDefinitions = Record<string, MigrationDefinition>
|
|
252
|
+
```
|
|
221
253
|
|
|
222
|
-
|
|
254
|
+
#### `MigrationRunnerWithPluginsOptions`
|
|
223
255
|
|
|
224
|
-
|
|
256
|
+
```typescript
|
|
257
|
+
interface MigrationRunnerWithPluginsOptions extends MigrationRunnerOptions {
|
|
258
|
+
plugins?: MigrationPlugin[]
|
|
259
|
+
}
|
|
260
|
+
```
|
|
225
261
|
|
|
226
|
-
|
|
262
|
+
#### `MigrationErrorCode`
|
|
227
263
|
|
|
228
264
|
```typescript
|
|
229
|
-
|
|
230
|
-
name: string,
|
|
231
|
-
up: (db: Kysely<any>) => Promise<void>,
|
|
232
|
-
down?: (db: Kysely<any>) => Promise<void>
|
|
233
|
-
): Migration
|
|
265
|
+
type MigrationErrorCode = 'MIGRATION_UP_FAILED' | 'MIGRATION_DOWN_FAILED' | 'MIGRATION_VALIDATION_FAILED'
|
|
234
266
|
```
|
|
235
267
|
|
|
236
|
-
|
|
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
|
|
268
|
+
### Factory Functions
|
|
240
269
|
|
|
241
|
-
|
|
270
|
+
#### `createMigration(name, up, down?)`
|
|
271
|
+
|
|
272
|
+
Create a simple migration:
|
|
242
273
|
|
|
243
|
-
**Example:**
|
|
244
274
|
```typescript
|
|
245
275
|
const migration = createMigration(
|
|
246
276
|
'001_create_users',
|
|
247
|
-
async (db) => {
|
|
248
|
-
|
|
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
|
-
}
|
|
277
|
+
async (db) => { /* up */ },
|
|
278
|
+
async (db) => { /* down */ }
|
|
257
279
|
)
|
|
258
280
|
```
|
|
259
281
|
|
|
260
|
-
|
|
282
|
+
#### `createMigrationWithMeta(name, options)`
|
|
261
283
|
|
|
262
|
-
Create a
|
|
284
|
+
Create a migration with metadata:
|
|
263
285
|
|
|
264
286
|
```typescript
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
287
|
+
const migration = createMigrationWithMeta('001_create_users', {
|
|
288
|
+
description: 'Create users table',
|
|
289
|
+
breaking: true,
|
|
290
|
+
tags: ['schema', 'users'],
|
|
291
|
+
estimatedDuration: 5000,
|
|
292
|
+
up: async (db) => { /* ... */ },
|
|
293
|
+
down: async (db) => { /* ... */ },
|
|
294
|
+
})
|
|
270
295
|
```
|
|
271
296
|
|
|
272
|
-
|
|
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
|
-
```
|
|
297
|
+
#### `createMigrationRunner(db, migrations, options?)`
|
|
287
298
|
|
|
288
|
-
|
|
299
|
+
Create a MigrationRunner instance:
|
|
289
300
|
|
|
290
|
-
**Example:**
|
|
291
301
|
```typescript
|
|
292
302
|
const runner = createMigrationRunner(db, migrations, {
|
|
293
303
|
dryRun: false,
|
|
294
|
-
logger:
|
|
304
|
+
logger: console.log,
|
|
305
|
+
useTransactions: true,
|
|
295
306
|
})
|
|
296
307
|
```
|
|
297
308
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
#### up()
|
|
309
|
+
#### `createMigrationRunnerWithPlugins(db, migrations, options?)`
|
|
301
310
|
|
|
302
|
-
|
|
311
|
+
Create a MigrationRunner with plugin support (async factory):
|
|
303
312
|
|
|
304
313
|
```typescript
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
+
const runner = await createMigrationRunnerWithPlugins(db, migrations, {
|
|
315
|
+
plugins: [createLoggingPlugin(), createMetricsPlugin()],
|
|
316
|
+
useTransactions: true,
|
|
317
|
+
})
|
|
314
318
|
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
319
|
+
// Runner is ready with plugins initialized via onInit
|
|
317
320
|
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
321
|
```
|
|
324
322
|
|
|
325
|
-
|
|
323
|
+
### One-Liner Functions (v0.5.0+)
|
|
326
324
|
|
|
327
|
-
|
|
325
|
+
#### `defineMigrations(definitions)`
|
|
326
|
+
|
|
327
|
+
Define migrations using object syntax:
|
|
328
328
|
|
|
329
329
|
```typescript
|
|
330
|
-
|
|
330
|
+
const migrations = defineMigrations({
|
|
331
|
+
'001_users': {
|
|
332
|
+
description: 'Create users',
|
|
333
|
+
up: async (db) => { /* ... */ },
|
|
334
|
+
down: async (db) => { /* ... */ },
|
|
335
|
+
},
|
|
336
|
+
})
|
|
331
337
|
```
|
|
332
338
|
|
|
333
|
-
|
|
334
|
-
- `steps` - Number of migrations to rollback (default: 1)
|
|
339
|
+
#### `runMigrations(db, migrations, options?)`
|
|
335
340
|
|
|
336
|
-
|
|
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
|
|
341
|
+
Run all pending migrations:
|
|
343
342
|
|
|
344
|
-
**Example:**
|
|
345
343
|
```typescript
|
|
346
|
-
|
|
347
|
-
await
|
|
348
|
-
|
|
349
|
-
// Rollback last 3 migrations
|
|
350
|
-
await runner.down(3)
|
|
344
|
+
const result = await runMigrations(db, migrations)
|
|
345
|
+
const result = await runMigrations(db, migrations, { dryRun: true })
|
|
351
346
|
```
|
|
352
347
|
|
|
353
|
-
####
|
|
348
|
+
#### `rollbackMigrations(db, migrations, steps?, options?)`
|
|
354
349
|
|
|
355
|
-
|
|
350
|
+
Rollback migrations:
|
|
356
351
|
|
|
357
352
|
```typescript
|
|
358
|
-
|
|
353
|
+
await rollbackMigrations(db, migrations) // Last 1
|
|
354
|
+
await rollbackMigrations(db, migrations, 3) // Last 3
|
|
355
|
+
await rollbackMigrations(db, migrations, 1, { dryRun: true })
|
|
359
356
|
```
|
|
360
357
|
|
|
361
|
-
|
|
362
|
-
```typescript
|
|
363
|
-
interface MigrationStatus {
|
|
364
|
-
/** List of executed migration names */
|
|
365
|
-
executed: string[]
|
|
358
|
+
#### `getMigrationStatus(db, migrations, options?)`
|
|
366
359
|
|
|
367
|
-
|
|
368
|
-
pending: string[]
|
|
369
|
-
}
|
|
370
|
-
```
|
|
360
|
+
Get migration status:
|
|
371
361
|
|
|
372
|
-
**Example:**
|
|
373
362
|
```typescript
|
|
374
|
-
const status = await
|
|
363
|
+
const status = await getMigrationStatus(db, migrations)
|
|
375
364
|
console.log(`Executed: ${status.executed.length}`)
|
|
376
365
|
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
366
|
```
|
|
390
367
|
|
|
391
|
-
|
|
368
|
+
### MigrationRunner Methods
|
|
369
|
+
|
|
370
|
+
#### `up(): Promise<MigrationResult>`
|
|
392
371
|
|
|
393
|
-
|
|
372
|
+
Run all pending migrations:
|
|
394
373
|
|
|
395
374
|
```typescript
|
|
396
|
-
|
|
375
|
+
const result = await runner.up()
|
|
376
|
+
console.log(`Executed: ${result.executed.length} migrations`)
|
|
397
377
|
```
|
|
398
378
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
- Only rolls back migrations that have down() methods
|
|
379
|
+
#### `down(steps?): Promise<MigrationResult>`
|
|
380
|
+
|
|
381
|
+
Rollback last N migrations:
|
|
403
382
|
|
|
404
|
-
**Example:**
|
|
405
383
|
```typescript
|
|
406
|
-
await runner.
|
|
407
|
-
//
|
|
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
|
|
384
|
+
await runner.down(1) // Rollback last one
|
|
385
|
+
await runner.down(3) // Rollback last three
|
|
415
386
|
```
|
|
416
387
|
|
|
417
|
-
####
|
|
388
|
+
#### `status(): Promise<MigrationStatus>`
|
|
418
389
|
|
|
419
|
-
|
|
390
|
+
Get migration status:
|
|
420
391
|
|
|
421
392
|
```typescript
|
|
422
|
-
|
|
393
|
+
const status = await runner.status()
|
|
394
|
+
// Logs status to console and returns object
|
|
423
395
|
```
|
|
424
396
|
|
|
425
|
-
|
|
426
|
-
- `targetName` - Name of the target migration
|
|
397
|
+
#### `reset(): Promise<MigrationResult>`
|
|
427
398
|
|
|
428
|
-
|
|
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
|
|
399
|
+
Rollback all migrations:
|
|
433
400
|
|
|
434
|
-
**Example:**
|
|
435
401
|
```typescript
|
|
436
|
-
await runner.
|
|
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
|
|
402
|
+
await runner.reset() // Dangerous! Rolls back everything
|
|
442
403
|
```
|
|
443
404
|
|
|
444
|
-
|
|
405
|
+
#### `upTo(targetName): Promise<MigrationResult>`
|
|
445
406
|
|
|
446
|
-
|
|
407
|
+
Run migrations up to a specific one:
|
|
447
408
|
|
|
448
409
|
```typescript
|
|
449
|
-
|
|
410
|
+
await runner.upTo('002_create_posts')
|
|
411
|
+
// Runs 001 and 002, stops before 003
|
|
450
412
|
```
|
|
451
413
|
|
|
452
|
-
|
|
414
|
+
#### `getExecutedMigrations(): Promise<string[]>`
|
|
453
415
|
|
|
454
|
-
|
|
455
|
-
```typescript
|
|
456
|
-
import { setupMigrations } from '@kysera/migrations'
|
|
416
|
+
Get list of executed migrations:
|
|
457
417
|
|
|
458
|
-
|
|
459
|
-
|
|
418
|
+
```typescript
|
|
419
|
+
const executed = await runner.getExecutedMigrations()
|
|
460
420
|
```
|
|
461
421
|
|
|
462
|
-
|
|
422
|
+
#### `markAsExecuted(name): Promise<void>`
|
|
463
423
|
|
|
464
|
-
|
|
424
|
+
Manually mark a migration as executed:
|
|
465
425
|
|
|
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
|
-
└─────────────────────────────────────────────────────────┘
|
|
426
|
+
```typescript
|
|
427
|
+
await runner.markAsExecuted('001_create_users')
|
|
496
428
|
```
|
|
497
429
|
|
|
498
|
-
|
|
430
|
+
#### `markAsRolledBack(name): Promise<void>`
|
|
499
431
|
|
|
500
|
-
|
|
432
|
+
Manually mark a migration as rolled back:
|
|
501
433
|
|
|
502
434
|
```typescript
|
|
503
|
-
|
|
504
|
-
createMigration('001_first', ...), // Runs first
|
|
505
|
-
createMigration('002_second', ...), // Runs second
|
|
506
|
-
createMigration('003_third', ...) // Runs third
|
|
507
|
-
]
|
|
435
|
+
await runner.markAsRolledBack('001_create_users')
|
|
508
436
|
```
|
|
509
437
|
|
|
510
|
-
|
|
511
|
-
- ✅ `001_create_users`, `002_create_posts`, `003_add_indexes`
|
|
512
|
-
- ❌ `create_users`, `create_posts` (no guaranteed order)
|
|
438
|
+
### Standalone Functions
|
|
513
439
|
|
|
514
|
-
|
|
440
|
+
#### `setupMigrations(db)`
|
|
515
441
|
|
|
516
|
-
|
|
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
|
|
442
|
+
Manually create the migrations tracking table:
|
|
521
443
|
|
|
522
444
|
```typescript
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
// Handle error: notify admins, rollback, etc.
|
|
528
|
-
}
|
|
445
|
+
import { setupMigrations } from '@kysera/migrations'
|
|
446
|
+
|
|
447
|
+
await setupMigrations(db)
|
|
448
|
+
// Creates migrations table if not exists
|
|
529
449
|
```
|
|
530
450
|
|
|
531
|
-
##
|
|
451
|
+
## Plugin System (v0.5.0+)
|
|
532
452
|
|
|
533
|
-
###
|
|
453
|
+
### Plugin Interface
|
|
534
454
|
|
|
535
|
-
|
|
455
|
+
Consistent with `@kysera/repository` Plugin interface:
|
|
536
456
|
|
|
537
457
|
```typescript
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
458
|
+
interface MigrationPlugin {
|
|
459
|
+
name: string
|
|
460
|
+
version: string
|
|
461
|
+
// Called once when runner is initialized (consistent with repository Plugin.onInit)
|
|
462
|
+
onInit?(runner: MigrationRunner): Promise<void> | void
|
|
463
|
+
beforeMigration?(migration: Migration, operation: 'up' | 'down'): Promise<void> | void
|
|
464
|
+
afterMigration?(migration: Migration, operation: 'up' | 'down', duration: number): Promise<void> | void
|
|
465
|
+
// Unknown error type for consistency with repository Plugin.onError
|
|
466
|
+
onMigrationError?(migration: Migration, operation: 'up' | 'down', error: unknown): Promise<void> | void
|
|
467
|
+
}
|
|
545
468
|
```
|
|
546
469
|
|
|
547
|
-
###
|
|
470
|
+
### Built-in Plugins
|
|
548
471
|
|
|
549
|
-
|
|
472
|
+
#### Logging Plugin
|
|
550
473
|
|
|
551
474
|
```typescript
|
|
552
|
-
import {
|
|
553
|
-
import { logger } from './logger'
|
|
554
|
-
|
|
555
|
-
const runner = createMigrationRunner(db, migrations, {
|
|
556
|
-
logger: (message) => {
|
|
557
|
-
logger.info('[MIGRATIONS]', message)
|
|
558
|
-
}
|
|
559
|
-
})
|
|
475
|
+
import { createLoggingPlugin } from '@kysera/migrations'
|
|
560
476
|
|
|
561
|
-
|
|
562
|
-
//
|
|
477
|
+
const loggingPlugin = createLoggingPlugin(console.log)
|
|
478
|
+
// or with custom logger
|
|
479
|
+
const loggingPlugin = createLoggingPlugin((msg) => logger.info(msg))
|
|
563
480
|
```
|
|
564
481
|
|
|
565
|
-
|
|
482
|
+
#### Metrics Plugin
|
|
566
483
|
|
|
567
484
|
```typescript
|
|
568
|
-
|
|
569
|
-
logger: () => {} // No-op logger
|
|
570
|
-
})
|
|
485
|
+
import { createMetricsPlugin } from '@kysera/migrations'
|
|
571
486
|
|
|
572
|
-
|
|
573
|
-
```
|
|
487
|
+
const metricsPlugin = createMetricsPlugin()
|
|
574
488
|
|
|
575
|
-
|
|
489
|
+
// After running migrations
|
|
490
|
+
const metrics = metricsPlugin.getMetrics()
|
|
491
|
+
console.log(metrics.migrations)
|
|
492
|
+
// [{ name: '001_users', operation: 'up', duration: 45, success: true }, ...]
|
|
493
|
+
```
|
|
576
494
|
|
|
577
|
-
|
|
495
|
+
### Creating Custom Plugins
|
|
578
496
|
|
|
579
497
|
```typescript
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
498
|
+
const notificationPlugin: MigrationPlugin = {
|
|
499
|
+
name: 'notification-plugin',
|
|
500
|
+
version: '1.0.0',
|
|
583
501
|
|
|
584
|
-
|
|
585
|
-
|
|
502
|
+
// Called when runner is created via createMigrationRunnerWithPlugins()
|
|
503
|
+
async onInit(runner) {
|
|
504
|
+
console.log('Notification plugin initialized')
|
|
505
|
+
},
|
|
586
506
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
507
|
+
async beforeMigration(migration, operation) {
|
|
508
|
+
await slack.send(`Starting ${operation} for ${migration.name}`)
|
|
509
|
+
},
|
|
590
510
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
511
|
+
async afterMigration(migration, operation, duration) {
|
|
512
|
+
await slack.send(`Completed ${migration.name} in ${duration}ms`)
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
async onMigrationError(migration, operation, error) {
|
|
516
|
+
// Error is unknown type - handle appropriately
|
|
517
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
518
|
+
await pagerduty.alert(`Migration failed: ${message}`)
|
|
519
|
+
},
|
|
598
520
|
}
|
|
599
521
|
```
|
|
600
522
|
|
|
601
|
-
|
|
523
|
+
## Error Handling
|
|
602
524
|
|
|
603
|
-
|
|
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()
|
|
525
|
+
### MigrationError
|
|
618
526
|
|
|
619
|
-
|
|
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()
|
|
527
|
+
Extends `DatabaseError` from `@kysera/core` for consistency:
|
|
628
528
|
|
|
629
|
-
|
|
630
|
-
|
|
529
|
+
```typescript
|
|
530
|
+
import { MigrationError } from '@kysera/migrations'
|
|
631
531
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
//
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
532
|
+
try {
|
|
533
|
+
await runner.up()
|
|
534
|
+
} catch (error) {
|
|
535
|
+
if (error instanceof MigrationError) {
|
|
536
|
+
console.log('Migration:', error.migrationName)
|
|
537
|
+
console.log('Operation:', error.operation) // 'up' or 'down'
|
|
538
|
+
console.log('Code:', error.code) // 'MIGRATION_UP_FAILED' or 'MIGRATION_DOWN_FAILED'
|
|
539
|
+
console.log('Cause:', error.cause?.message)
|
|
540
|
+
|
|
541
|
+
// Serialize for logging
|
|
542
|
+
console.log(error.toJSON())
|
|
543
|
+
// { name, message, code, detail, migrationName, operation, cause }
|
|
644
544
|
}
|
|
645
|
-
|
|
545
|
+
}
|
|
646
546
|
```
|
|
647
547
|
|
|
648
|
-
###
|
|
548
|
+
### BadRequestError
|
|
649
549
|
|
|
650
|
-
|
|
550
|
+
For validation errors (e.g., duplicate migration names):
|
|
651
551
|
|
|
652
552
|
```typescript
|
|
653
|
-
|
|
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()
|
|
553
|
+
import { BadRequestError } from '@kysera/migrations'
|
|
668
554
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
.dropColumn('role')
|
|
679
|
-
.execute()
|
|
555
|
+
try {
|
|
556
|
+
createMigrationRunner(db, [
|
|
557
|
+
createMigration('001_users', ...),
|
|
558
|
+
createMigration('001_users', ...), // Duplicate!
|
|
559
|
+
])
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if (error instanceof BadRequestError) {
|
|
562
|
+
console.log(error.message) // "Duplicate migration name: 001_users"
|
|
563
|
+
console.log(error.code) // "BAD_REQUEST"
|
|
680
564
|
}
|
|
681
|
-
|
|
565
|
+
}
|
|
682
566
|
```
|
|
683
567
|
|
|
684
|
-
|
|
568
|
+
### NotFoundError
|
|
685
569
|
|
|
686
|
-
|
|
570
|
+
Uses `NotFoundError` from `@kysera/core`:
|
|
687
571
|
|
|
688
572
|
```typescript
|
|
689
|
-
|
|
690
|
-
const migrations = [
|
|
691
|
-
createMigration('001_create_users', ...),
|
|
692
|
-
createMigration('002_create_posts', ...),
|
|
693
|
-
createMigration('003_add_indexes', ...)
|
|
694
|
-
]
|
|
573
|
+
import { NotFoundError } from '@kysera/migrations'
|
|
695
574
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
575
|
+
try {
|
|
576
|
+
await runner.upTo('nonexistent_migration')
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if (error instanceof NotFoundError) {
|
|
579
|
+
console.log(error.message) // "Migration not found"
|
|
580
|
+
}
|
|
581
|
+
}
|
|
702
582
|
```
|
|
703
583
|
|
|
704
|
-
|
|
584
|
+
## Best Practices
|
|
705
585
|
|
|
706
|
-
|
|
707
|
-
// ✅ Good: One table per migration
|
|
708
|
-
createMigration('001_create_users', async (db) => {
|
|
709
|
-
await db.schema.createTable('users')...execute()
|
|
710
|
-
})
|
|
586
|
+
### 1. Use Numeric Prefixes
|
|
711
587
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
588
|
+
```typescript
|
|
589
|
+
// Good - clear ordering
|
|
590
|
+
'001_create_users'
|
|
591
|
+
'002_create_posts'
|
|
592
|
+
'003_add_indexes'
|
|
715
593
|
|
|
716
|
-
//
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
await db.schema.createTable('posts')...execute()
|
|
720
|
-
await db.schema.createTable('comments')...execute()
|
|
721
|
-
})
|
|
594
|
+
// Bad - no guaranteed order
|
|
595
|
+
'create_users'
|
|
596
|
+
'create_posts'
|
|
722
597
|
```
|
|
723
598
|
|
|
724
|
-
###
|
|
599
|
+
### 2. Always Provide down() Methods
|
|
725
600
|
|
|
726
601
|
```typescript
|
|
727
|
-
// ✅ Good: Reversible migration
|
|
728
602
|
createMigration(
|
|
729
603
|
'001_create_users',
|
|
730
|
-
async (db) => {
|
|
731
|
-
|
|
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
|
|
604
|
+
async (db) => { /* up */ },
|
|
605
|
+
async (db) => { /* down - always provide this! */ }
|
|
748
606
|
)
|
|
749
607
|
```
|
|
750
608
|
|
|
751
|
-
###
|
|
609
|
+
### 3. Use Metadata for Complex Migrations
|
|
752
610
|
|
|
753
611
|
```typescript
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
612
|
+
createMigrationWithMeta('005_big_refactor', {
|
|
613
|
+
description: 'Refactors user permissions system',
|
|
614
|
+
breaking: true, // Will show warning
|
|
615
|
+
tags: ['breaking', 'permissions'],
|
|
616
|
+
up: async (db) => { /* ... */ },
|
|
757
617
|
})
|
|
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
618
|
```
|
|
771
619
|
|
|
772
|
-
###
|
|
620
|
+
### 4. Test with Dry Run First
|
|
773
621
|
|
|
774
622
|
```typescript
|
|
775
|
-
// Preview
|
|
776
|
-
|
|
777
|
-
dryRun: true,
|
|
778
|
-
logger: console.log
|
|
779
|
-
})
|
|
780
|
-
|
|
781
|
-
await dryRunner.up()
|
|
782
|
-
// Review output, ensure it's safe
|
|
623
|
+
// Preview in production
|
|
624
|
+
await runMigrations(db, migrations, { dryRun: true })
|
|
783
625
|
|
|
784
626
|
// Then run for real
|
|
785
|
-
|
|
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
|
-
]
|
|
627
|
+
await runMigrations(db, migrations)
|
|
814
628
|
```
|
|
815
629
|
|
|
816
|
-
###
|
|
630
|
+
### 5. Use Transactions for Safety
|
|
817
631
|
|
|
818
632
|
```typescript
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
-
}
|
|
633
|
+
const runner = createMigrationRunner(db, migrations, {
|
|
634
|
+
useTransactions: true, // Each migration wrapped in transaction
|
|
635
|
+
})
|
|
839
636
|
```
|
|
840
637
|
|
|
841
|
-
##
|
|
842
|
-
|
|
843
|
-
### PostgreSQL
|
|
638
|
+
## Migration Script Example
|
|
844
639
|
|
|
845
640
|
```typescript
|
|
641
|
+
// scripts/migrate.ts
|
|
846
642
|
import { Kysely, PostgresDialect } from 'kysely'
|
|
847
643
|
import { Pool } from 'pg'
|
|
848
|
-
import {
|
|
644
|
+
import { runMigrations, rollbackMigrations, getMigrationStatus, defineMigrations } from '@kysera/migrations'
|
|
645
|
+
|
|
646
|
+
const migrations = defineMigrations({
|
|
647
|
+
// ... your migrations
|
|
648
|
+
})
|
|
849
649
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
})
|
|
650
|
+
async function main() {
|
|
651
|
+
const db = new Kysely({
|
|
652
|
+
dialect: new PostgresDialect({
|
|
653
|
+
pool: new Pool({ connectionString: process.env.DATABASE_URL })
|
|
1284
654
|
})
|
|
1285
655
|
})
|
|
1286
656
|
|
|
1287
657
|
const command = process.argv[2]
|
|
1288
|
-
const runner = createMigrationRunner(db, migrations)
|
|
1289
658
|
|
|
1290
659
|
try {
|
|
1291
660
|
switch (command) {
|
|
1292
661
|
case 'up':
|
|
1293
662
|
console.log('Running migrations...')
|
|
1294
|
-
await
|
|
663
|
+
const upResult = await runMigrations(db, migrations)
|
|
664
|
+
console.log(`Executed: ${upResult.executed.length} migrations`)
|
|
1295
665
|
break
|
|
1296
666
|
|
|
1297
667
|
case 'down':
|
|
1298
668
|
const steps = parseInt(process.argv[3] || '1')
|
|
1299
669
|
console.log(`Rolling back ${steps} migration(s)...`)
|
|
1300
|
-
await
|
|
670
|
+
await rollbackMigrations(db, migrations, steps)
|
|
1301
671
|
break
|
|
1302
672
|
|
|
1303
673
|
case 'status':
|
|
1304
|
-
await
|
|
674
|
+
await getMigrationStatus(db, migrations)
|
|
1305
675
|
break
|
|
1306
676
|
|
|
1307
|
-
case '
|
|
1308
|
-
console.log('
|
|
1309
|
-
await
|
|
677
|
+
case 'dry-run':
|
|
678
|
+
console.log('Dry run mode...')
|
|
679
|
+
await runMigrations(db, migrations, { dryRun: true })
|
|
1310
680
|
break
|
|
1311
681
|
|
|
1312
682
|
default:
|
|
1313
|
-
console.log('Usage: pnpm migrate [up|down|status|
|
|
1314
|
-
process.exit(1)
|
|
683
|
+
console.log('Usage: pnpm migrate [up|down|status|dry-run] [steps]')
|
|
1315
684
|
}
|
|
1316
|
-
} catch (error) {
|
|
1317
|
-
console.error('Migration failed:', error)
|
|
1318
|
-
process.exit(1)
|
|
1319
685
|
} finally {
|
|
1320
686
|
await db.destroy()
|
|
1321
687
|
}
|
|
@@ -1328,82 +694,113 @@ main()
|
|
|
1328
694
|
// package.json
|
|
1329
695
|
{
|
|
1330
696
|
"scripts": {
|
|
1331
|
-
"migrate
|
|
697
|
+
"migrate": "tsx scripts/migrate.ts up",
|
|
1332
698
|
"migrate:down": "tsx scripts/migrate.ts down",
|
|
1333
699
|
"migrate:status": "tsx scripts/migrate.ts status",
|
|
1334
|
-
"migrate:
|
|
700
|
+
"migrate:dry-run": "tsx scripts/migrate.ts dry-run"
|
|
1335
701
|
}
|
|
1336
702
|
}
|
|
1337
703
|
```
|
|
1338
704
|
|
|
1339
|
-
##
|
|
705
|
+
## Multi-Database Support
|
|
1340
706
|
|
|
1341
|
-
###
|
|
707
|
+
### PostgreSQL
|
|
1342
708
|
|
|
1343
709
|
```typescript
|
|
1344
|
-
|
|
1345
|
-
import type { Database } from './database'
|
|
1346
|
-
|
|
1347
|
-
// Type-safe migration
|
|
1348
|
-
const migration = createMigration(
|
|
710
|
+
createMigration(
|
|
1349
711
|
'001_create_users',
|
|
1350
|
-
async (db
|
|
712
|
+
async (db) => {
|
|
1351
713
|
await db.schema
|
|
1352
714
|
.createTable('users')
|
|
1353
715
|
.addColumn('id', 'serial', col => col.primaryKey())
|
|
1354
|
-
.addColumn('
|
|
716
|
+
.addColumn('created_at', 'timestamp', col =>
|
|
717
|
+
col.notNull().defaultTo(db.fn('now'))
|
|
718
|
+
)
|
|
1355
719
|
.execute()
|
|
1356
|
-
},
|
|
1357
|
-
async (db: Kysely<Database>) => {
|
|
1358
|
-
await db.schema.dropTable('users').execute()
|
|
1359
720
|
}
|
|
1360
721
|
)
|
|
722
|
+
```
|
|
1361
723
|
|
|
1362
|
-
|
|
724
|
+
### MySQL
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
1363
727
|
createMigration(
|
|
1364
|
-
'
|
|
1365
|
-
async (db
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
.
|
|
1369
|
-
|
|
728
|
+
'001_create_users',
|
|
729
|
+
async (db) => {
|
|
730
|
+
await db.schema
|
|
731
|
+
.createTable('users')
|
|
732
|
+
.addColumn('id', 'integer', col =>
|
|
733
|
+
col.primaryKey().autoIncrement()
|
|
734
|
+
)
|
|
735
|
+
.addColumn('created_at', 'datetime', col =>
|
|
736
|
+
col.notNull().defaultTo(db.fn('now'))
|
|
737
|
+
)
|
|
1370
738
|
.execute()
|
|
739
|
+
}
|
|
740
|
+
)
|
|
741
|
+
```
|
|
1371
742
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
743
|
+
### SQLite
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
createMigration(
|
|
747
|
+
'001_create_users',
|
|
748
|
+
async (db) => {
|
|
749
|
+
await db.schema
|
|
750
|
+
.createTable('users')
|
|
751
|
+
.addColumn('id', 'integer', col =>
|
|
752
|
+
col.primaryKey().autoIncrement()
|
|
753
|
+
)
|
|
754
|
+
.addColumn('created_at', 'text', col =>
|
|
755
|
+
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
|
|
756
|
+
)
|
|
757
|
+
.execute()
|
|
1380
758
|
}
|
|
1381
759
|
)
|
|
1382
760
|
```
|
|
1383
761
|
|
|
1384
|
-
##
|
|
762
|
+
## Changelog
|
|
763
|
+
|
|
764
|
+
### v0.5.1
|
|
1385
765
|
|
|
1386
|
-
|
|
766
|
+
- **Breaking** `MigrationError` now extends `DatabaseError` from `@kysera/core` with `code` property
|
|
767
|
+
- **Breaking** `onMigrationError` hook now receives `error: unknown` (consistent with repository Plugin)
|
|
768
|
+
- **Breaking** `createMigrationRunnerWithPlugins()` is now async (returns `Promise<MigrationRunnerWithPlugins>`)
|
|
769
|
+
- **Added** `onInit` hook to `MigrationPlugin` interface (consistent with repository Plugin)
|
|
770
|
+
- **Added** `MigrationErrorCode` type export
|
|
771
|
+
- **Added** `MigrationDefinition` and `MigrationDefinitions` type exports
|
|
772
|
+
- **Added** `MigrationRunnerWithPluginsOptions` interface export
|
|
773
|
+
- **Added** `DatabaseError` and `BadRequestError` re-exports from `@kysera/core`
|
|
774
|
+
- **Changed** Validation errors now throw `BadRequestError` instead of generic `Error`
|
|
1387
775
|
|
|
1388
|
-
|
|
776
|
+
### v0.5.0
|
|
1389
777
|
|
|
1390
|
-
|
|
778
|
+
- **Added** `@kysera/core` integration with typed errors
|
|
779
|
+
- **Added** `MigrationWithMeta` support with description, breaking flag, tags
|
|
780
|
+
- **Added** `defineMigrations()` for object-based syntax
|
|
781
|
+
- **Added** `runMigrations()`, `rollbackMigrations()`, `getMigrationStatus()` one-liners
|
|
782
|
+
- **Added** `MigrationResult` return type for all operations
|
|
783
|
+
- **Added** Plugin system with `MigrationPlugin` interface
|
|
784
|
+
- **Added** Built-in `createLoggingPlugin()` and `createMetricsPlugin()`
|
|
785
|
+
- **Added** `MigrationError` class for better error handling
|
|
786
|
+
- **Added** Duplicate migration name validation
|
|
787
|
+
- **Added** `useTransactions` option for transaction wrapping
|
|
788
|
+
- **Added** `stopOnError` option for error handling control
|
|
789
|
+
- **Fixed** Inconsistent dry run behavior in `reset()` and `upTo()`
|
|
790
|
+
- **Fixed** `MigrationStatus` now includes `total` count
|
|
1391
791
|
|
|
1392
|
-
|
|
792
|
+
### v0.4.1
|
|
1393
793
|
|
|
1394
|
-
-
|
|
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
|
|
794
|
+
- Initial release
|
|
1399
795
|
|
|
1400
|
-
##
|
|
796
|
+
## Related Packages
|
|
1401
797
|
|
|
1402
|
-
- [
|
|
1403
|
-
- [
|
|
1404
|
-
- [
|
|
1405
|
-
- [
|
|
798
|
+
- [`@kysera/core`](../kysera-core) - Core utilities and error handling
|
|
799
|
+
- [`@kysera/repository`](../kysera-repository) - Repository pattern implementation
|
|
800
|
+
- [`@kysera/audit`](../kysera-audit) - Audit logging plugin
|
|
801
|
+
- [`@kysera/soft-delete`](../kysera-soft-delete) - Soft delete plugin
|
|
802
|
+
- [`@kysera/timestamps`](../kysera-timestamps) - Automatic timestamp management
|
|
1406
803
|
|
|
1407
|
-
|
|
804
|
+
## License
|
|
1408
805
|
|
|
1409
|
-
|
|
806
|
+
MIT
|