using MongoDB.Bson; using MongoDB.Driver; using QuestionsApi.Models; namespace QuestionsApi.Services; public static class QuestionAttemptCycleMigrationCommand { public const string Confirmation = "MIGRATE_ATTEMPT_CYCLES"; public const string LegacyIndexName = "QuestionExternalId_1_UserId_1"; public const string CycleIndexName = "uq_question_user_cycle"; public static async Task RunAsync( IConfiguration configuration, string[] args, TextWriter output) { var apply = args.Contains("--apply", StringComparer.Ordinal); var confirmation = Option(args, "--confirm"); var mongo = new MongoDatabaseProvider(configuration); var attempts = mongo.Database.GetCollection("QuestionAttempts"); var missingFilter = new BsonDocumentFilterDefinition( new BsonDocument(nameof(QuestionAttempt.BankCycle), new BsonDocument("$exists", false))); var missing = await attempts.CountDocumentsAsync(missingFilter); var indexes = (await attempts.Indexes.ListAsync()).ToList(); var names = indexes.Select(value => value["name"].AsString).ToHashSet(StringComparer.Ordinal); await output.WriteLineAsync( $"QuestionAttempts missing BankCycle: {missing}; legacy index: {names.Contains(LegacyIndexName)}; cycle index: {names.Contains(CycleIndexName)}."); if (!apply) { await output.WriteLineAsync( $"Dry run only. Re-run with --apply --confirm {Confirmation}."); return 0; } if (confirmation != Confirmation) { await output.WriteLineAsync($"Refusing migration: --confirm {Confirmation} is required."); return 2; } if (missing > 0) await attempts.UpdateManyAsync( missingFilter, Builders.Update.Set(value => value.BankCycle, 0)); if (!names.Contains(CycleIndexName)) await attempts.Indexes.CreateOneAsync(new CreateIndexModel( Builders.IndexKeys .Ascending(value => value.QuestionExternalId) .Ascending(value => value.UserId) .Ascending(value => value.BankCycle), new CreateIndexOptions { Unique = true, Name = CycleIndexName })); if (names.Contains(LegacyIndexName)) await attempts.Indexes.DropOneAsync(LegacyIndexName); var remaining = await attempts.CountDocumentsAsync(missingFilter); await output.WriteLineAsync( $"Migration complete. Backfilled {missing}; missing BankCycle: {remaining}; dropped legacy index: {names.Contains(LegacyIndexName)}."); return remaining == 0 ? 0 : 1; } private static string Option(string[] args, string name) { var index = Array.IndexOf(args, name); return index >= 0 && index + 1 < args.Length ? args[index + 1] : ""; } }