All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.trino.sql.analyzer.Scope Maven / Gradle / Ivy

/*
 * Licensed 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 io.trino.sql.analyzer;

import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.Immutable;
import io.trino.spi.type.RowType;
import io.trino.sql.tree.AllColumns;
import io.trino.sql.tree.Expression;
import io.trino.sql.tree.QualifiedName;
import io.trino.sql.tree.WithQuery;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;

import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Iterables.getOnlyElement;
import static io.trino.spi.StandardErrorCode.AMBIGUOUS_NAME;
import static io.trino.sql.analyzer.ExpressionTreeUtils.asQualifiedName;
import static io.trino.sql.analyzer.Scope.BasisType.FIELD;
import static io.trino.sql.analyzer.Scope.BasisType.TABLE;
import static io.trino.sql.analyzer.SemanticExceptions.ambiguousAttributeException;
import static io.trino.sql.analyzer.SemanticExceptions.missingAttributeException;
import static io.trino.sql.analyzer.SemanticExceptions.semanticException;
import static java.util.Objects.requireNonNull;

@Immutable
public class Scope
{
    private final Optional parent;
    private final boolean queryBoundary;
    private final RelationId relationId;
    private final RelationType relation;
    private final Map namedQueries;

    public static Scope create()
    {
        return builder().build();
    }

    public static Builder builder()
    {
        return new Builder();
    }

    private Scope(
            Optional parent,
            boolean queryBoundary,
            RelationId relationId,
            RelationType relation,
            Map namedQueries)
    {
        this.parent = requireNonNull(parent, "parent is null");
        this.relationId = requireNonNull(relationId, "relationId is null");
        this.queryBoundary = queryBoundary;
        this.relation = requireNonNull(relation, "relation is null");
        this.namedQueries = ImmutableMap.copyOf(requireNonNull(namedQueries, "namedQueries is null"));
    }

    public Scope withRelationType(RelationType relationType)
    {
        return new Scope(parent, queryBoundary, relationId, relationType, namedQueries);
    }

    public Scope getQueryBoundaryScope()
    {
        Scope scope = this;
        Optional parent = scope.getLocalParent();
        while (parent.isPresent()) {
            scope = parent.get();
            parent = scope.getLocalParent();
        }
        return scope;
    }

    public Optional getOuterQueryParent()
    {
        Scope scope = this;
        while (scope.parent.isPresent()) {
            if (scope.queryBoundary) {
                return scope.parent;
            }
            scope = scope.parent.get();
        }

        return Optional.empty();
    }

    public boolean hasOuterParent(Scope parent)
    {
        return getOuterQueryParent()
                .map(scope -> scope.isLocalScope(parent) || scope.hasOuterParent(parent))
                .orElse(false);
    }

    public Optional getLocalParent()
    {
        if (!queryBoundary) {
            return parent;
        }

        return Optional.empty();
    }

    public int getLocalScopeFieldCount()
    {
        int parent = getLocalParent()
                .map(Scope::getLocalScopeFieldCount)
                .orElse(0);

        return parent + getRelationType().getAllFieldCount();
    }

    public RelationId getRelationId()
    {
        return relationId;
    }

    public RelationType getRelationType()
    {
        return relation;
    }

    /**
     * Starting from this, finds the closest scope which satisfies given predicate,
     * within the query boundary.
     */
    private Optional findLocally(Predicate match)
    {
        Scope scope = this;

        do {
            if (match.test(scope)) {
                return Optional.of(scope);
            }

            Optional parent = scope.getLocalParent();
            if (parent.isEmpty()) {
                break;
            }

            scope = parent.get();
        }
        while (true);

        return Optional.empty();
    }

    /**
     * This method analyzes asterisked identifier chain in 'all fields reference', according to SQL standard.
     * The following rules are applied:
     * 1. if the identifier chain's length is lower than 4, it is attempted to resolve it as a table reference,
     * either in the local scope or in outer scope.
     * 2. if the identifier chain's length is greater than 1, it is attempted to match the chain's prefix of length 2
     * to a field of type Row, either in the local scope or in outer scope.
     * Any available match in the innermost scope is picked and the 'identifier chain basis' is defined.
     * If more than one basis is defined, it results in a failure (ambiguous identifier chain).
     * 

* NOTE: only ambiguity between table reference and field reference is currently detected. * If two or more tables in the same scope match the identifier chain, they are indistinguishable in current Scope implementation, * and such query results in returning fields from all of those tables. * If two or more fields in the same scope match the identifier chain, such query will fail later during analysis. *

* NOTE: Although references to outer scope tables are found, referencing all fields from outer scope tables is not yet supported. */ public Optional resolveAsteriskedIdentifierChainBasis(QualifiedName identifierChain, AllColumns selectItem) { int length = identifierChain.getParts().size(); Optional scopeForTableReference = Optional.empty(); Optional scopeForFieldReference = Optional.empty(); if (length <= 3) { scopeForTableReference = findLocally(scope -> scope.getRelationType() .getAllFields().stream() .anyMatch(field -> field.matchesPrefix(Optional.of(identifierChain)))); } if (length >= 2) { scopeForFieldReference = findLocally(scope -> scope.getRelationType() .getAllFields().stream() .anyMatch(field -> field.matchesPrefix(Optional.of(QualifiedName.of(identifierChain.getParts().get(0)))) && field.getName().isPresent() && field.getName().get().equals(identifierChain.getParts().get(1)) && field.getType() instanceof RowType)); } if (scopeForTableReference.isPresent() && scopeForFieldReference.isPresent()) { throw semanticException(AMBIGUOUS_NAME, selectItem, "Reference '%s' is ambiguous", identifierChain); } if (scopeForTableReference.isPresent()) { return Optional.of(new AsteriskedIdentifierChainBasis(TABLE, scopeForTableReference, Optional.of(scopeForTableReference.get().getRelationType()))); } if (scopeForFieldReference.isPresent()) { return Optional.of(new AsteriskedIdentifierChainBasis(FIELD, Optional.empty(), Optional.empty())); } return getOuterQueryParent() .flatMap(parent -> parent.resolveAsteriskedIdentifierChainBasis(identifierChain, selectItem)); } // check if other is within the query boundary starting from this public boolean isLocalScope(Scope other) { return findLocally(scope -> scope.getRelationId().equals(other.getRelationId())).isPresent(); } public ResolvedField resolveField(Expression expression, QualifiedName name) { return tryResolveField(expression, name).orElseThrow(() -> missingAttributeException(expression, name)); } public Optional tryResolveField(Expression expression) { QualifiedName qualifiedName = asQualifiedName(expression); if (qualifiedName != null) { return tryResolveField(expression, qualifiedName); } return Optional.empty(); } public Optional tryResolveField(Expression node, QualifiedName name) { return resolveField(node, name, true); } private Optional resolveField(Expression node, QualifiedName name, boolean local) { List matches = relation.resolveFields(name); if (matches.size() > 1) { throw ambiguousAttributeException(node, name); } if (matches.size() == 1) { int parentFieldCount = getLocalParent() .map(Scope::getLocalScopeFieldCount) .orElse(0); Field field = getOnlyElement(matches); return Optional.of(asResolvedField(field, parentFieldCount, local)); } if (isColumnReference(name, relation)) { return Optional.empty(); } if (parent.isPresent()) { if (queryBoundary) { return parent.get().resolveField(node, name, false); } return parent.get().resolveField(node, name, local); } return Optional.empty(); } public ResolvedField getField(int index) { int parentFieldCount = getLocalParent() .map(Scope::getLocalScopeFieldCount) .orElse(0); return asResolvedField(relation.getFieldByIndex(index), parentFieldCount, true); } private ResolvedField asResolvedField(Field field, int fieldIndexOffset, boolean local) { int relationFieldIndex = relation.indexOf(field); int hierarchyFieldIndex = relation.indexOf(field) + fieldIndexOffset; return new ResolvedField(this, field, hierarchyFieldIndex, relationFieldIndex, local); } public boolean isColumnReference(QualifiedName name) { Scope current = this; while (current != null) { if (isColumnReference(name, current.relation)) { return true; } current = current.parent.orElse(null); } return false; } private static boolean isColumnReference(QualifiedName name, RelationType relation) { while (name.getPrefix().isPresent()) { name = name.getPrefix().get(); if (!relation.resolveFields(name).isEmpty()) { return true; } } return false; } public Optional getNamedQuery(String name) { if (namedQueries.containsKey(name)) { return Optional.of(namedQueries.get(name)); } if (parent.isPresent()) { return parent.get().getNamedQuery(name); } return Optional.empty(); } @Override public String toString() { return toStringHelper(this) .addValue(relationId) .toString(); } public static final class Builder { private RelationId relationId = RelationId.anonymous(); private RelationType relationType = new RelationType(); private final Map namedQueries = new HashMap<>(); private Optional parent = Optional.empty(); private boolean queryBoundary; public Builder like(Scope other) { relationId = other.relationId; relationType = other.relation; namedQueries.putAll(other.namedQueries); parent = other.parent; queryBoundary = other.queryBoundary; return this; } public Builder withRelationType(RelationId relationId, RelationType relationType) { this.relationId = requireNonNull(relationId, "relationId is null"); this.relationType = requireNonNull(relationType, "relationType is null"); return this; } public Builder withParent(Scope parent) { checkArgument(this.parent.isEmpty(), "parent is already set"); this.parent = Optional.of(parent); return this; } public Builder withOuterQueryParent(Scope parent) { checkArgument(this.parent.isEmpty(), "parent is already set"); this.parent = Optional.of(parent); this.queryBoundary = true; return this; } public Builder withNamedQuery(String name, WithQuery withQuery) { checkArgument(!containsNamedQuery(name), "Query '%s' is already added", name); namedQueries.put(name, withQuery); return this; } public boolean containsNamedQuery(String name) { return namedQueries.containsKey(name); } public Scope build() { return new Scope(parent, queryBoundary, relationId, relationType, namedQueries); } } public static class AsteriskedIdentifierChainBasis { private final BasisType basisType; private final Optional scope; private final Optional relationType; public AsteriskedIdentifierChainBasis(BasisType basisType, Optional scope, Optional relationType) { this.basisType = requireNonNull(basisType, "basisType is null"); this.scope = requireNonNull(scope, "scope is null"); this.relationType = requireNonNull(relationType, "relationType is null"); checkArgument(basisType == FIELD || scope.isPresent(), "missing scope"); checkArgument(basisType == FIELD || relationType.isPresent(), "missing relationType"); } public BasisType getBasisType() { return basisType; } public Optional getScope() { return scope; } public Optional getRelationType() { return relationType; } } enum BasisType { TABLE, FIELD } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy