perf(selfop): paginate sales summary before day coverage enrichment

This commit is contained in:
2026-10-13 02:46:20 +08:00
parent 341b7635c7
commit 1fdd978084
3 changed files with 107 additions and 6 deletions
@@ -62,8 +62,16 @@ class AnalyticsReadMapperTest {
assertFalse(page.contains("metric_sales"));assertFalse(page.contains("FROM bounds WHERE 1=0"));
assertTrue(page.contains("bounds AS (SELECT evidence.*,"));
assertTrue(page.contains("counted AS (SELECT /*+ NO_MERGE(b) */ b.*,"));
assertTrue(page.endsWith("ORDER BY observed_quantity IS NULL,observed_quantity DESC,sku_code ASC,customer_code ASC LIMIT ? OFFSET ?"));
assertTrue(page.indexOf("GROUP BY d.customer_code,d.sku_code")<page.lastIndexOf("LIMIT"));
assertTrue(page.endsWith("ORDER BY observed_quantity IS NULL,observed_quantity DESC,sku_code ASC,customer_code ASC"));
assertTrue(page.contains("paged_keys AS (SELECT * FROM rankable ORDER BY sort_observed_quantity IS NULL,sort_observed_quantity DESC,sku_code ASC,customer_code ASC LIMIT ? OFFSET ?)"));
assertTrue(page.indexOf("GROUP BY d.customer_code,d.sku_code")<page.indexOf("LIMIT"));
assertTrue(page.indexOf("LIMIT")<page.indexOf("bounds AS"));
assertEquals(page.indexOf("LIMIT"),page.lastIndexOf("LIMIT"));
assertTrue(page.contains("FROM paged_keys evidence)"));
assertTrue(page.contains("FROM paged_keys k JOIN ll_selfop_sku_daily_sales d ON d.customer_code=k.customer_code AND d.sku_code=k.sku_code"));
String sorting=page.substring(page.indexOf("sort_sales AS"),page.indexOf("bounds AS"));
assertFalse(sorting.contains("outbound_order_count"));assertFalse(sorting.contains("actual_complete"));
assertTrue(sorting.contains("AND ?=1 AND (COALESCE(s.actual_days,0)>0 OR"));
assertFalse(page.contains("FROM bounds b JOIN ll_selfop_sku_daily_sales"));
assertTrue(page.contains("d2.customer_code=b.customer_code AND d2.sku_code=b.sku_code"));
assertTrue(page.contains("d2.sales_run_id=? AND d2.source_account=? AND d2.warehouse_code=?"));
@@ -49,7 +49,7 @@ public class AnalyticsSqlSemanticTest {
String hint="/*+ INDEX(d idx_selfop_daily_sku_cover) */";
assertEquals(days==366,sql.contains(hint),"window="+days);
if(days==366) {
assertTrue(sql.contains("ranged AS (SELECT "+hint+" d.customer_code"));
assertTrue(sql.contains("sort_sales AS (SELECT "+hint+" d.customer_code"));
assertEquals(sql.indexOf(hint),sql.lastIndexOf(hint));
}
assertFalse(AnalyticsReadSql.count(Map.of("p",p)).contains("INDEX("));
@@ -87,6 +87,77 @@ public class AnalyticsSqlSemanticTest {
assertEquals(0,new BigDecimal("5").compareTo(row.observedQuantity()));
assertEquals(2,row.completeDayCount());assertEquals(1,row.partialDayCount());assertEquals(0,row.unknownDayCount());
}
@Test void summaryPageFirstPreservesTiesNullUnitsZeroAndExactScope() throws Exception {
for(String owner:List.of("A","B")){
profile(11,owner,"TIE","VALID_OUTBOUND",utc(D.minusDays(40),0),"PCS");
daily(11,"W",owner,"TIE",D.minusDays(1),"5");
}
profile(22,"C","ZERO","INVENTORY_OBSERVATION",utc(D.minusDays(40),0),"PCS");
profile(22,"C","UNKNOWN","NONE",null,"PCS");
profile(11,"C","MIXED","VALID_OUTBOUND",utc(D.minusDays(40),0),"PCS");
daily(11,"W","C","MIXED",D.minusDays(1),"900");daily(11,"W","C","MIXED",D,"900");
profile(11,"C","NULL-UNIT","VALID_OUTBOUND",utc(D.minusDays(40),0),null);
daily(11,"W","C","NULL-UNIT",D,"800");
profile(22,"C","OUTSIDE","INVENTORY_OBSERVATION",utc(D.minusDays(40),0),"PCS");
profile(99,"C","OTHER-VERSION","VALID_OUTBOUND",utc(D.minusDays(40),0),"PCS");
daily(11,"W","C","NO-PROFILE",D,"9999");
daily(99,"W","A","TIE",D,"9999");daily(11,"OTHER","A","TIE",D,"9999");
daily(11,"W","A","TIE",D.plusDays(1),"9999");
try(var s=schema.createStatement()){
s.executeUpdate("UPDATE ll_selfop_sku_daily_sales SET quantity_unit='BOX' WHERE sku_code='MIXED' AND business_date=DATE '2026-10-12'");
s.executeUpdate("UPDATE ll_selfop_sku_profile SET warehouse_code='OTHER' WHERE sku_code='OUTSIDE'");
}
var p=request("SUMMARY",D.minusDays(1),D.plusDays(1),null,null,utc(D,8),0,2);
assertEquals(6,mapper.count(p));
assertEquals(List.of("A/TIE","B/TIE"),page(p).stream().map(r->r.customerCode()+"/"+r.skuCode()).toList());
var second=page(request("SUMMARY",D.minusDays(1),D.plusDays(1),null,null,utc(D,8),2,2));
assertEquals(List.of("ZERO","MIXED"),second.stream().map(AnalyticsReadRows.Aggregate::skuCode).toList());
assertDecimal("0",second.get(0).observedQuantity());assertNull(second.get(1).observedQuantity());
assertFalse(second.get(1).unitValid());assertEquals(2,second.get(1).unknownDayCount());
var last=page(request("SUMMARY",D.minusDays(1),D.plusDays(1),null,null,utc(D,8),4,2));
assertEquals(List.of("NULL-UNIT","UNKNOWN"),last.stream().map(AnalyticsReadRows.Aggregate::skuCode).toList());
assertTrue(last.stream().allMatch(r->r.observedQuantity()==null));
assertTrue(page(request("SUMMARY",D.minusDays(1),D.plusDays(1),null,null,utc(D,8),6,2)).isEmpty());
var scoped=page(request("SUMMARY",D.minusDays(1),D.plusDays(1),"B","TIE",utc(D,8),0,2));
assertEquals(1,scoped.size());assertDecimal("5",scoped.get(0).observedQuantity());
}
@Test void summaryPageFirstCountsIntersectionAndActualDaysOutsideEvidenceOnlyOnSelectedPage() throws Exception {
profile(11,"C","LATE","VALID_OUTBOUND",utc(D.minusDays(1),12),"PCS");
daily(11,"W","C","LATE",D.minusDays(2),"2");daily(11,"W","C","LATE",D,"3");
profile(22,"C","NO-PROOF","NONE",null,"PCS");daily(11,"W","C","NO-PROOF",D,"9");
profile(22,"C","ZERO","INVENTORY_OBSERVATION",utc(D.minusDays(1),12),"PCS");
var rows=page(request("SUMMARY",D.minusDays(2),D.plusDays(1),null,null,utc(D,8),1,1));
assertEquals(1,rows.size());var late=rows.get(0);assertEquals("LATE",late.skuCode());
assertDecimal("5",late.observedQuantity());assertEquals(2L,late.skuOrderCount());
assertEquals(1,late.completeDayCount());assertEquals(2,late.partialDayCount());assertEquals(0,late.unknownDayCount());
var noProof=page(request("SUMMARY",D.minusDays(2),D.plusDays(1),null,null,utc(D,8),0,1)).get(0);
assertEquals("NO-PROOF",noProof.skuCode());assertDecimal("9",noProof.observedQuantity());
assertEquals(0,noProof.completeDayCount());assertEquals(1,noProof.partialDayCount());assertEquals(2,noProof.unknownDayCount());
var zero=page(request("SUMMARY",D.minusDays(2),D.plusDays(1),null,null,utc(D,8),2,1)).get(0);
assertEquals("ZERO",zero.skuCode());assertDecimal("0",zero.observedQuantity());
assertEquals(0,zero.completeDayCount());assertEquals(2,zero.partialDayCount());assertEquals(1,zero.unknownDayCount());
}
@Test void summaryPageFirstNeverSortsUnknownSalesOrDisjointCoverageAsKnownZero() throws Exception {
profile(22,"C","A-UNKNOWN","NONE",null,"PCS");
profile(22,"C","Z-EVIDENCE","INVENTORY_OBSERVATION",utc(D,0),"PCS");
for(LocalDateTime[] span:List.of(
new LocalDateTime[]{utc(D,0),utc(D,0)},
new LocalDateTime[]{utc(D,8),utc(D,0)},
new LocalDateTime[]{utc(D.minusDays(2),0),utc(D,0)},
new LocalDateTime[]{utc(D.plusDays(1),0),utc(D.plusDays(2),0)},
new LocalDateTime[]{null,utc(D,8)})){
var p=new AnalyticsSqlRequest("demo","W",null,null,11,22,D,D.plusDays(1),D,D.minusDays(30),null,null,
span[0],span[1],true,false,false,false,true,0,1,"SUMMARY");
var first=page(p).get(0);assertEquals("A-UNKNOWN",first.skuCode());assertNull(first.observedQuantity());
var last=new AnalyticsSqlRequest("demo","W",null,null,11,22,D,D.plusDays(1),D,D.minusDays(30),null,null,
span[0],span[1],true,false,false,false,true,1,1,"SUMMARY");
assertNull(page(last).get(0).observedQuantity());
}
daily(11,"W","C","Z-EVIDENCE",D,"99");
var unavailable=new AnalyticsSqlRequest("demo","W",null,null,11,22,D,D.plusDays(1),D,D.minusDays(30),null,null,
utc(D,0),utc(D,8),false,false,false,false,true,0,1,"SUMMARY");
var first=page(unavailable).get(0);assertEquals("A-UNKNOWN",first.skuCode());assertNull(first.observedQuantity());
}
@Test void evidenceAfterCoveredTimeOnSameDayDoesNotBecomeObservedZero() throws Exception {
profile(22,"C","LATE","INVENTORY_OBSERVATION",utc(D,16),"PCS");
profile(22,"C","NO-EVIDENCE","NONE",null,"PCS");