Skip to content

Add generic type parameters for database-specific type safety #114

@vlavrynovych

Description

@vlavrynovych

Problem

Currently, MSR uses the generic IDB interface throughout the codebase, which provides limited type safety and forces users to cast to specific database types when accessing database-specific features.

Current Implementation:

interface IDatabaseMigrationHandler {
  db: IDB;  // Generic IDB type - no type safety
  // ...
}

interface IRunnableScript {
  up(db: IDB, info: IMigrationInfo, handler: IDatabaseMigrationHandler): Promise<string>;
  down(db: IDB, info: IMigrationInfo, handler: IDatabaseMigrationHandler): Promise<string>;
}

Issues:

  • ❌ No IDE autocomplete for database-specific methods
  • ❌ Requires as any or type casting in migration scripts
  • ❌ No compile-time validation of database operations
  • ❌ Cannot access PostgreSQL-specific features (COPY, etc.)
  • ❌ Cannot access MySQL-specific features without casting

Proposed Solution

Introduce generic type parameters to make database types strongly typed throughout the interface hierarchy.

Proposed Implementation:

interface IDatabaseMigrationHandler<DB extends IDB = IDB> {
  db: DB;  // Specific database type (IPostgresDB, IMySQLDB, etc.)
  schemaVersion: ISchemaVersion<DB>;
  backup?: IBackup<DB>;
  transactionManager?: ITransactionManager<DB>;
  // ...
}

interface IRunnableScript<DB extends IDB = IDB> {
  up(
    db: DB, 
    info: IMigrationInfo, 
    handler: IDatabaseMigrationHandler<DB>
  ): Promise<string>;
  
  down(
    db: DB, 
    info: IMigrationInfo, 
    handler: IDatabaseMigrationHandler<DB>
  ): Promise<string>;
}

interface ISchemaVersion<DB extends IDB = IDB> {
  migrationRecords: IMigrationRecords<DB>;
  // ...
}

// Usage example:
class PostgreSQLHandler implements IDatabaseMigrationHandler<IPostgresDB> {
  db: IPostgresDB;  // Strongly typed!
  // ...
}

// In migration script:
const migration: IRunnableScript<IPostgresDB> = {
  async up(db, info, handler) {
    // db is IPostgresDB - full autocomplete and type safety!
    await db.query('COPY users FROM ...');  // PostgreSQL-specific
  }
};

Benefits

1. Type Safety

  • ✅ Compile-time validation of database-specific operations
  • ✅ Prevent runtime errors from calling unsupported methods
  • ✅ No more as any casting in migration scripts

2. Developer Experience

  • ✅ Full IDE autocomplete for database-specific methods
  • ✅ Inline documentation for available operations
  • ✅ Better refactoring support

