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) {
