@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 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 and flexible rollback capabilities.
3
+ > Lightweight, type-safe database migration management for Kysera ORM with dry-run support, flexible rollback capabilities, and plugin system.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@kysera/migrations.svg)](https://www.npmjs.com/package/@kysera/migrations)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue)](https://www.typescriptlang.org/)
8
8
 
9
- ## 📦 Package Information
9
+ ## Package Information
10
10
 
11
11
  | Property | Value |
12
12
  |----------|-------|
13
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 |
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** | Full TypeScript support |
20
+ | **Type Safety** | Full TypeScript support |
20
21
 
21
- ## 🎯 Features
22
+ ## Features
22
23
 
23
24
  ### 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
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
- - **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
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
- ## 🚀 Quick Start
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
- // ✅ Rollback completed successfully
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 runner = createMigrationRunner(db, migrations, {
122
- dryRun: true,
123
- logger: console.log
124
- })
169
+ const result = await runMigrations(db, migrations, { dryRun: true })
125
170
 
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
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
- ## 📖 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:
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
- **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
190
+ #### `MigrationWithMeta`
182
191
 
183
- ### Migration Runner
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
- The `MigrationRunner` class manages migration execution and state:
201
+ #### `MigrationStatus`
186
202
 
187
203
  ```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>
204
+ interface MigrationStatus {
205
+ executed: string[]
206
+ pending: string[]
207
+ total: number
208
+ }
209
+ ```
194
210
 
195
- // Show migration status
196
- async status(): Promise<MigrationStatus>
211
+ #### `MigrationResult`
197
212
 
198
- // Reset all migrations (rollback everything)
199
- async reset(): Promise<void>
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
- // Migrate up to specific migration
202
- async upTo(targetName: string): Promise<void>
223
+ #### `MigrationRunnerOptions`
203
224
 
204
- // Get list of executed migrations
205
- async getExecutedMigrations(): Promise<string[]>
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
- ### State Tracking
210
-
211
- Migrations are tracked in a `migrations` table:
235
+ #### `MigrationDefinition`
212
236
 
213
- ```sql
214
- CREATE TABLE migrations (
215
- name VARCHAR(255) PRIMARY KEY,
216
- executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
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
- This table is automatically created when you run migrations and stores which migrations have been executed.
248
+ #### `MigrationDefinitions`
249
+
250
+ ```typescript
251
+ type MigrationDefinitions = Record<string, MigrationDefinition>
252
+ ```
221
253
 
222
- ## 📚 API Reference
254
+ #### `MigrationRunnerWithPluginsOptions`
223
255
 
224
- ### createMigration()
256
+ ```typescript
257
+ interface MigrationRunnerWithPluginsOptions extends MigrationRunnerOptions {
258
+ plugins?: MigrationPlugin[]
259
+ }
260
+ ```
225
261
 
226
- Create a migration object with up and optional down functions.
262
+ #### `MigrationErrorCode`
227
263
 
228
264
  ```typescript
229
- function createMigration(
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
- **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
268
+ ### Factory Functions
240
269
 
241
- **Returns:** Migration object
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
- 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
- }
277
+ async (db) => { /* up */ },
278
+ async (db) => { /* down */ }
257
279
  )
258
280
  ```
259
281
 
260
- ### createMigrationRunner()
282
+ #### `createMigrationWithMeta(name, options)`
261
283
 
262
- Create a MigrationRunner instance.
284
+ Create a migration with metadata:
263
285
 
264
286
  ```typescript
265
- function createMigrationRunner(
266
- db: Kysely<any>,
267
- migrations: Migration[],
268
- options?: MigrationRunnerOptions
269
- ): MigrationRunner
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
- **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
- ```
297
+ #### `createMigrationRunner(db, migrations, options?)`
287
298
 
288
- **Returns:** MigrationRunner instance
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: (msg) => console.log(`[MIGRATION] ${msg}`)
304
+ logger: console.log,
305
+ useTransactions: true,
295
306
  })
296
307
  ```
297
308
 
298
- ### MigrationRunner Methods
299
-
300
- #### up()
309
+ #### `createMigrationRunnerWithPlugins(db, migrations, options?)`
301
310
 
302
- Run all pending migrations sequentially.
311
+ Create a MigrationRunner with plugin support (async factory):
303
312
 
304
313
  ```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
+ const runner = await createMigrationRunnerWithPlugins(db, migrations, {
315
+ plugins: [createLoggingPlugin(), createMetricsPlugin()],
316
+ useTransactions: true,
317
+ })
314
318
 
315
- **Example:**
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
- #### down()
323
+ ### One-Liner Functions (v0.5.0+)
326
324
 
327
- Rollback the last N migrations.
325
+ #### `defineMigrations(definitions)`
326
+
327
+ Define migrations using object syntax:
328
328
 
329
329
  ```typescript
330
- async down(steps?: number): Promise<void>
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
- **Parameters:**
334
- - `steps` - Number of migrations to rollback (default: 1)
339
+ #### `runMigrations(db, migrations, options?)`
335
340
 
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
341
+ Run all pending migrations:
343
342
 
344
- **Example:**
345
343
  ```typescript
346
- // Rollback last migration
347
- await runner.down(1)
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
- #### status()
348
+ #### `rollbackMigrations(db, migrations, steps?, options?)`
354
349
 
355
- Display current migration status.
350
+ Rollback migrations:
356
351
 
357
352
  ```typescript
358
- async status(): Promise<MigrationStatus>
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
- **Returns:**
362
- ```typescript
363
- interface MigrationStatus {
364
- /** List of executed migration names */
365
- executed: string[]
358
+ #### `getMigrationStatus(db, migrations, options?)`
366
359
 
367
- /** List of pending migration names */
368
- pending: string[]
369
- }
370
- ```
360
+ Get migration status:
371
361
 
372
- **Example:**
373
362
  ```typescript
374
- const status = await runner.status()
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
- #### reset()
368
+ ### MigrationRunner Methods
369
+
370
+ #### `up(): Promise<MigrationResult>`
392
371
 
393
- Rollback all migrations (dangerous!).
372
+ Run all pending migrations:
394
373
 
395
374
  ```typescript
396
- async reset(): Promise<void>
375
+ const result = await runner.up()
376
+ console.log(`Executed: ${result.executed.length} migrations`)
397
377
  ```
398
378
 
399
- **Behavior:**
400
- - Gets count of executed migrations
401
- - Calls down() to rollback all migrations
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.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
384
+ await runner.down(1) // Rollback last one
385
+ await runner.down(3) // Rollback last three
415
386
  ```
416
387
 
417
- #### upTo()
388
+ #### `status(): Promise<MigrationStatus>`
418
389
 
419
- Run migrations up to (and including) a specific migration.
390
+ Get migration status:
420
391
 
421
392
  ```typescript
422
- async upTo(targetName: string): Promise<void>
393
+ const status = await runner.status()
394
+ // Logs status to console and returns object
423
395
  ```
424
396
 
425
- **Parameters:**
426
- - `targetName` - Name of the target migration
397
+ #### `reset(): Promise<MigrationResult>`
427
398
 
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
399
+ Rollback all migrations:
433
400
 
434
- **Example:**
435
401
  ```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
402
+ await runner.reset() // Dangerous! Rolls back everything
442
403
  ```
443
404
 
444
- ### setupMigrations()
405
+ #### `upTo(targetName): Promise<MigrationResult>`
445
406
 
446
- Manually create the migrations tracking table.
407
+ Run migrations up to a specific one:
447
408
 
448
409
  ```typescript
449
- async function setupMigrations(db: Kysely<any>): Promise<void>
410
+ await runner.upTo('002_create_posts')
411
+ // Runs 001 and 002, stops before 003
450
412
  ```
451
413
 
452
- **Note:** This is called automatically by the runner, but you can call it manually if needed.
414
+ #### `getExecutedMigrations(): Promise<string[]>`
453
415
 
454
- **Example:**
455
- ```typescript
456
- import { setupMigrations } from '@kysera/migrations'
416
+ Get list of executed migrations:
457
417
 
458
- await setupMigrations(db)
459
- // Creates migrations table if it doesn't exist
418
+ ```typescript
419
+ const executed = await runner.getExecutedMigrations()
460
420
  ```
461
421
 
462
- ## 🔄 Migration Lifecycle
422
+ #### `markAsExecuted(name): Promise<void>`
463
423
 
464
- ### Complete Migration Flow
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
- ### Migration Execution Order
430
+ #### `markAsRolledBack(name): Promise<void>`
499
431
 
500
- Migrations are executed in **array order**:
432
+ Manually mark a migration as rolled back:
501
433
 
502
434
  ```typescript
503
- const migrations = [
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
- **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)
438
+ ### Standalone Functions
513
439
 
514
- ### Error Handling
440
+ #### `setupMigrations(db)`
515
441
 
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
442
+ Manually create the migrations tracking table:
521
443
 
522
444
  ```typescript
523
- try {
524
- await runner.up()
525
- } catch (error) {
526
- console.error('Migration failed:', error)
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
- ## 🔧 Advanced Usage
451
+ ## Plugin System (v0.5.0+)
532
452
 
533
- ### Partial Migrations
453
+ ### Plugin Interface
534
454
 
535
- Run migrations incrementally:
455
+ Consistent with `@kysera/repository` Plugin interface:
536
456
 
537
457
  ```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()
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
- ### Custom Logger
470
+ ### Built-in Plugins
548
471
 
549
- Integrate with your logging system:
472
+ #### Logging Plugin
550
473
 
551
474
  ```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
- })
475
+ import { createLoggingPlugin } from '@kysera/migrations'
560
476
 
561
- await runner.up()
562
- // Logs will be sent to your logging system
477
+ const loggingPlugin = createLoggingPlugin(console.log)
478
+ // or with custom logger
479
+ const loggingPlugin = createLoggingPlugin((msg) => logger.info(msg))
563
480
  ```
564
481
 
565
- ### Disable Logging
482
+ #### Metrics Plugin
566
483
 
567
484
  ```typescript
568
- const runner = createMigrationRunner(db, migrations, {
569
- logger: () => {} // No-op logger
570
- })
485
+ import { createMetricsPlugin } from '@kysera/migrations'
571
486
 
572
- await runner.up() // Silent execution
573
- ```
487
+ const metricsPlugin = createMetricsPlugin()
574
488
 
575
- ### Migration Metadata
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
- Add metadata to migrations for documentation:
495
+ ### Creating Custom Plugins
578
496
 
579
497
  ```typescript
580
- interface MigrationWithMeta extends Migration {
581
- /** Human-readable description */
582
- description?: string
498
+ const notificationPlugin: MigrationPlugin = {
499
+ name: 'notification-plugin',
500
+ version: '1.0.0',
583
501
 
584
- /** Whether this is a breaking change */
585
- breaking?: boolean
502
+ // Called when runner is created via createMigrationRunnerWithPlugins()
503
+ async onInit(runner) {
504
+ console.log('Notification plugin initialized')
505
+ },
586
506
 
587
- /** Estimated duration in milliseconds */
588
- estimatedDuration?: number
589
- }
507
+ async beforeMigration(migration, operation) {
508
+ await slack.send(`Starting ${operation} for ${migration.name}`)
509
+ },
590
510
 
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) => { /* ... */ }
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
- ### Complex Schema Changes
523
+ ## Error Handling
602
524
 
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()
525
+ ### MigrationError
618
526
 
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()
527
+ Extends `DatabaseError` from `@kysera/core` for consistency:
628
528
 
629
- // 3. Drop old table
630
- await trx.schema.dropTable('users').execute()
529
+ ```typescript
530
+ import { MigrationError } from '@kysera/migrations'
631
531
 
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
- })
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
- ### Data Migrations
548
+ ### BadRequestError
649
549
 
650
- Migrate data along with schema:
550
+ For validation errors (e.g., duplicate migration names):
651
551
 
652
552
  ```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()
553
+ import { BadRequestError } from '@kysera/migrations'
668
554
 
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()
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
- ## ✅ Best Practices
568
+ ### NotFoundError
685
569
 
686
- ### 1. Always Use Numeric Prefixes
570
+ Uses `NotFoundError` from `@kysera/core`:
687
571
 
688
572
  ```typescript
