feat: publish atomic shadow snapshot generations
This commit is contained in:
@@ -2,6 +2,15 @@
|
||||
SET NOCOUNT ON;
|
||||
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
|
||||
|
||||
SELECT package_id, package_version, installed_at FROM alg.shadow_package_manifest;
|
||||
SELECT warehouse_id, snapshot_generation, snapshot_status, synchronized_at, source_signature, DATEDIFF(SECOND, synchronized_at, SYSUTCDATETIME()) AS generation_age_seconds FROM alg.shadow_snapshot_state;
|
||||
SELECT warehouse_id, snapshot_generation, COUNT_BIG(*) AS input_row_count FROM alg.shadow_inbound_request_snapshot GROUP BY warehouse_id, snapshot_generation;
|
||||
SELECT warehouse_id, instance_id, claimed_at, last_seen_at FROM alg.shadow_instance_heartbeat;
|
||||
SELECT warehouse_id, metric_name, bucket_upper_micros, metric_count, metric_total_micros, metric_max_micros FROM alg.shadow_runtime_metric;
|
||||
SELECT warehouse_id, snapshot_generation, COUNT_BIG(*) AS comparison_completeness FROM alg.shadow_outbound_request_snapshot GROUP BY warehouse_id, snapshot_generation;
|
||||
SELECT warehouse_id, COUNT_BIG(*) AS inventory_parity FROM alg.inventory_item_snapshot GROUP BY warehouse_id;
|
||||
SELECT request_session_id, blocking_session_id, wait_type, resource_description FROM sys.dm_exec_requests WHERE wait_type IS NOT NULL AND resource_description LIKE '%alg.%';
|
||||
|
||||
SELECT CAST(SERVERPROPERTY('ProductVersion') AS varchar(30)) AS product_version,
|
||||
DB_NAME() AS database_name,
|
||||
SYSUTCDATETIME() AS checked_at_utc;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,103 +1,24 @@
|
||||
/*
|
||||
Removes only the exact FT546 shadow package version after ownership checks.
|
||||
Export alg.decision_log and stop the shadow application first.
|
||||
No Legacy WMS business row is modified by this script.
|
||||
*/
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
IF SCHEMA_ID(N'alg') IS NULL
|
||||
THROW 51010, 'FT546 shadow uninstall refused: alg schema does not exist', 1;
|
||||
|
||||
IF OBJECT_ID(N'alg.shadow_package_manifest', N'U') IS NULL
|
||||
THROW 51011, 'FT546 shadow uninstall refused: shadow package manifest does not match', 1;
|
||||
|
||||
IF (SELECT COUNT(*) FROM alg.shadow_package_manifest
|
||||
WHERE package_id = 'FT546WMS_SHADOW_ONLY'
|
||||
AND package_version = '2026-08-12-1') <> 1
|
||||
OR (SELECT COUNT(*) FROM alg.shadow_package_manifest) <> 1
|
||||
THROW 51012, 'FT546 shadow uninstall refused: shadow package manifest does not match', 1;
|
||||
|
||||
DECLARE @expected_objects TABLE (
|
||||
object_name sysname NOT NULL PRIMARY KEY,
|
||||
object_type char(2) NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO @expected_objects (object_name, object_type) VALUES
|
||||
(N'shadow_package_manifest', 'U'),
|
||||
(N'warehouse_policy', 'U'),
|
||||
(N'project_policy', 'U'),
|
||||
(N'space_role', 'U'),
|
||||
(N'space_topology', 'U'),
|
||||
(N'inventory_item_snapshot', 'U'),
|
||||
(N'space_inventory_summary', 'U'),
|
||||
(N'lane_state', 'U'),
|
||||
(N'reservation', 'U'),
|
||||
(N'deep_workflow_lock', 'U'),
|
||||
(N'decision_log', 'U'),
|
||||
(N'v_inventory_item', 'V'),
|
||||
(N'v_active_job', 'V'),
|
||||
(N'v_space_state', 'V'),
|
||||
(N'usp_rebuild_space_topology', 'P'),
|
||||
(N'usp_sync_inventory_snapshot', 'P'),
|
||||
(N'usp_compute_lane_semantic_fingerprint', 'P'),
|
||||
(N'usp_rebuild_lane_state', 'P'),
|
||||
(N'usp_refresh_dirty_lanes', 'P');
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM sys.objects object
|
||||
LEFT JOIN @expected_objects expected
|
||||
ON expected.object_name = object.name AND expected.object_type = object.type
|
||||
WHERE object.schema_id = SCHEMA_ID(N'alg')
|
||||
AND object.is_ms_shipped = 0
|
||||
AND object.parent_object_id = 0
|
||||
AND expected.object_name IS NULL
|
||||
)
|
||||
THROW 51013, 'FT546 shadow uninstall refused: unknown object exists in alg schema', 1;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM @expected_objects expected
|
||||
LEFT JOIN sys.objects object
|
||||
ON object.schema_id = SCHEMA_ID(N'alg')
|
||||
AND object.name = expected.object_name
|
||||
AND object.type = expected.object_type
|
||||
WHERE object.object_id IS NULL
|
||||
)
|
||||
THROW 51014, 'FT546 shadow uninstall refused: expected object is missing or has wrong type', 1;
|
||||
|
||||
IF SCHEMA_ID(N'alg') IS NULL THROW 51010, 'FT546 shadow uninstall refused: alg schema does not exist', 1;
|
||||
IF OBJECT_ID(N'alg.shadow_package_manifest', N'U') IS NULL THROW 51011, 'FT546 shadow uninstall refused: shadow package manifest does not match', 1;
|
||||
IF (SELECT COUNT(*) FROM alg.shadow_package_manifest WHERE package_id='FT546WMS_SHADOW_ONLY' AND package_version='2026-09-03-3')<>1 OR (SELECT COUNT(*) FROM alg.shadow_package_manifest)<>1 THROW 51012, 'FT546 shadow uninstall refused: shadow package manifest does not match', 1;
|
||||
DECLARE @expected_objects TABLE (object_name sysname NOT NULL PRIMARY KEY, object_type char(2) NOT NULL);
|
||||
INSERT INTO @expected_objects VALUES
|
||||
(N'shadow_package_manifest','U'),(N'warehouse_policy','U'),(N'project_policy','U'),(N'space_role','U'),(N'space_topology','U'),(N'inventory_item_snapshot','U'),(N'space_inventory_summary','U'),(N'lane_state','U'),(N'reservation','U'),(N'deep_workflow_lock','U'),(N'decision_log','U'),(N'shadow_snapshot_state','U'),(N'cargo_space_snapshot','U'),(N'active_job_snapshot','U'),(N'shadow_inbound_request_snapshot','U'),(N'shadow_outbound_request_snapshot','U'),(N'shadow_runtime_metric','U'),(N'shadow_instance_heartbeat','U'),
|
||||
(N'v_inventory_item','V'),(N'v_active_job','V'),(N'v_space_state','V'),(N'usp_rebuild_space_topology','P'),(N'usp_sync_inventory_snapshot','P'),(N'usp_compute_lane_semantic_fingerprint','P'),(N'usp_rebuild_lane_state','P'),(N'usp_refresh_dirty_lanes','P'),(N'usp_claim_shadow_instance','P'),(N'usp_refresh_shadow_snapshot','P');
|
||||
IF EXISTS (SELECT 1 FROM sys.objects object LEFT JOIN @expected_objects expected ON expected.object_name = object.name AND expected.object_type = object.type WHERE object.schema_id = SCHEMA_ID(N'alg') AND object.is_ms_shipped = 0 AND object.parent_object_id = 0 AND expected.object_name IS NULL) THROW 51013, 'FT546 shadow uninstall refused: unknown object exists in alg schema', 1;
|
||||
IF EXISTS (SELECT 1 FROM @expected_objects expected LEFT JOIN sys.objects object ON object.schema_id=SCHEMA_ID(N'alg') AND object.name=expected.object_name AND object.type=expected.object_type WHERE object.object_id IS NULL) THROW 51014, 'FT546 shadow uninstall refused: expected object is missing or has wrong type', 1;
|
||||
BEGIN TRY
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DROP PROCEDURE alg.usp_refresh_dirty_lanes;
|
||||
DROP PROCEDURE alg.usp_rebuild_lane_state;
|
||||
DROP PROCEDURE alg.usp_compute_lane_semantic_fingerprint;
|
||||
DROP PROCEDURE alg.usp_sync_inventory_snapshot;
|
||||
DROP PROCEDURE alg.usp_rebuild_space_topology;
|
||||
|
||||
DROP VIEW alg.v_space_state;
|
||||
DROP VIEW alg.v_active_job;
|
||||
DROP VIEW alg.v_inventory_item;
|
||||
|
||||
DROP TABLE alg.decision_log;
|
||||
DROP TABLE alg.deep_workflow_lock;
|
||||
DROP TABLE alg.reservation;
|
||||
DROP TABLE alg.lane_state;
|
||||
DROP TABLE alg.space_inventory_summary;
|
||||
DROP TABLE alg.inventory_item_snapshot;
|
||||
DROP TABLE alg.space_topology;
|
||||
DROP TABLE alg.space_role;
|
||||
DROP TABLE alg.project_policy;
|
||||
DROP TABLE alg.warehouse_policy;
|
||||
DROP TABLE alg.shadow_package_manifest;
|
||||
|
||||
EXEC(N'DROP SCHEMA alg');
|
||||
COMMIT TRANSACTION;
|
||||
BEGIN TRANSACTION;
|
||||
DROP PROCEDURE alg.usp_refresh_shadow_snapshot; DROP PROCEDURE alg.usp_claim_shadow_instance; DROP PROCEDURE alg.usp_refresh_dirty_lanes; DROP PROCEDURE alg.usp_rebuild_lane_state; DROP PROCEDURE alg.usp_compute_lane_semantic_fingerprint; DROP PROCEDURE alg.usp_sync_inventory_snapshot; DROP PROCEDURE alg.usp_rebuild_space_topology;
|
||||
DROP VIEW alg.v_space_state; DROP VIEW alg.v_active_job; DROP VIEW alg.v_inventory_item;
|
||||
DROP TABLE alg.shadow_instance_heartbeat; DROP TABLE alg.shadow_runtime_metric; DROP TABLE alg.shadow_outbound_request_snapshot; DROP TABLE alg.shadow_inbound_request_snapshot; DROP TABLE alg.active_job_snapshot; DROP TABLE alg.cargo_space_snapshot; DROP TABLE alg.shadow_snapshot_state; DROP TABLE alg.decision_log; DROP TABLE alg.deep_workflow_lock; DROP TABLE alg.reservation; DROP TABLE alg.lane_state; DROP TABLE alg.space_inventory_summary; DROP TABLE alg.inventory_item_snapshot; DROP TABLE alg.space_topology; DROP TABLE alg.space_role; DROP TABLE alg.project_policy; DROP TABLE alg.warehouse_policy; DROP TABLE alg.shadow_package_manifest;
|
||||
DROP SCHEMA alg;
|
||||
COMMIT TRANSACTION;
|
||||
END TRY
|
||||
BEGIN CATCH
|
||||
IF @@TRANCOUNT > 0
|
||||
ROLLBACK TRANSACTION;
|
||||
THROW;
|
||||
IF @@TRANCOUNT>0 ROLLBACK TRANSACTION;
|
||||
THROW;
|
||||
END CATCH;
|
||||
GO
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,36 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ShadowSqlServerMigrationScriptTest {
|
||||
private static final Set<String> ALLOWED_INSTALL_OBJECTS = Set.of(
|
||||
"table alg.shadow_package_manifest",
|
||||
"table alg.warehouse_policy",
|
||||
"table alg.project_policy",
|
||||
"table alg.space_role",
|
||||
"table alg.space_topology",
|
||||
"table alg.inventory_item_snapshot",
|
||||
"table alg.space_inventory_summary",
|
||||
"table alg.lane_state",
|
||||
"table alg.reservation",
|
||||
"table alg.deep_workflow_lock",
|
||||
"table alg.decision_log",
|
||||
"table alg.shadow_snapshot_state",
|
||||
"table alg.cargo_space_snapshot",
|
||||
"table alg.active_job_snapshot",
|
||||
"table alg.shadow_inbound_request_snapshot",
|
||||
"table alg.shadow_outbound_request_snapshot",
|
||||
"table alg.shadow_runtime_metric",
|
||||
"table alg.shadow_instance_heartbeat",
|
||||
"view alg.v_inventory_item",
|
||||
"view alg.v_active_job",
|
||||
"view alg.v_space_state",
|
||||
"procedure alg.usp_rebuild_space_topology",
|
||||
"procedure alg.usp_sync_inventory_snapshot",
|
||||
"procedure alg.usp_compute_lane_semantic_fingerprint",
|
||||
"procedure alg.usp_rebuild_lane_state",
|
||||
"procedure alg.usp_refresh_dirty_lanes",
|
||||
"procedure alg.usp_claim_shadow_instance",
|
||||
"procedure alg.usp_refresh_shadow_snapshot");
|
||||
|
||||
private static final Set<String> V2_OBJECTS = Set.of(
|
||||
"table alg.shadow_package_manifest",
|
||||
"table alg.warehouse_policy",
|
||||
"table alg.project_policy",
|
||||
@@ -71,6 +101,11 @@ class ShadowSqlServerMigrationScriptTest {
|
||||
Set<String> removed = droppedObjects(resource("db/sqlserver/uninstall-shadow.sql"));
|
||||
|
||||
assertThat(removed).containsExactlyInAnyOrderElementsOf(installed);
|
||||
|
||||
String uninstall = normalize(resource("db/sqlserver/uninstall-shadow.sql"));
|
||||
installed.forEach(object -> assertThat(objectOccurrences(uninstall, object))
|
||||
.as("uninstall should drop %s exactly once", object)
|
||||
.isEqualTo(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -98,10 +133,118 @@ class ShadowSqlServerMigrationScriptTest {
|
||||
"inbound_compare",
|
||||
"outbound_compare",
|
||||
"from sys.dm_tran_locks",
|
||||
"from alg.shadow_snapshot_state",
|
||||
"from alg.shadow_runtime_metric",
|
||||
"from alg.shadow_instance_heartbeat");
|
||||
assertThat(diagnostics).doesNotContain("insert ", "update ", "delete ", "merge ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void v3InstallUpgradeAndUninstallUseExactManifestVersionsAndObjects() throws IOException {
|
||||
String install = normalize(resource("db/sqlserver/install-shadow.sql"));
|
||||
String upgrade = normalize(resource("db/sqlserver/upgrade-shadow-v2-to-v3.sql"));
|
||||
String uninstall = normalize(resource("db/sqlserver/uninstall-shadow.sql"));
|
||||
|
||||
assertThat(createdObjects(install)).contains(
|
||||
"table alg.shadow_snapshot_state",
|
||||
"table alg.cargo_space_snapshot",
|
||||
"table alg.active_job_snapshot",
|
||||
"table alg.shadow_inbound_request_snapshot",
|
||||
"table alg.shadow_outbound_request_snapshot",
|
||||
"table alg.shadow_runtime_metric",
|
||||
"table alg.shadow_instance_heartbeat",
|
||||
"procedure alg.usp_claim_shadow_instance",
|
||||
"procedure alg.usp_refresh_shadow_snapshot");
|
||||
assertThat(install).contains("'ft546wms_shadow_only', '2026-09-03-3'");
|
||||
assertThat(upgrade).contains("'2026-08-12-1'").contains("'2026-09-03-3'");
|
||||
assertThat(uninstall).contains("'2026-09-03-3'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void upgradeValidatesTheCompleteV2AllowListBeforeAnyAlter() throws IOException {
|
||||
String upgrade = normalize(resource("db/sqlserver/upgrade-shadow-v2-to-v3.sql"));
|
||||
int firstAlter = upgrade.indexOf("alter ");
|
||||
|
||||
assertThat(firstAlter).isGreaterThanOrEqualTo(0);
|
||||
V2_OBJECTS.forEach(object -> assertThat(upgrade.substring(0, firstAlter))
|
||||
.as("v2 object %s must be validated before DDL", object)
|
||||
.contains(object.substring(object.indexOf(' ') + 1)));
|
||||
assertThat(upgrade.substring(0, firstAlter)).contains(
|
||||
"object.parent_object_id = 0",
|
||||
"unknown object exists in alg schema",
|
||||
"expected object is missing or has wrong type");
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshCopiesInputsChecksSourceStabilityAndPublishesInsideOneTransaction() throws IOException {
|
||||
String install = normalize(resource("db/sqlserver/install-shadow.sql"));
|
||||
String refresh = install.substring(install.indexOf("create procedure alg.usp_refresh_shadow_snapshot"));
|
||||
|
||||
int xactAbort = refresh.indexOf("set xact_abort on");
|
||||
int begin = refresh.indexOf("begin transaction");
|
||||
int inputRefresh = refresh.indexOf("insert into alg.cargo_space_snapshot");
|
||||
int publish = refresh.indexOf("merge alg.shadow_snapshot_state with (holdlock)");
|
||||
int commit = refresh.indexOf("commit transaction");
|
||||
|
||||
assertThat(xactAbort).isGreaterThanOrEqualTo(0);
|
||||
assertThat(begin).isGreaterThan(xactAbort);
|
||||
assertThat(inputRefresh).isGreaterThan(begin);
|
||||
assertThat(publish).isGreaterThan(inputRefresh);
|
||||
assertThat(commit).isGreaterThan(publish);
|
||||
assertThat(refresh).contains(
|
||||
"from dbo.bas_cargo_space",
|
||||
"from dbo.bas_stock",
|
||||
"from dbo.mydo_job");
|
||||
assertThat(diagnostics).doesNotContain("insert ", "update ", "delete ", "merge ");
|
||||
"from dbo.mydo_job",
|
||||
"from dbo.mydo_job_detail",
|
||||
"from dbo.in_stockin_detail",
|
||||
"from dbo.out_stockout_detail",
|
||||
"hashbytes('sha2_256'",
|
||||
"@pre_source_count",
|
||||
"@post_source_count",
|
||||
"@pre_source_signature",
|
||||
"@post_source_signature",
|
||||
"throw 51025",
|
||||
"when matched then update set",
|
||||
"snapshot_generation = @snapshot_generation");
|
||||
}
|
||||
|
||||
@Test
|
||||
void plannerViewsContainOnlyGenerationQualifiedAlgSnapshots() throws IOException {
|
||||
String install = normalize(resource("db/sqlserver/install-shadow.sql"));
|
||||
|
||||
assertThat(viewBody(install, "v_inventory_item")).doesNotContain("dbo.");
|
||||
assertThat(viewBody(install, "v_active_job")).doesNotContain("dbo.");
|
||||
assertThat(viewBody(install, "v_space_state")).doesNotContain("dbo.");
|
||||
assertThat(install).contains(
|
||||
"snapshot_generation",
|
||||
"where snapshot_generation = published_snapshot.snapshot_generation");
|
||||
}
|
||||
|
||||
@Test
|
||||
void v3UpgradePreservesDecisionLogAndDoesNotRefreshAtInstallTime() throws IOException {
|
||||
String install = normalize(resource("db/sqlserver/install-shadow.sql"));
|
||||
String upgrade = normalize(resource("db/sqlserver/upgrade-shadow-v2-to-v3.sql"));
|
||||
|
||||
assertThat(install).doesNotContain(
|
||||
"if not exists (select 1 from alg.space_topology)",
|
||||
"exec alg.usp_refresh_shadow_snapshot");
|
||||
assertThat(upgrade).contains("decision_log").doesNotContain(
|
||||
"drop table alg.decision_log",
|
||||
"delete from alg.decision_log",
|
||||
"truncate table alg.decision_log");
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimUsesOnlyPackageHeartbeatAndA120SecondStaleThreshold() throws IOException {
|
||||
String install = normalize(resource("db/sqlserver/install-shadow.sql"));
|
||||
String claim = batchBody(install, "create procedure alg.usp_claim_shadow_instance");
|
||||
|
||||
assertThat(claim).contains(
|
||||
"alg.shadow_instance_heartbeat",
|
||||
"with (updlock, holdlock)",
|
||||
"dateadd(second, -120",
|
||||
"claimed");
|
||||
assertThat(claim).doesNotContain("dbo.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -121,7 +264,7 @@ class ShadowSqlServerMigrationScriptTest {
|
||||
"throw 51000",
|
||||
"create table alg.shadow_package_manifest",
|
||||
"ft546wms_shadow_only",
|
||||
"2026-08-12-1");
|
||||
"2026-09-03-3");
|
||||
assertThat(install.indexOf("throw 51000")).isLessThan(install.indexOf("create schema alg"));
|
||||
}
|
||||
|
||||
@@ -132,7 +275,7 @@ class ShadowSqlServerMigrationScriptTest {
|
||||
|
||||
assertThat(uninstall).contains(
|
||||
"ft546wms_shadow_only",
|
||||
"2026-08-12-1",
|
||||
"2026-09-03-3",
|
||||
"shadow package manifest does not match",
|
||||
"unknown object exists in alg schema",
|
||||
"object.parent_object_id = 0",
|
||||
@@ -152,6 +295,17 @@ class ShadowSqlServerMigrationScriptTest {
|
||||
"(?i)drop\\s+(table|view|procedure|trigger)\\s+([a-z0-9_]+\\.[a-z0-9_]+)"));
|
||||
}
|
||||
|
||||
private long objectOccurrences(String sql, String object) {
|
||||
String[] parts = object.split(" ");
|
||||
Matcher matcher = Pattern.compile("(?i)\\b" + parts[0] + "\\s+" + parts[1].replace(".", "\\."))
|
||||
.matcher(sql);
|
||||
long count = 0;
|
||||
while (matcher.find()) {
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private Set<String> objects(String sql, Pattern pattern) {
|
||||
Matcher matcher = pattern.matcher(sql);
|
||||
Set<String> objects = new LinkedHashSet<>();
|
||||
@@ -165,6 +319,22 @@ class ShadowSqlServerMigrationScriptTest {
|
||||
return sql.toLowerCase(Locale.ROOT).replaceAll("\\s+", " ");
|
||||
}
|
||||
|
||||
private String viewBody(String sql, String viewName) {
|
||||
int start = sql.indexOf("create view alg." + viewName + " as");
|
||||
assertThat(start).as("view %s", viewName).isGreaterThanOrEqualTo(0);
|
||||
return batchBody(sql, "create view alg." + viewName + " as");
|
||||
}
|
||||
|
||||
private String batchBody(String sql, String batchStart) {
|
||||
int start = sql.indexOf(batchStart);
|
||||
assertThat(start).as("batch %s", batchStart).isGreaterThanOrEqualTo(0);
|
||||
int end = sql.indexOf(" go ", start);
|
||||
if (end < 0) {
|
||||
end = sql.length();
|
||||
}
|
||||
return sql.substring(start, end);
|
||||
}
|
||||
|
||||
private String resource(String name) throws IOException {
|
||||
try (var stream = getClass().getClassLoader().getResourceAsStream(name)) {
|
||||
assertThat(stream).as("resource %s", name).isNotNull();
|
||||
|
||||
Reference in New Issue
Block a user