feat: add shadow-only SQL Server package
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
package com.ruoyi.algorithm.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ShadowSqlServerMigrationScriptTest {
|
||||
private static final Set<String> ALLOWED_INSTALL_OBJECTS = Set.of(
|
||||
"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",
|
||||
"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");
|
||||
|
||||
@Test
|
||||
void installCreatesOnlyAllowListedShadowObjects() throws IOException {
|
||||
String install = resource("db/sqlserver/install-shadow.sql");
|
||||
|
||||
assertThat(createdObjects(install)).containsExactlyInAnyOrderElementsOf(ALLOWED_INSTALL_OBJECTS);
|
||||
assertThat(install.toLowerCase(Locale.ROOT)).doesNotContain(
|
||||
"usp_try_reserve_inbound",
|
||||
"usp_release_reservation",
|
||||
"usp_acquire_deep_workflow_lock",
|
||||
"usp_release_deep_workflow_lock",
|
||||
"usp_complete_move_job",
|
||||
"usp_sync_replay_sequences",
|
||||
"tidy_",
|
||||
"outbound_workflow",
|
||||
"proc_yk_complete");
|
||||
}
|
||||
|
||||
@Test
|
||||
void installNeverMutatesLegacyBusinessTables() throws IOException {
|
||||
String install = normalize(resource("db/sqlserver/install-shadow.sql"));
|
||||
|
||||
assertThat(install).doesNotContain(
|
||||
"insert into dbo.",
|
||||
"update dbo.",
|
||||
"delete from dbo.",
|
||||
"merge dbo.",
|
||||
"create trigger",
|
||||
"on dbo.",
|
||||
"dirty_lane_queue");
|
||||
}
|
||||
|
||||
@Test
|
||||
void installAndUninstallOwnExactlyTheSameObjects() throws IOException {
|
||||
Set<String> installed = createdObjects(resource("db/sqlserver/install-shadow.sql"));
|
||||
Set<String> removed = droppedObjects(resource("db/sqlserver/uninstall-shadow.sql"));
|
||||
|
||||
assertThat(removed).containsExactlyInAnyOrderElementsOf(installed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshPerformsFullSynchronizationWithoutLegacyTriggers() throws IOException {
|
||||
String install = normalize(resource("db/sqlserver/install-shadow.sql"));
|
||||
String refresh = install.substring(install.indexOf("create procedure alg.usp_refresh_dirty_lanes"));
|
||||
|
||||
assertThat(refresh).contains(
|
||||
"exec alg.usp_rebuild_space_topology",
|
||||
"exec alg.usp_sync_inventory_snapshot",
|
||||
"exec alg.usp_rebuild_lane_state");
|
||||
assertThat(refresh).doesNotContain("dirty_lane_queue", "tidy_lane_queue");
|
||||
assertThat(install).contains(
|
||||
"exec alg.usp_rebuild_space_topology",
|
||||
"exec alg.usp_sync_inventory_snapshot",
|
||||
"exec alg.usp_rebuild_lane_state");
|
||||
}
|
||||
|
||||
@Test
|
||||
void diagnosticsSupportsSafetyChecksAndDecisionExport() throws IOException {
|
||||
String diagnostics = normalize(resource("db/sqlserver/diagnostics-shadow.sql"));
|
||||
|
||||
assertThat(diagnostics).contains(
|
||||
"from alg.decision_log",
|
||||
"inbound_compare",
|
||||
"outbound_compare",
|
||||
"from sys.dm_tran_locks",
|
||||
"from dbo.bas_cargo_space",
|
||||
"from dbo.bas_stock",
|
||||
"from dbo.mydo_job");
|
||||
assertThat(diagnostics).doesNotContain("insert ", "update ", "delete ", "merge ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void installationAndDiagnosticsReadLegacyWithoutTakingSharedLocks() throws IOException {
|
||||
String readUncommitted = "set transaction isolation level read uncommitted";
|
||||
|
||||
assertThat(normalize(resource("db/sqlserver/install-shadow.sql"))).contains(readUncommitted);
|
||||
assertThat(normalize(resource("db/sqlserver/diagnostics-shadow.sql"))).contains(readUncommitted);
|
||||
}
|
||||
|
||||
private Set<String> createdObjects(String sql) {
|
||||
return objects(sql, Pattern.compile(
|
||||
"(?i)create\\s+(table|view|procedure|trigger)\\s+([a-z0-9_]+\\.[a-z0-9_]+)"));
|
||||
}
|
||||
|
||||
private Set<String> droppedObjects(String sql) {
|
||||
return objects(sql, Pattern.compile(
|
||||
"(?i)drop\\s+(table|view|procedure|trigger)\\s+([a-z0-9_]+\\.[a-z0-9_]+)"));
|
||||
}
|
||||
|
||||
private Set<String> objects(String sql, Pattern pattern) {
|
||||
Matcher matcher = pattern.matcher(sql);
|
||||
Set<String> objects = new LinkedHashSet<>();
|
||||
while (matcher.find()) {
|
||||
objects.add((matcher.group(1) + " " + matcher.group(2)).toLowerCase(Locale.ROOT));
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
private String normalize(String sql) {
|
||||
return sql.toLowerCase(Locale.ROOT).replaceAll("\\s+", " ");
|
||||
}
|
||||
|
||||
private String resource(String name) throws IOException {
|
||||
try (var stream = getClass().getClassLoader().getResourceAsStream(name)) {
|
||||
assertThat(stream).as("resource %s", name).isNotNull();
|
||||
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
package com.ruoyi.algorithm.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SqlServerMigrationScriptTest {
|
||||
@Test
|
||||
void installDefinesPersistentOutboundWorkflowSchema() throws IOException {
|
||||
String install = resource("db/sqlserver/install.sql");
|
||||
|
||||
assertThat(install).contains(
|
||||
"CREATE TABLE alg.outbound_workflow (",
|
||||
"CREATE TABLE alg.outbound_workflow_step (",
|
||||
"CREATE TABLE alg.outbound_workflow_space (",
|
||||
"CK_alg_outbound_workflow_status",
|
||||
"CK_alg_outbound_workflow_step_phase",
|
||||
"CK_alg_outbound_workflow_step_status",
|
||||
"UX_alg_outbound_workflow_active_request",
|
||||
"UX_alg_outbound_workflow_step_wms_job",
|
||||
"UX_alg_outbound_workflow_space_active");
|
||||
}
|
||||
|
||||
@Test
|
||||
void installDefinesDirtyLaneQueueRefreshAndAllMutationSources() throws IOException {
|
||||
String install = resource("db/sqlserver/install.sql");
|
||||
|
||||
assertThat(install).contains(
|
||||
"CREATE TABLE alg.dirty_lane_queue (",
|
||||
"CREATE PROCEDURE alg.usp_mark_dirty_lane",
|
||||
"CREATE PROCEDURE alg.usp_refresh_dirty_lanes",
|
||||
"CREATE TRIGGER dbo.trg_alg_dirty_bas_stock",
|
||||
"CREATE TRIGGER dbo.trg_alg_dirty_bas_cargo_space",
|
||||
"CREATE TRIGGER dbo.trg_alg_dirty_mydo_job",
|
||||
"CREATE TRIGGER alg.trg_alg_dirty_space_role",
|
||||
"CREATE TRIGGER alg.trg_alg_dirty_project_policy",
|
||||
"CREATE TRIGGER alg.trg_alg_dirty_warehouse_policy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void installDefinesPersistentTidyRuntimeSchemaAndConstraints() throws IOException {
|
||||
String install = resource("db/sqlserver/install.sql");
|
||||
|
||||
assertThat(install).contains(
|
||||
"CREATE TABLE alg.tidy_mode (",
|
||||
"CREATE TABLE alg.tidy_lane_queue (",
|
||||
"CREATE TABLE alg.tidy_workflow (",
|
||||
"CREATE TABLE alg.tidy_step (",
|
||||
"CREATE TABLE alg.tidy_space_lock (",
|
||||
"CREATE TABLE alg.tidy_callback_event (",
|
||||
"CK_alg_tidy_mode_effective_state",
|
||||
"CK_alg_tidy_workflow_origin",
|
||||
"CK_alg_tidy_workflow_status",
|
||||
"CK_alg_tidy_space_lock_resource_type",
|
||||
"UX_alg_tidy_space_lock_active",
|
||||
"UX_alg_tidy_callback_event_key",
|
||||
"UX_alg_tidy_workflow_active_auto",
|
||||
"FK_alg_tidy_callback_event_step_workflow");
|
||||
}
|
||||
|
||||
@Test
|
||||
void installDefinesVersionedTidyLaneClaimAndSequenceSynchronization() throws IOException {
|
||||
String install = resource("db/sqlserver/install.sql");
|
||||
|
||||
assertThat(install).contains(
|
||||
"CREATE PROCEDURE alg.usp_claim_tidy_lane",
|
||||
"@eligible_lane_codes varchar(max) = NULL",
|
||||
"STRING_SPLIT(@eligible_lane_codes, '','')",
|
||||
"CREATE PROCEDURE alg.usp_release_tidy_lane",
|
||||
"CREATE PROCEDURE alg.usp_sync_replay_sequences",
|
||||
"sp_getapplock",
|
||||
"CK202607210002");
|
||||
}
|
||||
|
||||
@Test
|
||||
void installCompletesAlgorithmOwnedMoveJobsWithoutChangingTheLegacyProcedure() throws IOException {
|
||||
String install = resource("db/sqlserver/install.sql");
|
||||
|
||||
assertThat(install).contains(
|
||||
"CREATE PROCEDURE alg.usp_complete_move_job",
|
||||
"in_or_out = ''28''",
|
||||
"UPDATE dbo.bas_stock",
|
||||
"INSERT INTO dbo.mydo_job_finish",
|
||||
"DELETE FROM dbo.mydo_job");
|
||||
assertThat(install).doesNotContain("ALTER PROCEDURE dbo.proc_yk_complete");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dirtyLaneRefreshPublishesRebuiltLanesToPersistentTidyQueue() throws IOException {
|
||||
String install = resource("db/sqlserver/install.sql");
|
||||
String procedure = install.substring(
|
||||
install.indexOf("CREATE PROCEDURE alg.usp_refresh_dirty_lanes"),
|
||||
install.indexOf("IF OBJECT_ID(N'alg.usp_try_reserve_inbound'"));
|
||||
|
||||
int rebuildLaneState = procedure.lastIndexOf("EXEC alg.usp_rebuild_lane_state");
|
||||
int publishTidyLane = procedure.indexOf("MERGE alg.tidy_lane_queue WITH (HOLDLOCK)");
|
||||
|
||||
assertThat(rebuildLaneState).isGreaterThanOrEqualTo(0);
|
||||
assertThat(publishTidyLane).isGreaterThan(rebuildLaneState);
|
||||
assertThat(procedure).contains(
|
||||
"dirty_version = target.dirty_version + 1",
|
||||
"queue_status = ''PENDING''");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dirtyLaneRefreshReleasesQueueLocksBeforeReadingWarehouseTablesAndRequeuesFailures()
|
||||
throws IOException {
|
||||
String install = resource("db/sqlserver/install.sql");
|
||||
String procedure = install.substring(
|
||||
install.indexOf("CREATE PROCEDURE alg.usp_refresh_dirty_lanes"),
|
||||
install.indexOf("IF OBJECT_ID(N'alg.usp_try_reserve_inbound'"));
|
||||
|
||||
int claimDelete = procedure.indexOf("DELETE queue");
|
||||
int claimCommit = procedure.indexOf("COMMIT TRANSACTION;", claimDelete);
|
||||
int warehouseRead = procedure.indexOf("FROM dbo.bas_cargo_space", claimDelete);
|
||||
|
||||
assertThat(claimDelete).isGreaterThanOrEqualTo(0);
|
||||
assertThat(claimCommit).isBetween(claimDelete + 1, warehouseRead - 1);
|
||||
assertThat(procedure).contains(
|
||||
"BEGIN TRY",
|
||||
"BEGIN CATCH",
|
||||
"MERGE alg.dirty_lane_queue WITH (HOLDLOCK) AS target",
|
||||
"THROW;");
|
||||
}
|
||||
|
||||
@Test
|
||||
void topologyRebuildExcludesUnconfiguredCargoSpacesAndDiagnosticsReportsThem()
|
||||
throws IOException {
|
||||
String install = resource("db/sqlserver/install.sql");
|
||||
String diagnostics = resource("db/sqlserver/diagnostics.sql");
|
||||
String procedure = install.substring(
|
||||
install.indexOf("CREATE PROCEDURE alg.usp_rebuild_space_topology"),
|
||||
install.indexOf("IF OBJECT_ID(N'alg.usp_sync_inventory_snapshot'"));
|
||||
|
||||
assertThat(procedure).contains(
|
||||
"NULLIF(LTRIM(RTRIM(group_coding_no)), '''') IS NOT NULL",
|
||||
"TRY_CONVERT(int, group_coding) IS NOT NULL");
|
||||
assertThat(diagnostics).contains(
|
||||
"excluded_from_algorithm_topology_count",
|
||||
"group_coding_no IS NULL OR LTRIM(RTRIM(group_coding_no)) = ''",
|
||||
"TRY_CONVERT(int, group_coding) IS NULL");
|
||||
}
|
||||
|
||||
@Test
|
||||
void diagnosticsAndUninstallCoverNewRuntimeObjects() throws IOException {
|
||||
String diagnostics = resource("db/sqlserver/diagnostics.sql");
|
||||
String uninstall = resource("db/sqlserver/uninstall.sql");
|
||||
|
||||
assertThat(diagnostics).contains(
|
||||
"FROM alg.dirty_lane_queue",
|
||||
"FROM alg.outbound_workflow",
|
||||
"FROM alg.tidy_mode",
|
||||
"FROM alg.tidy_lane_queue",
|
||||
"FROM alg.tidy_workflow",
|
||||
"FROM alg.tidy_space_lock",
|
||||
"PAUSED_RECOVERY");
|
||||
assertThat(uninstall).contains(
|
||||
"DROP TRIGGER dbo.trg_alg_dirty_bas_stock",
|
||||
"DROP PROCEDURE alg.usp_refresh_dirty_lanes",
|
||||
"DROP PROCEDURE alg.usp_claim_tidy_lane",
|
||||
"DROP PROCEDURE alg.usp_release_tidy_lane",
|
||||
"DROP PROCEDURE alg.usp_sync_replay_sequences",
|
||||
"DROP TABLE alg.tidy_callback_event",
|
||||
"DROP TABLE alg.tidy_space_lock",
|
||||
"DROP TABLE alg.tidy_step",
|
||||
"DROP TABLE alg.tidy_workflow",
|
||||
"DROP TABLE alg.tidy_lane_queue",
|
||||
"DROP TABLE alg.tidy_mode",
|
||||
"DROP TABLE alg.outbound_workflow_space",
|
||||
"DROP TABLE alg.outbound_workflow_step",
|
||||
"DROP TABLE alg.outbound_workflow",
|
||||
"DROP TABLE alg.dirty_lane_queue");
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedInstallDoesNotDiscardPendingDirtyLanes() throws IOException {
|
||||
String install = resource("db/sqlserver/install.sql");
|
||||
String initialization = install.substring(install.lastIndexOf("IF NOT EXISTS (SELECT 1 FROM alg.space_topology)"));
|
||||
|
||||
assertThat(initialization).contains(
|
||||
"AND NOT EXISTS (SELECT 1 FROM alg.inventory_item_snapshot)",
|
||||
"AND NOT EXISTS (SELECT 1 FROM alg.lane_state)");
|
||||
assertThat(initialization.indexOf("DELETE FROM alg.dirty_lane_queue"))
|
||||
.isLessThan(initialization.indexOf("END;"));
|
||||
assertThat(initialization.substring(initialization.indexOf("END;") + 4))
|
||||
.doesNotContain("DELETE FROM alg.dirty_lane_queue");
|
||||
}
|
||||
|
||||
private String resource(String name) throws IOException {
|
||||
try (var stream = getClass().getClassLoader().getResourceAsStream(name)) {
|
||||
if (stream == null) {
|
||||
throw new IOException("Missing test resource " + name);
|
||||
}
|
||||
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user