Skip to content

Commit b00e187

Browse files
author
CIS Guru
committed
docs: add Database-Upgrade-Strategy.md with version-skip test plan
1 parent 1dab209 commit b00e187

File tree

1 file changed

+296
-0
lines changed

1 file changed

+296
-0
lines changed
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# Nine - Database Upgrade Strategy
2+
3+
**Version:** 1.1.0-dev
4+
**Last Updated:** March 12, 2026
5+
**Audience:** Developers, Contributors
6+
7+
---
8+
9+
## Overview
10+
11+
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.
12+
13+
---
14+
15+
## Database Versioning Policy
16+
17+
| App Version | Database File | Schema Version | Notes |
18+
| ----------- | ------------- | -------------- | ---------------------------------- |
19+
| v1.0.0 | app_v1.0.0.db | 1.0.0 | First public release |
20+
| v1.1.0 | app_v1.1.0.db | 1.1.0 | Additive columns only |
21+
| v2.0.0 | app_v2.0.0.db | 2.0.0 | Breaking schema changes |
22+
| v2.0.5 | app_v2.0.0.db | 2.0.0 | PATCH — same DB file |
23+
| v2.1.0 | app_v2.1.0.db | 2.1.0 | New DB file, EF migrations applied |
24+
25+
**Rules:**
26+
27+
- MAJOR or MINOR version bump → new `DatabaseFileName` (e.g. `app_v2.1.0.db`)
28+
- PATCH version bump → same `DatabaseFileName`, no DB file change
29+
- `PreviousDatabaseFileName` is set by `bump-version.sh` to the previous file name
30+
- A compiled binary is **version-locked**: it only looks for the filename baked into its `appsettings.json`
31+
32+
---
33+
34+
## Upgrade Path: How It Works
35+
36+
### Normal upgrade (v1.0.0 → v1.1.0)
37+
38+
`appsettings.json` in the v1.1.0 binary:
39+
40+
```json
41+
"DatabaseFileName": "app_v1.1.0.db",
42+
"PreviousDatabaseFileName": "app_v1.0.0.db"
43+
```
44+
45+
**Startup sequence (`Program.cs`):**
46+
47+
1. Target `app_v1.1.0.db` — not found
48+
2. `PreviousDatabaseFileName` = `app_v1.0.0.db` — found
49+
3. **Copy** `app_v1.0.0.db``app_v1.1.0.db`
50+
4. EF Core migrations apply the delta to `app_v1.1.0.db`
51+
5. App starts with all user data intact ✅
52+
53+
### Version-skip upgrade (v1.0.0 → v2.0.0, skipping v1.1.0)
54+
55+
`appsettings.json` in the v2.0.0 binary:
56+
57+
```json
58+
"DatabaseFileName": "app_v2.0.0.db",
59+
"PreviousDatabaseFileName": "app_v1.1.0.db"
60+
```
61+
62+
**Without the fix:**
63+
64+
1. Target `app_v2.0.0.db` — not found
65+
2. `PreviousDatabaseFileName` = `app_v1.1.0.db`**not found** (user skipped v1.1.0)
66+
3. Warning logged; no copy performed
67+
4. EF Core creates **blank** `app_v2.0.0.db`**user data is lost**
68+
69+
**With the fix (implemented in `app-database-upgrade` branch):**
70+
71+
1. Target `app_v2.0.0.db` — not found
72+
2. `PreviousDatabaseFileName` = `app_v1.1.0.db` — not found
73+
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
76+
6. App starts with all user data intact ✅
77+
78+
### Fresh install (no previous DB)
79+
80+
1. Target `app_vX.Y.0.db` — not found
81+
2. `PreviousDatabaseFileName` is empty (or not found on disk)
82+
3. Scan finds nothing
83+
4. EF Core creates a new blank database
84+
5. Seed data applied ✅
85+
86+
### Already upgraded (DB file exists)
87+
88+
1. Target `app_vX.Y.0.db`**found**
89+
2. Entire upgrade block is skipped (guarded by `!File.Exists(dbPath)`)
90+
3. EF checks for pending migrations, applies if any ✅
91+
92+
---
93+
94+
## The Fix: `System.Version` Fallback Scan
95+
96+
**File:** `4-Nine/Program.cs`
97+
98+
The existing `else` branch in the upgrade block (which only logged a warning) is replaced with:
99+
100+
```csharp
101+
else
102+
{
103+
// Direct predecessor not found — user may have skipped one or more versions.
104+
// Scan for any app_v*.db file in the same directory and use the highest version
105+
// that is older than this binary's target. System.Version ensures correct numeric
106+
// ordering (v10.0 > v9.0, which lexicographic sort would get wrong).
107+
var dbDir = Path.GetDirectoryName(dbPath)!;
108+
var targetVersion = Version.TryParse(
109+
Path.GetFileNameWithoutExtension(dbFileName).Replace("app_v", ""),
110+
out var tv) ? tv : null;
111+
112+
var bestCandidate = Directory.Exists(dbDir)
113+
? Directory.GetFiles(dbDir, "app_v*.db")
114+
.Where(f => f != dbPath)
115+
.Select(f => new
116+
{
117+
Path = f,
118+
Version = Version.TryParse(
119+
Path.GetFileNameWithoutExtension(f).Replace("app_v", ""),
120+
out var v) ? v : null
121+
})
122+
.Where(x => x.Version != null && (targetVersion == null || x.Version < targetVersion))
123+
.OrderByDescending(x => x.Version)
124+
.Select(x => x.Path)
125+
.FirstOrDefault()
126+
: null;
127+
128+
if (bestCandidate != null)
129+
{
130+
app.Logger.LogInformation(
131+
"Version skip detected: copying {Found} → {NewFile} (expected {Missing} was absent)",
132+
Path.GetFileName(bestCandidate), dbFileName, previousDbFileName);
133+
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
134+
File.Copy(bestCandidate, dbPath);
135+
}
136+
else
137+
{
138+
app.Logger.LogWarning(
139+
"Version upgrade: no previous database found at {PreviousPath} and no app_v*.db candidates in {DbDir}. " +
140+
"A new empty database will be created.",
141+
previousDbPath, dbDir);
142+
}
143+
}
144+
```
145+
146+
**Why `System.Version`?**
147+
Lexicographic string sorting fails at double-digit major versions:
148+
`"app_v10.0.0.db" < "app_v9.0.0.db"` (string sort — wrong)
149+
`Version(10,0,0) > Version(9,0,0)` (`System.Version` — correct)
150+
151+
**Why `x.Version < targetVersion`?**
152+
Guards against accidentally picking a DB file from a _newer_ version than the current binary — which could only exist if the user manually placed it there (unsupported).
153+
154+
---
155+
156+
## Testing Plan
157+
158+
### Prerequisites
159+
160+
All scenarios require a test user data directory. On Linux this is `~/.config/Nine/`. The test steps below create and manipulate files in that directory directly.
161+
162+
> **Important:** Back up any real production database before running these tests.
163+
164+
---
165+
166+
### Scenario 1: Fresh Install
167+
168+
**Setup:** Delete `~/.config/Nine/app_v*.db` if any exist. Set `appsettings.json` to `DatabaseFileName: "app_v1.0.0.db"`, `PreviousDatabaseFileName: ""`.
169+
170+
**Run:** `dotnet run` (or launch AppImage)
171+
172+
**Expected:**
173+
174+
- Log: no upgrade messages
175+
- `~/.config/Nine/app_v1.0.0.db` created
176+
- App starts, seed data present, login works
177+
178+
**Pass criteria:** Fresh DB created, no errors in logs.
179+
180+
---
181+
182+
### Scenario 2: Normal Upgrade (Direct Predecessor)
183+
184+
**Setup:**
185+
186+
1. Run Scenario 1 to create `app_v1.0.0.db` with real data
187+
2. Change `appsettings.json` to `DatabaseFileName: "app_v1.1.0.db"`, `PreviousDatabaseFileName: "app_v1.0.0.db"`
188+
3. (Optional) Add a pending EF migration in the `1.1.0` schema to verify migrations apply
189+
190+
**Run:** `dotnet run`
191+
192+
**Expected:**
193+
194+
- Log: `"Version upgrade detected: copying app_v1.0.0.db → app_v1.1.0.db"`
195+
- `app_v1.1.0.db` created
196+
- All data from `app_v1.0.0.db` present in new file
197+
- EF migrations applied (if any pending)
198+
- App starts normally
199+
200+
**Pass criteria:** Data preserved, no EF errors.
201+
202+
---
203+
204+
### Scenario 3: Version Skip (Missing Predecessor)
205+
206+
**Setup:**
207+
208+
1. Run Scenario 1 to create `app_v1.0.0.db` with real data
209+
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"`
211+
212+
**Run:** `dotnet run`
213+
214+
**Expected:**
215+
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`
218+
- All data preserved
219+
- EF migrations applied (full delta from v1.0.0 schema to v2.0.0)
220+
- App starts normally
221+
222+
**Pass criteria:** Data preserved despite skipped version.
223+
224+
---
225+
226+
### Scenario 4: Multi-Version Skip (v1.0.0 → v10.0.0)
227+
228+
**Setup:**
229+
230+
1. Create `app_v1.0.0.db` with real data
231+
2. Create a dummy `app_v9.0.0.db` in the config directory (copy of v1.0.0)
232+
3. Change `appsettings.json` to `DatabaseFileName: "app_v10.0.0.db"`, `PreviousDatabaseFileName: "app_v9.9.0.db"` (missing)
233+
234+
**Run:** `dotnet run`
235+
236+
**Expected:**
237+
238+
- Log shows `app_v9.0.0.db` was selected (highest version below v10.0.0)
239+
- `app_v10.0.0.db` created from `app_v9.0.0.db`, NOT `app_v1.0.0.db`
240+
- `System.Version` correctly ranked v9.0.0 > v1.0.0
241+
242+
**Pass criteria:** v10 is handled correctly; v9 preferred over v1.
243+
244+
---
245+
246+
### Scenario 5: Already Upgraded (Idempotency)
247+
248+
**Setup:**
249+
250+
1. Complete Scenario 2 (v1.1.0 DB exists)
251+
2. Restart the app without changing any config
252+
253+
**Run:** `dotnet run`
254+
255+
**Expected:**
256+
257+
- No copy block executing (guarded by `!File.Exists(dbPath)`)
258+
- No upgrade log messages
259+
- App starts normally from existing DB
260+
261+
**Pass criteria:** No duplicate copies, no accidental data overwrite.
262+
263+
---
264+
265+
### Scenario 6: No Previous DB, No Candidates
266+
267+
**Setup:**
268+
269+
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"`
271+
272+
**Run:** `dotnet run`
273+
274+
**Expected:**
275+
276+
- Log: warning that no previous database was found
277+
- EF creates a new blank `app_v2.0.0.db`
278+
- App starts, seed data present
279+
280+
**Pass criteria:** Clean degradation to fresh install behavior; no crash.
281+
282+
---
283+
284+
## Merge Plan
285+
286+
```
287+
phase-0-baseline (base, has Phase 18 + original upgrade block)
288+
↓ branch
289+
app-database-upgrade (implements fix: version-skip scan + System.Version)
290+
↓ test Scenarios 1-6
291+
↓ merge back to phase-0-baseline
292+
↓ merge to development
293+
↓ PR to main
294+
```
295+
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`.

0 commit comments

Comments
 (0)