Skip to content

Commit 771266d

Browse files
authored
[IOTDB-17635] Fix incorrect ordering of multiple window RANK functions (#17706)
1 parent c1bbbd1 commit 771266d

5 files changed

Lines changed: 256 additions & 17 deletions

File tree

integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBWindowFunction3IT.java

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ public class IoTDBWindowFunction3IT {
5151
"insert into demo values (2021-01-01T09:10:00, 'd1', 1)",
5252
"insert into demo values (2021-01-01T09:08:00, 'd2', 2)",
5353
"insert into demo values (2021-01-01T09:15:00, 'd2', 4)",
54+
"create table stock_rank_cases (symbol string tag, amplitude float field, close_hfq float field, ma_3 float field, ma_5 float field, ma_13 float field, ma_64 float field, ma_120 float field)",
55+
"insert into stock_rank_cases values (2026-03-20T00:00:00.000+08:00, '600000', 0.023391813, 131.21382, 129.4694, 127.74202, 136.61397, 146.17342, 124.21007)",
56+
"insert into stock_rank_cases values (2026-03-27T00:00:00.000+08:00, '600000', 0.03696498, 127.89519, 130.02252, 128.07388, 134.97429, 146.16643, 124.58377)",
57+
"insert into stock_rank_cases values (2026-04-03T00:00:00.000+08:00, '600000', 0.045908183, 129.17159, 129.42686, 129.095, 133.40334, 146.23744, 124.978065)",
58+
"insert into stock_rank_cases values (2026-04-10T00:00:00.000+08:00, '600000', 0.03063241, 126.23586, 127.76755, 129.095, 130.89963, 146.24904, 125.36582)",
59+
"insert into stock_rank_cases values (2026-04-17T00:00:00.000+08:00, '600000', 0.03943377, 125.85294, 127.0868, 128.07388, 129.25014, 146.22755, 125.76035)",
60+
"insert into stock_rank_cases values (2026-04-24T00:00:00.000+08:00, '600000', 0.048681542, 120.61971, 124.236176, 125.955055, 127.688995, 146.10107, 126.11027)",
61+
"insert into stock_rank_cases values (2026-05-01T00:00:00.000+08:00, '600000', 0.026455026, 118.32219, 121.59828, 124.04046, 126.47151, 145.88837, 126.43706)",
62+
"insert into stock_rank_cases values (2026-05-08T00:00:00.000+08:00, '600000', 0.025889968, 115.769394, 118.2371, 121.36002, 125.51912, 145.69579, 126.736595)",
63+
"insert into stock_rank_cases values (2026-05-15T00:00:00.000+08:00, '600000', 0.033076074, 115.769394, 116.62032, 119.26673, 124.48818, 145.48964, 127.05406)",
64+
"create table stock_rank_date_cases (symbol string tag, amplitude float field, close_hfq float field, ma_5 float field, ma_64 float field, ma_120 float field)",
65+
"insert into stock_rank_date_cases values (2026-04-17, '600000', 0.03943377, 125.85294, 128.07388, 146.22755, 125.76035)",
66+
"insert into stock_rank_date_cases values (2026-04-24, '600000', 0.048681542, 120.61971, 125.955055, 146.10107, 126.11027)",
67+
"insert into stock_rank_date_cases values (2026-05-01, '600000', 0.026455026, 118.32219, 124.04046, 145.88837, 126.43706)",
68+
"insert into stock_rank_date_cases values (2026-05-08, '600000', 0.025889968, 115.769394, 121.36002, 145.69579, 126.736595)",
69+
"insert into stock_rank_date_cases values (2026-05-15, '600000', 0.033076074, 115.769394, 119.26673, 145.48964, 127.05406)",
5470
"FLUSH",
5571
"CLEAR ATTRIBUTE CACHE",
5672
};
@@ -68,7 +84,11 @@ protected static void insertData() {
6884

6985
@BeforeClass
7086
public static void setUp() {
71-
EnvFactory.getEnv().getConfig().getCommonConfig().setSortBufferSize(1024 * 1024);
87+
EnvFactory.getEnv()
88+
.getConfig()
89+
.getCommonConfig()
90+
.setSortBufferSize(128 * 1024)
91+
.setMaxTsBlockSizeInByte(4 * 1024);
7292
EnvFactory.getEnv().initClusterEnvironment();
7393
insertData();
7494
}
@@ -135,6 +155,153 @@ public void testSameWindowFunctionWithDifferentOrdering() {
135155
DATABASE_NAME);
136156
}
137157

158+
@Test
159+
public void testSameWindowFunctionWithDifferentOrderingWithoutPartition() {
160+
String[] expectedHeader =
161+
new String[] {"time", "device", "value", "rank_time_desc", "rank_value", "rank_time_asc"};
162+
String[] retArray =
163+
new String[] {
164+
"2021-01-01T09:05:00.000Z,d1,3.0,6,3,1,",
165+
"2021-01-01T09:07:00.000Z,d1,5.0,5,6,2,",
166+
"2021-01-01T09:08:00.000Z,d2,2.0,4,2,3,",
167+
"2021-01-01T09:09:00.000Z,d1,3.0,3,3,4,",
168+
"2021-01-01T09:10:00.000Z,d1,1.0,2,1,5,",
169+
"2021-01-01T09:15:00.000Z,d2,4.0,1,5,6,",
170+
};
171+
tableResultSetEqualTest(
172+
"SELECT *, rank() OVER (ORDER BY \"time\" DESC) AS rank_time_desc, rank() OVER (ORDER BY value) AS rank_value, rank() OVER (ORDER BY \"time\") AS rank_time_asc FROM demo ORDER BY \"time\"",
173+
expectedHeader,
174+
retArray,
175+
DATABASE_NAME);
176+
}
177+
178+
@Test
179+
public void testSameWindowFunctionWithDifferentFieldOrderingWithoutPartition() {
180+
String[] expectedHeader =
181+
new String[] {"time", "ma_3", "ma_13", "ma_120", "rank_m3", "rank_ma13", "rank_ma120"};
182+
String[] retArray =
183+
new String[] {
184+
"2026-03-19T16:00:00.000Z,129.4694,136.61397,124.21007,2,3,1,",
185+
"2026-03-26T16:00:00.000Z,130.02252,134.97429,124.58377,3,2,2,",
186+
"2026-04-02T16:00:00.000Z,129.42686,133.40334,124.978065,1,1,3,",
187+
};
188+
tableResultSetEqualTest(
189+
"SELECT \"time\", ma_3, ma_13, ma_120, rank() OVER (ORDER BY ma_3) AS rank_m3, rank() OVER (ORDER BY ma_13) AS rank_ma13, rank() OVER (ORDER BY ma_120) AS rank_ma120 FROM stock_rank_cases WHERE \"time\" >= 2026-03-20T00:00:00.000+08:00 AND \"time\" <= 2026-04-03T00:00:00.000+08:00 AND symbol = '600000' ORDER BY \"time\"",
190+
expectedHeader,
191+
retArray,
192+
DATABASE_NAME);
193+
}
194+
195+
@Test
196+
public void testSameWindowFunctionWithDifferentStockOrderingWithoutTimeRank() {
197+
String[] expectedHeader =
198+
new String[] {"time", "amplitude", "close_hfq", "rank_amplitude", "rank_close_hfq"};
199+
String[] retArray =
200+
new String[] {
201+
"2026-04-16T16:00:00.000Z,0.03943377,125.85294,2,1,",
202+
"2026-04-23T16:00:00.000Z,0.048681542,120.61971,1,2,",
203+
"2026-04-30T16:00:00.000Z,0.026455026,118.32219,4,3,",
204+
"2026-05-07T16:00:00.000Z,0.025889968,115.769394,5,4,",
205+
"2026-05-14T16:00:00.000Z,0.033076074,115.769394,3,4,",
206+
};
207+
tableResultSetEqualTest(
208+
"SELECT \"time\", amplitude, close_hfq, rank() OVER (ORDER BY amplitude DESC) AS rank_amplitude, rank() OVER (ORDER BY close_hfq DESC) AS rank_close_hfq FROM stock_rank_cases WHERE \"time\" >= 2026-04-11T00:00:00.000+08:00 AND symbol = '600000' ORDER BY \"time\"",
209+
expectedHeader,
210+
retArray,
211+
DATABASE_NAME);
212+
}
213+
214+
@Test
215+
public void testSameWindowFunctionWithDifferentStockOrderingAfterTimeRank() {
216+
String[] expectedHeader =
217+
new String[] {
218+
"time",
219+
"amplitude",
220+
"close_hfq",
221+
"rank_time_desc",
222+
"rank_amplitude",
223+
"rank_time_asc",
224+
"rank_close_hfq"
225+
};
226+
String[] retArray =
227+
new String[] {
228+
"2026-04-16T16:00:00.000Z,0.03943377,125.85294,5,4,1,5,",
229+
"2026-04-23T16:00:00.000Z,0.048681542,120.61971,4,5,2,4,",
230+
"2026-04-30T16:00:00.000Z,0.026455026,118.32219,3,2,3,3,",
231+
"2026-05-07T16:00:00.000Z,0.025889968,115.769394,2,1,4,1,",
232+
"2026-05-14T16:00:00.000Z,0.033076074,115.769394,1,3,5,1,",
233+
};
234+
tableResultSetEqualTest(
235+
"SELECT \"time\", amplitude, close_hfq, rank() OVER (ORDER BY \"time\" DESC) AS rank_time_desc, rank() OVER (ORDER BY amplitude) AS rank_amplitude, rank() OVER (ORDER BY \"time\") AS rank_time_asc, rank() OVER (ORDER BY close_hfq) AS rank_close_hfq FROM stock_rank_cases WHERE \"time\" >= 2026-04-11T00:00:00.000+08:00 AND symbol = '600000' ORDER BY \"time\"",
236+
expectedHeader,
237+
retArray,
238+
DATABASE_NAME);
239+
}
240+
241+
@Test
242+
public void testSameWindowFunctionWithOriginalStockOrderingAfterTimeRank() {
243+
String[] expectedHeader =
244+
new String[] {
245+
"time",
246+
"amplitude",
247+
"close_hfq",
248+
"ma_5",
249+
"ma_64",
250+
"ma_120",
251+
"rank_time",
252+
"rank_amplitude",
253+
"rank_close_hfq",
254+
"rank_ma5",
255+
"rank_ma64",
256+
"rank_ma120"
257+
};
258+
String[] retArray =
259+
new String[] {
260+
"2026-04-16T16:00:00.000Z,0.03943377,125.85294,128.07388,146.22755,125.76035,1,2,1,5,1,1,",
261+
"2026-04-23T16:00:00.000Z,0.048681542,120.61971,125.955055,146.10107,126.11027,2,1,2,4,2,2,",
262+
"2026-04-30T16:00:00.000Z,0.026455026,118.32219,124.04046,145.88837,126.43706,3,4,3,3,3,3,",
263+
"2026-05-07T16:00:00.000Z,0.025889968,115.769394,121.36002,145.69579,126.736595,4,5,4,2,4,4,",
264+
"2026-05-14T16:00:00.000Z,0.033076074,115.769394,119.26673,145.48964,127.05406,5,3,4,1,5,5,",
265+
};
266+
tableResultSetEqualTest(
267+
"SELECT \"time\", amplitude, close_hfq, ma_5, ma_64, ma_120, rank() OVER (ORDER BY \"time\") AS rank_time, rank() OVER (ORDER BY amplitude DESC) AS rank_amplitude, rank() OVER (ORDER BY close_hfq DESC) AS rank_close_hfq, rank() OVER (ORDER BY ma_5) AS rank_ma5, rank() OVER (ORDER BY ma_64 DESC) AS rank_ma64, rank() OVER (ORDER BY ma_120) AS rank_ma120 FROM stock_rank_cases WHERE \"time\" >= 2026-04-11T00:00:00.000+08:00 AND symbol = '600000' ORDER BY \"time\"",
268+
expectedHeader,
269+
retArray,
270+
DATABASE_NAME);
271+
}
272+
273+
@Test
274+
public void testSameWindowFunctionWithDateOnlyTimeAfterTimeRank() {
275+
String[] expectedHeader =
276+
new String[] {
277+
"time",
278+
"amplitude",
279+
"close_hfq",
280+
"ma_5",
281+
"ma_64",
282+
"ma_120",
283+
"rank_time",
284+
"rank_amplitude",
285+
"rank_close_hfq",
286+
"rank_ma5",
287+
"rank_ma64",
288+
"rank_ma120"
289+
};
290+
String[] retArray =
291+
new String[] {
292+
"2026-04-17T00:00:00.000Z,0.03943377,125.85294,128.07388,146.22755,125.76035,1,2,1,5,1,1,",
293+
"2026-04-24T00:00:00.000Z,0.048681542,120.61971,125.955055,146.10107,126.11027,2,1,2,4,2,2,",
294+
"2026-05-01T00:00:00.000Z,0.026455026,118.32219,124.04046,145.88837,126.43706,3,4,3,3,3,3,",
295+
"2026-05-08T00:00:00.000Z,0.025889968,115.769394,121.36002,145.69579,126.736595,4,5,4,2,4,4,",
296+
"2026-05-15T00:00:00.000Z,0.033076074,115.769394,119.26673,145.48964,127.05406,5,3,4,1,5,5,",
297+
};
298+
tableResultSetEqualTest(
299+
"SELECT \"time\", amplitude, close_hfq, ma_5, ma_64, ma_120, rank() OVER (ORDER BY \"time\") AS rank_time, rank() OVER (ORDER BY amplitude DESC) AS rank_amplitude, rank() OVER (ORDER BY close_hfq DESC) AS rank_close_hfq, rank() OVER (ORDER BY ma_5) AS rank_ma5, rank() OVER (ORDER BY ma_64 DESC) AS rank_ma64, rank() OVER (ORDER BY ma_120) AS rank_ma120 FROM stock_rank_date_cases WHERE \"time\" >= 2026-04-11 AND symbol = '600000' ORDER BY \"time\"",
300+
expectedHeader,
301+
retArray,
302+
DATABASE_NAME);
303+
}
304+
138305
@Test
139306
public void testPushDownFilterIntoWindow() {
140307
String[] expectedHeader = new String[] {"time", "device", "value", "rn"};

iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/GatherAndMergeWindows.java

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@
2525
import org.apache.iotdb.calc.plan.relational.utils.matching.PropertyPattern;
2626
import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNode;
2727
import org.apache.iotdb.commons.queryengine.plan.relational.planner.Assignments;
28+
import org.apache.iotdb.commons.queryengine.plan.relational.planner.DataOrganizationSpecification;
2829
import org.apache.iotdb.commons.queryengine.plan.relational.planner.OrderingScheme;
30+
import org.apache.iotdb.commons.queryengine.plan.relational.planner.SortOrder;
2931
import org.apache.iotdb.commons.queryengine.plan.relational.planner.Symbol;
32+
import org.apache.iotdb.commons.queryengine.plan.relational.planner.node.GroupNode;
3033
import org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ProjectNode;
3134
import org.apache.iotdb.commons.queryengine.plan.relational.planner.node.WindowNode;
3235
import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Expression;
@@ -38,7 +41,9 @@
3841
import com.google.common.collect.ImmutableSet;
3942
import com.google.common.collect.Maps;
4043

44+
import java.util.ArrayList;
4145
import java.util.Collection;
46+
import java.util.HashMap;
4247
import java.util.Iterator;
4348
import java.util.List;
4449
import java.util.Map;
@@ -50,6 +55,7 @@
5055
import static com.google.common.collect.ImmutableList.toImmutableList;
5156
import static com.google.common.collect.ImmutableSet.toImmutableSet;
5257
import static org.apache.iotdb.calc.plan.relational.utils.matching.Capture.newCapture;
58+
import static org.apache.iotdb.commons.queryengine.plan.relational.planner.SortOrder.ASC_NULLS_LAST;
5359
import static org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.Util.restrictOutputs;
5460
import static org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.Util.transpose;
5561
import static org.apache.iotdb.db.queryengine.plan.relational.planner.node.Patterns.groupNode;
@@ -224,7 +230,9 @@ public SwapAdjacentWindowsBySpecifications(int numProjects) {
224230
@Override
225231
protected Optional<PlanNode> manipulateAdjacentWindowNodes(
226232
WindowNode parent, WindowNode child, Context context) {
227-
if ((compare(parent, child) < 0) && (!dependsOn(parent, child))) {
233+
if ((compare(parent, child) < 0)
234+
&& (!dependsOn(parent, child))
235+
&& childInputSatisfies(parent, child)) {
228236
PlanNode transposedWindows = transpose(parent, child);
229237
return Optional.of(
230238
restrictOutputs(
@@ -236,6 +244,55 @@ protected Optional<PlanNode> manipulateAdjacentWindowNodes(
236244
return Optional.empty();
237245
}
238246

247+
private static boolean childInputSatisfies(WindowNode parent, WindowNode child) {
248+
return inputSatisfies(parent.getSpecification(), child.getChild());
249+
}
250+
251+
private static boolean inputSatisfies(
252+
DataOrganizationSpecification specification, PlanNode input) {
253+
if (input instanceof ProjectNode) {
254+
return inputSatisfies(specification, ((ProjectNode) input).getChild());
255+
}
256+
if (!(input instanceof GroupNode)) {
257+
return false;
258+
}
259+
return orderingSatisfies(specification, ((GroupNode) input).getOrderingScheme());
260+
}
261+
262+
private static boolean orderingSatisfies(
263+
DataOrganizationSpecification specification, OrderingScheme orderingScheme) {
264+
List<Symbol> requiredSymbols = new ArrayList<>();
265+
Map<Symbol, SortOrder> requiredOrderings = new HashMap<>();
266+
for (Symbol symbol : specification.getPartitionBy()) {
267+
requiredSymbols.add(symbol);
268+
requiredOrderings.put(symbol, ASC_NULLS_LAST);
269+
}
270+
specification
271+
.getOrderingScheme()
272+
.ifPresent(
273+
scheme -> {
274+
for (Symbol symbol : scheme.getOrderBy()) {
275+
if (!requiredOrderings.containsKey(symbol)) {
276+
requiredSymbols.add(symbol);
277+
requiredOrderings.put(symbol, scheme.getOrdering(symbol));
278+
}
279+
}
280+
});
281+
282+
if (requiredSymbols.size() > orderingScheme.getOrderBy().size()) {
283+
return false;
284+
}
285+
for (int i = 0; i < requiredSymbols.size(); i++) {
286+
Symbol requiredSymbol = requiredSymbols.get(i);
287+
Symbol actualSymbol = orderingScheme.getOrderBy().get(i);
288+
if (!requiredSymbol.equals(actualSymbol)
289+
|| requiredOrderings.get(requiredSymbol) != orderingScheme.getOrdering(actualSymbol)) {
290+
return false;
291+
}
292+
}
293+
return true;
294+
}
295+
239296
private static int compare(WindowNode o1, WindowNode o2) {
240297
int comparison = comparePartitionBy(o1, o2);
241298
if (comparison != 0) {

iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/ParallelizeGrouping.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@ private void checkPrefixMatch(Context context, List<Symbol> childOrder) {
122122

123123
@Override
124124
public PlanNode visitGroup(GroupNode node, Context context) {
125+
// A GroupNode without partition keys is a pure global ordering requirement.
126+
if (node.getPartitionKeyCount() == 0) {
127+
return new SortNode(
128+
node.getPlanNodeId(),
129+
node.getChild().accept(this, new Context(null, 0)),
130+
node.getOrderingScheme(),
131+
false,
132+
false);
133+
}
134+
125135
checkPrefixMatch(
126136
context, node.getOrderingScheme().getOrderBy().subList(0, node.getPartitionKeyCount()));
127137
Context newContext = new Context(node.getOrderingScheme(), node.getPartitionKeyCount());

iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/TableFunctionTest.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,8 @@ public void testForecastFunction() {
385385
anyTree(
386386
tableFunctionProcessor(
387387
tableFunctionMatcher,
388-
group(
388+
sort(
389389
ImmutableList.of(sort("time_0", ASCENDING, FIRST)),
390-
0,
391390
topK(
392391
1440,
393392
ImmutableList.of(sort("time_0", DESCENDING, LAST)),
@@ -398,15 +397,15 @@ public void testForecastFunction() {
398397
/*
399398
* └──OutputNode
400399
* └──TableFunctionProcessor
401-
* └──GroupNode
400+
* └──SortNode
402401
* └──TableScan
403402
*/
404403
assertPlan(
405404
planTester.getFragmentPlan(0),
406405
output(
407406
tableFunctionProcessor(
408407
tableFunctionMatcher,
409-
group(ImmutableList.of(sort("time_0", ASCENDING, FIRST)), 0, tableScan))));
408+
sort(ImmutableList.of(sort("time_0", ASCENDING, FIRST)), tableScan))));
410409
}
411410

412411
@Test
@@ -445,9 +444,8 @@ public void testForecastFunctionWithNoLowerCase() {
445444
anyTree(
446445
tableFunctionProcessor(
447446
tableFunctionMatcher,
448-
group(
447+
sort(
449448
ImmutableList.of(sort("time_0", ASCENDING, FIRST)),
450-
0,
451449
topK(
452450
1440,
453451
ImmutableList.of(sort("time_0", DESCENDING, LAST)),
@@ -458,15 +456,15 @@ public void testForecastFunctionWithNoLowerCase() {
458456
/*
459457
* └──OutputNode
460458
* └──TableFunctionProcessor
461-
* └──GroupNode
459+
* └──SortNode
462460
* └──TableScan
463461
*/
464462
assertPlan(
465463
planTester.getFragmentPlan(0),
466464
output(
467465
tableFunctionProcessor(
468466
tableFunctionMatcher,
469-
group(ImmutableList.of(sort("time_0", ASCENDING, FIRST)), 0, tableScan))));
467+
sort(ImmutableList.of(sort("time_0", ASCENDING, FIRST)), tableScan))));
470468
}
471469

472470
@Test

iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/WindowFunctionOptimizationTest.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,29 @@ public void testSwapWindowFunctions() {
9797
"SELECT sum(s1) OVER (PARTITION BY tag1, s1), min(s1) OVER (PARTITION BY tag1) FROM table1";
9898
LogicalQueryPlan logicalQueryPlan2 = planTester.createPlan(sql2);
9999

100-
// Two window functions have swapped. Since the initial sort by (tag1, s1) satisfies both
101-
// windows, no extra sort is needed between them.
100+
// The initial sort by (tag1, s1) satisfies both windows. The second window can therefore
101+
// reuse the grouping over tag1 without an extra sort.
102102
/*
103103
* └──OutputNode
104104
* └──ProjectNode
105-
* └──WindowNode(PARTITION BY tag1, s1)
106-
* └──WindowNode(PARTITION BY tag1)
107-
* └──SortNode
108-
* └──TableScanNode
105+
* └──WindowNode(PARTITION BY tag1)
106+
* └──GroupNode(PARTITION BY tag1)
107+
* └──WindowNode(PARTITION BY tag1, s1)
108+
* └──SortNode
109+
* └──TableScanNode
109110
*/
110111
assertPlan(
111112
logicalQueryPlan2,
112113
output(
113114
project(
114115
window(
115-
ImmutableList.of("tag1", "s1"), ImmutableList.of(), window(sort(tableScan))))));
116+
ImmutableList.of("tag1"),
117+
ImmutableList.of(),
118+
group(
119+
window(
120+
ImmutableList.of("tag1", "s1"),
121+
ImmutableList.of(),
122+
sort(tableScan)))))));
116123
}
117124

118125
@Test

0 commit comments

Comments
 (0)