Built-in Validation

Complete guide to MSR’s built-in validation rules

Table of contents

  1. Overview
  2. Validation Categories
    1. 1. Structural Validation
      1. DEFAULT_EXPORT_NOT_FOUND
      2. INSTANTIATION_FAILED
    2. 2. Interface Validation
      1. MISSING_UP_METHOD
      2. INVALID_UP_SIGNATURE
      3. INVALID_DOWN_SIGNATURE
    3. 3. down() Method Validation
      1. MISSING_DOWN_METHOD
    4. 4. Integrity Validation
      1. MIGRATED_FILE_MISSING
      2. MIGRATED_FILE_MODIFIED
    5. 5. Transaction Configuration Validation
      1. Database Does Not Support Transactions
      2. Isolation Level Not Supported
      3. Rollback Strategy Incompatibility
      4. Transaction Timeout Warning
  3. Validation Flow
  4. Configuration Impact
    1. validateBeforeRun
    2. strictValidation
    3. downMethodPolicy
  5. Error Messages
    1. Example: MISSING_UP_METHOD
    2. Example: MIGRATED_FILE_MODIFIED
  6. Testing Validation
    1. Test Your Migrations
    2. Unit Test Validation
  7. Best Practices
    1. 1. Always Keep Validation Enabled
    2. 2. Use Strict Mode in CI/CD
    3. 3. Never Modify Executed Migrations
    4. 4. Keep Migration Files in Version Control
    5. 5. Test Migrations Before Deployment
  8. Troubleshooting
    1. Problem: Validation passes locally but fails in CI
    2. Problem: INSTANTIATION_FAILED error
    3. Problem: False positive on MIGRATED_FILE_MODIFIED
  9. Reference
    1. All Built-in Validation Error Codes

Overview

Migration Script Runner performs comprehensive built-in validation on all migration scripts before execution. This validation ensures scripts follow the correct structure, implement required methods, and maintain database integrity.

Built-in validation is automatic and runs when validateBeforeRun = true (default).


Validation Categories

1. Structural Validation

Validates the migration file structure and exports.

DEFAULT_EXPORT_NOT_FOUND

Error Code: DEFAULT_EXPORT_NOT_FOUND Type: ERROR When: Migration file doesn’t export a default class

Example Problem:

// ❌ Wrong - named export
export class MyMigration {
    async up() { }
}

Solution:

// ✅ Correct - default export
export default class MyMigration {
    async up(db, info, handler): Promise<string> {
        return 'Success';
    }
}

INSTANTIATION_FAILED

Error Code: INSTANTIATION_FAILED Type: ERROR When: Cannot create an instance of the migration class

Common Causes:

  • Constructor throws an error
  • Constructor requires parameters
  • Syntax errors in the class

Example Problem:

// ❌ Wrong - constructor with required parameters
export default class MyMigration {
    constructor(private config: Config) {
        // MSR cannot instantiate this
    }

    async up() { }
}

Solution:

// ✅ Correct - no-arg constructor or no constructor
export default class MyMigration {
    async up(db, info, handler): Promise<string> {
        return 'Success';
    }
}

2. Interface Validation

Validates method signatures match the IRunnableScript interface.

MISSING_UP_METHOD

Error Code: MISSING_UP_METHOD Type: ERROR When: Migration class doesn’t have an up() method

Example Problem:

// ❌ Wrong - no up() method
export default class MyMigration {
    async migrate() {
        // Wrong method name
    }
}

Solution:

// ✅ Correct - up() method exists
export default class MyMigration {
    async up(db, info, handler): Promise<string> {
        // Implementation here
        return 'Migration completed';
    }
}

INVALID_UP_SIGNATURE

Error Code: INVALID_UP_SIGNATURE Type: ERROR When: up() method has incorrect parameters or return type

Required Signature:

async up(
    db: IDB,
    info: IMigrationInfo,
    handler: IDatabaseMigrationHandler
): Promise<string>

Example Problems:

