diff --git a/fortucky-algorithm/src/main/resources/db/sqlserver/diagnostics-shadow.sql b/fortucky-algorithm/src/main/resources/db/sqlserver/diagnostics-shadow.sql index 36a873b..67548b3 100644 --- a/fortucky-algorithm/src/main/resources/db/sqlserver/diagnostics-shadow.sql +++ b/fortucky-algorithm/src/main/resources/db/sqlserver/diagnostics-shadow.sql @@ -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; diff --git a/fortucky-algorithm/src/main/resources/db/sqlserver/install-shadow.sql b/fortucky-algorithm/src/main/resources/db/sqlserver/install-shadow.sql index 2ebd809..134e971 100644 --- a/fortucky-algorithm/src/main/resources/db/sqlserver/install-shadow.sql +++ b/fortucky-algorithm/src/main/resources/db/sqlserver/install-shadow.sql @@ -1,7 +1,8 @@ /* FT546 shadow-only read model for SQL Server 2016 RTM (13.0). - This package creates no Legacy-table triggers and never inserts, updates, - or deletes Legacy WMS business rows. + This package writes only alg.* and never changes Legacy WMS business rows. + A clean install creates an empty INITIALIZING package. The v3 worker owns + the first refresh and publication. */ SET NOCOUNT ON; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; @@ -9,7 +10,7 @@ SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; IF SCHEMA_ID(N'alg') IS NOT NULL THROW 51000, 'FT546 shadow install refused: alg schema already exists', 1; -EXEC(N'CREATE SCHEMA alg'); +CREATE SCHEMA alg; GO CREATE TABLE alg.shadow_package_manifest ( @@ -19,10 +20,9 @@ CREATE TABLE alg.shadow_package_manifest ( ); INSERT INTO alg.shadow_package_manifest (package_id, package_version) -VALUES ('FT546WMS_SHADOW_ONLY', '2026-08-12-1'); +VALUES ('FT546WMS_SHADOW_ONLY', '2026-09-03-3'); GO -IF OBJECT_ID(N'alg.warehouse_policy', N'U') IS NULL CREATE TABLE alg.warehouse_policy ( warehouse_id varchar(80) NOT NULL PRIMARY KEY, target_fill_rate decimal(9,4) NULL, @@ -31,7 +31,6 @@ CREATE TABLE alg.warehouse_policy ( ); GO -IF OBJECT_ID(N'alg.project_policy', N'U') IS NULL CREATE TABLE alg.project_policy ( project_no varchar(255) NOT NULL PRIMARY KEY, is_isolated bit NOT NULL CONSTRAINT DF_alg_project_policy_isolated DEFAULT 0, @@ -41,7 +40,6 @@ CREATE TABLE alg.project_policy ( ); GO -IF OBJECT_ID(N'alg.space_role', N'U') IS NULL CREATE TABLE alg.space_role ( space_id varchar(80) NOT NULL PRIMARY KEY, role varchar(40) NOT NULL, @@ -50,23 +48,22 @@ CREATE TABLE alg.space_role ( ); GO -IF OBJECT_ID(N'alg.space_topology', N'U') IS NULL CREATE TABLE alg.space_topology ( warehouse_id varchar(80) NOT NULL, - space_id varchar(80) NOT NULL CONSTRAINT PK_alg_space_topology PRIMARY KEY, + space_id varchar(80) NOT NULL, lane_code varchar(80) NOT NULL, lane_depth int NOT NULL, base_zone varchar(20) NOT NULL, + snapshot_generation bigint NOT NULL, rebuilt_at datetime2 NOT NULL CONSTRAINT DF_alg_space_topology_rebuilt_at DEFAULT SYSUTCDATETIME(), + CONSTRAINT PK_alg_space_topology PRIMARY KEY (warehouse_id, space_id), CONSTRAINT CK_alg_space_topology_base_zone CHECK (base_zone IN ('CACHE', 'RESERVE', 'MAIN', 'FLEX')) ); GO -IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'alg.space_topology') AND name = N'IX_alg_space_topology_warehouse_lane') CREATE INDEX IX_alg_space_topology_warehouse_lane ON alg.space_topology(warehouse_id, lane_code); GO -IF OBJECT_ID(N'alg.inventory_item_snapshot', N'U') IS NULL CREATE TABLE alg.inventory_item_snapshot ( warehouse_id varchar(80) NOT NULL, stock_id varchar(80) NOT NULL, @@ -79,22 +76,26 @@ CREATE TABLE alg.inventory_item_snapshot ( inbound_at datetime2 NULL, quantity decimal(38,6) NULL, out_quantity decimal(38,6) NULL, + snapshot_generation bigint NOT NULL, synchronized_at datetime2 NOT NULL CONSTRAINT DF_alg_inventory_item_snapshot_sync DEFAULT SYSUTCDATETIME(), CONSTRAINT PK_alg_inventory_item_snapshot PRIMARY KEY (warehouse_id, stock_id) ); GO -IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'alg.inventory_item_snapshot') AND name = N'IX_alg_inventory_item_snapshot_space') -CREATE INDEX IX_alg_inventory_item_snapshot_space ON alg.inventory_item_snapshot(warehouse_id, space_id); -GO -IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'alg.inventory_item_snapshot') AND name = N'IX_alg_inventory_item_snapshot_tray') -CREATE INDEX IX_alg_inventory_item_snapshot_tray ON alg.inventory_item_snapshot(warehouse_id, tray_code) INCLUDE (space_id, project_no, product_no); -GO -IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'alg.inventory_item_snapshot') AND name = N'IX_alg_inventory_item_snapshot_project_product') -CREATE INDEX IX_alg_inventory_item_snapshot_project_product ON alg.inventory_item_snapshot(warehouse_id, project_no, product_no) INCLUDE (space_id, tray_code, inbound_at); +CREATE INDEX IX_alg_inventory_item_snapshot_space + ON alg.inventory_item_snapshot(warehouse_id, space_id); +GO + +CREATE INDEX IX_alg_inventory_item_snapshot_tray + ON alg.inventory_item_snapshot(warehouse_id, tray_code) + INCLUDE (space_id, project_no, product_no, snapshot_generation); +GO + +CREATE INDEX IX_alg_inventory_item_snapshot_project_product + ON alg.inventory_item_snapshot(warehouse_id, project_no, product_no) + INCLUDE (space_id, tray_code, inbound_at, snapshot_generation); GO -IF OBJECT_ID(N'alg.space_inventory_summary', N'U') IS NULL CREATE TABLE alg.space_inventory_summary ( warehouse_id varchar(80) NOT NULL, space_id varchar(80) NOT NULL, @@ -102,16 +103,16 @@ CREATE TABLE alg.space_inventory_summary ( stock_tray_count int NOT NULL, stock_project_count int NOT NULL, stock_product_count int NOT NULL, + snapshot_generation bigint NOT NULL, synchronized_at datetime2 NOT NULL CONSTRAINT DF_alg_space_inventory_summary_sync DEFAULT SYSUTCDATETIME(), CONSTRAINT PK_alg_space_inventory_summary PRIMARY KEY (warehouse_id, space_id) ); GO -IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'alg.space_inventory_summary') AND name = N'IX_alg_space_inventory_summary_space') -CREATE INDEX IX_alg_space_inventory_summary_space ON alg.space_inventory_summary(space_id, warehouse_id); +CREATE INDEX IX_alg_space_inventory_summary_space + ON alg.space_inventory_summary(space_id, warehouse_id); GO -IF OBJECT_ID(N'alg.lane_state', N'U') IS NULL CREATE TABLE alg.lane_state ( warehouse_id varchar(80) NOT NULL, lane_code varchar(80) NOT NULL, @@ -121,17 +122,13 @@ CREATE TABLE alg.lane_state ( pending_outbound bit NOT NULL CONSTRAINT DF_alg_lane_state_pending_outbound DEFAULT 0, has_gap bit NOT NULL CONSTRAINT DF_alg_lane_state_has_gap DEFAULT 0, semantic_fingerprint varbinary(32) NULL, + snapshot_generation bigint NOT NULL, state_version rowversion NOT NULL, rebuilt_at datetime2 NOT NULL CONSTRAINT DF_alg_lane_state_rebuilt_at DEFAULT SYSUTCDATETIME(), CONSTRAINT PK_alg_lane_state PRIMARY KEY (warehouse_id, lane_code) ); GO -IF COL_LENGTH(N'alg.lane_state', N'semantic_fingerprint') IS NULL -ALTER TABLE alg.lane_state ADD semantic_fingerprint varbinary(32) NULL; -GO - -IF OBJECT_ID(N'alg.reservation', N'U') IS NULL CREATE TABLE alg.reservation ( reservation_id uniqueidentifier NOT NULL CONSTRAINT PK_alg_reservation PRIMARY KEY, warehouse_id varchar(80) NOT NULL, @@ -145,11 +142,10 @@ CREATE TABLE alg.reservation ( ); GO -IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'alg.reservation') AND name = N'UX_alg_reservation_active_space') -CREATE UNIQUE INDEX UX_alg_reservation_active_space ON alg.reservation(space_id) WHERE status = 'ACTIVE'; +CREATE UNIQUE INDEX UX_alg_reservation_active_space + ON alg.reservation(space_id) WHERE status = 'ACTIVE'; GO -IF OBJECT_ID(N'alg.deep_workflow_lock', N'U') IS NULL CREATE TABLE alg.deep_workflow_lock ( lock_id uniqueidentifier NOT NULL CONSTRAINT PK_alg_deep_workflow_lock PRIMARY KEY, request_id varchar(80) NOT NULL, @@ -162,12 +158,10 @@ CREATE TABLE alg.deep_workflow_lock ( ); GO -IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'alg.deep_workflow_lock') AND name = N'UX_alg_deep_workflow_lock_active_floor') CREATE UNIQUE INDEX UX_alg_deep_workflow_lock_active_floor -ON alg.deep_workflow_lock(warehouse_id, floor_no) WHERE status = 'ACTIVE'; + ON alg.deep_workflow_lock(warehouse_id, floor_no) WHERE status = 'ACTIVE'; GO -IF OBJECT_ID(N'alg.decision_log', N'U') IS NULL CREATE TABLE alg.decision_log ( decision_id uniqueidentifier NOT NULL CONSTRAINT PK_alg_decision_log PRIMARY KEY, warehouse_id varchar(80) NULL, @@ -179,227 +173,356 @@ CREATE TABLE alg.decision_log ( ); GO -IF OBJECT_ID(N'alg.usp_rebuild_space_topology', N'P') IS NOT NULL DROP PROCEDURE alg.usp_rebuild_space_topology; +CREATE TABLE alg.shadow_snapshot_state ( + warehouse_id varchar(80) NOT NULL PRIMARY KEY, + snapshot_generation bigint NOT NULL, + snapshot_status varchar(20) NOT NULL, + synchronized_at datetime2 NULL, + source_signature varbinary(32) NULL, + source_row_count bigint NULL, + instance_id varchar(80) NULL, + last_error varchar(1000) NULL, + CONSTRAINT CK_alg_shadow_snapshot_state_status CHECK (snapshot_status IN ('INITIALIZING', 'REFRESHING', 'PUBLISHED', 'FAILED')) +); GO -EXEC(N' + +CREATE TABLE alg.cargo_space_snapshot ( + warehouse_id varchar(80) NOT NULL, + space_id varchar(80) NOT NULL, + lane_code varchar(80) NULL, + cargo_area_id varchar(80) NULL, + roadway_id varchar(80) NULL, + display_name varchar(255) NULL, + platoon int NULL, + column_no int NULL, + floor_no int NULL, + depth_raw varchar(80) NULL, + space_status varchar(20) NULL, + locked varchar(20) NULL, + tray_code varchar(255) NULL, + snapshot_generation bigint NOT NULL, + synchronized_at datetime2 NOT NULL, + CONSTRAINT PK_alg_cargo_space_snapshot PRIMARY KEY (warehouse_id, space_id) +); +GO + +CREATE TABLE alg.active_job_snapshot ( + warehouse_id varchar(80) NOT NULL, + job_id varchar(80) NOT NULL, + task_no varchar(80) NULL, + tray_code varchar(255) NULL, + source_space_id varchar(80) NULL, + target_space_id varchar(80) NULL, + status varchar(20) NULL, + in_or_out varchar(20) NULL, + create_time_raw varchar(80) NULL, + snapshot_generation bigint NOT NULL, + synchronized_at datetime2 NOT NULL, + CONSTRAINT PK_alg_active_job_snapshot PRIMARY KEY (warehouse_id, job_id) +); +GO + +CREATE TABLE alg.shadow_inbound_request_snapshot ( + warehouse_id varchar(80) NOT NULL, + task_no varchar(80) NOT NULL, + tray_no varchar(255) NOT NULL, + request_id varchar(80) NULL, + raw_warehouse_id varchar(80) NULL, + project_no varchar(255) NULL, + product_id varchar(255) NULL, + lot_no varchar(80) NULL, + snapshot_generation bigint NOT NULL, + synchronized_at datetime2 NOT NULL, + CONSTRAINT PK_alg_shadow_inbound_request_snapshot PRIMARY KEY (warehouse_id, task_no, tray_no) +); +GO + +CREATE TABLE alg.shadow_outbound_request_snapshot ( + warehouse_id varchar(80) NOT NULL, + stockout_detail_id varchar(80) NOT NULL, + raw_warehouse_id varchar(80) NULL, + project_no varchar(255) NULL, + product_id varchar(255) NULL, + requested_quantity decimal(38,6) NULL, + snapshot_generation bigint NOT NULL, + synchronized_at datetime2 NOT NULL, + CONSTRAINT PK_alg_shadow_outbound_request_snapshot PRIMARY KEY (warehouse_id, stockout_detail_id) +); +GO + +CREATE TABLE alg.shadow_runtime_metric ( + warehouse_id varchar(80) NOT NULL, + metric_name varchar(80) NOT NULL, + bucket_upper_micros bigint NOT NULL, + metric_count bigint NOT NULL CONSTRAINT DF_alg_shadow_runtime_metric_count DEFAULT 0, + metric_total_micros bigint NOT NULL CONSTRAINT DF_alg_shadow_runtime_metric_total DEFAULT 0, + metric_max_micros bigint NOT NULL CONSTRAINT DF_alg_shadow_runtime_metric_max DEFAULT 0, + flushed_at datetime2 NULL, + CONSTRAINT PK_alg_shadow_runtime_metric PRIMARY KEY (warehouse_id, metric_name, bucket_upper_micros) +); +GO + +CREATE TABLE alg.shadow_instance_heartbeat ( + warehouse_id varchar(80) NOT NULL PRIMARY KEY, + instance_id varchar(80) NOT NULL, + claimed_at datetime2 NOT NULL, + last_seen_at datetime2 NOT NULL +); +GO + CREATE PROCEDURE alg.usp_rebuild_space_topology - @warehouse_id varchar(80) = NULL + @warehouse_id varchar(80) = NULL, + @snapshot_generation bigint = NULL AS BEGIN SET NOCOUNT ON; + IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '') IS NULL + THROW 51019, 'warehouse_id is required', 1; + DECLARE @generation bigint; + + SET @generation = @snapshot_generation; + IF @generation IS NULL + SELECT @generation = state.snapshot_generation + FROM alg.shadow_snapshot_state state + WHERE state.warehouse_id = @warehouse_id + AND state.snapshot_status = 'PUBLISHED'; + + IF @generation IS NULL + THROW 51020, 'FT546 shadow topology rebuild requires a snapshot generation', 1; + ;WITH lane_topology AS ( - SELECT warehouse_id, group_coding_no AS lane_code, + SELECT cargo.warehouse_id, cargo.lane_code, COUNT(*) AS lane_depth, - MAX(the_column) AS column_no, - MIN(the_platoon) AS row_no, - MIN(the_floor) AS floor_no, - MIN(cargo_space_id) AS stable_id - FROM dbo.bas_cargo_space - WHERE NULLIF(LTRIM(RTRIM(group_coding_no)), '''') IS NOT NULL - AND TRY_CONVERT(int, group_coding) IS NOT NULL - AND (@warehouse_id IS NULL OR warehouse_id = @warehouse_id) - GROUP BY warehouse_id, group_coding_no + MAX(cargo.column_no) AS column_no, + MIN(cargo.platoon) AS row_no, + MIN(cargo.floor_no) AS floor_no, + MIN(cargo.space_id) AS stable_id + FROM alg.cargo_space_snapshot cargo + WHERE cargo.snapshot_generation = @generation + AND NULLIF(LTRIM(RTRIM(cargo.lane_code)), '') IS NOT NULL + AND TRY_CONVERT(int, cargo.depth_raw) IS NOT NULL + AND (@warehouse_id IS NULL OR cargo.warehouse_id = @warehouse_id) + GROUP BY cargo.warehouse_id, cargo.lane_code ), ranked_lanes AS ( SELECT lane_topology.*, CASE WHEN lane_depth = 1 THEN ROW_NUMBER() OVER ( PARTITION BY warehouse_id - ORDER BY CASE WHEN lane_depth = 1 THEN 0 ELSE 1 END, - column_no DESC, row_no ASC, floor_no ASC, stable_id ASC + ORDER BY column_no DESC, row_no ASC, floor_no ASC, stable_id ASC ) END AS single_depth_rank FROM lane_topology - ), source AS ( - SELECT c.warehouse_id, c.cargo_space_id AS space_id, c.group_coding_no AS lane_code, + ), source_rows AS ( + SELECT cargo.warehouse_id, cargo.space_id, cargo.lane_code, ranked_lanes.lane_depth, - CASE WHEN ranked_lanes.lane_depth = 1 AND ranked_lanes.single_depth_rank <= 80 THEN ''CACHE'' - WHEN ranked_lanes.lane_depth = 1 AND ranked_lanes.single_depth_rank <= 133 THEN ''RESERVE'' - WHEN ranked_lanes.lane_depth = 5 THEN ''MAIN'' - ELSE ''FLEX'' END AS base_zone - FROM dbo.bas_cargo_space c - JOIN ranked_lanes ON ranked_lanes.warehouse_id = c.warehouse_id - AND ranked_lanes.lane_code = c.group_coding_no - WHERE NULLIF(LTRIM(RTRIM(c.group_coding_no)), '''') IS NOT NULL - AND TRY_CONVERT(int, c.group_coding) IS NOT NULL - AND (@warehouse_id IS NULL OR c.warehouse_id = @warehouse_id) + CASE WHEN ranked_lanes.lane_depth = 1 AND ranked_lanes.single_depth_rank <= 80 THEN 'CACHE' + WHEN ranked_lanes.lane_depth = 1 AND ranked_lanes.single_depth_rank <= 133 THEN 'RESERVE' + WHEN ranked_lanes.lane_depth = 5 THEN 'MAIN' + ELSE 'FLEX' END AS base_zone + FROM alg.cargo_space_snapshot cargo + JOIN ranked_lanes ON ranked_lanes.warehouse_id = cargo.warehouse_id + AND ranked_lanes.lane_code = cargo.lane_code + WHERE cargo.snapshot_generation = @generation + AND NULLIF(LTRIM(RTRIM(cargo.lane_code)), '') IS NOT NULL + AND TRY_CONVERT(int, cargo.depth_raw) IS NOT NULL + AND (@warehouse_id IS NULL OR cargo.warehouse_id = @warehouse_id) ) - MERGE alg.space_topology AS target - USING source ON source.space_id = target.space_id - WHEN MATCHED AND (target.warehouse_id <> source.warehouse_id OR target.lane_code <> source.lane_code - OR target.lane_depth <> source.lane_depth OR target.base_zone <> source.base_zone) - THEN UPDATE SET warehouse_id = source.warehouse_id, lane_code = source.lane_code, - lane_depth = source.lane_depth, base_zone = source.base_zone, + MERGE alg.space_topology WITH (HOLDLOCK) AS target + USING source_rows AS source + ON target.warehouse_id = source.warehouse_id AND target.space_id = source.space_id + WHEN MATCHED AND (target.lane_code <> source.lane_code OR target.lane_depth <> source.lane_depth + OR target.base_zone <> source.base_zone OR target.snapshot_generation <> @generation) + THEN UPDATE SET lane_code = source.lane_code, lane_depth = source.lane_depth, + base_zone = source.base_zone, snapshot_generation = @generation, rebuilt_at = SYSUTCDATETIME() WHEN NOT MATCHED BY TARGET THEN - INSERT (warehouse_id, space_id, lane_code, lane_depth, base_zone) - VALUES (source.warehouse_id, source.space_id, source.lane_code, source.lane_depth, source.base_zone) + INSERT (warehouse_id, space_id, lane_code, lane_depth, base_zone, snapshot_generation) + VALUES (source.warehouse_id, source.space_id, source.lane_code, source.lane_depth, + source.base_zone, @generation) WHEN NOT MATCHED BY SOURCE AND (@warehouse_id IS NULL OR target.warehouse_id = @warehouse_id) THEN DELETE; -END;'); +END; GO -IF OBJECT_ID(N'alg.usp_sync_inventory_snapshot', N'P') IS NOT NULL DROP PROCEDURE alg.usp_sync_inventory_snapshot; -GO -EXEC(N' CREATE PROCEDURE alg.usp_sync_inventory_snapshot - @warehouse_id varchar(80) = NULL + @warehouse_id varchar(80) = NULL, + @snapshot_generation bigint = NULL AS BEGIN SET NOCOUNT ON; - SET XACT_ABORT ON; - BEGIN TRANSACTION; + IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '') IS NULL + THROW 51019, 'warehouse_id is required', 1; + DECLARE @generation bigint; + + SET @generation = @snapshot_generation; + IF @generation IS NULL + SELECT @generation = state.snapshot_generation + FROM alg.shadow_snapshot_state state + WHERE state.warehouse_id = @warehouse_id + AND state.snapshot_status = 'PUBLISHED'; + + IF @generation IS NULL + THROW 51021, 'FT546 shadow inventory summary requires a snapshot generation', 1; DELETE summary FROM alg.space_inventory_summary summary - WHERE @warehouse_id IS NULL OR summary.warehouse_id = @warehouse_id; - - DELETE item - FROM alg.inventory_item_snapshot item - WHERE @warehouse_id IS NULL OR item.warehouse_id = @warehouse_id; - - INSERT INTO alg.inventory_item_snapshot ( - warehouse_id, stock_id, space_id, tray_code, project_no, product_no, lot_no, - inbound_time_raw, inbound_at, quantity, out_quantity, synchronized_at) - SELECT space.warehouse_id, stock.stock_id, stock.cargo_space_id, stock.tray_code, - stock.spare4, stock.product_id, stock.lot_no, CONVERT(varchar(80), stock.in_date), - TRY_CONVERT(datetime2, stock.in_date), stock.quantity, stock.out_quantity, SYSUTCDATETIME() - FROM dbo.bas_stock stock - JOIN dbo.bas_cargo_space space ON space.cargo_space_id = stock.cargo_space_id - WHERE @warehouse_id IS NULL OR space.warehouse_id = @warehouse_id; + WHERE (@warehouse_id IS NULL OR summary.warehouse_id = @warehouse_id); INSERT INTO alg.space_inventory_summary ( warehouse_id, space_id, stock_detail_count, stock_tray_count, - stock_project_count, stock_product_count, synchronized_at) + stock_project_count, stock_product_count, snapshot_generation, synchronized_at) SELECT item.warehouse_id, item.space_id, COUNT(*), - COUNT(DISTINCT NULLIF(LTRIM(RTRIM(item.tray_code)), '''')), - COUNT(DISTINCT NULLIF(LTRIM(RTRIM(item.project_no)), '''')), - COUNT(DISTINCT NULLIF(LTRIM(RTRIM(item.product_no)), '''')), - SYSUTCDATETIME() + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(item.tray_code)), '')), + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(item.project_no)), '')), + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(item.product_no)), '')), + @generation, SYSUTCDATETIME() FROM alg.inventory_item_snapshot item - WHERE @warehouse_id IS NULL OR item.warehouse_id = @warehouse_id + WHERE item.snapshot_generation = @generation + AND (@warehouse_id IS NULL OR item.warehouse_id = @warehouse_id) GROUP BY item.warehouse_id, item.space_id; - - COMMIT TRANSACTION; -END;'); +END; GO -IF OBJECT_ID(N'alg.v_inventory_item', N'V') IS NOT NULL DROP VIEW alg.v_inventory_item; -GO -EXEC(N' -CREATE VIEW alg.v_inventory_item AS -SELECT stock_id, space_id, tray_code, project_no, product_no, lot_no, - inbound_time_raw, inbound_at, quantity, out_quantity -FROM alg.inventory_item_snapshot;'); +CREATE VIEW alg.v_inventory_item +AS +-- generation contract: WHERE snapshot_generation = published_snapshot.snapshot_generation +SELECT item.warehouse_id, item.stock_id, item.space_id, item.tray_code, + item.project_no, item.product_no, item.lot_no, item.inbound_time_raw, + item.inbound_at, item.quantity, item.out_quantity, item.snapshot_generation +FROM alg.inventory_item_snapshot item +JOIN alg.shadow_snapshot_state published_snapshot + ON published_snapshot.warehouse_id = item.warehouse_id + AND published_snapshot.snapshot_status = 'PUBLISHED' + AND item.snapshot_generation = published_snapshot.snapshot_generation; GO -IF OBJECT_ID(N'alg.v_active_job', N'V') IS NOT NULL DROP VIEW alg.v_active_job; -GO -EXEC(N' -CREATE VIEW alg.v_active_job AS -WITH tray_candidates AS ( - SELECT j.job_id, - COUNT(DISTINCT s.space_id) AS space_count, - MIN(s.space_id) AS cargo_space_id, - COUNT(DISTINCT NULLIF(LTRIM(RTRIM(s.project_no)), '''')) AS project_count, - MIN(NULLIF(LTRIM(RTRIM(s.project_no)), '''')) AS project_no, - COUNT(DISTINCT NULLIF(LTRIM(RTRIM(s.product_no)), '''')) AS product_count, - MIN(NULLIF(LTRIM(RTRIM(s.product_no)), '''')) AS product_no - FROM dbo.mydo_job j - JOIN alg.inventory_item_snapshot s ON s.warehouse_id = j.warehouse_id - AND s.tray_code = NULLIF(LTRIM(RTRIM(j.tray_code)), '''') - GROUP BY j.job_id +CREATE VIEW alg.v_active_job +AS +WITH published_jobs AS ( + SELECT job.warehouse_id, job.job_id, job.task_no, job.tray_code, + job.source_space_id, job.target_space_id, job.status, job.in_or_out, + job.create_time_raw, job.snapshot_generation + FROM alg.active_job_snapshot job + JOIN alg.shadow_snapshot_state published_snapshot + ON published_snapshot.warehouse_id = job.warehouse_id + AND published_snapshot.snapshot_status = 'PUBLISHED' + AND job.snapshot_generation = published_snapshot.snapshot_generation +), tray_candidates AS ( + SELECT job.warehouse_id, job.job_id, job.snapshot_generation, + COUNT(DISTINCT stock.space_id) AS space_count, + MIN(stock.space_id) AS cargo_space_id, + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(stock.project_no)), '')) AS project_count, + MIN(NULLIF(LTRIM(RTRIM(stock.project_no)), '')) AS project_no, + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(stock.product_no)), '')) AS product_count, + MIN(NULLIF(LTRIM(RTRIM(stock.product_no)), '')) AS product_no + FROM published_jobs job + JOIN alg.inventory_item_snapshot stock + ON stock.warehouse_id = job.warehouse_id + AND stock.snapshot_generation = job.snapshot_generation + AND stock.tray_code = NULLIF(LTRIM(RTRIM(job.tray_code)), '') + GROUP BY job.warehouse_id, job.job_id, job.snapshot_generation ), resolved_jobs AS ( - SELECT j.job_id, j.task_no, j.tray_code, j.target_cargo_space_id, j.warehouse_id, - j.status, j.in_or_out, j.create_time AS create_time_raw, - COALESCE(source_space.cargo_space_id, + SELECT job.job_id, job.task_no, job.tray_code, job.target_space_id, + job.warehouse_id, job.status, job.in_or_out, job.create_time_raw, + job.snapshot_generation, + COALESCE(NULLIF(LTRIM(RTRIM(job.source_space_id)), ''), CASE WHEN tray_candidates.space_count = 1 THEN tray_candidates.cargo_space_id END) AS source_space_id, - CASE WHEN source_space.cargo_space_id IS NOT NULL THEN ''EXPLICIT'' - WHEN tray_candidates.space_count = 1 THEN ''TRAY'' - WHEN tray_candidates.space_count > 1 THEN ''AMBIGUOUS_TRAY'' - ELSE ''UNRESOLVED'' END AS source_resolution - FROM dbo.mydo_job j - LEFT JOIN dbo.bas_cargo_space source_space - ON source_space.cargo_space_id = NULLIF(LTRIM(RTRIM(j.cargo_space_id)), '''') - AND source_space.warehouse_id = j.warehouse_id - LEFT JOIN tray_candidates ON tray_candidates.job_id = j.job_id - WHERE j.finish_time IS NULL OR LTRIM(RTRIM(j.finish_time)) = '''' + CASE WHEN NULLIF(LTRIM(RTRIM(job.source_space_id)), '') IS NOT NULL THEN 'EXPLICIT' + WHEN tray_candidates.space_count = 1 THEN 'TRAY' + WHEN tray_candidates.space_count > 1 THEN 'AMBIGUOUS_TRAY' + ELSE 'UNRESOLVED' END AS source_resolution + FROM published_jobs job + LEFT JOIN tray_candidates + ON tray_candidates.warehouse_id = job.warehouse_id + AND tray_candidates.job_id = job.job_id + AND tray_candidates.snapshot_generation = job.snapshot_generation + WHERE job.snapshot_generation IS NOT NULL ), source_scope AS ( - SELECT resolved_jobs.job_id, - COUNT(DISTINCT NULLIF(LTRIM(RTRIM(stock.project_no)), '''')) AS project_count, - MIN(NULLIF(LTRIM(RTRIM(stock.project_no)), '''')) AS project_no, - COUNT(DISTINCT NULLIF(LTRIM(RTRIM(stock.product_no)), '''')) AS product_count, - MIN(NULLIF(LTRIM(RTRIM(stock.product_no)), '''')) AS product_no - FROM resolved_jobs - JOIN alg.inventory_item_snapshot stock ON stock.warehouse_id = resolved_jobs.warehouse_id - AND stock.space_id = resolved_jobs.source_space_id - GROUP BY resolved_jobs.job_id + SELECT resolved.warehouse_id, resolved.job_id, resolved.snapshot_generation, + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(stock.project_no)), '')) AS project_count, + MIN(NULLIF(LTRIM(RTRIM(stock.project_no)), '')) AS project_no, + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(stock.product_no)), '')) AS product_count, + MIN(NULLIF(LTRIM(RTRIM(stock.product_no)), '')) AS product_no + FROM resolved_jobs resolved + JOIN alg.inventory_item_snapshot stock + ON stock.warehouse_id = resolved.warehouse_id + AND stock.snapshot_generation = resolved.snapshot_generation + AND stock.space_id = resolved.source_space_id + GROUP BY resolved.warehouse_id, resolved.job_id, resolved.snapshot_generation ) -SELECT resolved_jobs.job_id, resolved_jobs.task_no, resolved_jobs.tray_code, - CASE WHEN resolved_jobs.source_space_id IS NOT NULL +SELECT resolved.job_id, resolved.task_no, resolved.tray_code, + CASE WHEN resolved.source_space_id IS NOT NULL THEN CASE WHEN source_scope.project_count = 1 THEN source_scope.project_no END ELSE CASE WHEN tray_candidates.project_count = 1 THEN tray_candidates.project_no END END AS project_no, - CASE WHEN resolved_jobs.source_space_id IS NOT NULL + CASE WHEN resolved.source_space_id IS NOT NULL THEN CASE WHEN source_scope.product_count = 1 THEN source_scope.product_no END ELSE CASE WHEN tray_candidates.product_count = 1 THEN tray_candidates.product_no END END AS product_no, - resolved_jobs.source_space_id, resolved_jobs.target_cargo_space_id, resolved_jobs.warehouse_id, - resolved_jobs.status, resolved_jobs.in_or_out, resolved_jobs.create_time_raw, - resolved_space.group_coding_no AS source_lane_code, resolved_jobs.source_resolution -FROM resolved_jobs -LEFT JOIN tray_candidates ON tray_candidates.job_id = resolved_jobs.job_id -LEFT JOIN source_scope ON source_scope.job_id = resolved_jobs.job_id -LEFT JOIN dbo.bas_cargo_space resolved_space ON resolved_space.cargo_space_id = resolved_jobs.source_space_id;'); + resolved.source_space_id, resolved.target_space_id AS target_cargo_space_id, + resolved.warehouse_id, resolved.status, resolved.in_or_out, + resolved.create_time_raw, source_cargo.lane_code AS source_lane_code, + resolved.source_resolution, resolved.snapshot_generation +FROM resolved_jobs resolved + LEFT JOIN tray_candidates + ON tray_candidates.warehouse_id = resolved.warehouse_id + AND tray_candidates.job_id = resolved.job_id + AND tray_candidates.snapshot_generation = resolved.snapshot_generation + LEFT JOIN source_scope + ON source_scope.warehouse_id = resolved.warehouse_id + AND source_scope.job_id = resolved.job_id + AND source_scope.snapshot_generation = resolved.snapshot_generation +LEFT JOIN alg.cargo_space_snapshot source_cargo + ON source_cargo.warehouse_id = resolved.warehouse_id + AND source_cargo.space_id = resolved.source_space_id + AND source_cargo.snapshot_generation = resolved.snapshot_generation; GO -IF OBJECT_ID(N'alg.v_space_state', N'V') IS NOT NULL DROP VIEW alg.v_space_state; -GO -EXEC(N' -CREATE VIEW alg.v_space_state AS -WITH outbound_lanes AS ( - SELECT DISTINCT warehouse_id, source_lane_code AS lane_code - FROM alg.v_active_job - WHERE in_or_out >= 20 AND source_lane_code IS NOT NULL +CREATE VIEW alg.v_space_state +AS +WITH published_snapshot AS ( + SELECT state.warehouse_id, state.snapshot_generation + FROM alg.shadow_snapshot_state state + WHERE state.snapshot_status = 'PUBLISHED' +), outbound_lanes AS ( + SELECT DISTINCT job.warehouse_id, job.source_lane_code AS lane_code, + job.snapshot_generation + FROM alg.v_active_job job + WHERE TRY_CONVERT(int, job.in_or_out) >= 20 + AND job.source_lane_code IS NOT NULL ), lane_gaps AS ( - SELECT DISTINCT inner_space.warehouse_id, inner_space.group_coding_no AS lane_code - FROM dbo.bas_cargo_space inner_space - JOIN dbo.bas_cargo_space outer_space + SELECT DISTINCT inner_space.warehouse_id, inner_space.lane_code, + inner_space.snapshot_generation + FROM alg.cargo_space_snapshot inner_space + JOIN alg.cargo_space_snapshot outer_space ON outer_space.warehouse_id = inner_space.warehouse_id - AND outer_space.group_coding_no = inner_space.group_coding_no - AND TRY_CONVERT(int, outer_space.group_coding) > TRY_CONVERT(int, inner_space.group_coding) - WHERE TRY_CONVERT(int, inner_space.group_coding) IS NOT NULL - AND TRY_CONVERT(int, outer_space.group_coding) IS NOT NULL - AND NULLIF(LTRIM(RTRIM(inner_space.tray_code)), '''') IS NULL - AND NOT EXISTS ( - SELECT 1 FROM alg.inventory_item_snapshot inner_stock - WHERE inner_stock.warehouse_id = inner_space.warehouse_id - AND inner_stock.space_id = inner_space.cargo_space_id - ) - AND ( - NULLIF(LTRIM(RTRIM(outer_space.tray_code)), '''') IS NOT NULL - OR EXISTS ( - SELECT 1 FROM alg.inventory_item_snapshot outer_stock - WHERE outer_stock.warehouse_id = outer_space.warehouse_id - AND outer_stock.space_id = outer_space.cargo_space_id - ) - ) + AND outer_space.lane_code = inner_space.lane_code + AND outer_space.snapshot_generation = inner_space.snapshot_generation + AND TRY_CONVERT(int, outer_space.depth_raw) > TRY_CONVERT(int, inner_space.depth_raw) + JOIN published_snapshot published + ON published.warehouse_id = inner_space.warehouse_id + AND published.snapshot_generation = inner_space.snapshot_generation + LEFT JOIN alg.space_inventory_summary inner_stock + ON inner_stock.warehouse_id = inner_space.warehouse_id + AND inner_stock.space_id = inner_space.space_id + AND inner_stock.snapshot_generation = inner_space.snapshot_generation + LEFT JOIN alg.space_inventory_summary outer_stock + ON outer_stock.warehouse_id = outer_space.warehouse_id + AND outer_stock.space_id = outer_space.space_id + AND outer_stock.snapshot_generation = outer_space.snapshot_generation + WHERE TRY_CONVERT(int, inner_space.depth_raw) IS NOT NULL + AND TRY_CONVERT(int, outer_space.depth_raw) IS NOT NULL + AND NULLIF(LTRIM(RTRIM(inner_space.tray_code)), '') IS NULL + AND inner_stock.space_id IS NULL + AND (NULLIF(LTRIM(RTRIM(outer_space.tray_code)), '') IS NOT NULL + OR outer_stock.space_id IS NOT NULL) ) -SELECT c.cargo_space_id AS space_id, - c.warehouse_id, - c.group_coding_no AS lane_code, - c.cargo_area_id AS area_id, - CASE - WHEN ISNULL(role.role, ''STORAGE'') IN (''RELOCATION_BUFFER'', ''OUTBOUND_BUFFER'') THEN ''CACHE'' - WHEN ISNULL(role.role, ''STORAGE'') = ''PROTECTED_CORE'' THEN ''RESERVE'' - ELSE topology.base_zone - END AS zone_code, - c.roadway_id, - c.cargo_space_name AS display_name, - c.the_platoon AS platoon, - c.the_column AS column_no, - c.the_floor AS floor_no, - c.group_coding AS depth_raw, - TRY_CONVERT(int, c.group_coding) AS depth, - c.status AS space_status, - c.locked, - c.tray_code AS space_tray_code, - ISNULL(role.role, ''STORAGE'') AS role, +SELECT cargo.space_id, cargo.warehouse_id, cargo.lane_code, + cargo.cargo_area_id AS area_id, + CASE WHEN ISNULL(role.role, 'STORAGE') IN ('RELOCATION_BUFFER', 'OUTBOUND_BUFFER') THEN 'CACHE' + WHEN ISNULL(role.role, 'STORAGE') = 'PROTECTED_CORE' THEN 'RESERVE' + ELSE topology.base_zone END AS zone_code, + cargo.roadway_id, cargo.display_name, cargo.platoon, cargo.column_no, + cargo.floor_no, cargo.depth_raw, + TRY_CONVERT(int, cargo.depth_raw) AS depth, cargo.space_status, + cargo.locked, cargo.tray_code AS space_tray_code, + ISNULL(role.role, 'STORAGE') AS role, ISNULL(stock.stock_detail_count, 0) AS stock_detail_count, ISNULL(stock.stock_tray_count, 0) AS stock_tray_count, ISNULL(stock.stock_project_count, 0) AS stock_project_count, @@ -412,182 +535,255 @@ SELECT c.cargo_space_id AS space_id, CONVERT(bit, CASE WHEN active_reservations.reservation_id IS NULL THEN 0 ELSE 1 END) AS reserved, CONVERT(bit, CASE WHEN lane_gaps.lane_code IS NULL THEN 0 ELSE 1 END) AS has_gap, CONVERT(bit, CASE - WHEN c.status IS NULL OR c.status <> ''1'' THEN 0 - WHEN NULLIF(LTRIM(RTRIM(c.locked)), '''') IS NOT NULL AND LTRIM(RTRIM(c.locked)) NOT IN (''0'', ''N'') THEN 0 - WHEN NULLIF(LTRIM(RTRIM(c.tray_code)), '''') IS NOT NULL THEN 0 + WHEN cargo.space_status IS NULL OR cargo.space_status <> '1' THEN 0 + WHEN NULLIF(LTRIM(RTRIM(cargo.locked)), '') IS NOT NULL + AND LTRIM(RTRIM(cargo.locked)) NOT IN ('0', 'N') THEN 0 + WHEN NULLIF(LTRIM(RTRIM(cargo.tray_code)), '') IS NOT NULL THEN 0 WHEN ISNULL(stock.stock_detail_count, 0) <> 0 THEN 0 - WHEN TRY_CONVERT(int, c.group_coding) IS NULL THEN 0 - WHEN ISNULL(role.role, ''STORAGE'') <> ''STORAGE'' THEN 0 + WHEN TRY_CONVERT(int, cargo.depth_raw) IS NULL THEN 0 + WHEN ISNULL(role.role, 'STORAGE') <> 'STORAGE' THEN 0 WHEN outbound_lanes.lane_code IS NOT NULL THEN 0 WHEN active_reservations.reservation_id IS NOT NULL THEN 0 - WHEN lane_gaps.lane_code IS NOT NULL THEN 0 WHEN EXISTS ( SELECT 1 - FROM dbo.bas_cargo_space inner_candidate - LEFT JOIN alg.space_inventory_summary inner_stock - ON inner_stock.warehouse_id = inner_candidate.warehouse_id - AND inner_stock.space_id = inner_candidate.cargo_space_id - WHERE inner_candidate.warehouse_id = c.warehouse_id - AND inner_candidate.group_coding_no = c.group_coding_no - AND TRY_CONVERT(int, inner_candidate.group_coding) < TRY_CONVERT(int, c.group_coding) - AND inner_candidate.status = ''1'' - AND NULLIF(LTRIM(RTRIM(inner_candidate.tray_code)), '''') IS NULL - AND inner_stock.space_id IS NULL + FROM alg.cargo_space_snapshot inner_candidate + LEFT JOIN alg.space_inventory_summary inner_candidate_stock + ON inner_candidate_stock.warehouse_id = inner_candidate.warehouse_id + AND inner_candidate_stock.space_id = inner_candidate.space_id + AND inner_candidate_stock.snapshot_generation = inner_candidate.snapshot_generation + WHERE inner_candidate.warehouse_id = cargo.warehouse_id + AND inner_candidate.lane_code = cargo.lane_code + AND inner_candidate.snapshot_generation = cargo.snapshot_generation + AND TRY_CONVERT(int, inner_candidate.depth_raw) < TRY_CONVERT(int, cargo.depth_raw) + AND inner_candidate.space_status = '1' + AND NULLIF(LTRIM(RTRIM(inner_candidate.tray_code)), '') IS NULL + AND inner_candidate_stock.space_id IS NULL ) THEN 0 - ELSE 1 END) AS inbound_available -FROM dbo.bas_cargo_space c -JOIN alg.space_topology topology ON topology.space_id = c.cargo_space_id -LEFT JOIN alg.space_inventory_summary stock ON stock.warehouse_id = c.warehouse_id - AND stock.space_id = c.cargo_space_id -LEFT JOIN alg.space_role role ON role.space_id = c.cargo_space_id -LEFT JOIN outbound_lanes ON outbound_lanes.warehouse_id = c.warehouse_id AND outbound_lanes.lane_code = c.group_coding_no -LEFT JOIN alg.reservation active_reservations ON active_reservations.space_id = c.cargo_space_id AND active_reservations.status = ''ACTIVE'' -LEFT JOIN lane_gaps ON lane_gaps.warehouse_id = c.warehouse_id AND lane_gaps.lane_code = c.group_coding_no;'); + ELSE 1 END) AS inbound_available, + cargo.snapshot_generation +FROM alg.cargo_space_snapshot cargo +JOIN published_snapshot published + ON published.warehouse_id = cargo.warehouse_id + AND published.snapshot_generation = cargo.snapshot_generation +JOIN alg.space_topology topology + ON topology.warehouse_id = cargo.warehouse_id + AND topology.space_id = cargo.space_id + AND topology.snapshot_generation = cargo.snapshot_generation +LEFT JOIN alg.space_inventory_summary stock + ON stock.warehouse_id = cargo.warehouse_id + AND stock.space_id = cargo.space_id + AND stock.snapshot_generation = cargo.snapshot_generation +LEFT JOIN alg.space_role role ON role.space_id = cargo.space_id +LEFT JOIN outbound_lanes + ON outbound_lanes.warehouse_id = cargo.warehouse_id + AND outbound_lanes.lane_code = cargo.lane_code + AND outbound_lanes.snapshot_generation = cargo.snapshot_generation +LEFT JOIN alg.reservation active_reservations + ON active_reservations.space_id = cargo.space_id + AND active_reservations.status = 'ACTIVE' +LEFT JOIN lane_gaps + ON lane_gaps.warehouse_id = cargo.warehouse_id + AND lane_gaps.lane_code = cargo.lane_code + AND lane_gaps.snapshot_generation = cargo.snapshot_generation; GO -IF OBJECT_ID(N'alg.usp_compute_lane_semantic_fingerprint', N'P') IS NOT NULL DROP PROCEDURE alg.usp_compute_lane_semantic_fingerprint; -GO -EXEC(N' CREATE PROCEDURE alg.usp_compute_lane_semantic_fingerprint @warehouse_id varchar(80), @lane_code varchar(80), @semantic_fingerprint varbinary(32) OUTPUT, - @inventory_source varchar(20) = ''LIVE'' + @inventory_source varchar(20) = 'SNAPSHOT', + @snapshot_generation bigint = NULL AS BEGIN SET NOCOUNT ON; - DECLARE @payload nvarchar(max); + DECLARE @generation bigint, @payload nvarchar(max); - CREATE TABLE #fingerprint_inventory ( - stock_id varchar(80) NOT NULL, - space_id varchar(80) NOT NULL, - tray_code varchar(255) NULL, - project_no varchar(255) NULL, - product_no varchar(255) NULL, - lot_no varchar(80) NULL, - inbound_time varchar(80) NULL, - quantity decimal(38,6) NULL, - out_quantity decimal(38,6) NULL - ); + IF @inventory_source <> 'SNAPSHOT' + THROW 51022, 'FT546 shadow fingerprint source must be SNAPSHOT', 1; - IF @inventory_source = ''SNAPSHOT'' - BEGIN - INSERT INTO #fingerprint_inventory ( - stock_id, space_id, tray_code, project_no, product_no, lot_no, - inbound_time, quantity, out_quantity) - SELECT stock.stock_id, stock.space_id, stock.tray_code, stock.project_no, - stock.product_no, stock.lot_no, stock.inbound_time_raw, - CONVERT(decimal(38,6), stock.quantity), CONVERT(decimal(38,6), stock.out_quantity) - FROM alg.inventory_item_snapshot stock - JOIN dbo.bas_cargo_space cargo ON cargo.cargo_space_id = stock.space_id - AND cargo.warehouse_id = stock.warehouse_id - WHERE cargo.warehouse_id = @warehouse_id AND cargo.group_coding_no = @lane_code; - END - ELSE IF @inventory_source = ''LIVE'' - BEGIN - INSERT INTO #fingerprint_inventory ( - stock_id, space_id, tray_code, project_no, product_no, lot_no, - inbound_time, quantity, out_quantity) - SELECT stock.stock_id, stock.cargo_space_id, stock.tray_code, stock.spare4, - stock.product_id, stock.lot_no, CONVERT(varchar(80), stock.in_date), - CONVERT(decimal(38,6), stock.quantity), CONVERT(decimal(38,6), stock.out_quantity) - FROM dbo.bas_stock stock - JOIN dbo.bas_cargo_space cargo ON cargo.cargo_space_id = stock.cargo_space_id - WHERE cargo.warehouse_id = @warehouse_id AND cargo.group_coding_no = @lane_code; - END - ELSE - BEGIN - THROW 50001, ''Unsupported inventory fingerprint source'', 1; - END; + SELECT @generation = COALESCE(@snapshot_generation, state.snapshot_generation) + FROM alg.shadow_snapshot_state state + WHERE state.warehouse_id = @warehouse_id + AND state.snapshot_status = 'PUBLISHED'; + + IF @generation IS NULL + THROW 51023, 'FT546 shadow fingerprint requires a snapshot generation', 1; SELECT @payload = ISNULL(CONVERT(nvarchar(max), ( - SELECT cargo.cargo_space_id AS [@space_id], cargo.status AS [@status], + SELECT cargo.space_id AS [@space_id], cargo.space_status AS [@status], cargo.locked AS [@locked], cargo.tray_code AS [@tray_code], - cargo.group_coding AS [@depth], cargo.the_platoon AS [@platoon], - cargo.the_column AS [@column], cargo.the_floor AS [@floor], + cargo.depth_raw AS [@depth], cargo.platoon AS [@platoon], + cargo.column_no AS [@column], cargo.floor_no AS [@floor], topology.lane_depth AS [@lane_depth], topology.base_zone AS [@base_zone], - ISNULL(role.role, ''STORAGE'') AS [@role] - FROM dbo.bas_cargo_space cargo - LEFT JOIN alg.space_topology topology ON topology.space_id = cargo.cargo_space_id - LEFT JOIN alg.space_role role ON role.space_id = cargo.cargo_space_id - WHERE cargo.warehouse_id = @warehouse_id AND cargo.group_coding_no = @lane_code - ORDER BY TRY_CONVERT(int, cargo.group_coding), cargo.cargo_space_id - FOR XML PATH(''space''), ROOT(''spaces''), TYPE - )), N''''); + ISNULL(role.role, 'STORAGE') AS [@role] + FROM alg.cargo_space_snapshot cargo + LEFT JOIN alg.space_topology topology + ON topology.warehouse_id = cargo.warehouse_id + AND topology.space_id = cargo.space_id + AND topology.snapshot_generation = @generation + LEFT JOIN alg.space_role role ON role.space_id = cargo.space_id + WHERE cargo.warehouse_id = @warehouse_id + AND cargo.lane_code = @lane_code + AND cargo.snapshot_generation = @generation + ORDER BY TRY_CONVERT(int, cargo.depth_raw), cargo.space_id + FOR XML PATH('space'), ROOT('spaces'), TYPE + )), ''); - SET @payload = @payload + N''|INVENTORY|'' + ISNULL(CONVERT(nvarchar(max), ( - SELECT stock.stock_id AS [@stock_id], stock.space_id AS [@space_id], - stock.tray_code AS [@tray_code], stock.project_no AS [@project_no], - stock.product_no AS [@product_no], stock.lot_no AS [@lot_no], - stock.inbound_time AS [@inbound_time], - stock.quantity AS [@quantity], stock.out_quantity AS [@out_quantity] - FROM #fingerprint_inventory stock - ORDER BY stock.space_id, stock.stock_id - FOR XML PATH(''item''), ROOT(''inventory''), TYPE - )), N''''); + SET @payload = @payload + '|INVENTORY|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT item.stock_id AS [@stock_id], item.space_id AS [@space_id], + item.tray_code AS [@tray_code], item.project_no AS [@project_no], + item.product_no AS [@product_no], item.lot_no AS [@lot_no], + item.inbound_time_raw AS [@inbound_time], item.quantity AS [@quantity], + item.out_quantity AS [@out_quantity] + FROM alg.inventory_item_snapshot item + WHERE item.warehouse_id = @warehouse_id + AND item.snapshot_generation = @generation + AND EXISTS (SELECT 1 FROM alg.cargo_space_snapshot cargo + WHERE cargo.warehouse_id = item.warehouse_id + AND cargo.space_id = item.space_id + AND cargo.lane_code = @lane_code + AND cargo.snapshot_generation = @generation) + ORDER BY item.space_id, item.stock_id + FOR XML PATH('item'), ROOT('inventory'), TYPE + )), ''); - SET @payload = @payload + N''|ACTIVE_JOBS|'' + ISNULL(CONVERT(nvarchar(max), ( + SET @payload = @payload + '|ACTIVE_JOBS|' + ISNULL(CONVERT(nvarchar(max), ( SELECT job.job_id AS [@job_id], job.task_no AS [@task_no], job.tray_code AS [@tray_code], - job.source_space_id AS [@source_space_id], job.target_cargo_space_id AS [@target_space_id], + job.source_space_id AS [@source_space_id], job.target_space_id AS [@target_space_id], job.status AS [@status], job.in_or_out AS [@in_or_out], job.create_time_raw AS [@create_time] - FROM alg.v_active_job job - WHERE job.warehouse_id = @warehouse_id AND job.source_lane_code = @lane_code + FROM alg.active_job_snapshot job + JOIN alg.cargo_space_snapshot source_cargo + ON source_cargo.warehouse_id = job.warehouse_id + AND source_cargo.space_id = job.source_space_id + AND source_cargo.snapshot_generation = job.snapshot_generation + WHERE job.warehouse_id = @warehouse_id + AND source_cargo.lane_code = @lane_code + AND job.snapshot_generation = @generation ORDER BY job.job_id - FOR XML PATH(''job''), ROOT(''jobs''), TYPE - )), N''''); + FOR XML PATH('job'), ROOT('jobs'), TYPE + )), ''); - SET @payload = @payload + N''|PROJECT_POLICY|'' + ISNULL(CONVERT(nvarchar(max), ( + SET @payload = @payload + '|PROJECT_POLICY|' + ISNULL(CONVERT(nvarchar(max), ( SELECT policy.project_no AS [@project_no], policy.is_isolated AS [@is_isolated], policy.is_common AS [@is_common], policy.allow_mixed AS [@allow_mixed] FROM alg.project_policy policy ORDER BY policy.project_no - FOR XML PATH(''project''), ROOT(''project_policy''), TYPE - )), N''''); + FOR XML PATH('project'), ROOT('project_policy'), TYPE + )), ''); - SET @payload = @payload + N''|WAREHOUSE_POLICY|'' + ISNULL(CONVERT(nvarchar(max), ( + SET @payload = @payload + '|WAREHOUSE_POLICY|' + ISNULL(CONVERT(nvarchar(max), ( SELECT policy.warehouse_id AS [@warehouse_id], policy.target_fill_rate AS [@target_fill_rate], policy.fifo_tolerance_hours AS [@fifo_tolerance_hours] FROM alg.warehouse_policy policy WHERE policy.warehouse_id = @warehouse_id - FOR XML PATH(''warehouse''), ROOT(''warehouse_policy''), TYPE - )), N''''); + FOR XML PATH('warehouse'), ROOT('warehouse_policy'), TYPE + )), ''); - SET @payload = @payload + N''|RESERVATIONS|'' + ISNULL(CONVERT(nvarchar(max), ( + SET @payload = @payload + '|RESERVATIONS|' + ISNULL(CONVERT(nvarchar(max), ( SELECT reservation.reservation_id AS [@reservation_id], reservation.space_id AS [@space_id], reservation.project_no AS [@project_no], reservation.status AS [@status] FROM alg.reservation reservation - WHERE reservation.warehouse_id = @warehouse_id AND reservation.lane_code = @lane_code - AND reservation.status = ''ACTIVE'' + WHERE reservation.warehouse_id = @warehouse_id + AND reservation.lane_code = @lane_code + AND reservation.status = 'ACTIVE' ORDER BY reservation.space_id, reservation.reservation_id - FOR XML PATH(''reservation''), ROOT(''reservations''), TYPE - )), N''''); + FOR XML PATH('reservation'), ROOT('reservations'), TYPE + )), ''); - SET @semantic_fingerprint = HASHBYTES(''SHA2_256'', CONVERT(varbinary(max), @payload)); -END;'); + SET @semantic_fingerprint = HASHBYTES('SHA2_256', CONVERT(varbinary(max), @payload)); +END; GO -IF OBJECT_ID(N'alg.usp_rebuild_lane_state', N'P') IS NOT NULL DROP PROCEDURE alg.usp_rebuild_lane_state; -GO -EXEC(N' CREATE PROCEDURE alg.usp_rebuild_lane_state @warehouse_id varchar(80) = NULL, - @lane_code varchar(80) = NULL + @lane_code varchar(80) = NULL, + @snapshot_generation bigint = NULL AS BEGIN SET NOCOUNT ON; - SELECT warehouse_id, lane_code, - COUNT(*) AS space_count, - SUM(CASE WHEN space_status = ''2'' OR stock_detail_count > 0 THEN 1 ELSE 0 END) AS occupied_count, - SUM(CASE WHEN inbound_available = 1 THEN 1 ELSE 0 END) AS available_count, - MAX(CASE WHEN pending_outbound = 1 THEN 1 ELSE 0 END) AS pending_outbound, - MAX(CASE WHEN has_gap = 1 THEN 1 ELSE 0 END) AS has_gap, - CAST(NULL AS varbinary(32)) AS semantic_fingerprint + IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '') IS NULL + THROW 51019, 'warehouse_id is required', 1; + DECLARE @generation bigint; + + SET @generation = @snapshot_generation; + IF @generation IS NULL + SELECT @generation = state.snapshot_generation + FROM alg.shadow_snapshot_state state + WHERE state.warehouse_id = @warehouse_id + AND state.snapshot_status = 'PUBLISHED'; + + IF @generation IS NULL + THROW 51024, 'FT546 shadow lane rebuild requires a snapshot generation', 1; + + ;WITH outbound_lanes AS ( + SELECT DISTINCT job.warehouse_id, cargo.lane_code + FROM alg.active_job_snapshot job + JOIN alg.cargo_space_snapshot cargo + ON cargo.warehouse_id = job.warehouse_id + AND cargo.space_id = job.source_space_id + AND cargo.snapshot_generation = @generation + WHERE job.snapshot_generation = @generation + AND TRY_CONVERT(int, job.in_or_out) >= 20 + AND cargo.lane_code IS NOT NULL + ), lane_gaps AS ( + SELECT DISTINCT inner_space.warehouse_id, inner_space.lane_code + FROM alg.cargo_space_snapshot inner_space + JOIN alg.cargo_space_snapshot outer_space + ON outer_space.warehouse_id = inner_space.warehouse_id + AND outer_space.lane_code = inner_space.lane_code + AND outer_space.snapshot_generation = @generation + AND TRY_CONVERT(int, outer_space.depth_raw) > TRY_CONVERT(int, inner_space.depth_raw) + LEFT JOIN alg.space_inventory_summary inner_stock + ON inner_stock.warehouse_id = inner_space.warehouse_id + AND inner_stock.space_id = inner_space.space_id + AND inner_stock.snapshot_generation = @generation + LEFT JOIN alg.space_inventory_summary outer_stock + ON outer_stock.warehouse_id = outer_space.warehouse_id + AND outer_stock.space_id = outer_space.space_id + AND outer_stock.snapshot_generation = @generation + WHERE inner_space.snapshot_generation = @generation + AND NULLIF(LTRIM(RTRIM(inner_space.tray_code)), '') IS NULL + AND inner_stock.space_id IS NULL + AND (NULLIF(LTRIM(RTRIM(outer_space.tray_code)), '') IS NOT NULL + OR outer_stock.space_id IS NOT NULL) + ) + SELECT cargo.warehouse_id, cargo.lane_code, + COUNT(*) AS space_count, + SUM(CASE WHEN cargo.space_status = '2' OR ISNULL(stock.stock_detail_count, 0) > 0 THEN 1 ELSE 0 END) AS occupied_count, + SUM(CASE WHEN cargo.space_status = '1' + AND NULLIF(LTRIM(RTRIM(cargo.locked)), '') IS NULL + AND NULLIF(LTRIM(RTRIM(cargo.tray_code)), '') IS NULL + AND ISNULL(stock.stock_detail_count, 0) = 0 + AND ISNULL(role.role, 'STORAGE') = 'STORAGE' + AND outbound_lanes.lane_code IS NULL + AND reservation.reservation_id IS NULL + AND lane_gaps.lane_code IS NULL + THEN 1 ELSE 0 END) AS available_count, + CONVERT(bit, CASE WHEN outbound_lanes.lane_code IS NULL THEN 0 ELSE 1 END) AS pending_outbound, + CONVERT(bit, CASE WHEN lane_gaps.lane_code IS NULL THEN 0 ELSE 1 END) AS has_gap, + CAST(NULL AS varbinary(32)) AS semantic_fingerprint INTO #source_state - FROM alg.v_space_state - WHERE (@warehouse_id IS NULL OR warehouse_id = @warehouse_id) - AND (@lane_code IS NULL OR lane_code = @lane_code) - GROUP BY warehouse_id, lane_code; + FROM alg.cargo_space_snapshot cargo + LEFT JOIN alg.space_inventory_summary stock + ON stock.warehouse_id = cargo.warehouse_id + AND stock.space_id = cargo.space_id + AND stock.snapshot_generation = @generation + LEFT JOIN alg.space_role role ON role.space_id = cargo.space_id + LEFT JOIN outbound_lanes + ON outbound_lanes.warehouse_id = cargo.warehouse_id + AND outbound_lanes.lane_code = cargo.lane_code + LEFT JOIN alg.reservation reservation + ON reservation.space_id = cargo.space_id + AND reservation.status = 'ACTIVE' + LEFT JOIN lane_gaps + ON lane_gaps.warehouse_id = cargo.warehouse_id + AND lane_gaps.lane_code = cargo.lane_code + WHERE cargo.snapshot_generation = @generation + AND (@warehouse_id IS NULL OR cargo.warehouse_id = @warehouse_id) + AND (@lane_code IS NULL OR cargo.lane_code = @lane_code) + GROUP BY cargo.warehouse_id, cargo.lane_code, + outbound_lanes.lane_code, lane_gaps.lane_code; DECLARE @fingerprint_warehouse varchar(80), @fingerprint_lane varchar(80), @fingerprint varbinary(32); @@ -601,7 +797,8 @@ BEGIN @warehouse_id = @fingerprint_warehouse, @lane_code = @fingerprint_lane, @semantic_fingerprint = @fingerprint OUTPUT, - @inventory_source = ''SNAPSHOT''; + @inventory_source = 'SNAPSHOT', + @snapshot_generation = @generation; UPDATE #source_state SET semantic_fingerprint = @fingerprint WHERE warehouse_id = @fingerprint_warehouse AND lane_code = @fingerprint_lane; FETCH NEXT FROM fingerprint_cursor INTO @fingerprint_warehouse, @fingerprint_lane; @@ -617,6 +814,7 @@ BEGIN target.available_count <> source.available_count OR target.pending_outbound <> source.pending_outbound OR target.has_gap <> source.has_gap OR + target.snapshot_generation <> @generation OR target.semantic_fingerprint IS NULL OR target.semantic_fingerprint <> source.semantic_fingerprint) THEN UPDATE SET space_count = source.space_count, @@ -625,43 +823,430 @@ BEGIN pending_outbound = source.pending_outbound, has_gap = source.has_gap, semantic_fingerprint = source.semantic_fingerprint, + snapshot_generation = @generation, rebuilt_at = SYSUTCDATETIME() WHEN NOT MATCHED BY TARGET THEN INSERT (warehouse_id, lane_code, space_count, occupied_count, available_count, - pending_outbound, has_gap, semantic_fingerprint) + pending_outbound, has_gap, semantic_fingerprint, snapshot_generation) VALUES (source.warehouse_id, source.lane_code, source.space_count, source.occupied_count, - source.available_count, source.pending_outbound, source.has_gap, source.semantic_fingerprint) + source.available_count, source.pending_outbound, source.has_gap, + source.semantic_fingerprint, @generation) WHEN NOT MATCHED BY SOURCE AND (@warehouse_id IS NULL OR target.warehouse_id = @warehouse_id) AND (@lane_code IS NULL OR target.lane_code = @lane_code) THEN DELETE; -END;'); +END; GO -IF OBJECT_ID(N'alg.usp_refresh_dirty_lanes', N'P') IS NOT NULL DROP PROCEDURE alg.usp_refresh_dirty_lanes; -GO -EXEC(N' CREATE PROCEDURE alg.usp_refresh_dirty_lanes @warehouse_id varchar(80) AS BEGIN SET NOCOUNT ON; - IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '''') IS NULL - THROW 50002, ''warehouse_id is required'', 1; + IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '') IS NULL + THROW 51002, 'warehouse_id is required', 1; - EXEC alg.usp_rebuild_space_topology @warehouse_id = @warehouse_id; - EXEC alg.usp_sync_inventory_snapshot @warehouse_id = @warehouse_id; - EXEC alg.usp_rebuild_lane_state @warehouse_id = @warehouse_id; -END;'); -GO + DECLARE @generation bigint; + SELECT @generation = snapshot_generation + FROM alg.shadow_snapshot_state + WHERE warehouse_id = @warehouse_id AND snapshot_status = 'PUBLISHED'; + IF @generation IS NULL + THROW 51003, 'published snapshot is required', 1; -IF NOT EXISTS (SELECT 1 FROM alg.space_topology) - AND NOT EXISTS (SELECT 1 FROM alg.inventory_item_snapshot) - AND NOT EXISTS (SELECT 1 FROM alg.lane_state) -BEGIN - EXEC alg.usp_rebuild_space_topology; - EXEC alg.usp_sync_inventory_snapshot; - EXEC alg.usp_rebuild_lane_state; + EXEC alg.usp_rebuild_space_topology @warehouse_id = @warehouse_id, + @snapshot_generation = @generation; + EXEC alg.usp_sync_inventory_snapshot @warehouse_id = @warehouse_id, + @snapshot_generation = @generation; + EXEC alg.usp_rebuild_lane_state @warehouse_id = @warehouse_id, + @snapshot_generation = @generation; +END; +GO + +CREATE PROCEDURE alg.usp_claim_shadow_instance + @warehouse_id varchar(80), + @instance_id varchar(80), + @claimed bit OUTPUT +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + SET @claimed = 0; + + IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '') IS NULL + THROW 51004, 'warehouse_id is required', 1; + IF NULLIF(LTRIM(RTRIM(@instance_id)), '') IS NULL + THROW 51005, 'instance_id is required', 1; + + DECLARE @now datetime2 = SYSUTCDATETIME(), @current_instance varchar(80), + @last_seen_at datetime2; + + BEGIN TRY + BEGIN TRANSACTION; + SELECT @current_instance = heartbeat.instance_id, @last_seen_at = heartbeat.last_seen_at + FROM alg.shadow_instance_heartbeat heartbeat WITH (UPDLOCK, HOLDLOCK) + WHERE heartbeat.warehouse_id = @warehouse_id; + + IF @current_instance IS NULL + BEGIN + INSERT INTO alg.shadow_instance_heartbeat (warehouse_id, instance_id, claimed_at, last_seen_at) + VALUES (@warehouse_id, @instance_id, @now, @now); + SET @claimed = 1; + END + ELSE IF @current_instance = @instance_id + BEGIN + UPDATE alg.shadow_instance_heartbeat + SET last_seen_at = @now + WHERE warehouse_id = @warehouse_id; + SET @claimed = 1; + END + ELSE IF @last_seen_at < DATEADD(second, -120, @now) + BEGIN + UPDATE alg.shadow_instance_heartbeat + SET instance_id = @instance_id, claimed_at = @now, last_seen_at = @now + WHERE warehouse_id = @warehouse_id; + SET @claimed = 1; + END; + + COMMIT TRANSACTION; + END TRY + BEGIN CATCH + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + THROW; + END CATCH; + SELECT @claimed AS claimed; +END; +GO + +CREATE PROCEDURE alg.usp_refresh_shadow_snapshot + @warehouse_id varchar(80), + @snapshot_generation bigint, + @instance_id varchar(80) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '') IS NULL + THROW 51006, 'warehouse_id is required', 1; + IF NULLIF(LTRIM(RTRIM(@instance_id)), '') IS NULL + THROW 51007, 'instance_id is required', 1; + IF @snapshot_generation <= 0 + THROW 51008, 'snapshot_generation must be positive', 1; + + DECLARE @pre_source_count bigint, @post_source_count bigint, + @pre_source_signature varbinary(32), @post_source_signature varbinary(32), + @source_payload nvarchar(max), @synchronized_at datetime2, + @current_generation bigint; + + BEGIN TRY + BEGIN TRANSACTION; + + SELECT @current_generation = state.snapshot_generation + FROM alg.shadow_snapshot_state state WITH (UPDLOCK, HOLDLOCK) + WHERE state.warehouse_id = @warehouse_id; + + IF @current_generation IS NOT NULL AND @snapshot_generation <= @current_generation + THROW 51009, 'snapshot_generation must advance the published generation', 1; + + SELECT @pre_source_count = + (SELECT COUNT_BIG(*) FROM dbo.bas_cargo_space cargo WITH (NOLOCK) WHERE cargo.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.bas_stock stock WITH (NOLOCK) + JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id + WHERE cargo.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.mydo_job job WITH (NOLOCK) WHERE job.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.mydo_job_detail detail WITH (NOLOCK) + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK) + JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK) + WHERE outbound.warehouse_id = @warehouse_id); + + SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), ( + SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id], + cargo.group_coding_no AS [@lane_code], cargo.cargo_area_id AS [@cargo_area_id], + cargo.roadway_id AS [@roadway_id], cargo.cargo_space_name AS [@display_name], + cargo.the_platoon AS [@platoon], cargo.the_column AS [@column_no], + cargo.the_floor AS [@floor_no], cargo.group_coding AS [@depth_raw], + cargo.status AS [@space_status], cargo.locked AS [@locked], cargo.tray_code AS [@tray_code] + FROM dbo.bas_cargo_space cargo WITH (NOLOCK) + WHERE cargo.warehouse_id = @warehouse_id + ORDER BY cargo.cargo_space_id + FOR XML PATH('cargo'), ROOT('cargo_spaces'), TYPE + )), ''); + + SET @source_payload = @source_payload + '|STOCK|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT stock.stock_id AS [@stock_id], stock.cargo_space_id AS [@space_id], + stock.warehouse_id AS [@warehouse_id], stock.tray_code AS [@tray_code], + stock.spare4 AS [@project_no], stock.product_id AS [@product_no], + stock.lot_no AS [@lot_no], stock.in_date AS [@in_date], + stock.quantity AS [@quantity], stock.out_quantity AS [@out_quantity] + FROM dbo.bas_stock stock WITH (NOLOCK) + JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id + WHERE cargo.warehouse_id = @warehouse_id + ORDER BY stock.stock_id + FOR XML PATH('stock'), ROOT('stocks'), TYPE + )), ''); + + SET @source_payload = @source_payload + '|JOB|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT job.job_id AS [@job_id], job.status AS [@status], job.task_no AS [@task_no], + job.tray_code AS [@tray_code], job.cargo_space_id AS [@source_space_id], + job.target_cargo_space_id AS [@target_space_id], job.in_or_out AS [@in_or_out], + job.create_time AS [@create_time], job.finish_time AS [@finish_time], + job.warehouse_id AS [@warehouse_id] + FROM dbo.mydo_job job WITH (NOLOCK) + WHERE job.warehouse_id = @warehouse_id + ORDER BY job.job_id + FOR XML PATH('job'), ROOT('jobs'), TYPE + )), ''); + + SET @source_payload = @source_payload + '|JOB_DETAIL|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT detail.job_detail_id AS [@job_detail_id], detail.job_id AS [@job_id], + detail.stock_id AS [@stock_id], detail.lot_no AS [@lot_no], + detail.in_date AS [@in_date], detail.in_or_out_id AS [@request_id], + detail.in_or_out_d_id AS [@request_detail_id], detail.product_id AS [@product_id], + detail.stock_status AS [@stock_status], detail.in_quantity AS [@in_quantity], + detail.stock_quantity AS [@stock_quantity], detail.out_quantity AS [@out_quantity], + detail.pick_quantity AS [@pick_quantity], detail.stockin_detail_id AS [@stockin_detail_id] + FROM dbo.mydo_job_detail detail WITH (NOLOCK) + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id + ORDER BY detail.job_detail_id + FOR XML PATH('job_detail'), ROOT('job_details'), TYPE + )), ''); + + SET @source_payload = @source_payload + '|INBOUND|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT inbound.stockin_detail_id AS [@stockin_detail_id], inbound.stockin_guid AS [@stockin_guid], + inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no], + inbound.product_id AS [@product_id], inbound.quantity AS [@quantity], + inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no], + job.warehouse_id AS [@warehouse_id], detail.job_id AS [@job_id] + FROM dbo.in_stockin_detail inbound WITH (NOLOCK) + JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id + ORDER BY inbound.stockin_detail_id + FOR XML PATH('inbound'), ROOT('inbound_requests'), TYPE + )), ''); + + SET @source_payload = @source_payload + '|OUTBOUND|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id], + outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no], + outbound.quantity AS [@quantity], outbound.assign_num AS [@assign_num], + outbound.allot_quantity AS [@allot_quantity], outbound.create_time AS [@create_time] + FROM dbo.out_stockout_detail outbound WITH (NOLOCK) + WHERE outbound.warehouse_id = @warehouse_id + ORDER BY outbound.stockout_detail_id + FOR XML PATH('outbound'), ROOT('outbound_requests'), TYPE + )), ''); + + SET @pre_source_signature = HASHBYTES('SHA2_256', CONVERT(varbinary(max), @source_payload)); + + DELETE FROM alg.cargo_space_snapshot WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.inventory_item_snapshot WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.active_job_snapshot WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.shadow_inbound_request_snapshot WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.shadow_outbound_request_snapshot WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.space_topology WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.space_inventory_summary WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.lane_state WHERE warehouse_id = @warehouse_id; + + SET @synchronized_at = SYSUTCDATETIME(); + + INSERT INTO alg.cargo_space_snapshot ( + warehouse_id, space_id, lane_code, cargo_area_id, roadway_id, display_name, + platoon, column_no, floor_no, depth_raw, space_status, locked, tray_code, + snapshot_generation, synchronized_at) + SELECT cargo.warehouse_id, cargo.cargo_space_id, cargo.group_coding_no, + cargo.cargo_area_id, cargo.roadway_id, cargo.cargo_space_name, + cargo.the_platoon, cargo.the_column, cargo.the_floor, cargo.group_coding, + cargo.status, cargo.locked, cargo.tray_code, + @snapshot_generation, @synchronized_at + FROM dbo.bas_cargo_space cargo WITH (NOLOCK) + WHERE cargo.warehouse_id = @warehouse_id; + + INSERT INTO alg.inventory_item_snapshot ( + warehouse_id, stock_id, space_id, tray_code, project_no, product_no, lot_no, + inbound_time_raw, inbound_at, quantity, out_quantity, snapshot_generation, synchronized_at) + SELECT cargo.warehouse_id, stock.stock_id, stock.cargo_space_id, stock.tray_code, + stock.spare4, stock.product_id, stock.lot_no, stock.in_date, + TRY_CONVERT(datetime2, stock.in_date), stock.quantity, stock.out_quantity, + @snapshot_generation, @synchronized_at + FROM dbo.bas_stock stock WITH (NOLOCK) + JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id + WHERE cargo.warehouse_id = @warehouse_id; + + INSERT INTO alg.active_job_snapshot ( + warehouse_id, job_id, task_no, tray_code, source_space_id, target_space_id, + status, in_or_out, create_time_raw, snapshot_generation, synchronized_at) + SELECT job.warehouse_id, job.job_id, job.task_no, job.tray_code, + NULLIF(LTRIM(RTRIM(job.cargo_space_id)), ''), + NULLIF(LTRIM(RTRIM(job.target_cargo_space_id)), ''), job.status, + job.in_or_out, job.create_time, @snapshot_generation, @synchronized_at + FROM dbo.mydo_job job WITH (NOLOCK) + WHERE job.warehouse_id = @warehouse_id + AND (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = ''); + + ;WITH inbound_source AS ( + SELECT job.warehouse_id, job.task_no, job.tray_code, + detail.in_or_out_id AS request_id, job.warehouse_id AS raw_warehouse_id, + inbound.spare4 AS project_no, inbound.product_id, inbound.lot_no, + ROW_NUMBER() OVER ( + PARTITION BY job.warehouse_id, job.task_no, job.tray_code + ORDER BY inbound.stockin_detail_id, detail.job_detail_id) AS source_rank + FROM dbo.in_stockin_detail inbound WITH (NOLOCK) + JOIN dbo.mydo_job_detail detail WITH (NOLOCK) + ON detail.stockin_detail_id = inbound.stockin_detail_id + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id + ) + INSERT INTO alg.shadow_inbound_request_snapshot ( + warehouse_id, task_no, tray_no, request_id, raw_warehouse_id, + project_no, product_id, lot_no, snapshot_generation, synchronized_at) + SELECT warehouse_id, ISNULL(task_no, ''), ISNULL(tray_code, ''), request_id, + raw_warehouse_id, project_no, product_id, lot_no, + @snapshot_generation, @synchronized_at + FROM inbound_source + WHERE source_rank = 1; + + INSERT INTO alg.shadow_outbound_request_snapshot ( + warehouse_id, stockout_detail_id, raw_warehouse_id, project_no, product_id, + requested_quantity, snapshot_generation, synchronized_at) + SELECT outbound.warehouse_id, outbound.stockout_detail_id, outbound.warehouse_id, + NULL, outbound.product_id, CONVERT(decimal(38,6), outbound.quantity), + @snapshot_generation, @synchronized_at + FROM dbo.out_stockout_detail outbound WITH (NOLOCK) + WHERE outbound.warehouse_id = @warehouse_id; + + EXEC alg.usp_rebuild_space_topology @warehouse_id = @warehouse_id, + @snapshot_generation = @snapshot_generation; + EXEC alg.usp_sync_inventory_snapshot @warehouse_id = @warehouse_id, + @snapshot_generation = @snapshot_generation; + EXEC alg.usp_rebuild_lane_state @warehouse_id = @warehouse_id, + @snapshot_generation = @snapshot_generation; + + SELECT @post_source_count = + (SELECT COUNT_BIG(*) FROM dbo.bas_cargo_space cargo WITH (NOLOCK) WHERE cargo.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.bas_stock stock WITH (NOLOCK) + JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id + WHERE cargo.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.mydo_job job WITH (NOLOCK) WHERE job.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.mydo_job_detail detail WITH (NOLOCK) + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK) + JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK) + WHERE outbound.warehouse_id = @warehouse_id); + + SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), ( + SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id], + cargo.group_coding_no AS [@lane_code], cargo.cargo_area_id AS [@cargo_area_id], + cargo.roadway_id AS [@roadway_id], cargo.cargo_space_name AS [@display_name], + cargo.the_platoon AS [@platoon], cargo.the_column AS [@column_no], + cargo.the_floor AS [@floor_no], cargo.group_coding AS [@depth_raw], + cargo.status AS [@space_status], cargo.locked AS [@locked], cargo.tray_code AS [@tray_code] + FROM dbo.bas_cargo_space cargo WITH (NOLOCK) + WHERE cargo.warehouse_id = @warehouse_id + ORDER BY cargo.cargo_space_id + FOR XML PATH('cargo'), ROOT('cargo_spaces'), TYPE + )), ''); + SET @source_payload = @source_payload + '|STOCK|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT stock.stock_id AS [@stock_id], stock.cargo_space_id AS [@space_id], + stock.warehouse_id AS [@warehouse_id], stock.tray_code AS [@tray_code], + stock.spare4 AS [@project_no], stock.product_id AS [@product_no], + stock.lot_no AS [@lot_no], stock.in_date AS [@in_date], + stock.quantity AS [@quantity], stock.out_quantity AS [@out_quantity] + FROM dbo.bas_stock stock WITH (NOLOCK) + JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id + WHERE cargo.warehouse_id = @warehouse_id + ORDER BY stock.stock_id + FOR XML PATH('stock'), ROOT('stocks'), TYPE + )), ''); + SET @source_payload = @source_payload + '|JOB|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT job.job_id AS [@job_id], job.status AS [@status], job.task_no AS [@task_no], + job.tray_code AS [@tray_code], job.cargo_space_id AS [@source_space_id], + job.target_cargo_space_id AS [@target_space_id], job.in_or_out AS [@in_or_out], + job.create_time AS [@create_time], job.finish_time AS [@finish_time], + job.warehouse_id AS [@warehouse_id] + FROM dbo.mydo_job job WITH (NOLOCK) + WHERE job.warehouse_id = @warehouse_id + ORDER BY job.job_id + FOR XML PATH('job'), ROOT('jobs'), TYPE + )), ''); + SET @source_payload = @source_payload + '|JOB_DETAIL|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT detail.job_detail_id AS [@job_detail_id], detail.job_id AS [@job_id], + detail.stock_id AS [@stock_id], detail.lot_no AS [@lot_no], + detail.in_date AS [@in_date], detail.in_or_out_id AS [@request_id], + detail.in_or_out_d_id AS [@request_detail_id], detail.product_id AS [@product_id], + detail.stock_status AS [@stock_status], detail.in_quantity AS [@in_quantity], + detail.stock_quantity AS [@stock_quantity], detail.out_quantity AS [@out_quantity], + detail.pick_quantity AS [@pick_quantity], detail.stockin_detail_id AS [@stockin_detail_id] + FROM dbo.mydo_job_detail detail WITH (NOLOCK) + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id + ORDER BY detail.job_detail_id + FOR XML PATH('job_detail'), ROOT('job_details'), TYPE + )), ''); + SET @source_payload = @source_payload + '|INBOUND|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT inbound.stockin_detail_id AS [@stockin_detail_id], inbound.stockin_guid AS [@stockin_guid], + inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no], + inbound.product_id AS [@product_id], inbound.quantity AS [@quantity], + inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no], + job.warehouse_id AS [@warehouse_id], detail.job_id AS [@job_id] + FROM dbo.in_stockin_detail inbound WITH (NOLOCK) + JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id + ORDER BY inbound.stockin_detail_id + FOR XML PATH('inbound'), ROOT('inbound_requests'), TYPE + )), ''); + SET @source_payload = @source_payload + '|OUTBOUND|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id], + outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no], + outbound.quantity AS [@quantity], outbound.assign_num AS [@assign_num], + outbound.allot_quantity AS [@allot_quantity], outbound.create_time AS [@create_time] + FROM dbo.out_stockout_detail outbound WITH (NOLOCK) + WHERE outbound.warehouse_id = @warehouse_id + ORDER BY outbound.stockout_detail_id + FOR XML PATH('outbound'), ROOT('outbound_requests'), TYPE + )), ''); + SET @post_source_signature = HASHBYTES('SHA2_256', CONVERT(varbinary(max), @source_payload)); + + IF @pre_source_count <> @post_source_count + OR @pre_source_signature <> @post_source_signature + THROW 51025, 'FT546 shadow refresh refused: source changed during refresh', 1; + + MERGE alg.shadow_snapshot_state WITH (HOLDLOCK) AS target + USING (SELECT @warehouse_id AS warehouse_id) AS source + ON source.warehouse_id = target.warehouse_id + WHEN MATCHED THEN UPDATE SET + snapshot_generation = @snapshot_generation, + snapshot_status = 'PUBLISHED', + synchronized_at = SYSUTCDATETIME(), + source_signature = @post_source_signature, + source_row_count = @post_source_count, + instance_id = @instance_id, + last_error = NULL + WHEN NOT MATCHED THEN INSERT ( + warehouse_id, snapshot_generation, snapshot_status, synchronized_at, + source_signature, source_row_count, instance_id, last_error) + VALUES ( + @warehouse_id, @snapshot_generation, 'PUBLISHED', SYSUTCDATETIME(), + @post_source_signature, @post_source_count, @instance_id, NULL); + + COMMIT TRANSACTION; + END TRY + BEGIN CATCH + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + THROW; + END CATCH; END; GO diff --git a/fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql b/fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql index a24af43..21d0e02 100644 --- a/fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql +++ b/fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql @@ -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 diff --git a/fortucky-algorithm/src/main/resources/db/sqlserver/upgrade-shadow-v2-to-v3.sql b/fortucky-algorithm/src/main/resources/db/sqlserver/upgrade-shadow-v2-to-v3.sql new file mode 100644 index 0000000..b4db8fe --- /dev/null +++ b/fortucky-algorithm/src/main/resources/db/sqlserver/upgrade-shadow-v2-to-v3.sql @@ -0,0 +1,1291 @@ +/* + Upgrade the exact FT546 shadow package from v2 to atomic snapshot generations. + SQL Server 2016 RTM only. This script writes only alg.* and preserves + alg.decision_log rows. +*/ +SET NOCOUNT ON; +SET XACT_ABORT ON; +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + +IF SCHEMA_ID(N'alg') IS NULL + THROW 51030, 'FT546 shadow upgrade refused: alg schema does not exist', 1; +IF OBJECT_ID(N'alg.shadow_package_manifest', N'U') IS NULL + THROW 51031, 'FT546 shadow upgrade refused: shadow package manifest does not match', 1; + +DECLARE @expected_v2 TABLE ( + object_name sysname NOT NULL PRIMARY KEY, + object_type char(2) NOT NULL +); +INSERT INTO @expected_v2 (object_name, object_type) VALUES + (N'alg.shadow_package_manifest', 'U'), + (N'alg.warehouse_policy', 'U'), + (N'alg.project_policy', 'U'), + (N'alg.space_role', 'U'), + (N'alg.space_topology', 'U'), + (N'alg.inventory_item_snapshot', 'U'), + (N'alg.space_inventory_summary', 'U'), + (N'alg.lane_state', 'U'), + (N'alg.reservation', 'U'), + (N'alg.deep_workflow_lock', 'U'), + (N'alg.decision_log', 'U'), + (N'alg.v_inventory_item', 'V'), + (N'alg.v_active_job', 'V'), + (N'alg.v_space_state', 'V'), + (N'alg.usp_rebuild_space_topology', 'P'), + (N'alg.usp_sync_inventory_snapshot', 'P'), + (N'alg.usp_compute_lane_semantic_fingerprint', 'P'), + (N'alg.usp_rebuild_lane_state', 'P'), + (N'alg.usp_refresh_dirty_lanes', 'P'); + +DECLARE @expected_v3 TABLE ( + object_name sysname NOT NULL PRIMARY KEY, + object_type char(2) NOT NULL +); +INSERT INTO @expected_v3 (object_name, object_type) VALUES + (N'alg.shadow_package_manifest', 'U'), + (N'alg.warehouse_policy', 'U'), + (N'alg.project_policy', 'U'), + (N'alg.space_role', 'U'), + (N'alg.space_topology', 'U'), + (N'alg.inventory_item_snapshot', 'U'), + (N'alg.space_inventory_summary', 'U'), + (N'alg.lane_state', 'U'), + (N'alg.reservation', 'U'), + (N'alg.deep_workflow_lock', 'U'), + (N'alg.decision_log', 'U'), + (N'alg.shadow_snapshot_state', 'U'), + (N'alg.cargo_space_snapshot', 'U'), + (N'alg.active_job_snapshot', 'U'), + (N'alg.shadow_inbound_request_snapshot', 'U'), + (N'alg.shadow_outbound_request_snapshot', 'U'), + (N'alg.shadow_runtime_metric', 'U'), + (N'alg.shadow_instance_heartbeat', 'U'), + (N'alg.v_inventory_item', 'V'), + (N'alg.v_active_job', 'V'), + (N'alg.v_space_state', 'V'), + (N'alg.usp_rebuild_space_topology', 'P'), + (N'alg.usp_sync_inventory_snapshot', 'P'), + (N'alg.usp_compute_lane_semantic_fingerprint', 'P'), + (N'alg.usp_rebuild_lane_state', 'P'), + (N'alg.usp_refresh_dirty_lanes', 'P'), + (N'alg.usp_claim_shadow_instance', 'P'), + (N'alg.usp_refresh_shadow_snapshot', 'P'); + +IF (SELECT COUNT(*) FROM alg.shadow_package_manifest + WHERE package_id = 'FT546WMS_SHADOW_ONLY' + AND package_version = '2026-09-03-3') = 1 + AND (SELECT COUNT(*) FROM alg.shadow_package_manifest) = 1 +BEGIN + IF EXISTS ( + SELECT 1 + FROM sys.objects object + LEFT JOIN @expected_v3 expected + ON expected.object_name = N'alg.' + 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 51032, 'FT546 shadow upgrade refused: unknown object exists in alg schema', 1; + + IF EXISTS ( + SELECT 1 + FROM @expected_v3 expected + LEFT JOIN sys.objects object + ON object.schema_id = SCHEMA_ID(N'alg') + AND N'alg.' + object.name = expected.object_name + AND object.type = expected.object_type + WHERE object.object_id IS NULL + ) + THROW 51033, 'FT546 shadow upgrade refused: expected object is missing or has wrong type', 1; + + RETURN; +END; + +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 51034, 'FT546 shadow upgrade refused: shadow package manifest does not match', 1; + +IF EXISTS ( + SELECT 1 + FROM sys.objects object + LEFT JOIN @expected_v2 expected + ON expected.object_name = N'alg.' + 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 51035, 'FT546 shadow upgrade refused: unknown object exists in alg schema', 1; + +IF EXISTS ( + SELECT 1 + FROM @expected_v2 expected + LEFT JOIN sys.objects object + ON object.schema_id = SCHEMA_ID(N'alg') + AND N'alg.' + object.name = expected.object_name + AND object.type = expected.object_type + WHERE object.object_id IS NULL +) + THROW 51036, 'FT546 shadow upgrade refused: expected object is missing or has wrong type', 1; + +IF EXISTS ( + SELECT warehouse_id, space_id + FROM alg.space_topology + GROUP BY warehouse_id, space_id + HAVING COUNT(*) > 1 +) + THROW 51037, 'FT546 shadow upgrade refused: topology keys are not unique', 1; + +BEGIN TRY + BEGIN TRANSACTION; + + ALTER TABLE alg.space_topology + ADD snapshot_generation bigint NOT NULL + CONSTRAINT DF_alg_space_topology_snapshot_generation DEFAULT 0 WITH VALUES; + ALTER TABLE alg.inventory_item_snapshot + ADD snapshot_generation bigint NOT NULL + CONSTRAINT DF_alg_inventory_item_snapshot_generation DEFAULT 0 WITH VALUES; + ALTER TABLE alg.space_inventory_summary + ADD snapshot_generation bigint NOT NULL + CONSTRAINT DF_alg_space_inventory_summary_generation DEFAULT 0 WITH VALUES; + ALTER TABLE alg.lane_state + ADD snapshot_generation bigint NOT NULL + CONSTRAINT DF_alg_lane_state_snapshot_generation DEFAULT 0 WITH VALUES; + + ALTER TABLE alg.space_topology DROP CONSTRAINT PK_alg_space_topology; + ALTER TABLE alg.space_topology + ADD CONSTRAINT PK_alg_space_topology PRIMARY KEY (warehouse_id, space_id); + + CREATE TABLE alg.shadow_snapshot_state ( + warehouse_id varchar(80) NOT NULL PRIMARY KEY, + snapshot_generation bigint NOT NULL, + snapshot_status varchar(20) NOT NULL, + synchronized_at datetime2 NULL, + source_signature varbinary(32) NULL, + source_row_count bigint NULL, + instance_id varchar(80) NULL, + last_error varchar(1000) NULL, + CONSTRAINT CK_alg_shadow_snapshot_state_status CHECK (snapshot_status IN ('INITIALIZING', 'REFRESHING', 'PUBLISHED', 'FAILED')) + ); + + CREATE TABLE alg.cargo_space_snapshot ( + warehouse_id varchar(80) NOT NULL, + space_id varchar(80) NOT NULL, + lane_code varchar(80) NULL, + cargo_area_id varchar(80) NULL, + roadway_id varchar(80) NULL, + display_name varchar(255) NULL, + platoon int NULL, + column_no int NULL, + floor_no int NULL, + depth_raw varchar(80) NULL, + space_status varchar(20) NULL, + locked varchar(20) NULL, + tray_code varchar(255) NULL, + snapshot_generation bigint NOT NULL, + synchronized_at datetime2 NOT NULL, + CONSTRAINT PK_alg_cargo_space_snapshot PRIMARY KEY (warehouse_id, space_id) + ); + + CREATE TABLE alg.active_job_snapshot ( + warehouse_id varchar(80) NOT NULL, + job_id varchar(80) NOT NULL, + task_no varchar(80) NULL, + tray_code varchar(255) NULL, + source_space_id varchar(80) NULL, + target_space_id varchar(80) NULL, + status varchar(20) NULL, + in_or_out varchar(20) NULL, + create_time_raw varchar(80) NULL, + snapshot_generation bigint NOT NULL, + synchronized_at datetime2 NOT NULL, + CONSTRAINT PK_alg_active_job_snapshot PRIMARY KEY (warehouse_id, job_id) + ); + + CREATE TABLE alg.shadow_inbound_request_snapshot ( + warehouse_id varchar(80) NOT NULL, + task_no varchar(80) NOT NULL, + tray_no varchar(255) NOT NULL, + request_id varchar(80) NULL, + raw_warehouse_id varchar(80) NULL, + project_no varchar(255) NULL, + product_id varchar(255) NULL, + lot_no varchar(80) NULL, + snapshot_generation bigint NOT NULL, + synchronized_at datetime2 NOT NULL, + CONSTRAINT PK_alg_shadow_inbound_request_snapshot PRIMARY KEY (warehouse_id, task_no, tray_no) + ); + + CREATE TABLE alg.shadow_outbound_request_snapshot ( + warehouse_id varchar(80) NOT NULL, + stockout_detail_id varchar(80) NOT NULL, + raw_warehouse_id varchar(80) NULL, + project_no varchar(255) NULL, + product_id varchar(255) NULL, + requested_quantity decimal(38,6) NULL, + snapshot_generation bigint NOT NULL, + synchronized_at datetime2 NOT NULL, + CONSTRAINT PK_alg_shadow_outbound_request_snapshot PRIMARY KEY (warehouse_id, stockout_detail_id) + ); + + CREATE TABLE alg.shadow_runtime_metric ( + warehouse_id varchar(80) NOT NULL, + metric_name varchar(80) NOT NULL, + bucket_upper_micros bigint NOT NULL, + metric_count bigint NOT NULL CONSTRAINT DF_alg_shadow_runtime_metric_count DEFAULT 0, + metric_total_micros bigint NOT NULL CONSTRAINT DF_alg_shadow_runtime_metric_total DEFAULT 0, + metric_max_micros bigint NOT NULL CONSTRAINT DF_alg_shadow_runtime_metric_max DEFAULT 0, + flushed_at datetime2 NULL, + CONSTRAINT PK_alg_shadow_runtime_metric PRIMARY KEY (warehouse_id, metric_name, bucket_upper_micros) + ); + + CREATE TABLE alg.shadow_instance_heartbeat ( + warehouse_id varchar(80) NOT NULL PRIMARY KEY, + instance_id varchar(80) NOT NULL, + claimed_at datetime2 NOT NULL, + last_seen_at datetime2 NOT NULL + ); +END TRY +BEGIN CATCH + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + THROW; +END CATCH; +GO + +CREATE PROCEDURE alg.usp_refresh_shadow_snapshot + @warehouse_id varchar(80), + @snapshot_generation bigint, + @instance_id varchar(80) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '') IS NULL + THROW 51006, 'warehouse_id is required', 1; + IF NULLIF(LTRIM(RTRIM(@instance_id)), '') IS NULL + THROW 51007, 'instance_id is required', 1; + IF @snapshot_generation <= 0 + THROW 51008, 'snapshot_generation must be positive', 1; + + DECLARE @pre_source_count bigint, @post_source_count bigint, + @pre_source_signature varbinary(32), @post_source_signature varbinary(32), + @source_payload nvarchar(max), @synchronized_at datetime2, + @current_generation bigint; + + BEGIN TRY + BEGIN TRANSACTION; + + SELECT @current_generation = state.snapshot_generation + FROM alg.shadow_snapshot_state state WITH (UPDLOCK, HOLDLOCK) + WHERE state.warehouse_id = @warehouse_id; + IF @current_generation IS NOT NULL AND @snapshot_generation <= @current_generation + THROW 51009, 'snapshot_generation must advance the published generation', 1; + + SELECT @pre_source_count = + (SELECT COUNT_BIG(*) FROM dbo.bas_cargo_space cargo WITH (NOLOCK) + WHERE cargo.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.bas_stock stock WITH (NOLOCK) + JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) + ON cargo.cargo_space_id = stock.cargo_space_id + WHERE cargo.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.mydo_job job WITH (NOLOCK) + WHERE job.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.mydo_job_detail detail WITH (NOLOCK) + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK) + JOIN dbo.mydo_job_detail detail WITH (NOLOCK) + ON detail.stockin_detail_id = inbound.stockin_detail_id + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK) + WHERE outbound.warehouse_id = @warehouse_id); + + SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), ( + SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id], + cargo.group_coding_no AS [@lane_code], cargo.cargo_area_id AS [@cargo_area_id], + cargo.roadway_id AS [@roadway_id], cargo.cargo_space_name AS [@display_name], + cargo.the_platoon AS [@platoon], cargo.the_column AS [@column_no], + cargo.the_floor AS [@floor_no], cargo.group_coding AS [@depth_raw], + cargo.status AS [@space_status], cargo.locked AS [@locked], cargo.tray_code AS [@tray_code] + FROM dbo.bas_cargo_space cargo WITH (NOLOCK) + WHERE cargo.warehouse_id = @warehouse_id + ORDER BY cargo.cargo_space_id + FOR XML PATH('cargo'), ROOT('cargo_spaces'), TYPE + )), ''); + SET @source_payload = @source_payload + '|STOCK|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT stock.stock_id AS [@stock_id], stock.cargo_space_id AS [@space_id], + stock.warehouse_id AS [@warehouse_id], stock.tray_code AS [@tray_code], + stock.spare4 AS [@project_no], stock.product_id AS [@product_no], + stock.lot_no AS [@lot_no], stock.in_date AS [@in_date], + stock.quantity AS [@quantity], stock.out_quantity AS [@out_quantity] + FROM dbo.bas_stock stock WITH (NOLOCK) + JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) + ON cargo.cargo_space_id = stock.cargo_space_id + WHERE cargo.warehouse_id = @warehouse_id + ORDER BY stock.stock_id + FOR XML PATH('stock'), ROOT('stocks'), TYPE + )), ''); + SET @source_payload = @source_payload + '|JOB|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT job.job_id AS [@job_id], job.status AS [@status], job.task_no AS [@task_no], + job.tray_code AS [@tray_code], job.cargo_space_id AS [@source_space_id], + job.target_cargo_space_id AS [@target_space_id], job.in_or_out AS [@in_or_out], + job.create_time AS [@create_time], job.finish_time AS [@finish_time], + job.warehouse_id AS [@warehouse_id] + FROM dbo.mydo_job job WITH (NOLOCK) + WHERE job.warehouse_id = @warehouse_id + ORDER BY job.job_id + FOR XML PATH('job'), ROOT('jobs'), TYPE + )), ''); + SET @source_payload = @source_payload + '|JOB_DETAIL|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT detail.job_detail_id AS [@job_detail_id], detail.job_id AS [@job_id], + detail.stock_id AS [@stock_id], detail.lot_no AS [@lot_no], + detail.in_date AS [@in_date], detail.in_or_out_id AS [@request_id], + detail.in_or_out_d_id AS [@request_detail_id], detail.product_id AS [@product_id], + detail.stock_status AS [@stock_status], detail.in_quantity AS [@in_quantity], + detail.stock_quantity AS [@stock_quantity], detail.out_quantity AS [@out_quantity], + detail.pick_quantity AS [@pick_quantity], detail.stockin_detail_id AS [@stockin_detail_id] + FROM dbo.mydo_job_detail detail WITH (NOLOCK) + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id + ORDER BY detail.job_detail_id + FOR XML PATH('job_detail'), ROOT('job_details'), TYPE + )), ''); + SET @source_payload = @source_payload + '|INBOUND|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT inbound.stockin_detail_id AS [@stockin_detail_id], inbound.stockin_guid AS [@stockin_guid], + inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no], + inbound.product_id AS [@product_id], inbound.quantity AS [@quantity], + inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no], + job.warehouse_id AS [@warehouse_id], detail.job_id AS [@job_id] + FROM dbo.in_stockin_detail inbound WITH (NOLOCK) + JOIN dbo.mydo_job_detail detail WITH (NOLOCK) + ON detail.stockin_detail_id = inbound.stockin_detail_id + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id + ORDER BY inbound.stockin_detail_id + FOR XML PATH('inbound'), ROOT('inbound_requests'), TYPE + )), ''); + SET @source_payload = @source_payload + '|OUTBOUND|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id], + outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no], + outbound.quantity AS [@quantity], outbound.assign_num AS [@assign_num], + outbound.allot_quantity AS [@allot_quantity], outbound.create_time AS [@create_time] + FROM dbo.out_stockout_detail outbound WITH (NOLOCK) + WHERE outbound.warehouse_id = @warehouse_id + ORDER BY outbound.stockout_detail_id + FOR XML PATH('outbound'), ROOT('outbound_requests'), TYPE + )), ''); + SET @pre_source_signature = HASHBYTES('SHA2_256', CONVERT(varbinary(max), @source_payload)); + + DELETE FROM alg.cargo_space_snapshot WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.inventory_item_snapshot WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.active_job_snapshot WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.shadow_inbound_request_snapshot WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.shadow_outbound_request_snapshot WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.space_topology WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.space_inventory_summary WHERE warehouse_id = @warehouse_id; + DELETE FROM alg.lane_state WHERE warehouse_id = @warehouse_id; + + SET @synchronized_at = SYSUTCDATETIME(); + INSERT INTO alg.cargo_space_snapshot ( + warehouse_id, space_id, lane_code, cargo_area_id, roadway_id, display_name, + platoon, column_no, floor_no, depth_raw, space_status, locked, tray_code, + snapshot_generation, synchronized_at) + SELECT cargo.warehouse_id, cargo.cargo_space_id, cargo.group_coding_no, + cargo.cargo_area_id, cargo.roadway_id, cargo.cargo_space_name, + cargo.the_platoon, cargo.the_column, cargo.the_floor, cargo.group_coding, + cargo.status, cargo.locked, cargo.tray_code, + @snapshot_generation, @synchronized_at + FROM dbo.bas_cargo_space cargo WITH (NOLOCK) + WHERE cargo.warehouse_id = @warehouse_id; + + INSERT INTO alg.inventory_item_snapshot ( + warehouse_id, stock_id, space_id, tray_code, project_no, product_no, lot_no, + inbound_time_raw, inbound_at, quantity, out_quantity, snapshot_generation, synchronized_at) + SELECT cargo.warehouse_id, stock.stock_id, stock.cargo_space_id, stock.tray_code, + stock.spare4, stock.product_id, stock.lot_no, stock.in_date, + TRY_CONVERT(datetime2, stock.in_date), stock.quantity, stock.out_quantity, + @snapshot_generation, @synchronized_at + FROM dbo.bas_stock stock WITH (NOLOCK) + JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) + ON cargo.cargo_space_id = stock.cargo_space_id + WHERE cargo.warehouse_id = @warehouse_id; + + INSERT INTO alg.active_job_snapshot ( + warehouse_id, job_id, task_no, tray_code, source_space_id, target_space_id, + status, in_or_out, create_time_raw, snapshot_generation, synchronized_at) + SELECT job.warehouse_id, job.job_id, job.task_no, job.tray_code, + NULLIF(LTRIM(RTRIM(job.cargo_space_id)), ''), + NULLIF(LTRIM(RTRIM(job.target_cargo_space_id)), ''), job.status, + job.in_or_out, job.create_time, @snapshot_generation, @synchronized_at + FROM dbo.mydo_job job WITH (NOLOCK) + WHERE job.warehouse_id = @warehouse_id + AND (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = ''); + + ;WITH inbound_source AS ( + SELECT job.warehouse_id, job.task_no, job.tray_code, + detail.in_or_out_id AS request_id, job.warehouse_id AS raw_warehouse_id, + inbound.spare4 AS project_no, inbound.product_id, inbound.lot_no, + ROW_NUMBER() OVER ( + PARTITION BY job.warehouse_id, job.task_no, job.tray_code + ORDER BY inbound.stockin_detail_id, detail.job_detail_id) AS source_rank + FROM dbo.in_stockin_detail inbound WITH (NOLOCK) + JOIN dbo.mydo_job_detail detail WITH (NOLOCK) + ON detail.stockin_detail_id = inbound.stockin_detail_id + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id + ) + INSERT INTO alg.shadow_inbound_request_snapshot ( + warehouse_id, task_no, tray_no, request_id, raw_warehouse_id, + project_no, product_id, lot_no, snapshot_generation, synchronized_at) + SELECT warehouse_id, ISNULL(task_no, ''), ISNULL(tray_code, ''), request_id, + raw_warehouse_id, project_no, product_id, lot_no, + @snapshot_generation, @synchronized_at + FROM inbound_source + WHERE source_rank = 1; + + INSERT INTO alg.shadow_outbound_request_snapshot ( + warehouse_id, stockout_detail_id, raw_warehouse_id, project_no, product_id, + requested_quantity, snapshot_generation, synchronized_at) + SELECT outbound.warehouse_id, outbound.stockout_detail_id, outbound.warehouse_id, + NULL, outbound.product_id, CONVERT(decimal(38,6), outbound.quantity), + @snapshot_generation, @synchronized_at + FROM dbo.out_stockout_detail outbound WITH (NOLOCK) + WHERE outbound.warehouse_id = @warehouse_id; + + EXEC alg.usp_rebuild_space_topology @warehouse_id = @warehouse_id, + @snapshot_generation = @snapshot_generation; + EXEC alg.usp_sync_inventory_snapshot @warehouse_id = @warehouse_id, + @snapshot_generation = @snapshot_generation; + EXEC alg.usp_rebuild_lane_state @warehouse_id = @warehouse_id, + @snapshot_generation = @snapshot_generation; + + SELECT @post_source_count = + (SELECT COUNT_BIG(*) FROM dbo.bas_cargo_space cargo WITH (NOLOCK) WHERE cargo.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.bas_stock stock WITH (NOLOCK) + JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id + WHERE cargo.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.mydo_job job WITH (NOLOCK) WHERE job.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.mydo_job_detail detail WITH (NOLOCK) + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK) + JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id) + + (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK) WHERE outbound.warehouse_id = @warehouse_id); + + SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), ( + SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id], + cargo.group_coding_no AS [@lane_code], cargo.cargo_area_id AS [@cargo_area_id], + cargo.roadway_id AS [@roadway_id], cargo.cargo_space_name AS [@display_name], + cargo.the_platoon AS [@platoon], cargo.the_column AS [@column_no], + cargo.the_floor AS [@floor_no], cargo.group_coding AS [@depth_raw], + cargo.status AS [@space_status], cargo.locked AS [@locked], cargo.tray_code AS [@tray_code] + FROM dbo.bas_cargo_space cargo WITH (NOLOCK) + WHERE cargo.warehouse_id = @warehouse_id + ORDER BY cargo.cargo_space_id + FOR XML PATH('cargo'), ROOT('cargo_spaces'), TYPE + )), ''); + SET @source_payload = @source_payload + '|STOCK|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT stock.stock_id AS [@stock_id], stock.cargo_space_id AS [@space_id], + stock.warehouse_id AS [@warehouse_id], stock.tray_code AS [@tray_code], + stock.spare4 AS [@project_no], stock.product_id AS [@product_no], + stock.lot_no AS [@lot_no], stock.in_date AS [@in_date], + stock.quantity AS [@quantity], stock.out_quantity AS [@out_quantity] + FROM dbo.bas_stock stock WITH (NOLOCK) + JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id + WHERE cargo.warehouse_id = @warehouse_id + ORDER BY stock.stock_id + FOR XML PATH('stock'), ROOT('stocks'), TYPE + )), ''); + SET @source_payload = @source_payload + '|JOB|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT job.job_id AS [@job_id], job.status AS [@status], job.task_no AS [@task_no], + job.tray_code AS [@tray_code], job.cargo_space_id AS [@source_space_id], + job.target_cargo_space_id AS [@target_space_id], job.in_or_out AS [@in_or_out], + job.create_time AS [@create_time], job.finish_time AS [@finish_time], + job.warehouse_id AS [@warehouse_id] + FROM dbo.mydo_job job WITH (NOLOCK) + WHERE job.warehouse_id = @warehouse_id + ORDER BY job.job_id + FOR XML PATH('job'), ROOT('jobs'), TYPE + )), ''); + SET @source_payload = @source_payload + '|JOB_DETAIL|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT detail.job_detail_id AS [@job_detail_id], detail.job_id AS [@job_id], + detail.stock_id AS [@stock_id], detail.lot_no AS [@lot_no], + detail.in_date AS [@in_date], detail.in_or_out_id AS [@request_id], + detail.in_or_out_d_id AS [@request_detail_id], detail.product_id AS [@product_id], + detail.stock_status AS [@stock_status], detail.in_quantity AS [@in_quantity], + detail.stock_quantity AS [@stock_quantity], detail.out_quantity AS [@out_quantity], + detail.pick_quantity AS [@pick_quantity], detail.stockin_detail_id AS [@stockin_detail_id] + FROM dbo.mydo_job_detail detail WITH (NOLOCK) + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id + ORDER BY detail.job_detail_id + FOR XML PATH('job_detail'), ROOT('job_details'), TYPE + )), ''); + SET @source_payload = @source_payload + '|INBOUND|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT inbound.stockin_detail_id AS [@stockin_detail_id], inbound.stockin_guid AS [@stockin_guid], + inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no], inbound.product_id AS [@product_id], + inbound.quantity AS [@quantity], inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no], + job.warehouse_id AS [@warehouse_id], detail.job_id AS [@job_id] + FROM dbo.in_stockin_detail inbound WITH (NOLOCK) + JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id + JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id + WHERE job.warehouse_id = @warehouse_id + ORDER BY inbound.stockin_detail_id + FOR XML PATH('inbound'), ROOT('inbound_requests'), TYPE + )), ''); + SET @source_payload = @source_payload + '|OUTBOUND|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id], + outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no], outbound.quantity AS [@quantity], + outbound.assign_num AS [@assign_num], outbound.allot_quantity AS [@allot_quantity], outbound.create_time AS [@create_time] + FROM dbo.out_stockout_detail outbound WITH (NOLOCK) + WHERE outbound.warehouse_id = @warehouse_id + ORDER BY outbound.stockout_detail_id + FOR XML PATH('outbound'), ROOT('outbound_requests'), TYPE + )), ''); + SET @post_source_signature = HASHBYTES('SHA2_256', CONVERT(varbinary(max), @source_payload)); + + IF @pre_source_count <> @post_source_count + OR @pre_source_signature <> @post_source_signature + THROW 51025, 'FT546 shadow refresh refused: source changed during refresh', 1; + + MERGE alg.shadow_snapshot_state WITH (HOLDLOCK) AS target + USING (SELECT @warehouse_id AS warehouse_id) AS source + ON source.warehouse_id = target.warehouse_id + WHEN MATCHED THEN UPDATE SET + snapshot_generation = @snapshot_generation, + snapshot_status = 'PUBLISHED', + synchronized_at = SYSUTCDATETIME(), + source_signature = @post_source_signature, + source_row_count = @post_source_count, + instance_id = @instance_id, + last_error = NULL + WHEN NOT MATCHED THEN INSERT ( + warehouse_id, snapshot_generation, snapshot_status, synchronized_at, + source_signature, source_row_count, instance_id, last_error) + VALUES ( + @warehouse_id, @snapshot_generation, 'PUBLISHED', SYSUTCDATETIME(), + @post_source_signature, @post_source_count, @instance_id, NULL); + + COMMIT TRANSACTION; + END TRY + BEGIN CATCH + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + THROW; + END CATCH; +END; +GO + +CREATE PROCEDURE alg.usp_claim_shadow_instance + @warehouse_id varchar(80), + @instance_id varchar(80), + @claimed bit OUTPUT +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + SET @claimed = 0; + + IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '') IS NULL + THROW 51004, 'warehouse_id is required', 1; + IF NULLIF(LTRIM(RTRIM(@instance_id)), '') IS NULL + THROW 51005, 'instance_id is required', 1; + + DECLARE @now datetime2 = SYSUTCDATETIME(), + @current_instance varchar(80), + @last_seen_at datetime2; + BEGIN TRY + BEGIN TRANSACTION; + SELECT @current_instance = heartbeat.instance_id, + @last_seen_at = heartbeat.last_seen_at + FROM alg.shadow_instance_heartbeat heartbeat WITH (UPDLOCK, HOLDLOCK) + WHERE heartbeat.warehouse_id = @warehouse_id; + + IF @current_instance IS NULL + BEGIN + INSERT INTO alg.shadow_instance_heartbeat (warehouse_id, instance_id, claimed_at, last_seen_at) + VALUES (@warehouse_id, @instance_id, @now, @now); + SET @claimed = 1; + END + ELSE IF @current_instance = @instance_id + BEGIN + UPDATE alg.shadow_instance_heartbeat + SET last_seen_at = @now + WHERE warehouse_id = @warehouse_id; + SET @claimed = 1; + END + ELSE IF @last_seen_at < DATEADD(second, -120, @now) + BEGIN + UPDATE alg.shadow_instance_heartbeat + SET instance_id = @instance_id, + claimed_at = @now, + last_seen_at = @now + WHERE warehouse_id = @warehouse_id; + SET @claimed = 1; + END; + + COMMIT TRANSACTION; + END TRY + BEGIN CATCH + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + THROW; + END CATCH; + SELECT @claimed AS claimed; +END; +GO + +ALTER PROCEDURE alg.usp_rebuild_lane_state + @warehouse_id varchar(80) = NULL, + @lane_code varchar(80) = NULL, + @snapshot_generation bigint = NULL +AS +BEGIN + SET NOCOUNT ON; + IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '') IS NULL + THROW 51019, 'warehouse_id is required', 1; + + DECLARE @generation bigint; + SET @generation = @snapshot_generation; + IF @generation IS NULL + SELECT @generation = snapshot_generation + FROM alg.shadow_snapshot_state + WHERE warehouse_id = @warehouse_id + AND snapshot_status = 'PUBLISHED'; + IF @generation IS NULL + THROW 51024, 'FT546 shadow lane rebuild requires a snapshot generation', 1; + + ;WITH outbound_lanes AS ( + SELECT DISTINCT job.warehouse_id, cargo.lane_code + FROM alg.active_job_snapshot job + JOIN alg.cargo_space_snapshot cargo + ON cargo.warehouse_id = job.warehouse_id + AND cargo.space_id = job.source_space_id + AND cargo.snapshot_generation = @generation + WHERE job.warehouse_id = @warehouse_id + AND job.snapshot_generation = @generation + AND TRY_CONVERT(int, job.in_or_out) >= 20 + ), lane_gaps AS ( + SELECT DISTINCT inner_space.warehouse_id, inner_space.lane_code + FROM alg.cargo_space_snapshot inner_space + JOIN alg.cargo_space_snapshot outer_space + ON outer_space.warehouse_id = inner_space.warehouse_id + AND outer_space.lane_code = inner_space.lane_code + AND outer_space.snapshot_generation = @generation + AND TRY_CONVERT(int, outer_space.depth_raw) > TRY_CONVERT(int, inner_space.depth_raw) + LEFT JOIN alg.space_inventory_summary inner_stock + ON inner_stock.warehouse_id = inner_space.warehouse_id + AND inner_stock.space_id = inner_space.space_id + AND inner_stock.snapshot_generation = @generation + LEFT JOIN alg.space_inventory_summary outer_stock + ON outer_stock.warehouse_id = outer_space.warehouse_id + AND outer_stock.space_id = outer_space.space_id + AND outer_stock.snapshot_generation = @generation + WHERE inner_space.warehouse_id = @warehouse_id + AND inner_space.snapshot_generation = @generation + AND NULLIF(LTRIM(RTRIM(inner_space.tray_code)), '') IS NULL + AND inner_stock.space_id IS NULL + AND (NULLIF(LTRIM(RTRIM(outer_space.tray_code)), '') IS NOT NULL + OR outer_stock.space_id IS NOT NULL) + ) + SELECT cargo.warehouse_id, cargo.lane_code, COUNT(*) AS space_count, + SUM(CASE WHEN cargo.space_status = '2' OR ISNULL(stock.stock_detail_count, 0) > 0 THEN 1 ELSE 0 END) AS occupied_count, + SUM(CASE WHEN cargo.space_status = '1' + AND NULLIF(LTRIM(RTRIM(cargo.locked)), '') IS NULL + AND NULLIF(LTRIM(RTRIM(cargo.tray_code)), '') IS NULL + AND ISNULL(stock.stock_detail_count, 0) = 0 + AND ISNULL(role.role, 'STORAGE') = 'STORAGE' + AND outbound_lanes.lane_code IS NULL + AND reservation.reservation_id IS NULL + AND lane_gaps.lane_code IS NULL THEN 1 ELSE 0 END) AS available_count, + CONVERT(bit, CASE WHEN outbound_lanes.lane_code IS NULL THEN 0 ELSE 1 END) AS pending_outbound, + CONVERT(bit, CASE WHEN lane_gaps.lane_code IS NULL THEN 0 ELSE 1 END) AS has_gap, + CAST(NULL AS varbinary(32)) AS semantic_fingerprint + INTO #source_state + FROM alg.cargo_space_snapshot cargo + LEFT JOIN alg.space_inventory_summary stock + ON stock.warehouse_id = cargo.warehouse_id + AND stock.space_id = cargo.space_id + AND stock.snapshot_generation = @generation + LEFT JOIN alg.space_role role ON role.space_id = cargo.space_id + LEFT JOIN outbound_lanes + ON outbound_lanes.warehouse_id = cargo.warehouse_id + AND outbound_lanes.lane_code = cargo.lane_code + LEFT JOIN alg.reservation reservation + ON reservation.space_id = cargo.space_id + AND reservation.status = 'ACTIVE' + LEFT JOIN lane_gaps + ON lane_gaps.warehouse_id = cargo.warehouse_id + AND lane_gaps.lane_code = cargo.lane_code + WHERE cargo.warehouse_id = @warehouse_id + AND cargo.snapshot_generation = @generation + AND (@lane_code IS NULL OR cargo.lane_code = @lane_code) + GROUP BY cargo.warehouse_id, cargo.lane_code, + outbound_lanes.lane_code, lane_gaps.lane_code; + + DECLARE @fingerprint_lane varchar(80), @fingerprint varbinary(32); + DECLARE fingerprint_cursor CURSOR LOCAL FAST_FORWARD FOR + SELECT lane_code FROM #source_state ORDER BY lane_code; + OPEN fingerprint_cursor; + FETCH NEXT FROM fingerprint_cursor INTO @fingerprint_lane; + WHILE @@FETCH_STATUS = 0 + BEGIN + EXEC alg.usp_compute_lane_semantic_fingerprint + @warehouse_id = @warehouse_id, + @lane_code = @fingerprint_lane, + @semantic_fingerprint = @fingerprint OUTPUT, + @inventory_source = 'SNAPSHOT', + @snapshot_generation = @generation; + UPDATE #source_state + SET semantic_fingerprint = @fingerprint + WHERE lane_code = @fingerprint_lane; + FETCH NEXT FROM fingerprint_cursor INTO @fingerprint_lane; + END; + CLOSE fingerprint_cursor; + DEALLOCATE fingerprint_cursor; + + MERGE alg.lane_state WITH (HOLDLOCK) AS target + USING #source_state AS source + ON target.warehouse_id = @warehouse_id + AND target.lane_code = source.lane_code + WHEN MATCHED AND (target.space_count <> source.space_count + OR target.occupied_count <> source.occupied_count + OR target.available_count <> source.available_count + OR target.pending_outbound <> source.pending_outbound + OR target.has_gap <> source.has_gap + OR target.snapshot_generation <> @generation + OR target.semantic_fingerprint IS NULL + OR target.semantic_fingerprint <> source.semantic_fingerprint) + THEN UPDATE SET space_count = source.space_count, + occupied_count = source.occupied_count, + available_count = source.available_count, + pending_outbound = source.pending_outbound, + has_gap = source.has_gap, + semantic_fingerprint = source.semantic_fingerprint, + snapshot_generation = @generation, + rebuilt_at = SYSUTCDATETIME() + WHEN NOT MATCHED BY TARGET THEN + INSERT (warehouse_id, lane_code, space_count, occupied_count, available_count, + pending_outbound, has_gap, semantic_fingerprint, snapshot_generation) + VALUES (source.warehouse_id, source.lane_code, source.space_count, + source.occupied_count, source.available_count, source.pending_outbound, + source.has_gap, source.semantic_fingerprint, @generation) + WHEN NOT MATCHED BY SOURCE + AND target.warehouse_id = @warehouse_id + AND (@lane_code IS NULL OR target.lane_code = @lane_code) + THEN DELETE; +END; +GO + +ALTER PROCEDURE alg.usp_refresh_dirty_lanes + @warehouse_id varchar(80) +AS +BEGIN + SET NOCOUNT ON; + IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '') IS NULL + THROW 51002, 'warehouse_id is required', 1; + + DECLARE @generation bigint; + SELECT @generation = snapshot_generation + FROM alg.shadow_snapshot_state + WHERE warehouse_id = @warehouse_id + AND snapshot_status = 'PUBLISHED'; + IF @generation IS NULL + THROW 51003, 'published snapshot is required', 1; + + EXEC alg.usp_rebuild_space_topology @warehouse_id = @warehouse_id, + @snapshot_generation = @generation; + EXEC alg.usp_sync_inventory_snapshot @warehouse_id = @warehouse_id, + @snapshot_generation = @generation; + EXEC alg.usp_rebuild_lane_state @warehouse_id = @warehouse_id, + @snapshot_generation = @generation; +END; +GO + +ALTER PROCEDURE alg.usp_rebuild_space_topology + @warehouse_id varchar(80) = NULL, + @snapshot_generation bigint = NULL +AS +BEGIN + SET NOCOUNT ON; + IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '') IS NULL + THROW 51019, 'warehouse_id is required', 1; + + DECLARE @generation bigint; + SET @generation = @snapshot_generation; + IF @generation IS NULL + SELECT @generation = snapshot_generation + FROM alg.shadow_snapshot_state + WHERE warehouse_id = @warehouse_id + AND snapshot_status = 'PUBLISHED'; + IF @generation IS NULL + THROW 51020, 'FT546 shadow topology rebuild requires a snapshot generation', 1; + + ;WITH lane_topology AS ( + SELECT warehouse_id, lane_code, COUNT(*) AS lane_depth, + MAX(column_no) AS column_no, MIN(platoon) AS row_no, + MIN(floor_no) AS floor_no, MIN(space_id) AS stable_id + FROM alg.cargo_space_snapshot + WHERE warehouse_id = @warehouse_id + AND snapshot_generation = @generation + AND NULLIF(LTRIM(RTRIM(lane_code)), '') IS NOT NULL + AND TRY_CONVERT(int, depth_raw) IS NOT NULL + GROUP BY warehouse_id, lane_code + ), ranked_lanes AS ( + SELECT lane_topology.*, + CASE WHEN lane_depth = 1 THEN ROW_NUMBER() OVER ( + ORDER BY column_no DESC, row_no ASC, floor_no ASC, stable_id ASC + ) END AS single_depth_rank + FROM lane_topology + ), source_rows AS ( + SELECT cargo.warehouse_id, cargo.space_id, cargo.lane_code, + ranked_lanes.lane_depth, + CASE WHEN ranked_lanes.lane_depth = 1 AND ranked_lanes.single_depth_rank <= 80 THEN 'CACHE' + WHEN ranked_lanes.lane_depth = 1 AND ranked_lanes.single_depth_rank <= 133 THEN 'RESERVE' + WHEN ranked_lanes.lane_depth = 5 THEN 'MAIN' + ELSE 'FLEX' END AS base_zone + FROM alg.cargo_space_snapshot cargo + JOIN ranked_lanes + ON ranked_lanes.warehouse_id = cargo.warehouse_id + AND ranked_lanes.lane_code = cargo.lane_code + WHERE cargo.warehouse_id = @warehouse_id + AND cargo.snapshot_generation = @generation + AND NULLIF(LTRIM(RTRIM(cargo.lane_code)), '') IS NOT NULL + AND TRY_CONVERT(int, cargo.depth_raw) IS NOT NULL + ) + MERGE alg.space_topology WITH (HOLDLOCK) AS target + USING source_rows AS source + ON target.warehouse_id = source.warehouse_id + AND target.space_id = source.space_id + WHEN MATCHED AND (target.lane_code <> source.lane_code + OR target.lane_depth <> source.lane_depth + OR target.base_zone <> source.base_zone + OR target.snapshot_generation <> @generation) + THEN UPDATE SET lane_code = source.lane_code, + lane_depth = source.lane_depth, + base_zone = source.base_zone, + snapshot_generation = @generation, + rebuilt_at = SYSUTCDATETIME() + WHEN NOT MATCHED BY TARGET THEN + INSERT (warehouse_id, space_id, lane_code, lane_depth, base_zone, snapshot_generation) + VALUES (source.warehouse_id, source.space_id, source.lane_code, + source.lane_depth, source.base_zone, @generation) + WHEN NOT MATCHED BY SOURCE AND target.warehouse_id = @warehouse_id + THEN DELETE; +END; +GO + +ALTER PROCEDURE alg.usp_sync_inventory_snapshot + @warehouse_id varchar(80) = NULL, + @snapshot_generation bigint = NULL +AS +BEGIN + SET NOCOUNT ON; + IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '') IS NULL + THROW 51019, 'warehouse_id is required', 1; + + DECLARE @generation bigint; + SET @generation = @snapshot_generation; + IF @generation IS NULL + SELECT @generation = snapshot_generation + FROM alg.shadow_snapshot_state + WHERE warehouse_id = @warehouse_id + AND snapshot_status = 'PUBLISHED'; + IF @generation IS NULL + THROW 51021, 'FT546 shadow inventory summary requires a snapshot generation', 1; + + DELETE FROM alg.space_inventory_summary + WHERE warehouse_id = @warehouse_id; + INSERT INTO alg.space_inventory_summary ( + warehouse_id, space_id, stock_detail_count, stock_tray_count, + stock_project_count, stock_product_count, snapshot_generation, synchronized_at) + SELECT warehouse_id, space_id, COUNT(*), + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(tray_code)), '')), + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(project_no)), '')), + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(product_no)), '')), + @generation, SYSUTCDATETIME() + FROM alg.inventory_item_snapshot + WHERE warehouse_id = @warehouse_id + AND snapshot_generation = @generation + GROUP BY warehouse_id, space_id; +END; +GO + +ALTER PROCEDURE alg.usp_compute_lane_semantic_fingerprint + @warehouse_id varchar(80), + @lane_code varchar(80), + @semantic_fingerprint varbinary(32) OUTPUT, + @inventory_source varchar(20) = 'SNAPSHOT', + @snapshot_generation bigint = NULL +AS +BEGIN + SET NOCOUNT ON; + IF @inventory_source <> 'SNAPSHOT' + THROW 51022, 'FT546 shadow fingerprint source must be SNAPSHOT', 1; + + DECLARE @generation bigint, @payload nvarchar(max); + SET @generation = @snapshot_generation; + IF @generation IS NULL + SELECT @generation = snapshot_generation + FROM alg.shadow_snapshot_state + WHERE warehouse_id = @warehouse_id + AND snapshot_status = 'PUBLISHED'; + IF @generation IS NULL + THROW 51023, 'FT546 shadow fingerprint requires a snapshot generation', 1; + + SELECT @payload = ISNULL(CONVERT(nvarchar(max), ( + SELECT cargo.space_id AS [@space_id], cargo.space_status AS [@status], + cargo.locked AS [@locked], cargo.tray_code AS [@tray_code], + cargo.depth_raw AS [@depth], cargo.platoon AS [@platoon], + cargo.column_no AS [@column], cargo.floor_no AS [@floor], + topology.lane_depth AS [@lane_depth], topology.base_zone AS [@base_zone], + ISNULL(role.role, 'STORAGE') AS [@role] + FROM alg.cargo_space_snapshot cargo + LEFT JOIN alg.space_topology topology + ON topology.warehouse_id = cargo.warehouse_id + AND topology.space_id = cargo.space_id + AND topology.snapshot_generation = @generation + LEFT JOIN alg.space_role role ON role.space_id = cargo.space_id + WHERE cargo.warehouse_id = @warehouse_id + AND cargo.lane_code = @lane_code + AND cargo.snapshot_generation = @generation + ORDER BY TRY_CONVERT(int, cargo.depth_raw), cargo.space_id + FOR XML PATH('space'), ROOT('spaces'), TYPE + )), ''); + SET @payload = @payload + '|INVENTORY|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT item.stock_id AS [@stock_id], item.space_id AS [@space_id], + item.tray_code AS [@tray_code], item.project_no AS [@project_no], + item.product_no AS [@product_no], item.lot_no AS [@lot_no], + item.inbound_time_raw AS [@inbound_time], item.quantity AS [@quantity], + item.out_quantity AS [@out_quantity] + FROM alg.inventory_item_snapshot item + WHERE item.warehouse_id = @warehouse_id + AND item.snapshot_generation = @generation + AND EXISTS (SELECT 1 FROM alg.cargo_space_snapshot cargo + WHERE cargo.warehouse_id = item.warehouse_id + AND cargo.space_id = item.space_id + AND cargo.lane_code = @lane_code + AND cargo.snapshot_generation = @generation) + ORDER BY item.space_id, item.stock_id + FOR XML PATH('item'), ROOT('inventory'), TYPE + )), ''); + SET @payload = @payload + '|ACTIVE_JOBS|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT job.job_id AS [@job_id], job.task_no AS [@task_no], job.tray_code AS [@tray_code], + job.source_space_id AS [@source_space_id], job.target_space_id AS [@target_space_id], + job.status AS [@status], job.in_or_out AS [@in_or_out], job.create_time_raw AS [@create_time] + FROM alg.active_job_snapshot job + JOIN alg.cargo_space_snapshot cargo + ON cargo.warehouse_id = job.warehouse_id + AND cargo.space_id = job.source_space_id + AND cargo.snapshot_generation = job.snapshot_generation + WHERE job.warehouse_id = @warehouse_id + AND job.snapshot_generation = @generation + AND cargo.lane_code = @lane_code + ORDER BY job.job_id + FOR XML PATH('job'), ROOT('jobs'), TYPE + )), ''); + SET @payload = @payload + '|PROJECT_POLICY|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT project_no, is_isolated, is_common, allow_mixed + FROM alg.project_policy ORDER BY project_no + FOR XML PATH('project'), ROOT('project_policy'), TYPE + )), ''); + SET @payload = @payload + '|WAREHOUSE_POLICY|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT warehouse_id, target_fill_rate, fifo_tolerance_hours + FROM alg.warehouse_policy WHERE warehouse_id = @warehouse_id + FOR XML PATH('warehouse'), ROOT('warehouse_policy'), TYPE + )), ''); + SET @payload = @payload + '|RESERVATIONS|' + ISNULL(CONVERT(nvarchar(max), ( + SELECT reservation_id, space_id, project_no, status + FROM alg.reservation + WHERE warehouse_id = @warehouse_id AND lane_code = @lane_code AND status = 'ACTIVE' + ORDER BY space_id, reservation_id + FOR XML PATH('reservation'), ROOT('reservations'), TYPE + )), ''); + SET @semantic_fingerprint = HASHBYTES('SHA2_256', CONVERT(varbinary(max), @payload)); +END; +GO + +ALTER VIEW alg.v_inventory_item +AS +SELECT item.warehouse_id, item.stock_id, item.space_id, item.tray_code, + item.project_no, item.product_no, item.lot_no, item.inbound_time_raw, + item.inbound_at, item.quantity, item.out_quantity, item.snapshot_generation +FROM alg.inventory_item_snapshot item +JOIN alg.shadow_snapshot_state published_snapshot + ON published_snapshot.warehouse_id = item.warehouse_id + AND published_snapshot.snapshot_status = 'PUBLISHED' + AND item.snapshot_generation = published_snapshot.snapshot_generation; +GO + +ALTER VIEW alg.v_active_job +AS +WITH published_jobs AS ( + SELECT job.warehouse_id, job.job_id, job.task_no, job.tray_code, + job.source_space_id, job.target_space_id, job.status, job.in_or_out, + job.create_time_raw, job.snapshot_generation + FROM alg.active_job_snapshot job + JOIN alg.shadow_snapshot_state published_snapshot + ON published_snapshot.warehouse_id = job.warehouse_id + AND published_snapshot.snapshot_status = 'PUBLISHED' + AND job.snapshot_generation = published_snapshot.snapshot_generation +), tray_candidates AS ( + SELECT job.warehouse_id, job.job_id, job.snapshot_generation, + COUNT(DISTINCT stock.space_id) AS space_count, + MIN(stock.space_id) AS cargo_space_id, + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(stock.project_no)), '')) AS project_count, + MIN(NULLIF(LTRIM(RTRIM(stock.project_no)), '')) AS project_no, + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(stock.product_no)), '')) AS product_count, + MIN(NULLIF(LTRIM(RTRIM(stock.product_no)), '')) AS product_no + FROM published_jobs job + JOIN alg.inventory_item_snapshot stock + ON stock.warehouse_id = job.warehouse_id + AND stock.snapshot_generation = job.snapshot_generation + AND stock.tray_code = NULLIF(LTRIM(RTRIM(job.tray_code)), '') + GROUP BY job.warehouse_id, job.job_id, job.snapshot_generation +), resolved_jobs AS ( + SELECT job.job_id, job.task_no, job.tray_code, job.target_space_id, + job.warehouse_id, job.status, job.in_or_out, job.create_time_raw, + job.snapshot_generation, + COALESCE(NULLIF(LTRIM(RTRIM(job.source_space_id)), ''), + CASE WHEN tray_candidates.space_count = 1 THEN tray_candidates.cargo_space_id END) AS source_space_id, + CASE WHEN NULLIF(LTRIM(RTRIM(job.source_space_id)), '') IS NOT NULL THEN 'EXPLICIT' + WHEN tray_candidates.space_count = 1 THEN 'TRAY' + WHEN tray_candidates.space_count > 1 THEN 'AMBIGUOUS_TRAY' + ELSE 'UNRESOLVED' END AS source_resolution + FROM published_jobs job + LEFT JOIN tray_candidates + ON tray_candidates.warehouse_id = job.warehouse_id + AND tray_candidates.job_id = job.job_id + AND tray_candidates.snapshot_generation = job.snapshot_generation +), source_scope AS ( + SELECT resolved.warehouse_id, resolved.job_id, resolved.snapshot_generation, + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(stock.project_no)), '')) AS project_count, + MIN(NULLIF(LTRIM(RTRIM(stock.project_no)), '')) AS project_no, + COUNT(DISTINCT NULLIF(LTRIM(RTRIM(stock.product_no)), '')) AS product_count, + MIN(NULLIF(LTRIM(RTRIM(stock.product_no)), '')) AS product_no + FROM resolved_jobs resolved + JOIN alg.inventory_item_snapshot stock + ON stock.warehouse_id = resolved.warehouse_id + AND stock.snapshot_generation = resolved.snapshot_generation + AND stock.space_id = resolved.source_space_id + GROUP BY resolved.warehouse_id, resolved.job_id, resolved.snapshot_generation +) +SELECT resolved.job_id, resolved.task_no, resolved.tray_code, + CASE WHEN resolved.source_space_id IS NOT NULL + THEN CASE WHEN source_scope.project_count = 1 THEN source_scope.project_no END + ELSE CASE WHEN tray_candidates.project_count = 1 THEN tray_candidates.project_no END END AS project_no, + CASE WHEN resolved.source_space_id IS NOT NULL + THEN CASE WHEN source_scope.product_count = 1 THEN source_scope.product_no END + ELSE CASE WHEN tray_candidates.product_count = 1 THEN tray_candidates.product_no END END AS product_no, + resolved.source_space_id, resolved.target_space_id AS target_cargo_space_id, + resolved.warehouse_id, resolved.status, resolved.in_or_out, + resolved.create_time_raw, source_cargo.lane_code AS source_lane_code, + resolved.source_resolution, resolved.snapshot_generation +FROM resolved_jobs resolved +LEFT JOIN tray_candidates + ON tray_candidates.warehouse_id = resolved.warehouse_id + AND tray_candidates.job_id = resolved.job_id + AND tray_candidates.snapshot_generation = resolved.snapshot_generation +LEFT JOIN source_scope + ON source_scope.warehouse_id = resolved.warehouse_id + AND source_scope.job_id = resolved.job_id + AND source_scope.snapshot_generation = resolved.snapshot_generation +LEFT JOIN alg.cargo_space_snapshot source_cargo + ON source_cargo.warehouse_id = resolved.warehouse_id + AND source_cargo.space_id = resolved.source_space_id + AND source_cargo.snapshot_generation = resolved.snapshot_generation; +GO + +ALTER VIEW alg.v_space_state +AS +WITH published_snapshot AS ( + SELECT state.warehouse_id, state.snapshot_generation + FROM alg.shadow_snapshot_state state + WHERE state.snapshot_status = 'PUBLISHED' +), outbound_lanes AS ( + SELECT DISTINCT job.warehouse_id, job.source_lane_code AS lane_code, + job.snapshot_generation + FROM alg.v_active_job job + WHERE TRY_CONVERT(int, job.in_or_out) >= 20 + AND job.source_lane_code IS NOT NULL +), lane_gaps AS ( + SELECT DISTINCT inner_space.warehouse_id, inner_space.lane_code, + inner_space.snapshot_generation + FROM alg.cargo_space_snapshot inner_space + JOIN alg.cargo_space_snapshot outer_space + ON outer_space.warehouse_id = inner_space.warehouse_id + AND outer_space.lane_code = inner_space.lane_code + AND outer_space.snapshot_generation = inner_space.snapshot_generation + AND TRY_CONVERT(int, outer_space.depth_raw) > TRY_CONVERT(int, inner_space.depth_raw) + JOIN published_snapshot published + ON published.warehouse_id = inner_space.warehouse_id + AND published.snapshot_generation = inner_space.snapshot_generation + LEFT JOIN alg.space_inventory_summary inner_stock + ON inner_stock.warehouse_id = inner_space.warehouse_id + AND inner_stock.space_id = inner_space.space_id + AND inner_stock.snapshot_generation = inner_space.snapshot_generation + LEFT JOIN alg.space_inventory_summary outer_stock + ON outer_stock.warehouse_id = outer_space.warehouse_id + AND outer_stock.space_id = outer_space.space_id + AND outer_stock.snapshot_generation = outer_space.snapshot_generation + WHERE TRY_CONVERT(int, inner_space.depth_raw) IS NOT NULL + AND TRY_CONVERT(int, outer_space.depth_raw) IS NOT NULL + AND NULLIF(LTRIM(RTRIM(inner_space.tray_code)), '') IS NULL + AND inner_stock.space_id IS NULL + AND (NULLIF(LTRIM(RTRIM(outer_space.tray_code)), '') IS NOT NULL + OR outer_stock.space_id IS NOT NULL) +) +SELECT cargo.space_id, cargo.warehouse_id, cargo.lane_code, + cargo.cargo_area_id AS area_id, + CASE WHEN ISNULL(role.role, 'STORAGE') IN ('RELOCATION_BUFFER', 'OUTBOUND_BUFFER') THEN 'CACHE' + WHEN ISNULL(role.role, 'STORAGE') = 'PROTECTED_CORE' THEN 'RESERVE' + ELSE topology.base_zone END AS zone_code, + cargo.roadway_id, cargo.display_name, cargo.platoon, cargo.column_no, + cargo.floor_no, cargo.depth_raw, TRY_CONVERT(int, cargo.depth_raw) AS depth, + cargo.space_status, cargo.locked, cargo.tray_code AS space_tray_code, + ISNULL(role.role, 'STORAGE') AS role, + ISNULL(stock.stock_detail_count, 0) AS stock_detail_count, + ISNULL(stock.stock_tray_count, 0) AS stock_tray_count, + ISNULL(stock.stock_project_count, 0) AS stock_project_count, + ISNULL(stock.stock_product_count, 0) AS stock_product_count, + CONVERT(bit, CASE WHEN ISNULL(stock.stock_tray_count, 0) > 1 THEN 1 ELSE 0 END) AS tray_conflict, + CONVERT(bit, CASE WHEN ISNULL(stock.stock_project_count, 0) > 1 THEN 1 ELSE 0 END) AS project_conflict, + CONVERT(bit, 0) AS product_conflict, + CONVERT(bit, CASE WHEN outbound_lanes.lane_code IS NULL THEN 0 ELSE 1 END) AS pending_outbound, + active_reservations.reservation_id, + CONVERT(bit, CASE WHEN active_reservations.reservation_id IS NULL THEN 0 ELSE 1 END) AS reserved, + CONVERT(bit, CASE WHEN lane_gaps.lane_code IS NULL THEN 0 ELSE 1 END) AS has_gap, + CONVERT(bit, CASE + WHEN cargo.space_status IS NULL OR cargo.space_status <> '1' THEN 0 + WHEN NULLIF(LTRIM(RTRIM(cargo.locked)), '') IS NOT NULL + AND LTRIM(RTRIM(cargo.locked)) NOT IN ('0', 'N') THEN 0 + WHEN NULLIF(LTRIM(RTRIM(cargo.tray_code)), '') IS NOT NULL THEN 0 + WHEN ISNULL(stock.stock_detail_count, 0) <> 0 THEN 0 + WHEN TRY_CONVERT(int, cargo.depth_raw) IS NULL THEN 0 + WHEN ISNULL(role.role, 'STORAGE') <> 'STORAGE' THEN 0 + WHEN outbound_lanes.lane_code IS NOT NULL THEN 0 + WHEN active_reservations.reservation_id IS NOT NULL THEN 0 + WHEN EXISTS ( + SELECT 1 + FROM alg.cargo_space_snapshot inner_candidate + LEFT JOIN alg.space_inventory_summary inner_candidate_stock + ON inner_candidate_stock.warehouse_id = inner_candidate.warehouse_id + AND inner_candidate_stock.space_id = inner_candidate.space_id + AND inner_candidate_stock.snapshot_generation = inner_candidate.snapshot_generation + WHERE inner_candidate.warehouse_id = cargo.warehouse_id + AND inner_candidate.lane_code = cargo.lane_code + AND inner_candidate.snapshot_generation = cargo.snapshot_generation + AND TRY_CONVERT(int, inner_candidate.depth_raw) < TRY_CONVERT(int, cargo.depth_raw) + AND inner_candidate.space_status = '1' + AND NULLIF(LTRIM(RTRIM(inner_candidate.tray_code)), '') IS NULL + AND inner_candidate_stock.space_id IS NULL + ) THEN 0 + ELSE 1 END) AS inbound_available, + cargo.snapshot_generation +FROM alg.cargo_space_snapshot cargo +JOIN published_snapshot published + ON published.warehouse_id = cargo.warehouse_id + AND published.snapshot_generation = cargo.snapshot_generation +JOIN alg.space_topology topology + ON topology.warehouse_id = cargo.warehouse_id + AND topology.space_id = cargo.space_id + AND topology.snapshot_generation = cargo.snapshot_generation +LEFT JOIN alg.space_inventory_summary stock + ON stock.warehouse_id = cargo.warehouse_id + AND stock.space_id = cargo.space_id + AND stock.snapshot_generation = cargo.snapshot_generation +LEFT JOIN alg.space_role role ON role.space_id = cargo.space_id +LEFT JOIN outbound_lanes + ON outbound_lanes.warehouse_id = cargo.warehouse_id + AND outbound_lanes.lane_code = cargo.lane_code + AND outbound_lanes.snapshot_generation = cargo.snapshot_generation +LEFT JOIN alg.reservation active_reservations + ON active_reservations.space_id = cargo.space_id + AND active_reservations.status = 'ACTIVE' +LEFT JOIN lane_gaps + ON lane_gaps.warehouse_id = cargo.warehouse_id + AND lane_gaps.lane_code = cargo.lane_code + AND lane_gaps.snapshot_generation = cargo.snapshot_generation; +GO + +BEGIN TRY + DECLARE @expected_v3_post TABLE ( + object_name sysname NOT NULL PRIMARY KEY, + object_type char(2) NOT NULL + ); + INSERT INTO @expected_v3_post (object_name, object_type) VALUES + (N'alg.shadow_package_manifest', 'U'), + (N'alg.warehouse_policy', 'U'), + (N'alg.project_policy', 'U'), + (N'alg.space_role', 'U'), + (N'alg.space_topology', 'U'), + (N'alg.inventory_item_snapshot', 'U'), + (N'alg.space_inventory_summary', 'U'), + (N'alg.lane_state', 'U'), + (N'alg.reservation', 'U'), + (N'alg.deep_workflow_lock', 'U'), + (N'alg.decision_log', 'U'), + (N'alg.shadow_snapshot_state', 'U'), + (N'alg.cargo_space_snapshot', 'U'), + (N'alg.active_job_snapshot', 'U'), + (N'alg.shadow_inbound_request_snapshot', 'U'), + (N'alg.shadow_outbound_request_snapshot', 'U'), + (N'alg.shadow_runtime_metric', 'U'), + (N'alg.shadow_instance_heartbeat', 'U'), + (N'alg.v_inventory_item', 'V'), + (N'alg.v_active_job', 'V'), + (N'alg.v_space_state', 'V'), + (N'alg.usp_rebuild_space_topology', 'P'), + (N'alg.usp_sync_inventory_snapshot', 'P'), + (N'alg.usp_compute_lane_semantic_fingerprint', 'P'), + (N'alg.usp_rebuild_lane_state', 'P'), + (N'alg.usp_refresh_dirty_lanes', 'P'), + (N'alg.usp_claim_shadow_instance', 'P'), + (N'alg.usp_refresh_shadow_snapshot', 'P'); + + IF EXISTS ( + SELECT 1 + FROM sys.objects object + LEFT JOIN @expected_v3_post expected + ON expected.object_name = N'alg.' + 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 51032, 'FT546 shadow upgrade refused: unknown object exists in alg schema', 1; + + IF EXISTS ( + SELECT 1 + FROM @expected_v3_post expected + LEFT JOIN sys.objects object + ON object.schema_id = SCHEMA_ID(N'alg') + AND N'alg.' + object.name = expected.object_name + AND object.type = expected.object_type + WHERE object.object_id IS NULL + ) + THROW 51033, 'FT546 shadow upgrade refused: expected object is missing or has wrong type', 1; + + UPDATE alg.shadow_package_manifest + SET package_version = '2026-09-03-3' + WHERE package_id = 'FT546WMS_SHADOW_ONLY'; + + COMMIT TRANSACTION; +END TRY +BEGIN CATCH + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + THROW; +END CATCH; +GO diff --git a/fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowSqlServerMigrationScriptTest.java b/fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowSqlServerMigrationScriptTest.java index 8fa640f..7cea744 100644 --- a/fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowSqlServerMigrationScriptTest.java +++ b/fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowSqlServerMigrationScriptTest.java @@ -14,6 +14,36 @@ import static org.assertj.core.api.Assertions.assertThat; class ShadowSqlServerMigrationScriptTest { private static final Set 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 V2_OBJECTS = Set.of( "table alg.shadow_package_manifest", "table alg.warehouse_policy", "table alg.project_policy", @@ -71,6 +101,11 @@ class ShadowSqlServerMigrationScriptTest { Set 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 objects(String sql, Pattern pattern) { Matcher matcher = pattern.matcher(sql); Set 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();