fix(database): qualify RD21 saves against pinned MariaDB 12.2.2
This commit is contained in:
@@ -939,7 +939,7 @@ Canonical profile:
|
||||
_db.ChangeTracker.Clear();
|
||||
return Conflict(new { code = "revision_conflict", message = "This job changed while saving. Your draft was not saved. Reload and review before saving again." });
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
catch
|
||||
{
|
||||
_db.ChangeTracker.Clear();
|
||||
throw;
|
||||
|
||||
@@ -216,8 +216,11 @@ public sealed partial class ProfileCvController : ControllerBase
|
||||
|
||||
var runsQuery = _db.CvExtractionRuns
|
||||
.AsNoTracking()
|
||||
.Where(x => x.OwnerUserId == user.Id)
|
||||
.Select(x => new CvExtractionRunListItem(
|
||||
.Where(x => x.OwnerUserId == user.Id);
|
||||
var isSqlite = _db.Database.IsSqlite();
|
||||
if (!isSqlite)
|
||||
runsQuery = runsQuery.OrderByDescending(x => x.StartedAtUtc).Take(10);
|
||||
var runs = await runsQuery.Select(x => new CvExtractionRunListItem(
|
||||
x.Id,
|
||||
x.Trigger,
|
||||
x.Status,
|
||||
@@ -229,10 +232,9 @@ public sealed partial class ProfileCvController : ControllerBase
|
||||
x.NormalizerVersion,
|
||||
x.LlmPromptVersion,
|
||||
x.ErrorMessage,
|
||||
null));
|
||||
var runs = _db.Database.IsSqlite()
|
||||
? (await runsQuery.ToListAsync(HttpContext.RequestAborted)).OrderByDescending(x => x.StartedAtUtc).Take(10).ToList()
|
||||
: await runsQuery.OrderByDescending(x => x.StartedAtUtc).Take(10).ToListAsync(HttpContext.RequestAborted);
|
||||
null)).ToListAsync(HttpContext.RequestAborted);
|
||||
if (isSqlite)
|
||||
runs = runs.OrderByDescending(x => x.StartedAtUtc).Take(10).ToList();
|
||||
|
||||
var runIds = runs.Select(run => run.Id.ToString(System.Globalization.CultureInfo.InvariantCulture)).ToList();
|
||||
var operations = await _db.UserOperations.AsNoTracking()
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobTrackerApi.Data;
|
||||
|
||||
public static class DatabaseConfiguration
|
||||
{
|
||||
public static string GetProvider(IConfiguration configuration) =>
|
||||
(configuration["Database:Provider"] ?? "mariadb").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"mysql" or "mariadb" => "mariadb",
|
||||
"sqlite" => "sqlite",
|
||||
_ => throw new InvalidOperationException("Database:Provider must be mariadb (or mysql) or explicitly sqlite for transitional preservation."),
|
||||
};
|
||||
|
||||
public static string GetConnectionString(IConfiguration configuration, string sqlitePath)
|
||||
{
|
||||
var provider = GetProvider(configuration);
|
||||
var connection = configuration.GetConnectionString("JobTracker");
|
||||
if (!string.IsNullOrWhiteSpace(connection)) return connection;
|
||||
if (provider == "sqlite") return $"Data Source={sqlitePath}";
|
||||
throw new InvalidOperationException("ConnectionStrings:JobTracker is required for MariaDB. SQLite is only used when Database:Provider is explicitly sqlite.");
|
||||
}
|
||||
|
||||
public static void Configure(DbContextOptionsBuilder options, IConfiguration configuration, string sqlitePath)
|
||||
{
|
||||
var provider = GetProvider(configuration);
|
||||
var connection = GetConnectionString(configuration, sqlitePath);
|
||||
if (provider == "mariadb")
|
||||
{
|
||||
var configuredVersion = configuration["Database:ServerVersion"] ?? "12.2.2";
|
||||
if (!Version.TryParse(configuredVersion, out var version) || version.Major < 1 || version.Build < 0)
|
||||
throw new InvalidOperationException("Database:ServerVersion must be a MariaDB version in major.minor.patch form.");
|
||||
// AutoDetect would open a server connection while options are being registered.
|
||||
options.UseMySql(connection, new MariaDbServerVersion(version), mysql => mysql.MigrationsAssembly("JobTrackerApi"));
|
||||
}
|
||||
else
|
||||
options.UseSqlite(connection, sqlite => sqlite.MigrationsAssembly("JobTrackerApi"));
|
||||
}
|
||||
}
|
||||
@@ -97,31 +97,7 @@ builder.Services.AddDbContext<JobTrackerContext>((sp, options) =>
|
||||
var cfg = sp.GetRequiredService<IConfiguration>();
|
||||
var paths = sp.GetRequiredService<AppPaths>();
|
||||
|
||||
var provider = (cfg["Database:Provider"] ?? "sqlite").Trim().ToLowerInvariant();
|
||||
var cs = cfg.GetConnectionString("JobTracker");
|
||||
if (string.IsNullOrWhiteSpace(cs))
|
||||
{
|
||||
cs = $"Data Source={paths.GetDbPath()}";
|
||||
provider = "sqlite";
|
||||
}
|
||||
|
||||
if (provider is "mysql" or "mariadb")
|
||||
{
|
||||
// Avoid ServerVersion.AutoDetect here because it forces an immediate DB connection
|
||||
// during service registration, which can crash the API if MariaDB is temporarily
|
||||
// unavailable or on a different network during deploy startup.
|
||||
options.UseMySql(cs, new MariaDbServerVersion(new Version(11, 0, 0)), mysql =>
|
||||
{
|
||||
mysql.MigrationsAssembly("JobTrackerApi");
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
options.UseSqlite(cs, sqlite =>
|
||||
{
|
||||
sqlite.MigrationsAssembly("JobTrackerApi");
|
||||
});
|
||||
}
|
||||
DatabaseConfiguration.Configure(options, cfg, paths.GetDbPath());
|
||||
|
||||
// We create Identity tables on startup in environments where `dotnet ef` isn't available.
|
||||
// That can cause EF to detect "pending model changes" and throw on Migrate(). Ignore it.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using JobTrackerApi.Data;
|
||||
|
||||
namespace JobTrackerApi.Services
|
||||
{
|
||||
@@ -28,15 +29,8 @@ namespace JobTrackerApi.Services
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
var provider = (cfg["Database:Provider"] ?? "sqlite").Trim().ToLowerInvariant();
|
||||
var cs = cfg.GetConnectionString("JobTracker");
|
||||
if (string.IsNullOrWhiteSpace(cs))
|
||||
{
|
||||
cs = $"Data Source={paths.GetDbPath()}";
|
||||
provider = "sqlite";
|
||||
}
|
||||
|
||||
_connectionString = cs;
|
||||
var provider = DatabaseConfiguration.GetProvider(cfg);
|
||||
_connectionString = DatabaseConfiguration.GetConnectionString(cfg, paths.GetDbPath());
|
||||
IsSupported = provider == "sqlite";
|
||||
BackupsRoot = Path.Combine(paths.DataRoot, "backups");
|
||||
_retainCount = Math.Clamp(cfg.GetValue("Backups:RetainCount", 14), 1, 365);
|
||||
|
||||
@@ -264,8 +264,8 @@ public static class StartupInitializationExtensions
|
||||
var paths = scope.ServiceProvider.GetRequiredService<AppPaths>();
|
||||
var users = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
var roles = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
|
||||
var provider = (app.Configuration["Database:Provider"] ?? "sqlite").Trim().ToLowerInvariant();
|
||||
var useSqliteBootstrap = provider is not "mysql" and not "mariadb";
|
||||
var provider = DatabaseConfiguration.GetProvider(app.Configuration);
|
||||
var useSqliteBootstrap = provider == "sqlite";
|
||||
|
||||
// Reconciliation opens the file before EF creates it; enable WAL explicitly so readers do not block commits.
|
||||
if (useSqliteBootstrap) db.Database.ExecuteSqlRaw("PRAGMA journal_mode = WAL;");
|
||||
@@ -519,6 +519,9 @@ public static class StartupInitializationExtensions
|
||||
// Correspondences/Attachments) -- re-run once more after Migrate() below via
|
||||
// ReconcileCoreAppColumnsMySql, in case this is a brand-new database.
|
||||
ReconcileCoreAppColumnsMySql(conn);
|
||||
EnsureMySqlColumn(conn, "AspNetUsers", "FirstName", "ALTER TABLE `AspNetUsers` ADD COLUMN `FirstName` longtext NULL;");
|
||||
EnsureMySqlColumn(conn, "AspNetUsers", "LastName", "ALTER TABLE `AspNetUsers` ADD COLUMN `LastName` longtext NULL;");
|
||||
EnsureMySqlColumn(conn, "AspNetUsers", "DisplayName", "ALTER TABLE `AspNetUsers` ADD COLUMN `DisplayName` longtext NULL;");
|
||||
EnsureMySqlColumn(conn, "AspNetUsers", "ProfileCvText", "ALTER TABLE `AspNetUsers` ADD COLUMN `ProfileCvText` longtext NULL;");
|
||||
EnsureMySqlColumn(conn, "AspNetUsers", "ProfileCvStructureJson", "ALTER TABLE `AspNetUsers` ADD COLUMN `ProfileCvStructureJson` longtext NULL;");
|
||||
EnsureMySqlColumn(conn, "AspNetUsers", "CurrentCvUploadArtifactId", "ALTER TABLE `AspNetUsers` ADD COLUMN `CurrentCvUploadArtifactId` int NULL;");
|
||||
@@ -848,7 +851,7 @@ public static class StartupInitializationExtensions
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
|
||||
var runtimeProvider = (app.Configuration["Database:Provider"] ?? "sqlite").Trim().ToLowerInvariant();
|
||||
var runtimeProvider = DatabaseConfiguration.GetProvider(app.Configuration);
|
||||
using var conn = db.Database.GetDbConnection();
|
||||
conn.Open();
|
||||
if (!CoreSchemaReady(conn, runtimeProvider))
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"Database": {
|
||||
"Provider": "mariadb",
|
||||
"ServerVersion": "12.2.2"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"JobTracker": "Server=127.0.0.1;Port=33306;Database=jobtracker_dev;User ID=jobtracker_dev;Password=jt-development-only;SslMode=None"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
||||
Reference in New Issue
Block a user