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

  1. Overview
    1. What’s New in v0.6.0
    2. Migration Effort
  2. Prerequisites
  3. Upgrade Steps
    1. 1. Update Package
    2. 2. Add Type Parameters to Interfaces
      1. BEFORE (v0.5.x):
      2. AFTER (v0.6.0):
    3. 3. Update Constructor Calls
      1. BEFORE (v0.5.x):
      2. AFTER (v0.6.0):
    4. 4. Run Your Test Suite
    5. 5. Optional: Adopt Database-Specific Type Safety
  4. Breaking Changes
    1. 1. Constructor Signature Change
    2. 2. Generic Type Parameters (BREAKING)
  5. What Are Generic Type Parameters?
    1. The Problem (v0.5.x)
    2. The Solution (v0.6.0)
  6. Type Safety Adoption Guide
    1. Step 1: Define Your Database Interface
    2. Step 2: Type Your Handler
      1. BEFORE (v0.5.x)
      2. AFTER (v0.6.0)
    3. Step 3: Type Your Migrations
      1. BEFORE (v0.5.x)
      2. AFTER (v0.6.0)
    4. Step 4: Type Your Executor
      1. BEFORE (v0.5.x)
      2. AFTER (v0.6.0)
  7. Enhanced Type Guards
    1. isImperativeTransactional
      1. BEFORE (v0.5.0)
      2. AFTER (v0.6.0)
    2. isCallbackTransactional
      1. BEFORE (v0.5.0)
      2. AFTER (v0.6.0)
  8. Real-World Examples
    1. PostgreSQL Migration with Full Type Safety
  9. New Feature: Metrics Collection
    1. Quick Start
    2. Built-in Collectors
    3. When to Use
    4. Learn More
  10. Benefits Summary
    1. For TypeScript Users
    2. For JavaScript Users
  11. Common Patterns
    1. Pattern 1: Gradual Type Enhancement
    2. Pattern 2: Shared Database Type
    3. Pattern 3: Multiple Database Support
  12. Troubleshooting
    1. Issue: “Generic type ‘IDatabaseMigrationHandler’ requires 1 type argument(s)”
    2. Issue: “Type ‘IDB’ is missing properties”
    3. Issue: “Cannot find name ‘IPostgresDB’”
    4. Issue: Autocomplete not working
  13. FAQ
    1. Q: Do I need to update my v0.5.x code?
    2. Q: What if I don’t use TypeScript?
    3. Q: Will this affect runtime performance?
    4. Q: Can I use generics with SQL files (.up.sql)?
    5. Q: Should I type all migrations at once?
    6. Q: Should I use metrics collection?
  14. Summary
  15. 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>() and isCallbackTransactional<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 dependencies object 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 any casting 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:

  1. Add <IDB> type parameters to all interface implementations (interfaces, classes implementing them)
  2. Update MigrationScriptExecutor constructor 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 metricsCollectors array
  • 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

  1. Update package: npm install @migration-script-runner/core@^0.6.0
  2. Add type parameters: Add <IDB> to all IDatabaseMigrationHandler, IRunnableScript, ISchemaVersion, etc. implementations
  3. Update constructor calls: Change new MigrationScriptExecutor({ handler }, config) to new MigrationScriptExecutor({ handler }, config)
  4. Verify tests pass: npm test
  5. Optional: Define your database-specific interface (e.g., IPostgresDB)
  6. Optional: Enhance type parameters from <IDB> to <IPostgresDB> for better type safety
  7. Optional: Add metrics collection for observability

For detailed documentation, see: