UNPKG

@yihuangdb/storage-object

Version:

A Node.js storage object layer library using Redis OM

200 lines 8.94 kB
"use strict"; /** * 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