@yihuangdb/storage-object
Version:
A Node.js storage object layer library using Redis OM
200 lines • 8.94 kB
JavaScript
/**
* Schema Migration Manager
*
* Optimized schema migration with intelligent index recreation.
* Only recreates index when indexed fields change.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SchemaMigrationManager = void 0;
const redis_om_1 = require("redis-om");
const schema_index_detector_1 = require("./schema-index-detector");
class SchemaMigrationManager {
redis;
entityName;
constructor(redis, entityName) {
this.redis = redis;
this.entityName = entityName;
}
/**
* Apply multiple schema migrations with optimized index recreation
*/
async applyMigrations(migrations, currentSchema, repository, options = {}) {
const startTime = Date.now();
const steps = [];
const log = (msg) => {
steps.push(msg);
options.progressCallback?.(msg);
};
try {
if (migrations.length === 0) {
return {
success: true,
migrationsApplied: 0,
indexRecreated: false,
totalTime: 0,
steps: ['No migrations to apply']
};
}
log(`Analyzing ${migrations.length} schema migration(s)...`);
// Prepare schemas for analysis
const allSchemas = [currentSchema, ...migrations.map(m => m.schema)];
// Detect index changes across all versions
const analysis = schema_index_detector_1.SchemaIndexDetector.detectIndexChangesAcrossVersions(allSchemas);
if (options.dryRun) {
log('[DRY RUN MODE]');
}
// Determine migration strategy
const needsIndexRecreation = analysis.hasAnyIndexChanges || options.forceIndexRecreation;
if (needsIndexRecreation) {
log(`Index recreation required: ${analysis.hasAnyIndexChanges ? 'indexed fields changed' : 'forced'}`);
// Log which versions have index changes
if (analysis.hasAnyIndexChanges) {
log(`First index change at migration ${analysis.firstIndexChangeAt}`);
log(`Last index change at migration ${analysis.lastIndexChangeAt}`);
}
if (!options.dryRun) {
// Drop index ONCE before all migrations
log('Dropping existing index...');
try {
await repository.dropIndex();
log('Index dropped successfully');
}
catch (error) {
if (!error.message?.includes('Unknown') && !error.message?.includes('index')) {
throw error;
}
log('No existing index to drop');
}
}
}
else {
log('No index recreation needed - only non-indexed fields changed');
}
// Apply all schema changes
let finalSchema = currentSchema;
for (let i = 0; i < migrations.length; i++) {
const migration = migrations[i];
log(`Applying migration ${i + 1}/${migrations.length}: v${migration.version}`);
if (migration.description) {
log(` Description: ${migration.description}`);
}
// Detect specific changes for this migration
const changes = schema_index_detector_1.SchemaIndexDetector.detectIndexChanges(finalSchema, migration.schema);
if (changes.addedIndexedFields.length > 0) {
log(` Added indexed fields: ${changes.addedIndexedFields.join(', ')}`);
}
if (changes.removedIndexedFields.length > 0) {
log(` Removed indexed fields: ${changes.removedIndexedFields.join(', ')}`);
}
if (changes.modifiedIndexedFields.length > 0) {
log(` Modified indexed fields: ${changes.modifiedIndexedFields.join(', ')}`);
}
if (changes.safeNonIndexedChanges.length > 0) {
log(` Safe non-indexed changes: ${changes.safeNonIndexedChanges.length}`);
}
finalSchema = migration.schema;
}
// Recreate index ONCE after all migrations (if needed)
let indexRecreationTime;
if (needsIndexRecreation && !options.dryRun) {
log('Recreating index with final schema...');
const indexStart = Date.now();
// Create new schema and repository with final schema
const newSchema = new redis_om_1.Schema(this.entityName, finalSchema);
const newRepository = new redis_om_1.Repository(newSchema, this.redis);
await newRepository.createIndex();
indexRecreationTime = Date.now() - indexStart;
log(`Index created in ${indexRecreationTime}ms`);
if (options.waitForIndexCompletion) {
log('Waiting for background indexing to complete...');
await this.waitForIndexCompletion(newSchema.indexName);
const totalIndexTime = Date.now() - indexStart;
log(`Indexing completed in ${totalIndexTime}ms`);
indexRecreationTime = totalIndexTime;
}
}
const totalTime = Date.now() - startTime;
return {
success: true,
migrationsApplied: migrations.length,
indexRecreated: needsIndexRecreation || false,
indexRecreationTime,
totalTime,
steps
};
}
catch (error) {
const totalTime = Date.now() - startTime;
return {
success: false,
migrationsApplied: 0,
indexRecreated: false,
totalTime,
steps,
error: error.message
};
}
}
/**
* Wait for index to complete background indexing
*/
async waitForIndexCompletion(indexName, maxWaitMs = 60000) {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
try {
const info = await this.redis.sendCommand(['FT.INFO', indexName]);
let isIndexing = false;
let percentIndexed = 0;
for (let i = 0; i < info.length; i += 2) {
if (info[i] === 'indexing') {
isIndexing = info[i + 1] === '1';
}
if (info[i] === 'percent_indexed') {
percentIndexed = parseFloat(info[i + 1]);
}
}
if (!isIndexing || percentIndexed >= 1.0) {
return; // Indexing complete
}
await new Promise(resolve => setTimeout(resolve, 100));
}
catch (error) {
// Index might not exist yet
await new Promise(resolve => setTimeout(resolve, 100));
}
}
throw new Error(`Index did not complete within ${maxWaitMs}ms`);
}
/**
* Analyze migration impact without applying changes
*/
static analyzeMigrationImpact(migrations, currentSchema) {
const allSchemas = [currentSchema, ...migrations.map(m => m.schema)];
const analysis = schema_index_detector_1.SchemaIndexDetector.detectIndexChangesAcrossVersions(allSchemas);
const migrationDetails = migrations.map((migration, index) => {
const previousSchema = index === 0 ? currentSchema : migrations[index - 1].schema;
const changes = schema_index_detector_1.SchemaIndexDetector.detectIndexChanges(previousSchema, migration.schema);
const strategy = schema_index_detector_1.SchemaIndexDetector.generateMigrationStrategy(changes);
return {
version: migration.version,
changes,
strategy
};
});
const totalIndexedFieldChanges = migrationDetails.reduce((sum, detail) => {
return sum +
detail.changes.addedIndexedFields.length +
detail.changes.removedIndexedFields.length +
detail.changes.modifiedIndexedFields.length;
}, 0);
return {
requiresIndexRecreation: analysis.hasAnyIndexChanges,
totalIndexedFieldChanges,
migrationDetails
};
}
}
exports.SchemaMigrationManager = SchemaMigrationManager;
exports.default = SchemaMigrationManager;
//# sourceMappingURL=schema-migration-manager.js.map
;