diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.IntegrationTests/SqlServerReadTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.IntegrationTests/SqlServerReadTests.cs index ebf2d212..ff6271bc 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.IntegrationTests/SqlServerReadTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.IntegrationTests/SqlServerReadTests.cs @@ -58,6 +58,64 @@ public sealed class SqlServerReadTests driver.GetHostStatuses().ShouldHaveSingleItem().State.ShouldBe(HostState.Running); } + // ---- R06:S-1: the row cap, on real T-SQL ---- + + /// + /// A where-pair selector pointed at an events table (40 rows for one station) used to pull every + /// matching row on every poll. The plan now emits SELECT TOP (2) … — proven valid T-SQL here — + /// and the second row makes the tag publish BadConfigurationError while the driver stays + /// Healthy (the server answered). The positive control: a station with exactly one row reads Good + /// through the same capped statement. + /// + [Fact] + public async Task ReadAsync_realSqlServer_anAmbiguousSelectorIsRowCappedToAConfigurationError() + { + SkipUnlessLive(); + const string table = "RowCapEvents"; + + await using (var arrange = await _sql.OpenInspectionConnectionAsync()) + { + await using var command = arrange.CreateCommand(); + command.CommandText = string.Create(CultureInfo.InvariantCulture, $""" + IF OBJECT_ID(N'{SqlPollServerFixture.Schema}.{table}') IS NOT NULL DROP TABLE {SqlPollServerFixture.Schema}.{table}; + CREATE TABLE {SqlPollServerFixture.Schema}.{table} ( + {SqlPollServerFixture.WideRowSelectorColumn} int NOT NULL, + oven_temp float NULL, + {SqlPollServerFixture.TimestampColumn} datetime2 NOT NULL); + INSERT INTO {SqlPollServerFixture.Schema}.{table} VALUES (1, 21.5, '2026-07-24T10:00:00'); + DECLARE @i int = 0; + WHILE @i < 40 + BEGIN + INSERT INTO {SqlPollServerFixture.Schema}.{table} VALUES (7, @i, DATEADD(second, @i, '2026-07-24T10:00:00')); + SET @i += 1; + END + """); + await command.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); + } + + try + { + await using var driver = await StartAsync( + WideRowWhere("Sql/Events/Ambiguous", "oven_temp", "7", table), + WideRowWhere("Sql/Events/Clean", "oven_temp", "1", table)); + + var snapshots = await driver.ReadAsync( + ["Sql/Events/Ambiguous", "Sql/Events/Clean"], TestContext.Current.CancellationToken); + + snapshots[0].StatusCode.ShouldBe(SqlStatusCodes.BadConfigurationError); + snapshots[1].StatusCode.ShouldBe(SqlStatusCodes.Good); + Convert.ToDouble(snapshots[1].Value).ShouldBe(21.5); + driver.GetHealth().State.ShouldBe(DriverState.Healthy); + } + finally + { + await using var cleanup = await _sql.OpenInspectionConnectionAsync(); + await using var drop = cleanup.CreateCommand(); + drop.CommandText = $"DROP TABLE IF EXISTS {SqlPollServerFixture.Schema}.{table}"; + await drop.ExecuteNonQueryAsync(CancellationToken.None); + } + } + // ---- key-value model ---- /// The plan's headline case: a seeded key-value row round-trips as a Good snapshot.