Skip to content

Commit 41d0ee0

Browse files
committed
enable the 2nd-escape when fold constant in where-like
Signed-off-by: Joob1n <1208247864@qq.com>
1 parent 9f95b67 commit 41d0ee0

2 files changed

Lines changed: 229 additions & 13 deletions

File tree

fe/fe-core/src/main/java/com/starrocks/sql/optimizer/rewrite/scalar/FoldConstantsRule.java

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
import com.starrocks.type.ScalarType;
4747
import com.starrocks.type.Type;
4848
import com.starrocks.type.TypeFactory;
49-
import org.apache.commons.lang3.StringUtils;
5049
import org.apache.logging.log4j.LogManager;
5150
import org.apache.logging.log4j.Logger;
5251

@@ -56,6 +55,8 @@
5655
import java.util.function.BiFunction;
5756
import java.util.function.BinaryOperator;
5857
import java.util.function.Function;
58+
import java.util.regex.Matcher;
59+
import java.util.regex.Pattern;
5960
import java.util.stream.Collectors;
6061
import java.util.stream.IntStream;
6162

@@ -543,23 +544,47 @@ public ScalarOperator visitLikePredicateOperator(LikePredicateOperator predicate
543544
String text = ((ConstantOperator) predicate.getChild(0)).getVarchar();
544545
String pattern = ((ConstantOperator) predicate.getChild(1)).getVarchar();
545546

546-
if (pattern.matches("^[^_%]+$")) {
547-
return ConstantOperator.createBoolean(text.equals(pattern));
547+
// Group 1 captures the literal body; %-wildcards are matched outside the group so an
548+
// escaped \% inside the body is never mistaken for a trailing/leading wildcard.
549+
Matcher equals = LIKE_EQUALS.matcher(pattern);
550+
if (equals.matches()) {
551+
return ConstantOperator.createBoolean(text.equals(removeLikeEscape(equals.group(1))));
548552
}
549-
if (pattern.matches("^%+[^_%]+$")) {
550-
pattern = StringUtils.stripStart(pattern, "%");
551-
return ConstantOperator.createBoolean(text.endsWith(pattern));
552-
} else if (pattern.matches("^[^_%]+%+$")) {
553-
pattern = StringUtils.stripEnd(pattern, "%");
554-
return ConstantOperator.createBoolean(text.startsWith(pattern));
555-
} else if (pattern.matches("^%+[^_%]+%+$")) {
556-
pattern = StringUtils.stripStart(pattern, "%");
557-
pattern = StringUtils.stripEnd(pattern, "%");
558-
return ConstantOperator.createBoolean(text.contains(pattern));
553+
Matcher endsWith = LIKE_ENDS_WITH.matcher(pattern);
554+
if (endsWith.matches()) {
555+
return ConstantOperator.createBoolean(text.endsWith(removeLikeEscape(endsWith.group(1))));
556+
}
557+
Matcher startsWith = LIKE_STARTS_WITH.matcher(pattern);
558+
if (startsWith.matches()) {
559+
return ConstantOperator.createBoolean(text.startsWith(removeLikeEscape(startsWith.group(1))));
560+
}
561+
Matcher substring = LIKE_SUBSTRING.matcher(pattern);
562+
if (substring.matches()) {
563+
return ConstantOperator.createBoolean(text.contains(removeLikeEscape(substring.group(1))));
559564
}
560565
return predicate;
561566
}
562567

568+
// LIKE body token = an escaped sequence \x (atomic) or a plain char that is not %, _ or \
569+
private static final String LIKE_BODY = "((?:\\\\.|[^%_\\\\])+)";
570+
private static final Pattern LIKE_EQUALS = Pattern.compile("^" + LIKE_BODY + "$");
571+
private static final Pattern LIKE_ENDS_WITH = Pattern.compile("^%+" + LIKE_BODY + "$");
572+
private static final Pattern LIKE_STARTS_WITH = Pattern.compile("^" + LIKE_BODY + "%+$");
573+
private static final Pattern LIKE_SUBSTRING = Pattern.compile("^%+" + LIKE_BODY + "%+$");
574+
575+
// Strips LIKE-layer escape sequences: \\ -> \, \% -> %, \_ -> _, \x -> x
576+
private static String removeLikeEscape(String s) {
577+
StringBuilder sb = new StringBuilder(s.length());
578+
for (int i = 0; i < s.length(); i++) {
579+
if (s.charAt(i) == '\\' && i + 1 < s.length()) {
580+
sb.append(s.charAt(++i));
581+
} else {
582+
sb.append(s.charAt(i));
583+
}
584+
}
585+
return sb.toString();
586+
}
587+
563588
private boolean notAllConstant(List<ScalarOperator> operators) {
564589
return !operators.stream().allMatch(ScalarOperator::isConstantRef);
565590
}

fe/fe-core/src/test/java/com/starrocks/sql/optimizer/rewrite/scalar/FoldConstantsRuleTest.java

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.starrocks.sql.optimizer.operator.scalar.ConstantOperator;
2828
import com.starrocks.sql.optimizer.operator.scalar.InPredicateOperator;
2929
import com.starrocks.sql.optimizer.operator.scalar.IsNullPredicateOperator;
30+
import com.starrocks.sql.optimizer.operator.scalar.LikePredicateOperator;
3031
import com.starrocks.sql.optimizer.operator.scalar.ScalarOperator;
3132
import com.starrocks.sql.optimizer.rewrite.ScalarOperatorRewriteContext;
3233
import com.starrocks.type.BooleanType;
@@ -210,4 +211,194 @@ public void applyBinary() {
210211
assertEquals(OB_FALSE, rule.apply(bpo10, null));
211212

212213
}
214+
215+
@Test
216+
public void applyLikeFolding() {
217+
// Values below are what the FE Parser produces after the first layer of string-literal
218+
// escaping (AstBuilder.escapeBackSlash). Java string mapping:
219+
// SQL 'a\\b' → Java "a\\b" (3 chars: a, \, b)
220+
// SQL 'a\\\\b' → Java "a\\\\b" (4 chars: a, \, \, b)
221+
// SQL '100\\%' → Java "100\\%" (5 chars: 1,0,0,\,%) — Parser keeps \% intact
222+
// SQL 'a\\_b' → Java "a\\_b" (4 chars: a,\,_,b) — Parser keeps \_ intact
223+
224+
// Bug regression: 'a\\b' LIKE 'a\\\\b' must return 1 (MySQL), was 0 before fix
225+
assertEquals(OB_TRUE, rule.apply(new LikePredicateOperator(
226+
LikePredicateOperator.LikeType.LIKE,
227+
ConstantOperator.createVarchar("a\\b"),
228+
ConstantOperator.createVarchar("a\\\\b")), null));
229+
230+
// Bug regression: 'a\\b' LIKE 'a\\b' must return 0 (MySQL), was 1 before fix
231+
assertEquals(OB_FALSE, rule.apply(new LikePredicateOperator(
232+
LikePredicateOperator.LikeType.LIKE,
233+
ConstantOperator.createVarchar("a\\b"),
234+
ConstantOperator.createVarchar("a\\b")), null));
235+
236+
// Escaped % as literal: '100%' LIKE '100\%' → 1 (\% matches the literal % in text)
237+
assertEquals(OB_TRUE, rule.apply(new LikePredicateOperator(
238+
LikePredicateOperator.LikeType.LIKE,
239+
ConstantOperator.createVarchar("100%"),
240+
ConstantOperator.createVarchar("100\\%")), null));
241+
242+
// Escaped % must NOT act as wildcard: '100abc' LIKE '100\%' → 0
243+
assertEquals(OB_FALSE, rule.apply(new LikePredicateOperator(
244+
LikePredicateOperator.LikeType.LIKE,
245+
ConstantOperator.createVarchar("100abc"),
246+
ConstantOperator.createVarchar("100\\%")), null));
247+
248+
// Plain equals (no backslash) — existing behavior preserved
249+
assertEquals(OB_TRUE, rule.apply(new LikePredicateOperator(
250+
LikePredicateOperator.LikeType.LIKE,
251+
ConstantOperator.createVarchar("hello"),
252+
ConstantOperator.createVarchar("hello")), null));
253+
254+
// Plain startsWith — existing behavior preserved
255+
assertEquals(OB_TRUE, rule.apply(new LikePredicateOperator(
256+
LikePredicateOperator.LikeType.LIKE,
257+
ConstantOperator.createVarchar("abcdef"),
258+
ConstantOperator.createVarchar("abc%")), null));
259+
260+
// Pattern with _ wildcard: not a fast-path pattern, returned to BE as-is
261+
LikePredicateOperator withUnderscore = new LikePredicateOperator(
262+
LikePredicateOperator.LikeType.LIKE,
263+
ConstantOperator.createVarchar("abc"),
264+
ConstantOperator.createVarchar("a_c"));
265+
assertEquals(withUnderscore, rule.apply(withUnderscore, null));
266+
267+
// Non-constant text: not folded, returned as-is
268+
LikePredicateOperator nonConst = new LikePredicateOperator(
269+
LikePredicateOperator.LikeType.LIKE,
270+
new ColumnRefOperator(1, VarcharType.VARCHAR, "col", true),
271+
ConstantOperator.createVarchar("a\\\\b"));
272+
assertEquals(nonConst, rule.apply(nonConst, null));
273+
274+
// endsWith path: 'xyzabc' LIKE '%abc' → 1
275+
assertEquals(OB_TRUE, rule.apply(new LikePredicateOperator(
276+
LikePredicateOperator.LikeType.LIKE,
277+
ConstantOperator.createVarchar("xyzabc"),
278+
ConstantOperator.createVarchar("%abc")), null));
279+
280+
// contains path: 'xyzabcdef' LIKE '%abc%' → 1
281+
assertEquals(OB_TRUE, rule.apply(new LikePredicateOperator(
282+
LikePredicateOperator.LikeType.LIKE,
283+
ConstantOperator.createVarchar("xyzabcdef"),
284+
ConstantOperator.createVarchar("%abc%")), null));
285+
286+
// Trailing lone backslash — invalid LIKE escape, not folded → return predicate
287+
LikePredicateOperator trailingSlash = new LikePredicateOperator(
288+
LikePredicateOperator.LikeType.LIKE,
289+
ConstantOperator.createVarchar("a\\"),
290+
ConstantOperator.createVarchar("a\\"));
291+
assertEquals(trailingSlash, rule.apply(trailingSlash, null));
292+
293+
// startsWith with escaped % in body: text must start with literal "a%xy"
294+
// SQL pattern 'a\%xy%': Parser keeps \% → Java "a\\%xy%"; trailing % is the real wildcard
295+
assertEquals(OB_TRUE, rule.apply(new LikePredicateOperator(
296+
LikePredicateOperator.LikeType.LIKE,
297+
ConstantOperator.createVarchar("a%xyz"),
298+
ConstantOperator.createVarchar("a\\%xy%")), null));
299+
300+
// \% in body is NOT a wildcard: "aXxyz" does not start with "a%xy"
301+
assertEquals(OB_FALSE, rule.apply(new LikePredicateOperator(
302+
LikePredicateOperator.LikeType.LIKE,
303+
ConstantOperator.createVarchar("aXxyz"),
304+
ConstantOperator.createVarchar("a\\%xy%")), null));
305+
306+
// --- Escaped wildcard directly adjacent to a real wildcard (regression for the
307+
// stripStart/stripEnd over-strip bug; only passes with the capture-group fix) ---
308+
309+
// startsWith: SQL 'abc\%%' → Java "abc\\%%": literal "abc%" + trailing wildcard %.
310+
// Must match text starting with "abc%".
311+
assertEquals(OB_TRUE, rule.apply(new LikePredicateOperator(
312+
LikePredicateOperator.LikeType.LIKE,
313+
ConstantOperator.createVarchar("abc%xyz"),
314+
ConstantOperator.createVarchar("abc\\%%")), null));
315+
assertEquals(OB_FALSE, rule.apply(new LikePredicateOperator(
316+
LikePredicateOperator.LikeType.LIKE,
317+
ConstantOperator.createVarchar("abcXyz"),
318+
ConstantOperator.createVarchar("abc\\%%")), null));
319+
320+
// endsWith: SQL '%\%abc' → Java "%\\%abc": leading wildcard % + literal "%abc".
321+
// Must match text ending with "%abc".
322+
assertEquals(OB_TRUE, rule.apply(new LikePredicateOperator(
323+
LikePredicateOperator.LikeType.LIKE,
324+
ConstantOperator.createVarchar("xyz%abc"),
325+
ConstantOperator.createVarchar("%\\%abc")), null));
326+
assertEquals(OB_FALSE, rule.apply(new LikePredicateOperator(
327+
LikePredicateOperator.LikeType.LIKE,
328+
ConstantOperator.createVarchar("xyzabc"),
329+
ConstantOperator.createVarchar("%\\%abc")), null));
330+
331+
// contains: SQL '%a\%b%' → Java "%a\\%b%": wildcards on both ends, literal "a%b" inside.
332+
// Must match text containing "a%b".
333+
assertEquals(OB_TRUE, rule.apply(new LikePredicateOperator(
334+
LikePredicateOperator.LikeType.LIKE,
335+
ConstantOperator.createVarchar("xa%by"),
336+
ConstantOperator.createVarchar("%a\\%b%")), null));
337+
assertEquals(OB_FALSE, rule.apply(new LikePredicateOperator(
338+
LikePredicateOperator.LikeType.LIKE,
339+
ConstantOperator.createVarchar("xaXby"),
340+
ConstantOperator.createVarchar("%a\\%b%")), null));
341+
342+
// --- Other escape forms ---
343+
344+
// Double backslash + wildcard: SQL 'a\\%' → Java "a\\\\%": literal "a\" + wildcard %.
345+
// Must match text starting with "a\".
346+
assertEquals(OB_TRUE, rule.apply(new LikePredicateOperator(
347+
LikePredicateOperator.LikeType.LIKE,
348+
ConstantOperator.createVarchar("a\\xyz"),
349+
ConstantOperator.createVarchar("a\\\\%")), null));
350+
351+
// Trailing escaped backslash (EQUALS): SQL 'abc\\' → Java "abc\\\\": literal "abc\".
352+
assertEquals(OB_TRUE, rule.apply(new LikePredicateOperator(
353+
LikePredicateOperator.LikeType.LIKE,
354+
ConstantOperator.createVarchar("abc\\"),
355+
ConstantOperator.createVarchar("abc\\\\")), null));
356+
357+
// Escaped underscore as a literal: SQL 'a\_b' → Java "a\\_b": literal "a_b".
358+
assertEquals(OB_TRUE, rule.apply(new LikePredicateOperator(
359+
LikePredicateOperator.LikeType.LIKE,
360+
ConstantOperator.createVarchar("a_b"),
361+
ConstantOperator.createVarchar("a\\_b")), null));
362+
assertEquals(OB_FALSE, rule.apply(new LikePredicateOperator(
363+
LikePredicateOperator.LikeType.LIKE,
364+
ConstantOperator.createVarchar("aXb"),
365+
ConstantOperator.createVarchar("a\\_b")), null));
366+
367+
// --- Cases that are intentionally NOT folded (returned to BE) ---
368+
369+
// Pure wildcards have no literal body → not folded
370+
LikePredicateOperator pureWildcard = new LikePredicateOperator(
371+
LikePredicateOperator.LikeType.LIKE,
372+
ConstantOperator.createVarchar("anything"),
373+
ConstantOperator.createVarchar("%"));
374+
assertEquals(pureWildcard, rule.apply(pureWildcard, null));
375+
376+
// Empty pattern → not folded
377+
LikePredicateOperator emptyPattern = new LikePredicateOperator(
378+
LikePredicateOperator.LikeType.LIKE,
379+
ConstantOperator.createVarchar("x"),
380+
ConstantOperator.createVarchar(""));
381+
assertEquals(emptyPattern, rule.apply(emptyPattern, null));
382+
383+
// REGEXP (not LIKE) type → not folded by this rule
384+
LikePredicateOperator regexp = new LikePredicateOperator(
385+
LikePredicateOperator.LikeType.REGEXP,
386+
ConstantOperator.createVarchar("abc"),
387+
ConstantOperator.createVarchar("abc"));
388+
assertEquals(regexp, rule.apply(regexp, null));
389+
390+
// --- Other correctness checks ---
391+
392+
// NULL operand → NULL
393+
assertEquals(OB_NULL, rule.apply(new LikePredicateOperator(
394+
LikePredicateOperator.LikeType.LIKE,
395+
ConstantOperator.createNull(VarcharType.VARCHAR),
396+
ConstantOperator.createVarchar("abc")), null));
397+
398+
// LIKE folding is case-sensitive (binary collation): 'ABC' LIKE 'abc' → 0
399+
assertEquals(OB_FALSE, rule.apply(new LikePredicateOperator(
400+
LikePredicateOperator.LikeType.LIKE,
401+
ConstantOperator.createVarchar("ABC"),
402+
ConstantOperator.createVarchar("abc")), null));
403+
}
213404
}

0 commit comments

Comments
 (0)