This is an automated email from the ASF dual-hosted git repository.

JackieTien97 pushed a commit to branch ty/fix-pattern-match-empty-groups
in repository https://gitbox.apache.org/repos/asf/iotdb.git

commit db245388a8ae6dee60aa7ad83e4c284c69937649
Author: JackieTien97 <[email protected]>
AuthorDate: Wed Oct 21 16:47:43 2026 +0800

    [Bug] Reject empty repetition groups in pattern_match
---
 .../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) {

Reply via email to