diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs index 136e721..a62f567 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs @@ -518,7 +518,7 @@ public sealed class AppDbContext(DbContextOptions options) x.CourseSelectionOfferingId, x.Status, x.WaitlistedAt - }); + }).HasDatabaseName("IX_CE_Offering_Status_WaitlistedAt"); entity.HasOne(x => x.CourseSelectionOffering) .WithMany(x => x.Enrollments) .HasForeignKey(x => x.CourseSelectionOfferingId) diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContextFactory.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContextFactory.cs index 3c3a423..f130f8a 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContextFactory.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContextFactory.cs @@ -12,7 +12,7 @@ public sealed class AppDbContextFactory : IDesignTimeDbContextFactory() - .UseMySQL(connectionString) + .UseMySQL(MySqlConnectionStringPolicy.ForApplication(connectionString)) .Options; return new AppDbContext(options); diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs index df4200c..56ce0d0 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs @@ -1611,7 +1611,7 @@ public sealed class DevelopmentSqliteMigrator( ADD COLUMN "WaitlistedAt" TEXT NULL; """, """ - CREATE INDEX IF NOT EXISTS "IX_CourseEnrollments_CourseSelectionOfferingId_Status_WaitlistedAt" + CREATE INDEX IF NOT EXISTS "IX_CE_Offering_Status_WaitlistedAt" ON "CourseEnrollments" ("CourseSelectionOfferingId", "Status", "WaitlistedAt"); """ ]; diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726170000_CourseSelectionWaitlist.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726170000_CourseSelectionWaitlist.cs index 448cf49..a410c5b 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726170000_CourseSelectionWaitlist.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726170000_CourseSelectionWaitlist.cs @@ -12,26 +12,91 @@ public partial class CourseSelectionWaitlist : Migration { protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AddColumn( - name: "WaitlistedAt", - table: "CourseEnrollments", - type: "datetime(6)", - nullable: true); + migrationBuilder.Sql(""" + SET @waitlisted_column_exists = ( + SELECT COUNT(*) + FROM `INFORMATION_SCHEMA`.`COLUMNS` + WHERE `TABLE_SCHEMA` = DATABASE() + AND `TABLE_NAME` = 'CourseEnrollments' + AND `COLUMN_NAME` = 'WaitlistedAt' + ); + """); + migrationBuilder.Sql(""" + SET @add_waitlisted_column_sql = IF( + @waitlisted_column_exists = 0, + 'ALTER TABLE `CourseEnrollments` ADD COLUMN `WaitlistedAt` datetime(6) NULL', + 'SELECT 1' + ); + """); + migrationBuilder.Sql( + "PREPARE add_waitlisted_column_stmt FROM @add_waitlisted_column_sql;"); + migrationBuilder.Sql("EXECUTE add_waitlisted_column_stmt;"); + migrationBuilder.Sql("DEALLOCATE PREPARE add_waitlisted_column_stmt;"); - migrationBuilder.CreateIndex( - name: "IX_CourseEnrollments_CourseSelectionOfferingId_Status_WaitlistedAt", - table: "CourseEnrollments", - columns: new[] { "CourseSelectionOfferingId", "Status", "WaitlistedAt" }); + migrationBuilder.Sql(""" + SET @waitlist_index_exists = ( + SELECT COUNT(*) + FROM `INFORMATION_SCHEMA`.`STATISTICS` + WHERE `TABLE_SCHEMA` = DATABASE() + AND `TABLE_NAME` = 'CourseEnrollments' + AND `INDEX_NAME` = 'IX_CE_Offering_Status_WaitlistedAt' + ); + """); + migrationBuilder.Sql(""" + SET @add_waitlist_index_sql = IF( + @waitlist_index_exists = 0, + 'CREATE INDEX `IX_CE_Offering_Status_WaitlistedAt` ON `CourseEnrollments` (`CourseSelectionOfferingId`, `Status`, `WaitlistedAt`)', + 'SELECT 1' + ); + """); + migrationBuilder.Sql( + "PREPARE add_waitlist_index_stmt FROM @add_waitlist_index_sql;"); + migrationBuilder.Sql("EXECUTE add_waitlist_index_stmt;"); + migrationBuilder.Sql("DEALLOCATE PREPARE add_waitlist_index_stmt;"); } protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropIndex( - name: "IX_CourseEnrollments_CourseSelectionOfferingId_Status_WaitlistedAt", - table: "CourseEnrollments"); + migrationBuilder.Sql(""" + SET @waitlist_index_exists = ( + SELECT COUNT(*) + FROM `INFORMATION_SCHEMA`.`STATISTICS` + WHERE `TABLE_SCHEMA` = DATABASE() + AND `TABLE_NAME` = 'CourseEnrollments' + AND `INDEX_NAME` = 'IX_CE_Offering_Status_WaitlistedAt' + ); + """); + migrationBuilder.Sql(""" + SET @drop_waitlist_index_sql = IF( + @waitlist_index_exists > 0, + 'DROP INDEX `IX_CE_Offering_Status_WaitlistedAt` ON `CourseEnrollments`', + 'SELECT 1' + ); + """); + migrationBuilder.Sql( + "PREPARE drop_waitlist_index_stmt FROM @drop_waitlist_index_sql;"); + migrationBuilder.Sql("EXECUTE drop_waitlist_index_stmt;"); + migrationBuilder.Sql("DEALLOCATE PREPARE drop_waitlist_index_stmt;"); - migrationBuilder.DropColumn( - name: "WaitlistedAt", - table: "CourseEnrollments"); + migrationBuilder.Sql(""" + SET @waitlisted_column_exists = ( + SELECT COUNT(*) + FROM `INFORMATION_SCHEMA`.`COLUMNS` + WHERE `TABLE_SCHEMA` = DATABASE() + AND `TABLE_NAME` = 'CourseEnrollments' + AND `COLUMN_NAME` = 'WaitlistedAt' + ); + """); + migrationBuilder.Sql(""" + SET @drop_waitlisted_column_sql = IF( + @waitlisted_column_exists > 0, + 'ALTER TABLE `CourseEnrollments` DROP COLUMN `WaitlistedAt`', + 'SELECT 1' + ); + """); + migrationBuilder.Sql( + "PREPARE drop_waitlisted_column_stmt FROM @drop_waitlisted_column_sql;"); + migrationBuilder.Sql("EXECUTE drop_waitlisted_column_stmt;"); + migrationBuilder.Sql("DEALLOCATE PREPARE drop_waitlisted_column_stmt;"); } } diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726180000_CourseSelectionGradeLimits.Designer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726180000_CourseSelectionGradeLimits.Designer.cs index babd87d..24602b0 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726180000_CourseSelectionGradeLimits.Designer.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726180000_CourseSelectionGradeLimits.Designer.cs @@ -748,7 +748,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("StudentId", "Status"); - b.HasIndex("CourseSelectionOfferingId", "Status", "WaitlistedAt"); + b.HasIndex("CourseSelectionOfferingId", "Status", "WaitlistedAt") + .HasDatabaseName("IX_CE_Offering_Status_WaitlistedAt"); b.ToTable("CourseEnrollments"); }); diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs index 82c4ac9..5f013d3 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs @@ -745,7 +745,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("StudentId", "Status"); - b.HasIndex("CourseSelectionOfferingId", "Status", "WaitlistedAt"); + b.HasIndex("CourseSelectionOfferingId", "Status", "WaitlistedAt") + .HasDatabaseName("IX_CE_Offering_Status_WaitlistedAt"); b.ToTable("CourseEnrollments"); }); diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/MySqlConnectionStringPolicy.cs b/src/Jiaowu.Api/Infrastructure/Persistence/MySqlConnectionStringPolicy.cs new file mode 100644 index 0000000..71ff212 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/MySqlConnectionStringPolicy.cs @@ -0,0 +1,15 @@ +using MySql.Data.MySqlClient; + +namespace Jiaowu.Api.Infrastructure.Persistence; + +public static class MySqlConnectionStringPolicy +{ + public static string ForApplication(string connectionString) + { + var builder = new MySqlConnectionStringBuilder(connectionString) + { + AllowUserVariables = true + }; + return builder.ConnectionString; + } +} diff --git a/src/Jiaowu.Api/Program.cs b/src/Jiaowu.Api/Program.cs index 94a07a9..008a727 100644 --- a/src/Jiaowu.Api/Program.cs +++ b/src/Jiaowu.Api/Program.cs @@ -141,14 +141,16 @@ builder.Services.AddDbContextPool(options => throw new InvalidOperationException( "缺少 MySQL 连接串。请通过 ConnectionStrings__MySql 环境变量配置。"); } - options.UseMySQL(connectionString, mySqlOptions => - { - mySqlOptions.CommandTimeout(databaseOptions.CommandTimeoutSeconds); - mySqlOptions.EnableRetryOnFailure( - maxRetryCount: 5, - maxRetryDelay: TimeSpan.FromSeconds(10), - errorNumbersToAdd: null); - }); + options.UseMySQL( + MySqlConnectionStringPolicy.ForApplication(connectionString), + mySqlOptions => + { + mySqlOptions.CommandTimeout(databaseOptions.CommandTimeoutSeconds); + mySqlOptions.EnableRetryOnFailure( + maxRetryCount: 5, + maxRetryDelay: TimeSpan.FromSeconds(10), + errorNumbersToAdd: null); + }); }); var redisConnectionString = builder.Configuration.GetConnectionString("Redis"); diff --git a/tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs b/tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs index 7af6629..683e9be 100644 --- a/tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs +++ b/tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs @@ -2,6 +2,7 @@ using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; +using MySql.Data.MySqlClient; namespace Jiaowu.Api.Tests; @@ -27,9 +28,14 @@ public sealed class MySqlMigrationTests Assert.Contains("CREATE TABLE `WarningRules`", script); Assert.Contains("ADD `CheckInMethod` int NOT NULL DEFAULT 1", script); Assert.Contains("CREATE UNIQUE INDEX `IX_AttendanceSheets_CheckInToken`", script); - Assert.Contains("ADD `WaitlistedAt` datetime(6) NULL", script); + Assert.Contains("ADD COLUMN `WaitlistedAt` datetime(6) NULL", script); Assert.Contains( - "CREATE INDEX `IX_CourseEnrollments_CourseSelectionOfferingId_Status_WaitlistedAt`", + "CREATE INDEX `IX_CE_Offering_Status_WaitlistedAt`", + script); + Assert.Contains("@waitlisted_column_exists", script); + Assert.Contains("@waitlist_index_exists", script); + Assert.DoesNotContain( + "IX_CourseEnrollments_CourseSelectionOfferingId_Status_WaitlistedAt", script); Assert.Contains( "ADD `MaxCourseCount` int NULL", @@ -86,6 +92,34 @@ public sealed class MySqlMigrationTests Assert.Contains(ids[1].ToString(), sql, StringComparison.OrdinalIgnoreCase); } + [Fact] + public void MySql_index_names_fit_the_server_identifier_limit() + { + using var db = new AppDbContext(CreateMySqlOptions()); + + var indexNames = db.Model.GetEntityTypes() + .SelectMany(x => x.GetIndexes()) + .Select(x => x.GetDatabaseName()) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + + Assert.NotEmpty(indexNames); + Assert.All(indexNames, name => Assert.True( + name!.Length <= 64, + $"MySQL index name '{name}' has {name.Length} characters.")); + } + + [Fact] + public void Application_mysql_connections_allow_migration_user_variables() + { + var normalized = MySqlConnectionStringPolicy.ForApplication( + "Server=localhost;Database=jiaowu;User=test;Password=test;"); + + var builder = new MySqlConnectionStringBuilder(normalized); + + Assert.True(builder.AllowUserVariables); + } + private static DbContextOptions CreateMySqlOptions() => new DbContextOptionsBuilder() .UseMySQL(