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]