feat(pending-updates): short-circuit an unchanged fleet on a DB rowversion watermark (#62)
Owner decision (b), 2026-11-09. PendingUpdatesEvaluator re-flattened every deployed instance on each cache-TTL expiry even when nothing had changed. - Migration AddConfigGraphRowVersions: a non-concurrency rowversion column + index on the 18 configuration tables the evaluation reads (Templates + 5 member tables, Instances + 4 override/binding tables, DataConnections, DeployedConfigSnapshots, Sites, SharedScripts, ExternalSystemDefinitions, ExternalSystemMethods, DatabaseConnectionDefinitions). SQL Server only in the model; SQLite test contexts are untouched. Script: docs/plans/sql/AddConfigGraphRowVersions.sql. Joins the coordinated central-pair migration window (#107 item 4). - IConfigGraphWatermarkReader / ConfigGraphWatermarkReader: ONE query of MAX(RowVersion) + COUNT_BIG(*) per table (count catches deletes, which raise no rowversion). rowversion is engine-bumped for any writer, so a peer central node's edit moves it — not the per-process watermark MG-3 removed. - The secret store (outside the DB) gets its own stamp: SHA-256 over ListAsync(includeDeleted: true) metadata. - Evaluator re-serves the last CLEAN snapshot while watermark + secret stamp are unchanged; every PendingUpdatesFullEvaluationEvery'th (default 10, validated >= 1, 1 = off) is full regardless — the safety net for a same-KEK re-wrap or an input table missing from the set. Always full when the token is unavailable/unreadable, after an evaluation that could not compute a site's artifact hash, on Re-check, and after a deploy completion. It only re-serves a real earlier answer, never manufactures one. Tests: SQL Server (real migrated DB) — token stable without writes, moves for a peer-connection INSERT/UPDATE and a non-max DELETE, moves for a local EF write, a detached Update still saves (not a concurrency token), and a guard that every table the evaluator's repository reads touch is covered. Evaluator — reuse without flatten (re-served answer still shows the stale instance), watermark move, 4 secret changes, every-Nth, 5 no-token modes, unclean evaluation not reused, cache Re-check and deploy completion force full. Mutation-proven: token compare, clean check, cache force flag, row count, and an uncovered read each turn tests red. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #62: the <c>rowversion</c> shadow columns behind
|
||||
/// <see cref="Commons.Interfaces.Repositories.IConfigGraphWatermarkReader"/>.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>The covered set is exactly what <c>PendingUpdatesEvaluator</c> reads.</b> Instance
|
||||
/// staleness re-flattens each deployed instance (<c>FlatteningPipeline.FlattenCoreAsync</c>:
|
||||
/// the instance with its four override/binding collections, the template inheritance and
|
||||
/// composition graph with its five member collections, the site's data connections) and
|
||||
/// compares against the deployed snapshots; the artifact hash
|
||||
/// (<c>ArtifactDeploymentService.ComputeCurrentArtifactHashesAsync</c>) reads shared
|
||||
/// scripts, external systems + their methods, database connections and data connections,
|
||||
/// and compares against the hash stamped on each site row. The secret store, which also
|
||||
/// feeds the artifact hash, is outside this database and has its own stamp in the evaluator.
|
||||
/// A table added to either read path MUST be added here, or a change to it is only picked
|
||||
/// up by the evaluator's periodic forced full evaluation.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The columns are NOT concurrency tokens: they exist only to be compared as a maximum, and
|
||||
/// making them tokens would change the conflict behaviour of every save on these tables
|
||||
/// (a detached <c>Update</c> would carry a null original value and fail). They are SQL
|
||||
/// Server only — <see cref="ScadaBridgeDbContext"/> applies them only under that provider,
|
||||
/// so the SQLite test contexts never see a column SQLite cannot generate.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class ConfigGraphRowVersions
|
||||
{
|
||||
/// <summary>The shadow property / column name on every covered table.</summary>
|
||||
public const string ColumnName = "RowVersion";
|
||||
|
||||
/// <summary>The entity types whose tables are covered by the watermark.</summary>
|
||||
public static IReadOnlyList<Type> EntityTypes { get; } =
|
||||
[
|
||||
// Flatten inputs: the template graph.
|
||||
typeof(Template),
|
||||
typeof(TemplateAttribute),
|
||||
typeof(TemplateAlarm),
|
||||
typeof(TemplateScript),
|
||||
typeof(TemplateComposition),
|
||||
typeof(TemplateNativeAlarmSource),
|
||||
// Flatten inputs: the instance and its overrides/bindings.
|
||||
typeof(Instance),
|
||||
typeof(InstanceAttributeOverride),
|
||||
typeof(InstanceAlarmOverride),
|
||||
typeof(InstanceConnectionBinding),
|
||||
typeof(InstanceNativeAlarmSourceOverride),
|
||||
// Flatten input AND artifact input.
|
||||
typeof(DataConnection),
|
||||
// The deployed side of instance staleness.
|
||||
typeof(DeployedConfigSnapshot),
|
||||
// Artifact inputs, and the site row carrying the stamped artifact hash.
|
||||
typeof(Site),
|
||||
typeof(SharedScript),
|
||||
typeof(ExternalSystemDefinition),
|
||||
typeof(ExternalSystemMethod),
|
||||
typeof(DatabaseConnectionDefinition),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Adds the non-concurrency <c>rowversion</c> shadow column (and an index on it, so
|
||||
/// <c>MAX</c> is a seek and <c>COUNT_BIG</c> scans the narrowest index) to every covered table.
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder">The model builder.</param>
|
||||
public static void Apply(ModelBuilder modelBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(modelBuilder);
|
||||
foreach (var type in EntityTypes)
|
||||
{
|
||||
var entity = modelBuilder.Entity(type);
|
||||
// Spelled out rather than IsRowVersion(): that also makes the column a concurrency
|
||||
// token, and switching the token off afterwards drops the provider's rowversion
|
||||
// type mapping (the column would scaffold as a nullable varbinary(900)).
|
||||
entity.Property<byte[]>(ColumnName)
|
||||
.HasColumnType("rowversion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.IsConcurrencyToken(false);
|
||||
entity.HasIndex(ColumnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2381
File diff suppressed because it is too large
Load Diff
+393
@@ -0,0 +1,393 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Gitea #62: a <c>rowversion</c> column (+ index) on the 18 configuration tables that feed
|
||||
/// the pending-updates evaluation — the flatten inputs (Templates and their attribute /
|
||||
/// alarm / script / composition / native-alarm-source members, Instances and their four
|
||||
/// override/binding tables, DataConnections), the deployed side (DeployedConfigSnapshots)
|
||||
/// and the artifact-hash inputs (Sites, SharedScripts, ExternalSystemDefinitions,
|
||||
/// ExternalSystemMethods, DatabaseConnectionDefinitions). <c>PendingUpdatesEvaluator</c>
|
||||
/// short-circuits when MAX(RowVersion) + COUNT_BIG(*) over them is unchanged.
|
||||
/// <para>
|
||||
/// Purely additive: SQL Server fills a rowversion for every existing row as the column is
|
||||
/// added (a size-of-data operation on each table — these are configuration tables, small
|
||||
/// next to AuditLog). Old binaries ignore the column (it is never a concurrency token and
|
||||
/// is never written by the application). Ships in the coordinated central-pair migration
|
||||
/// window with the other pending migrations (ScadaBridge#107 item 4).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public partial class AddConfigGraphRowVersions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "TemplateScripts",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "Templates",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "TemplateNativeAlarmSources",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "TemplateCompositions",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "TemplateAttributes",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "TemplateAlarms",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "Sites",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "SharedScripts",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "Instances",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "InstanceNativeAlarmSourceOverrides",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "InstanceConnectionBindings",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "InstanceAttributeOverrides",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "InstanceAlarmOverrides",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "ExternalSystemMethods",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "ExternalSystemDefinitions",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "DeployedConfigSnapshots",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "DataConnections",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "RowVersion",
|
||||
table: "DatabaseConnectionDefinitions",
|
||||
type: "rowversion",
|
||||
rowVersion: true,
|
||||
nullable: false);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TemplateScripts_RowVersion",
|
||||
table: "TemplateScripts",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Templates_RowVersion",
|
||||
table: "Templates",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TemplateNativeAlarmSources_RowVersion",
|
||||
table: "TemplateNativeAlarmSources",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TemplateCompositions_RowVersion",
|
||||
table: "TemplateCompositions",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TemplateAttributes_RowVersion",
|
||||
table: "TemplateAttributes",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TemplateAlarms_RowVersion",
|
||||
table: "TemplateAlarms",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Sites_RowVersion",
|
||||
table: "Sites",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SharedScripts_RowVersion",
|
||||
table: "SharedScripts",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Instances_RowVersion",
|
||||
table: "Instances",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InstanceNativeAlarmSourceOverrides_RowVersion",
|
||||
table: "InstanceNativeAlarmSourceOverrides",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InstanceConnectionBindings_RowVersion",
|
||||
table: "InstanceConnectionBindings",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InstanceAttributeOverrides_RowVersion",
|
||||
table: "InstanceAttributeOverrides",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InstanceAlarmOverrides_RowVersion",
|
||||
table: "InstanceAlarmOverrides",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExternalSystemMethods_RowVersion",
|
||||
table: "ExternalSystemMethods",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExternalSystemDefinitions_RowVersion",
|
||||
table: "ExternalSystemDefinitions",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DeployedConfigSnapshots_RowVersion",
|
||||
table: "DeployedConfigSnapshots",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DataConnections_RowVersion",
|
||||
table: "DataConnections",
|
||||
column: "RowVersion");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DatabaseConnectionDefinitions_RowVersion",
|
||||
table: "DatabaseConnectionDefinitions",
|
||||
column: "RowVersion");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_TemplateScripts_RowVersion",
|
||||
table: "TemplateScripts");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Templates_RowVersion",
|
||||
table: "Templates");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_TemplateNativeAlarmSources_RowVersion",
|
||||
table: "TemplateNativeAlarmSources");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_TemplateCompositions_RowVersion",
|
||||
table: "TemplateCompositions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_TemplateAttributes_RowVersion",
|
||||
table: "TemplateAttributes");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_TemplateAlarms_RowVersion",
|
||||
table: "TemplateAlarms");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Sites_RowVersion",
|
||||
table: "Sites");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_SharedScripts_RowVersion",
|
||||
table: "SharedScripts");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Instances_RowVersion",
|
||||
table: "Instances");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_InstanceNativeAlarmSourceOverrides_RowVersion",
|
||||
table: "InstanceNativeAlarmSourceOverrides");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_InstanceConnectionBindings_RowVersion",
|
||||
table: "InstanceConnectionBindings");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_InstanceAttributeOverrides_RowVersion",
|
||||
table: "InstanceAttributeOverrides");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_InstanceAlarmOverrides_RowVersion",
|
||||
table: "InstanceAlarmOverrides");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ExternalSystemMethods_RowVersion",
|
||||
table: "ExternalSystemMethods");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ExternalSystemDefinitions_RowVersion",
|
||||
table: "ExternalSystemDefinitions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_DeployedConfigSnapshots_RowVersion",
|
||||
table: "DeployedConfigSnapshots");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_DataConnections_RowVersion",
|
||||
table: "DataConnections");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_DatabaseConnectionDefinitions_RowVersion",
|
||||
table: "DatabaseConnectionDefinitions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "TemplateScripts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "Templates");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "TemplateNativeAlarmSources");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "TemplateCompositions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "TemplateAttributes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "TemplateAlarms");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "Sites");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "SharedScripts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "Instances");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "InstanceNativeAlarmSourceOverrides");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "InstanceConnectionBindings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "InstanceAttributeOverrides");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "InstanceAlarmOverrides");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "ExternalSystemMethods");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "ExternalSystemDefinitions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "DeployedConfigSnapshots");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "DataConnections");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "DatabaseConnectionDefinitions");
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
@@ -229,6 +229,11 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeploymentId");
|
||||
@@ -236,6 +241,8 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.HasIndex("InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.ToTable("DeployedConfigSnapshots");
|
||||
});
|
||||
|
||||
@@ -408,11 +415,18 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.Property<TimeSpan>("RetryDelay")
|
||||
.HasColumnType("time");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.ToTable("DatabaseConnectionDefinitions");
|
||||
});
|
||||
|
||||
@@ -449,6 +463,11 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.Property<TimeSpan>("RetryDelay")
|
||||
.HasColumnType("time");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("TimeoutSeconds")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
@@ -459,6 +478,8 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.ToTable("ExternalSystemDefinitions");
|
||||
});
|
||||
|
||||
@@ -496,8 +517,15 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("nvarchar(4000)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.HasIndex("ExternalSystemDefinitionId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
@@ -586,6 +614,11 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.Property<int?>("AreaId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("SiteId")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -606,6 +639,8 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
|
||||
b.HasIndex("AreaId");
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.HasIndex("SiteId");
|
||||
|
||||
b.HasIndex("TemplateId");
|
||||
@@ -638,12 +673,19 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.Property<int?>("PriorityLevelOverride")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<string>("TriggerConfigurationOverride")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("nvarchar(4000)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.HasIndex("InstanceId", "AlarmCanonicalName")
|
||||
.IsUnique();
|
||||
|
||||
@@ -673,8 +715,15 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.Property<string>("OverrideValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.HasIndex("InstanceId", "AttributeName")
|
||||
.IsUnique();
|
||||
|
||||
@@ -704,10 +753,17 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.Property<int>("InstanceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DataConnectionId");
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.HasIndex("InstanceId", "AttributeName")
|
||||
.IsUnique();
|
||||
|
||||
@@ -736,6 +792,11 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.Property<bool?>("IsMesReportableOverride")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<string>("SourceCanonicalName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(400)
|
||||
@@ -747,6 +808,8 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.HasIndex("InstanceId", "SourceCanonicalName")
|
||||
.IsUnique();
|
||||
|
||||
@@ -1175,11 +1238,18 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("nvarchar(4000)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.ToTable("SharedScripts");
|
||||
});
|
||||
|
||||
@@ -1384,11 +1454,18 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("SiteId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.HasIndex("SiteId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
@@ -1435,6 +1512,11 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<string>("SiteIdentifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
@@ -1445,6 +1527,8 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.HasIndex("SiteIdentifier")
|
||||
.IsUnique();
|
||||
|
||||
@@ -1480,6 +1564,11 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.Property<int?>("ParentTemplateId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
@@ -1490,6 +1579,8 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
|
||||
b.HasIndex("ParentTemplateId");
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.ToTable("Templates");
|
||||
});
|
||||
|
||||
@@ -1528,6 +1619,11 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.Property<int>("PriorityLevel")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("TemplateId")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -1542,6 +1638,8 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.HasIndex("TemplateId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
@@ -1587,6 +1685,11 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("TemplateId")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -1595,6 +1698,8 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.HasIndex("TemplateId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
@@ -1617,6 +1722,11 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("TemplateId")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -1624,6 +1734,8 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
|
||||
b.HasIndex("ComposedTemplateId");
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.HasIndex("TemplateId", "InstanceName")
|
||||
.IsUnique();
|
||||
|
||||
@@ -1696,6 +1808,11 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<string>("SourceReference")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
@@ -1706,6 +1823,8 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.HasIndex("TemplateId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
@@ -1752,6 +1871,11 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("nvarchar(4000)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("TemplateId")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -1765,6 +1889,8 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RowVersion");
|
||||
|
||||
b.HasIndex("TemplateId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// SQL Server <see cref="IConfigGraphWatermarkReader"/> (Gitea #62): ONE query returning
|
||||
/// <c>MAX(RowVersion)</c> and <c>COUNT_BIG(*)</c> for every table in
|
||||
/// <see cref="ConfigGraphRowVersions.EntityTypes"/>, folded into an opaque token.
|
||||
///
|
||||
/// <para>
|
||||
/// Why both numbers: <c>rowversion</c> is a database-wide monotonic counter bumped by the
|
||||
/// engine on every INSERT and UPDATE — whoever issues it (the peer central node, a bundle
|
||||
/// import, an <c>ExecuteUpdate</c>, a hand-run script) — so any insert or update raises some
|
||||
/// table's maximum. A DELETE raises nothing, so the per-table row count catches it; a
|
||||
/// delete paired with an insert is caught by the insert's new maximum.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class ConfigGraphWatermarkReader : IConfigGraphWatermarkReader
|
||||
{
|
||||
private readonly ScadaBridgeDbContext _context;
|
||||
|
||||
/// <summary>Creates the reader over the scoped configuration context.</summary>
|
||||
/// <param name="context">The configuration database context.</param>
|
||||
public ConfigGraphWatermarkReader(ScadaBridgeDbContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> ReadAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// The columns only exist under SQL Server (ScadaBridgeDbContext.OnModelCreating).
|
||||
if (!_context.Database.IsSqlServer())
|
||||
return null;
|
||||
|
||||
var sql = BuildQuery(_context);
|
||||
var connection = _context.Database.GetDbConnection();
|
||||
var openedHere = connection.State != ConnectionState.Open;
|
||||
if (openedHere)
|
||||
await _context.Database.OpenConnectionAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
command.Transaction = _context.Database.CurrentTransaction?.GetDbTransaction();
|
||||
|
||||
var token = new StringBuilder();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
token.Append(reader.GetString(0)).Append(':');
|
||||
if (!reader.IsDBNull(1))
|
||||
token.Append(Convert.ToHexString((byte[])reader.GetValue(1)));
|
||||
token.Append(':')
|
||||
.Append(reader.GetInt64(2).ToString(CultureInfo.InvariantCulture))
|
||||
.Append(';');
|
||||
}
|
||||
|
||||
return token.ToString();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (openedHere)
|
||||
await _context.Database.CloseConnectionAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the single <c>UNION ALL</c> query from the EF model's table mappings, so a
|
||||
/// renamed table follows the model rather than a hard-coded name.
|
||||
/// </summary>
|
||||
/// <param name="context">The context whose model supplies the table names.</param>
|
||||
/// <returns>The SQL text.</returns>
|
||||
internal static string BuildQuery(DbContext context)
|
||||
{
|
||||
var parts = new List<string>(ConfigGraphRowVersions.EntityTypes.Count);
|
||||
foreach (var type in ConfigGraphRowVersions.EntityTypes)
|
||||
{
|
||||
var entity = context.Model.FindEntityType(type)
|
||||
?? throw new InvalidOperationException($"Entity {type.Name} is not in the model.");
|
||||
var table = entity.GetTableName()
|
||||
?? throw new InvalidOperationException($"Entity {type.Name} is not mapped to a table.");
|
||||
var schema = entity.GetSchema() ?? "dbo";
|
||||
parts.Add(
|
||||
$"SELECT N'{table}' AS [T], MAX([{ConfigGraphRowVersions.ColumnName}]) AS [V], COUNT_BIG(*) AS [C] " +
|
||||
$"FROM [{schema}].[{table}]");
|
||||
}
|
||||
|
||||
// ORDER BY: a UNION ALL has no guaranteed row order, and the token is compared as a string.
|
||||
return string.Join(" UNION ALL ", parts) + " ORDER BY [T];";
|
||||
}
|
||||
}
|
||||
@@ -186,6 +186,12 @@ public class ScadaBridgeDbContext : DbContext, IDataProtectionKeyContext
|
||||
|
||||
ApplySecretColumnEncryption(modelBuilder);
|
||||
|
||||
// Gitea #62: rowversion change-detection columns on the pending-updates input tables.
|
||||
// SQL Server only (true for production and the design-time factory, so the migration
|
||||
// and snapshot carry them); SQLite cannot generate a rowversion.
|
||||
if (Database.IsSqlServer())
|
||||
ConfigGraphRowVersions.Apply(modelBuilder);
|
||||
|
||||
NeutralizeSqlServerComputedColumnsForNonSqlServerProviders(modelBuilder);
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<ICentralUiRepository, CentralUiRepository>();
|
||||
services.AddScoped<ITemplateEngineRepository, TemplateEngineRepository>();
|
||||
services.AddScoped<IDeploymentManagerRepository, DeploymentManagerRepository>();
|
||||
services.AddScoped<IConfigGraphWatermarkReader, ConfigGraphWatermarkReader>();
|
||||
services.AddScoped<ISiteRepository, SiteRepository>();
|
||||
services.AddScoped<IExternalSystemRepository, ExternalSystemRepository>();
|
||||
services.AddScoped<INotificationRepository, NotificationRepository>();
|
||||
|
||||
Reference in New Issue
Block a user