689
- // Good: Clear ordering
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
- // ❌ Bad: No guaranteed order
697
- const migrations = [
698
- createMigration('create_users', ...),
699
- createMigration('create_posts', ...),
700
- createMigration('add_indexes', ...)
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
- ### 2. Keep Migrations Small and Focused
584
+ ## Best Practices
705
585
 
706
- ```typescript
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
- createMigration('002_create_posts', async (db) => {
713
- await db.schema.createTable('posts')...execute()
714
- })
588
+ ```typescript
589
+ // Good - clear ordering
590
+ '001_create_users'
591
+ '002_create_posts'
592
+ '003_add_indexes'
715
593
 
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
- })
594
+ // Bad - no guaranteed order
595
+ 'create_users'
596
+ 'create_posts'
722
597
  ```
723
598
 
724
- ### 3. Always Provide down() Methods
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
- 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
604
+ async (db) => { /* up */ },
605
+ async (db) => { /* down - always provide this! */ }
748
606
  )
749
607
  ```
750
608
 
751
- ### 4. Test Migrations Before Production
609
+ ### 3. Use Metadata for Complex Migrations
752
610
 
753
611
  ```typescript
754
- // Test in development
755
- const runner = createMigrationRunner(devDb, migrations, {
756
- logger: console.log
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
- ### 5. Use Dry Run for Production
620
+ ### 4. Test with Dry Run First
773
621
 
774
622
  ```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
623
+ // Preview in production
624
+ await runMigrations(db, migrations, { dryRun: true })
783
625
 
784
626
  // 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
- ]
627
+ await runMigrations(db, migrations)
814
628
  ```
815
629
 
816
- ### 7. Handle Breaking Changes Carefully
630
+ ### 5. Use Transactions for Safety
817
631
 
818
632
  ```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
- }
633
+ const runner = createMigrationRunner(db, migrations, {
634
+ useTransactions: true, // Each migration wrapped in transaction
635
+ })
839
636
  ```
840
637
 
841
- ## 🗃️ Multi-Database Support
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 { createMigrationRunner, createMigration } from '@kysera/migrations'
644
+ import { runMigrations, rollbackMigrations, getMigrationStatus, defineMigrations } from '@kysera/migrations'
645
+
646
+ const migrations = defineMigrations({
647
+ // ... your migrations
648
+ })
849
649
 
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
- })
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 runner.up()
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 runner.down(steps)
670
+ await rollbackMigrations(db, migrations, steps)
1301
671
  break
1302
672
 
1303
673
  case 'status':
1304
- await runner.status()
674
+ await getMigrationStatus(db, migrations)
1305
675
  break
1306
676
 
1307
- case 'reset':
1308
- console.log('⚠️ WARNING: This will rollback ALL migrations!')
1309
- await runner.reset()
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|reset] [steps]')
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:up": "tsx scripts/migrate.ts up",
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:reset": "tsx scripts/migrate.ts reset"
700
+ "migrate:dry-run": "tsx scripts/migrate.ts dry-run"
1335
701
  }
1336
702
  }
1337
703
  ```
1338
704
 
1339
- ## 🔒 Type Safety
705
+ ## Multi-Database Support
1340
706
 
1341
- ### Fully Typed Migrations
707
+ ### PostgreSQL
1342
708
 
1343
709
  ```typescript
1344
- import type { Kysely } from 'kysely'
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: Kysely<Database>) => {
712
+ async (db) => {
1351
713
  await db.schema
1352
714
  .createTable('users')
1353
715
  .addColumn('id', 'serial', col => col.primaryKey())
1354
- .addColumn('email', 'varchar(255)', col => col.notNull())
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
- // Type-safe data migration
724
+ ### MySQL
725
+
726
+ ```typescript
1363
727
  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()
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
- // 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
- }
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
- ## 📄 License
762
+ ## Changelog
763
+
764
+ ### v0.5.1
1385
765
 
1386
- MIT © [Kysera Team](https://github.com/omnitron-dev)
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
- ## 🤝 Contributing
776
+ ### v0.5.0
1389
777
 
1390
- Contributions are welcome! Please read our [Contributing Guide](../../CONTRIBUTING.md) for details.
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
- ## 📚 Related Packages
792
+ ### v0.4.1
1393
793
 
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
794
+ - Initial release
1399
795
 
1400
- ## 🔗 Links
796
+ ## Related Packages
1401
797
 
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)
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
- **Built with ❤️ using TypeScript and Kysely**
806
+ MIT