From 2a28c06fc790fe5d985e27e427a7879af3cab03c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Oct 2026 16:04:59 -0400 Subject: [PATCH] fix(sql): a skipped tag reports the deploy gate's reason, pinned for the deferred Query model (R06:S-4) A model:Query tag reached the runtime skip ledger as a generic 'did not map'. The ledger reason (and the skip warning) now comes from SqlTagDefinitionFactory.Inspect - the deploy gate's own explanation over the same parser - so /hosts names the deferral. A test pins that the deploy gate names Query through the parser. Making it a hard deploy error in Warn mode is a ControlPlane change (follow-up); MySql/Odbc and allowWrites remain feature issues. Refs #744 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT --- .../ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriver.cs | 11 +++++++--- .../SqlDriverTests.cs | 20 +++++++++++++++++++ .../SqlEquipmentTagParserTests.cs | 15 ++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriver.cs index 5492c293..7fbd99d7 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriver.cs @@ -929,10 +929,15 @@ public sealed class SqlDriver table[entry.RawPath] = definition; else { - blobSkips.Add(new SkippedTag(entry.RawPath, "TagConfig did not map to a definition")); + // The reason is the DEPLOY GATE's own explanation (R06:S-4) — SqlTagDefinitionFactory.Inspect + // asks this same parser and then says why — so /hosts names e.g. the deferred Query model + // instead of a generic "did not map" for a tag the gate already warned about. + var reason = SqlTagDefinitionFactory.Inspect(entry.TagConfig).FirstOrDefault() + ?? "TagConfig did not map to a definition"; + blobSkips.Add(new SkippedTag(entry.RawPath, reason)); _logger.LogWarning( - "Sql tag config did not map to a definition; skipping. Driver={DriverInstanceId} RawPath={RawPath}", - _driverInstanceId, entry.RawPath); + "Sql tag config did not map to a definition; skipping. Driver={DriverInstanceId} RawPath={RawPath} Reason={Reason}", + _driverInstanceId, entry.RawPath, reason); } } catch (Exception ex) diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverTests.cs index 320a0a0f..1eec94e5 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverTests.cs @@ -464,6 +464,26 @@ public sealed class SqlDriverTests } } + /// + /// R06:S-4. A skipped tag's ledger reason (what /hosts shows) is the deploy gate's own explanation + /// — for a Query tag, that the model is deferred — instead of a generic "did not map". Control: a + /// well-formed tag is not skipped at all. + /// + [Fact] + public async Task ADeferredQueryTag_isSkipped_withTheGatesReason() + { + using var fixture = new SqlitePollFixture(); + await using var driver = NewDriver(fixture, + KvEntry("Speed", SqlitePollFixture.PresentKey), + new RawTagEntry("Q", """{"driver":"Sql","model":"Query","table":"T","query":"SELECT 1"}""", WriteIdempotent: false)); + await driver.InitializeAsync(ConfigJson, CancellationToken.None); + + var report = ((ISkippedTagReporter)driver).GetSkippedTags().ShouldNotBeNull(); + var skipped = report.Sample.ShouldHaveSingleItem(); + skipped.RawPath.ShouldBe("Q"); + skipped.Reason.ShouldContain("Query"); + } + // ---- Driver.Sql-010: the health field is read-modify-written from several poll loops ---- [Fact] diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlEquipmentTagParserTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlEquipmentTagParserTests.cs index 4734ee30..3433a4af 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlEquipmentTagParserTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlEquipmentTagParserTests.cs @@ -232,6 +232,21 @@ public class SqlEquipmentTagParserTests SqlEquipmentTagParser.TryParse(json, "raw", out _).ShouldBeFalse(); } + /// + /// R06:S-4. The deploy gate (EquipmentTagConfigInspector) reaches this through the parser: + /// a Query tag draws a warning naming the deferral (a rejection under TagConfigValidation=Error). + /// Control: a served key-value blob draws nothing. + /// + [Fact] + public void Inspect_theDeferredQueryModel_isNamedAtTheDeployGate() + { + SqlTagDefinitionFactory.Inspect("""{"driver":"Sql","model":"Query","table":"T","query":"SELECT 1"}""") + .ShouldHaveSingleItem().ShouldContain("Query"); + SqlTagDefinitionFactory.Inspect( + """{"driver":"Sql","model":"KeyValue","table":"T","keyColumn":"k","keyValue":"v","valueColumn":"c"}""") + .ShouldBeEmpty(); + } + [Theory] [InlineData("""{"driver":"Sql","model":"KeyValue","keyColumn":"k","keyValue":"v","valueColumn":"c"}""")] [InlineData("""{"driver":"Sql","model":"KeyValue","table":"","keyColumn":"k","keyValue":"v","valueColumn":"c"}""")]