Skip to content

Commit 39d7d5b

Browse files
committed
[FLINK-39402][table] Fix LIKE quick path ignoring default escape character '\'
1 parent 463330c commit 39d7d5b

2 files changed

Lines changed: 70 additions & 35 deletions

File tree

flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/LikeCallGen.scala

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -54,47 +54,58 @@ class LikeCallGen extends CallGenerator {
5454
terms =>
5555
val pattern = operands(1).literalValue.get.toString
5656
var newPattern: String = pattern
57-
val allowQuick = if (operands.length == 2) {
58-
!pattern.contains("_")
59-
} else {
60-
val escape = operands(2).literalValue.get.toString
61-
if (escape.isEmpty) {
62-
!pattern.contains("_")
57+
val allowQuick = {
58+
// Determine escape character:
59+
// - operands.length == 2 (no ESCAPE clause): default escape is '\'
60+
// - operands.length == 3 with empty escape: no escape character
61+
// - operands.length == 3 with non-empty escape: use specified escape
62+
val escapeCharOpt: Option[Char] = if (operands.length == 2) {
63+
Some('\\')
6364
} else {
64-
if (escape.length > 1) {
65-
throw SqlLikeUtils.invalidEscapeCharacter(escape)
65+
val escape = operands(2).literalValue.get.toString
66+
if (escape.isEmpty) {
67+
None
68+
} else {
69+
if (escape.length > 1) {
70+
throw SqlLikeUtils.invalidEscapeCharacter(escape)
71+
}
72+
Some(escape.charAt(escape.length - 1))
6673
}
67-
val escapeChar = escape.charAt(escape.length - 1)
68-
var matched = true
69-
var i = 0
70-
val newBuilder = new StringBuilder
71-
while (i < pattern.length && matched) {
72-
val c = pattern.charAt(i)
73-
if (c == escapeChar) {
74-
if (i == (pattern.length - 1)) {
75-
throw SqlLikeUtils.invalidEscapeSequence(pattern, i)
76-
}
77-
val nextChar = pattern.charAt(i + 1)
78-
if (nextChar == '%') {
74+
}
75+
76+
escapeCharOpt match {
77+
case None => !pattern.contains("_")
78+
case Some(escapeChar) =>
79+
var matched = true
80+
var i = 0
81+
val newBuilder = new StringBuilder
82+
while (i < pattern.length && matched) {
83+
val c = pattern.charAt(i)
84+
if (c == escapeChar) {
85+
if (i == (pattern.length - 1)) {
86+
throw SqlLikeUtils.invalidEscapeSequence(pattern, i)
87+
}
88+
val nextChar = pattern.charAt(i + 1)
89+
if (nextChar == '%') {
90+
matched = false
91+
} else if ((nextChar == '_') || (nextChar == escapeChar)) {
92+
newBuilder.append(nextChar)
93+
i += 1
94+
} else {
95+
throw SqlLikeUtils.invalidEscapeSequence(pattern, i)
96+
}
97+
} else if (c == '_') {
7998
matched = false
80-
} else if ((nextChar == '_') || (nextChar == escapeChar)) {
81-
newBuilder.append(nextChar)
82-
i += 1
8399
} else {
84-
throw SqlLikeUtils.invalidEscapeSequence(pattern, i)
100+
newBuilder.append(c)
85101
}
86-
} else if (c == '_') {
87-
matched = false
88-
} else {
89-
newBuilder.append(c)
102+
i += 1
90103
}
91-
i += 1
92-
}
93104

94-
if (matched) {
95-
newPattern = newBuilder.toString
96-
}
97-
matched
105+
if (matched) {
106+
newPattern = newBuilder.toString
107+
}
108+
matched
98109
}
99110
}
100111

flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/expressions/ScalarFunctionsTest.scala

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,11 +408,28 @@ class ScalarFunctionsTest extends ScalarTypesTestBase {
408408
testAllApis("abcxxxdef".like("%abc%qef%"), "'abcxxxdef' LIKE '%abc%qef%'", "FALSE")
409409
testAllApis("abcxxxdef".like("abc%qef"), "'abcxxxdef' LIKE 'abc%qef'", "FALSE")
410410

411-
// reported in FLINK-36100
411+
// reported in FLINK-36100: verify default escape char '\' behavior
412+
// \_ means literal '_', \% means literal '%'
412413
testAllApis("TE_ST".like("%E_S%"), "'TE_ST' LIKE '%E_S%'", "TRUE")
413414
testAllApis("TE-ST".like("%E_S%"), "'TE-ST' LIKE '%E_S%'", "TRUE")
414415
testAllApis("TE_ST".like("%E\\_S%"), "'TE_ST' LIKE '%E\\_S%'", "TRUE")
415416
testAllApis("TE-ST".like("%E\\_S%"), "'TE-ST' LIKE '%E\\_S%'", "FALSE")
417+
testAllApis("TE%ST".like("TE\\%ST"), "'TE%ST' LIKE 'TE\\%ST'", "TRUE")
418+
testAllApis("TExST".like("TE\\%ST"), "'TExST' LIKE 'TE\\%ST'", "FALSE")
419+
420+
// escape character at the end
421+
testExpectedAllApisException(
422+
"TE-ST".like("%E-S%\\"),
423+
"'TE-ST' LIKE '%E-S%\\'",
424+
"",
425+
classOf[RuntimeException])
426+
427+
// invalid character after escape character
428+
testExpectedAllApisException(
429+
"TEST".like("%E\\S%"),
430+
"'TEST' LIKE '%E\\S%'",
431+
"Invalid escape",
432+
classOf[RuntimeException])
416433
}
417434

418435
@Test
@@ -429,6 +446,13 @@ class ScalarFunctionsTest extends ScalarTypesTestBase {
429446

430447
@Test
431448
def testLikeWithEscape(): Unit = {
449+
// empty ESCAPE string: no escape character, _ and % retain wildcard semantics
450+
testAllApis("abc".like("abc", ""), "'abc' LIKE 'abc' ESCAPE ''", "TRUE")
451+
testAllApis("abd".like("abc", ""), "'abd' LIKE 'abc' ESCAPE ''", "FALSE")
452+
testAllApis("abcdef".like("abc%", ""), "'abcdef' LIKE 'abc%' ESCAPE ''", "TRUE")
453+
testAllApis("abcdef".like("%def", ""), "'abcdef' LIKE '%def' ESCAPE ''", "TRUE")
454+
testAllApis("abcdef".like("%cd%", ""), "'abcdef' LIKE '%cd%' ESCAPE ''", "TRUE")
455+
432456
testAllApis('f23.like("&%Th_s%", "&"), "f23 LIKE '&%Th_s%' ESCAPE '&'", "TRUE")
433457
testAllApis('f23.like("&%%is a%", "&"), "f23 LIKE '&%%is a%' ESCAPE '&'", "TRUE")
434458
testAllApis('f0.like("Th_s%", "&"), "f0 LIKE 'Th_s%' ESCAPE '&'", "TRUE")

0 commit comments

Comments
 (0)