Skip to content

Commit bc1eea9

Browse files
author
CIS Guru
committed
fix: version-skip fallback scan and schema version update on upgrade
1 parent b00e187 commit bc1eea9

File tree

2 files changed

+98
-25
lines changed

2 files changed

+98
-25
lines changed

4-Nine/Program.cs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -309,10 +309,49 @@
309309
}
310310
else
311311
{
312-
app.Logger.LogWarning(
313-
"Version upgrade: expected previous database {PreviousFile} not found at {PreviousPath}. " +
314-
"A new empty database will be created.",
315-
previousDbFileName, previousDbPath);
312+
// Direct predecessor not found — user may have skipped one or more versions.
313+
// Scan for any app_v*.db in the config directory and pick the highest version
314+
// that is older than this binary's target. System.Version is used instead of
315+
// string sort so that v10.0.0 correctly ranks above v9.0.0.
316+
var dbDir = Path.GetDirectoryName(dbPath)!;
317+
var targetVersion = Version.TryParse(
318+
Path.GetFileNameWithoutExtension(dbFileName).Replace("app_v", ""),
319+
out var tv) ? tv : null;
320+
321+
var bestCandidate = Directory.Exists(dbDir)
322+
? Directory.GetFiles(dbDir, "app_v*.db")
323+
.Where(f => f != dbPath)
324+
.Select(f => new
325+
{
326+
Path = f,
327+
Ver = Version.TryParse(
328+
Path.GetFileNameWithoutExtension(f).Replace("app_v", ""),
329+
out var v) ? v : null
330+
})
331+
.Where(x => x.Ver != null && (targetVersion == null || x.Ver < targetVersion))
332+
.OrderByDescending(x => x.Ver)
333+
.Select(x => x.Path)
334+
.FirstOrDefault()
335+
: null;
336+
337+
if (bestCandidate != null)
338+
{
339+
app.Logger.LogInformation(
340+
"Version skip detected: copying {Found} → {NewFile} (expected {Missing} was absent)",
341+
Path.GetFileName(bestCandidate), dbFileName, previousDbFileName);
342+
Directory.CreateDirectory(dbDir);
343+
File.Copy(bestCandidate, dbPath);
344+
app.Logger.LogInformation(
345+
"Database file upgraded to {NewFileName}. EF migrations will apply schema changes.",
346+
dbFileName);
347+
}
348+
else
349+
{
350+
app.Logger.LogWarning(
351+
"Version upgrade: no previous database found at {PreviousPath} and no app_v*.db " +
352+
"candidates in {DbDir}. A new empty database will be created.",
353+
previousDbPath, dbDir);
354+
}
316355
}
317356
}
318357