// ❌ Wrong - missing parameters
export default class MyMigration {
    async up(): Promise<string> {
        return 'Success';
    }
}

// ❌ Wrong - wrong return type
export default class MyMigration {
    async up(db, info, handler): Promise<void> {
        // Should return Promise<string>
    }
}

// ❌ Wrong - not async
export default class MyMigration {
    up(db, info, handler): string {
        return 'Success';
    }
}

Solution:

// ✅ Correct signature
export default class MyMigration {
    async up(
        db: IDB,
        info: IMigrationInfo,
        handler: IDatabaseMigrationHandler
    ): Promise<string> {
        // Your migration logic
        return 'Migration completed successfully';
    }
}

Parameters:

  • db - Database connection/interface for executing queries
  • info - Migration metadata (timestamp, name, etc.)
  • handler - Database handler with schema version and backup capabilities

Return Value:

  • Must return Promise<string> with a success message
  • The message is stored in the schema version table

INVALID_DOWN_SIGNATURE

Error Code: INVALID_DOWN_SIGNATURE Type: ERROR When: down() method exists but has incorrect signature

Required Signature:

async down(
    db: IDB,
    info: IMigrationInfo,
    handler: IDatabaseMigrationHandler
): Promise<string>

Example Problem:

// ❌ Wrong - incorrect parameters
export default class MyMigration {
    async up(db, info, handler): Promise<string> {
        return 'Up';
    }

    async down(): Promise<string> {
        // Missing required parameters
        return 'Down';
    }
}

Solution:

// ✅ Correct - same signature as up()
export default class MyMigration {
    async up(db, info, handler): Promise<string> {
        await db.execute('CREATE TABLE users (id INT)');
        return 'Table created';
    }

    async down(db, info, handler): Promise<string> {
        await db.execute('DROP TABLE users');
        return 'Table dropped';
    }
}

3. down() Method Validation

Validates presence of down() method based on configuration policy.

MISSING_DOWN_METHOD

Error Code: MISSING_DOWN_METHOD Type: ERROR or WARNING (depends on policy) When: down() method is missing and policy requires/recommends it

Controlled by: config.downMethodPolicy

Policy Behavior:

Policy Strategy Severity
AUTO + DOWN down() required ERROR
AUTO + BOTH down() recommended WARNING
AUTO + BACKUP/NONE No check -
REQUIRED Always required ERROR
RECOMMENDED Always recommended WARNING
OPTIONAL Never checked -

Example Configuration:

import { DownMethodPolicy, RollbackStrategy } from '@migration-script-runner/core';

// Require down() for DOWN rollback strategy
config.rollbackStrategy = RollbackStrategy.DOWN;
config.downMethodPolicy = DownMethodPolicy.AUTO;  // Will error if down() missing

// Always require down()
config.downMethodPolicy = DownMethodPolicy.REQUIRED;

// Recommend but don't enforce
config.downMethodPolicy = DownMethodPolicy.RECOMMENDED;

// Never check
config.downMethodPolicy = DownMethodPolicy.OPTIONAL;

Solution:

// Add down() method to your migration
export default class MyMigration {
    async up(db, info, handler): Promise<string> {
        await db.execute('ALTER TABLE users ADD COLUMN email VARCHAR(255)');
        return 'Column added';
    }

    async down(db, info, handler): Promise<string> {
        await db.execute('ALTER TABLE users DROP COLUMN email');
        return 'Column removed';
    }
}

4. Integrity Validation

Validates already-executed migrations haven’t been modified.

MIGRATED_FILE_MISSING

Error Code: MIGRATED_FILE_MISSING Type: ERROR When: A previously executed migration file no longer exists

Cause:

  • Migration file was deleted
  • File was moved to a different location
  • File was renamed

Example Scenario:

# Database shows this migration was executed:
schema_version:
  timestamp: 202501220100
  name: V202501220100_create_users.ts
  executed_at: 2025-01-22 10:30:00

# But the file is missing:
migrations/
  ├── V202501220200_add_roles.ts  ✅ exists
  └── V202501220100_create_users.ts  ❌ MISSING

