| /* |
| * 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.sql.SqlCall; |
| import org.apache.calcite.sql.SqlIdentifier; |
| import org.apache.calcite.sql.SqlKind; |
| import org.apache.calcite.sql.SqlNode; |
| import org.apache.calcite.sql.SqlNodeList; |
| import org.apache.calcite.sql.parser.SqlParserPos; |
| import org.apache.calcite.util.Pair; |
| |
| import com.google.common.collect.ImmutableList; |
| |
| import org.checkerframework.checker.nullness.qual.MonotonicNonNull; |
| import org.checkerframework.checker.nullness.qual.Nullable; |
| |
| import java.util.List; |
| |
| import static org.apache.calcite.util.Static.RESOURCE; |
| |
| import static java.util.Objects.requireNonNull; |
| |
| /** |
| * Namespace whose contents are defined by the type of an |
| * {@link org.apache.calcite.sql.SqlIdentifier identifier}. |
| */ |
| public class IdentifierNamespace extends AbstractNamespace { |
| //~ Instance fields -------------------------------------------------------- |
| |
| private final SqlIdentifier id; |
| private final SqlValidatorScope parentScope; |
| public final @Nullable SqlNodeList extendList; |
| |
| /** |
| * The underlying namespace. Often a {@link TableNamespace}. |
| * Set on validate. |
| */ |
| private @MonotonicNonNull SqlValidatorNamespace resolvedNamespace; |
| |
| /** |
| * List of monotonic expressions. Set on validate. |
| */ |
| private @Nullable List<Pair<SqlNode, SqlMonotonicity>> monotonicExprs; |
| |
| //~ Constructors ----------------------------------------------------------- |
| |
| /** |
| * Creates an IdentifierNamespace. |
| * |
| * @param validator Validator |
| * @param id Identifier node (or "identifier EXTEND column-list") |
| * @param extendList Extension columns, or null |
| * @param enclosingNode Enclosing node |
| * @param parentScope Parent scope which this namespace turns to in order to |
| */ |
| IdentifierNamespace(SqlValidatorImpl validator, SqlIdentifier id, |
| @Nullable SqlNodeList extendList, @Nullable SqlNode enclosingNode, |
| SqlValidatorScope parentScope) { |
| super(validator, enclosingNode); |
| this.id = id; |
| this.extendList = extendList; |
| this.parentScope = requireNonNull(parentScope, "parentScope"); |
| } |
| |
| IdentifierNamespace(SqlValidatorImpl validator, SqlNode node, |
| @Nullable SqlNode enclosingNode, SqlValidatorScope parentScope) { |
| this(validator, split(node).left, split(node).right, enclosingNode, |
| parentScope); |
| } |
| |
| //~ Methods ---------------------------------------------------------------- |
| |
| protected static Pair<SqlIdentifier, @Nullable SqlNodeList> split(SqlNode node) { |
| switch (node.getKind()) { |
| case EXTEND: |
| final SqlCall call = (SqlCall) node; |
| final SqlNode operand0 = call.operand(0); |
| final SqlIdentifier identifier = operand0.getKind() == SqlKind.TABLE_REF |
| ? ((SqlCall) operand0).operand(0) |
| : (SqlIdentifier) operand0; |
| return Pair.of(identifier, call.operand(1)); |
| case TABLE_REF: |
| final SqlCall tableRef = (SqlCall) node; |
| //noinspection ConstantConditions |
| return Pair.of(tableRef.operand(0), null); |
| default: |
| //noinspection ConstantConditions |
| return Pair.of((SqlIdentifier) node, null); |
| } |
| } |
| |
| private SqlValidatorNamespace resolveImpl(SqlIdentifier id) { |
| final SqlNameMatcher nameMatcher = validator.catalogReader.nameMatcher(); |
| final SqlValidatorScope.ResolvedImpl resolved = |
| new SqlValidatorScope.ResolvedImpl(); |
| final List<String> names = SqlIdentifier.toStar(id.names); |
| try { |
| parentScope.resolveTable(names, nameMatcher, |
| SqlValidatorScope.Path.EMPTY, resolved); |
| } catch (CyclicDefinitionException e) { |
| if (e.depth == 1) { |
| throw validator.newValidationError(id, |
| RESOURCE.cyclicDefinition(id.toString(), |
| SqlIdentifier.getString(e.path))); |
| } else { |
| throw new CyclicDefinitionException(e.depth - 1, e.path); |
| } |
| } |
| SqlValidatorScope.Resolve previousResolve = null; |
| if (resolved.count() == 1) { |
| final SqlValidatorScope.Resolve resolve = |
| previousResolve = resolved.only(); |
| if (resolve.remainingNames.isEmpty()) { |
| return resolve.namespace; |
| } |
| // If we're not case sensitive, give an error. |
| // If we're case sensitive, we'll shortly try again and give an error |
| // then. |
| if (!nameMatcher.isCaseSensitive()) { |
| throw validator.newValidationError(id, |
| RESOURCE.objectNotFoundWithin(resolve.remainingNames.get(0), |
| SqlIdentifier.getString(resolve.path.stepNames()))); |
| } |
| } |
| |
| // Failed to match. If we're matching case-sensitively, try a more |
| // lenient match. If we find something we can offer a helpful hint. |
| if (nameMatcher.isCaseSensitive()) { |
| final SqlNameMatcher liberalMatcher = SqlNameMatchers.liberal(); |
| resolved.clear(); |
| parentScope.resolveTable(names, liberalMatcher, |
| SqlValidatorScope.Path.EMPTY, resolved); |
| if (resolved.count() == 1) { |
| final SqlValidatorScope.Resolve resolve = resolved.only(); |
| if (resolve.remainingNames.isEmpty() |
| || previousResolve == null) { |
| // We didn't match it case-sensitive, so they must have had the |
| // right identifier, wrong case. |
| // |
| // If previousResolve is null, we matched nothing case-sensitive and |
| // everything case-insensitive, so the mismatch must have been at |
| // position 0. |
| final int i = |
| previousResolve == null ? 0 |
| : previousResolve.path.stepCount(); |
| final int offset = resolve.path.stepCount() |
| + resolve.remainingNames.size() - names.size(); |
| final List<String> prefix = |
| resolve.path.stepNames().subList(0, offset + i); |
| final String next = resolve.path.stepNames().get(i + offset); |
| if (prefix.isEmpty()) { |
| throw validator.newValidationError(id, |
| RESOURCE.objectNotFoundDidYouMean(names.get(i), next)); |
| } else { |
| throw validator.newValidationError(id, |
| RESOURCE.objectNotFoundWithinDidYouMean(names.get(i), |
| SqlIdentifier.getString(prefix), next)); |
| } |
| } else { |
| throw validator.newValidationError(id, |
| RESOURCE.objectNotFoundWithin(resolve.remainingNames.get(0), |
| SqlIdentifier.getString(resolve.path.stepNames()))); |
| } |
| } |
| } |
| throw validator.newValidationError(id, |
| RESOURCE.objectNotFound(id.getComponent(0).toString())); |
| } |
| |
| @Override public RelDataType validateImpl(RelDataType targetRowType) { |
| resolvedNamespace = resolveImpl(id); |
| validator.validateNamespace(resolvedNamespace, targetRowType); |
| |
| if (validator.config().identifierExpansion()) { |
| SqlValidatorTable table = resolvedNamespace.getTable(); |
| if (table != null) { |
| // TODO: expand qualifiers for column references also |
| List<String> qualifiedNames = table.getQualifiedName(); |
| // Assign positions to the components of the fully-qualified |
| // identifier, as best we can. We assume that qualification |
| // adds names to the front, e.g. FOO.BAR becomes BAZ.FOO.BAR. |
| // Test offset in case catalog supports fewer qualifiers than catalog |
| // reader. |
| ImmutableList.Builder<SqlParserPos> positions = |
| ImmutableList.builder(); |
| int offset = qualifiedNames.size() - id.names.size(); |
| for (int i = 0; i < qualifiedNames.size(); i++) { |
| positions.add(offset >= 0 && i >= offset |
| ? id.getComponentParserPosition(i - offset) |
| : id.getParserPosition()); |
| } |
| id.setNames(qualifiedNames, positions.build()); |
| } |
| } |
| |
| RelDataType rowType = resolvedNamespace.getRowType(); |
| |
| if (extendList != null) { |
| if (!(resolvedNamespace instanceof TableNamespace)) { |
| throw new RuntimeException("cannot convert"); |
| } |
| resolvedNamespace = |
| ((TableNamespace) resolvedNamespace).extend(extendList); |
| rowType = resolvedNamespace.getRowType(); |
| } |
| |
| // Build a list of monotonic expressions. |
| final ImmutableList.Builder<Pair<SqlNode, SqlMonotonicity>> builder = |
| ImmutableList.builder(); |
| List<RelDataTypeField> fields = rowType.getFieldList(); |
| for (RelDataTypeField field : fields) { |
| final String fieldName = field.getName(); |
| final SqlMonotonicity monotonicity = |
| resolvedNamespace.getMonotonicity(fieldName); |
| if (monotonicity != null && monotonicity != SqlMonotonicity.NOT_MONOTONIC) { |
| builder.add( |
| Pair.of(new SqlIdentifier(fieldName, SqlParserPos.ZERO), |
| monotonicity)); |
| } |
| } |
| monotonicExprs = builder.build(); |
| |
| // Validation successful. |
| return rowType; |
| } |
| |
| public SqlIdentifier getId() { |
| return id; |
| } |
| |
| @Override public @Nullable SqlNode getNode() { |
| return id; |
| } |
| |
| @Override public SqlValidatorNamespace resolve() { |
| assert resolvedNamespace != null : "must call validate first"; |
| return resolvedNamespace.resolve(); |
| } |
| |
| @Override public @Nullable SqlValidatorTable getTable() { |
| return resolvedNamespace == null ? null : resolve().getTable(); |
| } |
| |
| @Override public List<Pair<SqlNode, SqlMonotonicity>> getMonotonicExprs() { |
| List<Pair<SqlNode, SqlMonotonicity>> monotonicExprs = this.monotonicExprs; |
| return monotonicExprs == null ? ImmutableList.of() : monotonicExprs; |
| } |
| |
| @Override public SqlMonotonicity getMonotonicity(String columnName) { |
| final SqlValidatorTable table = getTable(); |
| if (table == null) { |
| return SqlMonotonicity.NOT_MONOTONIC; |
| } |
| return table.getMonotonicity(columnName); |
| } |
| |
| @Override public boolean supportsModality(SqlModality modality) { |
| final SqlValidatorTable table = getTable(); |
| if (table == null) { |
| return modality == SqlModality.RELATION; |
| } |
| return table.supportsModality(modality); |
| } |
| } |