MaterializedViewPlanValidator.java

/*
 * 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 com.facebook.presto.sql.analyzer;

import com.facebook.presto.sql.tree.AliasedRelation;
import com.facebook.presto.sql.tree.ComparisonExpression;
import com.facebook.presto.sql.tree.DefaultTraversalVisitor;
import com.facebook.presto.sql.tree.Join;
import com.facebook.presto.sql.tree.JoinCriteria;
import com.facebook.presto.sql.tree.JoinOn;
import com.facebook.presto.sql.tree.LogicalBinaryExpression;
import com.facebook.presto.sql.tree.OrderBy;
import com.facebook.presto.sql.tree.Query;
import com.facebook.presto.sql.tree.QuerySpecification;
import com.facebook.presto.sql.tree.SubqueryExpression;
import com.facebook.presto.sql.tree.Table;
import com.facebook.presto.sql.tree.Unnest;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import static com.facebook.presto.sql.analyzer.SemanticErrorCode.NOT_SUPPORTED;

// TODO: Add more cases https://github.com/prestodb/presto/issues/16032
public class MaterializedViewPlanValidator
        extends DefaultTraversalVisitor<Void, MaterializedViewPlanValidator.MaterializedViewPlanValidatorContext>
{
    protected MaterializedViewPlanValidator()
    {}

    public static void validate(Query viewQuery)
    {
        new MaterializedViewPlanValidator().process(viewQuery, new MaterializedViewPlanValidatorContext());
    }

    @Override
    protected Void visitTable(Table node, MaterializedViewPlanValidatorContext context)
    {
        // Materialized View Definition does not support have multiple instances of same table. We have this assumption throughout our codebase as we use it
        // for keys in several maps. For e.g. Partition mapping logic would need to be rewritten by considering partitions from each instance
        // of base table separately. We will need to use (table name + node location) as an identifier in all such places. For now, we just
        // forbid it.
        if (!context.addTable(node)) {
            throw new SemanticException(NOT_SUPPORTED, node, "Materialized View definition does not support multiple instances of same table");
        }

        return super.visitTable(node, context);
    }

    @Override
    protected Void visitQuery(Query node, MaterializedViewPlanValidatorContext context)
    {
        if (node.getLimit().isPresent()) {
            throw new SemanticException(NOT_SUPPORTED, node, "LIMIT clause in materialized view is not supported.");
        }
        return super.visitQuery(node, context);
    }

    @Override
    protected Void visitQuerySpecification(QuerySpecification node, MaterializedViewPlanValidatorContext context)
    {
        if (node.getLimit().isPresent()) {
            throw new SemanticException(NOT_SUPPORTED, node, "LIMIT clause in materialized view is not supported.");
        }
        return super.visitQuerySpecification(node, context);
    }

    @Override
    protected Void visitJoin(Join node, MaterializedViewPlanValidatorContext context)
    {
        context.pushJoinNode(node);

        JoinCriteria joinCriteria;
        switch (node.getType()) {
            case INNER:
                if (!node.getCriteria().isPresent()) {
                    throw new SemanticException(NOT_SUPPORTED, node, "Inner join with no criteria is not supported for materialized view.");
                }

                joinCriteria = node.getCriteria().get();
                if (!(joinCriteria instanceof JoinOn)) {
                    throw new SemanticException(NOT_SUPPORTED, node, "Only join-on is supported for materialized view.");
                }

                process(node.getLeft(), context);
                process(node.getRight(), context);

                context.setWithinJoinOn(true);
                process(((JoinOn) joinCriteria).getExpression(), context);
                context.setWithinJoinOn(false);

                break;
            case CROSS:
                if (!(node.getRight() instanceof AliasedRelation)) {
                    throw new SemanticException(NOT_SUPPORTED, node, "Cross join is supported only with unnest for materialized view.");
                }
                AliasedRelation right = (AliasedRelation) node.getRight();
                if (!(right.getRelation() instanceof Unnest)) {
                    throw new SemanticException(NOT_SUPPORTED, node, "Cross join is supported only with unnest for materialized view.");
                }

                process(node.getLeft(), context);

                break;

            case LEFT:
                if (!node.getCriteria().isPresent()) {
                    throw new SemanticException(NOT_SUPPORTED, node, "Outer join with no criteria is not supported for materialized view.");
                }

                joinCriteria = node.getCriteria().get();
                if (!(joinCriteria instanceof JoinOn)) {
                    throw new SemanticException(NOT_SUPPORTED, node, "Only join-on is supported for materialized view.");
                }

                process(node.getLeft(), context);

                boolean wasWithinOuterJoin = context.isWithinOuterJoin();
                context.setWithinOuterJoin(true);
                process(node.getRight(), context);
                // withinOuterJoin denotes if we are within an outer side of a join. Because it can be nested, replace it with its older value.
                // So we set it to false, only when we leave the topmost outer join.
                context.setWithinOuterJoin(wasWithinOuterJoin);

                context.setWithinJoinOn(true);
                process(((JoinOn) joinCriteria).getExpression(), context);
                context.setWithinJoinOn(false);

                break;

            default:
                throw new SemanticException(NOT_SUPPORTED, node, "Only inner join, left join and cross join unnested are supported for materialized view.");
        }

        context.popJoinNode();
        return null;
    }

    @Override
    protected Void visitLogicalBinaryExpression(LogicalBinaryExpression node, MaterializedViewPlanValidatorContext context)
    {
        if (context.isWithinJoinOn()) {
            if (!node.getOperator().equals(LogicalBinaryExpression.Operator.AND)) {
                throw new SemanticException(NOT_SUPPORTED, node, "Only AND operator is supported for join criteria for materialized view.");
            }
        }

        return super.visitLogicalBinaryExpression(node, context);
    }

    @Override
    protected Void visitComparisonExpression(ComparisonExpression node, MaterializedViewPlanValidatorContext context)
    {
        if (context.isWithinJoinOn()) {
            if (!node.getOperator().equals(ComparisonExpression.Operator.EQUAL)) {
                throw new SemanticException(NOT_SUPPORTED, node, "Only EQUAL join is supported for materialized view.");
            }
        }

        return super.visitComparisonExpression(node, context);
    }

    @Override
    protected Void visitSubqueryExpression(SubqueryExpression node, MaterializedViewPlanValidatorContext context)
    {
        throw new SemanticException(NOT_SUPPORTED, node, "Subqueries are not supported for materialized view.");
    }

    @Override
    protected Void visitOrderBy(OrderBy node, MaterializedViewPlanValidatorContext context)
    {
        throw new SemanticException(NOT_SUPPORTED, node, "OrderBy are not supported for materialized view.");
    }

    public static final class MaterializedViewPlanValidatorContext
    {
        private boolean isWithinJoinOn;
        private final LinkedList<Join> joinNodeStack;
        private boolean isWithinOuterJoin;
        private final HashSet<Table> tables;

        public MaterializedViewPlanValidatorContext()
        {
            isWithinJoinOn = false;
            joinNodeStack = new LinkedList<>();
            isWithinOuterJoin = false;
            tables = new HashSet<>();
        }

        public boolean isWithinJoinOn()
        {
            return isWithinJoinOn;
        }

        public void setWithinJoinOn(boolean withinJoinOn)
        {
            isWithinJoinOn = withinJoinOn;
        }

        public boolean isWithinOuterJoin()
        {
            return isWithinOuterJoin;
        }

        public void setWithinOuterJoin(boolean withinOuterJoin)
        {
            isWithinOuterJoin = withinOuterJoin;
        }

        public void pushJoinNode(Join join)
        {
            joinNodeStack.push(join);
        }

        public Join popJoinNode()
        {
            return joinNodeStack.pop();
        }

        public Join getTopJoinNode()
        {
            return joinNodeStack.getFirst();
        }

        public List<Join> getJoinNodes()
        {
            return ImmutableList.copyOf(joinNodeStack);
        }

        public boolean addTable(Table table)
        {
            return tables.add(table);
        }

        public Set<Table> getTables()
        {
            return ImmutableSet.copyOf(tables);
        }
    }
}