feat(config): migration AddClusterNodeTransportPorts

Additive only — two AddColumn ops, no AlterColumn, so no accumulated model
drift is riding along with this change:

  ALTER TABLE [ClusterNode] ADD [AkkaPort] int NOT NULL DEFAULT 4053;
  ALTER TABLE [ClusterNode] ADD [GrpcPort] int NULL;

Adds a third test covering the DB-side default specifically. The entity
round-trip cannot see it: ClusterNode.AkkaPort's CLR initializer is also 4053,
so that assertion passes with the mapping default deleted. The new test inserts
through raw SQL with the column omitted, so only the schema can supply the
value — verified by setting defaultValue: 0, which turns exactly that one test
red and leaves the other two green.

Configuration.Tests 95/95.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
Joseph Doherty
2026-07-22 08:26:39 -04:00
parent 2bbb02713c
commit da74ebd696
4 changed files with 1788 additions and 0 deletions
@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
{
/// <inheritdoc />
public partial class AddClusterNodeTransportPorts : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "AkkaPort",
table: "ClusterNode",
type: "int",
nullable: false,
defaultValue: 4053);
migrationBuilder.AddColumn<int>(
name: "GrpcPort",
table: "ClusterNode",
type: "int",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AkkaPort",
table: "ClusterNode");
migrationBuilder.DropColumn(
name: "GrpcPort",
table: "ClusterNode");
}
}
}
@@ -47,6 +47,11 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<int>("AkkaPort")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(4053);
b.Property<string>("ApplicationUri")
.IsRequired()
.HasMaxLength(256)
@@ -76,6 +81,9 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
b.Property<bool>("Enabled")
.HasColumnType("bit");
b.Property<int?>("GrpcPort")
.HasColumnType("int");
b.Property<string>("Host")
.IsRequired()
.HasMaxLength(255)
@@ -49,6 +49,41 @@ public sealed class ClusterNodeTransportPortsTests(SchemaComplianceFixture fixtu
defaulted.GrpcPort.ShouldBeNull();
}
/// <summary>
/// Verifies the <b>DB-side</b> default on <c>AkkaPort</c> — the migration's
/// <c>defaultValue: 4053</c>, which is what rows created before the column existed inherit.
/// The entity round-trip above cannot see this: <see cref="ClusterNode.AkkaPort"/>'s CLR
/// initializer is also 4053, so that assertion passes with the mapping default deleted. This
/// one inserts through raw SQL with the column omitted, so only the schema can supply the value.
/// </summary>
[Fact]
public async Task AkkaPort_has_a_DB_side_default_of_4053()
{
await using var ctx = NewContext();
var clusterId = await SeedClusterAsync(ctx);
await using (var conn = fixture.OpenConnection())
{
var cmd = conn.CreateCommand();
cmd.CommandText = """
INSERT INTO dbo.ClusterNode (NodeId, ClusterId, Host, OpcUaPort, DashboardPort,
ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
VALUES (@nodeId, @clusterId, @host, 4840, 8081, @uri, 200, 1, 'raw-sql');
""";
cmd.Parameters.AddWithValue("@nodeId", $"{clusterId}-raw:4053");
cmd.Parameters.AddWithValue("@clusterId", clusterId);
cmd.Parameters.AddWithValue("@host", $"{clusterId}-raw");
cmd.Parameters.AddWithValue("@uri", $"urn:OtOpcUa:{clusterId}-raw");
await cmd.ExecuteNonQueryAsync();
}
await using var read = NewContext();
var row = await read.ClusterNodes.AsNoTracking()
.SingleAsync(n => n.NodeId == $"{clusterId}-raw:4053");
row.AkkaPort.ShouldBe(4053);
row.GrpcPort.ShouldBeNull();
}
/// <summary>
/// Verifies the columns are queryable server-side — Phase 2's contact-point refresh selects
/// them in SQL rather than materialising every node, so a client-side-evaluation regression
@@ -98,7 +133,9 @@ public sealed class ClusterNodeTransportPortsTests(SchemaComplianceFixture fixtu
Name = clusterId,
Enterprise = "zb",
Site = "ports-test",
// CK_ServerCluster_RedundancyMode_NodeCount: Warm/Hot require exactly 2 nodes.
RedundancyMode = RedundancyMode.Warm,
NodeCount = 2,
CreatedBy = nameof(ClusterNodeTransportPortsTests),
});
await ctx.SaveChangesAsync();