DelegatingScope.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.calcite.sql.validate;
import org.apache.calcite.prepare.Prepare;
import org.apache.calcite.rel.type.DynamicRecordType;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rel.type.StructKind;
import org.apache.calcite.schema.CustomColumnResolvingTable;
import org.apache.calcite.schema.Table;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlLambda;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.SqlSelect;
import org.apache.calcite.sql.SqlWindow;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.util.Pair;
import org.apache.calcite.util.Util;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import org.checkerframework.checker.nullness.qual.Nullable;
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;
import java.util.function.Supplier;
import static com.google.common.base.Preconditions.checkArgument;
import static org.apache.calcite.sql.validate.SqlNonNullableAccessors.getSelectList;
import static org.apache.calcite.util.Static.RESOURCE;
import static java.util.Objects.requireNonNull;
/**
* A scope which delegates all requests to its parent scope. Use this as a base
* class for defining nested scopes.
*/
public abstract class DelegatingScope implements SqlValidatorScope {
//~ Instance fields --------------------------------------------------------
/**
* Parent scope. This is where to look next to resolve an identifier; it is
* not always the parent object in the parse tree.
*
* <p>This is never null: at the top of the tree, it is an
* {@link EmptyScope}.
*/
protected final SqlValidatorScope parent;
protected final SqlValidatorImpl validator;
/** Computes and stores information that cannot be computed on construction,
* but only after sub-queries have been validated. */
@SuppressWarnings({"methodref.receiver.bound.invalid"})
public final Supplier<AggregatingSelectScope.Resolved> resolved =
Suppliers.memoize(this::resolve);
/** Use while resolving. */
SqlValidatorUtil.@Nullable GroupAnalyzer groupAnalyzer;
//~ Constructors -----------------------------------------------------------
/**
* Creates a <code>DelegatingScope</code>.
*
* @param parent Parent scope
*/
DelegatingScope(SqlValidatorScope parent) {
super();
this.parent = requireNonNull(parent, "parent");
this.validator = (SqlValidatorImpl) parent.getValidator();
}
//~ Methods ----------------------------------------------------------------
@Override public void addChild(SqlValidatorNamespace ns, String alias,
boolean nullable) {
// By default, you cannot add to a scope. Derived classes can
// override.
throw new UnsupportedOperationException();
}
@Override public void resolve(List<String> names, SqlNameMatcher nameMatcher,
boolean deep, Resolved resolved) {
parent.resolve(names, nameMatcher, deep, resolved);
}
/** If a record type allows implicit references to fields, recursively looks
* into the fields. Otherwise, returns immediately. */
void resolveInNamespace(SqlValidatorNamespace ns, boolean nullable,
List<String> names, SqlNameMatcher nameMatcher, Path path,
Resolved resolved) {
if (names.isEmpty()) {
resolved.found(ns, nullable, this, path, names);
return;
}
final RelDataType rowType = ns.getRowType();
if (rowType.isStruct()) {
SqlValidatorTable validatorTable = ns.getTable();
if (validatorTable instanceof Prepare.PreparingTable) {
Table t = ((Prepare.PreparingTable) validatorTable).unwrap(Table.class);
if (t instanceof CustomColumnResolvingTable) {
final List<Pair<RelDataTypeField, List<String>>> entries =
((CustomColumnResolvingTable) t).resolveColumn(
rowType, validator.getTypeFactory(), names);
for (Pair<RelDataTypeField, List<String>> entry : entries) {
final RelDataTypeField field = entry.getKey();
final List<String> remainder = entry.getValue();
final SqlValidatorNamespace ns2 =
new FieldNamespace(validator, field.getType());
final Step path2 =
path.plus(rowType, field.getIndex(), field.getName(),
StructKind.FULLY_QUALIFIED);
resolveInNamespace(ns2, nullable, remainder, nameMatcher, path2,
resolved);
}
return;
}
}
final String name = names.get(0);
final RelDataTypeField field0 = nameMatcher.field(rowType, name);
if (field0 != null) {
final SqlValidatorNamespace ns2 =
requireNonNull(ns.lookupChild(field0.getName()),
() -> "field " + field0.getName() + " is not found in " + ns);
final Step path2 =
path.plus(rowType, field0.getIndex(),
field0.getName(), StructKind.FULLY_QUALIFIED);
resolveInNamespace(ns2, nullable, names.subList(1, names.size()),
nameMatcher, path2, resolved);
} else {
for (RelDataTypeField field : rowType.getFieldList()) {
switch (field.getType().getStructKind()) {
case PEEK_FIELDS:
case PEEK_FIELDS_DEFAULT:
case PEEK_FIELDS_NO_EXPAND:
final Step path2 =
path.plus(rowType, field.getIndex(),
field.getName(), field.getType().getStructKind());
final SqlValidatorNamespace ns2 =
requireNonNull(ns.lookupChild(field.getName()),
() -> "field " + field.getName() + " is not found in " + ns);
resolveInNamespace(ns2, nullable, names, nameMatcher, path2,
resolved);
break;
default:
break;
}
}
}
}
}
protected void addColumnNames(
SqlValidatorNamespace ns,
List<SqlMoniker> colNames) {
final RelDataType rowType;
try {
rowType = ns.getRowType();
} catch (Error e) {
// namespace is not good - bail out.
return;
}
for (RelDataTypeField field : rowType.getFieldList()) {
colNames.add(
new SqlMonikerImpl(
field.getName(),
SqlMonikerType.COLUMN));
}
}
@Override public void findAllColumnNames(List<SqlMoniker> result) {
parent.findAllColumnNames(result);
}
@Override public void findAliases(Collection<SqlMoniker> result) {
parent.findAliases(result);
}
@SuppressWarnings("deprecation")
@Override public Pair<String, SqlValidatorNamespace> findQualifyingTableName(
String columnName, SqlNode ctx) {
//noinspection deprecation
return parent.findQualifyingTableName(columnName, ctx);
}
@Override public Map<String, ScopeChild> findQualifyingTableNames(String columnName,
SqlNode ctx, SqlNameMatcher nameMatcher) {
return parent.findQualifyingTableNames(columnName, ctx, nameMatcher);
}
@Override public @Nullable RelDataType resolveColumn(String name, SqlNode ctx) {
return parent.resolveColumn(name, ctx);
}
@Override public RelDataType nullifyType(SqlNode node, RelDataType type) {
return parent.nullifyType(node, type);
}
@SuppressWarnings("deprecation")
@Override public @Nullable SqlValidatorNamespace getTableNamespace(List<String> names) {
return parent.getTableNamespace(names);
}
@Override public void resolveTable(List<String> names, SqlNameMatcher nameMatcher,
Path path, Resolved resolved) {
parent.resolveTable(names, nameMatcher, path, resolved);
}
@Override public SqlValidatorScope getOperandScope(SqlCall call) {
if (call instanceof SqlSelect) {
return validator.getSelectScope((SqlSelect) call);
} else if (call instanceof SqlLambda) {
return validator.getLambdaScope((SqlLambda) call);
}
return this;
}
@Override public SqlValidator getValidator() {
return validator;
}
/**
* Converts an identifier into a fully-qualified identifier. For example,
* the "empno" in "select empno from emp natural join dept" becomes
* "emp.empno".
*
* <p>If the identifier cannot be resolved, throws. Never returns null.
*/
@Override public SqlQualified fullyQualify(SqlIdentifier identifier) {
if (identifier.isStar()) {
return SqlQualified.create(this, 1, null, identifier);
}
final SqlIdentifier previous = identifier;
final SqlNameMatcher nameMatcher = validator.catalogReader.nameMatcher();
String columnName;
final String tableName;
final SqlValidatorNamespace namespace;
switch (identifier.names.size()) {
case 1: {
columnName = identifier.names.get(0);
final Map<String, ScopeChild> map =
findQualifyingTableNames(columnName, identifier, nameMatcher);
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);
throw validator.newValidationError(identifier,
RESOURCE.columnNotFoundDidYouMean(columnName,
Util.sepList(list, "', '")));
}
}
throw validator.newValidationError(identifier,
RESOURCE.columnNotFound(columnName));
case 1:
tableName = map.keySet().iterator().next();
namespace = map.get(tableName).namespace;
break;
default:
throw validator.newValidationError(identifier,
RESOURCE.columnAmbiguous(columnName));
}
final ResolvedImpl resolved = new ResolvedImpl();
resolveInNamespace(namespace, false, identifier.names, nameMatcher,
Path.EMPTY, resolved);
final RelDataTypeField field =
nameMatcher.field(namespace.getRowType(), columnName);
if (field != null) {
if (hasAmbiguousField(namespace.getRowType(), field,
columnName, nameMatcher)) {
throw validator.newValidationError(identifier,
RESOURCE.columnAmbiguous(columnName));
}
columnName = field.getName(); // use resolved field name
}
// todo: do implicit collation here
final SqlParserPos pos = identifier.getParserPosition();
identifier =
new SqlIdentifier(ImmutableList.of(tableName, columnName), null,
pos, ImmutableList.of(SqlParserPos.ZERO, pos));
}
// fall through
default: {
SqlValidatorNamespace fromNs = null;
Path fromPath = null;
RelDataType fromRowType = null;
final ResolvedImpl resolved = new ResolvedImpl();
int size = identifier.names.size();
int i = size - 1;
for (; i > 0; i--) {
final SqlIdentifier prefix = identifier.getComponent(0, i);
resolved.clear();
resolve(prefix.names, nameMatcher, false, resolved);
if (resolved.count() == 1) {
final Resolve resolve = resolved.only();
fromNs = resolve.namespace;
fromPath = resolve.path;
fromRowType = resolve.rowType();
break;
}
// 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());
throw validator.newValidationError(prefix,
RESOURCE.tableNameNotFoundDidYouMean(prefix.toString(),
lastStep.name));
}
}
}
if (fromNs == null || fromNs instanceof SchemaNamespace) {
// Look for a column not qualified by a table alias.
columnName = identifier.names.get(0);
final Map<String, ScopeChild> map =
findQualifyingTableNames(columnName, identifier, nameMatcher);
switch (map.size()) {
default:
final SqlIdentifier prefix1 = identifier.skipLast(1);
throw validator.newValidationError(prefix1,
RESOURCE.tableNameNotFound(prefix1.toString()));
case 1: {
final Map.Entry<String, ScopeChild> entry =
map.entrySet().iterator().next();
final String tableName2 = map.keySet().iterator().next();
fromNs = entry.getValue().namespace;
fromPath = Path.EMPTY;
// Adding table name is for RecordType column with StructKind.PEEK_FIELDS or
// StructKind.PEEK_FIELDS only. Access to a field in a RecordType column of
// other StructKind should always be qualified with table name.
final RelDataTypeField field =
nameMatcher.field(fromNs.getRowType(), columnName);
if (field != null) {
switch (field.getType().getStructKind()) {
case PEEK_FIELDS:
case PEEK_FIELDS_DEFAULT:
case PEEK_FIELDS_NO_EXPAND:
columnName = field.getName(); // use resolved field name
resolve(ImmutableList.of(tableName2), nameMatcher, false,
resolved);
if (resolved.count() == 1) {
final Resolve resolve = resolved.only();
fromNs = resolve.namespace;
fromPath = resolve.path;
fromRowType = resolve.rowType();
identifier = identifier
.setName(0, columnName)
.add(0, tableName2, SqlParserPos.ZERO);
++i;
++size;
}
break;
default:
// Throw an error if the table was not found.
// If one or more of the child namespaces allows peeking
// (e.g. if they are Phoenix column families) then we relax the SQL
// standard requirement that record fields are qualified by table alias.
final SqlIdentifier prefix = identifier.skipLast(1);
throw validator.newValidationError(prefix,
RESOURCE.tableNameNotFound(prefix.toString()));
}
}
}
}
}
// If a table alias is part of the identifier, make sure that the table
// alias uses the same case as it was defined. For example, in
//
// SELECT e.empno FROM Emp as E
//
// change "e.empno" to "E.empno".
if (fromNs.getEnclosingNode() != null
&& !(this instanceof MatchRecognizeScope)) {
@Nullable String alias =
SqlValidatorUtil.alias(fromNs.getEnclosingNode());
if (alias != null
&& i > 0
&& !alias.equals(identifier.names.get(i - 1))) {
identifier = identifier.setName(i - 1, alias);
}
}
if (requireNonNull(fromPath, "fromPath").stepCount() > 1) {
requireNonNull(fromRowType, "fromRowType");
for (Step p : fromPath.steps()) {
fromRowType = fromRowType.getFieldList().get(p.i).getType();
}
++i;
}
final SqlIdentifier suffix = identifier.getComponent(i, size);
resolved.clear();
resolveInNamespace(fromNs, false, suffix.names, nameMatcher, Path.EMPTY,
resolved);
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) {
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));
}
}
// Find the shortest suffix that also fails. Suppose we cannot resolve
// "a.b.c"; we find we cannot resolve "a.b" but can resolve "a". So,
// the error will be "Column 'a.b' not found".
int k = size - 1;
for (; k > i; --k) {
SqlIdentifier suffix2 = identifier.getComponent(i, k);
resolved.clear();
resolveInNamespace(fromNs, false, suffix2.names, nameMatcher,
Path.EMPTY, resolved);
if (resolved.count() > 0) {
break;
}
}
final SqlIdentifier prefix = identifier.getComponent(0, i);
final SqlIdentifier suffix3 = identifier.getComponent(i, k + 1);
throw validator.newValidationError(suffix3,
RESOURCE.columnNotFoundInTable(suffix3.toString(), prefix.toString()));
case 1:
path = resolved.only().path;
break;
default:
final Comparator<Resolve> c =
new Comparator<Resolve>() {
@Override public int compare(Resolve o1, Resolve o2) {
// Name resolution that uses fewer implicit steps wins.
int c = Integer.compare(worstKind(o1.path), worstKind(o2.path));
if (c != 0) {
return c;
}
// Shorter path wins
return Integer.compare(o1.path.stepCount(), o2.path.stepCount());
}
private int worstKind(Path path) {
int kind = -1;
for (Step step : path.steps()) {
kind = Math.max(kind, step.kind.ordinal());
}
return kind;
}
};
resolved.resolves.sort(c);
if (c.compare(resolved.resolves.get(0), resolved.resolves.get(1)) == 0) {
throw validator.newValidationError(suffix,
RESOURCE.columnAmbiguous(suffix.toString()));
}
path = resolved.resolves.get(0).path;
}
// Normalize case to match definition, make elided fields explicit,
// and check that references to dynamic stars ("**") are unambiguous.
int k = i;
for (Step step : path.steps()) {
final String name = identifier.names.get(k);
if (step.i < 0) {
throw validator.newValidationError(
identifier, RESOURCE.columnNotFound(name));
}
final RelDataTypeField field0 =
requireNonNull(step.rowType, () -> "rowType of step " + step.name)
.getFieldList().get(step.i);
final String fieldName = field0.getName();
switch (step.kind) {
case PEEK_FIELDS:
case PEEK_FIELDS_DEFAULT:
case PEEK_FIELDS_NO_EXPAND:
identifier = identifier.add(k, fieldName, SqlParserPos.ZERO);
break;
default:
if (!fieldName.equals(name)) {
identifier = identifier.setName(k, fieldName);
}
if (hasAmbiguousField(step.rowType, field0, name, nameMatcher)) {
throw validator.newValidationError(identifier,
RESOURCE.columnAmbiguous(name));
}
}
++k;
}
// Multiple name components may have been resolved as one step by
// CustomResolvingTable.
if (identifier.names.size() > k) {
identifier = identifier.getComponent(0, k);
}
if (i > 1) {
// Simplify overqualified identifiers.
// For example, schema.emp.deptno becomes emp.deptno.
//
// It is safe to convert schema.emp or database.schema.emp to emp
// because it would not have resolved if the FROM item had an alias. The
// following query is invalid:
// SELECT schema.emp.deptno FROM schema.emp AS e
identifier = identifier.getComponent(i - 1, identifier.names.size());
}
if (!previous.equals(identifier)) {
validator.setOriginal(identifier, previous);
}
return SqlQualified.create(this, i, fromNs, identifier);
}
}
}
@Override public void validateExpr(SqlNode expr) {
// Do not delegate to parent. An expression valid in this scope may not
// be valid in the parent scope.
}
@Override public @Nullable SqlWindow lookupWindow(String name) {
return parent.lookupWindow(name);
}
@Override public SqlMonotonicity getMonotonicity(SqlNode expr) {
return parent.getMonotonicity(expr);
}
@Override public @Nullable SqlNodeList getOrderList() {
return parent.getOrderList();
}
/** Returns whether {@code rowType} contains more than one star column or
* fields with the same name, which implies ambiguous column. */
private static boolean hasAmbiguousField(RelDataType rowType,
RelDataTypeField field, String columnName, SqlNameMatcher nameMatcher) {
if (field.isDynamicStar()
&& !DynamicRecordType.isDynamicStarColName(columnName)) {
int count = 0;
for (RelDataTypeField possibleStar : rowType.getFieldList()) {
if (possibleStar.isDynamicStar()) {
if (++count > 1) {
return true;
}
}
}
return false;
} else { // check if there are fields with the same name
int count = 0;
for (RelDataTypeField f : rowType.getFieldList()) {
if (Util.matches(nameMatcher.isCaseSensitive(), f.getName(), columnName)) {
count++;
}
}
return count > 1;
}
}
private AggregatingSelectScope.Resolved resolve() {
checkArgument(groupAnalyzer == null, "resolve already in progress");
SqlValidatorUtil.GroupAnalyzer groupAnalyzer = new SqlValidatorUtil.GroupAnalyzer();
this.groupAnalyzer = groupAnalyzer;
try {
analyze(groupAnalyzer);
return groupAnalyzer.finish();
} finally {
this.groupAnalyzer = null;
}
}
/** Analyzes expressions in this scope and populates a
* {@code GroupAnalyzer}. */
protected void analyze(SqlValidatorUtil.GroupAnalyzer analyzer) {
final SelectScope selectScope = SqlValidatorUtil.getEnclosingSelectScope(this);
if (selectScope != null) {
// Find all expressions in this scope that reference measures
for (ScopeChild child : selectScope.children) {
final RelDataType rowType = child.namespace.getRowType();
if (child.namespace instanceof SelectNamespace) {
final SqlSelect select = ((SelectNamespace) child.namespace).getNode();
Pair.forEach(select.getSelectList(),
rowType.getFieldList(),
(selectItem, field) -> {
if (SqlValidatorUtil.isMeasure(selectItem)) {
analyzer.measureExprs.add(
new SqlIdentifier(
Arrays.asList(child.name, field.getName()),
SqlParserPos.ZERO));
}
});
} else {
rowType.getFieldList().forEach(field -> {
if (field.getType().isMeasure()) {
analyzer.measureExprs.add(
new SqlIdentifier(
Arrays.asList(child.name, field.getName()),
SqlParserPos.ZERO));
}
});
}
}
}
}
/**
* Returns the parent scope of this <code>DelegatingScope</code>.
*/
public SqlValidatorScope getParent() {
return parent;
}
/** Qualifies an identifier by looking for an alias in the current
* select-list.
*
* <p>Used when resolving ORDER BY items (when the conformance allows order by
* alias, such as "SELECT x - y AS z FROM t ORDER BY z") and measures
* (when one measure refers to another, for example
* "SELECT SUM(x) AS MEASURE m1, SUM(y) - m1 AS MEASURE m2 FROM t"). */
protected @Nullable SqlQualified qualifyUsingAlias(SqlSelect select,
SqlIdentifier identifier) {
final String name = identifier.names.get(0);
final SqlNameMatcher nameMatcher = validator.catalogReader.nameMatcher();
final int aliasCount = aliasCount(select, nameMatcher, name);
switch (aliasCount) {
case 0:
return null;
case 1:
final SqlValidatorNamespace selectNs =
validator.getNamespaceOrThrow(select);
return SqlQualified.create(this, 1, selectNs, identifier);
default:
// More than one column has this alias.
throw validator.newValidationError(identifier,
RESOURCE.columnAmbiguous(name));
}
}
/** Returns the number of columns in the SELECT clause that have {@code name}
* as their implicit (e.g. {@code t.name}) or explicit (e.g.
* {@code t.c as name}) alias. */
private static int aliasCount(SqlSelect select, SqlNameMatcher nameMatcher,
String name) {
int n = 0;
for (SqlNode s : getSelectList(select)) {
final @Nullable String alias = SqlValidatorUtil.alias(s);
if (alias != null && nameMatcher.matches(alias, name)) {
n++;
}
}
return n;
}
}