Skip to content

Commit 1224bae

Browse files
authored
[BugFix] fix information_schema.tables not escaping special characters in equality predicates (#71273)
Signed-off-by: dontknow9179 <clin56322@gmail.com>
1 parent 4eaea65 commit 1224bae

8 files changed

Lines changed: 223 additions & 69 deletions

File tree

fe/fe-core/src/main/java/com/starrocks/catalog/system/information/TablesSystemTable.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.starrocks.catalog.Table;
2121
import com.starrocks.catalog.system.SystemId;
2222
import com.starrocks.catalog.system.SystemTable;
23+
import com.starrocks.common.PatternMatcher;
2324
import com.starrocks.qe.ConnectContext;
2425
import com.starrocks.service.InformationSchemaDataSource;
2526
import com.starrocks.sql.analyzer.SemanticException;
@@ -170,10 +171,10 @@ public List<List<ScalarOperator>> evaluate(ScalarOperator predicate) {
170171
ConstantOperator value = binary.getChild(1).cast();
171172
switch (name.toUpperCase()) {
172173
case "TABLE_NAME":
173-
params.setTable_name(value.getVarchar());
174+
params.setTable_name(PatternMatcher.escapeLikeValue(value.getVarchar()));
174175
break;
175176
case "TABLE_SCHEMA":
176-
authInfo.setPattern(value.getVarchar());
177+
authInfo.setPattern(PatternMatcher.escapeLikeValue(value.getVarchar()));
177178
break;
178179
default:
179180
throw new NotImplementedException("unsupported column: " + name);

fe/fe-core/src/main/java/com/starrocks/common/PatternMatcher.java

Lines changed: 43 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -41,96 +41,66 @@ public boolean match(String candidate) {
4141
/*
4242
* Mysql has only 2 patterns.
4343
* '%' to match any character sequence
44-
* '_' to master any single character.
44+
* '_' to match any single character.
4545
* So we convert '%' to '.*', and '_' to '.'
4646
*
4747
* eg:
4848
* abc% -> abc.*
4949
* ab_c -> ab.c
5050
*
5151
* We also need to handle escape character '\'.
52-
* User use '\' to escape reserved words like '%', '_', or '\' it self
52+
* User use '\' to escape reserved words like '%', '_', or '\' itself
5353
*
5454
* eg:
55-
* ab\%c = ab%c
56-
* ab\_c = ab_c
57-
* ab\\c = ab\c
55+
* abc% -> abc.*
56+
* ab_c -> ab.c
57+
* ab\%c -> matches ab%c
58+
* ab\_c -> matches ab_c
59+
* ab\\c -> matches ab\c
5860
*
5961
* We also have to ignore meaningless '\' like:'ab\c', convert it to 'abc'.
60-
* The following characters are not permitted:
61-
* <([{^=$!|]})?*+>
62+
* Literal segments are wrapped with {@link Pattern#quote} so regex metacharacters
63+
* (for example '(', ')', '+') in table or database names do not break compilation.
6264
*/
6365
private static String convertMysqlPattern(String mysqlPattern) {
64-
String newMysqlPattern = mysqlPattern;
6566
StringBuilder sb = new StringBuilder();
66-
for (int i = 0; i < newMysqlPattern.length(); ++i) {
67-
char ch = newMysqlPattern.charAt(i);
67+
for (int i = 0; i < mysqlPattern.length(); ++i) {
68+
char ch = mysqlPattern.charAt(i);
6869
switch (ch) {
6970
case '%':
7071
sb.append(".*");
7172
break;
72-
case '.':
73-
sb.append("\\.");
74-
break;
7573
case '_':
7674
sb.append(".");
7775
break;
78-
case '\\': {
79-
if (i == newMysqlPattern.length() - 1) {
80-
// last character of this pattern. leave this '\' as it is
81-
sb.append('\\');
82-
break;
83-
}
84-
// we need to look ahead the next character
85-
// to decide ignore this '\' or treat it as escape character.
86-
char nextChar = newMysqlPattern.charAt(i + 1);
87-
switch (nextChar) {
88-
case '%':
89-
case '_':
90-
case '\\':
91-
// this is a escape character, eat this '\' and get next character.
92-
sb.append(nextChar);
93-
++i;
94-
break;
95-
default:
96-
// ignore this '\' and continue;
97-
break;
98-
}
99-
break;
100-
}
101-
default:
102-
sb.append(ch);
103-
break;
104-
}
105-
}
106-
107-
// Replace all the '\' to '\\' in Java pattern
108-
newMysqlPattern = sb.toString();
109-
sb = new StringBuilder();
110-
for (int i = 0; i < newMysqlPattern.length(); ++i) {
111-
char ch = newMysqlPattern.charAt(i);
112-
switch (ch) {
11376
case '\\':
114-
if (i == newMysqlPattern.length() - 1) {
115-
// last character of this pattern. leave this '\' as it is
116-
sb.append('\\').append('\\');
117-
break;
118-
}
119-
// look ahead
120-
if (newMysqlPattern.charAt(i + 1) == '.') {
121-
// leave '\.' as it is.
122-
sb.append('\\').append('.');
123-
i++;
124-
break;
77+
if (i + 1 < mysqlPattern.length()) {
78+
char next = mysqlPattern.charAt(i + 1);
79+
if (next == '%' || next == '_') {
80+
// \% or \_ → literal char (not special in regex)
81+
sb.append(next);
82+
i++;
83+
} else if (next == '\\') {
84+
// \\ → literal backslash → regex needs "\\\\"
85+
sb.append("\\\\");
86+
i++;
87+
} else {
88+
// meaningless \, ignore
89+
}
90+
} else {
91+
// trailing \, treat as literal backslash
92+
sb.append("\\\\");
12593
}
126-
sb.append('\\').append('\\');
12794
break;
12895
default:
96+
// escape regex-special characters
97+
if (".[]{}()*+?^$|".indexOf(ch) >= 0) {
98+
sb.append('\\');
99+
}
129100
sb.append(ch);
130101
break;
131102
}
132103
}
133-
134104
return sb.toString();
135105
}
136106

@@ -153,6 +123,18 @@ public static PatternMatcher createMysqlPattern(String mysqlPattern, boolean cas
153123
return matcher;
154124
}
155125

126+
/**
127+
* Escape a literal value so it can be used as a MySQL LIKE pattern that
128+
* matches the value exactly. The three LIKE-special characters {@code \},
129+
* {@code %} and {@code _} are each prefixed with a backslash.
130+
*/
131+
public static String escapeLikeValue(String value) {
132+
if (value == null) {
133+
return null;
134+
}
135+
return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
136+
}
137+
156138
public static boolean matchPattern(String pattern, String tableName, PatternMatcher matcher,
157139
boolean caseSensitive) {
158140
if (matcher != null && !matcher.match(tableName)) {

fe/fe-core/src/main/java/com/starrocks/service/InformationSchemaDataSource.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -544,8 +544,7 @@ public static TGetTablesInfoResponse generateTablesInfoResponse(TGetTablesInfoRe
544544
List<BasicTable> tables = new ArrayList<>();
545545
List<String> tableNames = metadataMgr.listTableNames(context, catalogName, dbName);
546546
for (String tableName : tableNames) {
547-
if (request.isSetTable_name() &&
548-
!PatternMatcher.matchPattern(request.getTable_name(), tableName, matcher, caseSensitive)) {
547+
if (matcher != null && !matcher.match(tableName)) {
549548
continue;
550549
}
551550

fe/fe-core/src/main/java/com/starrocks/sql/plan/PlanFragmentBuilder.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,16 @@
4242
import com.starrocks.catalog.system.information.FeMetricsSystemTable;
4343
import com.starrocks.catalog.system.information.LoadTrackingLogsSystemTable;
4444
import com.starrocks.catalog.system.information.LoadsSystemTable;
45+
import com.starrocks.catalog.system.information.RoutineLoadJobsSystemTable;
46+
import com.starrocks.catalog.system.information.StreamLoadsSystemTable;
4547
import com.starrocks.catalog.system.information.TaskRunsSystemTable;
4648
import com.starrocks.common.AnalysisException;
4749
import com.starrocks.common.Config;
4850
import com.starrocks.common.DdlException;
4951
import com.starrocks.common.IdGenerator;
5052
import com.starrocks.common.LocalExchangerType;
5153
import com.starrocks.common.Pair;
54+
import com.starrocks.common.PatternMatcher;
5255
import com.starrocks.common.StarRocksException;
5356
import com.starrocks.connector.BucketProperty;
5457
import com.starrocks.connector.metadata.MetadataTable;
@@ -264,6 +267,13 @@
264267
public class PlanFragmentBuilder {
265268
private static final Logger LOG = LogManager.getLogger(PlanFragmentBuilder.class);
266269

270+
private static final Set<String> TABLES_USING_EXACT_DB_MATCH = Set.of(
271+
LoadsSystemTable.NAME,
272+
LoadTrackingLogsSystemTable.NAME,
273+
StreamLoadsSystemTable.NAME,
274+
RoutineLoadJobsSystemTable.NAME
275+
);
276+
267277
public static ExecPlan createPhysicalPlan(OptExpression plan, ConnectContext connectContext,
268278
List<ColumnRefOperator> outputColumns, ColumnRefFactory columnRefFactory,
269279
List<String> colNames,
@@ -1822,13 +1832,19 @@ public PlanFragment visitPhysicalSchemaScan(OptExpression optExpression, ExecPla
18221832
if (predicate instanceof BinaryPredicateOperator) {
18231833
BinaryPredicateOperator binaryPredicateOperator = (BinaryPredicateOperator) predicate;
18241834
if (binaryPredicateOperator.getBinaryType() == BinaryType.EQ) {
1835+
boolean escapeLike = !TABLES_USING_EXACT_DB_MATCH.contains(
1836+
scanNode.getTableName().toLowerCase());
18251837
switch (columnRefOperator.getName()) {
18261838
case "TABLE_SCHEMA":
18271839
case "DATABASE_NAME":
1828-
scanNode.setSchemaDb(constantOperator.getVarchar());
1840+
scanNode.setSchemaDb(escapeLike
1841+
? PatternMatcher.escapeLikeValue(constantOperator.getVarchar())
1842+
: constantOperator.getVarchar());
18291843
break;
18301844
case "TABLE_NAME":
1831-
scanNode.setSchemaTable(constantOperator.getVarchar());
1845+
scanNode.setSchemaTable(escapeLike
1846+
? PatternMatcher.escapeLikeValue(constantOperator.getVarchar())
1847+
: constantOperator.getVarchar());
18321848
break;
18331849
case "BE_ID":
18341850
scanNode.setBeId(constantOperator.getBigint());

fe/fe-core/src/test/java/com/starrocks/common/PatternMatcherTest.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,73 @@ public void testNormal() {
102102
Assertions.fail(e.getMessage());
103103
}
104104
}
105+
106+
@Test
107+
public void testBackslashFollowedByUnderscore() {
108+
// LIKE pattern "a\\_a" means: a, literal \, any single char, a
109+
// This is the pattern that results from SQL: LIKE 'a\\\\_a'
110+
PatternMatcher matcher = PatternMatcher.createMysqlPattern("a\\\\_a", true);
111+
Assertions.assertTrue(matcher.match("a\\_a"));
112+
Assertions.assertTrue(matcher.match("a\\1a"));
113+
Assertions.assertTrue(matcher.match("a\\%a"));
114+
Assertions.assertFalse(matcher.match("a_a"));
115+
Assertions.assertFalse(matcher.match("a\\a"));
116+
Assertions.assertFalse(matcher.match("a\\_ab"));
117+
118+
// LIKE pattern "a\\_" means: a, literal \, any single char
119+
matcher = PatternMatcher.createMysqlPattern("a\\\\_", true);
120+
Assertions.assertTrue(matcher.match("a\\x"));
121+
Assertions.assertTrue(matcher.match("a\\_"));
122+
Assertions.assertFalse(matcher.match("a_"));
123+
124+
// LIKE pattern "a\\%" means: a, literal \, followed by any sequence
125+
matcher = PatternMatcher.createMysqlPattern("a\\\\%", true);
126+
Assertions.assertTrue(matcher.match("a\\"));
127+
Assertions.assertTrue(matcher.match("a\\anything"));
128+
Assertions.assertFalse(matcher.match("a_anything"));
129+
}
130+
131+
@Test
132+
public void testRegexMetacharactersInPattern() {
133+
// Table names with regex metacharacters like (, ), +, *, ? should work
134+
PatternMatcher matcher = PatternMatcher.createMysqlPattern("a(b)c", true);
135+
Assertions.assertTrue(matcher.match("a(b)c"));
136+
Assertions.assertFalse(matcher.match("abc"));
137+
138+
matcher = PatternMatcher.createMysqlPattern("a+b", true);
139+
Assertions.assertTrue(matcher.match("a+b"));
140+
Assertions.assertFalse(matcher.match("aab"));
141+
142+
matcher = PatternMatcher.createMysqlPattern("a[0]b", true);
143+
Assertions.assertTrue(matcher.match("a[0]b"));
144+
Assertions.assertFalse(matcher.match("a0b"));
145+
}
146+
147+
@Test
148+
public void testEscapeLikeValue() {
149+
Assertions.assertNull(PatternMatcher.escapeLikeValue(null));
150+
Assertions.assertEquals("abc", PatternMatcher.escapeLikeValue("abc"));
151+
Assertions.assertEquals("a\\_a", PatternMatcher.escapeLikeValue("a_a"));
152+
Assertions.assertEquals("a\\%a", PatternMatcher.escapeLikeValue("a%a"));
153+
Assertions.assertEquals("a\\\\a", PatternMatcher.escapeLikeValue("a\\a"));
154+
Assertions.assertEquals("a\\\\\\_a", PatternMatcher.escapeLikeValue("a\\_a"));
155+
}
156+
157+
@Test
158+
public void testEscapeLikeValueRoundTrip() {
159+
// Escaping a value and then using it as a LIKE pattern should match only the original value
160+
String[] testValues = {"a_a", "a%b", "a\\b", "a\\_a", "hello", "test(1)+2"};
161+
for (String value : testValues) {
162+
String escaped = PatternMatcher.escapeLikeValue(value);
163+
PatternMatcher matcher = PatternMatcher.createMysqlPattern(escaped, true);
164+
Assertions.assertTrue(matcher.match(value),
165+
"Escaped pattern for '" + value + "' should match itself");
166+
}
167+
168+
// Escaped "a_a" should NOT match "aba" (the underscore is not a wildcard)
169+
String escaped = PatternMatcher.escapeLikeValue("a_a");
170+
PatternMatcher matcher = PatternMatcher.createMysqlPattern(escaped, true);
171+
Assertions.assertFalse(matcher.match("aba"));
172+
Assertions.assertFalse(matcher.match("a1a"));
173+
}
105174
}

fe/fe-core/src/test/java/com/starrocks/sql/plan/ScanTest.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.starrocks.catalog.Partition;
2121
import com.starrocks.catalog.Tablet;
2222
import com.starrocks.common.FeConstants;
23+
import com.starrocks.common.PatternMatcher;
2324
import com.starrocks.planner.ScanNode;
2425
import com.starrocks.planner.SchemaScanNode;
2526
import org.junit.jupiter.api.Assertions;
@@ -386,8 +387,10 @@ public void testSchemaScanWithWhere() throws Exception {
386387
String sql = "select column_name, table_name from information_schema.columns" +
387388
" where table_schema = 'information_schema' and table_name = 'columns'";
388389
ExecPlan plan = getExecPlan(sql);
389-
Assertions.assertTrue(((SchemaScanNode) plan.getScanNodes().get(0)).getSchemaDb().equals("information_schema"));
390-
Assertions.assertTrue(((SchemaScanNode) plan.getScanNodes().get(0)).getSchemaTable().equals("columns"));
390+
SchemaScanNode scanNode = (SchemaScanNode) plan.getScanNodes().get(0);
391+
// Equality values are escaped for LIKE-style pushdown; '_' in database names must be literal.
392+
Assertions.assertEquals(PatternMatcher.escapeLikeValue("information_schema"), scanNode.getSchemaDb());
393+
Assertions.assertEquals(PatternMatcher.escapeLikeValue("columns"), scanNode.getSchemaTable());
391394
}
392395

393396
@Test

test/sql/test_information_schema/R/test_tables_like_escape

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,61 @@ a_a
1818
-- !result
1919
drop database if exists db_${uuid0};
2020
-- result:
21+
-- !result
22+
23+
-- name: test_table_name_escape_underscore_and_backslash
24+
create database db_${uuid0};
25+
-- result:
26+
-- !result
27+
use db_${uuid0};
28+
-- result:
29+
-- !result
30+
CREATE TABLE `a\_a` (`c1` int) DISTRIBUTED BY HASH(`c1`) BUCKETS 1 PROPERTIES ("replication_num" = "1");
31+
-- result:
32+
-- !result
33+
CREATE TABLE `a_a` (`c1` int) DISTRIBUTED BY HASH(`c1`) BUCKETS 1 PROPERTIES ("replication_num" = "1");
34+
-- result:
35+
-- !result
36+
CREATE TABLE `aba` (`c1` int) DISTRIBUTED BY HASH(`c1`) BUCKETS 1 PROPERTIES ("replication_num" = "1");
37+
-- result:
38+
-- !result
39+
CREATE TABLE `a\ba` (`c1` int) DISTRIBUTED BY HASH(`c1`) BUCKETS 1 PROPERTIES ("replication_num" = "1");
40+
-- result:
41+
-- !result
42+
select table_name from information_schema.tables where table_schema='db_${uuid0}' and table_name = 'a_a';
43+
-- result:
44+
a_a
45+
-- !result
46+
select table_name from information_schema.tables where table_schema='db_${uuid0}' and table_name = 'a\_a';
47+
-- result:
48+
a\_a
49+
-- !result
50+
select table_name from information_schema.tables where table_schema='db_${uuid0}' and table_name = 'a\\_a';
51+
-- result:
52+
a\_a
53+
-- !result
54+
select table_name from information_schema.tables where table_schema='db_${uuid0}' and table_name like 'a_a' order by table_name;
55+
-- result:
56+
a_a
57+
aba
58+
-- !result
59+
select table_name from information_schema.tables where table_schema='db_${uuid0}' and table_name like 'a\_a';
60+
-- result:
61+
a_a
62+
-- !result
63+
select table_name from information_schema.tables where table_schema='db_${uuid0}' and table_name like 'a\\_a';
64+
-- result:
65+
a_a
66+
-- !result
67+
select table_name from information_schema.tables where table_schema='db_${uuid0}' and table_name like 'a\\\\_a' order by table_name;
68+
-- result:
69+
a\_a
70+
a\ba
71+
-- !result
72+
select table_name from information_schema.tables where table_schema='db_${uuid0}' and table_name like 'a\\\\\\_a';
73+
-- result:
74+
a\_a
75+
-- !result
76+
drop database if exists db_${uuid0};
77+
-- result:
2178
-- !result

0 commit comments

Comments
 (0)