@@ -505,9 +544,14 @@
505544
}
506545
else if (currentDbVersion != appSettings.SchemaVersion)
507546
{
508-
// Schema version mismatch - log warning but allow startup
509-
app.Logger.LogWarning("Schema version mismatch! Database: {DbVersion}, Application: {AppVersion}",
547+
// Schema version mismatch after a version upgrade — update the stored version to match.
548+
// This is the normal post-copy state: the copied DB still records the old version but
549+
// EF migrations have already brought the schema up to date.
550+
app.Logger.LogInformation(
551+
"Updating schema version from {DbVersion} to {AppVersion} (post-upgrade)",
510552
currentDbVersion, appSettings.SchemaVersion);
553+
await schemaService.UpdateSchemaVersionAsync(appSettings.SchemaVersion, $"Version upgrade from {currentDbVersion}");
554+
app.Logger.LogInformation("Schema version updated successfully");
511555
}
512556
else
513557
{

Documentation/Database-Upgrade-Strategy.md

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
Nine uses versioned SQLite database files (`app_vX.Y.0.db`) tied to the application's MAJOR.MINOR version. This document describes how the upgrade path works, the known failure mode for version-skipping users, the fix implemented in `app-database-upgrade`, and how to test all upgrade scenarios.
1212

13+
**This fix ships in v1.1.0** alongside the `app_v1.1.0.db` database file. The normal upgrade path (v1.0.0 → v1.1.0) is covered by `PreviousDatabaseFileName`. The version-skip fallback scan is active in every release from v1.1.0 onward — it protects against any skip at any version boundary (v1.0.0 → v1.2.0, v1.1.0 → v1.3.0, v1.2.0 → v2.0.0, etc.).
14+
1315
---
1416

1517
## Database Versioning Policy
@@ -50,31 +52,37 @@ Nine uses versioned SQLite database files (`app_vX.Y.0.db`) tied to the applicat
5052
4. EF Core migrations apply the delta to `app_v1.1.0.db`
5153
5. App starts with all user data intact ✅
5254

53-
### Version-skip upgrade (v1.0.0 → v2.0.0, skipping v1.1.0)
55+
### Version-skip upgrade (any version)
56+
57+
This scenario can occur at any version boundary. The earliest possible real-world instance is a user on v1.0.0 who skips v1.1.0 and installs v1.2.0 directly. The same logic handles v1.1.0 → v1.3.0, v1.2.0 → v2.0.0, v1.0.0 → v10.0.0, or any other gap.
58+
59+
Example: user on v1.0.0 installs v1.2.0 directly.
5460

55-
`appsettings.json` in the v2.0.0 binary:
61+
`appsettings.json` in the v1.2.0 binary:
5662

5763
```json
58-
"DatabaseFileName": "app_v2.0.0.db",
64+
"DatabaseFileName": "app_v1.2.0.db",
5965
"PreviousDatabaseFileName": "app_v1.1.0.db"
6066
```
6167

62-
**Without the fix:**
68+
**Without the fix (pre-v1.1.0 behaviour):**
6369

64-
1. Target `app_v2.0.0.db` — not found
70+
1. Target `app_v1.2.0.db` — not found
6571
2. `PreviousDatabaseFileName` = `app_v1.1.0.db`**not found** (user skipped v1.1.0)
6672
3. Warning logged; no copy performed
67-
4. EF Core creates **blank** `app_v2.0.0.db`**user data is lost**
73+
4. EF Core creates **blank** `app_v1.2.0.db`**user data is lost**
6874

69-
**With the fix (implemented in `app-database-upgrade` branch):**
75+
**With the fix (shipped in v1.1.0, present in every subsequent release):**
7076

71-
1. Target `app_v2.0.0.db` — not found
77+
1. Target `app_v1.2.0.db` — not found
7278
2. `PreviousDatabaseFileName` = `app_v1.1.0.db` — not found
7379
3. **Fallback scan**: glob `app_v*.db` in the config directory, parse each filename as `System.Version`, pick the highest version that is **less than** the target
74-
4. Finds `app_v1.0.0.db`, copies it to `app_v2.0.0.db`
75-
5. EF Core migrations apply the **full delta** from v1.0.0 schema to v2.0.0 schema
80+
4. Finds `app_v1.0.0.db`, copies it to `app_v1.2.0.db`
81+
5. EF Core migrations apply the **full delta** from v1.0.0 schema to v1.2.0 schema
7682
6. App starts with all user data intact ✅
7783

84+
The same logic handles multi-step skips (e.g. only `app_v1.0.0.db` present when installing v1.4.0) and double-digit versions (v9.x → v10.x) correctly via `System.Version` numeric comparison.
85+
7886
### Fresh install (no previous DB)
7987

8088
1. Target `app_vX.Y.0.db` — not found
@@ -203,20 +211,22 @@ All scenarios require a test user data directory. On Linux this is `~/.config/Ni
203211

204212
### Scenario 3: Version Skip (Missing Predecessor)
205213

214+
Simulates a user on v1.0.0 who skips v1.1.0 and installs v1.2.0 directly. This is the earliest possible real-world version-skip.
215+
206216
**Setup:**
207217

208218
1. Run Scenario 1 to create `app_v1.0.0.db` with real data
209219
2. Do **not** create `app_v1.1.0.db`
210-
3. Change `appsettings.json` to `DatabaseFileName: "app_v2.0.0.db"`, `PreviousDatabaseFileName: "app_v1.1.0.db"`
220+
3. Change `appsettings.json` to `DatabaseFileName: "app_v1.2.0.db"`, `PreviousDatabaseFileName: "app_v1.1.0.db"`
211221

212222
**Run:** `dotnet run`
213223

214224
**Expected:**
215225

216-
- Log: `"Version skip detected: copying app_v1.0.0.db → app_v2.0.0.db (expected app_v1.1.0.db was absent)"`
217-
- `app_v2.0.0.db` created from `app_v1.0.0.db`
226+
- Log: `"Version skip detected: copying app_v1.0.0.db → app_v1.2.0.db (expected app_v1.1.0.db was absent)"`
227+
- `app_v1.2.0.db` created from `app_v1.0.0.db`
218228
- All data preserved
219-
- EF migrations applied (full delta from v1.0.0 schema to v2.0.0)
229+
- EF migrations applied (full delta from v1.0.0 schema to v1.2.0)
220230
- App starts normally
221231

222232
**Pass criteria:** Data preserved despite skipped version.
@@ -267,14 +277,14 @@ All scenarios require a test user data directory. On Linux this is `~/.config/Ni
267277
**Setup:**
268278

269279
1. Empty `~/.config/Nine/` (no DB files at all)
270-
2. Set `appsettings.json` to `DatabaseFileName: "app_v2.0.0.db"`, `PreviousDatabaseFileName: "app_v1.9.0.db"`
280+
2. Set `appsettings.json` to `DatabaseFileName: "app_v1.2.0.db"`, `PreviousDatabaseFileName: "app_v1.1.0.db"`
271281

272282
**Run:** `dotnet run`
273283

274284
**Expected:**
275285

276286
- Log: warning that no previous database was found
277-
- EF creates a new blank `app_v2.0.0.db`
287+
- EF creates a new blank `app_v1.2.0.db`
278288
- App starts, seed data present
279289

280290
**Pass criteria:** Clean degradation to fresh install behavior; no crash.
@@ -283,14 +293,33 @@ All scenarios require a test user data directory. On Linux this is `~/.config/Ni
283293

284294
## Merge Plan
285295

296+
This fix ships as part of **v1.1.0**. The branch workflow:
297+
286298
```
287-
phase-0-baseline (base, has Phase 18 + original upgrade block)
299+
phase-0-baseline (base: Phase 18 + original one-step upgrade block)
288300
↓ branch
289-
app-database-upgrade (implements fix: version-skip scan + System.Version)
301+
app-database-upgrade (this branch: version-skip scan + System.Version fix)
290302
↓ test Scenarios 1-6
291303
↓ merge back to phase-0-baseline
292304
↓ merge to development
293305
↓ PR to main
306+
↓ bump-version.sh → v1.1.0
307+
sets DatabaseFileName: "app_v1.1.0.db"
308+
sets PreviousDatabaseFileName: "app_v1.0.0.db"
294309
```
295310

296-
Once merged to `development`, run a full build and Scenarios 1 and 5 against the actual `~/.config/Nine/` directory (with a real v1.0.0 DB backup) before merging to `main`.
311+
**Version notes:**
312+
313+
- `PreviousDatabaseFileName` covers the direct one-step upgrade (v1.0.0 → v1.1.0, v1.1.0 → v1.2.0, etc.)
314+
- The fallback scan covers **any skip at any version boundary** — v1.0.0 → v1.2.0, v1.1.0 → v1.3.0, v1.2.0 → v2.0.0, v1.0.0 → v10.0.0, etc.
315+
- This protection is present in every release from v1.1.0 onward; there is no version at which it "becomes" relevant
316+
- Run all 6 scenarios against `~/.config/Nine/` before merging to `main`
317+
318+
```
319+
320+
**Version notes:**
321+
322+
- v1.1.0 `PreviousDatabaseFileName` = `app_v1.0.0.db` — covers the direct v1.0.0 → v1.1.0 upgrade path (no skip possible yet, only one prior version exists)
323+
- The fallback scan becomes the safety net for v1.2.0+ when a user could first skip a version
324+
- Once merged to `development`, run Scenarios 1 and 5 against the actual `~/.config/Nine/` directory (with a real v1.0.0 DB backup) before merging to `main`
325+
```

0 commit comments

Comments
 (0)