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"}""")]