Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@
import io.queryaudit.core.parser.ColumnReference;
import io.queryaudit.core.parser.EnhancedSqlParser;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

private static final Pattern FROM_ALIAS =
Pattern.compile("\\bFROM\\s+(\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?", Pattern.CASE_INSENSITIVE);

private static final Pattern JOIN_ALIAS =
Pattern.compile("\\bJOIN\\s+(\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?", Pattern.CASE_INSENSITIVE);

@Override
public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetadata) {
List<Issue> issues = new ArrayList<>();
Expand Down Expand Up @@ -72,7 +64,7 @@ public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetada
continue;
}

Map<String, String> aliasToTable = resolveAliases(sql);
Map<String, String> aliasToTable = MissingIndexDetector.resolveAliases(sql);

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

private Map<String, String> resolveAliases(String sql) {
Map<String, String> aliasToTable = new HashMap<>();

Matcher fromMatcher = FROM_ALIAS.matcher(sql);
while (fromMatcher.find()) {
String table = fromMatcher.group(1);
String alias = fromMatcher.group(2);
aliasToTable.put(table.toLowerCase(), table.toLowerCase());
if (alias != null) {
aliasToTable.put(alias.toLowerCase(), table.toLowerCase());
}
}

Matcher joinMatcher = JOIN_ALIAS.matcher(sql);
while (joinMatcher.find()) {
String table = joinMatcher.group(1);
String alias = joinMatcher.group(2);
aliasToTable.put(table.toLowerCase(), table.toLowerCase());
if (alias != null) {
aliasToTable.put(alias.toLowerCase(), table.toLowerCase());
}
}

return aliasToTable;
}

