This is an automated email from the ASF dual-hosted git repository.
JackieTien97 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new cef612edb89 [Bug] Reject empty repetition groups in pattern_match
(#18961)
cef612edb89 is described below
commit cef612edb89a28d7cf64444d7acf2c4b21d7d7d3
Author: Jackie Tien <[email protected]>
AuthorDate: Wed Oct 21 19:30:08 2026 +0800
[Bug] Reject empty repetition groups in pattern_match (#18961)
---
.../relational/it/db/it/IoTDBWindowTVFIT.java | 46 +++++++++
.../apache/iotdb/commons/i18n/QueryMessages.java | 6 ++
.../apache/iotdb/commons/i18n/QueryMessages.java | 6 ++
.../function/tvf/PatternMatchTableFunction.java | 9 +-
.../function/tvf/match/QetchAlgorithm.java | 57 ++++++++++-
.../tvf/PatternMatchTableFunctionTest.java | 112 +++++++++++++++++++--
6 files changed, 223 insertions(+), 13 deletions(-)
diff --git
a/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBWindowTVFIT.java
b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBWindowTVFIT.java
index b2ed0576891..22d099e5bae 100644
---
a/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBWindowTVFIT.java
+++
b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBWindowTVFIT.java
@@ -23,6 +23,7 @@ import org.apache.iotdb.it.env.EnvFactory;
import org.apache.iotdb.it.framework.IoTDBTestRunner;
import org.apache.iotdb.itbase.category.TableClusterIT;
import org.apache.iotdb.itbase.category.TableLocalStandaloneIT;
+import org.apache.iotdb.rpc.TSStatusCode;
import org.junit.AfterClass;
import org.junit.BeforeClass;
@@ -43,6 +44,8 @@ import java.util.List;
import static org.apache.iotdb.db.it.utils.TestUtils.*;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@RunWith(IoTDBTestRunner.class)
@@ -1078,6 +1081,49 @@ public class IoTDBWindowTVFIT {
DATABASE_NAME);
}
+ @Test
+ public void testPatternMatchEmptyRepetitionGroups() throws SQLException {
+ try (Connection connection = EnvFactory.getEnv().getTableConnection();
+ Statement statement = connection.createStatement()) {
+ statement.execute("USE " + DATABASE_NAME);
+ for (String data : new String[] {"t1", "(SELECT * FROM t1 WHERE time <
0)"}) {
+ for (String pattern :
+ new String[] {
+ "1,{,}+,1",
+ "1,{}+,1",
+ "1,{ , , }*,1",
+ "{,}+,1,2",
+ "1,2,{,}+",
+ "1,{2,{,}+,3}+,1",
+ "1,{{,}*}+,1"
+ }) {
+ SQLException exception =
+ assertThrows(
+ SQLException.class,
+ () ->
+ statement.execute(
+ "SELECT * FROM pattern_match(data => "
+ + data
+ + " ORDER BY time, data_col => 'value', pattern
=> '"
+ + pattern
+ + "', smooth => 0.0, threshold => 100.0)"));
+ assertEquals(
+ exception.getMessage(),
+ TSStatusCode.SEMANTIC_ERROR.getStatusCode(),
+ exception.getErrorCode());
+ assertTrue(
+ exception.getMessage(),
+ exception
+ .getMessage()
+ .contains(
+ "Invalid pattern: '"
+ + pattern
+ + "'. Repetition groups must contain at least one
numeric data point."));
+ }
+ }
+ }
+ }
+
@Test
public void testPatternMatchPartitions() {
assertPatternMatchPartitions("1,2,3,2,1");
diff --git
a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/QueryMessages.java
b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/QueryMessages.java
index 68f680ab4aa..6f8fe1f305f 100644
---
a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/QueryMessages.java
+++
b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/QueryMessages.java
@@ -165,6 +165,12 @@ public final class QueryMessages {
"Invalid pattern: %s, missing repeat sign after '}'";
public static final String INVALID_PATTERN_MUST_CONTAIN_DATA_POINTS =
"Invalid pattern: '%s'. Pattern must contain at least two numeric data
points separated by commas, e.g., '1,2,3'";
+ public static final String
EXCEPTION_INVALID_PATTERN_ARG_REPETITION_GROUPS_MUST_CONTAIN_AT_LEAST_ONE_NUMERIC_DATA_POINT_E1880CCB
=
+ "Invalid pattern: '%s'. Repetition groups must contain at least one
numeric data point.";
+ public static final String
EXCEPTION_INVALID_PATTERN_ARG_UNMATCHED_OR_9B5E4AE4 =
+ "Invalid pattern: '%s'. Unmatched '{' or '}'.";
+ public static final String
EXCEPTION_INVALID_PATTERN_ARG_INVALID_NUMERIC_DATA_POINT_ARG_12165323 =
+ "Invalid pattern: '%s'. Invalid numeric data point: '%s'.";
// ======================== Forecast / Classify TVF ========================
diff --git
a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/QueryMessages.java
b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/QueryMessages.java
index a2e059c5e7b..19c5d8db981 100644
---
a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/QueryMessages.java
+++
b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/QueryMessages.java
@@ -165,6 +165,12 @@ public final class QueryMessages {
"无效的模式:%s,'}' 后缺少重复符号";
public static final String INVALID_PATTERN_MUST_CONTAIN_DATA_POINTS =
"无效的模式:'%s'。模式必须包含至少两个以逗号分隔的数值数据点,例如 '1,2,3'";
+ public static final String
EXCEPTION_INVALID_PATTERN_ARG_REPETITION_GROUPS_MUST_CONTAIN_AT_LEAST_ONE_NUMERIC_DATA_POINT_E1880CCB
=
+ "无效的模式:'%s'。重复组必须包含至少一个数值数据点。";
+ public static final String
EXCEPTION_INVALID_PATTERN_ARG_UNMATCHED_OR_9B5E4AE4 =
+ "无效的模式:'%s'。'{' 或 '}' 不匹配。";
+ public static final String
EXCEPTION_INVALID_PATTERN_ARG_INVALID_NUMERIC_DATA_POINT_ARG_12165323 =
+ "无效的模式:'%s'。无效的数值数据点:'%s'。";
// ======================== Forecast / Classify TVF ========================
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/function/tvf/PatternMatchTableFunction.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/function/tvf/PatternMatchTableFunction.java
index 83bd76b4ab7..cf69570d4ab 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/function/tvf/PatternMatchTableFunction.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/function/tvf/PatternMatchTableFunction.java
@@ -124,6 +124,13 @@ public class PatternMatchTableFunction implements
TableFunction {
throw new
UDFException(String.format(QueryMessages.HEIGHT_MUST_BE_NON_NEGATIVE,
heightValue));
}
+ String pattern = (String) ((ScalarArgument)
arguments.get(PATTERN_PARAM)).getValue();
+ try {
+ QetchAlgorithm.validatePattern(pattern);
+ } catch (IllegalArgumentException e) {
+ throw new UDFException(e.getMessage(), e);
+ }
+
// outputColumnSchema description
DescribedSchema properColumnSchema =
new DescribedSchema.Builder()
@@ -134,7 +141,7 @@ public class PatternMatchTableFunction implements
TableFunction {
// this is for transferring the parameters to the processor
MapTableFunctionHandle handle =
new MapTableFunctionHandle.Builder()
- .addProperty(PATTERN_PARAM, ((ScalarArgument)
arguments.get(PATTERN_PARAM)).getValue())
+ .addProperty(PATTERN_PARAM, pattern)
.addProperty(SMOOTH_PARAM, ((ScalarArgument)
arguments.get(SMOOTH_PARAM)).getValue())
.addProperty(
THRESHOLD_PARAM, ((ScalarArgument)
arguments.get(THRESHOLD_PARAM)).getValue())
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/function/tvf/match/QetchAlgorithm.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/function/tvf/match/QetchAlgorithm.java
index ac52240c0f7..d0ef585197a 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/function/tvf/match/QetchAlgorithm.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/function/tvf/match/QetchAlgorithm.java
@@ -75,7 +75,15 @@ public class QetchAlgorithm {
public QetchAlgorithm() {}
- private List<PatternSegment> parsePattern2DataSegment(String pattern) {
+ public static void validatePattern(String pattern) {
+ parseAndValidatePattern(pattern);
+ }
+
+ private static List<String> parseAndValidatePattern(String pattern) {
+ if (pattern == null) {
+ throw new IllegalArgumentException(
+
String.format(QueryMessages.INVALID_PATTERN_MUST_CONTAIN_DATA_POINTS, pattern));
+ }
// this pattern is divided by ",", such as "{,1,2,1,},3,6,9"
// "()" claim as a point while "{}" claim as a repeat regex sign "+" which
is supported to nest
@@ -114,6 +122,53 @@ public class QetchAlgorithm {
}
}
+ int pointCount = 0;
+ Deque<Integer> groupStarts = new ArrayDeque<>();
+ for (String piece : patternPieces) {
+ if (piece.equals("{")) {
+ groupStarts.push(pointCount);
+ } else if (piece.equals("}*") || piece.equals("}+")) {
+ if (groupStarts.isEmpty()) {
+ throw new IllegalArgumentException(
+ String.format(
+
QueryMessages.EXCEPTION_INVALID_PATTERN_ARG_UNMATCHED_OR_9B5E4AE4, pattern));
+ }
+ if (groupStarts.pop() == pointCount) {
+ throw new IllegalArgumentException(
+ String.format(
+ QueryMessages
+
.EXCEPTION_INVALID_PATTERN_ARG_REPETITION_GROUPS_MUST_CONTAIN_AT_LEAST_ONE_NUMERIC_DATA_POINT_E1880CCB,
+ pattern));
+ }
+ } else {
+ try {
+ Double.parseDouble(piece);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ String.format(
+ QueryMessages
+
.EXCEPTION_INVALID_PATTERN_ARG_INVALID_NUMERIC_DATA_POINT_ARG_12165323,
+ pattern,
+ piece),
+ e);
+ }
+ pointCount++;
+ }
+ }
+ if (!groupStarts.isEmpty()) {
+ throw new IllegalArgumentException(
+ String.format(
+
QueryMessages.EXCEPTION_INVALID_PATTERN_ARG_UNMATCHED_OR_9B5E4AE4, pattern));
+ }
+ if (pointCount < 2) {
+ throw new IllegalArgumentException(
+
String.format(QueryMessages.INVALID_PATTERN_MUST_CONTAIN_DATA_POINTS, pattern));
+ }
+ return patternPieces;
+ }
+
+ private List<PatternSegment> parsePattern2DataSegment(String pattern) {
+ List<String> patternPieces = parseAndValidatePattern(pattern);
// classify the Pieces to different dataSegment
List<PatternSegment> patternSegments = new ArrayList<>();
PatternSegment patternSegment = null;
diff --git
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/queryengine/plan/relational/function/tvf/PatternMatchTableFunctionTest.java
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/queryengine/plan/relational/function/tvf/PatternMatchTableFunctionTest.java
index dcb7467c370..6dd55366ec1 100644
---
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/queryengine/plan/relational/function/tvf/PatternMatchTableFunctionTest.java
+++
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/queryengine/plan/relational/function/tvf/PatternMatchTableFunctionTest.java
@@ -19,10 +19,16 @@
package org.apache.iotdb.commons.queryengine.plan.relational.function.tvf;
+import org.apache.iotdb.commons.i18n.QueryMessages;
+import
org.apache.iotdb.commons.queryengine.plan.relational.function.tvf.match.QetchAlgorithm;
+import org.apache.iotdb.udf.api.exception.UDFException;
import org.apache.iotdb.udf.api.relational.access.Record;
-import org.apache.iotdb.udf.api.relational.table.MapTableFunctionHandle;
import
org.apache.iotdb.udf.api.relational.table.TableFunctionProcessorProvider;
+import org.apache.iotdb.udf.api.relational.table.argument.Argument;
+import org.apache.iotdb.udf.api.relational.table.argument.ScalarArgument;
+import org.apache.iotdb.udf.api.relational.table.argument.TableArgument;
import
org.apache.iotdb.udf.api.relational.table.processor.TableFunctionDataProcessor;
+import org.apache.iotdb.udf.api.type.Type;
import org.apache.tsfile.block.column.Column;
import org.apache.tsfile.block.column.ColumnBuilder;
@@ -32,14 +38,84 @@ import
org.apache.tsfile.read.common.block.column.LongColumnBuilder;
import org.junit.Test;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class PatternMatchTableFunctionTest {
+ @Test
+ public void testRejectEmptyRepetitionGroups() {
+ for (String pattern :
+ new String[] {
+ "1,{,}+,1",
+ "1,{}+,1",
+ "1,{ , , }*,1",
+ "{,}+,1,2",
+ "1,2,{,}+",
+ "1,{2,{,}+,3}+,1",
+ "1,{{,}*}+,1"
+ }) {
+ String expectedMessage =
+ String.format(
+ QueryMessages
+
.EXCEPTION_INVALID_PATTERN_ARG_REPETITION_GROUPS_MUST_CONTAIN_AT_LEAST_ONE_NUMERIC_DATA_POINT_E1880CCB,
+ pattern);
+ assertInvalidPattern(pattern, expectedMessage);
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new QetchAlgorithm().parsePattern2Automaton(pattern));
+ assertEquals(expectedMessage, exception.getMessage());
+ }
+ }
+
+ @Test
+ public void testRejectMalformedPatternsDuringAnalysis() {
+ for (String pattern : new String[] {null, "", "1", "abc,def,ghi"}) {
+ assertInvalidPattern(
+ pattern,
String.format(QueryMessages.INVALID_PATTERN_MUST_CONTAIN_DATA_POINTS, pattern));
+ }
+ for (String pattern : new String[] {"1,{2,3,1", "1,2,3}+,1"}) {
+ assertInvalidPattern(
+ pattern,
+ String.format(
+
QueryMessages.EXCEPTION_INVALID_PATTERN_ARG_UNMATCHED_OR_9B5E4AE4, pattern));
+ }
+ assertInvalidPattern(
+ "1,{2,3},1",
String.format(QueryMessages.INVALID_PATTERN_MISSING_REPEAT_SIGN, "1,{2,3},1"));
+ assertInvalidPattern(
+ "1,--2,3",
+ String.format(
+
QueryMessages.EXCEPTION_INVALID_PATTERN_ARG_INVALID_NUMERIC_DATA_POINT_ARG_12165323,
+ "1,--2,3",
+ "--2"));
+ }
+
+ @Test
+ public void testAcceptNonEmptyRepetitionGroups() {
+ for (String pattern :
+ new String[] {"1,{2,3,2}+,1", "1,{,2,3,2,}*,1", "1,{{2,3,2}+}+,1",
"1,{2,{3,2}+,1}+,1"}) {
+ assertNotNull(createProvider(pattern).getDataProcessor());
+ }
+ }
+
+ private void assertInvalidPattern(String pattern, String expectedMessage) {
+ UDFException exception =
+ assertThrows(
+ UDFException.class,
+ () -> new
PatternMatchTableFunction().analyze(createArguments(pattern)));
+ assertEquals(expectedMessage, exception.getMessage());
+ }
+
@Test
public void testPartitionStateIsIsolated() {
assertPartitionStateIsIsolated("1,2,3,2,1");
@@ -122,16 +198,30 @@ public class PatternMatchTableFunctionTest {
}
private TableFunctionProcessorProvider createProvider(String pattern) {
- MapTableFunctionHandle handle =
- new MapTableFunctionHandle.Builder()
- .addProperty("PATTERN", pattern)
- .addProperty("SMOOTH", 0.0)
- .addProperty("THRESHOLD", 100.0)
- .addProperty("WIDTH", 1000.0)
- .addProperty("HEIGHT", 1000.0)
- .addProperty("SMOOTH_ON_PATTERN", false)
- .build();
- return new PatternMatchTableFunction().getProcessorProvider(handle);
+ PatternMatchTableFunction function = new PatternMatchTableFunction();
+ return function.getProcessorProvider(
+ function.analyze(createArguments(pattern)).getTableFunctionHandle());
+ }
+
+ private Map<String, Argument> createArguments(String pattern) {
+ Map<String, Argument> arguments = new HashMap<>();
+ arguments.put(
+ "DATA",
+ new TableArgument(
+ Arrays.asList(Optional.of("time"), Optional.of("value")),
+ Arrays.asList(Type.TIMESTAMP, Type.DOUBLE),
+ Collections.emptyList(),
+ Collections.singletonList("time"),
+ false));
+ arguments.put("TIME_COL", new ScalarArgument(Type.STRING, "time"));
+ arguments.put("DATA_COL", new ScalarArgument(Type.STRING, "value"));
+ arguments.put("PATTERN", new ScalarArgument(Type.STRING, pattern));
+ arguments.put("SMOOTH", new ScalarArgument(Type.DOUBLE, 0.0));
+ arguments.put("THRESHOLD", new ScalarArgument(Type.DOUBLE, 100.0));
+ arguments.put("WIDTH", new ScalarArgument(Type.DOUBLE, 1000.0));
+ arguments.put("HEIGHT", new ScalarArgument(Type.DOUBLE, 1000.0));
+ arguments.put("SMOOTH_ON_PATTERN", new ScalarArgument(Type.BOOLEAN,
false));
+ return arguments;
}
private Column[] processPartition(TableFunctionDataProcessor processor,
double[] values) {