using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Conformance;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests;
///
/// DeleteRaw is paged: DELETE TOP (n) batches, each with its trail rows, instead of one unbounded statement and
/// transaction (lock escalation on Sample, log growth, a CommandTimeout rolling the whole range back). The result, the
/// window bounds and the Part 11 trail are unchanged.
///
[Collection(SqlHistorianCollection.Name)]
public sealed class DeleteRawPagingTests(SqlHistorianFixture fx)
{
private static readonly DateTime T0 = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static async Task ScalarAsync(ReadTestDb db, string sql)
{
await using var c = await db.Conn.OpenAsync(CancellationToken.None);
await using var cmd = db.Conn.CreateCommand(c, sql);
return Convert.ToInt64(await cmd.ExecuteScalarAsync());
}
[Fact]
public async Task A_range_larger_than_a_batch_is_deleted_in_batches_with_every_trail_row()
{
var db = await ReadTestDb.CreateAsync(fx);
var u = new SqlServerHistorianDataUpdater(db.Conn, db.Boot, new SqlServerTagCatalog(db.Conn, db.Boot)) { DeleteRawBatchSize = 40 };
// Seeded with plain INSERTs (a 1,000-row HistoryUpdate MERGE is not what this test is about).
await db.SeedAsync("t", Enumerable.Range(0, 102)
.Select(i => (T0.AddSeconds(i), HistorianValue.FromDouble(i), ReadTestDb.Good)).ToArray());
var trailBefore = await ScalarAsync(db, $"SELECT COUNT(*) FROM {db.Conn.Q("SampleModified")}");
// [T0 + 1 s, T0 + 101 s): 100 rows — the first and the last sample lie outside the window.
(await u.DeleteRawAsync("t", T0.AddSeconds(1), T0.AddSeconds(101), ConformanceData.Editor, CancellationToken.None))
.ShouldBe(HistorianStatusCodes.Good);
u.DeleteRawBatches.ShouldBe(3, "40 + 40 + 20 — paged, not one statement");
(await ScalarAsync(db, $"SELECT COUNT(*) FROM {db.Conn.Q("Sample")}")).ShouldBe(2);
(await ScalarAsync(db, $"SELECT COUNT(*) FROM {db.Conn.Q("SampleModified")}")).ShouldBe(trailBefore + 100,
"every deleted row has its Delete trail row");
(await ScalarAsync(db, $"SELECT COUNT(*) FROM {db.Conn.Q("SampleModified")} WHERE UpdateType = {(byte)HistoryModificationType.Delete}"))
.ShouldBe(100);
// An empty range is still BadNoData, and an exact multiple of the batch size ends after one short batch.
(await u.DeleteRawAsync("t", T0.AddSeconds(1), T0.AddSeconds(101), ConformanceData.Editor, CancellationToken.None))
.ShouldBe(HistorianStatusCodes.BadNoData);
}
[Fact]
public async Task A_cancelled_delete_between_batches_stops_and_what_it_deleted_stays_audited()
{
var db = await ReadTestDb.CreateAsync(fx);
var u = new SqlServerHistorianDataUpdater(db.Conn, db.Boot, new SqlServerTagCatalog(db.Conn, db.Boot)) { DeleteRawBatchSize = 10 };
await db.SeedAsync("t", Enumerable.Range(0, 100)
.Select(i => (T0.AddSeconds(i), HistorianValue.FromDouble(i), ReadTestDb.Good)).ToArray());
using var cts = new CancellationTokenSource();
await cts.CancelAsync();
await Should.ThrowAsync(
() => u.DeleteRawAsync("t", T0, T0.AddSeconds(100), ConformanceData.Editor, cts.Token));
(await ScalarAsync(db, $"SELECT COUNT(*) FROM {db.Conn.Q("Sample")}")).ShouldBe(100, "cancelled before the first batch");
// Positive control: the same delete on a live token removes everything, auditing each row.
(await u.DeleteRawAsync("t", T0, T0.AddSeconds(100), ConformanceData.Editor, CancellationToken.None)).ShouldBe(HistorianStatusCodes.Good);
(await ScalarAsync(db, $"SELECT COUNT(*) FROM {db.Conn.Q("Sample")}")).ShouldBe(0);
u.DeleteRawBatches.ShouldBe(11);
}
}