846c8d6335
Every Part 9 transition used to re-serialize the alarm's whole comment trail into alarm_condition_state.comments_json and replicate it to the pair peer: O(n) per transition, O(n^2) over an alarm's life (measured: one ack at 10k comments shipped 1,080,553 oplog bytes; now ~1.6 KB, the same as at 10). - New replicated table alarm_condition_comment, PK (scripted_alarm_id, seq), seq = the entry's ordinal in the trail. Ordinal (not content) keying makes the two pair nodes' copies of one logical entry (each stamped by its own clock, since both nodes apply every command) collapse to one row under LWW. - LocalDbAlarmConditionStateStore.SaveAsync appends only entries past the stored MAX(seq) (INSERT OR IGNORE, append-only) and upserts a fixed-size condition row with comments_json = "[]", in one transaction. Remove drops the trail with the row. Load merges the table with any inline trail. - comments_json column kept (always "[]") so the condition table's shape and replication digest are unchanged and old-shape rows still load exactly. - AlarmConditionStateSchema.MigrateInlineComments: idempotent per-row move of inline trails, run by LocalDbSetup.OnReady after RegisterReplicated so the moved entries replicate. Comment table joins ReplicatedTables (now five). Tests: constant transition payload at 10 vs 10k comments (+ instrument positive control), old-shape back-compat load, migration idempotency / partial re-run / lazy move, OnReady boot migration captured in the oplog, and two loopback pair tests (A->B and B->A trail convergence; the shared entry converges to one copy, with a distinct-alarm positive control). Refs #748 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
65 lines
2.3 KiB
C#
65 lines
2.3 KiB
C#
using Microsoft.Data.Sqlite;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests;
|
|
|
|
/// <summary>
|
|
/// Verifies <see cref="AlarmConditionStateSchema.Apply"/> creates its table and is idempotent —
|
|
/// the DDL depends on nothing but a <see cref="SqliteConnection"/>, so a bare in-memory
|
|
/// connection is a faithful harness (no <c>zb_hlc_next()</c> UDF is involved in <c>CREATE TABLE</c>).
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AlarmConditionStateSchemaTests
|
|
{
|
|
private static SqliteConnection OpenInMemory()
|
|
{
|
|
var connection = new SqliteConnection("Data Source=:memory:");
|
|
connection.Open();
|
|
return connection;
|
|
}
|
|
|
|
/// <summary>A single Apply creates the table.</summary>
|
|
[Fact]
|
|
public void Apply_creates_the_state_table()
|
|
{
|
|
using var connection = OpenInMemory();
|
|
|
|
AlarmConditionStateSchema.Apply(connection);
|
|
|
|
using var cmd = connection.CreateCommand();
|
|
cmd.CommandText =
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = @Name";
|
|
cmd.Parameters.AddWithValue("@Name", AlarmConditionStateSchema.StateTable);
|
|
((long)cmd.ExecuteScalar()!).ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>Apply also creates the append-only comment table, keyed by alarm id + ordinal.</summary>
|
|
[Fact]
|
|
public void Apply_creates_the_comment_table_with_a_composite_key()
|
|
{
|
|
using var connection = OpenInMemory();
|
|
|
|
AlarmConditionStateSchema.Apply(connection);
|
|
|
|
using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = "SELECT name FROM pragma_table_info(@Name) WHERE pk > 0 ORDER BY pk";
|
|
cmd.Parameters.AddWithValue("@Name", AlarmConditionStateSchema.CommentsTable);
|
|
var pk = new List<string>();
|
|
using (var reader = cmd.ExecuteReader())
|
|
while (reader.Read()) pk.Add(reader.GetString(0));
|
|
|
|
pk.ShouldBe(["scripted_alarm_id", "seq"]);
|
|
}
|
|
|
|
/// <summary>Applying twice on the same connection does not throw (CREATE TABLE IF NOT EXISTS).</summary>
|
|
[Fact]
|
|
public void Apply_is_idempotent()
|
|
{
|
|
using var connection = OpenInMemory();
|
|
|
|
AlarmConditionStateSchema.Apply(connection);
|
|
Should.NotThrow(() => AlarmConditionStateSchema.Apply(connection));
|
|
}
|
|
}
|