diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundAggregateMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundAggregateMapper.java index 7594c32a6..56cbdfe5c 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundAggregateMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundAggregateMapper.java @@ -56,7 +56,8 @@ public interface OutboundAggregateMapper { MAX(CASE WHEN outbound_rank=1 THEN observation_run_id END) evidence_run_id, MAX(CASE WHEN seen_rank=1 THEN observation_run_id END) last_seen_run_id FROM ( - SELECT o.customer_code,i.sku_code,i.product_name,i.quantity_unit,o.outbound_at_utc, + SELECT /*+ JOIN_ORDER(o,i,s,r,t) INDEX(s idx_selfop_stage_fact_observation) */ + o.customer_code,i.sku_code,i.product_name,i.quantity_unit,o.outbound_at_utc, s.observed_at_utc,s.run_id observation_run_id, ROW_NUMBER() OVER (PARTITION BY o.customer_code,i.sku_code ORDER BY o.outbound_at_utc,o.id) outbound_rank, ROW_NUMBER() OVER (PARTITION BY o.customer_code,i.sku_code ORDER BY s.observed_at_utc DESC,s.id DESC) seen_rank diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundAggregateMapperTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundAggregateMapperTest.java index ea22d433f..1e7fa2fec 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundAggregateMapperTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundAggregateMapperTest.java @@ -79,6 +79,30 @@ public class OutboundAggregateMapperTest { assertEquals(1L,profiles.get(0).getEvidenceRunId());assertEquals(1L,profiles.get(0).getLastSeenRunId()); assertEquals(LocalDateTime.of(2026,10,12,2,0),profiles.get(0).getFirstSeenAtUtc()); } + @Test void orderDrivenProfileBatchPreservesCursorAndIndependentEvidenceRanks() throws Exception { + order(1,"demo","C1","A","SKU-B",1,"2026-10-02 00:00:00",2); + order(2,"demo","C1","A","SKU-B",1,"2026-10-01 00:00:00",3); + order(3,"demo","C1","A","SKU-A",1,"2026-09-01 00:00:00",7); + order(4,"demo","C2","A","SKU-A",1,"2026-10-03 00:00:00",5); + order(5,"demo","C2","A","ROOTED",1,"2026-09-01 00:00:00",9); + order(6,"demo","C2","A","VALIDATION",1,"2026-09-01 00:00:00",9); + sql("UPDATE ll_selfop_sync_stage SET observed_at_utc='2026-10-13 01:00:00' WHERE id=1"); + sql("UPDATE ll_selfop_sync_task SET validation_root_task_id=99 WHERE id=5"); + sql("UPDATE ll_selfop_sync_task SET execution_purpose='VALIDATION' WHERE id=6"); + var rows=mapper.profileBatch("demo","A","C1","SKU-A"); + assertEquals(2,rows.size()); + assertEquals("C1",rows.get(0).getCustomerCode());assertEquals("SKU-B",rows.get(0).getSkuCode()); + assertEquals(2L,rows.get(0).getEvidenceRunId());assertEquals(1L,rows.get(0).getLastSeenRunId()); + assertEquals(LocalDateTime.of(2026,10,12,1,0),rows.get(0).getFirstSeenAtUtc()); + assertEquals(LocalDateTime.of(2026,10,1,0,0),rows.get(0).getEarliestEvidenceAtUtc()); + assertEquals("C2",rows.get(1).getCustomerCode());assertEquals("SKU-A",rows.get(1).getSkuCode()); + assertEquals(1,mapper.profileBatch("demo","A","C1","SKU-B").size()); + // H2 verifies result semantics but ignores MySQL hints; root separately verifies EXPLAIN and SHOW WARNINGS. + var statement=session.getConfiguration().getMappedStatement(OutboundAggregateMapper.class.getName()+".profileBatch"); + String bound=statement.getBoundSql(Map.of("account","demo","warehouse","A","customer","C1","sku","SKU-A")).getSql(); + assertTrue(bound.contains("JOIN_ORDER(o,i,s,r,t) INDEX(s idx_selfop_stage_fact_observation)")); + assertTrue(bound.indexOf("JOIN_ORDER(")