Impact:

  • Cannot verify migration integrity
  • Cannot perform rollback operations
  • Deployment history is incomplete

Solution:

# Restore the file from version control
git checkout HEAD -- migrations/V202501220100_create_users.ts

# Or restore from backup
cp backups/V202501220100_create_users.ts migrations/

Never delete executed migrations! Always keep migration files in version control, even after they’ve been executed in production.


MIGRATED_FILE_MODIFIED

Error Code: MIGRATED_FILE_MODIFIED Type: ERROR When: A previously executed migration file has been changed

Cause:

  • File content was edited after execution
  • Checksum no longer matches database record

Example Scenario:

// Original migration (executed in production):
export default class CreateUsers {
    async up(db, info, handler): Promise<string> {
        await db.execute('CREATE TABLE users (id INT)');
        return 'Created';
    }
}

// Someone modified it (❌ DON'T DO THIS):
export default class CreateUsers {
    async up(db, info, handler): Promise<string> {
        await db.execute('CREATE TABLE users (id INT, name VARCHAR(255))');
        //                                           ^^^ MODIFIED
        return 'Created';
    }
}

Detection:

❌ MIGRATED_FILE_MODIFIED: V202501220100_create_users.ts
   Expected checksum: abc123def456
   Actual checksum:   xyz789ghi012

Impact:

  • Environment divergence (dev vs prod have different migration code)
  • Rollback operations may fail
  • Database state inconsistency

Solution:

# Revert the changes
git checkout HEAD -- migrations/V202501220100_create_users.ts

# If you need to make changes, create a NEW migration
# migrations/V202501220300_add_name_to_users.ts
export default class AddNameToUsers {
    async up(db, info, handler): Promise<string> {
        await db.execute('ALTER TABLE users ADD COLUMN name VARCHAR(255)');
        return 'Column added';
    }
}

Never modify executed migrations! Always create new migrations for schema changes.


5. Transaction Configuration Validation

New in v0.5.0

Validates transaction configuration compatibility and warns about potential issues.

Database Does Not Support Transactions

Error Code: IMPORT_FAILED Type: ERROR When: Transaction mode is enabled but database doesn’t implement transaction interfaces

Cause:

  • Database handler doesn’t implement ITransactionalDB or ICallbackTransactionalDB
  • Transaction mode is set to PER_MIGRATION or PER_BATCH

Example Problem:

const config = new Config();
config.transaction.mode = TransactionMode.PER_MIGRATION;  // Requires ITransactionalDB

class MyDB implements IDB {
  async checkConnection(): Promise<boolean> { return true; }
  // ❌ Missing: beginTransaction(), commit(), rollback()
}

Error Message:

❌ Transaction configuration validation failed:

  ❌ Database does not support transactions, but transaction mode is PER_MIGRATION
     Database must implement ITransactionalDB or ICallbackTransactionalDB.
     Set transaction.mode to NONE or implement transaction methods.

Solution:

import { ITransactionalDB } from '@migration-script-runner/core';

// Option 1: Implement ITransactionalDB
class MyDB implements ITransactionalDB {
  async checkConnection(): Promise<boolean> { return true; }

  async beginTransaction(): Promise<void> {
    await this.client.query('BEGIN');
  }

  async commit(): Promise<void> {
    await this.client.query('COMMIT');
  }

  async rollback(): Promise<void> {
    await this.client.query('ROLLBACK');
  }
}

// Option 2: Disable transaction mode
config.transaction.mode = TransactionMode.NONE;

Isolation Level Not Supported

Error Code: MISSING_DOWN_WITH_BOTH_STRATEGY (repurposed for warnings) Type: WARNING When: Isolation level configured but database may not support setIsolationLevel()

Cause:

  • config.transaction.isolation is set
  • Database doesn’t have setIsolationLevel() method

Example Scenario:

const config = new Config();
config.transaction.mode = TransactionMode.PER_MIGRATION;
config.transaction.isolation = IsolationLevel.SERIALIZABLE;

