Skip to content

Commit 6ee894a

Browse files
committed
Support NULL time in First/Last/FirstBy/LastBy aggregations
1 parent f198b8e commit 6ee894a

22 files changed

+3627
-1961
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.iotdb.db.it.query;
20+
21+
import org.apache.iotdb.it.env.EnvFactory;
22+
import org.apache.iotdb.it.framework.IoTDBTestRunner;
23+
import org.apache.iotdb.itbase.category.TableClusterIT;
24+
import org.apache.iotdb.itbase.category.TableLocalStandaloneIT;
25+
26+
import org.junit.AfterClass;
27+
import org.junit.BeforeClass;
28+
import org.junit.Test;
29+
import org.junit.experimental.categories.Category;
30+
import org.junit.runner.RunWith;
31+
32+
import static org.apache.iotdb.db.it.utils.TestUtils.prepareTableData;
33+
import static org.apache.iotdb.db.it.utils.TestUtils.tableResultSetEqualTest;
34+
35+
@RunWith(IoTDBTestRunner.class)
36+
@Category({TableLocalStandaloneIT.class, TableClusterIT.class})
37+
public class IoTDBAggregationFirstByIT {
38+
private static final String DATABASE_NAME = "test_first_by_agg";
39+
40+
private static final String[] createSqls =
41+
new String[] {
42+
"CREATE DATABASE " + DATABASE_NAME,
43+
"USE " + DATABASE_NAME,
44+
"CREATE TABLE table_a("
45+
+ "device STRING TAG, "
46+
+ "s_int INT32 FIELD, "
47+
+ "s_long INT64 FIELD, "
48+
+ "s_float FLOAT FIELD, "
49+
+ "s_double DOUBLE FIELD, "
50+
+ "s_bool BOOLEAN FIELD, "
51+
+ "s_string STRING FIELD, "
52+
+ "time_type TIMESTAMP FIELD, "
53+
+ "y_criteria INT32 FIELD)", // Acts as s2
54+
"CREATE TABLE table_b(" + "time TIMESTAMP TIME, " + "s_back INT32 FIELD)",
55+
56+
// Batch Insert
57+
"INSERT INTO table_a(time, device, s_int, s_long, s_float, s_double, s_bool, s_string, time_type, y_criteria) VALUES "
58+
59+
// Case 1: s2 (y_criteria) has NO NULLs in Valid Times.
60+
// Device: d1
61+
+ "(1, 'd1', 1, 1, 1.0, 1.0, true, '1s', NULL, 1),"
62+
+ "(200, 'd1', 200, 200, 200.0, 200.0, true, '200s', 200, 200),"
63+
+ "(100, 'd1', 100, 100, 100.0, 100.0, true, '100s', 100, 100),"
64+
+ "(10, 'd1', 10, 10, 10.0, 10.0, true, '10s', 10, 10),"
65+
+ "(5, 'd1', 5, 5, 5.0, 5.0, false, '5s', 5, 5)," // Target
66+
67+
// Case 2: s2 (y_criteria) has NULLs
68+
// Device: d2
69+
+ "(2, 'd2', 2, 2, 2.0, 2.0, true, '2s', NULL, 2),"
70+
+ "(5, 'd2', 5, 5, 5.0, 5.0, false, '5s', 5, NULL),"
71+
+ "(8, 'd2', 8, 8, 8.0, 8.0, false, '8s', 8, NULL),"
72+
+ "(10, 'd2', 10, 10, 10.0, 10.0, true, '10s', 10, 10)," // Target
73+
+ "(20, 'd2', 20, 20, 20.0, 20.0, true, '20s', 20, 20),"
74+
75+
// Case 3: s1 (value) has NULLs.
76+
// Device: d3
77+
+ "(3, 'd3', 3, 3, 3.0, 3.0, true, '3s', NULL, 3),"
78+
+ "(5, 'd3', 5, 5, NULL, NULL, NULL, NULL, 5, 5)," // Target
79+
+ "(10, 'd3', 10, 10, 10.0, 10.0, true, '10s', 10, NULL),"
80+
+ "(20, 'd3', 20, 20, 20.0, 20.0, true, '20s', 20, 20),"
81+
82+
// Case 4: s2 (y_criteria) is ALL NULLs.
83+
// Device: d4
84+
+ "(4, 'd4', 66, 66, 66.0, 66.0, true, '66s', NULL, NULL),"
85+
+ "(5, 'd4', 5, 5, 5.0, 5.0, false, '5s', 5, NULL),"
86+
+ "(10, 'd4', 10, 10, 10.0, 10.0, true, '10s', 10, NULL),"
87+
+ "(20, 'd4', 20, 20, 20.0, 20.0, true, '20s', 20, NULL),"
88+
89+
// Case 5: All time_type are NULL.
90+
// Device: d5
91+
+ "(1, 'd5', 10, 10, 10.0, 10.0, true, '10s', NULL, NULL),"
92+
+ "(2, 'd5', 50, 50, 50.0, 50.0, false, '50s', NULL, 50)" // target
93+
};
94+
95+
@BeforeClass
96+
public static void setUp() throws Exception {
97+
EnvFactory.getEnv().initClusterEnvironment();
98+
prepareTableData(createSqls);
99+
}
100+
101+
@AfterClass
102+
public static void tearDown() throws Exception {
103+
EnvFactory.getEnv().cleanClusterEnvironment();
104+
}
105+
106+
@Test
107+
public void testFirstBy_d1_NoNulls() {
108+
String[] expectedHeader = {"_col0", "_col1", "_col2", "_col3", "_col4", "_col5"};
109+
String[] retArray = {"5,5,5.0,5.0,false,5s,"};
110+
runTest("d1", expectedHeader, retArray);
111+
}
112+
113+
@Test
114+
public void testFirstBy_d2_ForwardTracking() {
115+
String[] expectedHeader = {"_col0", "_col1", "_col2", "_col3", "_col4", "_col5"};
116+
String[] retArray = {"10,10,10.0,10.0,true,10s,"};
117+
runTest("d2", expectedHeader, retArray);
118+
}
119+
120+
@Test
121+
public void testFirstBy_d3_TargetNull() {
122+
String[] expectedHeader = {"_col0", "_col1", "_col2", "_col3", "_col4", "_col5"};
123+
String[] retArray = {"5,5,null,null,null,null,"};
124+
runTest("d3", expectedHeader, retArray);
125+
}
126+
127+
@Test
128+
public void testFirstBy_d4_AllNullCriteria() {
129+
String[] expectedHeader = {"_col0", "_col1", "_col2", "_col3", "_col4", "_col5"};
130+
// Expected: No valid s2 found.
131+
String[] retArray = {"null,null,null,null,null,null,"};
132+
runTest("d4", expectedHeader, retArray);
133+
}
134+
135+
@Test
136+
public void testFirstBy_d5_AllTimeNull() {
137+
String[] expectedHeader = {"_col0", "_col1", "_col2", "_col3", "_col4", "_col5"};
138+
// Expected: The row with y_criteria=NULL is skipped. The row with y_criteria=50 is picked.
139+
String[] retArray = {"50,50,50.0,50.0,false,50s,"};
140+
runTest("d5", expectedHeader, retArray);
141+
}
142+
143+
private void runTest(String deviceId, String[] expectedHeader, String[] retArray) {
144+
tableResultSetEqualTest(
145+
"select "
146+
+ "first_by(s_int, y_criteria, time), "
147+
+ "first_by(s_long, y_criteria, time), "
148+
+ "first_by(s_float, y_criteria, time), "
149+
+ "first_by(s_double, y_criteria, time), "
150+
+ "first_by(s_bool, y_criteria, time), "
151+
+ "first_by(s_string, y_criteria, time) "
152+
+ "from "
153+
+ "(select time_type as time, s_int, s_long, s_float, s_double, s_bool, s_string, y_criteria "
154+
+ "from table_a left join table_b on table_a.time=table_b.time "
155+
+ "where table_a.device='"
156+
+ deviceId
157+
+ "') ",
158+
expectedHeader,
159+
retArray,
160+
DATABASE_NAME);
161+
}
162+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.iotdb.db.it.query;
21+
22+
import org.apache.iotdb.it.env.EnvFactory;
23+
import org.apache.iotdb.it.framework.IoTDBTestRunner;
24+
import org.apache.iotdb.itbase.category.TableClusterIT;
25+
import org.apache.iotdb.itbase.category.TableLocalStandaloneIT;
26+
27+
import org.junit.AfterClass;
28+
import org.junit.BeforeClass;
29+
import org.junit.Test;
30+
import org.junit.experimental.categories.Category;
31+
import org.junit.runner.RunWith;
32+
33+
import static org.apache.iotdb.db.it.utils.TestUtils.prepareTableData;
34+
import static org.apache.iotdb.db.it.utils.TestUtils.tableResultSetEqualTest;
35+
36+
@RunWith(IoTDBTestRunner.class)
37+
@Category({TableLocalStandaloneIT.class, TableClusterIT.class})
38+
public class IoTDBAggregationFirstByInGroupIT {
39+
40+
private static final String DATABASE_NAME = "test_first_by_in_group_agg";
41+
42+
private static final String[] createSqls =
43+
new String[] {
44+
"CREATE DATABASE " + DATABASE_NAME,
45+
"USE " + DATABASE_NAME,
46+
"CREATE TABLE table_a("
47+
+ "s_int INT32 FIELD, "
48+
+ "s_long INT64 FIELD, "
49+
+ "s_float FLOAT FIELD, "
50+
+ "s_double DOUBLE FIELD, "
51+
+ "s_bool BOOLEAN FIELD, "
52+
+ "s_string STRING FIELD, "
53+
+ "time_type TIMESTAMP FIELD, "
54+
+ "y_criteria INT32 FIELD, " // Acts as s2
55+
+ "partition STRING FIELD)",
56+
"CREATE TABLE table_b(" + "time TIMESTAMP TIME, " + "s_back INT32 FIELD)",
57+
58+
// Batch Insert: 5 Partitions (p1-p5)
59+
"INSERT INTO table_a(time, s_int, s_long, s_float, s_double, s_bool, s_string, time_type, y_criteria, partition) VALUES "
60+
61+
// Partition p1: s2 has NO NULLs in Valid Times.
62+
+ "(1, 99, 99, 99.0, 99.0, true, '99s', NULL, 99, 'p1'),"
63+
+ "(2, 200, 200, 200.0, 200.0, true, '200s', 200, 200, 'p1'),"
64+
+ "(3, 100, 100, 100.0, 100.0, true, '100s', 100, 100, 'p1'),"
65+
+ "(4, 10, 10, 10.0, 10.0, true, '10s', 10, 10, 'p1'),"
66+
+ "(5, 5, 5, 5.0, 5.0, false, '5s', 5, 5, 'p1')," // Target
67+
68+
// Partition p2: s2 has NULLs
69+
+ "(6, 88, 88, 88.0, 88.0, true, '88s', NULL, 88, 'p2'),"
70+
+ "(7, 5, 5, 5.0, 5.0, false, '5s', 5, NULL,'p2'),"
71+
+ "(8, 8, 8, 8.0, 8.0, false, '8s', 8, NULL,'p2'),"
72+
+ "(9, 10, 10, 10.0, 10.0, true, '10s', 10, 10, 'p2')," // Target
73+
+ "(10, 20, 20, 20.0, 20.0, true, '20s', 20, 20, 'p2'),"
74+
75+
// Partition p3: s1 (value) has NULLs.
76+
+ "(11, 77, 77, 77.0, 77.0, true, '77s', NULL, 77, 'p3'),"
77+
+ "(12, NULL, NULL, 5.0, 5.0, NULL, NULL, 5, 5, 'p3')," // Target
78+
// (Values null)
79+
+ "(13, 10, 10, 10.0, 10.0, true, '10s', 10, NULL,'p3'),"
80+
+ "(14, 20, 20, 20.0, 20.0, true, '20s', 20, 20, 'p3'),"
81+
82+
// Partition p4: s2 is ALL NULLs.
83+
// Logic: No row satisfies the criteria. Result is NULL.
84+
+ "(15, 66, 66, 66.0, 66.0, true, '66s', NULL, NULL,'p4'),"
85+
+ "(16, 5, 5, 5.0, 5.0, false, '5s', 5, NULL,'p4'),"
86+
+ "(17, 10, 10, 10.0, 10.0, true, '10s', 10, NULL,'p4'),"
87+
+ "(18, 20, 20, 20.0, 20.0, true, '20s', 20, NULL,'p4'),"
88+
89+
// Partition p5: All time_type are NULL.
90+
+ "(19, 10, 10, 10.0, 10.0, true, '10s', NULL, NULL,'p5'),"
91+
+ "(20, 50, 50, 50.0, 50.0, false, '50s', NULL, 50, 'p5')" // Target
92+
};
93+
94+
@BeforeClass
95+
public static void setUp() throws Exception {
96+
EnvFactory.getEnv().initClusterEnvironment();
97+
prepareTableData(createSqls);
98+
}
99+
100+
@AfterClass
101+
public static void tearDown() throws Exception {
102+
EnvFactory.getEnv().cleanClusterEnvironment();
103+
}
104+
105+
@Test
106+
public void testGroupedFirstByAggregation() {
107+
108+
// Expected Header: partition column + 6 aggregation results
109+
String[] expectedHeader = {"partition", "_col1", "_col2", "_col3", "_col4", "_col5", "_col6"};
110+
111+
// Expected Results:
112+
String[] retArray = {
113+
"p1,5,5,5.0,5.0,false,5s,",
114+
"p2,10,10,10.0,10.0,true,10s,",
115+
"p3,null,null,5.0,5.0,null,null,",
116+
"p4,null,null,null,null,null,null,",
117+
"p5,50,50,50.0,50.0,false,50s,"
118+
};
119+
120+
tableResultSetEqualTest(
121+
"select "
122+
+ "partition, "
123+
+ "first_by(s_int, y_criteria, time), "
124+
+ "first_by(s_long, y_criteria, time), "
125+
+ "first_by(s_float, y_criteria, time), "
126+
+ "first_by(s_double, y_criteria, time), "
127+
+ "first_by(s_bool, y_criteria, time), "
128+
+ "first_by(s_string, y_criteria, time) "
129+
+ "from "
130+
// SubQuery: Rename time_type to 'ts' to avoid ambiguity with physical 'time'
131+
+ "(select time_type as time, s_int, s_long, s_float, s_double, s_bool, s_string, y_criteria, partition "
132+
+ "from table_a left join table_b on table_a.time=table_b.time) "
133+
+ "group by partition "
134+
+ "order by partition",
135+
expectedHeader,
136+
retArray,
137+
DATABASE_NAME);
138+
}
139+
}

0 commit comments

Comments
 (0)