refactor(sql): split ISqlDialect/SqlServerDialect into a .Dialect leaf (#526)

`Driver.Sql.Browser` referenced the runtime `Driver.Sql` project. The AdminUI
references that browser, so the whole SQL driver runtime — and
`ZB.MOM.WW.OtOpcUa.Core` through it — sat in the AdminUI's build/deploy graph.
This is the same layering violation #512 removed from the MQTT browser, in a
second driver, and it is why #512's expected removal of `Core.dll` from the
AdminUI's output did not happen.

MEASURED, both directions, on a clean AdminUI build:

  before: ZB.MOM.WW.OtOpcUa.Core.dll + ZB.MOM.WW.OtOpcUa.Driver.Sql.dll present
  after:  both absent

The old csproj comment's premise was sound and survives — the dialect's catalog
SQL *is* the browse engine, so it must be shared rather than duplicated. What
was wrong was the conclusion that sharing it required referencing the runtime
driver. `ISqlDialect`/`SqlServerDialect` depend on nothing in `Driver.Sql`, so
they move to a `.Dialect` leaf that both consume.

This is the `.Transport` shape, not the cheap one: `ISqlDialect` exposes
`DbProviderFactory` and `SqlServerDialect` returns `SqlClientFactory.Instance`,
so `.Contracts` (deliberately provider-free) cannot hold them. Assembly name is
`.Dialect`; the NAMESPACE stays `ZB.MOM.WW.OtOpcUa.Driver.Sql`, matching the
`Driver.Mqtt.Transport` precedent — the extraction cost zero `using` changes.

Two dependencies the cut edge surfaced as the implicit ones they were:

- The AdminUI itself read `SqlDriver.DriverTypeName` in three places — the tag
  editor map, the tag config validator, and the Sql address picker. All three
  now read `DriverTypeNames.Sql`, which is exactly what that constant resolves
  to and exactly the repoint `SqlDriverBrowser`'s own comment had deferred
  until a factory was registered against the shared constant. One exists now.
- `SqlDriverFormContractTests` asserts the AdminUI form's emitted JSON against
  the runtime factory that reads it, so it genuinely needs the runtime driver.
  `AdminUI.Tests` now references it explicitly. A test project referencing a
  runtime driver is fine — tests are not in the AdminUI's deploy graph.

New guard: `DriverBrowserLayeringTests` (Commons.Tests) scans the csproj graph
of every `*.Browser` project in the tree, transitively. It reads csproj files
rather than loaded assemblies because the cost is a build-graph cost — the
compiler emits no assembly reference for an unused `ProjectReference`, so
`MqttBrowserLayeringTests`' assembly-reference approach cannot see that case.
The two are complementary and each now points at the other.

Falsified three ways, each red then green again: the direct reference re-added
to the browser; the same reference one hop out via `.Dialect` (proving the walk
is really transitive); and both controls — browsers-discovered and
references-parsed — which exist so an empty discovery cannot pass the absence
assertion vacuously.

#526's claim that the reference "carries no comment at all" was wrong; it
carried a detailed one from the scaffold commit. Corrected on the issue and in
the design doc's project table, which had recorded the deviation as deliberate.

Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
This commit is contained in:
Joseph Doherty
2026-07-31 08:38:57 -04:00
parent 857a10b4f7
commit 803f4e6efe
15 changed files with 249 additions and 27 deletions
@@ -4,6 +4,7 @@ using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser;
@@ -100,12 +101,14 @@ public sealed class SqlDriverBrowser : IDriverBrowser
/// <inheritdoc />
/// <remarks>
/// Sourced from <see cref="SqlDriver.DriverTypeName"/>, the driver's interim local constant, rather
/// than from <c>DriverTypeNames</c> — the shared constant set is added by the driver-factory task,
/// because adding a member there reddens <c>DriverTypeNamesGuardTests</c> until a factory is
/// registered against it. Repointing that one constant repoints this too.
/// Sourced from the shared <see cref="DriverTypeNames"/> set, which is what the driver's own
/// <c>SqlDriver.DriverTypeName</c> resolves to. This used to read that interim local constant, and
/// the comment here said so; repointing it was deferred until a factory was registered against
/// <see cref="DriverTypeNames.Sql"/> (else <c>DriverTypeNamesGuardTests</c> goes red). One exists
/// now, and reading the shared constant is also what lets this browser stay off the runtime driver
/// — <c>SqlDriver</c> is the only type it still needed from there (Gitea #526).
/// </remarks>
public string DriverType => SqlDriver.DriverTypeName;
public string DriverType => DriverTypeNames.Sql;
/// <summary>The environment variable a <c>connectionStringRef</c> resolves through.</summary>
/// <param name="connectionStringRef">The authored reference name.</param>
@@ -11,15 +11,34 @@
<ItemGroup>
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Commons\ZB.MOM.WW.OtOpcUa.Commons.csproj" />
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts\ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts.csproj" />
<!-- Deliberate deviation from the OpcUaClient.Browser house style (which refs only its
Contracts project and owns its own transport packages): the dialect's catalog SQL
(ISqlDialect / SqlServerDialect, in Driver.Sql) *is* the browse engine, so it must be
shared here rather than duplicated. Microsoft.Data.SqlClient comes in transitively.
The resulting transitive SqlClient reference on AdminUI is reviewed and accepted —
AdminUI already carries SqlClient for ConfigDb. See design doc §2, table row for this
project: docs/plans/2026-07-15-sql-poll-driver-design.md. Do not "fix" this to a
Contracts-only reference without reading that context first. -->
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Sql\ZB.MOM.WW.OtOpcUa.Driver.Sql.csproj" />
<!--
The layering exception this project used to carry is GONE (Gitea #526). It referenced the
runtime Driver.Sql project, and the AdminUI references this browser: the AdminUI's
build/deploy graph therefore pulled in the whole SQL driver runtime and, through it,
ZB.MOM.WW.OtOpcUa.Core.
The premise of the old comment was sound and survives — the dialect's catalog SQL
(ISqlDialect / SqlServerDialect) *is* the browse engine, so it must be SHARED here rather
than duplicated. What was wrong was the conclusion that sharing it required a reference to
the runtime driver. Those two types depend on nothing from Driver.Sql, so they now live in
the .Dialect leaf below, referenced by the runtime driver and by this browser alike.
Note this is the .Transport shape, not the cheap one: ISqlDialect exposes DbProviderFactory
and SqlServerDialect returns SqlClientFactory.Instance, so the dialect cannot move into
.Contracts (deliberately provider-free) — it needs a leaf of its own.
MEASURED, not assumed: after this change ZB.MOM.WW.OtOpcUa.Driver.Sql.dll and
ZB.MOM.WW.OtOpcUa.Core.dll are both absent from the AdminUI's output, and both were present
immediately before it. #512 removed the matching MQTT reference and expected Core to
disappear with it; it did not, because this project was the remaining cause.
The transitive Microsoft.Data.SqlClient on AdminUI is unchanged and remains accepted —
AdminUI already carries SqlClient for ConfigDb. Design doc context, still worth reading
before changing the sharing decision: docs/plans/2026-07-15-sql-poll-driver-design.md §2.
Guarded by DriverBrowserLayeringTests, which scans every *.Browser csproj in the tree.
-->
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect\ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.Tests"/>