Migrating from v0.5.x to v0.6.0
Guide for upgrading from v0.5.x to v0.6.0 of Migration Script Runner.
Table of Contents
- Overview
- Prerequisites
- Upgrade Steps
- Breaking Changes
- What Are Generic Type Parameters?
- Type Safety Adoption Guide
- Enhanced Type Guards
- Real-World Examples
- New Feature: Metrics Collection
- Benefits Summary
- Common Patterns
- Troubleshooting
- FAQ
- Summary
- Next Steps
Overview
v0.6.0 is a major release with breaking changes in the constructor signature and interface type parameters. This release adds powerful generic type parameters throughout the API for database-specific type safety and improves the constructor with dependency injection pattern.
What’s New in v0.6.0
- ✨ Generic Type Parameters - Full type safety for database-specific operations
- 🔨 Breaking Change: Constructor Signature - Now uses
{ handler }dependency injection pattern - ✨ Enhanced Type Guards - Type-preserving runtime checks with
isImperativeTransactional<DB>()andisCallbackTransactional<DB>() - 🎯 IDE Autocomplete - Full IntelliSense for database-specific methods
- 🛡️ Compile-Time Validation - Catch database errors at compile time, not runtime
- 🔧 Auto-Config Loading - Config parameter now optional (auto-loads if not provided)
- 📊 Metrics Collection - New built-in metrics collectors (Console, Logger, Json, Csv) for observability
Migration Effort
Estimated Time: 10-30 minutes to update constructor calls and add type parameters
Complexity: Medium - Constructor signature change (required) and type parameters (required for interfaces)
Prerequisites
Before upgrading, ensure:
- You’re currently on v0.5.x
- Your tests are passing
- You have TypeScript 4.5+ (for best generic inference)
Upgrade Steps
1. Update Package
npm install @migration-script-runner/core@^0.6.0
Or with yarn:
yarn upgrade @migration-script-runner/core@^0.6.0
2. Add Type Parameters to Interfaces
REQUIRED: Add explicit type parameters to all interface implementations:
BEFORE (v0.5.x):
import { IDatabaseMigrationHandler, IRunnableScript } from '@migration-script-runner/core';
// Handler without type parameter
class MyHandler implements IDatabaseMigrationHandler {
db: IDB;
schemaVersion: ISchemaVersion;
// ...
}
// Migration without type parameter
const migration: IRunnableScript = {
async up(db, info, handler) {
// ...
}
};
AFTER (v0.6.0):
import { IDatabaseMigrationHandler, IRunnableScript, IDB } from '@migration-script-runner/core';
// Handler WITH type parameter (REQUIRED)
class MyHandler implements IDatabaseMigrationHandler<IDB> {
db: IDB;
schemaVersion: ISchemaVersion<IDB>;
// ...
}
// Migration WITH type parameter (REQUIRED)
const migration: IRunnableScript<IDB> = {
async up(db, info, handler) {
// ...
}
};
Key Changes:
- All interface implementations must specify
<IDB>or your custom database type - This applies to:
IDatabaseMigrationHandler,IRunnableScript,ISchemaVersion,IBackup,ITransactionManager
3. Update Constructor Calls
REQUIRED: Update all constructor calls to use the new dependency injection pattern:
BEFORE (v0.5.x):
import { MigrationScriptExecutor, Config } from '@migration-script-runner/core';
const executor = new MigrationScriptExecutor({ handler }, config);
AFTER (v0.6.0):
import { MigrationScriptExecutor, Config } from '@migration-script-runner/core';
const executor = new MigrationScriptExecutor({ handler }, config);
// Note: config is now optional - auto-loads if not provided
Key Changes:
- Handler must be wrapped in object:
{ handler } - Config is now second parameter (and optional)
4. Run Your Test Suite
npm test
All tests should pass after updating the type parameters (Step 2) and constructor calls (Step 3).
5. Optional: Adopt Database-Specific Type Safety
v0.6.0’s generic type parameters allow you to go beyond <IDB> to use database-specific types for enhanced type safety. See sections below for details.
Breaking Changes
v0.6.0 includes two breaking changes:
1. Constructor Signature Change
| Component | v0.5.x | v0.6.0 |
|---|---|---|
| MigrationScriptExecutor | new MigrationScriptExecutor({ handler }, config) | new MigrationScriptExecutor({ handler }, config?) |
What Changed:
- Handler is now required in a
dependenciesobject as the first parameter - Config is now the second parameter and is optional (auto-loads if not provided)
Why: This enables dependency injection and makes the API more extensible for future features.
2. Generic Type Parameters (BREAKING)
All interfaces now require explicit generic type parameters:
| API | v0.5.x Signature | v0.6.0 Signature |
|---|---|---|
| IDatabaseMigrationHandler | interface IDatabaseMigrationHandler | interface IDatabaseMigrationHandler<DB extends IDB> |
| IRunnableScript | interface IRunnableScript | interface IRunnableScript<DB extends IDB> |
| MigrationScriptExecutor | class MigrationScriptExecutor | class MigrationScriptExecutor<DB extends IDB> |
| ISchemaVersion | interface ISchemaVersion | interface ISchemaVersion<DB extends IDB> |
| ITransactionManager | interface ITransactionManager | interface ITransactionManager<DB extends IDB> |
You must specify the type parameter - there is no default. Minimum requirement is to use <IDB> for all implementations.
What Are Generic Type Parameters?
Generic type parameters allow you to specify the exact database type throughout MSR’s interfaces, giving you:
- Full IDE autocomplete for database-specific methods
- Compile-time type checking - catch errors before runtime
- Self-documenting code - types show what’s available
- No more
as anycasting in migration scripts
The Problem (v0.5.x)
// v0.5.x - Limited type safety (no type parameters)
interface IDatabaseMigrationHandler {
db: IDB; // Generic IDB type - no database-specific methods
}
// In migration script - no type parameter
const migration: IRunnableScript = {
async up(db, info, handler) {
// ❌ No autocomplete for PostgreSQL-specific methods
// ❌ TypeScript doesn't know about query(), COPY, etc.
await (db as any).query('SELECT * FROM users'); // Have to cast!
}
};
The Solution (v0.6.0)
// v0.6.0 - Full type safety with required generics
interface IDatabaseMigrationHandler<DB extends IDB> {
db: DB; // Specific database type (IPostgresDB, IMySQLDB, etc.)
}
// In migration script - type parameter REQUIRED
const migration: IRunnableScript<IPostgresDB> = {
async up(db, info, handler) {
// ✅ Full autocomplete for PostgreSQL methods
// ✅ TypeScript validates db.query() exists
await db.query('SELECT * FROM users'); // No casting needed!
await db.query('COPY users FROM STDIN'); // PostgreSQL-specific
}
};
Type Safety Adoption Guide
The minimum requirement is using <IDB> for all interfaces. You can then incrementally adopt database-specific types for enhanced type safety.
Step 1: Define Your Database Interface
Create a typed interface for your database:
import { IDB, ITransactionalDB } from '@migration-script-runner/core';
// PostgreSQL example - ITransactionalDB already extends IDB
interface IPostgresDB extends ITransactionalDB {
query<T = any>(sql: string, params?: any[]): Promise<T[]>;
// Add other PostgreSQL-specific methods as needed
}
// MySQL example - ITransactionalDB already extends IDB
interface IMySQLDB extends ITransactionalDB {
query(sql: string, values?: any[]): Promise<any>;
execute(sql: string, values?: any[]): Promise<any>;
}
// MongoDB example - No transactions, extends IDB directly
interface IMongoDatabase extends IDB {
collection(name: string): any;
listCollections(): any;
}
Step 2: Type Your Handler
Add the generic parameter to your handler:
BEFORE (v0.5.x)
import { IDatabaseMigrationHandler, IDB } from '@migration-script-runner/core';
class PostgreSQLHandler implements IDatabaseMigrationHandler<IDB> {
db: IDB; // ❌ Generic IDB - no PostgreSQL methods visible
constructor(db: IDB) {
this.db = db;
}
// ... other methods
}
AFTER (v0.6.0)
import { IDatabaseMigrationHandler } from '@migration-script-runner/core';
import { IPostgresDB } from './types';
class PostgreSQLHandler implements IDatabaseMigrationHandler<IPostgresDB> {
db: IPostgresDB; // ✅ Typed - full PostgreSQL methods visible
constructor(db: IPostgresDB) {
this.db = db;
}
// ... other methods
}
Step 3: Type Your Migrations
Add type parameters to migration scripts:
BEFORE (v0.5.x)
import { IRunnableScript, IMigrationInfo, IDatabaseMigrationHandler } from '@migration-script-runner/core';
const migration: IRunnableScript = {
async up(db, info, handler) {
// ❌ db is IDB - no database-specific autocomplete
// ❌ Have to cast to access query()
await (db as any).query('CREATE TABLE users (id SERIAL PRIMARY KEY)');
},
async down(db, info, handler) {
await (db as any).query('DROP TABLE users');
}
};
export default migration;
AFTER (v0.6.0)
import { IRunnableScript, IMigrationInfo, IDatabaseMigrationHandler } from '@migration-script-runner/core';
import { IPostgresDB } from '../types';
const migration: IRunnableScript<IPostgresDB> = {
async up(db, info, handler) {
// ✅ db is IPostgresDB - full autocomplete!
// ✅ No casting needed
await db.query('CREATE TABLE users (id SERIAL PRIMARY KEY)');
// ✅ PostgreSQL-specific features work
await db.query('CREATE INDEX CONCURRENTLY idx_email ON users(email)');
},
async down(db, info, handler) {
await db.query('DROP TABLE users');
}
};
export default migration;
Step 4: Type Your Executor
Update the constructor call and add the generic parameter:
BEFORE (v0.5.x)
import { MigrationScriptExecutor, Config } from '@migration-script-runner/core';
// Old constructor signature
const executor = new MigrationScriptExecutor({ handler }, config);
AFTER (v0.6.0)
import { MigrationScriptExecutor, Config } from '@migration-script-runner/core';
import { IPostgresDB } from './types';
// New constructor signature with generic type parameter
const executor = new MigrationScriptExecutor<IPostgresDB>({ handler }, config);
// Now executor knows about PostgreSQL-specific types throughout
Enhanced Type Guards
v0.6.0 enhances the transaction type guard functions to preserve your specific database type while checking for transaction support.
isImperativeTransactional
BEFORE (v0.5.0)
import { isImperativeTransactional } from '@migration-script-runner/core';
const db: IPostgresDB = new PostgresDB(pool);
if (isImperativeTransactional(db)) {
// db is narrowed to ITransactionalDB
// ❌ Lost PostgreSQL-specific methods!
await db.beginTransaction();
await (db as any).query('SELECT * FROM users'); // Have to cast!
}
AFTER (v0.6.0)
import { isImperativeTransactional } from '@migration-script-runner/core';
const db: IPostgresDB = new PostgresDB(pool);
if (isImperativeTransactional(db)) {
// db is now: IPostgresDB & ITransactionalDB
// ✅ Has BOTH PostgreSQL methods AND transaction methods
await db.beginTransaction();
await db.query('SELECT * FROM users'); // ✅ No cast needed!
await db.commit();
}
isCallbackTransactional
BEFORE (v0.5.0)
import { isCallbackTransactional } from '@migration-script-runner/core';
import { Transaction } from '@google-cloud/firestore';
const db: IFirestoreDB = new FirestoreDB(firestore);
if (isCallbackTransactional<Transaction>(db)) {
// db is narrowed to ICallbackTransactionalDB<Transaction>
// ❌ Lost Firestore-specific methods!
await db.runTransaction(async (tx) => {
// Have to access firestore instance separately
});
}
AFTER (v0.6.0)
import { isCallbackTransactional } from '@migration-script-runner/core';
import { Transaction } from '@google-cloud/firestore';
const db: IFirestoreDB = new FirestoreDB(firestore);
if (isCallbackTransactional<IFirestoreDB, Transaction>(db)) {
// db is now: IFirestoreDB & ICallbackTransactionalDB<Transaction>
// ✅ Has BOTH Firestore methods AND transaction methods
await db.runTransaction(async (tx) => {
const docRef = db.collection('users').doc('user1'); // ✅ Works!
const doc = await tx.get(docRef);
tx.update(docRef, { migrated: true });
});
}
Real-World Examples
PostgreSQL Migration with Full Type Safety
// types.ts
import { ITransactionalDB } from '@migration-script-runner/core';
import { Pool, QueryResult } from 'pg';
export interface IPostgresDB extends ITransactionalDB {
query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>>;
copy(sql: string): Promise<void>;
}
// db.ts
export class PostgresDB implements IPostgresDB {
constructor(private pool: Pool) {}
async checkConnection(): Promise<boolean> {
try {
await this.pool.query('SELECT 1');
return true;
} catch {
return false;
}
}
async beginTransaction(): Promise<void> {
await this.pool.query('BEGIN');
}
async commit(): Promise<void> {
await this.pool.query('COMMIT');
}
async rollback(): Promise<void> {
await this.pool.query('ROLLBACK');
}
async query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>> {
return this.pool.query<T>(sql, params);
}
async copy(sql: string): Promise<void> {
const client = await this.pool.connect();
try {
const stream = client.query(sql);
// Handle COPY operation
} finally {
client.release();
}
}
}
// handler.ts
import { IDatabaseMigrationHandler, ISchemaVersion, IBackup } from '@migration-script-runner/core';
import { IPostgresDB } from './types';
class PostgreSQLHandler implements IDatabaseMigrationHandler<IPostgresDB> {
db: IPostgresDB;
schemaVersion: ISchemaVersion<IPostgresDB>;
backup?: IBackup<IPostgresDB>;
constructor(db: IPostgresDB, schemaVersion: ISchemaVersion<IPostgresDB>) {
this.db = db;
this.schemaVersion = schemaVersion;
}
getName(): string {
return 'PostgreSQL Handler';
}
getVersion(): string {
return '1.0.0';
}
}
// migration: V1234567890000_create_users_table.ts
import { IRunnableScript, IMigrationInfo, IDatabaseMigrationHandler } from '@migration-script-runner/core';
import { IPostgresDB } from './types';
const migration: IRunnableScript<IPostgresDB> = {
async up(db, info, handler) {
// ✅ Full autocomplete and type checking
await db.query(`
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// ✅ PostgreSQL-specific COPY command
await db.copy('COPY users FROM STDIN');
// ✅ Concurrent index creation
await db.query('CREATE INDEX CONCURRENTLY idx_email ON users(email)');
},
async down(db, info, handler) {
await db.query('DROP TABLE IF EXISTS users');
}
};
export default migration;
// executor.ts
import { MigrationScriptExecutor, Config } from '@migration-script-runner/core';
import { IPostgresDB } from './types';
import { PostgreSQLHandler } from './handler';
import { PostgresDB } from './db';
const db = new PostgresDB(pool);
const handler = new PostgreSQLHandler(db, schemaVersionImpl);
const config = new Config();
const executor = new MigrationScriptExecutor<IPostgresDB>({ handler }, config);
await executor.up();
New Feature: Metrics Collection
v0.6.0 introduces metrics collection for observability and performance monitoring. This is an optional feature that can be adopted at any time.
Quick Start
Add metrics collectors to track migration performance:
import {
MigrationScriptExecutor,
ConsoleMetricsCollector,
JsonMetricsCollector
} from '@migration-script-runner/core';
const executor = new MigrationScriptExecutor<IPostgresDB>({
handler,
metricsCollectors: [
new ConsoleMetricsCollector(), // Real-time console output
new JsonMetricsCollector({ // Detailed JSON reports
filePath: './metrics/migration.json'
})
]
}, config);
await executor.up();
Console output:
[METRICS] Migration started - 3 pending scripts
[METRICS] V1_CreateUsers completed in 823ms
[METRICS] V2_AddEmail completed in 645ms
[METRICS] Migration completed - 3 scripts in 1468ms (success)
Built-in Collectors
v0.6.0 includes four production-ready collectors:
| Collector | Use Case | Configuration |
|---|---|---|
| ConsoleMetricsCollector | Development, debugging | Zero config |
| LoggerMetricsCollector | Production, cloud logging | Requires ILogger |
| JsonMetricsCollector | Analysis, detailed debugging | File path |
| CsvMetricsCollector | Excel, historical tracking | File path |
When to Use
Add metrics collection if you:
- Need to monitor migration performance
- Want to track execution times in production
- Need to debug slow migrations
- Want historical data for analysis
- Need integration with monitoring tools (Datadog, CloudWatch, etc.)
Skip metrics collection if:
- You’re just starting with MSR
- Your migrations are simple and fast
- You don’t need performance tracking
Learn More
For complete documentation on metrics collection:
Benefits Summary
For TypeScript Users
✅ IDE Autocomplete - Full IntelliSense for database-specific methods ✅ Compile-Time Errors - Catch typos and method errors before runtime ✅ Refactoring Support - Safe renames and API changes ✅ Self-Documenting - Types show exactly what methods are available ✅ No More Casting - Eliminate as any and type assertions
For JavaScript Users
✅ Better JSDoc - More accurate autocomplete in VS Code ✅ Runtime Safety - Same runtime behavior, better development experience ✅ Simple Update - Just update constructor calls, no type annotations needed
Common Patterns
Pattern 1: Gradual Type Enhancement
Start with <IDB> (minimum requirement), then enhance to database-specific types:
// Step 1: Minimum requirement - use <IDB>
const basicMigration: IRunnableScript<IDB> = { /* ... */ };
// Step 2: Later, enhance to database-specific type
const typedMigration: IRunnableScript<IPostgresDB> = { /* ... */ };
// Both work together - enhance incrementally!
Pattern 2: Shared Database Type
Create a shared types file for your entire project:
// types/database.ts
import { ITransactionalDB } from '@migration-script-runner/core';
export interface IAppDatabase extends ITransactionalDB {
query(sql: string): Promise<any>;
// Add all your database methods here
}
// Use everywhere:
import { IAppDatabase } from './types/database';
const migration: IRunnableScript<IAppDatabase> = { /* ... */ };
Pattern 3: Multiple Database Support
Support multiple databases with union types:
import { IPostgresDB, IMySQLDB } from './types';
type SupportedDB = IPostgresDB | IMySQLDB;
const migration: IRunnableScript<SupportedDB> = {
async up(db, info, handler) {
// TypeScript knows methods available on BOTH databases
await db.query('SELECT 1');
}
};
Troubleshooting
Issue: “Generic type ‘IDatabaseMigrationHandler’ requires 1 type argument(s)”
Cause: You didn’t specify the required type parameter.
Solution: Add <IDB> or your custom database type:
// ❌ Error: Missing type parameter
class MyHandler implements IDatabaseMigrationHandler {
// ✅ Fixed: Type parameter specified
class MyHandler implements IDatabaseMigrationHandler<IDB> {
Issue: “Type ‘IDB’ is missing properties”
Cause: Your database class doesn’t implement the full interface.
Solution: Implement missing methods or use type assertions for custom databases:
const executor = new MigrationScriptExecutor<IDB>({ handler }, config);
// Use IDB (base) instead of specific type if not fully implemented
Issue: “Cannot find name ‘IPostgresDB’”
Cause: Database-specific interfaces are not included in MSR core.
Solution: Define your own interfaces:
import { IDB } from '@migration-script-runner/core';
interface IPostgresDB extends IDB {
query(sql: string): Promise<any>;
}
Issue: Autocomplete not working
Cause: TypeScript may not infer the generic automatically.
Solution: Explicitly specify the type parameter:
const migration: IRunnableScript<IPostgresDB> = {
// Now autocomplete works!
};
FAQ
Q: Do I need to update my v0.5.x code?
A: Yes, you must:
- Add
<IDB>type parameters to all interface implementations (interfaces, classes implementing them) - Update
MigrationScriptExecutorconstructor calls to use the new{ handler }syntax
This is typically a 10-30 minute task depending on your codebase size.
Q: What if I don’t use TypeScript?
A: You only need to update the constructor call from new MigrationScriptExecutor({ handler }, config) to new MigrationScriptExecutor({ handler }, config). The generic type parameters are TypeScript-only and don’t affect JavaScript users.
Q: Will this affect runtime performance?
A: No. Generic types are TypeScript compile-time features - they’re erased at runtime. Zero performance impact.
Q: Can I use generics with SQL files (.up.sql)?
A: SQL migrations don’t benefit from generics (they’re not TypeScript), but your handler can still be typed.
Q: Should I type all migrations at once?
A: No need. Adopt incrementally - start with new migrations, update old ones as needed.
Q: Should I use metrics collection?
A: It’s optional. Add metrics collection if you need to monitor performance, debug slow migrations, or track execution times in production. Start with ConsoleMetricsCollector for development, then add JsonMetricsCollector or LoggerMetricsCollector for production monitoring.
Summary
v0.6.0 adds generic type parameters for enhanced type safety and metrics collection for observability, with two breaking changes:
- 🔨 Type parameters required - Must add
<IDB>to all interface implementations (10-30 min task) - 🔨 Constructor update required - Change from
(handler, config)to({ handler }, config?) - ✅ Optional database-specific types - Can enhance from
<IDB>to<IPostgresDB>etc. incrementally - ✅ Optional metrics collection - Add ConsoleMetricsCollector, JsonMetricsCollector, etc. via
metricsCollectorsarray - ✅ Better developer experience - Full autocomplete and compile-time validation
- ✅ No runtime impact - Types are compile-time only
Recommendation: Update to v0.6.0, add <IDB> type parameters and fix constructor calls (required), then adopt database-specific types and metrics collection incrementally as needed.
Next Steps
- Update package:
npm install @migration-script-runner/core@^0.6.0 - Add type parameters: Add
<IDB>to allIDatabaseMigrationHandler,IRunnableScript,ISchemaVersion, etc. implementations - Update constructor calls: Change
new MigrationScriptExecutor({ handler }, config)tonew MigrationScriptExecutor({ handler }, config) - Verify tests pass:
npm test - Optional: Define your database-specific interface (e.g.,
IPostgresDB) - Optional: Enhance type parameters from
<IDB>to<IPostgresDB>for better type safety - Optional: Add metrics collection for observability
For detailed documentation, see: