perf(lastleg): reduce inventory turnover query aggregation work
This commit is contained in:
+6
-1
@@ -35,7 +35,12 @@ class InventoryTurnoverContextTest {
|
||||
assertEquals(END.minusDays(29),first.analyticsContext().rangeStartDate());
|
||||
assertEquals(END.plusDays(1),first.analyticsContext().rangeEndDateExclusive());
|
||||
assertEquals(first,at(NOW.plusSeconds(60)).resolve(request(first.snapshotContext())).response());
|
||||
verify(mapper,times(2)).samples("demo","W",IH,utc(NOW),END.minusDays(29),END.plusDays(1));
|
||||
var page2=new InventoryTurnoverRequest("demo","W",END,30,"SKU",null,null,"ALL",null,null,"PHYSICAL_DESC",2,20,false,first.snapshotContext());
|
||||
var second=at(NOW.plusSeconds(90)).resolve(page2);
|
||||
assertEquals(first,second.response());
|
||||
assertEquals(InventoryTurnoverSql.input(request(first.snapshotContext()),contexts.resolve(request(first.snapshotContext()))).days(),
|
||||
InventoryTurnoverSql.input(page2,second).days());
|
||||
verify(mapper,times(4)).samples("demo","W",IH,utc(NOW),END.minusDays(29),END.plusDays(1));
|
||||
// A subsequently published sample cannot appear in the old asOf query. If reconstruction
|
||||
// ever differs, reject the context instead of mixing old charts and new rows.
|
||||
when(mapper.samples(eq("demo"),eq("W"),eq(IH),any(),any(),any())).thenReturn(List.of());
|
||||
|
||||
+239
@@ -103,6 +103,16 @@ class InventoryTurnoverSqlTest {
|
||||
assertNull(absent.get("w30_outbound"));assertNull(absent.get("w30_observed"));
|
||||
decimal("0",query("overview",input(q,30,0,true)).get(0).get("no_outbound_count"));
|
||||
}
|
||||
@Test void warehouseChartUsesSalesUnitsAndCoverageIndependentlyOfStockProfiles() throws Exception {
|
||||
seed("A","SKU",30);
|
||||
execute("UPDATE ll_selfop_sku_profile SET quantity_unit='UNKNOWN'");
|
||||
execute("UPDATE ll_selfop_inventory_snapshot SET physical_quantity=NULL");
|
||||
var p=input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),30,0,false);
|
||||
var row=query("chartWindows",p).get(0);decimal("300",row.get("w30_outbound"));
|
||||
execute("UPDATE ll_selfop_sku_daily_sales SET quantity_unit='UNKNOWN' WHERE business_date=DATE '"+END+"'");
|
||||
row=query("chartWindows",p).get(0);assertNull(row.get("w30_outbound"));
|
||||
decimal("290",row.get("w30_observed"));decimal("29",row.get("w30_complete"));decimal("6",row.get("w7_complete"));
|
||||
}
|
||||
@Test void fullScopeFilterPrecedesPaginationAndKeepsSoldOutHistoricalCandidates() throws Exception {
|
||||
seed("A","LOW",30);seed("A","HIGH",30);seed("B","SOLD",30);
|
||||
execute("UPDATE ll_selfop_inventory_snapshot SET available_quantity=3000,physical_quantity=3000 WHERE sku_code='HIGH' AND snapshot_run_id=22 AND product_quality=0");
|
||||
@@ -137,6 +147,231 @@ class InventoryTurnoverSqlTest {
|
||||
assertEquals(2,report.stream().filter(r->"OWNER".equals(r.get("section"))).count());
|
||||
assertEquals(1,report.stream().filter(r->"OVERVIEW".equals(r.get("section"))).count());
|
||||
}
|
||||
@Test void frozenSamplesExcludeUnselectedVersionsWithoutDroppingMissingDays() throws Exception {
|
||||
seed("A","SKU",30);
|
||||
profile(999,"A","UNSELECTED");stock(999,"A","UNSELECTED",0,999,999);
|
||||
var q=request("SKU","ALL","PHYSICAL_DESC",null,null,1,20);
|
||||
var rows=reportRows(input(q,30,2,true));
|
||||
assertEquals(1,rows.size());decimal("2",rows.get(0).get("sample_count"));
|
||||
assertNull(rows.get(0).get("turnover"));
|
||||
assertEquals(28,rows.get(0).get("missing_dates").toString().split(",").length);
|
||||
rows=reportRows(input(q,30,0,true));
|
||||
assertEquals(1,rows.size());decimal("0",rows.get(0).get("sample_count"));
|
||||
assertEquals(30,rows.get(0).get("missing_dates").toString().split(",").length);
|
||||
decimal("300",rows.get(0).get("w30_outbound"));
|
||||
}
|
||||
@Test void repeatedSampleRunAndHighestDateBitKeepAllThreeWindowsExact() throws Exception {
|
||||
seed("A","SKU",30);
|
||||
for(boolean onlyLast:List.of(true,false)) for(int n:List.of(7,15,30)) for(String group:List.of("SKU","OWNER")) {
|
||||
var dates=new ArrayList<InventoryTurnoverSql.Day>();var missing=new ArrayList<String>();
|
||||
for(int i=0;i<30;i++) {
|
||||
var date=END.minusDays(29-i);boolean present=onlyLast?i==29:i!=29;
|
||||
// Reusing a version on multiple frozen dates must retain each date.
|
||||
dates.add(new InventoryTurnoverSql.Day(date,date.atStartOfDay().minusHours(8),1,present?100L:null));
|
||||
if(i>=30-n&&!present)missing.add(date.toString());
|
||||
}
|
||||
var q=new InventoryTurnoverRequest("demo","W",END,n,group,null,null,"ALL",null,null,null,1,20,true,null);
|
||||
var p=new InventoryTurnoverSql.Input(q,11,22,true,true,List.copyOf(dates));
|
||||
var row=query("page",p).get(0);
|
||||
decimal(onlyLast?"536870912":"536870911",row.get("sample_mask"));
|
||||
decimal(String.valueOf(onlyLast?1:n-1),row.get("sample_count"));
|
||||
assertEquals(String.join(",",missing),row.get("missing_dates"));
|
||||
assertNull(row.get("average_physical"));assertNull(row.get("turnover"));
|
||||
assertEquals(row.get("missing_dates"),reportRows(p).get(0).get("missing_dates"));
|
||||
}
|
||||
}
|
||||
@Test void sparseSamplesKeepExactDateComplementAndFullOwnerDenominator() throws Exception {
|
||||
seed("A","ONE",30);seed("A","TWO",30);
|
||||
// Both edge bits and a middle bit, with different missing members per day.
|
||||
execute("DELETE FROM ll_selfop_inventory_snapshot WHERE warehouse_code='W' AND ((sku_code='ONE' AND snapshot_run_id IN (100,114)) OR (sku_code='TWO' AND snapshot_run_id=129))");
|
||||
for(int n:List.of(7,15,30)) {
|
||||
var q=new InventoryTurnoverRequest("demo","W",END,n,"OWNER",null,null,"ALL",null,null,null,1,20,true,null);
|
||||
var p=input(q,30,30,true);
|
||||
var row=reportRows(p).get(0);
|
||||
var expected=new ArrayList<LocalDate>();
|
||||
for(int index:List.of(0,14,29)) if(index>=30-n) expected.add(END.minusDays(29-index));
|
||||
decimal(String.valueOf(n-expected.size()),row.get("sample_count"));
|
||||
assertEquals(String.join(",",expected.stream().map(LocalDate::toString).toList()),row.get("missing_dates"));
|
||||
assertNull(row.get("average_physical"));assertNull(row.get("turnover"));
|
||||
assertEquals(row.get("missing_dates"),query("page",p).get(0).get("missing_dates"));
|
||||
}
|
||||
var pageTwo=new InventoryTurnoverRequest("demo","W",END,30,"SKU",null,null,"ALL",null,null,null,2,1,false,null);
|
||||
var second=reportRows(input(pageTwo,30,30,true)).get(0);
|
||||
assertEquals("TWO",second.get("sku_code"));decimal("29",second.get("sample_count"));assertEquals(END.toString(),second.get("missing_dates"));
|
||||
// A stock/profile-only candidate without samples invalidates all owner days.
|
||||
profile(22,"A","NO-SAMPLES");stock(22,"A","NO-SAMPLES",0,0,0);
|
||||
var owner=reportRows(input(request("OWNER","ALL","PHYSICAL_DESC",null,null,1,20),30,30,true)).get(0);
|
||||
decimal("0",owner.get("sample_count"));assertEquals(30,owner.get("missing_dates").toString().split(",").length);
|
||||
}
|
||||
@Test void reportBindsExactLargeSampleVersionsAndKeepsWindowDenominators() throws Exception {
|
||||
seed("A","SKU",30);
|
||||
long first=9_007_199_254_740_993L;
|
||||
for(int i=0;i<30;i++) {
|
||||
execute("UPDATE ll_selfop_sku_profile SET profile_run_id="+(first+i)+" WHERE profile_run_id="+(100+i));
|
||||
execute("UPDATE ll_selfop_inventory_snapshot SET snapshot_run_id="+(first+i)+",physical_quantity=CASE WHEN product_quality=0 THEN "+((i+1)*10)+" ELSE 0 END WHERE snapshot_run_id="+(100+i));
|
||||
}
|
||||
List<InventoryTurnoverSql.Day> days=new ArrayList<>();
|
||||
for(int i=0;i<30;i++){var date=END.minusDays(29-i);days.add(new InventoryTurnoverSql.Day(date,date.atStartOfDay().minusHours(8),1,first+i));}
|
||||
for(int n:List.of(7,15,30)) {
|
||||
var q=new InventoryTurnoverRequest("demo","W",END,n,"SKU",null,null,"ALL",null,null,null,1,20,true,null);
|
||||
var p=new InventoryTurnoverSql.Input(q,11,22,true,true,List.copyOf(days));
|
||||
var row=reportRows(p).get(0);
|
||||
decimal(String.valueOf(n),row.get("sample_count"));
|
||||
decimal(n==7?"270":n==15?"230":"155",row.get("average_physical"));
|
||||
decimal("70",row.get("w7_outbound"));decimal("150",row.get("w15_outbound"));decimal("300",row.get("w30_outbound"));
|
||||
assertNotNull(row.get("turnover"));
|
||||
}
|
||||
var missing=days.get(14);days.set(14,new InventoryTurnoverSql.Day(missing.date(),missing.startUtc(),1,null));
|
||||
var row=reportRows(new InventoryTurnoverSql.Input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),11,22,true,true,List.copyOf(days))).get(0);
|
||||
decimal("29",row.get("sample_count"));assertNull(row.get("turnover"));
|
||||
assertEquals(missing.date().toString(),row.get("missing_dates"));
|
||||
}
|
||||
@Test void reportPaginationKeepsTotalAndCanOmitOverview() throws Exception {
|
||||
seed("A","SKU",30);seed("B","SKU",30);
|
||||
var first=reportRows(input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,1),30,30,true));
|
||||
var secondQuery=new InventoryTurnoverRequest("demo","W",END,30,"SKU",null,null,"ALL",null,null,"PHYSICAL_DESC",2,1,false,"frozen");
|
||||
var second=query("report",input(secondQuery,30,30,true));
|
||||
var row=second.stream().filter(r->"ROW".equals(r.get("section"))).findFirst().orElseThrow();
|
||||
assertNotEquals(first.get(0).get("customer_code"),payload(row).path("customer_code").asText());
|
||||
assertEquals(2,second.size());
|
||||
var beyond=new InventoryTurnoverRequest("demo","W",END,30,"SKU",null,null,"ALL",null,null,"PHYSICAL_DESC",3,1,false,"frozen");
|
||||
var report=query("report",input(beyond,30,30,true));assertEquals(1,report.size());
|
||||
assertEquals("TOTAL",report.get(0).get("section"));assertEquals(2,payload(report.get(0)).path("total").asInt());
|
||||
}
|
||||
@Test void projectedFormalWindowsKeepPartialObservationsAndUnknownDistinctFromZero() throws Exception {
|
||||
seed("A","SKU",30);
|
||||
for(String group:List.of("SKU","OWNER")) {
|
||||
var p=input(request(group,"ALL","PHYSICAL_DESC",null,null,1,20),0,30,true);
|
||||
var row=reportRows(p).get(0);
|
||||
assertNull(row.get("w30_outbound"));decimal("300",row.get("w30_observed"));decimal("30",row.get("w30_partial"));
|
||||
}
|
||||
execute("DELETE FROM ll_selfop_sku_daily_sales WHERE warehouse_code='W'");
|
||||
for(String group:List.of("SKU","OWNER")) {
|
||||
var p=input(request(group,"ALL","PHYSICAL_DESC",null,null,1,20),0,30,true);
|
||||
var unknown=reportRows(p).get(0);assertNull(unknown.get("w30_outbound"));assertNull(unknown.get("w30_observed"));
|
||||
var partialDays=p.days().stream().map(d->new InventoryTurnoverSql.Day(d.date(),d.startUtc(),2,d.sampleRun())).toList();
|
||||
var partial=reportRows(new InventoryTurnoverSql.Input(p.query(),11,22,true,true,partialDays)).get(0);
|
||||
assertNull(partial.get("w30_outbound"));decimal("0",partial.get("w30_observed"));decimal("30",partial.get("w30_partial"));
|
||||
var complete=reportRows(input(p.query(),30,30,true)).get(0);
|
||||
decimal("0",complete.get("w30_outbound"));decimal("30",complete.get("w30_complete"));
|
||||
}
|
||||
}
|
||||
@Test void zeroEvidenceUsesExactBeijingMidnightIncludingMilliseconds() throws Exception {
|
||||
seed("A","SKU",0);execute("DELETE FROM ll_selfop_sku_daily_sales WHERE warehouse_code='W'");
|
||||
for(int n:List.of(7,15,30)) for(int millis:List.of(-1,0,1)) {
|
||||
LocalDate start=END.minusDays(n-1);var utc=start.atStartOfDay().minusHours(8).plusNanos(millis*1_000_000L);
|
||||
execute("UPDATE ll_selfop_sku_profile SET earliest_evidence_at_utc=TIMESTAMP '"+utc.toString().replace('T',' ')+"'");
|
||||
var p=input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),30,0,true);
|
||||
var row=reportRows(p).get(0);decimal(String.valueOf(n-(millis>0?1:0)),row.get("w"+n+"_complete"));
|
||||
if(millis>0)assertNull(row.get("w"+n+"_outbound"));else decimal("0",row.get("w"+n+"_outbound"));
|
||||
execute("INSERT INTO ll_selfop_sku_daily_sales VALUES(11,'demo','W','A','SKU',DATE '"+start+"',0,'PCS')");
|
||||
row=reportRows(p).get(0);decimal(String.valueOf(n),row.get("w"+n+"_complete"));decimal("0",row.get("w"+n+"_outbound"));
|
||||
execute("DELETE FROM ll_selfop_sku_daily_sales WHERE warehouse_code='W'");
|
||||
}
|
||||
}
|
||||
@Test void sparseCorrectionRevokesInvalidActualZeroAndHandlesEvidenceAfterLastBoundary() throws Exception {
|
||||
seed("A","SKU",0);execute("DELETE FROM ll_selfop_sku_daily_sales WHERE warehouse_code='W'");
|
||||
var p=input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),1,0,true);
|
||||
var afterLast=END.atStartOfDay().minusHours(8).plusNanos(1_000_000);
|
||||
execute("UPDATE ll_selfop_sku_profile SET earliest_evidence_at_utc=TIMESTAMP '"+afterLast.toString().replace('T',' ')+"'");
|
||||
var row=reportRows(p).get(0);decimal("0",row.get("w7_complete"));assertNull(row.get("w7_observed"));decimal("0",row.get("w7_partial"));
|
||||
execute("INSERT INTO ll_selfop_sku_daily_sales VALUES(11,'demo','W','A','SKU',DATE '"+END+"',1.234567,'PCS')");
|
||||
row=reportRows(p).get(0);decimal("1",row.get("w7_complete"));decimal("1.234567",row.get("w7_observed"));
|
||||
execute("UPDATE ll_selfop_sku_profile SET earliest_evidence_at_utc=TIMESTAMP '2026-01-01 00:00:00'");
|
||||
execute("UPDATE ll_selfop_sku_daily_sales SET quantity_unit='UNKNOWN' WHERE warehouse_code='W'");
|
||||
row=reportRows(p).get(0);decimal("0",row.get("w7_complete"));assertNull(row.get("w7_observed"));decimal("0",row.get("w7_partial"));
|
||||
}
|
||||
@Test void presentInvalidSamplesNeverQualifyAndOwnerZeroNeedsEverySku() throws Exception {
|
||||
seed("A","ONE",30);seed("A","TWO",30);
|
||||
String key=" WHERE snapshot_run_id=100 AND sku_code='TWO' AND product_quality=0";
|
||||
for(String mutation:List.of("physical_quantity=NULL","physical_quantity=-1","product_quality=9","quality_status='UNKNOWN',quality_reason='UNCONFIRMED'","profile")) {
|
||||
if("profile".equals(mutation))execute("UPDATE ll_selfop_sku_profile SET quantity_unit='UNKNOWN' WHERE profile_run_id=100 AND sku_code='TWO'");
|
||||
else execute("UPDATE ll_selfop_inventory_snapshot SET "+mutation+key);
|
||||
var owner=reportRows(input(request("OWNER","ALL","PHYSICAL_DESC",null,null,1,20),30,30,true)).get(0);
|
||||
decimal("29",owner.get("sample_count"));assertNull(owner.get("turnover"));assertEquals(END.minusDays(29).toString(),owner.get("missing_dates"));
|
||||
execute("DELETE FROM ll_selfop_inventory_snapshot WHERE snapshot_run_id=100 AND sku_code='TWO'");stock(100,"A","TWO",0,450,450);stock(100,"A","TWO",1,50,0);
|
||||
execute("UPDATE ll_selfop_sku_profile SET quantity_unit='PCS' WHERE profile_run_id=100 AND sku_code='TWO'");
|
||||
}
|
||||
execute("UPDATE ll_selfop_inventory_snapshot SET physical_quantity=0 WHERE snapshot_run_id>=100");
|
||||
var owner=reportRows(input(request("OWNER","ALL","PHYSICAL_DESC",null,null,1,20),30,30,true)).get(0);
|
||||
decimal("30",owner.get("sample_count"));decimal("0",owner.get("average_physical"));assertNull(owner.get("turnover"));
|
||||
}
|
||||
@Test void ownerInvalidActualAndAbsentMemberKeepCoveredZeroDifferentFromUnknown() throws Exception {
|
||||
seed("A","ONE",0);seed("A","TWO",0);execute("DELETE FROM ll_selfop_sku_daily_sales WHERE warehouse_code='W'");
|
||||
execute("INSERT INTO ll_selfop_sku_daily_sales VALUES(11,'demo','W','A','ONE',DATE '"+END+"',10,'UNKNOWN')");
|
||||
var q=request("OWNER","ALL","PHYSICAL_DESC",null,null,1,20);
|
||||
var row=reportRows(input(q,30,0,true)).get(0);decimal("6",row.get("w7_complete"));decimal("0",row.get("w7_observed"));decimal("1",row.get("w7_partial"));
|
||||
row=reportRows(input(q,0,0,true)).get(0);assertNull(row.get("w7_observed"));decimal("0",row.get("w7_partial"));
|
||||
execute("INSERT INTO ll_selfop_sku_daily_sales VALUES(11,'demo','W','A','TWO',DATE '"+END+"',10,'UNKNOWN')");
|
||||
row=reportRows(input(q,1,0,true)).get(0);assertNull(row.get("w7_observed"));decimal("0",row.get("w7_complete"));decimal("0",row.get("w7_partial"));
|
||||
}
|
||||
@Test void decimalWindowsRemainExactBeyondJavascriptIntegerRangeWithoutDuplicatingCandidates() throws Exception {
|
||||
for(String sku:List.of("ONE","TWO","THREE","FOUR"))seed("A",sku,30);
|
||||
String amount="99999999999999.123456";var quantity=new BigDecimal(amount);
|
||||
execute("UPDATE ll_selfop_sku_daily_sales SET outbound_quantity="+amount+" WHERE warehouse_code='W'");
|
||||
execute("UPDATE ll_selfop_inventory_snapshot SET physical_quantity=CASE WHEN product_quality=0 THEN "+amount+" ELSE 0 END");
|
||||
var report=query("report",input(request("OWNER","ALL","PHYSICAL_DESC",null,null,1,20),30,30,true));
|
||||
var row=payload(report.stream().filter(r->"ROW".equals(r.get("section"))).findFirst().orElseThrow());
|
||||
decimal(quantity.multiply(BigDecimal.valueOf(120)).toPlainString(),row.path("w30_outbound").decimalValue());
|
||||
decimal(quantity.multiply(BigDecimal.valueOf(4)).toPlainString(),row.path("average_physical").decimalValue());
|
||||
decimal("30",row.path("turnover").decimalValue());assertEquals(30,row.path("sample_count").asInt());
|
||||
var skuReport=query("report",input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),30,30,true));
|
||||
assertEquals(4,skuReport.stream().filter(r->"ROW".equals(r.get("section"))).count());
|
||||
for(var raw:skuReport)if("ROW".equals(raw.get("section")))decimal(quantity.multiply(BigDecimal.valueOf(30)).toPlainString(),payload(raw).path("w30_outbound").decimalValue());
|
||||
}
|
||||
@Test void threeSourceCandidateUnionKeepsFullOwnerDenominator() throws Exception {
|
||||
profile(100,"A","HISTORY");profile(11,"A","HISTORY");profile(22,"A","HISTORY");
|
||||
stock(22,"A","STOCK",0,0,0);
|
||||
execute("INSERT INTO ll_selfop_sku_daily_sales VALUES(11,'demo','W','A','SALES',DATE '"+END+"',1.123456,'PCS')");
|
||||
var skuRows=reportRows(input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),30,30,true));
|
||||
assertEquals(Set.of("HISTORY","STOCK","SALES"),new HashSet<>(skuRows.stream().map(r->r.get("sku_code")).toList()));
|
||||
assertEquals(3,skuRows.size());
|
||||
var report=query("report",input(request("OWNER","ALL","PHYSICAL_DESC",null,null,1,20),30,30,true));
|
||||
var row=payload(report.stream().filter(r->"ROW".equals(r.get("section"))).findFirst().orElseThrow());
|
||||
decimal("1.123456",row.path("w30_outbound").decimalValue());
|
||||
assertEquals(0,row.path("sample_count").asInt());assertTrue(row.path("average_physical").isNull());
|
||||
assertEquals(30,row.path("missing_dates").asText().split(",").length);
|
||||
}
|
||||
@Test void ownerStockChartSurvivesMissingWindowsAndEmptySkuFilter() throws Exception {
|
||||
seed("A","SKU",0);seed("B","SKU",0);seed("C","SOLD",0);
|
||||
execute("UPDATE ll_selfop_inventory_snapshot SET physical_quantity=NULL,quality_status='UNKNOWN',quality_reason='FIELD_MISSING' WHERE customer_code='B' AND snapshot_run_id=22");
|
||||
execute("UPDATE ll_selfop_inventory_snapshot SET physical_quantity=0,available_quantity=0 WHERE customer_code='C' AND snapshot_run_id=22");
|
||||
var baseline=query("report",input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),7,0,true));
|
||||
var baselineSummary=payload(baseline.stream().filter(r->"OVERVIEW".equals(r.get("section"))).findFirst().orElseThrow());
|
||||
for(String group:List.of("SKU","OWNER")) {
|
||||
var full=input(request(group,"ALL","PHYSICAL_DESC",null,null,1,20),7,0,true);
|
||||
var standalone=query("page",full);var reportRows=reportRows(full);
|
||||
assertEquals(standalone.size(),reportRows.size());
|
||||
for(int i=0;i<standalone.size();i++) for(var field:reportRows.get(i).entrySet()) {
|
||||
Object expected=standalone.get(i).get(field.getKey()),actual=field.getValue();
|
||||
if(expected instanceof Number)decimal(expected.toString(),actual);
|
||||
else if(expected instanceof java.sql.Date)assertEquals(expected.toString(),actual);
|
||||
else assertEquals(expected,actual,field.getKey());
|
||||
}
|
||||
var q=request(group,"ALL","PHYSICAL_DESC","SKU".equals(group)?"not-present":null,null,1,20);
|
||||
var report=query("report",input(q,7,0,true));
|
||||
var chart=report.stream().filter(r->"OWNER".equals(r.get("section"))).toList();
|
||||
assertEquals(2,chart.size());
|
||||
var known=payload(chart.get(0));assertEquals("A",known.path("customer_code").asText());decimal("610",known.path("physical").decimalValue());
|
||||
var unknown=report.stream().filter(r->"OWNER_META".equals(r.get("section"))).findFirst().orElseThrow();
|
||||
assertEquals(1,payload(unknown).path("unknown_count").asInt());
|
||||
var summary=report.stream().filter(r->"OVERVIEW".equals(r.get("section"))).findFirst().orElseThrow();
|
||||
assertEquals(3,payload(summary).path("sku_count").asInt());assertEquals(baselineSummary,payload(summary));
|
||||
var total=report.stream().filter(r->"TOTAL".equals(r.get("section"))).findFirst().orElseThrow();
|
||||
assertEquals("SKU".equals(group)?0:3,payload(total).path("total").asInt());
|
||||
assertEquals("SKU".equals(group)?0:3,report.stream().filter(r->"ROW".equals(r.get("section"))).count());
|
||||
}
|
||||
}
|
||||
private static com.fasterxml.jackson.databind.JsonNode payload(Map<String,Object> row) throws Exception {
|
||||
var json=new com.fasterxml.jackson.databind.ObjectMapper().enable(com.fasterxml.jackson.databind.DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);Object raw=row.get("payload");
|
||||
return raw instanceof byte[] bytes?json.readTree(bytes):json.readTree(raw.toString());
|
||||
}
|
||||
private List<Map<String,Object>> reportRows(InventoryTurnoverSql.Input p) throws Exception {
|
||||
var result=new ArrayList<Map<String,Object>>();var json=new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
for(var row:query("report",p)) if("ROW".equals(row.get("section")))
|
||||
result.add(json.convertValue(payload(row),new com.fasterxml.jackson.core.type.TypeReference<Map<String,Object>>(){}));
|
||||
return result;
|
||||
}
|
||||
private void seed(String owner,String sku,int samples) throws Exception {
|
||||
profile(11,owner,sku);profile(22,owner,sku);stock(22,owner,sku,0,600,600);stock(22,owner,sku,1,10,0);
|
||||
for(int i=0;i<30;i++) {
|
||||
@@ -159,6 +394,10 @@ class InventoryTurnoverSqlTest {
|
||||
Object value=meta.getValue(parameter.getProperty());String literal=value==null?"NULL":value instanceof Boolean b?b?"1":"0":value instanceof Number?value.toString():"'"+value.toString().replace("'","''")+"'";
|
||||
sql=sql.replaceFirst("\\?",java.util.regex.Matcher.quoteReplacement(literal));
|
||||
}
|
||||
// MySQL SIGNED is a signed 64-bit integer; H2's alias is only 32-bit.
|
||||
sql=sql.replace(" AS SIGNED)"," AS BIGINT)");
|
||||
// MySQL's unbounded CHAR cast preserves the date; H2 defaults to CHAR(1).
|
||||
sql=sql.replace("CAST(business_date AS CHAR)","CAST(business_date AS VARCHAR(10))");
|
||||
// H2 inlines repeatedly referenced CTEs exponentially. Materialize each unchanged
|
||||
// production relation in this local fixture; MySQL execution plans remain unverified.
|
||||
List<String> relations=new ArrayList<>();int cursor=5;
|
||||
|
||||
Reference in New Issue
Block a user