优化
This commit is contained in:
+115
@@ -0,0 +1,115 @@
|
||||
package com.baiyue.lastleg.selfoperated.analytics.query;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.*;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.apache.ibatis.reflection.SystemMetaObject;
|
||||
import org.junit.jupiter.api.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/** Synthetic local SQL semantics only. Does not assert MySQL performance or deployed availability. */
|
||||
class InventoryTurnoverSqlTest {
|
||||
private Connection db;
|
||||
private final Configuration config=new Configuration();
|
||||
static final LocalDate END=LocalDate.of(2026,10,12);
|
||||
@BeforeEach void setup() throws Exception {
|
||||
db=DriverManager.getConnection("jdbc:h2:mem:turnover_"+UUID.randomUUID()+";MODE=MySQL","sa","");
|
||||
config.addMapper(InventoryTurnoverReadMapper.class);
|
||||
execute("CREATE ALIAS CONVERT_TZ FOR 'com.baiyue.lastleg.selfoperated.analytics.query.AnalyticsSqlSemanticTest.convertZone'");
|
||||
execute("CREATE TABLE ll_selfop_sku_profile(profile_run_id BIGINT,source_account VARCHAR,warehouse_code VARCHAR,customer_code VARCHAR,sku_code VARCHAR,product_name VARCHAR,quantity_unit VARCHAR,evidence_type VARCHAR,evidence_run_id BIGINT,earliest_evidence_at_utc TIMESTAMP)");
|
||||
execute("CREATE TABLE ll_selfop_sku_daily_sales(sales_run_id BIGINT,source_account VARCHAR,warehouse_code VARCHAR,customer_code VARCHAR,sku_code VARCHAR,business_date DATE,outbound_quantity DECIMAL(20,6),quantity_unit VARCHAR)");
|
||||
execute("CREATE TABLE ll_selfop_inventory_snapshot(snapshot_run_id BIGINT,source_account VARCHAR,warehouse_code VARCHAR,customer_code VARCHAR,sku_code VARCHAR,product_quality INT,physical_quantity DECIMAL(20,6),available_quantity DECIMAL(20,6),frozen_quantity DECIMAL(20,6),quality_status VARCHAR,quality_reason VARCHAR)");
|
||||
}
|
||||
@AfterEach void close() throws Exception {db.close();}
|
||||
static InventoryTurnoverRequest request(String group,String focus,String sort,String sku,Integer threshold,int page,int size) {
|
||||
return new InventoryTurnoverRequest("demo","W",END,30,group,null,sku,focus,threshold,null,sort,page,size,true,null);
|
||||
}
|
||||
static InventoryTurnoverSql.Input input(InventoryTurnoverRequest q,int covered,int samples,boolean fresh) {
|
||||
List<InventoryTurnoverSql.Day> days=new ArrayList<>();
|
||||
for(int i=0;i<30;i++){LocalDate date=END.minusDays(29-i);days.add(new InventoryTurnoverSql.Day(date,date.atStartOfDay().minusHours(8),i>=30-covered?1:0,i<samples?100L+i:null));}
|
||||
return new InventoryTurnoverSql.Input(q,11,22,fresh,true,List.copyOf(days));
|
||||
}
|
||||
@Test void threeWindowsAndDailyQualitiesAreNotMultiplied() throws Exception {
|
||||
seed("A","SKU",30);
|
||||
var p=input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),30,30,true);
|
||||
var row=query("page",p).get(0);
|
||||
decimal("70",row.get("w7_outbound"));decimal("150",row.get("w15_outbound"));decimal("300",row.get("w30_outbound"));
|
||||
decimal("600",row.get("available"));decimal("60",row.get("coverage_days"));
|
||||
decimal("500",row.get("average_physical"));decimal("0.6",row.get("turnover"));
|
||||
assertEquals(30,((Number)row.get("sample_count")).intValue());
|
||||
decimal("1",query("count",p).get(0).values().iterator().next());
|
||||
}
|
||||
@Test void gapsKeepSevenDaysAndInvalidateThirtyWithoutShrinking() throws Exception {
|
||||
seed("A","SKU",2);
|
||||
var row=query("page",input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),7,2,true)).get(0);
|
||||
decimal("70",row.get("w7_outbound"));assertNull(row.get("w15_outbound"));assertNull(row.get("w30_outbound"));
|
||||
decimal("300",row.get("w30_observed"));assertNull(row.get("coverage_days"));assertNull(row.get("turnover"));
|
||||
assertEquals(2,((Number)row.get("sample_count")).intValue());
|
||||
}
|
||||
@Test void historicalTurnoverSurvivesStaleCurrentAndMissingCurrentIsNotZero() throws Exception {
|
||||
seed("A","SKU",30);
|
||||
var row=query("page",input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),30,30,false)).get(0);
|
||||
assertNull(row.get("physical"));assertNull(row.get("coverage_days"));decimal("0.6",row.get("turnover"));
|
||||
execute("DELETE FROM ll_selfop_inventory_snapshot WHERE snapshot_run_id=22");
|
||||
row=query("page",input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),30,30,true)).get(0);
|
||||
assertNull(row.get("physical"));decimal("300",row.get("w30_outbound"));
|
||||
}
|
||||
@Test void ownerAggregationAndUnknownPropagationUseFullCandidates() throws Exception {
|
||||
seed("A","SKU",30);seed("B","SKU",30);
|
||||
execute("UPDATE ll_selfop_inventory_snapshot SET physical_quantity=NULL,quality_status='UNKNOWN',quality_reason='FIELD_MISSING' WHERE customer_code='A' AND snapshot_run_id=22");
|
||||
var rows=query("page",input(request("OWNER","ALL","PHYSICAL_DESC",null,null,1,20),30,30,true));
|
||||
assertEquals(2,rows.size());assertEquals("B",rows.get(0).get("customer_code"));
|
||||
assertNull(rows.get(1).get("physical"));decimal("300",rows.get(1).get("w30_outbound"));
|
||||
decimal("0.6",rows.get(1).get("turnover"));
|
||||
}
|
||||
@Test void zeroAvailableNeverMatchesFastAndOverviewIgnoresSkuFocus() throws Exception {
|
||||
seed("A","SKU",0);
|
||||
execute("UPDATE ll_selfop_inventory_snapshot SET available_quantity=0 WHERE snapshot_run_id=22");
|
||||
var q=request("SKU","COVERAGE_LE","PHYSICAL_DESC","does-not-exist",60,1,20);
|
||||
var p=input(q,7,0,true);
|
||||
assertTrue(query("page",p).isEmpty());
|
||||
decimal("1",query("overview",p).get(0).get("sku_count"));
|
||||
var row=query("page",input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),7,0,true)).get(0);
|
||||
decimal("0",row.get("coverage_days"));decimal("1",row.get("unassessable"));
|
||||
}
|
||||
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++) {
|
||||
execute("INSERT INTO ll_selfop_sku_daily_sales VALUES(11,'demo','W','"+owner+"','"+sku+"',DATE '"+END.minusDays(i)+"',10,'PCS')");
|
||||
if(i<samples){profile(100+i,owner,sku);stock(100+i,owner,sku,0,450,450);stock(100+i,owner,sku,1,50,0);}
|
||||
}
|
||||
execute("INSERT INTO ll_selfop_sku_daily_sales VALUES(11,'demo','OTHER','"+owner+"','"+sku+"',DATE '"+END+"',999,'PCS')");
|
||||
}
|
||||
private void profile(long run,String owner,String sku) throws Exception {
|
||||
execute("INSERT INTO ll_selfop_sku_profile VALUES("+run+",'demo','W','"+owner+"','"+sku+"','Fixture','PCS','INVENTORY_OBSERVATION',1,TIMESTAMP '2026-01-01 00:00:00')");
|
||||
}
|
||||
private void stock(long run,String owner,String sku,int quality,int physical,int available) throws Exception {
|
||||
execute("INSERT INTO ll_selfop_inventory_snapshot VALUES("+run+",'demo','W','"+owner+"','"+sku+"',"+quality+","+physical+","+available+",0,'VALID',NULL)");
|
||||
}
|
||||
private void execute(String sql) throws Exception {try(var s=db.createStatement()){s.execute(sql);}}
|
||||
private List<Map<String,Object>> query(String method,InventoryTurnoverSql.Input p) throws Exception {
|
||||
var args=Map.of("p",p);var bound=config.getMappedStatement(InventoryTurnoverReadMapper.class.getName()+"."+method).getBoundSql(args);
|
||||
var meta=SystemMetaObject.forObject(args);String sql=bound.getSql();
|
||||
for(var parameter:bound.getParameterMappings()) {
|
||||
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));
|
||||
}
|
||||
// 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;
|
||||
while(true) {
|
||||
int as=sql.indexOf(" AS (",cursor);String name=sql.substring(cursor,as).trim();int start=as+5,end=start,depth=1;boolean quoted=false;
|
||||
while(depth>0){char ch=sql.charAt(end++);if(ch=='\''){if(quoted&&end<sql.length()&&sql.charAt(end)=='\''){end++;continue;}quoted=!quoted;}else if(!quoted){if(ch=='(')depth++;if(ch==')')depth--;}}
|
||||
execute("CREATE LOCAL TEMPORARY TABLE "+name+" AS "+sql.substring(start,end-1));relations.add(name);
|
||||
cursor=end;while(cursor<sql.length()&&Character.isWhitespace(sql.charAt(cursor)))cursor++;
|
||||
if(sql.charAt(cursor)!=',')break;cursor++;while(Character.isWhitespace(sql.charAt(cursor)))cursor++;
|
||||
}
|
||||
try(var s=db.createStatement();var rs=s.executeQuery(sql.substring(cursor))) {
|
||||
List<Map<String,Object>> rows=new ArrayList<>();
|
||||
while(rs.next()){Map<String,Object> row=new HashMap<>();for(int i=1;i<=rs.getMetaData().getColumnCount();i++)row.put(rs.getMetaData().getColumnLabel(i).toLowerCase(Locale.ROOT),rs.getObject(i));rows.add(row);}return rows;
|
||||
} finally {Collections.reverse(relations);for(String name:relations)execute("DROP TABLE "+name);}
|
||||
}
|
||||
private static void decimal(String expected,Object actual){assertNotNull(actual);assertEquals(0,new BigDecimal(expected).compareTo(new BigDecimal(actual.toString())));}
|
||||
}
|
||||
Reference in New Issue
Block a user