fix(localdb): disable SQLite pooling on legacy reads - a Windows site node could not boot

SiteLocalDbLegacyMigrator opens each pre-Phase-2 file read-only, copies its rows into
the consolidated site database, then renames the file so a later boot skips it.
Microsoft.Data.Sqlite POOLS connections, so disposing one returns it to the pool and
leaves the underlying sqlite3 handle - and the OS file handle - open. On Windows the
subsequent File.Move throws IOException ("being used by another process"), the
migration faults out of AddZbLocalDb's factory, and the site node does not start at
all. There is no partial-migration path: it is a hard boot failure, once, on the first
upgrade past LocalDb Phase 2.

Why it shipped: POSIX rename ignores open handles, so this cannot reproduce on Linux or
macOS. Every existing rename assertion in SiteLocalDbLegacyMigratorTests - including
LegacyTrackingRows_AreCopiedAndTheFileIsRenamed - passes with the bug fully present, and
the docker rig migrates cleanly. It was found by pre-flighting the wonder-app-vd03
upgrade against COPIES of that box's real site databases; an earlier pre-flight pass
with empty data directories had nothing to drain and passed clean.

Fix: both legacy read-only connection strings now go through one
LegacyReadOnlyConnectionString helper carrying Pooling=False.

The accompanying test is deliberately white-box. A behavioural assertion cannot
discriminate here on the platform this suite runs on, so it pins the connection string
instead; it is red without the fix.

Verified: Host.Tests 440/440, 0 warnings. The build deployed to wonder-app-vd03 carries
this change (as the then-uncommitted fix) and has been running since 2026-08-05.
This commit is contained in:
Joseph Doherty
2026-08-05 17:06:54 -04:00
parent b8f91bab2d
commit 7caa8bfd99
2 changed files with 48 additions and 2 deletions
@@ -228,6 +228,29 @@ public class SiteLocalDbLegacyMigratorTests : IDisposable
Assert.True(File.Exists(trackingPath + ".migrated"));
}
[Fact]
public void LegacyReadsDisablePooling_OrTheRenameFailsOnWindows()
{
// Deliberately white-box, and deliberately NOT a behavioural test.
//
// Microsoft.Data.Sqlite pools connections, so disposing one returns it to the pool
// and leaves the underlying sqlite3 handle — and the OS file handle — open. Every
// migrate step then renames its legacy file. On Windows, File.Move against a file
// someone still holds open throws IOException, the migration faults out of
// AddZbLocalDb's factory, and the site node cannot boot at all.
//
// It cannot be reproduced from a behavioural assertion off Windows: POSIX rename does
// not care about open handles, so every rename test above — including
// LegacyTrackingRows_AreCopiedAndTheFileIsRenamed — passes on Linux and macOS with the
// pooling bug fully present. That is exactly how it shipped and reached a production
// Windows box. Asserting on the connection string is the only guard that holds on the
// platform this suite actually runs on.
var cs = SiteLocalDbLegacyMigrator.LegacyReadOnlyConnectionString(Path_("whatever.db"));
Assert.Contains("Pooling=False", cs, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Mode=ReadOnly", cs, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void LegacyEvents_GetDeterministicIds_NotFreshGuids()
{