class MyDB implements ITransactionalDB {
  async beginTransaction(): Promise<void> { /* ... */ }
  async commit(): Promise<void> { /* ... */ }
  async rollback(): Promise<void> { /* ... */ }
  // ⚠️  Missing: setIsolationLevel(level: string): Promise<void>
}

Warning Message:

⚠️  Transaction configuration warnings:

  ⚠️  Isolation level SERIALIZABLE is configured, but database may not support setIsolationLevel()
      Verify your database adapter implements setIsolationLevel() method.
      Isolation level may be ignored.

Solution:

class MyDB implements ITransactionalDB {
  // ... other methods ...

  async setIsolationLevel(level: string): Promise<void> {
    if (!this.client) throw new Error('Call beginTransaction() first');
    await this.client.query(`SET TRANSACTION ISOLATION LEVEL ${level}`);
  }
}

Rollback Strategy Incompatibility

Error Code: MISSING_DOWN_WITH_BOTH_STRATEGY (repurposed for warnings) Type: WARNING When: Using PER_BATCH mode with DOWN rollback strategy

Cause:

  • Transaction mode is PER_BATCH
  • Rollback strategy is DOWN
  • Individual migration down() methods cannot rollback within a batch transaction

Example Problem:

const config = new Config();
config.transaction.mode = TransactionMode.PER_BATCH;
config.rollbackStrategy = RollbackStrategy.DOWN;
// ⚠️  Incompatible combination

Warning Message:

⚠️  Transaction configuration warnings:

  ⚠️  Rollback strategy DOWN is not fully compatible with PER_BATCH transaction mode
      If a migration fails in PER_BATCH mode, the entire batch transaction will rollback.
      Individual migration down() methods cannot rollback within the batch.
      Consider using BACKUP or BOTH strategy with PER_BATCH mode.

Explanation:

PER_BATCH transaction:
BEGIN
  Migration 1 ✓
  Migration 2 ✓
  Migration 3 ✗ (fails)
ROLLBACK  ← Database automatically rolls back entire batch
          ← down() methods cannot execute inside rolled-back transaction

Solutions:

// Option 1: Use BACKUP strategy with PER_BATCH
config.transaction.mode = TransactionMode.PER_BATCH;
config.rollbackStrategy = RollbackStrategy.BACKUP;

// Option 2: Use BOTH strategy (backup + down)
config.transaction.mode = TransactionMode.PER_BATCH;
config.rollbackStrategy = RollbackStrategy.BOTH;

// Option 3: Use PER_MIGRATION with DOWN
config.transaction.mode = TransactionMode.PER_MIGRATION;
config.rollbackStrategy = RollbackStrategy.DOWN;

Transaction Timeout Warning

Error Code: MISSING_DOWN_WITH_BOTH_STRATEGY (repurposed for warnings) Type: WARNING When: Many migrations in PER_BATCH mode with timeout configured, or very long batch without timeout

Causes:

  • With timeout: > 10 migrations in PER_BATCH mode with transaction.timeout set
  • Without timeout: > 20 migrations in PER_BATCH mode without timeout

Example Scenarios:

Scenario 1: Timeout Risk

const config = new Config();
config.transaction.mode = TransactionMode.PER_BATCH;
config.transaction.timeout = 5000;  // 5 seconds

// 15 pending migrations to execute in single transaction

Warning Message:

⚠️  Transaction configuration warnings:

  ⚠️  15 migrations will execute in a single transaction (PER_BATCH mode)
      Transaction timeout is 5000ms. If migrations take too long, transaction may timeout.
      Consider using PER_MIGRATION mode or increasing timeout.

Scenario 2: Long-Running Transaction

const config = new Config();
config.transaction.mode = TransactionMode.PER_BATCH;
// No timeout configured

// 25 pending migrations to execute in single transaction

Warning Message:

⚠️  Transaction configuration warnings:

  ⚠️  25 migrations will execute in a single long-running transaction
      Consider setting transaction.timeout or using PER_MIGRATION mode
      to avoid holding locks for extended periods.

