Skip to content

Commit 304ccf1

Browse files
haroya01Haroya
andauthored
fix(core): unquote backtick/double-quoted table tokens in alias resolution (#82)
Consolidates two commits: - Allow FROM/JOIN segments in alias resolution to match `\w+`, backtick-, or double-quote-wrapped identifiers, then strip the quotes before lowercasing the canonical key. Fixes the case where Hibernate emits quoted-identifier SQL (e.g. `hibernate.globally_quoted_identifiers=true`) and the empty alias map silently drops legitimate missing-index, redundant-filter, composite-index, and for-update findings. - Route RedundantFilterDetector, CompositeIndexDetector, and ForUpdateNonUniqueIndexDetector through MissingIndexDetector's now-canonical alias map instead of each maintaining its own regex copy. Closes #82. Co-authored-by: Haroya <haroya0117@gmail.com>
1 parent e71a226 commit 304ccf1

6 files changed

Lines changed: 199 additions & 120 deletions

File tree

query-audit-core/src/main/java/io/queryaudit/core/detector/ForUpdateNonUniqueIndexDetector.java

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@
99
import io.queryaudit.core.parser.ColumnReference;
1010
import io.queryaudit.core.parser.EnhancedSqlParser;
1111
import java.util.ArrayList;
12-
import java.util.HashMap;
1312
import java.util.LinkedHashSet;
1413
import java.util.List;
1514
import java.util.Map;
1615
import java.util.Set;
17-
import java.util.regex.Matcher;
1816
import java.util.regex.Pattern;
1917

2018
/**
@@ -35,12 +33,6 @@ public class ForUpdateNonUniqueIndexDetector implements DetectionRule {
3533
private static final Pattern FOR_UPDATE_OR_SHARE =
3634
Pattern.compile("\\bFOR\\s+(?:UPDATE|SHARE)\\b", Pattern.CASE_INSENSITIVE);
3735

38-
private static final Pattern FROM_ALIAS =
39-
Pattern.compile("\\bFROM\\s+(\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?", Pattern.CASE_INSENSITIVE);
40-
41-
private static final Pattern JOIN_ALIAS =
42-
Pattern.compile("\\bJOIN\\s+(\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?", Pattern.CASE_INSENSITIVE);
43-
4436
@Override
4537
public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetadata) {
4638
List<Issue> issues = new ArrayList<>();
@@ -72,7 +64,7 @@ public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetada
7264
continue;
7365
}
7466

75-
Map<String, String> aliasToTable = resolveAliases(sql);
67+
Map<String, String> aliasToTable = MissingIndexDetector.resolveAliases(sql);
7668

7769
for (ColumnReference col : whereColumns) {
7870
String table = resolveTable(col.tableOrAlias(), aliasToTable);
@@ -128,32 +120,6 @@ private boolean isOnlyNonUniqueIndexed(IndexMetadata indexMetadata, String table
128120
return hasIndex;
129121
}
130122

131-
private Map<String, String> resolveAliases(String sql) {
132-
Map<String, String> aliasToTable = new HashMap<>();
133-
134-
Matcher fromMatcher = FROM_ALIAS.matcher(sql);
135-
while (fromMatcher.find()) {
136-
String table = fromMatcher.group(1);
137-
String alias = fromMatcher.group(2);
138-
aliasToTable.put(table.toLowerCase(), table.toLowerCase());
139-
if (alias != null) {
140-
aliasToTable.put(alias.toLowerCase(), table.toLowerCase());
141-
}
142-
}
143-
144-
Matcher joinMatcher = JOIN_ALIAS.matcher(sql);
145-
while (joinMatcher.find()) {
146-
String table = joinMatcher.group(1);
147-
String alias = joinMatcher.group(2);
148-
aliasToTable.put(table.toLowerCase(), table.toLowerCase());
149-
if (alias != null) {
150-
aliasToTable.put(alias.toLowerCase(), table.toLowerCase());
151-
}
152-
}
153-
154-
return aliasToTable;
155-
}
156-
157123
private String resolveTable(String tableOrAlias, Map<String, String> aliasToTable) {
158124
if (tableOrAlias != null) {
159125
String resolved = aliasToTable.get(tableOrAlias.toLowerCase());

query-audit-core/src/main/java/io/queryaudit/core/detector/MissingIndexDetector.java

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,25 @@
3030
*/
3131
public class MissingIndexDetector implements DetectionRule {
3232

33+
// A single identifier segment: bare \w+, backtick-quoted, or double-quoted.
34+
// Hibernate emits backtick- or double-quoted identifiers under
35+
// hibernate.globally_quoted_identifiers=true or for reserved-word table names.
36+
private static final String IDENT_SEGMENT = "(?:\\w+|`[^`]+`|\"[^\"]+\")";
37+
3338
// Matches "FROM <table>" with optional schema/database prefixes (e.g. "myschema.users",
34-
// "db.schema.users") and an optional alias.
39+
// "db.schema.users", "`messages`", "\"users\"") and an optional bare alias.
3540
private static final Pattern FROM_ALIAS =
3641
Pattern.compile(
37-
"\\bFROM\\s+((?:\\w+\\.){0,2}\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?",
42+
"\\bFROM\\s+((?:" + IDENT_SEGMENT + "\\.){0,2}" + IDENT_SEGMENT + ")(?:\\s+(?:AS\\s+)?(\\w+))?",
3843
Pattern.CASE_INSENSITIVE);
3944

4045
private static final Pattern JOIN_ALIAS =
4146
Pattern.compile(
42-
"\\bJOIN\\s+((?:\\w+\\.){0,2}\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?",
47+
"\\bJOIN\\s+((?:" + IDENT_SEGMENT + "\\.){0,2}" + IDENT_SEGMENT + ")(?:\\s+(?:AS\\s+)?(\\w+))?",
4348
Pattern.CASE_INSENSITIVE);
4449

50+
private static final Pattern QUOTED_IDENT = Pattern.compile("`([^`]+)`|\"([^\"]+)\"");
51+
4552
// ── Improvement 1: Low cardinality column name patterns ──────────
4653
private static final Set<String> LOW_CARDINALITY_EXACT_NAMES =
4754
Set.of(
@@ -675,17 +682,19 @@ private static void registerAlias(
675682
if (tableToken == null) {
676683
return;
677684
}
685+
// Strip backticks / double-quotes from each segment so the canonical key matches the
686+
// bare names produced by the WHERE-column extractor (JSqlParser already unquotes).
687+
String normalized = unquoteSegments(tableToken).toLowerCase();
678688
// The token may be schema-qualified ("myschema.users", "db.schema.users"). Drop the prefix so
679689
// detectors look up metadata under the canonical bare name; preserve the qualified form too so
680690
// `WHERE myschema.users.col` resolves through the same map.
681-
String unqualified = stripSchemaPrefix(tableToken).toLowerCase();
691+
String unqualified = stripSchemaPrefix(normalized);
682692
if (isKeyword(unqualified)) {
683693
return;
684694
}
685695
aliasToTable.put(unqualified, unqualified);
686-
String qualified = tableToken.toLowerCase();
687-
if (!qualified.equals(unqualified)) {
688-
aliasToTable.put(qualified, unqualified);
696+
if (!normalized.equals(unqualified)) {
697+
aliasToTable.put(normalized, unqualified);
689698
}
690699
if (aliasToken != null && !isKeyword(aliasToken)) {
691700
aliasToTable.put(aliasToken.toLowerCase(), unqualified);
@@ -697,6 +706,20 @@ private static String stripSchemaPrefix(String token) {
697706
return lastDot < 0 ? token : token.substring(lastDot + 1);
698707
}
699708

709+
private static String unquoteSegments(String token) {
710+
if (token == null || token.indexOf('`') < 0 && token.indexOf('"') < 0) {
711+
return token;
712+
}
713+
Matcher m = QUOTED_IDENT.matcher(token);
714+
StringBuilder out = new StringBuilder();
715+
while (m.find()) {
716+
String inner = m.group(1) != null ? m.group(1) : m.group(2);
717+
m.appendReplacement(out, Matcher.quoteReplacement(inner));
718+
}
719+
m.appendTail(out);
720+
return out.toString();
721+
}
722+
700723
/**
701724
* Resolve an alias/table reference to the actual table name. If tableOrAlias is null, try to
702725
* infer from the first table in the alias map.

query-audit-core/src/main/java/io/queryaudit/core/detector/NonDeterministicPaginationDetector.java

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@
1010
import io.queryaudit.core.parser.EnhancedSqlParser;
1111
import io.queryaudit.core.parser.SqlParser;
1212
import java.util.ArrayList;
13-
import java.util.HashMap;
1413
import java.util.LinkedHashSet;
1514
import java.util.List;
1615
import java.util.Map;
1716
import java.util.Set;
18-
import java.util.regex.Matcher;
1917
import java.util.regex.Pattern;
2018

2119
/**
@@ -38,14 +36,6 @@ public class NonDeterministicPaginationDetector implements DetectionRule {
3836
private static final Pattern LIMIT_PATTERN =
3937
Pattern.compile("\\bLIMIT\\b", Pattern.CASE_INSENSITIVE);
4038

41-
private static final Pattern FROM_ALIAS =
42-
Pattern.compile(
43-
"\\bFROM\\s+`?(\\w+)`?(?:\\s+(?:AS\\s+)?`?(\\w+)`?)?", Pattern.CASE_INSENSITIVE);
44-
45-
private static final Pattern JOIN_ALIAS =
46-
Pattern.compile(
47-
"\\bJOIN\\s+`?(\\w+)`?(?:\\s+(?:AS\\s+)?`?(\\w+)`?)?", Pattern.CASE_INSENSITIVE);
48-
4939
@Override
5040
public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetadata) {
5141
List<Issue> issues = new ArrayList<>();
@@ -80,7 +70,7 @@ public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetada
8070
continue;
8171
}
8272

83-
Map<String, String> aliasToTable = resolveAliases(sql);
73+
Map<String, String> aliasToTable = MissingIndexDetector.resolveAliases(sql);
8474

8575
// Check if any ORDER BY column has a unique index
8676
boolean hasUniqueTiebreaker = false;
@@ -142,32 +132,6 @@ private boolean hasUniqueIndex(IndexMetadata indexMetadata, String table, String
142132
return false;
143133
}
144134

145-
private Map<String, String> resolveAliases(String sql) {
146-
Map<String, String> aliasToTable = new HashMap<>();
147-
148-
Matcher fromMatcher = FROM_ALIAS.matcher(sql);
149-
while (fromMatcher.find()) {
150-
String table = fromMatcher.group(1);
151-
String alias = fromMatcher.group(2);
152-
aliasToTable.put(table.toLowerCase(), table.toLowerCase());
153-
if (alias != null) {
154-
aliasToTable.put(alias.toLowerCase(), table.toLowerCase());
155-
}
156-
}
157-
158-
Matcher joinMatcher = JOIN_ALIAS.matcher(sql);
159-
while (joinMatcher.find()) {
160-
String table = joinMatcher.group(1);
161-
String alias = joinMatcher.group(2);
162-
aliasToTable.put(table.toLowerCase(), table.toLowerCase());
163-
if (alias != null) {
164-
aliasToTable.put(alias.toLowerCase(), table.toLowerCase());
165-
}
166-
}
167-
168-
return aliasToTable;
169-
}
170-
171135
private String resolveTable(String tableOrAlias, Map<String, String> aliasToTable) {
172136
if (tableOrAlias != null) {
173137
String resolved = aliasToTable.get(tableOrAlias.toLowerCase());

query-audit-core/src/main/java/io/queryaudit/core/detector/OrderByLimitWithoutIndexDetector.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,6 @@ public class OrderByLimitWithoutIndexDetector implements DetectionRule {
3434
private static final Pattern ORDER_BY_PATTERN =
3535
Pattern.compile("\\bORDER\\s+BY\\b", Pattern.CASE_INSENSITIVE);
3636

37-
private static final Pattern FROM_ALIAS =
38-
Pattern.compile("\\bFROM\\s+(\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?", Pattern.CASE_INSENSITIVE);
39-
40-
private static final Pattern JOIN_ALIAS =
41-
Pattern.compile("\\bJOIN\\s+(\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?", Pattern.CASE_INSENSITIVE);
42-
4337
@Override
4438
public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetadata) {
4539
List<Issue> issues = new ArrayList<>();

query-audit-core/src/main/java/io/queryaudit/core/detector/RangeLockDetector.java

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,10 @@
88
import io.queryaudit.core.parser.EnhancedSqlParser;
99
import io.queryaudit.core.parser.WhereColumnReference;
1010
import java.util.ArrayList;
11-
import java.util.HashMap;
1211
import java.util.LinkedHashSet;
1312
import java.util.List;
1413
import java.util.Map;
1514
import java.util.Set;
16-
import java.util.regex.Matcher;
1715
import java.util.regex.Pattern;
1816

1917
/**
@@ -38,12 +36,6 @@ public class RangeLockDetector implements DetectionRule {
3836
/** Range operators that cause gap locks in InnoDB. */
3937
private static final Set<String> RANGE_OPERATORS = Set.of(">", "<", ">=", "<=", "BETWEEN");
4038

41-
private static final Pattern FROM_ALIAS =
42-
Pattern.compile("\\bFROM\\s+(\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?", Pattern.CASE_INSENSITIVE);
43-
44-
private static final Pattern JOIN_ALIAS =
45-
Pattern.compile("\\bJOIN\\s+(\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?", Pattern.CASE_INSENSITIVE);
46-
4739
@Override
4840
public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetadata) {
4941
List<Issue> issues = new ArrayList<>();
@@ -78,7 +70,7 @@ public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetada
7870
continue; // No WHERE clause -- ForUpdateWithoutIndexDetector handles this
7971
}
8072

81-
Map<String, String> aliasToTable = resolveAliases(sql);
73+
Map<String, String> aliasToTable = MissingIndexDetector.resolveAliases(sql);
8274

8375
for (WhereColumnReference col : whereColumns) {
8476
String operator = col.operator() != null ? col.operator().trim().toUpperCase() : "";
@@ -124,32 +116,6 @@ public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetada
124116
return issues;
125117
}
126118

127-
private Map<String, String> resolveAliases(String sql) {
128-
Map<String, String> aliasToTable = new HashMap<>();
129-
130-
Matcher fromMatcher = FROM_ALIAS.matcher(sql);
131-
while (fromMatcher.find()) {
132-
String table = fromMatcher.group(1);
133-
String alias = fromMatcher.group(2);
134-
aliasToTable.put(table.toLowerCase(), table.toLowerCase());
135-
if (alias != null) {
136-
aliasToTable.put(alias.toLowerCase(), table.toLowerCase());
137-
}
138-
}
139-
140-
Matcher joinMatcher = JOIN_ALIAS.matcher(sql);
141-
while (joinMatcher.find()) {
142-
String table = joinMatcher.group(1);
143-
String alias = joinMatcher.group(2);
144-
aliasToTable.put(table.toLowerCase(), table.toLowerCase());
145-
if (alias != null) {
146-
aliasToTable.put(alias.toLowerCase(), table.toLowerCase());
147-
}
148-
}
149-
150-
return aliasToTable;
151-
}
152-
153119
private String resolveTable(String tableOrAlias, Map<String, String> aliasToTable) {
154120
if (tableOrAlias != null) {
155121
String resolved = aliasToTable.get(tableOrAlias.toLowerCase());

0 commit comments

Comments
 (0)