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
This commit is contained in:
Joseph Doherty
2026-10-22 16:04:59 -04:00
parent a7d21d5a65
commit 2a28c06fc7
3 changed files with 43 additions and 3 deletions
@@ -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)
@@ -464,6 +464,26 @@ public sealed class SqlDriverTests
}
}
/// <summary>
/// <b>R06:S-4.</b> A skipped tag's ledger reason (what /hosts shows) is the deploy gate's own explanation
/// — for a <c>Query</c> tag, that the model is deferred — instead of a generic "did not map". Control: a
/// well-formed tag is not skipped at all.
/// </summary>
[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]
@@ -232,6 +232,21 @@ public class SqlEquipmentTagParserTests
SqlEquipmentTagParser.TryParse(json, "raw", out _).ShouldBeFalse();
}
/// <summary>
/// <b>R06:S-4.</b> The deploy gate (<c>EquipmentTagConfigInspector</c>) reaches this through the parser:
/// a <c>Query</c> tag draws a warning naming the deferral (a rejection under TagConfigValidation=Error).
/// Control: a served key-value blob draws nothing.
/// </summary>
[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"}""")]