Skip to content
193 changes: 162 additions & 31 deletions core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -194,6 +193,127 @@
}
}

private ImmutableList<String> findCaseInsensitiveColumnSuggestions(String columnName,
SqlIdentifier identifier) {
final SqlNameMatcher liberalMatcher = SqlNameMatchers.liberal();
final Map<String, ScopeChild> map =
findQualifyingTableNames(columnName, identifier, liberalMatcher);
if (map.isEmpty()) {
return ImmutableList.of();
}
final List<String> suggestions = new ArrayList<>();
for (ScopeChild entry : map.values()) {
final RelDataTypeField field =
liberalMatcher.field(entry.namespace.getRowType(), columnName);
if (field != null) {
suggestions.add(field.getName());
}
}
suggestions.sort(String::compareTo);
return ImmutableList.copyOf(suggestions);
}

private List<String> findColumnSuggestions(String columnName) {
final List<SqlMoniker> columnNames = new ArrayList<>();
findAllColumnNames(columnNames);
return SqlNameMatchers.bestMatches(columnName, simpleNames(columnNames));
}

private @Nullable String findCaseInsensitiveTableSuggestion(List<String> names) {
final SqlNameMatcher liberalMatcher = SqlNameMatchers.liberal();
final ResolvedImpl resolved = new ResolvedImpl();

Check warning on line 224 in core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename "resolved" which hides the field declared at line 76.

See more on https://sonarcloud.io/project/issues?id=apache_calcite&issues=AZ1jdmSblZMhgCvZnhBA&open=AZ1jdmSblZMhgCvZnhBA&pullRequest=4866
resolve(names, liberalMatcher, false, resolved);
if (resolved.count() == 1) {
final Step lastStep = Util.last(resolved.only().path.steps());
return lastStep.name;
}
return null;
}

private @Nullable String findTableSuggestion(SqlIdentifier prefix) {
final @Nullable String caseInsensitiveSuggestion =
findCaseInsensitiveTableSuggestion(prefix.names);
if (caseInsensitiveSuggestion != null) {
return caseInsensitiveSuggestion;
}
if (prefix.names.size() == 1) {
if (prefix.names.get(0).length() <= 2) {
return null;
}
final List<SqlMoniker> aliases = new ArrayList<>();
findAliases(aliases);
return SqlNameMatchers.bestObjectMatch(prefix.names.get(0), simpleNames(aliases));
}
final SqlNameMatchers.NameSuggestion suggestion =
SqlNameMatchers.bestObjectName(validator.catalogReader, prefix.names);
return suggestion == null ? null : suggestion.suggestion;
}

private @Nullable String findCaseInsensitiveFieldSuggestion(
SqlValidatorNamespace namespace, List<String> names) {
final SqlNameMatcher liberalMatcher = SqlNameMatchers.liberal();
final ResolvedImpl resolved = new ResolvedImpl();

Check warning on line 255 in core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename "resolved" which hides the field declared at line 76.

See more on https://sonarcloud.io/project/issues?id=apache_calcite&issues=AZ1jdmSblZMhgCvZnhBB&open=AZ1jdmSblZMhgCvZnhBB&pullRequest=4866
resolveInNamespace(namespace, false, names, liberalMatcher, Path.EMPTY, resolved);
if (resolved.count() == 0) {
return null;
}
final Step step = Util.last(resolved.resolves.get(0).path.steps());
return step.name;
}