Solutions:

// Option 1: Use PER_MIGRATION mode for many migrations
config.transaction.mode = TransactionMode.PER_MIGRATION;

// Option 2: Increase timeout
config.transaction.timeout = 30000;  // 30 seconds

// Option 3: Set reasonable timeout
config.transaction.timeout = 10000;  // 10 seconds

// Option 4: Run migrations in smaller batches
await executor.up(202501220500);  // First batch
await executor.up(202501220800);  // Second batch

Validation Flow

Built-in validation runs in this order:

1. File Loading
   ↓
2. Structural Validation
   - DEFAULT_EXPORT_NOT_FOUND
   - INSTANTIATION_FAILED
   ↓
3. Interface Validation
   - MISSING_UP_METHOD
   - INVALID_UP_SIGNATURE
   - INVALID_DOWN_SIGNATURE
   ↓
4. down() Method Validation
   - MISSING_DOWN_METHOD (based on policy)
   ↓
5. Integrity Validation (for migrated scripts)
   - MIGRATED_FILE_MISSING
   - MIGRATED_FILE_MODIFIED
   ↓
6. Transaction Configuration Validation (v0.5.0+)
   - Database transaction support check
   - Isolation level compatibility
   - Rollback strategy compatibility
   - Transaction timeout warnings
   ↓
7. Custom Validators (if configured)

If any validation fails with an ERROR:

  • Migration execution stops
  • ValidationError is thrown
  • No database changes occur
  • No backup is created

Configuration Impact

validateBeforeRun

Controls whether built-in validation runs:

// Validation enabled (default)
config.validateBeforeRun = true;  // ✅ Recommended

// Validation disabled
config.validateBeforeRun = false;  // ⚠️  Dangerous - only for debugging

When false:

  • No structural validation - malformed scripts may cause runtime errors
  • No interface validation - incorrect signatures may crash migrations
  • No integrity validation - modified migrations won’t be detected
  • ⚠️ Only disable for local debugging

strictValidation

Controls whether warnings become errors:

// Warnings allowed (default)
config.strictValidation = false;

// Warnings block execution
config.strictValidation = true;

When true:

  • MISSING_DOWN_METHOD warnings become errors (if policy is RECOMMENDED)
  • Custom validator warnings block execution
  • Useful for CI/CD to enforce strict quality

Example:

// In CI/CD, use strict mode
if (process.env.CI === 'true') {
    config.strictValidation = true;
    config.downMethodPolicy = DownMethodPolicy.REQUIRED;
}

downMethodPolicy

Controls down() method validation:

import { DownMethodPolicy } from '@migration-script-runner/core';

// Auto-detect based on rollback strategy (default)
config.downMethodPolicy = DownMethodPolicy.AUTO;

// Always require down()
config.downMethodPolicy = DownMethodPolicy.REQUIRED;

// Always warn about missing down()
config.downMethodPolicy = DownMethodPolicy.RECOMMENDED;

// Never check for down()
config.downMethodPolicy = DownMethodPolicy.OPTIONAL;

Recommendations:

Environment Recommended Policy
Production AUTO or RECOMMENDED
CI/CD REQUIRED + strictValidation = true
Development AUTO or RECOMMENDED
Testing OPTIONAL (if using backup strategy)

Error Messages

Built-in validation provides detailed error messages:

Example: MISSING_UP_METHOD

❌ Validation Error: MISSING_UP_METHOD

Migration: V202501220100_create_users.ts
Issue: The migration class must implement an up() method

Expected signature:
  async up(db: IDB, info: IMigrationInfo, handler: IDatabaseMigrationHandler): Promise<string>

Fix: Add the up() method to your migration class.

Example: MIGRATED_FILE_MODIFIED

❌ Validation Error: MIGRATED_FILE_MODIFIED

Migration: V202501220100_create_users.ts
Executed: 2025-01-22 10:30:00

Checksum mismatch:
  Expected: abc123def456789
  Actual:   xyz987ghi654321

