DB migrations: bundled SQL applied automatically after install
Each PROSERVE ZIP can now ship a _migrations/ subfolder with versioned SQL scripts. The launcher applies them right after extraction, in the same install session as the binary copy, so the user never lands on a PROSERVE build whose schema doesn't match its code. Service (PSLauncher.Core/Migrations) - IDatabaseMigrationService + DatabaseMigrationService (MySqlConnector, BSD-licensed). Each .sql runs in a transaction; on failure the DB state is rolled back and the install completes (files only) but the user is warned to fix the connection / replay later. - Tracking table _launcher_migrations (filename, applied_at, checksum, duration_ms) — same model as Flyway / Doctrine. Already-applied scripts are skipped on subsequent installs. Modified scripts trigger a warning log without blocking. - Custom SQL splitter that respects strings/comments/backticks so a single .sql file can contain multiple statements separated by `;`. - DatabasePasswordProtector: DPAPI CurrentUser scope for the MySQL password in config.json (same protection as the license key). Config (PSLauncher.Models/LocalConfig.cs) - New DatabaseConfig section: Host=localhost, Port=3306, User=root, empty password, Database=proserve, AutoApplyMigrations=true. Defaults match a fresh XAMPP install. Override via Settings (next commit). Install pipeline (MainViewModel) - After ZipInstaller.InstallAsync and before declaring the install complete, if AutoApplyMigrations and a _migrations/ folder exists, run ApplyMigrationsAsync with progress reporting (per-file %, filename in footer). Failure shows MsgMigrationFailed dialog explaining XAMPP must be running and pointing to Settings → Database for connection params. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ using PSLauncher.Core.Installations;
|
||||
using PSLauncher.Core.Licensing;
|
||||
using PSLauncher.Core.Localization;
|
||||
using PSLauncher.Core.Manifests;
|
||||
using PSLauncher.Core.Migrations;
|
||||
using PSLauncher.Core.Process;
|
||||
using PSLauncher.Core.Updates;
|
||||
using PSLauncher.Models;
|
||||
@@ -33,6 +34,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly ILauncherSelfUpdater _selfUpdater;
|
||||
private readonly IToastService _toastService;
|
||||
private readonly IDatabaseMigrationService _migrationService;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<MainViewModel> _logger;
|
||||
|
||||
@@ -173,6 +175,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
ILicenseService licenseService,
|
||||
ILauncherSelfUpdater selfUpdater,
|
||||
IToastService toastService,
|
||||
IDatabaseMigrationService migrationService,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<MainViewModel> logger)
|
||||
{
|
||||
@@ -187,6 +190,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
_licenseService = licenseService;
|
||||
_selfUpdater = selfUpdater;
|
||||
_toastService = toastService;
|
||||
_migrationService = migrationService;
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
@@ -655,6 +659,45 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
|
||||
try { File.Delete(zipPath); } catch { /* non critique */ }
|
||||
|
||||
// 5) Migrations DB MySQL bundled dans _migrations/. Skip si la config
|
||||
// a désactivé l'auto-apply ou si le ZIP ne contient pas de répertoire.
|
||||
if (_config.Database.AutoApplyMigrations)
|
||||
{
|
||||
StatusMessage = Strings.StatusApplyingMigrations(row.Version);
|
||||
ProgressDetail = null;
|
||||
ProgressPercent = 0;
|
||||
var migProgress = new Progress<MigrationProgress>(mp =>
|
||||
{
|
||||
if (mp.Total > 0)
|
||||
{
|
||||
var pct = (double)mp.Done / mp.Total * 100.0;
|
||||
ProgressPercent = pct;
|
||||
row.ProgressPercent = pct;
|
||||
}
|
||||
var label = string.IsNullOrEmpty(mp.CurrentFilename)
|
||||
? Strings.StatusApplyingMigrations(row.Version)
|
||||
: Strings.ProgressMigration(mp.Done + 1, mp.Total, mp.CurrentFilename);
|
||||
ProgressDetail = label;
|
||||
row.ProgressDetail = label;
|
||||
});
|
||||
var migResult = await _migrationService.ApplyMigrationsAsync(target, migProgress, ct);
|
||||
if (!migResult.AllSucceeded)
|
||||
{
|
||||
// L'install a réussi côté fichiers ; on garde la version installée mais on
|
||||
// notifie l'utilisateur que la DB n'a pas été migrée — la prochaine relance
|
||||
// de l'install (ou un Settings → "Rejouer migrations") tentera à nouveau.
|
||||
_logger.LogError("DB migration failed for v{Version}: {Detail}", row.Version, migResult.FailureMessage);
|
||||
MessageBox.Show(
|
||||
Strings.MsgMigrationFailed(migResult.FailureMessage ?? "?"),
|
||||
Strings.MsgBoxMigrationFailed,
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
else if (migResult.AppliedCount > 0)
|
||||
{
|
||||
_logger.LogInformation("Applied {N} DB migration(s) for v{Version}", migResult.AppliedCount, row.Version);
|
||||
}
|
||||
}
|
||||
|
||||
StatusMessage = Strings.StatusInstalledSuccess(row.Version);
|
||||
ProgressDetail = null;
|
||||
ProgressPercent = 0;
|
||||
|
||||
Reference in New Issue
Block a user