private @Nullable String findFieldSuggestion(SqlValidatorNamespace namespace,
SqlNameMatcher nameMatcher, List<String> names) {
SqlValidatorNamespace currentNamespace = namespace;
final SqlNameMatcher liberalMatcher = SqlNameMatchers.liberal();
for (String name : names) {

Check warning on line 268 in core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce the total number of break and continue statements in this loop to use at most one.

See more on https://sonarcloud.io/project/issues?id=apache_calcite&issues=AZ1jdmSblZMhgCvZnhA_&open=AZ1jdmSblZMhgCvZnhA_&pullRequest=4866
final RelDataType rowType = currentNamespace.getRowType();
if (!rowType.isStruct()) {
return null;
}
final ResolvedImpl resolved = new ResolvedImpl();

Check warning on line 273 in core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename "resolved" which hides the field declared at line 76.

See more on https://sonarcloud.io/project/issues?id=apache_calcite&issues=AZ1jdmSblZMhgCvZnhBC&open=AZ1jdmSblZMhgCvZnhBC&pullRequest=4866
resolveInNamespace(currentNamespace, false, ImmutableList.of(name), nameMatcher,
Path.EMPTY, resolved);
if (resolved.count() == 0) {
final ResolvedImpl liberalResolved = new ResolvedImpl();
resolveInNamespace(currentNamespace, false, ImmutableList.of(name),
liberalMatcher, Path.EMPTY, liberalResolved);
if (liberalResolved.count() == 1) {
currentNamespace = liberalResolved.only().namespace;
continue;
}
if (liberalResolved.count() == 0) {
return SqlNameMatchers.bestMatch(name, fieldNames(rowType));
}
} else if (resolved.count() > 1) {
return null;
} else {
currentNamespace = resolved.only().namespace;
continue;
}
return null;
}
return null;
}

private static ImmutableList<String> fieldNames(RelDataType rowType) {
if (!rowType.isStruct()) {
return ImmutableList.of();
}
final ImmutableList.Builder<String> names = ImmutableList.builder();
for (RelDataTypeField field : rowType.getFieldList()) {
names.add(field.getName());
}
return names.build();
}

private static List<String> simpleNames(Iterable<? extends SqlMoniker> monikers) {
final List<String> names = new ArrayList<>();
for (SqlMoniker moniker : monikers) {
names.add(Util.last(moniker.getFullyQualifiedNames()));
}
return names;
}

@Override public void findAllColumnNames(List<SqlMoniker> result) {
parent.findAllColumnNames(result);
}
Expand Down Expand Up @@ -270,24 +390,18 @@
switch (map.size()) {
case 0:
if (nameMatcher.isCaseSensitive()) {
final SqlNameMatcher liberalMatcher = SqlNameMatchers.liberal();
final Map<String, ScopeChild> map2 =
findQualifyingTableNames(columnName, identifier, liberalMatcher);
if (!map2.isEmpty()) {
final List<String> list = new ArrayList<>();
for (ScopeChild entry : map2.values()) {
final RelDataTypeField field =
liberalMatcher.field(entry.namespace.getRowType(),
columnName);
if (field == null) {
continue;
}
list.add(field.getName());
}
Collections.sort(list);
final List<String> caseInsensitiveSuggestions =
findCaseInsensitiveColumnSuggestions(columnName, identifier);
if (!caseInsensitiveSuggestions.isEmpty()) {
throw validator.newValidationError(identifier,
RESOURCE.columnNotFoundDidYouMean(columnName,
Util.sepList(list, "', '")));
Util.sepList(caseInsensitiveSuggestions, "', '")));
}
final List<String> suggestions = findColumnSuggestions(columnName);
if (!suggestions.isEmpty()) {
throw validator.newValidationError(identifier,
RESOURCE.columnNotFoundDidYouMean(columnName,
Util.sepList(suggestions, "', '")));
}
}
throw validator.newValidationError(identifier,
Expand Down Expand Up @@ -342,14 +456,11 @@
}
// Look for a table alias that is the wrong case.
if (nameMatcher.isCaseSensitive()) {
final SqlNameMatcher liberalMatcher = SqlNameMatchers.liberal();
resolved.clear();
resolve(prefix.names, liberalMatcher, false, resolved);
if (resolved.count() == 1) {
final Step lastStep = Util.last(resolved.only().path.steps());
final @Nullable String suggestion = findTableSuggestion(prefix);
if (suggestion != null) {
throw validator.newValidationError(prefix,
RESOURCE.tableNameNotFoundDidYouMean(prefix.toString(),
lastStep.name));
suggestion));
}
}
}
Expand All @@ -361,6 +472,18 @@
switch (map.size()) {
default:
final SqlIdentifier prefix1 = identifier.skipLast(1);
if (nameMatcher.isCaseSensitive()) {
final @Nullable String suggestion =
fromNs instanceof SchemaNamespace
? SqlNameMatchers.bestObjectMatch(Util.last(prefix1.names),
fieldNames(fromNs.getRowType()))
: findTableSuggestion(prefix1);
if (suggestion != null) {
throw validator.newValidationError(prefix1,
RESOURCE.tableNameNotFoundDidYouMean(prefix1.toString(),
suggestion));
}
}
throw validator.newValidationError(prefix1,
RESOURCE.tableNameNotFound(prefix1.toString()));
case 1: {
Expand Down Expand Up @@ -439,20 +562,28 @@
final Path path;
switch (resolved.count()) {
case 0:
// Maybe the last component was correct, just wrong case
if (nameMatcher.isCaseSensitive()) {
SqlNameMatcher liberalMatcher = SqlNameMatchers.liberal();
resolved.clear();
resolveInNamespace(fromNs, false, suffix.names, liberalMatcher,
Path.EMPTY, resolved);
if (resolved.count() > 0) {
final @Nullable String caseInsensitiveSuggestion =
findCaseInsensitiveFieldSuggestion(requireNonNull(fromNs, "fromNs"),
suffix.names);
if (caseInsensitiveSuggestion != null) {
int k = size - 1;
final SqlIdentifier prefix = identifier.getComponent(0, i);
final SqlIdentifier suffix3 = identifier.getComponent(i, k + 1);
throw validator.newValidationError(suffix3,
RESOURCE.columnNotFoundInTableDidYouMean(suffix3.toString(),
prefix.toString(), caseInsensitiveSuggestion));
}
final @Nullable String suggestion =
findFieldSuggestion(requireNonNull(fromNs, "fromNs"), nameMatcher,
suffix.names);
if (suggestion != null) {
int k = size - 1;
final SqlIdentifier prefix = identifier.getComponent(0, i);
final SqlIdentifier suffix3 = identifier.getComponent(i, k + 1);
final Step step = Util.last(resolved.resolves.get(0).path.steps());
throw validator.newValidationError(suffix3,
RESOURCE.columnNotFoundInTableDidYouMean(suffix3.toString(),
prefix.toString(), step.name));
prefix.toString(), suggestion));
}
}
// Find the shortest suffix that also fails. Suppose we cannot resolve
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.ArrayList;
import java.util.List;

import static org.apache.calcite.util.Static.RESOURCE;
Expand Down Expand Up @@ -175,11 +176,42 @@ private SqlValidatorNamespace resolveImpl(SqlIdentifier id) {
SqlIdentifier.getString(prefix), next));
}
} else {
final String missingName = resolve.remainingNames.get(0);
final List<SqlMoniker> hints = new ArrayList<>();
SqlValidatorUtil.getSchemaObjectMonikers(validator.catalogReader, names,
hints);
final List<String> candidateNames = new ArrayList<>();
for (SqlMoniker hint : hints) {
final List<String> hintNames = hint.getFullyQualifiedNames();
candidateNames.add(hintNames.get(hintNames.size() - 1));
}
final String suggestion =
SqlNameMatchers.bestObjectMatch(missingName, candidateNames);
if (suggestion != null) {
throw validator.newValidationError(id,
RESOURCE.objectNotFoundWithinDidYouMean(missingName,
SqlIdentifier.getString(resolve.path.stepNames()),
suggestion));
}
throw validator.newValidationError(id,
RESOURCE.objectNotFoundWithin(resolve.remainingNames.get(0),
RESOURCE.objectNotFoundWithin(missingName,
SqlIdentifier.getString(resolve.path.stepNames())));
}
}
final SqlNameMatchers.NameSuggestion suggestion =
SqlNameMatchers.bestObjectName(validator.catalogReader, names);
if (suggestion != null) {
if (suggestion.prefixNames.isEmpty()) {
throw validator.newValidationError(id,
RESOURCE.objectNotFoundDidYouMean(suggestion.name,
suggestion.suggestion));
} else {
throw validator.newValidationError(id,
RESOURCE.objectNotFoundWithinDidYouMean(suggestion.name,
SqlIdentifier.getString(suggestion.prefixNames),
suggestion.suggestion));
}
}
}
throw validator.newValidationError(id,
RESOURCE.objectNotFound(id.getComponent(0).toString()));
Expand Down
Loading
Loading