chore(localdb): adopt LocalDb 0.1.1 and drop the directory shim
LocalDb 0.1.1 creates the parent directory of LocalDb:Path itself, so the SiteLocalDbDirectory shim this repo carried through Phase 2 is deleted along with its call site. The gap was found here but was never ScadaBridge's alone — every LocalDb consumer had it — so the fix moved to the library. SiteLocalDbDirectoryTests is RETAINED and retargeted rather than deleted with the shim. It was already written against the site registration path, not the mechanism, so it needed only its Ensure() call removed: what a site node requires is that resolving ILocalDb not fail on a fresh machine, regardless of who provides that. Verified it still earns its place — pinned back to LocalDb 0.1.0 it fails inside SqliteLocalDb..ctor -> SqliteConnection.Open(), so it genuinely depends on the library behaviour and not on a coincidence. Also corrects the coverage-split note in StoreAndForwardStorageTests, which asserted that directory creation is "NOT LocalDb's" — true when written, wrong as of 0.1.1. Build 0 warnings; Host 330, StoreAndForward 130, LocalDb integration 20 pass. Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
This commit is contained in:
@@ -108,9 +108,9 @@
|
||||
<PackageVersion Include="ZB.MOM.WW.Secrets.Abstractions" Version="0.2.3" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Secrets.Ui" Version="0.2.3" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Secrets.Replicator.SqlServer" Version="0.2.3" />
|
||||
<PackageVersion Include="ZB.MOM.WW.LocalDb" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.LocalDb.Replication" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.LocalDb.Contracts" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.LocalDb" Version="0.1.1" />
|
||||
<PackageVersion Include="ZB.MOM.WW.LocalDb.Replication" Version="0.1.1" />
|
||||
<PackageVersion Include="ZB.MOM.WW.LocalDb.Contracts" Version="0.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the directory holding the consolidated site database (<c>LocalDb:Path</c>)
|
||||
/// exists before LocalDb opens the file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// SQLite creates the database file on demand but never its parent directory, and
|
||||
/// <c>SqliteLocalDb</c> opens the file eagerly in its constructor — so a missing directory
|
||||
/// is a hard boot failure ("SQLite Error 14: unable to open database file"), not a degraded
|
||||
/// start. Neither the LocalDb library nor its options validation does this.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The guarantee previously lived in <c>StoreAndForwardStorage.EnsureDatabaseDirectoryExists</c>,
|
||||
/// which created the directory for the store's own SQLite file. Once that file was folded
|
||||
/// into the consolidated database the store stopped owning a path, so the guarantee had to
|
||||
/// be re-established at the layer that now configures the path: the site host.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It matters because the default site configuration uses the <i>relative</i> path
|
||||
/// <c>./data/site-localdb.db</c>. The docker rig never noticed the gap only because its
|
||||
/// volume mount happens to create <c>/app/data</c>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class SiteLocalDbDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the directory containing <c>LocalDb:Path</c> when it does not already exist.
|
||||
/// No-op when the key is unset.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Best-effort by design: if the directory cannot be created (permissions, read-only
|
||||
/// mount) this stays silent and lets LocalDb raise the real error, which names the
|
||||
/// actual path and is a better diagnostic than anything thrown from here.
|
||||
/// </remarks>
|
||||
/// <param name="config">Configuration carrying <c>LocalDb:Path</c>.</param>
|
||||
public static void Ensure(IConfiguration config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
|
||||
var path = config["LocalDb:Path"];
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(Path.GetFullPath(path));
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException)
|
||||
{
|
||||
// Deliberately swallowed — see the remarks above.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,17 +82,13 @@ public static class SiteServiceRegistration
|
||||
//
|
||||
// Design: scadaproj docs/plans/2026-07-19-scadabridge-localdb-design.md
|
||||
//
|
||||
// Create the parent directory FIRST. SQLite creates the database file on demand
|
||||
// but not its directory, and neither the LocalDb library nor its options
|
||||
// validation does this — SqliteLocalDb's constructor opens the file eagerly, so a
|
||||
// missing directory is a hard boot failure ("SQLite Error 14: unable to open
|
||||
// database file"), not a degraded start. The default site path is the relative
|
||||
// "./data/site-localdb.db", so this bites any site node whose data directory has
|
||||
// not been pre-created; the docker rig only escapes it because the volume mount
|
||||
// creates /app/data. StoreAndForwardStorage used to do this for its own file
|
||||
// (EnsureDatabaseDirectoryExists) and that guarantee has to keep existing
|
||||
// somewhere now that LocalDb owns the file.
|
||||
SiteLocalDbDirectory.Ensure(config);
|
||||
// The parent directory of LocalDb:Path is created by the library as of 0.1.1.
|
||||
// ScadaBridge carried a SiteLocalDbDirectory shim for it during Phase 2, because
|
||||
// SQLite creates the database file on demand but not its directory and
|
||||
// SqliteLocalDb opens the file eagerly — a missing directory was a hard boot
|
||||
// failure, and the default site path is the relative "./data/site-localdb.db".
|
||||
// The shim is gone; SiteLocalDbDirectoryTests still pins the outcome here, since
|
||||
// what this host needs is the guarantee, not any particular owner of it.
|
||||
services.AddZbLocalDb(config, db => SiteLocalDbSetup.OnReady(db, config));
|
||||
|
||||
// The replication engine, likewise unconditional but INERT by default: with no
|
||||
|
||||
@@ -10,12 +10,14 @@ namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This guarantee used to live in <c>StoreAndForwardStorage.EnsureDatabaseDirectoryExists</c>,
|
||||
/// which created the parent directory for its own SQLite file. LocalDb Phase 2 moved that
|
||||
/// file into the consolidated database — but the LocalDb library does <b>not</b> create the
|
||||
/// directory, and <c>SqliteLocalDb</c> opens the file eagerly in its constructor, so a
|
||||
/// missing directory is a hard boot failure ("SQLite Error 14: unable to open database
|
||||
/// file") rather than a degraded start.
|
||||
/// The guarantee has now had three owners: <c>StoreAndForwardStorage</c> created the
|
||||
/// directory for its own SQLite file, LocalDb Phase 2 folded that file into the
|
||||
/// consolidated database and the Host took it over as a <c>SiteLocalDbDirectory</c> shim,
|
||||
/// and LocalDb 0.1.1 took it into the library itself. This test survived all three moves
|
||||
/// unchanged in intent, because what a site node needs is the OUTCOME — resolving
|
||||
/// <c>ILocalDb</c> from the site registration must not fail on a fresh machine — not any
|
||||
/// particular implementer of it. It is deliberately written against the registration path
|
||||
/// rather than the mechanism, so a future move costs nothing here.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This is not hypothetical: the default site configuration points at the <i>relative</i>
|
||||
@@ -44,9 +46,9 @@ public class SiteLocalDbDirectoryTests
|
||||
.Build();
|
||||
|
||||
// The registration path under test. Resolving ILocalDb is what actually opens
|
||||
// the file, so this fails with SQLite Error 14 if the directory step regresses.
|
||||
// the file, so this fails with SQLite Error 14 if the directory step regresses
|
||||
// — wherever it lives.
|
||||
var services = new ServiceCollection();
|
||||
SiteLocalDbDirectory.Ensure(config);
|
||||
services.AddZbLocalDb(config);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
@@ -642,12 +642,12 @@ public class StoreAndForwardStorageTests : IAsyncLifetime, IDisposable
|
||||
// * WAL is genuinely LocalDb's — it sets the journal mode on the database it owns.
|
||||
// The test below still asserts it, now against the LocalDb-backed store.
|
||||
//
|
||||
// * Directory creation is NOT LocalDb's. The library does not create the parent
|
||||
// directory and opens the file eagerly, so a missing directory is a hard boot
|
||||
// failure. The guarantee had to be re-established explicitly in the Host
|
||||
// (SiteServiceRegistration.EnsureLocalDbDirectoryExists), and it is pinned there
|
||||
// by SiteLocalDbDirectoryTests — the layer that now owns it. Asserting it here
|
||||
// would be asserting a guarantee this class no longer provides.
|
||||
// * Directory creation was NOT LocalDb's when Phase 2 landed: the library did not
|
||||
// create the parent directory yet opened the file eagerly, so a missing directory
|
||||
// was a hard boot failure, and the Host re-established the guarantee with a
|
||||
// SiteLocalDbDirectory shim. LocalDb 0.1.1 took the guarantee back into the
|
||||
// library, so the shim is gone — but either way it is not this class's to assert,
|
||||
// and SiteLocalDbDirectoryTests pins the outcome for the site host.
|
||||
|
||||
[Fact]
|
||||
public async Task Initialize_LeavesTheDatabaseInWalJournalMode()
|
||||
|
||||
Reference in New Issue
Block a user