diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/pattern/TreePatternPruningTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/pattern/TreePatternPruningTest.java new file mode 100644 index 000000000000..23b6c334b5cc --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/pattern/TreePatternPruningTest.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.db.pipe.pattern; + +import org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant; +import org.apache.iotdb.commons.pipe.datastructure.pattern.IoTDBTreePattern; +import org.apache.iotdb.commons.pipe.datastructure.pattern.PrefixTreePattern; +import org.apache.iotdb.commons.pipe.datastructure.pattern.TreePattern; +import org.apache.iotdb.commons.pipe.datastructure.pattern.UnionIoTDBTreePattern; +import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters; +import org.apache.iotdb.pipe.api.exception.PipeException; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; + +public class TreePatternPruningTest { + + @Test + public void testUnionInternalPruning_Cover() { + final PipeParameters params = + new PipeParameters( + new HashMap() { + { + put(PipeSourceConstant.SOURCE_PATH_KEY, "root.db.d1.*,root.db.d1.s1"); + } + }); + + final TreePattern result = TreePattern.parsePipePatternFromSourceParameters(params); + + Assert.assertTrue("Should be IoTDBTreePattern", result instanceof IoTDBTreePattern); + Assert.assertEquals("root.db.d1.*", result.getPattern()); + } + + @Test + public void testUnionInternalPruning_Duplicates() { + final PipeParameters params = + new PipeParameters( + new HashMap() { + { + put(PipeSourceConstant.SOURCE_PATH_KEY, "root.db.d1,root.db.d1"); + } + }); + + final TreePattern result = TreePattern.parsePipePatternFromSourceParameters(params); + + Assert.assertTrue(result instanceof IoTDBTreePattern); + Assert.assertEquals("root.db.d1", result.getPattern()); + } + + @Test + public void testInclusionPrunedByExclusion_Partial() { + final PipeParameters params = + new PipeParameters( + new HashMap() { + { + put(PipeSourceConstant.SOURCE_PATH_KEY, "root.sg.d1,root.sg.d2"); + put(PipeSourceConstant.SOURCE_PATH_EXCLUSION_KEY, "root.sg.d1"); + } + }); + + final TreePattern result = TreePattern.parsePipePatternFromSourceParameters(params); + + Assert.assertTrue(result instanceof IoTDBTreePattern); + Assert.assertEquals("root.sg.d2", result.getPattern()); + } + + @Test + public void testInclusionPrunedByExclusion_FullCoverage_Exception() { + final PipeParameters params = + new PipeParameters( + new HashMap() { + { + put(PipeSourceConstant.SOURCE_PATH_KEY, "root.sg.d1"); + put(PipeSourceConstant.SOURCE_PATH_EXCLUSION_KEY, "root.sg.**"); + } + }); + + try { + TreePattern.parsePipePatternFromSourceParameters(params); + Assert.fail("Should throw PipeException because Exclusion fully covers Inclusion"); + } catch (final PipeException ignored) { + // Expected exception + } + } + + @Test + public void testComplexPruning() { + final PipeParameters params = + new PipeParameters( + new HashMap() { + { + put(PipeSourceConstant.SOURCE_PATH_KEY, "root.sg.A,root.sg.B,root.sg.A.sub"); + put(PipeSourceConstant.SOURCE_PATH_EXCLUSION_KEY, "root.sg.A,root.sg.A.**"); + } + }); + + final TreePattern result = TreePattern.parsePipePatternFromSourceParameters(params); + + Assert.assertTrue(result instanceof IoTDBTreePattern); + Assert.assertEquals("root.sg.B", result.getPattern()); + } + + @Test + public void testComplexPruning_Prefix() { + final PipeParameters params = + new PipeParameters( + new HashMap() { + { + put(PipeSourceConstant.SOURCE_PATTERN_KEY, "root.sg.A,root.sg.B,root.sg.A.sub"); + put(PipeSourceConstant.SOURCE_PATTERN_EXCLUSION_KEY, "root.sg.A"); + put(PipeSourceConstant.SOURCE_PATTERN_FORMAT_KEY, "prefix"); + } + }); + + final TreePattern result = TreePattern.parsePipePatternFromSourceParameters(params); + + Assert.assertTrue(result instanceof PrefixTreePattern); + Assert.assertEquals("root.sg.B", result.getPattern()); + } + + @Test + public void testUnionPreservedWhenNotCovered() { + final PipeParameters params = + new PipeParameters( + new HashMap() { + { + put(PipeSourceConstant.SOURCE_PATH_KEY, "root.sg.d1,root.sg.d2"); + put(PipeSourceConstant.SOURCE_PATH_EXCLUSION_KEY, "root.other"); + } + }); + + final TreePattern result = TreePattern.parsePipePatternFromSourceParameters(params); + + Assert.assertTrue(result instanceof UnionIoTDBTreePattern); + Assert.assertEquals("root.sg.d1,root.sg.d2", result.getPattern()); + } +} diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/TreePattern.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/TreePattern.java index e9b0fbe2180e..abc4406ddb7f 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/TreePattern.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/TreePattern.java @@ -19,9 +19,11 @@ package org.apache.iotdb.commons.pipe.datastructure.pattern; +import org.apache.iotdb.commons.conf.IoTDBConstant; import org.apache.iotdb.commons.path.PartialPath; import org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant; import org.apache.iotdb.commons.pipe.config.constant.SystemConstant; +import org.apache.iotdb.commons.utils.TestOnly; import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters; import org.apache.iotdb.pipe.api.exception.PipeException; @@ -32,7 +34,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; @@ -140,60 +144,83 @@ public static TreePattern parsePipePatternFromSourceParameters( final boolean isTreeModelDataAllowedToBeCaptured = isTreeModelDataAllowToBeCaptured(sourceParameters); - // 1. Define the default inclusion pattern (matches all, "root.**") - // This is used if no inclusion patterns are specified. - final TreePattern defaultInclusionPattern = - buildUnionPattern( - isTreeModelDataAllowedToBeCaptured, - Collections.singletonList( - new IoTDBTreePattern(isTreeModelDataAllowedToBeCaptured, null))); - - // 2. Parse INCLUSION patterns using the helper - final TreePattern inclusionPattern = - parsePatternUnion( + // 1. Parse INCLUSION patterns into a list + List inclusionPatterns = + parsePatternList( sourceParameters, isTreeModelDataAllowedToBeCaptured, - // 'path' keys (IoTDB wildcard) EXTRACTOR_PATH_KEY, SOURCE_PATH_KEY, - // 'pattern' keys (Prefix or IoTDB via format) EXTRACTOR_PATTERN_KEY, - SOURCE_PATTERN_KEY, - // Default pattern if no keys are found - defaultInclusionPattern); + SOURCE_PATTERN_KEY); + + // If no inclusion patterns are specified, use default "root.**" + if (inclusionPatterns.isEmpty()) { + inclusionPatterns = + new ArrayList<>( + Collections.singletonList( + new IoTDBTreePattern(isTreeModelDataAllowedToBeCaptured, null))); + } - // 3. Parse EXCLUSION patterns using the helper - final TreePattern exclusionPattern = - parsePatternUnion( + // 2. Parse EXCLUSION patterns into a list + List exclusionPatterns = + parsePatternList( sourceParameters, isTreeModelDataAllowedToBeCaptured, - // 'path.exclusion' keys (IoTDB wildcard) EXTRACTOR_PATH_EXCLUSION_KEY, SOURCE_PATH_EXCLUSION_KEY, - // 'pattern.exclusion' keys (Prefix) EXTRACTOR_PATTERN_EXCLUSION_KEY, - SOURCE_PATTERN_EXCLUSION_KEY, - // Default for exclusion is "match nothing" (null) - null); - - // 4. Combine inclusion and exclusion - if (exclusionPattern == null) { - // No exclusion defined, return the inclusion pattern directly - return inclusionPattern; - } else { - // If both inclusion and exclusion patterns support IoTDB operations, - // use the specialized ExclusionIoTDBTreePattern - if (inclusionPattern instanceof IoTDBTreePatternOperations - && exclusionPattern instanceof IoTDBTreePatternOperations) { - return new WithExclusionIoTDBTreePattern( - isTreeModelDataAllowedToBeCaptured, - (IoTDBTreePatternOperations) inclusionPattern, - (IoTDBTreePatternOperations) exclusionPattern); - } - // Both are defined, wrap them in an ExclusionTreePattern - return new WithExclusionTreePattern( - isTreeModelDataAllowedToBeCaptured, inclusionPattern, exclusionPattern); + SOURCE_PATTERN_EXCLUSION_KEY); + + // 3. Optimize the lists: remove redundant patterns (e.g., if "root.**" exists, "root.db" is + // redundant) + inclusionPatterns = optimizePatterns(inclusionPatterns); + exclusionPatterns = optimizePatterns(exclusionPatterns); + + // 4. Prune inclusion patterns: if an inclusion pattern is fully covered by an exclusion + // pattern, remove it + inclusionPatterns = pruneInclusionPatterns(inclusionPatterns, exclusionPatterns); + + // 5. Check if the resulting inclusion pattern is empty + if (inclusionPatterns.isEmpty()) { + final String msg = + String.format( + "Pipe: The provided exclusion pattern fully covers the inclusion pattern. " + + "This pipe pattern will match nothing. " + + "Inclusion: %s, Exclusion: %s", + sourceParameters.getStringByKeys(EXTRACTOR_PATTERN_KEY, SOURCE_PATTERN_KEY), + sourceParameters.getStringByKeys( + EXTRACTOR_PATTERN_EXCLUSION_KEY, SOURCE_PATTERN_EXCLUSION_KEY)); + LOGGER.warn(msg); + throw new PipeException(msg); + } + + // 6. Prune exclusion patterns: if an exclusion pattern does not overlap with + // ANY of the remaining inclusion patterns, it is useless and should be removed. + exclusionPatterns = pruneIrrelevantExclusions(inclusionPatterns, exclusionPatterns); + + // 7. Build final patterns + final TreePattern finalInclusionPattern = + buildUnionPattern(isTreeModelDataAllowedToBeCaptured, inclusionPatterns); + + if (exclusionPatterns.isEmpty()) { + return finalInclusionPattern; } + + final TreePattern finalExclusionPattern = + buildUnionPattern(isTreeModelDataAllowedToBeCaptured, exclusionPatterns); + + // 8. Combine inclusion and exclusion + if (finalInclusionPattern instanceof IoTDBTreePatternOperations + && finalExclusionPattern instanceof IoTDBTreePatternOperations) { + return new WithExclusionIoTDBTreePattern( + isTreeModelDataAllowedToBeCaptured, + (IoTDBTreePatternOperations) finalInclusionPattern, + (IoTDBTreePatternOperations) finalExclusionPattern); + } + + return new WithExclusionTreePattern( + isTreeModelDataAllowedToBeCaptured, finalInclusionPattern, finalExclusionPattern); } /** @@ -274,65 +301,227 @@ public static TreePattern parsePatternFromString( } /** - * A private helper method to parse a set of 'path' and 'pattern' keys into a single union - * TreePattern. This contains the original logic of parsePipePatternFromSourceParameters. - * - * @param sourceParameters The source parameters. - * @param isTreeModelDataAllowedToBeCaptured Flag for TreePattern constructor. - * @param extractorPathKey Key for extractor path (e.g., "extractor.path"). - * @param sourcePathKey Key for source path (e.g., "source.path"). - * @param extractorPatternKey Key for extractor pattern (e.g., "extractor.pattern"). - * @param sourcePatternKey Key for source pattern (e.g., "source.pattern"). - * @param defaultPattern The pattern to return if both path and pattern are null. If this - * parameter is null, this method returns null. - * @return The parsed TreePattern, or defaultPattern, or null if defaultPattern is null and no - * patterns are specified. + * Helper method to parse pattern parameters into a list of patterns without creating the Union + * object immediately. */ - private static TreePattern parsePatternUnion( + private static List parsePatternList( final PipeParameters sourceParameters, final boolean isTreeModelDataAllowedToBeCaptured, final String extractorPathKey, final String sourcePathKey, final String extractorPatternKey, - final String sourcePatternKey, - final TreePattern defaultPattern) { + final String sourcePatternKey) { final String path = sourceParameters.getStringByKeys(extractorPathKey, sourcePathKey); final String pattern = sourceParameters.getStringByKeys(extractorPatternKey, sourcePatternKey); - // 1. If both "source.path" and "source.pattern" are specified, their union will be used. - if (path != null && pattern != null) { - final List result = new ArrayList<>(); - // Parse "source.path" as IoTDB-style path. + final List result = new ArrayList<>(); + + if (path != null) { result.addAll( parseMultiplePatterns( path, p -> new IoTDBTreePattern(isTreeModelDataAllowedToBeCaptured, p))); - // Parse "source.pattern" using the helper method. + } + + if (pattern != null) { result.addAll( parsePatternsFromPatternParameter( pattern, sourceParameters, isTreeModelDataAllowedToBeCaptured)); - return buildUnionPattern(isTreeModelDataAllowedToBeCaptured, result); } - // 2. If only "source.path" is specified, it will be interpreted as an IoTDB-style path. - if (path != null) { - return buildUnionPattern( - isTreeModelDataAllowedToBeCaptured, - parseMultiplePatterns( - path, p -> new IoTDBTreePattern(isTreeModelDataAllowedToBeCaptured, p))); + return result; + } + + /** + * Removes patterns from the list that are covered by other patterns in the same list. For + * example, if "root.**" and "root.db.**" are present, "root.db.**" is removed. + * + *

Optimization Strategy: + * + *

    + *
  1. Sort First: Patterns are sorted by "broadness" (shortest length and most wildcards + * first). This ensures that dominating patterns (like {@code root.**}) are processed first. + *
  2. Filter with Trie: Instead of comparing every pattern against every other pattern + * (O(N^2)), we build a Trie to check for coverage. For each pattern, we check if it is + * already "covered" by the Trie. If it is, we discard it; if not, we add it to the Trie. + *
+ * + *

Time Complexity: O(N * L), where N is the number of patterns and L is the average + * path length. + */ + private static List optimizePatterns(final List patterns) { + if (patterns == null || patterns.isEmpty()) { + return new ArrayList<>(); + } + if (patterns.size() == 1) { + return patterns; } - // 3. If only "source.pattern" is specified, parse it using the helper method. - if (pattern != null) { - return buildUnionPattern( - isTreeModelDataAllowedToBeCaptured, - parsePatternsFromPatternParameter( - pattern, sourceParameters, isTreeModelDataAllowedToBeCaptured)); + // 1. Sort patterns by "Broadness" + // Heuristic: Shorter paths and paths with wildcards should come first. + // This allows us to insert 'root.**' first, so we can quickly skip 'root.sg.d1' later. + final List sortedPatterns = new ArrayList<>(patterns); + sortedPatterns.sort( + (o1, o2) -> { + // We can only approximate comparison here since TreePattern represents multiple paths. + // We use the first inclusion path as a representative. + final PartialPath p1 = o1.getBaseInclusionPaths().get(0); + final PartialPath p2 = o2.getBaseInclusionPaths().get(0); + + // 1. Length: Shorter is generally broader (e.g., root.** vs root.sg.d1) + final int lenCompare = Integer.compare(p1.getNodeLength(), p2.getNodeLength()); + if (lenCompare != 0) { + return lenCompare; + } + + // 2. Wildcards: Pattern with wildcards is broader (e.g., root.sg.* vs root.sg.d1) + final boolean w1 = p1.hasWildcard(); + final boolean w2 = p2.hasWildcard(); + if (w1 && !w2) { + return -1; + } + if (!w1 && w2) { + return 1; + } + + // 3. Deterministic tie-breaker + return p1.compareTo(p2); + }); + + // 2. Filter using Trie + final PatternTrie trie = new PatternTrie(); + final List optimized = new ArrayList<>(); + + for (final TreePattern pattern : sortedPatterns) { + boolean isCovered = true; + // A pattern is redundant only if ALL its base paths are covered by the Trie + for (final PartialPath path : pattern.getBaseInclusionPaths()) { + if (!trie.isCovered(path)) { + isCovered = false; + break; + } + } + + if (!isCovered) { + optimized.add(pattern); + // Add all its paths to the Trie to cover future patterns + for (final PartialPath path : pattern.getBaseInclusionPaths()) { + trie.add(path); + } + } } - // 4. If neither "source.path" nor "source.pattern" is specified, - // return the provided default pattern (which may be null). - return defaultPattern; + return optimized; + } + + /** + * Prunes patterns from the inclusion list that are fully covered by ANY pattern in the exclusion + * list. + * + *

Optimization Strategy: + * + *

    + *
  1. Build Exclusion Trie: Construct a Trie containing all paths from the exclusion + * list. This aggregates the coverage of all exclusion patterns into a single structure. + *
  2. Check Coverage: Iterate through the inclusion list. For each inclusion pattern, + * check if ALL of its represented paths are covered by the Exclusion Trie. If so, the + * pattern is redundant and removed. + *
+ * + *

Time Complexity: O((N + M) * L), where N is the number of inclusion patterns, M is + * the number of exclusion patterns, and L is the average path length. + */ + private static List pruneInclusionPatterns( + final List inclusion, final List exclusion) { + if (inclusion == null || inclusion.isEmpty()) { + return new ArrayList<>(); + } + if (exclusion == null || exclusion.isEmpty()) { + return inclusion; + } + + // 1. Build Trie with all exclusion paths + // The Trie represents the union of all excluded areas. + final PatternTrie exclusionTrie = new PatternTrie(); + for (final TreePattern exc : exclusion) { + for (final PartialPath path : exc.getBaseInclusionPaths()) { + exclusionTrie.add(path); + } + } + + final List prunedInclusion = new ArrayList<>(); + // 2. Filter inclusion patterns + for (final TreePattern inc : inclusion) { + boolean isFullyExcluded = true; + // An inclusion pattern is fully excluded only if ALL its constituent base paths + // are covered by the exclusion Trie. + for (final PartialPath path : inc.getBaseInclusionPaths()) { + if (!exclusionTrie.isCovered(path)) { + isFullyExcluded = false; + break; + } + } + + // If not fully excluded (i.e., at least one path survives), keep it. + if (!isFullyExcluded) { + prunedInclusion.add(inc); + } + } + return prunedInclusion; + } + + /** + * Prunes patterns from the exclusion list that do NOT overlap with any of the remaining inclusion + * patterns. + * + *

Optimization Strategy: + * + *

    + *
  1. Build Inclusion Trie: Construct a Trie containing all paths from the inclusion + * list. This aggregates the search space of inclusion patterns. + *
  2. Filter Exclusions: Iterate through the exclusion list. For each exclusion pattern, + * check if it overlaps with the Inclusion Trie. Only exclusions that overlap with at least + * one inclusion pattern are kept. + *
+ * + *

Time Complexity: O((N + M) * L), where N is the number of inclusion patterns, M is + * the number of exclusion patterns, and L is the average path length. + */ + private static List pruneIrrelevantExclusions( + final List inclusion, final List exclusion) { + if (exclusion == null || exclusion.isEmpty()) { + return new ArrayList<>(); + } + if (inclusion == null || inclusion.isEmpty()) { + // If inclusion is empty, exclusion is irrelevant anyway. + return new ArrayList<>(); + } + + // 1. Build Trie from Inclusion Patterns + final PatternTrie inclusionTrie = new PatternTrie(); + for (final TreePattern inc : inclusion) { + for (final PartialPath path : inc.getBaseInclusionPaths()) { + inclusionTrie.add(path); + } + } + + // 2. Filter Exclusion Patterns using the Trie + final List relevantExclusion = new ArrayList<>(); + for (final TreePattern exc : exclusion) { + boolean overlapsWithAnyInclusion = false; + // An exclusion pattern is relevant if ANY of its base paths overlap with the inclusion Trie + for (final PartialPath path : exc.getBaseInclusionPaths()) { + if (inclusionTrie.overlaps(path)) { + overlapsWithAnyInclusion = true; + break; + } + } + + if (overlapsWithAnyInclusion) { + relevantExclusion.add(exc); + } + } + return relevantExclusion; } /** @@ -412,6 +601,10 @@ private static List parseMultiplePatterns( */ private static TreePattern buildUnionPattern( final boolean isTreeModelDataAllowedToBeCaptured, final List patterns) { + if (patterns.size() == 1) { + return patterns.get(0); + } + // Check if all instances in the list are of type IoTDBTreePattern boolean allIoTDB = true; for (final TreePattern p : patterns) { @@ -490,6 +683,7 @@ public static boolean isTreeModelDataAllowToBeCaptured(final PipeParameters sour * @return An int array `[coveredCount, totalInclusionPaths]` for testing non-failing scenarios. * @throws PipeException If the inclusion pattern is fully covered by the exclusion pattern. */ + @TestOnly public static int[] checkAndLogPatternCoverage( final TreePattern inclusion, final TreePattern exclusion) throws PipeException { if (inclusion == null || exclusion == null) { @@ -553,4 +747,140 @@ public static int[] checkAndLogPatternCoverage( return new int[] {coveredCount, inclusionPaths.size()}; } + + /** A specialized Trie to efficiently check path coverage. */ + private static class PatternTrie { + private final TrieNode root = new TrieNode(); + + private static class TrieNode { + // Children nodes mapped by specific path segments (excluding *) + Map children = new HashMap<>(); + // Optimized field for One Level Wildcard (*) child to reduce map lookups + TrieNode wildcardNode = null; + + // Marks if a pattern ends here (e.g., "root.sg" is a set path) + boolean isLeaf = false; + // Special flags for optimization + boolean isMultiLevelWildcard = false; // Ends with ** + } + + /** Adds a path to the Trie. */ + public void add(final PartialPath path) { + TrieNode node = root; + final String[] nodes = path.getNodes(); + + for (final String segment : nodes) { + // If we are at a node that is already a MultiLevelWildcard (**), + // everything below is already covered. We can stop adding. + if (node.isMultiLevelWildcard) { + return; + } + + // Check for Multi-Level Wildcard (**) + if (segment.equals(IoTDBConstant.MULTI_LEVEL_PATH_WILDCARD)) { + node.isMultiLevelWildcard = true; + // Optimization: clear children as ** covers everything + node.children = Collections.emptyMap(); + node.wildcardNode = null; + node.isLeaf = true; + return; + } + + // Check for One-Level Wildcard (*) + if (segment.equals(IoTDBConstant.ONE_LEVEL_PATH_WILDCARD)) { + if (node.wildcardNode == null) { + node.wildcardNode = new TrieNode(); + } + node = node.wildcardNode; + } else { + // Regular specific node + node = node.children.computeIfAbsent(segment, k -> new TrieNode()); + } + } + node.isLeaf = true; + } + + /** Checks if the given path is covered by any existing pattern in the Trie. */ + public boolean isCovered(final PartialPath path) { + return checkCoverage(root, path.getNodes(), 0); + } + + private boolean checkCoverage(final TrieNode node, final String[] pathNodes, final int index) { + // 1. If the Trie node is a Multi-Level Wildcard (**), it covers everything remainder + if (node.isMultiLevelWildcard) { + return true; + } + + // 2. If we reached the end of the query path + if (index >= pathNodes.length) { + // The path is covered if the Trie also ends here (isLeaf) + return node.isLeaf; + } + + final String currentSegment = pathNodes[index]; + + // 3. Direct Match in Trie + final TrieNode child = node.children.get(currentSegment); + if (child != null && checkCoverage(child, pathNodes, index + 1)) { + return true; + } + + // 4. Single Level Wildcard (*) in Trie + if (node.wildcardNode != null) { + return checkCoverage(node.wildcardNode, pathNodes, index + 1); + } + + return false; + } + + /** Checks if the given path overlaps with any pattern in the Trie. */ + public boolean overlaps(final PartialPath path) { + return checkOverlap(root, path.getNodes(), 0); + } + + private boolean checkOverlap(final TrieNode node, final String[] pathNodes, final int index) { + // 1. If Trie has '**', it overlaps everything. + if (node.isMultiLevelWildcard) { + return true; + } + + // 2. If Query Path has '**', it overlaps everything remaining in this valid branch. + if (index < pathNodes.length + && pathNodes[index].equals(IoTDBConstant.MULTI_LEVEL_PATH_WILDCARD)) { + return true; + } + + // 3. End of Query Path: Overlap exists if Trie also ends here. + if (index >= pathNodes.length) { + return node.isLeaf; + } + + final String pNode = pathNodes[index]; + + // 4. Case: Query Node is '*' (matches any child in Trie, both specific and wildcard) + if (pNode.equals(IoTDBConstant.ONE_LEVEL_PATH_WILDCARD)) { + // Check all specific children + for (final TrieNode child : node.children.values()) { + if (checkOverlap(child, pathNodes, index + 1)) { + return true; + } + } + // Check wildcard child + if (node.wildcardNode != null) { + return checkOverlap(node.wildcardNode, pathNodes, index + 1); + } + return false; + } + + // 5. Case: Query Node is specific (e.g., "d1") + // 5a. Check exact match in Trie + final TrieNode exactChild = node.children.get(pNode); + if (exactChild != null && checkOverlap(exactChild, pathNodes, index + 1)) { + return true; + } + + // 5b. Check '*' in Trie (matches specific query node) + return node.wildcardNode != null && checkOverlap(node.wildcardNode, pathNodes, index + 1); + } + } } diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/UnionIoTDBTreePattern.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/UnionIoTDBTreePattern.java index 1d047ed89b8e..379232ad2534 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/UnionIoTDBTreePattern.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/UnionIoTDBTreePattern.java @@ -21,6 +21,7 @@ import org.apache.iotdb.commons.path.PartialPath; import org.apache.iotdb.commons.path.PathPatternTree; +import org.apache.iotdb.commons.utils.TestOnly; import org.apache.tsfile.file.metadata.IDeviceID; @@ -55,6 +56,11 @@ public UnionIoTDBTreePattern(final IoTDBTreePattern pattern) { this.patterns = Collections.singletonList(pattern); } + @TestOnly + public List getPatterns() { + return patterns; + } + //////////////////////////// Tree Pattern Operations //////////////////////////// @Override diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/WithExclusionIoTDBTreePattern.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/WithExclusionIoTDBTreePattern.java index f2e580e9cdfe..677507603392 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/WithExclusionIoTDBTreePattern.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/WithExclusionIoTDBTreePattern.java @@ -46,8 +46,6 @@ public WithExclusionIoTDBTreePattern( super(isTreeModelDataAllowedToBeCaptured); this.inclusionPattern = inclusionPattern; this.exclusionPattern = exclusionPattern; - - TreePattern.checkAndLogPatternCoverage(inclusionPattern, exclusionPattern); } public WithExclusionIoTDBTreePattern( diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/WithExclusionTreePattern.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/WithExclusionTreePattern.java index 0cf1d0d1179a..eb255ed8392b 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/WithExclusionTreePattern.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/datastructure/pattern/WithExclusionTreePattern.java @@ -44,8 +44,6 @@ public WithExclusionTreePattern( super(isTreeModelDataAllowedToBeCaptured); this.inclusionPattern = inclusionPattern; this.exclusionPattern = exclusionPattern; - - TreePattern.checkAndLogPatternCoverage(inclusionPattern, exclusionPattern); } @Override