3. Code Quality

  • ✅ Self-documenting code (types show what's available)
  • ✅ Easier to maintain and extend
  • ✅ Catches errors at compile time, not runtime

4. Database-Specific Features

  • ✅ Access PostgreSQL features (COPY, LISTEN/NOTIFY, etc.)
  • ✅ Access MySQL features (specific functions, etc.)
  • ✅ Access SQLite features without casting

Implementation Considerations

1. Backward Compatibility

Challenge: This is a breaking change for existing handlers and migration scripts.

Mitigation:

  • Use default type parameter <DB extends IDB = IDB> for backward compatibility
  • Existing code without generics continues to work with base IDB type
  • Provide migration guide for upgrading to typed versions
  • Consider making this a major version bump (v1.0.0?)

2. Cascade Effect

Challenge: Generic type needs to flow through multiple interfaces.

Affected interfaces:

  • IDatabaseMigrationHandler<DB>
  • IRunnableScript<DB>
  • ISchemaVersion<DB>
  • IMigrationRecords<DB>
  • IBackup<DB>
  • ITransactionManager<DB>
  • MigrationScriptExecutor constructor
  • MigrationScript<DB> class

3. API Complexity

Challenge: Adds complexity to public API.

Considerations:

  • Keep default parameter for simple use cases
  • Document when generics are needed vs optional
  • Provide examples for common patterns
  • Balance type safety vs API simplicity

4. Testing Impact

Challenge: All tests need to be updated.

Scope:

  • Update all test mocks to use generics
  • Test with different database types
  • Verify backward compatibility
  • Maintain 100% coverage

Implementation Phases

Phase 1: Analysis & Design (🔍 Investigation Required)

  • Analyze all interfaces that need generic parameters
  • Map dependencies and cascade effects
  • Design backward-compatible approach
  • Create proof of concept
  • Identify breaking changes vs non-breaking changes
  • Evaluate impact on existing handlers (@migration-script-runner/postgresql, etc.)

Phase 2: Core Interfaces

  • Update IDB and related interfaces
  • Update IDatabaseMigrationHandler<DB>
  • Update IRunnableScript<DB>
  • Update ISchemaVersion<DB>
  • Update tests

Phase 3: Supporting Interfaces

  • Update IMigrationRecords<DB>
  • Update IBackup<DB>
  • Update ITransactionManager<DB>
  • Update MigrationScript<DB>
  • Update tests

Phase 4: Executor & Services

  • Update MigrationScriptExecutor
  • Update all service classes
  • Update loader implementations
  • Update tests

Phase 5: Documentation & Migration

  • Update all API documentation
  • Create migration guide for existing projects
  • Update examples to show typed usage
  • Document when to use generics vs defaults
  • Update DBMS-specific package examples

Examples

PostgreSQL Handler

import { IDatabaseMigrationHandler, IPostgresDB } from '@migration-script-runner/core';

class PostgreSQLHandler implements IDatabaseMigrationHandler<IPostgresDB> {
  db: IPostgresDB;
  
  constructor(db: IPostgresDB) {
    this.db = db;
  }
}

Strongly-Typed Migration Script

import { IRunnableScript, IPostgresDB, IMigrationInfo } from '@migration-script-runner/core';

const migration: IRunnableScript<IPostgresDB> = {
  async up(db, info, handler) {
    // Full autocomplete for PostgreSQL-specific features!
    await db.query('CREATE INDEX CONCURRENTLY idx_email ON users(email)');
    await db.copy('COPY users FROM STDIN');  // PostgreSQL COPY command
    
    // TypeScript knows about PostgreSQL-specific methods
    const result = await db.query<{count: number}>('SELECT COUNT(*) FROM users');
  },
  
  async down(db, info, handler) {
    await db.query('DROP INDEX idx_email');
  }
};

export default migration;

Backward Compatible (Default Generic)

// Still works - uses default IDB type
const migration: IRunnableScript = {
  async up(db, info, handler) {
    // db is IDB (base interface)
    await db.checkConnection();
  }
};

Open Questions

  1. Breaking Change Strategy: Should this be v1.0.0 or can we maintain backward compatibility?
  2. Opt-in vs Mandatory: Should generics be optional (with defaults) or required?
  3. Database Interface Hierarchy: Do we need IPostgresDB extends IDB, IMySQLDB extends IDB, etc.?
  4. Migration Script Typing: How do we infer database type in migration files without explicit typing?
  5. Loader Impact: How do loaders (TypeScriptLoader, SqlLoader) handle generic types?

Success Criteria

  • All core interfaces support generic database types
  • Backward compatibility maintained (existing code works)
  • Full type safety for database-specific operations
  • IDE autocomplete works for typed database instances
  • No as any casting needed in migrations
  • 100% test coverage maintained
  • Documentation updated with typed examples
  • Migration guide for upgrading existing projects

Related Issues


Priority

Backlog - Requires deep analysis during implementation to ensure:

  • Minimal breaking changes
  • Clean generic type propagation
  • Backward compatibility where possible
  • Clear migration path for existing users

Note: This is a significant architectural change that will improve type safety across the entire framework but requires careful planning and execution.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions