merge: SQL contracts review findings resolved (Driver.Sql.Contracts-001 scalar keyValue, -002 fleet-wide numeric-enum rejection)

This commit is contained in:
Joseph Doherty
2026-08-10 08:10:36 -04:00
12 changed files with 332 additions and 34 deletions
@@ -28,6 +28,55 @@ public class SqlEquipmentTagParserTests
def.Name.ShouldBe("Plant/Sql/dev1/Speed"); // Name == RawPath (forward-router key)
}
/// <summary>
/// Driver.Sql.Contracts-001. The two value-bearing fields are read the same way: a JSON string,
/// number or boolean is captured as text, because both end up BOUND as a <c>DbParameter</c> and
/// both are matched back as text — <c>SqlPollReader.IndexByKey</c> stringifies the key cell
/// precisely so an authored key can face an INTEGER key column. <c>keyValue</c> used to go through
/// the narrower string-only read, so a numerically-authored key refused the tag outright while the
/// identically-bound <c>whereValue</c> sailed through as the design doc's own example authors it.
/// </summary>
[Theory]
[InlineData("7", "7")]
[InlineData("-12.5", "-12.5")]
[InlineData("true", "true")]
public void TryParse_KeyValue_aNonStringScalarKey_isCapturedAsTextLikeWhereValue(string token, string expected)
{
var json =
$$"""{"driver":"Sql","model":"KeyValue","table":"dbo.TagValues","keyColumn":"station_id","keyValue":{{token}},"valueColumn":"num_value"}""";
SqlEquipmentTagParser.TryParse(json, "raw", out var def).ShouldBeTrue();
def.KeyValue.ShouldBe(expected);
}
/// <summary>
/// …and a non-scalar key is still refused — the widening is to the scalar shapes a parameter can
/// carry, not to anything at all.
/// </summary>
[Theory]
[InlineData("""{"driver":"Sql","model":"KeyValue","table":"T","keyColumn":"k","keyValue":{"a":1},"valueColumn":"c"}""")]
[InlineData("""{"driver":"Sql","model":"KeyValue","table":"T","keyColumn":"k","keyValue":[1],"valueColumn":"c"}""")]
[InlineData("""{"driver":"Sql","model":"KeyValue","table":"T","keyColumn":"k","keyValue":null,"valueColumn":"c"}""")]
[InlineData("""{"driver":"Sql","model":"KeyValue","table":"T","keyColumn":"k","valueColumn":"c"}""")]
public void TryParse_KeyValue_aNonScalarOrAbsentKey_rejectsTag(string json)
=> SqlEquipmentTagParser.TryParse(json, "raw", out _).ShouldBeFalse();
/// <summary>
/// The <c>Inspect</c> half of the same rule. A numerically-authored key is served, so the deploy
/// gate must say nothing about it; an absent one is refused, and the gate must still name it.
/// </summary>
[Fact]
public void Inspect_aNumericallyAuthoredKeyValue_drawsNoWarning()
=> SqlTagDefinitionFactory.Inspect(
"""{"driver":"Sql","model":"KeyValue","table":"T","keyColumn":"k","keyValue":7,"valueColumn":"c"}""")
.ShouldBeEmpty();
/// <summary>Falsifiability control for the assertion above: a genuinely absent key still warns.</summary>
[Fact]
public void Inspect_anAbsentKeyValue_stillNamesTheField()
=> SqlTagDefinitionFactory.Inspect(
"""{"driver":"Sql","model":"KeyValue","table":"T","keyColumn":"k","valueColumn":"c"}""")
.ShouldContain(w => w.Contains("keyValue"));
[Fact]
public void TryParse_invalidModelEnum_rejectsTag()
=> SqlEquipmentTagParser.TryParse(
@@ -196,4 +245,31 @@ public class SqlEquipmentTagParserTests
=> SqlEquipmentTagParser.TryParse(
"""{"driver":"Sql","model":"KeyValue","table":"T","keyColumn":"k","keyValue":"v","valueColumn":"c","type":"Nonsense"}""",
"raw", out _).ShouldBeFalse();
/// <summary>
/// Driver.Sql.Contracts-002. A numerically-spelled enum is not a member name, and the strict read
/// must refuse it exactly as it refuses a typo. It used to parse — <c>"99"</c> gave
/// <c>(DriverDataType)99</c>, a declared type nothing can map, and <c>SqlPollReader.TryCoerce</c>
/// then fell through its switch to the <c>String</c> arm and published a stringified cell under it.
/// </summary>
[Theory]
[InlineData("""{"driver":"Sql","model":"KeyValue","table":"T","keyColumn":"k","keyValue":"v","valueColumn":"c","type":"99"}""")]
[InlineData("""{"driver":"Sql","model":"KeyValue","table":"T","keyColumn":"k","keyValue":"v","valueColumn":"c","type":"8"}""")]
// "1" is SqlTagModel.WideRow's ordinal — an otherwise-complete WideRow blob, so the rejection can only
// come from the enum read and not from a missing field.
[InlineData("""{"driver":"Sql","model":"1","table":"T","columnName":"c","rowSelector":{"topByTimestamp":"ts"}}""")]
public void TryParse_aNumericallySpelledEnum_rejectsTag(string json)
=> SqlEquipmentTagParser.TryParse(json, "raw", out _).ShouldBeFalse();
/// <summary>
/// …and the deploy gate explains it in the same words every other invalid enum draws, rather than
/// falling through to the backstop warning that names no field at all.
/// </summary>
[Fact]
public void Inspect_aNumericallySpelledType_namesTheFieldAndTheValidMembers()
{
var warnings = SqlTagDefinitionFactory.Inspect(
"""{"driver":"Sql","model":"KeyValue","table":"T","keyColumn":"k","keyValue":"v","valueColumn":"c","type":"99"}""");
warnings.ShouldContain(w => w.Contains("'99'") && w.Contains("type") && w.Contains(nameof(DriverDataType.Float64)));
}
}