-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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 anyor 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 anycasting 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
IDBtype - 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>MigrationScriptExecutorconstructorMigrationScript<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
IDBand 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
- Breaking Change Strategy: Should this be v1.0.0 or can we maintain backward compatibility?
- Opt-in vs Mandatory: Should generics be optional (with defaults) or required?
- Database Interface Hierarchy: Do we need
IPostgresDB extends IDB,IMySQLDB extends IDB, etc.? - Migration Script Typing: How do we infer database type in migration files without explicit typing?
- 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 anycasting needed in migrations - 100% test coverage maintained
- Documentation updated with typed examples
- Migration guide for upgrading existing projects
Related Issues
- Related to DBMS-specific packages (
@migration-script-runner/postgresql, etc.) - May impact Add migration template generator command #83 (Generator) - templates should generate typed migrations
- Complements Support additional config file formats (YAML, TOML, XML) #98 (Config formats) - type safety for config validation
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.