private String resolveTable(String tableOrAlias, Map<String, String> aliasToTable) {
if (tableOrAlias != null) {
String resolved = aliasToTable.get(tableOrAlias.toLowerCase());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,25 @@
*/
public class MissingIndexDetector implements DetectionRule {

// A single identifier segment: bare \w+, backtick-quoted, or double-quoted.
// Hibernate emits backtick- or double-quoted identifiers under
// hibernate.globally_quoted_identifiers=true or for reserved-word table names.
private static final String IDENT_SEGMENT = "(?:\\w+|`[^`]+`|\"[^\"]+\")";

// Matches "FROM <table>" with optional schema/database prefixes (e.g. "myschema.users",
// "db.schema.users") and an optional alias.
// "db.schema.users", "`messages`", "\"users\"") and an optional bare alias.
private static final Pattern FROM_ALIAS =
Pattern.compile(
"\\bFROM\\s+((?:\\w+\\.){0,2}\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?",
"\\bFROM\\s+((?:" + IDENT_SEGMENT + "\\.){0,2}" + IDENT_SEGMENT + ")(?:\\s+(?:AS\\s+)?(\\w+))?",
Pattern.CASE_INSENSITIVE);

private static final Pattern JOIN_ALIAS =
Pattern.compile(
"\\bJOIN\\s+((?:\\w+\\.){0,2}\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?",
"\\bJOIN\\s+((?:" + IDENT_SEGMENT + "\\.){0,2}" + IDENT_SEGMENT + ")(?:\\s+(?:AS\\s+)?(\\w+))?",
Pattern.CASE_INSENSITIVE);

private static final Pattern QUOTED_IDENT = Pattern.compile("`([^`]+)`|\"([^\"]+)\"");

// ── Improvement 1: Low cardinality column name patterns ──────────
private static final Set<String> LOW_CARDINALITY_EXACT_NAMES =
Set.of(
Expand Down Expand Up @@ -675,17 +682,19 @@ private static void registerAlias(
if (tableToken == null) {
return;
}
// Strip backticks / double-quotes from each segment so the canonical key matches the
// bare names produced by the WHERE-column extractor (JSqlParser already unquotes).
String normalized = unquoteSegments(tableToken).toLowerCase();
// The token may be schema-qualified ("myschema.users", "db.schema.users"). Drop the prefix so
// detectors look up metadata under the canonical bare name; preserve the qualified form too so
// `WHERE myschema.users.col` resolves through the same map.
String unqualified = stripSchemaPrefix(tableToken).toLowerCase();
String unqualified = stripSchemaPrefix(normalized);
if (isKeyword(unqualified)) {
return;
}
aliasToTable.put(unqualified, unqualified);
String qualified = tableToken.toLowerCase();
if (!qualified.equals(unqualified)) {
aliasToTable.put(qualified, unqualified);
if (!normalized.equals(unqualified)) {
aliasToTable.put(normalized, unqualified);
}
if (aliasToken != null && !isKeyword(aliasToken)) {
aliasToTable.put(aliasToken.toLowerCase(), unqualified);
Expand All @@ -697,6 +706,20 @@ private static String stripSchemaPrefix(String token) {
return lastDot < 0 ? token : token.substring(lastDot + 1);
}

private static String unquoteSegments(String token) {
if (token == null || token.indexOf('`') < 0 && token.indexOf('"') < 0) {
return token;
}
Matcher m = QUOTED_IDENT.matcher(token);
StringBuilder out = new StringBuilder();
while (m.find()) {
String inner = m.group(1) != null ? m.group(1) : m.group(2);
m.appendReplacement(out, Matcher.quoteReplacement(inner));
}
m.appendTail(out);
return out.toString();
}

/**
* Resolve an alias/table reference to the actual table name. If tableOrAlias is null, try to
* infer from the first table in the alias map.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@
import io.queryaudit.core.parser.EnhancedSqlParser;
import io.queryaudit.core.parser.SqlParser;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

private static final Pattern FROM_ALIAS =
Pattern.compile(
"\\bFROM\\s+`?(\\w+)`?(?:\\s+(?:AS\\s+)?`?(\\w+)`?)?", Pattern.CASE_INSENSITIVE);

private static final Pattern JOIN_ALIAS =
Pattern.compile(
"\\bJOIN\\s+`?(\\w+)`?(?:\\s+(?:AS\\s+)?`?(\\w+)`?)?", Pattern.CASE_INSENSITIVE);

@Override
public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetadata) {
List<Issue> issues = new ArrayList<>();
Expand Down Expand Up @@ -80,7 +70,7 @@ public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetada
continue;
}

Map<String, String> aliasToTable = resolveAliases(sql);
Map<String, String> aliasToTable = MissingIndexDetector.resolveAliases(sql);

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

private Map<String, String> resolveAliases(String sql) {
Map<String, String> aliasToTable = new HashMap<>();

Matcher fromMatcher = FROM_ALIAS.matcher(sql);
while (fromMatcher.find()) {
String table = fromMatcher.group(1);
String alias = fromMatcher.group(2);
aliasToTable.put(table.toLowerCase(), table.toLowerCase());
if (alias != null) {
aliasToTable.put(alias.toLowerCase(), table.toLowerCase());
}
}

Matcher joinMatcher = JOIN_ALIAS.matcher(sql);
while (joinMatcher.find()) {
String table = joinMatcher.group(1);
String alias = joinMatcher.group(2);
aliasToTable.put(table.toLowerCase(), table.toLowerCase());
if (alias != null) {
aliasToTable.put(alias.toLowerCase(), table.toLowerCase());
}
}

return aliasToTable;
}

private String resolveTable(String tableOrAlias, Map<String, String> aliasToTable) {
if (tableOrAlias != null) {
String resolved = aliasToTable.get(tableOrAlias.toLowerCase());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,6 @@ public class OrderByLimitWithoutIndexDetector implements DetectionRule {
private static final Pattern ORDER_BY_PATTERN =
Pattern.compile("\\bORDER\\s+BY\\b", Pattern.CASE_INSENSITIVE);

private static final Pattern FROM_ALIAS =
Pattern.compile("\\bFROM\\s+(\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?", Pattern.CASE_INSENSITIVE);

private static final Pattern JOIN_ALIAS =
Pattern.compile("\\bJOIN\\s+(\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?", Pattern.CASE_INSENSITIVE);

@Override
public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetadata) {
List<Issue> issues = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@
import io.queryaudit.core.parser.EnhancedSqlParser;
import io.queryaudit.core.parser.WhereColumnReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

private static final Pattern FROM_ALIAS =
Pattern.compile("\\bFROM\\s+(\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?", Pattern.CASE_INSENSITIVE);

private static final Pattern JOIN_ALIAS =
Pattern.compile("\\bJOIN\\s+(\\w+)(?:\\s+(?:AS\\s+)?(\\w+))?", Pattern.CASE_INSENSITIVE);

@Override
public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetadata) {
List<Issue> issues = new ArrayList<>();
Expand Down Expand Up @@ -78,7 +70,7 @@ public List<Issue> evaluate(List<QueryRecord> queries, IndexMetadata indexMetada
continue; // No WHERE clause -- ForUpdateWithoutIndexDetector handles this
}

Map<String, String> aliasToTable = resolveAliases(sql);
Map<String, String> aliasToTable = MissingIndexDetector.resolveAliases(sql);

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

private Map<String, String> resolveAliases(String sql) {
Map<String, String> aliasToTable = new HashMap<>();

Matcher fromMatcher = FROM_ALIAS.matcher(sql);
while (fromMatcher.find()) {
String table = fromMatcher.group(1);
String alias = fromMatcher.group(2);
aliasToTable.put(table.toLowerCase(), table.toLowerCase());
if (alias != null) {
aliasToTable.put(alias.toLowerCase(), table.toLowerCase());
}
}

Matcher joinMatcher = JOIN_ALIAS.matcher(sql);
while (joinMatcher.find()) {
String table = joinMatcher.group(1);
String alias = joinMatcher.group(2);
aliasToTable.put(table.toLowerCase(), table.toLowerCase());
if (alias != null) {
aliasToTable.put(alias.toLowerCase(), table.toLowerCase());
}
}

return aliasToTable;
}

private String resolveTable(String tableOrAlias, Map<String, String> aliasToTable) {
if (tableOrAlias != null) {
String resolved = aliasToTable.get(tableOrAlias.toLowerCase());
Expand Down
Loading
Loading