morphschemax
v1.0.0
Published
Zero-downtime database schema migration framework for PostgreSQL and MySQL. Dual-write, shadow-read, and expand-contract strategies built-in.
Maintainers
Readme
MorphSchemaX
Zero-downtime database schema migrations for PostgreSQL and MySQL
Why MorphSchemaX?
Traditional database migrations require downtime:
Stop app → Run ALTER TABLE → Wait 10-30 min → Restart app → All users blockedMorphSchemaX eliminates that entirely:
Run migration → App keeps serving users → Zero downtime ✅Features
| Feature | Description | |---|---| | 🚫 Zero Downtime | Three built-in strategies to avoid service interruption | | 🔄 Dual-Write | Keeps old and new columns in sync via DB triggers | | 👁 Shadow Read | Back-fills silently, verifies data before switching | | 📐 Expand-Contract | Classic parallel-change pattern without triggers | | ↩️ Auto Rollback | Snapshot-based rollback on any failure | | ⏸ Pause / Resume | Stop mid-migration and continue from checkpoint | | 📊 Live Progress | Subscribe to row-level progress events | | 🔍 Pre-flight Validation | Catch problems before any DDL is issued | | 🗄 Multi-dialect | PostgreSQL, MySQL, MariaDB | | 🔒 Type-safe | Full TypeScript generics throughout |
Installation
npm install MorphSchemaX
# peer deps
npm install pg # for PostgreSQL
npm install mysql2 # for MySQL / MariaDBQuick Start
import { SchemaMorph } from 'MorphSchemaX';
const morph = await SchemaMorph.create({
database: {
type: 'postgres',
host: 'localhost',
database: 'myapp',
user: 'admin',
password: 'secret',
},
});
// Change users.age from VARCHAR → INTEGER — zero downtime
const result = await morph.changeColumnType('users', 'age', 'int', {
strategy: 'dual-write',
batchSize: 1000,
});
// Live progress
morph.onProgress(result.migrationId, (p) => {
console.log(`${p.percentage.toFixed(1)}% — ${p.rowsPerSecond.toFixed(0)} rows/sec`);
});
await result.completion;
await morph.disconnect();Migration Strategies
dual-write (recommended)
- Create shadow column (nullable, no table rewrite)
- Install DB trigger → every new write goes to both columns
- Back-fill existing rows in small batches
- Swap — rename shadow → canonical, old → deprecated
- Cleanup — drop trigger, drop deprecated column
Best for: most production environments with write traffic.
shadow-read
Same as dual-write but includes an explicit verification phase that samples old vs new values and reports mismatches before promoting the shadow column.
Best for: high-stakes migrations where data correctness must be verified.
expand-contract
Classic three-step pattern: Expand (add new column) → Migrate (batch back-fill) → Contract (drop old column). No triggers required — the application layer handles dual-write during the window.
Best for: restricted environments where trigger creation is not allowed.
API Reference
new SchemaMorph(config) / SchemaMorph.create(config)
const morph = new SchemaMorph(config);
await morph.connect();
// OR one-liner:
const morph = await SchemaMorph.create(config);Core operations
// Change column type
await morph.changeColumnType(table, column, newType, options?);
// Add a column (with optional back-fill)
await morph.addColumn(table, column, type, options?);
// Drop a column safely
await morph.dropColumn(table, column, options?);
// Rename a column
await morph.renameColumn(table, oldName, newName, options?);
// Split one column into many
await morph.splitColumn(table, source, [targets], splitter, options?);
// Merge many columns into one
await morph.mergeColumns(table, [sources], target, merger, options?);Validation
const check = await morph.validateMigration('users', 'age', 'int');
// { safe: true, warnings: [], errors: [], estimatedDurationMs: 500, estimatedRowsAffected: 100000 }Progress & events
const unsub = morph.onProgress(result.migrationId, (p) => {
console.log(`${p.percentage.toFixed(1)}% — ETA: ${(p.etaMs! / 1000).toFixed(0)}s`);
});
await result.completion;
unsub();Control
await morph.pauseMigration(migrationId);
await morph.resumeMigration(migrationId);
await morph.cancelMigration(migrationId); // triggers rollbackHistory & statistics
const history = await morph.getMigrationHistory('users');
const stats = morph.getStatistics(migrationId);
// { totalDurationMs, avgRowsPerSecond, peakRowsPerSecond, p95RowsPerSecond, ... }Configuration
const morph = new SchemaMorph({
database: {
type: 'postgres', // 'postgres' | 'mysql' | 'mariadb'
host: 'localhost',
port: 5432,
database: 'myapp',
user: 'admin',
password: 'secret',
maxConnections: 20, // pool size
ssl: true, // optional SSL
},
migration: {
strategy: 'dual-write', // default strategy
batchSize: 1000, // rows per batch
parallel: false, // run back-fill workers in parallel
parallelWorkers: 4,
delayBetweenBatches: 50, // ms sleep between batches
timeout: 3600000, // 1 hour overall timeout
lockTimeout: 5000, // lock acquisition timeout
onError: 'rollback', // 'rollback' | 'pause' | 'continue'
skipValidation: false,
},
logging: {
level: 'info', // 'error' | 'warn' | 'info' | 'debug' | 'verbose'
destination: 'console', // 'console' | 'file' | 'both'
logFile: './migrations.log',
},
});Environment variables
| Variable | Config key |
|---|---|
| MORPH_DB_TYPE | database.type |
| MORPH_DB_HOST | database.host |
| MORPH_DB_PORT | database.port |
| MORPH_DB_NAME | database.database |
| MORPH_DB_USER | database.user |
| MORPH_DB_PASSWORD | database.password |
| MORPH_STRATEGY | migration.strategy |
| MORPH_BATCH_SIZE | migration.batchSize |
| MORPH_PARALLEL | migration.parallel |
| MORPH_TIMEOUT | migration.timeout |
| MORPH_ON_ERROR | migration.onError |
.morphrc.json
{
"database": { "type": "postgres", "host": "localhost", "database": "myapp", "user": "admin", "password": "secret" },
"migration": { "strategy": "dual-write", "batchSize": 2000 }
}Custom Converters
const result = await morph.changeColumnType('elements', 'width', 'int', {
// strip CSS units: "42px" → 42
converter: (v) => parseInt(String(v).replace(/[^0-9.-]/g, ''), 10),
fallbackValue: 0,
});Error Handling
morph.onMigrationError((error, migrationId) => {
console.error(`Migration ${migrationId} failed:`, error.message);
});
morph.onRollbackComplete((migrationId) => {
console.log(`Migration ${migrationId} rolled back safely`);
});All errors extend SchemaMorphError:
import {
ConversionError,
ValidationError,
RollbackError,
MigrationTimeoutError,
TableNotFoundError,
ColumnNotFoundError,
} from 'MorphSchemaX';Project Structure
MorphSchemaX/
├── src/
│ ├── api/ # Public API: SchemaMorph class, decorators, index
│ ├── config/ # ConfigLoader, defaults
│ ├── connectors/ # BaseConnector, PostgresConnector, MySQLConnector
│ ├── core/
│ │ ├── strategies/ # DualWrite, ShadowRead, ExpandContract
│ │ ├── migrations/ # MigrationPlan, MigrationRunner
│ │ ├── types.ts # All TypeScript interfaces
│ │ ├── errors.ts # Typed error hierarchy
│ │ ├── events.ts # Typed EventEmitter
│ │ ├── constants.ts # Package constants
│ │ └── morphEngine.ts # Central coordinator
│ ├── monitoring/ # ProgressTracker, ErrorHandler, MetricsCollector
│ ├── rollback/ # SnapshotManager, CheckpointManager, RollbackManager
│ ├── transforms/ # TypeConverter, DataTransform, MigrationValidator
│ └── utils/ # helpers, timing, logger
├── tests/
│ ├── unit/ # Pure unit tests (no DB)
│ ├── integration/ # Integration tests (mock connector)
│ ├── performance/ # Throughput benchmarks
│ └── e2e/ # End-to-end API tests
├── examples/
│ ├── basicMigration.ts
│ └── advancedScenarios.ts
└── docs/
├── api.md
├── strategies.md
└── troubleshooting.mdPricing
| Plan | Price | Databases | Features | |---|---|---|---| | Free | $0 | PostgreSQL | Core migrations, basic monitoring | | Pro | $39/mo | + MySQL, MariaDB | Advanced perf, auto-backup, priority support | | Enterprise | Custom | All | Dedicated processing, SLA, 24/7 support |
License
MIT © MorphSchemaX contributors