Impact: This migration was previously executed but the file has been modified.
        This may cause inconsistency between environments.

Fix: Revert the changes to this file. Create a new migration for schema changes.

Testing Validation

Test Your Migrations

Run validation without executing:

import { MigrationScriptExecutor, ValidationError } from '@migration-script-runner/core';

const executor = new MigrationScriptExecutor({ handler }, config);

try {
    // This will validate but not execute if you catch the error early
    const result = await executor.migrate();
} catch (error) {
    if (error instanceof ValidationError) {
        console.log('Validation failed (as expected for testing)');
        console.log(`Errors: ${error.errorCount}`);
        console.log(`Warnings: ${error.warningCount}`);

        error.validationResults.forEach(result => {
            if (!result.valid) {
                console.log(`\n${result.script.name}:`);
                result.issues.forEach(issue => {
                    console.log(`  [${issue.code}] ${issue.message}`);
                });
            }
        });
    }
}

Unit Test Validation

import { expect } from 'chai';
import { MigrationValidationService } from '@migration-script-runner/core';

describe('Migration Validation', () => {
    it('should validate migration structure', async () => {
        const service = new MigrationValidationService();
        const result = await service.validateOne(script, config);

        expect(result.valid).to.be.true;
        expect(result.issues).to.have.lengthOf(0);
    });

    it('should detect missing up() method', async () => {
        const service = new MigrationValidationService();
        const result = await service.validateOne(badScript, config);

        expect(result.valid).to.be.false;
        expect(result.issues[0].code).to.equal('MISSING_UP_METHOD');
    });
});

Best Practices

1. Always Keep Validation Enabled

// ✅ Good - validation enabled
config.validateBeforeRun = true;

// ❌ Bad - only disable for debugging
config.validateBeforeRun = false;

2. Use Strict Mode in CI/CD

if (process.env.CI) {
    config.strictValidation = true;
    config.downMethodPolicy = DownMethodPolicy.REQUIRED;
}

3. Never Modify Executed Migrations

# ✅ Good - create new migration
git add migrations/V202501220300_add_email_column.ts

# ❌ Bad - modifying executed migration
git add migrations/V202501220100_create_users.ts  # Already executed!

4. Keep Migration Files in Version Control

# ✅ Good - track migrations
migrations/**/*.ts

# ❌ Bad - don't ignore migrations
# migrations/

5. Test Migrations Before Deployment

# Run migrations in dev/staging first
npm run migrate

# Validation will catch issues before production

Troubleshooting

Problem: Validation passes locally but fails in CI

Cause: File modifications or missing files

Solution:

# Ensure all migrations are committed
git status migrations/

# Check for uncommitted changes
git diff migrations/

Problem: INSTANTIATION_FAILED error

Cause: Constructor throws an error or requires parameters

Solution:

// Remove constructor or make it parameter-free
export default class MyMigration {
    // No constructor, or:
    constructor() {
        // Don't throw errors here
    }
}

Problem: False positive on MIGRATED_FILE_MODIFIED

Cause: Line ending differences (CRLF vs LF)

Solution:

# Normalize line endings
git config core.autocrlf input

# Re-checkout files
git checkout -- migrations/

Reference

All Built-in Validation Error Codes

Code Type Description
DEFAULT_EXPORT_NOT_FOUND ERROR Missing default export
INSTANTIATION_FAILED ERROR Cannot create class instance
MISSING_UP_METHOD ERROR No up() method
INVALID_UP_SIGNATURE ERROR Wrong up() signature
INVALID_DOWN_SIGNATURE ERROR Wrong down() signature
MISSING_DOWN_METHOD ERROR/WARNING No down() method (policy-based)
MIGRATED_FILE_MISSING ERROR Executed migration file deleted
MIGRATED_FILE_MODIFIED ERROR Executed migration file changed
IMPORT_FAILED ERROR Database doesn’t support transactions (v0.5.0+)
MISSING_DOWN_WITH_BOTH_STRATEGY WARNING Transaction configuration warnings (v0.5.0+)