ListScope.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.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rel.type.StructKind;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.util.Pair;
import org.apache.calcite.util.Util;
import com.google.common.collect.ImmutableList;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.apache.calcite.util.Static.RESOURCE;
import static java.util.Objects.requireNonNull;
/**
* Abstract base for a scope which is defined by a list of child namespaces and
* which inherits from a parent scope.
*/
public abstract class ListScope extends DelegatingScope {
//~ Instance fields --------------------------------------------------------
/**
* List of child {@link SqlValidatorNamespace} objects and their names.
*/
public final List<ScopeChild> children = new ArrayList<>();
//~ Constructors -----------------------------------------------------------
protected ListScope(SqlValidatorScope parent) {
super(parent);
}
//~ Methods ----------------------------------------------------------------
@Override public void addChild(SqlValidatorNamespace ns, String alias,
boolean nullable) {
requireNonNull(alias, "alias");
children.add(new ScopeChild(children.size(), alias, ns, nullable));
}
/**
* Returns an immutable list of child namespaces.
*
* @return list of child namespaces
*/
public List<SqlValidatorNamespace> getChildren() {
return Util.transform(children, scopeChild -> scopeChild.namespace);
}
/**
* Returns an immutable list of child names.
*
* @return list of child namespaces
*/
List<@Nullable String> getChildNames() {
return Util.transform(children, scopeChild -> scopeChild.name);
}
/**
* Whether the ith child namespace produces nullable result.
*
* <p>For example, in below query,
*
* <blockquote><pre>{@code
* SELECT *
* FROM EMPS
* LEFT OUTER JOIN DEPT
* }</pre></blockquote>
*
* <p>the namespace which corresponding to 'DEPT' is nullable.
*
* @param i The child index.
* @return Whether it's nullable.
*/
public boolean isChildNullable(int i) {
return children.get(i).nullable;
}
private @Nullable ScopeChild findChild(List<String> names,
SqlNameMatcher nameMatcher) {
for (ScopeChild child : children) {
String lastName = Util.last(names);
if (child.name != null) {
if (!nameMatcher.matches(child.name, lastName)) {
// Alias does not match last segment. Don't consider the
// fully-qualified name. E.g.
// SELECT sales.emp.name FROM sales.emp AS otherAlias
continue;
}
if (names.size() == 1) {
return child;
}
}
// Make sure namespace has been validated.
validator.validateNamespace(child.namespace, validator.getUnknownType());
// Look up the 2 tables independently, in case one is qualified with
// catalog & schema and the other is not.
final SqlValidatorTable table = child.namespace.getTable();
if (table != null) {
final ResolvedImpl resolved = new ResolvedImpl();
resolveTable(names, nameMatcher, Path.EMPTY, resolved);
if (resolved.count() == 1) {
Resolve only = resolved.only();
List<String> qualifiedName = table.getQualifiedName();
if (only.remainingNames.isEmpty()
&& only.namespace instanceof TableNamespace
&& Objects.equals(qualifiedName, getQualifiedName(only.namespace.getTable()))) {
return child;
}
}
}
}
return null;
}
private static @Nullable List<String> getQualifiedName(@Nullable SqlValidatorTable table) {
return table == null ? null : table.getQualifiedName();
}
@Override public void findAllColumnNames(List<SqlMoniker> result) {
for (ScopeChild child : children) {
addColumnNames(child.namespace, result);
}
parent.findAllColumnNames(result);
}
@Override public void findAliases(Collection<SqlMoniker> result) {
for (ScopeChild child : children) {
result.add(new SqlMonikerImpl(child.name, SqlMonikerType.TABLE));
}
parent.findAliases(result);
}
@SuppressWarnings("deprecation")
@Override public Pair<String, SqlValidatorNamespace>
findQualifyingTableName(final String columnName, SqlNode ctx) {
final SqlNameMatcher nameMatcher = validator.catalogReader.nameMatcher();
final Map<String, ScopeChild> map =
findQualifyingTableNames(columnName, ctx, nameMatcher);
switch (map.size()) {
case 0:
throw validator.newValidationError(ctx,
RESOURCE.columnNotFound(columnName));
case 1:
final Map.Entry<String, ScopeChild> entry =
map.entrySet().iterator().next();
return Pair.of(entry.getKey(), entry.getValue().namespace);
default:
throw validator.newValidationError(ctx,
RESOURCE.columnAmbiguous(columnName));
}
}
@Override public Map<String, ScopeChild>
findQualifyingTableNames(String columnName, SqlNode ctx,
SqlNameMatcher nameMatcher) {
final Map<String, ScopeChild> map = new HashMap<>();
for (ScopeChild child : children) {
final ResolvedImpl resolved = new ResolvedImpl();
resolve(ImmutableList.of(child.name, columnName), nameMatcher, true,
resolved);
if (resolved.count() > 0) {
map.put(child.name, child);
}
}
switch (map.size()) {
case 0:
return parent.findQualifyingTableNames(columnName, ctx, nameMatcher);
default:
return map;
}
}
@Override public void resolve(List<String> names, SqlNameMatcher nameMatcher,
boolean deep, Resolved resolved) {
// First resolve by looking through the child namespaces.
final ScopeChild child0 = findChild(names, nameMatcher);
if (child0 != null) {
final Step path =
Path.EMPTY.plus(child0.namespace.getRowType(), child0.ordinal,
child0.name, StructKind.FULLY_QUALIFIED);
resolved.found(child0.namespace, child0.nullable, this, path,
ImmutableList.of());
return;
}
// Recursively look deeper into the record-valued fields of the namespace,
// if it allows skipping fields.
if (deep) {
for (ScopeChild child : children) {
// If identifier starts with table alias, remove the alias.
final List<String> names2 =
nameMatcher.matches(child.name, names.get(0))
? names.subList(1, names.size())
: names;
resolveInNamespace(child.namespace, child.nullable, names2, nameMatcher,
Path.EMPTY, resolved);
}
if (resolved.count() > 0) {
return;
}
}
// Then call the base class method, which will delegate to the
// parent scope.
super.resolve(names, nameMatcher, deep, resolved);
}
@Override public @Nullable RelDataType resolveColumn(String columnName, SqlNode ctx) {
final SqlNameMatcher nameMatcher = validator.catalogReader.nameMatcher();
int found = 0;
RelDataType type = null;
for (ScopeChild child : children) {
SqlValidatorNamespace childNs = child.namespace;
final RelDataType childRowType = childNs.getRowType();
final RelDataTypeField field =
nameMatcher.field(childRowType, columnName);
if (field != null) {
found++;
type = field.getType();
}
}
switch (found) {
case 0:
return null;
case 1:
return type;
default:
throw validator.newValidationError(ctx,
RESOURCE.columnAmbiguous(columnName));
}
}
}