From 597d3fdd162925cfbb2d45626344c594d77202c6 Mon Sep 17 00:00:00 2001 From: jukejian Date: Sun, 23 Aug 2026 18:57:47 +0800 Subject: [PATCH 1/2] [feature](lance) push down common string and boolean predicates --- .../lance/source/LancePredicateConverter.java | 73 +++++++++++++++++++ .../lance/LancePredicateConverterTest.java | 64 ++++++++++++++++ .../test_lance_scalar_predicate_pushdown.out | 50 +++++++++++++ ...est_lance_scalar_predicate_pushdown.groovy | 63 +++++++++++++++- 4 files changed, 249 insertions(+), 1 deletion(-) diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LancePredicateConverter.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LancePredicateConverter.java index 784896eff09708..9e98d8b854714a 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LancePredicateConverter.java +++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LancePredicateConverter.java @@ -24,10 +24,12 @@ import org.apache.doris.analysis.DecimalLiteral; import org.apache.doris.analysis.Expr; import org.apache.doris.analysis.FloatLiteral; +import org.apache.doris.analysis.FunctionCallExpr; import org.apache.doris.analysis.InPredicate; import org.apache.doris.analysis.IntLiteral; import org.apache.doris.analysis.IsNullPredicate; import org.apache.doris.analysis.LargeIntLiteral; +import org.apache.doris.analysis.LikePredicate; import org.apache.doris.analysis.LiteralExpr; import org.apache.doris.analysis.NullLiteral; import org.apache.doris.analysis.SlotRef; @@ -131,6 +133,15 @@ private Optional convert(Expr expr) { if (expr instanceof IsNullPredicate) { return convertIsNull((IsNullPredicate) expr); } + if (expr instanceof LikePredicate) { + return convertLike((LikePredicate) expr); + } + if (expr instanceof FunctionCallExpr) { + return convertStringFunction((FunctionCallExpr) expr); + } + if (expr instanceof SlotRef) { + return convertBooleanSlot((SlotRef) expr); + } return Optional.empty(); } @@ -257,6 +268,60 @@ private Optional convertIsNull(IsNullPredicate predicate) { return Optional.of(comparisonFunction(function, fieldReference(field))); } + private Optional convertLike(LikePredicate predicate) { + if (predicate.getOp() != LikePredicate.Operator.LIKE) { + return Optional.empty(); + } + return convertStringPredicate("like:str_str", predicate.getChild(0), predicate.getChild(1), true); + } + + private Optional convertStringFunction(FunctionCallExpr function) { + if (function.getFnName() == null || function.getChildren().size() != 2) { + return Optional.empty(); + } + String functionName = function.getFnName().getFunction().toLowerCase(Locale.ROOT); + switch (functionName) { + case "like": + return convertStringPredicate( + "like:str_str", function.getChild(0), function.getChild(1), true); + case "starts_with": + return convertStringPredicate( + "starts_with:str_str", function.getChild(0), function.getChild(1), false); + case "ends_with": + return convertStringPredicate( + "ends_with:str_str", function.getChild(0), function.getChild(1), false); + default: + return Optional.empty(); + } + } + + private Optional convertStringPredicate( + String function, Expr input, Expr pattern, boolean rejectEscapedPattern) { + SlotRef slot = directSlot(input); + LiteralExpr literal = directLiteral(pattern); + ResolvedField field = slot == null ? null : findField(slot); + if (field == null || !isStringType(field.field.getType()) || !(literal instanceof StringLiteral)) { + return Optional.empty(); + } + String patternValue = literal.getStringValue(); + // Doris uses backslash as LIKE's default escape character, while the Substrait function + // has no escape argument. Keep escaped LIKE patterns in Doris rather than changing meaning. + if (rejectEscapedPattern && patternValue.indexOf('\\') >= 0) { + return Optional.empty(); + } + return Optional.of(stringFunction(function, fieldReference(field), + ExpressionCreator.string(false, patternValue))); + } + + private Optional convertBooleanSlot(SlotRef slot) { + ResolvedField field = findField(slot); + if (field == null || !(field.field.getType() instanceof ArrowType.Bool)) { + return Optional.empty(); + } + return Optional.of(comparisonFunction("equal:any_any", + fieldReference(field), ExpressionCreator.bool(false, true))); + } + // convert doris literal to Substrait literal with arrow type private Optional convertLiteral(ArrowType type, LiteralExpr literal) { if (type instanceof ArrowType.Bool && literal instanceof BoolLiteral) { @@ -450,6 +515,10 @@ private static boolean isPushdownType(ArrowType type) { || type instanceof ArrowType.LargeUtf8; } + private static boolean isStringType(ArrowType type) { + return type instanceof ArrowType.Utf8 || type instanceof ArrowType.LargeUtf8; + } + // slotref with ordinal index with Substrait Type private Expression fieldReference(ResolvedField field) { return FieldReference.newRootStructReference(field.ordinal, toSubstraitType(field.field)); @@ -502,6 +571,10 @@ private static Expression booleanFunction(String key, List arguments return scalarFunction(DefaultExtensionCatalog.FUNCTIONS_BOOLEAN, key, arguments); } + private static Expression stringFunction(String key, Expression... arguments) { + return scalarFunction(DefaultExtensionCatalog.FUNCTIONS_STRING, key, Arrays.asList(arguments)); + } + private static Expression scalarFunction(String uri, String key, List arguments) { SimpleExtension.ScalarFunctionVariant declaration = EXTENSIONS.getScalarFunction( SimpleExtension.FunctionAnchor.of(uri, key)); diff --git a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LancePredicateConverterTest.java b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LancePredicateConverterTest.java index 66709546786a6a..ceb9d209af7bb7 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LancePredicateConverterTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LancePredicateConverterTest.java @@ -27,6 +27,7 @@ import org.apache.doris.analysis.IntLiteral; import org.apache.doris.analysis.IsNullPredicate; import org.apache.doris.analysis.LargeIntLiteral; +import org.apache.doris.analysis.LikePredicate; import org.apache.doris.analysis.SlotRef; import org.apache.doris.analysis.StringLiteral; import org.apache.doris.catalog.ScalarType; @@ -215,6 +216,69 @@ public void testComparisonInAndIsNull() { Assertions.assertEquals(3, result.getPushedConjuncts().size()); } + @Test + public void testStringPredicates() { + Expr legacyLike = new LikePredicate(LikePredicate.Operator.LIKE, + new SlotRef(null, "label"), new StringLiteral("ready%")); + Expr like = new FunctionCallExpr("like", + Arrays.asList(new SlotRef(null, "label"), new StringLiteral("%ead_"))); + Expr startsWith = new FunctionCallExpr("starts_with", + Arrays.asList(new SlotRef(null, "label"), new StringLiteral("ready"))); + Expr endsWith = new FunctionCallExpr("ends_with", + Arrays.asList(new SlotRef(null, "large_label"), new StringLiteral("done"))); + + LancePredicateConverter.ConversionResult result = + converter.convert(Arrays.asList(legacyLike, like, startsWith, endsWith)); + + ExtendedExpression envelope = Assertions.assertDoesNotThrow( + () -> ExtendedExpression.parseFrom(result.getSubstraitFilter())); + String serialized = envelope.toString(); + Assertions.assertTrue(serialized.contains("like:str_str")); + Assertions.assertTrue(serialized.contains("starts_with:str_str")); + Assertions.assertTrue(serialized.contains("ends_with:str_str")); + Assertions.assertEquals(4, result.getPushedConjuncts().size()); + } + + @Test + public void testUnsupportedStringPredicatesRemainResidual() { + Expr regexp = new LikePredicate(LikePredicate.Operator.REGEXP, + new SlotRef(null, "label"), new StringLiteral("ready.*")); + Expr escapedLike = new LikePredicate(LikePredicate.Operator.LIKE, + new SlotRef(null, "label"), new StringLiteral("ready\\%")); + Expr explicitEscape = new FunctionCallExpr("like", Arrays.asList( + new SlotRef(null, "label"), new StringLiteral("ready!%"), + new StringLiteral("!"))); + Expr nonLiteralPattern = new FunctionCallExpr("starts_with", + Arrays.asList(new SlotRef(null, "label"), new SlotRef(null, "event-type"))); + Expr nonStringInput = new FunctionCallExpr("ends_with", + Arrays.asList(new SlotRef(null, "row_id"), new StringLiteral("1"))); + + LancePredicateConverter.ConversionResult result = converter.convert( + Arrays.asList(regexp, escapedLike, explicitEscape, nonLiteralPattern, nonStringInput)); + + Assertions.assertEquals(0, result.getSubstraitFilter().length); + Assertions.assertTrue(result.getPushedConjuncts().isEmpty()); + } + + @Test + public void testDirectBooleanPredicates() { + LancePredicateConverter boolConverter = new LancePredicateConverter(new Schema( + Collections.singletonList(Field.nullable("active", ArrowType.Bool.INSTANCE)))); + Expr active = new SlotRef(null, "active"); + Expr notActive = new CompoundPredicate( + CompoundPredicate.Operator.NOT, new SlotRef(null, "active"), null); + + LancePredicateConverter.ConversionResult result = + boolConverter.convert(Arrays.asList(active, notActive)); + + ExtendedExpression envelope = Assertions.assertDoesNotThrow( + () -> ExtendedExpression.parseFrom(result.getSubstraitFilter())); + String serialized = envelope.toString(); + Assertions.assertTrue(serialized.contains("equal:any_any")); + Assertions.assertTrue(serialized.contains("not:bool")); + Assertions.assertEquals(2, result.getPushedConjuncts().size()); + } + @Test public void testNullableNullSafeEqualityPreservesTwoValuedSemantics() { Expr nullableNullSafeEqual = new BinaryPredicate(BinaryPredicate.Operator.EQ_FOR_NULL, diff --git a/regression-test/data/external_table_p0/lance/test_lance_scalar_predicate_pushdown.out b/regression-test/data/external_table_p0/lance/test_lance_scalar_predicate_pushdown.out index 91b378709d62cb..8236fc47101e9f 100644 --- a/regression-test/data/external_table_p0/lance/test_lance_scalar_predicate_pushdown.out +++ b/regression-test/data/external_table_p0/lance/test_lance_scalar_predicate_pushdown.out @@ -78,6 +78,56 @@ 9 10 +-- !select_bool_direct -- +5 +6 +7 +9 +10 + +-- !select_bool_direct_not -- +2 +3 +4 +8 + +-- !select_utf8_starts_with -- +7 +8 + +-- !select_utf8_ends_with -- +4 +6 + +-- !select_utf8_like_prefix -- +2 +4 +10 + +-- !select_utf8_like_contains -- +3 +7 +10 + +-- !select_utf8_like_single_wildcard -- +7 +8 + +-- !select_utf8_not_like -- +2 +3 +4 +5 +6 +9 +10 + +-- !select_utf8_like_explicit_escape -- + +-- !select_utf8_regexp_residual -- +7 +8 + -- !select_float32_eq -- 7 8 diff --git a/regression-test/suites/external_table_p0/lance/test_lance_scalar_predicate_pushdown.groovy b/regression-test/suites/external_table_p0/lance/test_lance_scalar_predicate_pushdown.groovy index edd47dff285869..fccfe388547ce4 100644 --- a/regression-test/suites/external_table_p0/lance/test_lance_scalar_predicate_pushdown.groovy +++ b/regression-test/suites/external_table_p0/lance/test_lance_scalar_predicate_pushdown.groovy @@ -22,7 +22,7 @@ suite("test_lance_scalar_predicate_pushdown", "p0,external") { * * | Lance / Arrow type | Doris type | Operators exercised | * |---|---|---| - * | bool | boolean | =, !=, <>, <=>, IN, NOT IN, IS NULL, IS NOT NULL, OR, NOT | + * | bool | boolean | Direct predicate, =, !=, <>, <=>, IN, NOT IN, IS NULL, IS NOT NULL, OR, NOT | * | float32 | float | All operators below | * | float64 | double | All operators below | * | decimal128 | decimal(18,2) | All operators below | @@ -84,6 +84,19 @@ suite("test_lance_scalar_predicate_pushdown", "p0,external") { } } + Closure verifyResidual = { String query, String expression -> + explain { + sql(query) + notContains "lancePushdownPredicate=" + check { explainString -> + String residual = explainString.readLines() + .find { line -> line.trim().startsWith("predicates:") } + return residual != null + && residual.toLowerCase().contains(expression.toLowerCase()) + } + } + } + Closure verifyOrderedScalarPushdown = { String tableName, String typeName, String columnName, Map values -> String eqQuery = """ SELECT row_id FROM ${tableName} WHERE ${columnName} = ${values.equal} ORDER BY row_id; """ verifyFullyPushedDown(eqQuery, columnName) @@ -202,10 +215,58 @@ suite("test_lance_scalar_predicate_pushdown", "p0,external") { String boolReversedQuery = """ SELECT row_id FROM predicate_pushdown WHERE true = bool_value ORDER BY row_id; """ verifyFullyPushedDown(boolReversedQuery, "bool_value") quickTest("select_bool_reversed", boolReversedQuery) + + String boolDirectQuery = """ SELECT row_id FROM predicate_pushdown WHERE bool_value ORDER BY row_id; """ + verifyFullyPushedDown(boolDirectQuery, "bool_value") + quickTest("select_bool_direct", boolDirectQuery) + + String boolDirectNotQuery = """ SELECT row_id FROM predicate_pushdown WHERE NOT bool_value ORDER BY row_id; """ + verifyFullyPushedDown(boolDirectNotQuery, "bool_value") + quickTest("select_bool_direct_not", boolDirectNotQuery) } verifyBooleanPushdown() + String startsWithQuery = + """ SELECT row_id FROM predicate_pushdown WHERE starts_with(utf8_value, 'ten') ORDER BY row_id; """ + verifyFullyPushedDown(startsWithQuery, "utf8_value") + quickTest("select_utf8_starts_with", startsWithQuery) + + String endsWithQuery = + """ SELECT row_id FROM predicate_pushdown WHERE ends_with(utf8_value, 'one') ORDER BY row_id; """ + verifyFullyPushedDown(endsWithQuery, "utf8_value") + quickTest("select_utf8_ends_with", endsWithQuery) + + String likePrefixQuery = + """ SELECT row_id FROM predicate_pushdown WHERE utf8_value LIKE 'm%' ORDER BY row_id; """ + verifyFullyPushedDown(likePrefixQuery, "utf8_value") + quickTest("select_utf8_like_prefix", likePrefixQuery) + + String likeContainsQuery = + """ SELECT row_id FROM predicate_pushdown WHERE utf8_value LIKE '%a%' ORDER BY row_id; """ + verifyFullyPushedDown(likeContainsQuery, "utf8_value") + quickTest("select_utf8_like_contains", likeContainsQuery) + + String likeSingleWildcardQuery = + """ SELECT row_id FROM predicate_pushdown WHERE utf8_value LIKE 'ten-_' ORDER BY row_id; """ + verifyFullyPushedDown(likeSingleWildcardQuery, "utf8_value") + quickTest("select_utf8_like_single_wildcard", likeSingleWildcardQuery) + + String notLikeQuery = + """ SELECT row_id FROM predicate_pushdown WHERE utf8_value NOT LIKE 'ten-%' ORDER BY row_id; """ + verifyFullyPushedDown(notLikeQuery, "utf8_value") + quickTest("select_utf8_not_like", notLikeQuery) + + String explicitEscapeQuery = + """ SELECT row_id FROM predicate_pushdown WHERE utf8_value LIKE 'ten!_%' ESCAPE '!' ORDER BY row_id; """ + verifyResidual(explicitEscapeQuery, "like") + quickTest("select_utf8_like_explicit_escape", explicitEscapeQuery) + + String regexpQuery = + """ SELECT row_id FROM predicate_pushdown WHERE utf8_value REGEXP '^ten-' ORDER BY row_id; """ + verifyResidual(regexpQuery, "regexp") + quickTest("select_utf8_regexp_residual", regexpQuery) + verifyOrderedScalarPushdown("predicate_pushdown", "float32", "float32_value", [ equal: "10", threshold: "0", From f613605eaf8d35d53dfdbf0644cf6d669f01ba19 Mon Sep 17 00:00:00 2001 From: jukejian Date: Sat, 29 Aug 2026 13:06:54 +0800 Subject: [PATCH 2/2] [fix](lance) keep unsafe string predicates residual --- .../lance/source/LancePredicateConverter.java | 8 ++- .../lance/LancePredicateConverterTest.java | 52 ++++++++++++++----- .../test_lance_scalar_predicate_pushdown.out | 5 ++ ...est_lance_scalar_predicate_pushdown.groovy | 5 ++ 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LancePredicateConverter.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LancePredicateConverter.java index 9e98d8b854714a..41fc1d855f86c8 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LancePredicateConverter.java +++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LancePredicateConverter.java @@ -34,6 +34,7 @@ import org.apache.doris.analysis.NullLiteral; import org.apache.doris.analysis.SlotRef; import org.apache.doris.analysis.StringLiteral; +import org.apache.doris.thrift.TFunctionBinaryType; import io.substrait.expression.Expression; import io.substrait.expression.ExpressionCreator; @@ -276,7 +277,9 @@ private Optional convertLike(LikePredicate predicate) { } private Optional convertStringFunction(FunctionCallExpr function) { - if (function.getFnName() == null || function.getChildren().size() != 2) { + if (function.getFnName() == null || function.getFn() == null + || function.getFn().getBinaryType() != TFunctionBinaryType.BUILTIN + || function.getChildren().size() != 2) { return Optional.empty(); } String functionName = function.getFnName().getFunction().toLowerCase(Locale.ROOT); @@ -306,7 +309,8 @@ private Optional convertStringPredicate( String patternValue = literal.getStringValue(); // Doris uses backslash as LIKE's default escape character, while the Substrait function // has no escape argument. Keep escaped LIKE patterns in Doris rather than changing meaning. - if (rejectEscapedPattern && patternValue.indexOf('\\') >= 0) { + if (patternValue.indexOf('\0') >= 0 + || (rejectEscapedPattern && patternValue.indexOf('\\') >= 0)) { return Optional.empty(); } return Optional.of(stringFunction(function, fieldReference(field), diff --git a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LancePredicateConverterTest.java b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LancePredicateConverterTest.java index ceb9d209af7bb7..64e6fd70724430 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LancePredicateConverterTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LancePredicateConverterTest.java @@ -23,6 +23,7 @@ import org.apache.doris.analysis.DecimalLiteral; import org.apache.doris.analysis.Expr; import org.apache.doris.analysis.FunctionCallExpr; +import org.apache.doris.analysis.FunctionName; import org.apache.doris.analysis.InPredicate; import org.apache.doris.analysis.IntLiteral; import org.apache.doris.analysis.IsNullPredicate; @@ -30,9 +31,11 @@ import org.apache.doris.analysis.LikePredicate; import org.apache.doris.analysis.SlotRef; import org.apache.doris.analysis.StringLiteral; +import org.apache.doris.catalog.ScalarFunction; import org.apache.doris.catalog.ScalarType; import org.apache.doris.catalog.Type; import org.apache.doris.datasource.lance.source.LancePredicateConverter; +import org.apache.doris.thrift.TFunctionBinaryType; import io.substrait.proto.ExtendedExpression; import org.apache.arrow.vector.types.DateUnit; @@ -220,12 +223,12 @@ public void testComparisonInAndIsNull() { public void testStringPredicates() { Expr legacyLike = new LikePredicate(LikePredicate.Operator.LIKE, new SlotRef(null, "label"), new StringLiteral("ready%")); - Expr like = new FunctionCallExpr("like", - Arrays.asList(new SlotRef(null, "label"), new StringLiteral("%ead_"))); - Expr startsWith = new FunctionCallExpr("starts_with", - Arrays.asList(new SlotRef(null, "label"), new StringLiteral("ready"))); - Expr endsWith = new FunctionCallExpr("ends_with", - Arrays.asList(new SlotRef(null, "large_label"), new StringLiteral("done"))); + Expr like = stringFunction( + "like", new SlotRef(null, "label"), new StringLiteral("%ead_")); + Expr startsWith = stringFunction( + "starts_with", new SlotRef(null, "label"), new StringLiteral("ready")); + Expr endsWith = stringFunction( + "ends_with", new SlotRef(null, "large_label"), new StringLiteral("done")); LancePredicateConverter.ConversionResult result = converter.convert(Arrays.asList(legacyLike, like, startsWith, endsWith)); @@ -245,13 +248,12 @@ public void testUnsupportedStringPredicatesRemainResidual() { new SlotRef(null, "label"), new StringLiteral("ready.*")); Expr escapedLike = new LikePredicate(LikePredicate.Operator.LIKE, new SlotRef(null, "label"), new StringLiteral("ready\\%")); - Expr explicitEscape = new FunctionCallExpr("like", Arrays.asList( - new SlotRef(null, "label"), new StringLiteral("ready!%"), - new StringLiteral("!"))); - Expr nonLiteralPattern = new FunctionCallExpr("starts_with", - Arrays.asList(new SlotRef(null, "label"), new SlotRef(null, "event-type"))); - Expr nonStringInput = new FunctionCallExpr("ends_with", - Arrays.asList(new SlotRef(null, "row_id"), new StringLiteral("1"))); + Expr explicitEscape = stringFunction("like", + new SlotRef(null, "label"), new StringLiteral("ready!%"), new StringLiteral("!")); + Expr nonLiteralPattern = stringFunction( + "starts_with", new SlotRef(null, "label"), new SlotRef(null, "event-type")); + Expr nonStringInput = stringFunction( + "ends_with", new SlotRef(null, "row_id"), new StringLiteral("1")); LancePredicateConverter.ConversionResult result = converter.convert( Arrays.asList(regexp, escapedLike, explicitEscape, nonLiteralPattern, nonStringInput)); @@ -260,6 +262,23 @@ public void testUnsupportedStringPredicatesRemainResidual() { Assertions.assertTrue(result.getPushedConjuncts().isEmpty()); } + @Test + public void testResolvedUdfAndNulStringPredicatesRemainResidual() { + FunctionCallExpr udf = stringFunction( + "starts_with", new SlotRef(null, "label"), new StringLiteral("ready")); + udf.getFn().setBinaryType(TFunctionBinaryType.JAVA_UDF); + Expr legacyNulLike = new LikePredicate(LikePredicate.Operator.LIKE, + new SlotRef(null, "label"), new StringLiteral("m\0_")); + Expr functionNulLike = stringFunction( + "like", new SlotRef(null, "label"), new StringLiteral("m\0_")); + + LancePredicateConverter.ConversionResult result = + converter.convert(Arrays.asList(udf, legacyNulLike, functionNulLike)); + + Assertions.assertEquals(0, result.getSubstraitFilter().length); + Assertions.assertTrue(result.getPushedConjuncts().isEmpty()); + } + @Test public void testDirectBooleanPredicates() { LancePredicateConverter boolConverter = new LancePredicateConverter(new Schema( @@ -537,6 +556,13 @@ private void assertUnsignedIntegerLiteral( Assertions.assertEquals(1, result.getPushedConjuncts().size()); } + private FunctionCallExpr stringFunction(String name, Expr... arguments) { + FunctionCallExpr function = new FunctionCallExpr(name, Arrays.asList(arguments)); + function.setFn(new ScalarFunction(new FunctionName(name), + Collections.nCopies(arguments.length, Type.VARCHAR), Type.BOOLEAN, false, true)); + return function; + } + private void assertNullSafeEqualityComposition(Expr predicate) { LancePredicateConverter.ConversionResult result = converter.convert(Collections.singletonList(predicate)); diff --git a/regression-test/data/external_table_p0/lance/test_lance_scalar_predicate_pushdown.out b/regression-test/data/external_table_p0/lance/test_lance_scalar_predicate_pushdown.out index 8236fc47101e9f..595e35993dd0c7 100644 --- a/regression-test/data/external_table_p0/lance/test_lance_scalar_predicate_pushdown.out +++ b/regression-test/data/external_table_p0/lance/test_lance_scalar_predicate_pushdown.out @@ -124,6 +124,11 @@ -- !select_utf8_like_explicit_escape -- +-- !select_utf8_like_nul_residual -- +2 +4 +10 + -- !select_utf8_regexp_residual -- 7 8 diff --git a/regression-test/suites/external_table_p0/lance/test_lance_scalar_predicate_pushdown.groovy b/regression-test/suites/external_table_p0/lance/test_lance_scalar_predicate_pushdown.groovy index fccfe388547ce4..f52f37cdfaa16c 100644 --- a/regression-test/suites/external_table_p0/lance/test_lance_scalar_predicate_pushdown.groovy +++ b/regression-test/suites/external_table_p0/lance/test_lance_scalar_predicate_pushdown.groovy @@ -262,6 +262,11 @@ suite("test_lance_scalar_predicate_pushdown", "p0,external") { verifyResidual(explicitEscapeQuery, "like") quickTest("select_utf8_like_explicit_escape", explicitEscapeQuery) + String nulLikeQuery = + """ SELECT row_id FROM predicate_pushdown WHERE utf8_value LIKE 'm\\0_' ORDER BY row_id; """ + verifyResidual(nulLikeQuery, "like") + quickTest("select_utf8_like_nul_residual", nulLikeQuery) + String regexpQuery = """ SELECT row_id FROM predicate_pushdown WHERE utf8_value REGEXP '^ten-' ORDER BY row_id; """ verifyResidual(regexpQuery, "regexp")