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

clintropolis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new ab12df66fbe feat: add getDimensionRangeSet support to LikeDimFilter 
for equality and prefix cases (#19524)
ab12df66fbe is described below

commit ab12df66fbe63bd892ade1302f11e718508515c8
Author: Clint Wylie <[email protected]>
AuthorDate: Wed May 27 17:21:53 2026 -0700

    feat: add getDimensionRangeSet support to LikeDimFilter for equality and 
prefix cases (#19524)
---
 .../apache/druid/query/filter/LikeDimFilter.java   |  53 +++++++++
 .../druid/query/filter/LikeDimFilterTest.java      | 131 +++++++++++++++++++++
 2 files changed, 184 insertions(+)

diff --git 
a/processing/src/main/java/org/apache/druid/query/filter/LikeDimFilter.java 
b/processing/src/main/java/org/apache/druid/query/filter/LikeDimFilter.java
index b5f67595ffa..96668b30688 100644
--- a/processing/src/main/java/org/apache/druid/query/filter/LikeDimFilter.java
+++ b/processing/src/main/java/org/apache/druid/query/filter/LikeDimFilter.java
@@ -24,10 +24,13 @@ import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableRangeSet;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Range;
 import com.google.common.collect.RangeSet;
 import com.google.common.io.BaseEncoding;
 import com.google.common.primitives.Chars;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.query.extraction.ExtractionFn;
 import org.apache.druid.segment.filter.LikeFilter;
@@ -154,6 +157,20 @@ public class LikeDimFilter extends 
AbstractOptimizableDimFilter implements DimFi
   @Override
   public RangeSet<String> getDimensionRangeSet(String dimension)
   {
+    if (!this.dimension.equals(dimension) || extractionFn != null) {
+      return null;
+    }
+    final LikeDimFilter.LikeMatcher.SuffixMatch suffixMatch = 
likeMatcher.getSuffixMatch();
+    final String prefix = likeMatcher.getPrefix();
+    if (suffixMatch == LikeMatcher.SuffixMatch.MATCH_EMPTY) {
+      // The full pattern was a literal (no wildcards); LIKE acts as equality 
on `prefix`.
+      return ImmutableRangeSet.of(Range.singleton(prefix));
+    }
+    if (suffixMatch == LikeMatcher.SuffixMatch.MATCH_ANY) {
+      // LIKE 'prefix%' matches every string starting with `prefix`; bare LIKE 
'%' matches everything
+      return ImmutableRangeSet.of(prefix.isEmpty() ? Range.all() : 
prefixRange(prefix));
+    }
+    // mid-string wildcards aren't expressible as a single Range.
     return null;
   }
 
@@ -197,6 +214,42 @@ public class LikeDimFilter extends 
AbstractOptimizableDimFilter implements DimFi
     return builder.appendFilterTuning(filterTuning).build();
   }
 
+  /**
+   * Range covering every string that starts with {@code prefix}
+   */
+  public static Range<String> prefixRange(String prefix)
+  {
+    if (prefix.isEmpty()) {
+      throw DruidException.defensive("prefix is empty; use Range.all() 
explicitly for the match-everything case");
+    }
+    final String successor = lexicographicSuccessor(prefix);
+    return successor == null ? Range.atLeast(prefix) : 
Range.closedOpen(prefix, successor);
+  }
+
+  /**
+   * Smallest string strictly greater than {@code s} in lexicographic (UTF-16) 
order: increment the last
+   * non-{@link Character#MAX_VALUE} char and truncate everything after it. 
Returns {@code null} when {@code s}
+   * is a non-empty run of {@code MAX_VALUE} chars and the carry would 
overflow.
+   */
+  @Nullable
+  @VisibleForTesting
+  static String lexicographicSuccessor(String s)
+  {
+    if (s.isEmpty()) {
+      return "\u0000";
+    }
+    final char[] chars = s.toCharArray();
+    int i = chars.length - 1;
+    while (i >= 0 && chars[i] == Character.MAX_VALUE) {
+      i--;
+    }
+    if (i < 0) {
+      return null;
+    }
+    chars[i]++;
+    return new String(chars, 0, i + 1);
+  }
+
   public static class LikeMatcher
   {
     public enum SuffixMatch
diff --git 
a/processing/src/test/java/org/apache/druid/query/filter/LikeDimFilterTest.java 
b/processing/src/test/java/org/apache/druid/query/filter/LikeDimFilterTest.java
index afa450bc471..d122963f2ef 100644
--- 
a/processing/src/test/java/org/apache/druid/query/filter/LikeDimFilterTest.java
+++ 
b/processing/src/test/java/org/apache/druid/query/filter/LikeDimFilterTest.java
@@ -20,8 +20,11 @@
 package org.apache.druid.query.filter;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableRangeSet;
+import com.google.common.collect.Range;
 import com.google.common.collect.Sets;
 import nl.jqno.equalsverifier.EqualsVerifier;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.jackson.DefaultObjectMapper;
 import org.apache.druid.query.extraction.SubstringDimExtractionFn;
 import org.apache.druid.segment.column.ColumnIndexSupplier;
@@ -322,6 +325,134 @@ public class LikeDimFilterTest extends 
InitializedNullHandlingTest
     assertMatch("1 _ 5%6", "1 2 3 1 4 5 6", DruidPredicateMatch.FALSE);
   }
 
+  @Test
+  public void testGetDimensionRangeSet_literalPattern()
+  {
+    final LikeDimFilter filter = new LikeDimFilter("foo", "bar", null, null);
+    Assert.assertEquals(
+        ImmutableRangeSet.of(Range.singleton("bar")),
+        filter.getDimensionRangeSet("foo")
+    );
+  }
+
+  @Test
+  public void testGetDimensionRangeSet_prefixPattern()
+  {
+    final LikeDimFilter filter = new LikeDimFilter("foo", "bar%", null, null);
+    Assert.assertEquals(
+        ImmutableRangeSet.of(Range.closedOpen("bar", "bas")),
+        filter.getDimensionRangeSet("foo")
+    );
+  }
+
+  @Test
+  public void testGetDimensionRangeSet_midPatternWildcard_returnsNull()
+  {
+    final LikeDimFilter filter = new LikeDimFilter("foo", "bar%baz", null, 
null);
+    Assert.assertNull(filter.getDimensionRangeSet("foo"));
+  }
+
+  @Test
+  public void testGetDimensionRangeSet_suffixPattern_returnsNull()
+  {
+    final LikeDimFilter filter = new LikeDimFilter("foo", "%bar", null, null);
+    Assert.assertNull(filter.getDimensionRangeSet("foo"));
+  }
+
+  @Test
+  public void testGetDimensionRangeSet_singleWildcard_returnsAll()
+  {
+    final LikeDimFilter filter = new LikeDimFilter("foo", "%", null, null);
+    Assert.assertEquals(
+        ImmutableRangeSet.of(Range.all()),
+        filter.getDimensionRangeSet("foo")
+    );
+  }
+
+  @Test
+  public void testGetDimensionRangeSet_otherDimension_returnsNull()
+  {
+    final LikeDimFilter filter = new LikeDimFilter("foo", "bar%", null, null);
+    Assert.assertNull(filter.getDimensionRangeSet("other"));
+  }
+
+  @Test
+  public void testGetDimensionRangeSet_withExtractionFn_returnsNull()
+  {
+    final LikeDimFilter filter = new LikeDimFilter("foo", "bar%", null, new 
SubstringDimExtractionFn(0, 3));
+    Assert.assertNull(filter.getDimensionRangeSet("foo"));
+  }
+
+  @Test
+  public void testPrefixRange_singleLowercaseChar()
+  {
+    Assert.assertEquals(Range.closedOpen("foo", "fop"), 
LikeDimFilter.prefixRange("foo"));
+  }
+
+  @Test
+  public void testPrefixRange_uppercaseCarryStaysWithinAscii()
+  {
+    Assert.assertEquals(Range.closedOpen("foZ", "fo["), 
LikeDimFilter.prefixRange("foZ"));
+  }
+
+  @Test
+  public void testPrefixRange_trailingMaxValue_carriesPastIt()
+  {
+    Assert.assertEquals(
+        Range.closedOpen("foo�", "fop"),
+        LikeDimFilter.prefixRange("foo�")
+    );
+  }
+
+  @Test
+  public void testPrefixRange_allMaxValue_fallsBackToAtLeast()
+  {
+    Assert.assertEquals(Range.atLeast("��"), LikeDimFilter.prefixRange("��"));
+  }
+
+  @Test
+  public void testPrefixRange_empty_throws()
+  {
+    Assert.assertThrows(DruidException.class, () -> 
LikeDimFilter.prefixRange(""));
+  }
+
+  @Test
+  public void testPrefixRange_enclosesAllPrefixedStrings()
+  {
+    final Range<String> range = LikeDimFilter.prefixRange("foo");
+    Assert.assertTrue(range.contains("foo"));
+    Assert.assertTrue(range.contains("foo0"));
+    Assert.assertTrue(range.contains("foobar"));
+    Assert.assertTrue(range.contains("foozzz"));
+    Assert.assertFalse(range.contains("fo"));
+    Assert.assertFalse(range.contains("fop"));
+    Assert.assertFalse(range.contains("fox"));
+  }
+
+  @Test
+  public void testLexicographicSuccessor_basic()
+  {
+    Assert.assertEquals("fop", LikeDimFilter.lexicographicSuccessor("foo"));
+  }
+
+  @Test
+  public void testLexicographicSuccessor_empty_returnsNullChar()
+  {
+    Assert.assertEquals("\u0000", LikeDimFilter.lexicographicSuccessor(""));
+  }
+
+  @Test
+  public void testLexicographicSuccessor_singleMaxValue_returnsNull()
+  {
+    Assert.assertNull(LikeDimFilter.lexicographicSuccessor("�"));
+  }
+
+  @Test
+  public void 
testLexicographicSuccessor_trailingMaxValues_truncatedAndCarried()
+  {
+    Assert.assertEquals("fop", LikeDimFilter.lexicographicSuccessor("foo��"));
+  }
+
   private void assertCompilation(String pattern, String expected)
   {
     LikeDimFilter.LikeMatcher matcher = 
LikeDimFilter.LikeMatcher.from(pattern, '\\');


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to