InlineViewRef.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.
// This file is copied from
// https://github.com/apache/impala/blob/branch-2.9.0/fe/src/main/java/org/apache/impala/InlineViewRef.java
// and modified by Doris

package org.apache.doris.analysis;

import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.InlineView;
import org.apache.doris.catalog.View;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.UserException;
import org.apache.doris.nereids.parser.Dialect;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.rewrite.ExprRewriter;
import org.apache.doris.thrift.TNullSide;

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * An inline view is a query statement with an alias. Inline views can be parsed directly
 * from a query string or represent a reference to a local or catalog view.
 */
public class InlineViewRef extends TableRef {
    private static final Logger LOG = LogManager.getLogger(InlineViewRef.class);

    private static final String DEFAULT_TABLE_ALIAS_FOR_SPARK_SQL = "__auto_generated_subquery_name";

    // Catalog or local view that is referenced.
    // Null for inline views parsed directly from a query string.
    private final View view;

    // If not null, these will serve as the column labels for the inline view. This provides
    // a layer of separation between column labels visible from outside the inline view
    // and column labels used in the query definition. Either all or none of the column
    // labels must be overridden.
    private List<String> explicitColLabels;
    private List<List<String>> explicitSubColPath;

    // ///////////////////////////////////////
    // BEGIN: Members that need to be reset()

    // The select or union statement of the inline view
    private QueryStmt queryStmt;

    // queryStmt has its own analysis context
    private Analyzer inlineViewAnalyzer;

    // list of tuple ids materialized by queryStmt
    private final ArrayList<TupleId> materializedTupleIds = Lists.newArrayList();

    // Map inline view's output slots to the corresponding resultExpr of queryStmt.
    protected final ExprSubstitutionMap sMap;

    // Map inline view's output slots to the corresponding baseTblResultExpr of queryStmt.
    protected final ExprSubstitutionMap baseTblSmap;

    // When parsing a ddl of hive view, it does not contains any catalog info,
    // so we need to record it in Analyzer
    // otherwise some error will occurs when resolving TableRef later.
    protected String externalCtl;

    // END: Members that need to be reset()
    // ///////////////////////////////////////

    /**
     * C'tor for creating inline views parsed directly from the a query string.
     */
    public InlineViewRef(String alias, QueryStmt queryStmt) {
        super(null, alias);
        this.queryStmt = queryStmt;
        this.view = null;
        sMap = new ExprSubstitutionMap();
        baseTblSmap = new ExprSubstitutionMap();
    }

    public InlineViewRef(String alias, QueryStmt queryStmt, List<String> colLabels) {
        this(alias, queryStmt);
        explicitColLabels = Lists.newArrayList(colLabels);
        if (LOG.isDebugEnabled()) {
            LOG.debug("inline view explicitColLabels {}", explicitColLabels);
        }
    }

    /**
     * C'tor for creating inline views that replace a local or catalog view ref.
     */
    public InlineViewRef(View view, TableRef origTblRef) {
        super(origTblRef.getName(), origTblRef.getExplicitAlias());
        queryStmt = view.getQueryStmt().clone();
        if (view.isLocalView()) {
            queryStmt.reset();
        }
        this.view = view;
        sMap = new ExprSubstitutionMap();
        baseTblSmap = new ExprSubstitutionMap();
        setJoinAttrs(origTblRef);
        explicitColLabels = view.getColLabels();
        // Set implicit aliases if no explicit one was given.
        if (hasExplicitAlias()) {
            return;
        }
        // TODO(zc)
        // view_.getTableName().toString().toLowerCase(), view.getName().toLowerCase()
        if (view.isLocalView()) {
            aliases = new String[]{view.getName()};
        } else {
            aliases = new String[]{name.toString(), view.getName()};
        }
        if (origTblRef.getLateralViewRefs() != null) {
            lateralViewRefs = (ArrayList<LateralViewRef>) origTblRef.getLateralViewRefs().clone();
        }
    }

    protected InlineViewRef(InlineViewRef other) {
        super(other);
        queryStmt = other.queryStmt.clone();
        view = other.view;
        inlineViewAnalyzer = other.inlineViewAnalyzer;
        if (other.explicitColLabels != null) {
            explicitColLabels = Lists.newArrayList(other.explicitColLabels);
        }
        materializedTupleIds.addAll(other.materializedTupleIds);
        sMap = other.sMap.clone();
        baseTblSmap = other.baseTblSmap.clone();
    }

    public List<String> getExplicitColLabels() {
        return explicitColLabels;
    }

    public List<String> getColLabels() {
        if (explicitColLabels != null) {
            return explicitColLabels;
        }
        return queryStmt.getColLabels();
    }

    public List<List<String>> getSubColPath() {
        if (explicitSubColPath != null) {
            return explicitSubColPath;
        }
        return queryStmt.getSubColPath();
    }

    @Override
    public void reset() {
        super.reset();
        queryStmt.reset();
        inlineViewAnalyzer = null;
        materializedTupleIds.clear();
        sMap.clear();
        baseTblSmap.clear();
    }

    @Override
    public TableRef clone() {
        return new InlineViewRef(this);
    }

    public void setNeedToSql(boolean needToSql) {
        queryStmt.setNeedToSql(needToSql);
    }

    /**
     * Analyzes the inline view query block in a child analyzer of 'analyzer', creates
     * a new tuple descriptor for the inline view and registers auxiliary eq predicates
     * between the slots of that descriptor and the select list exprs of the inline view;
     * then performs join clause analysis.
     */
    @Override
    public void analyze(Analyzer analyzer) throws AnalysisException, UserException {
        if (isAnalyzed) {
            return;
        }

        if (view == null && !hasExplicitAlias()) {
            String dialect = ConnectContext.get().getSessionVariable().getSqlDialect();
            Dialect sqlDialect = Dialect.getByName(dialect);
            if (Dialect.SPARK != sqlDialect) {
                ErrorReport.reportAnalysisException(ErrorCode.ERR_DERIVED_MUST_HAVE_ALIAS);
            }
            hasExplicitAlias = true;
            aliases = new String[] { DEFAULT_TABLE_ALIAS_FOR_SPARK_SQL };
        }

        // Analyze the inline view query statement with its own analyzer
        inlineViewAnalyzer = new Analyzer(analyzer);
        inlineViewAnalyzer.setInlineView(true);
        if (hasExplicitAlias) {
            inlineViewAnalyzer.setExplicitViewAlias(aliases[0]);
        }
        queryStmt.analyze(inlineViewAnalyzer);
        correlatedTupleIds.addAll(queryStmt.getCorrelatedTupleIds(inlineViewAnalyzer));

        queryStmt.getMaterializedTupleIds(materializedTupleIds);
        if (view != null && !hasExplicitAlias() && !view.isLocalView()) {
            name = analyzer.getFqTableName(name);
            aliases = new String[] { name.toString(), view.getName() };
        }
        //TODO(chenhao16): fix TableName in Db.Table style
        // name.analyze(analyzer);
        desc = analyzer.registerTableRef(this);
        isAnalyzed = true;  // true now that we have assigned desc

        // For constant selects we materialize its exprs into a tuple.
        if (materializedTupleIds.isEmpty()) {
            Preconditions.checkState(queryStmt instanceof SelectStmt);
            Preconditions.checkState(((SelectStmt) queryStmt).getTableRefs().isEmpty());
            desc.setIsMaterialized(true);
            materializedTupleIds.add(desc.getId());
        }
        // create sMap and baseTblSmap and register auxiliary eq predicates between our
        // tuple descriptor's slots and our *unresolved* select list exprs;
        // we create these auxiliary predicates so that the analyzer can compute the value
        // transfer graph through this inline view correctly (ie, predicates can get
        // propagated through the view);
        // if the view stmt contains analytic functions, we cannot propagate predicates
        // into the view, unless the predicates are compatible with the analytic
        // function's partition by clause, because those extra filters
        // would alter the results of the analytic functions (see IMPALA-1243)
        // TODO: relax this a bit by allowing propagation out of the inline view (but
        // not into it)
        List<SlotDescriptor> slots = analyzer.changeSlotToNullableOfOuterJoinedTuples();
        if (LOG.isDebugEnabled()) {
            LOG.debug("inline view query {}", queryStmt.toSql());
        }
        for (int i = 0; i < getColLabels().size(); ++i) {
            String colName = getColLabels().get(i);
            if (LOG.isDebugEnabled()) {
                LOG.debug("inline view register {}", colName);
            }
            SlotDescriptor slotDesc = analyzer.registerColumnRef(getAliasAsName(),
                                            colName, getSubColPath().get(i));
            Expr colExpr = queryStmt.getResultExprs().get(i);
            if (queryStmt instanceof SelectStmt && ((SelectStmt) queryStmt).getValueList() != null) {
                ValueList valueList = ((SelectStmt) queryStmt).getValueList();
                for (int j = 0; j < valueList.getRows().size(); ++j) {
                    slotDesc.addSourceExpr(valueList.getRows().get(j).get(i));
                }
            } else {
                slotDesc.setSourceExpr(colExpr);
            }
            slotDesc.setIsNullable(slotDesc.getIsNullable() || colExpr.isNullable());
            SlotRef slotRef = new SlotRef(slotDesc);
            // to solve select * from (values(1, 2, 3), (4, 5, 6)) a returns only one row.
            if (slotDesc.getSourceExprs().size() == 1) {
                sMap.put(slotRef, colExpr);
                baseTblSmap.put(slotRef, queryStmt.getBaseTblResultExprs().get(i));
            }
            if (createAuxPredicate(colExpr)) {
                analyzer.createAuxEquivPredicate(new SlotRef(slotDesc), colExpr.clone());
            }
        }
        analyzer.changeSlotsToNotNullable(slots);
        if (LOG.isDebugEnabled()) {
            LOG.debug("inline view " + getUniqueAlias() + " smap: " + sMap.debugString());
            LOG.debug("inline view " + getUniqueAlias() + " baseTblSmap: " + baseTblSmap.debugString());
        }

        // analyzeLateralViewRefs
        analyzeLateralViewRef(analyzer);

        // Now do the remaining join analysis
        // In general, we should do analyze join before do RegisterColumnRef. However, We cannot move analyze join
        // before generate sMap and baseTblSmap, because generate sMap and baseTblSmap will register all column refs
        // in the inline view. If inline view is on right side of left semi join, exception will be thrown.
        // Instead, we do a little trick in RegisterColumnRef to avoid this problem.
        analyzeJoin(analyzer);
    }

    /**
     * Checks if an auxiliary predicate should be created for an expr. Returns False if the
     * inline view has a SELECT stmt with analytic functions and the expr is not in the
     * common partition exprs of all the analytic functions computed by this inline view.
     */
    public boolean createAuxPredicate(Expr e) {
        if (!(queryStmt instanceof SelectStmt)
                || !((SelectStmt) queryStmt).hasAnalyticInfo()) {
            return true;
        }
        AnalyticInfo analyticInfo = ((SelectStmt) queryStmt).getAnalyticInfo();
        return analyticInfo.getCommonPartitionExprs().contains(e);
    }

    /**
     * Create a non-materialized tuple descriptor in descTbl for this inline view.
     * This method is called from the analyzer when registering this inline view.
     */
    @Override
    public TupleDescriptor createTupleDescriptor(Analyzer analyzer) throws AnalysisException {
        // Create a fake catalog table for the inline view
        int numColLabels = getColLabels().size();
        Preconditions.checkState(numColLabels > 0);
        Set<String> columnSet = Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER);
        List<Column> columnList = Lists.newArrayList();
        for (int i = 0; i < numColLabels; ++i) {
            // inline view select statement has been analyzed. Col label should be filled.
            Expr selectItemExpr = queryStmt.getResultExprs().get(i);
            // String colAlias = queryStmt.getColLabels().get(i);
            String colAlias = getColLabels().get(i);

            // inline view col cannot have duplicate name
            if (columnSet.contains(colAlias)) {
                throw new AnalysisException(
                        "Duplicated inline view column alias: '" + colAlias + "'" + " in inline view "
                                + "'" + getAlias() + "'");
            }

            columnSet.add(colAlias);
            columnList.add(new Column(colAlias, selectItemExpr.getType(),
                    false, null, selectItemExpr.isNullable(),
                    null, ""));
        }
        InlineView inlineView = (view != null) ? new InlineView(view, columnList)
                : new InlineView(getExplicitAlias(), columnList);

        // Create the non-materialized tuple and set the fake table in it.
        TupleDescriptor result = analyzer.getDescTbl().createTupleDescriptor();
        result.setIsMaterialized(false);
        result.setTable(inlineView);
        analyzer.registerInlineViewTupleId(result.getId());
        return result;
    }

    /**
     * Makes each rhs expr in sMap nullable if necessary by wrapping as follows:
     * IF(TupleIsNull(), NULL, rhs expr)
     * Should be called only if this inline view is a nullable side of an outer join.
     * <p/>
     * We need to make an rhs exprs nullable if it evaluates to a non-NULL value
     * when all of its contained SlotRefs evaluate to NULL.
     * For example, constant exprs need to be wrapped or an expr such as
     * 'case slotref is null then 1 else 2 end'
     */
    //
    // protected void makeOutputNullable(Analyzer analyzer) throws AnalysisException, InternalException {
    //     // Gather all unique rhs SlotRefs into rhsSlotRefs
    //     List<SlotRef> rhsSlotRefs = Lists.newArrayList();
    //     Expr.collectList(sMap.rhs, SlotRef.class, rhsSlotRefs);
    //     // Map for substituting SlotRefs with NullLiterals.
    //     Expr.SubstitutionMap nullSMap = new Expr.SubstitutionMap();
    //     for (SlotRef rhsSlotRef : rhsSlotRefs) {
    //         nullSMap.lhs.add(rhsSlotRef.clone());
    //         nullSMap.rhs.add(NullLiteral.create(rhsSlotRef.getType()));
    //     }
    //
    //     // Make rhs exprs nullable if necessary.
    //     for (int i = 0; i < sMap.rhs.size(); ++i) {
    //         List<Expr> params = Lists.newArrayList();
    //         if (!requiresNullWrapping(analyzer, sMap.rhs.get(i), nullSMap)) {
    //             continue;
    //         }
    //         params.add(new TupleIsNullPredicate(materializedTupleIds));
    //         params.add(NullLiteral.create(sMap.rhs.get(i).getType()));
    //         params.add(sMap.rhs.get(i));
    //         Expr ifExpr = new FunctionCallExpr("if", params);
    //         ifExpr.analyze(analyzer);
    //         sMap.rhs.set(i, ifExpr);
    //     }
    // }

    protected void makeOutputNullable(Analyzer analyzer) throws AnalysisException, UserException {
        try {
            makeOutputNullableHelper(analyzer, sMap);
            makeOutputNullableHelper(analyzer, baseTblSmap);
        } catch (Exception e) {
            // should never happen
            throw new IllegalStateException(e);
        }
    }

    protected void makeOutputNullableHelper(Analyzer analyzer, ExprSubstitutionMap smap)
            throws Exception {
        // Gather all unique rhs SlotRefs into rhsSlotRefs
        List<SlotRef> rhsSlotRefs = Lists.newArrayList();
        Expr.collectList(smap.getRhs(), SlotRef.class, rhsSlotRefs);
        // Map for substituting SlotRefs with NullLiterals.
        ExprSubstitutionMap nullSMap = new ExprSubstitutionMap();
        for (SlotRef rhsSlotRef : rhsSlotRefs) {
            nullSMap.put(rhsSlotRef.clone(), NullLiteral.create(rhsSlotRef.getType()));
        }


        // Make rhs exprs nullable if necessary.
        for (int i = 0; i < smap.getRhs().size(); ++i) {
            List<Expr> params = Lists.newArrayList();
            if (!requiresNullWrapping(analyzer, smap.getRhs().get(i), nullSMap)) {
                continue;
            }
            if (analyzer.isOuterJoinedLeftSide(materializedTupleIds.get(0))) {
                params.add(new TupleIsNullPredicate(materializedTupleIds, TNullSide.LEFT));
            } else {
                params.add(new TupleIsNullPredicate(materializedTupleIds, TNullSide.RIGHT));
            }
            params.add(NullLiteral.create(smap.getRhs().get(i).getType()));
            params.add(smap.getRhs().get(i));
            Expr ifExpr = new FunctionCallExpr("if", params);
            ifExpr.analyze(analyzer);
            smap.getRhs().set(i, ifExpr);
        }
    }

    /**
     * Replaces all SloRefs in expr with a NullLiteral using nullSMap, and evaluates the
     * resulting constant expr. Returns true if the constant expr yields a non-NULL value,
     * false otherwise.
     */
    private boolean requiresNullWrapping(Analyzer analyzer, Expr expr, ExprSubstitutionMap nullSMap)
            throws UserException {
        // If the expr is already wrapped in an IF(TupleIsNull(), NULL, expr)
        // then do not try to execute it.
        if (expr.contains(TupleIsNullPredicate.class)) {
            return true;
        }
        return true;
    }

    @Override
    public void rewriteExprs(ExprRewriter rewriter, Analyzer analyzer)
            throws AnalysisException {
        super.rewriteExprs(rewriter, analyzer);
        queryStmt.rewriteExprs(rewriter);
    }

    @Override
    public List<TupleId> getMaterializedTupleIds() {
        Preconditions.checkState(isAnalyzed);
        Preconditions.checkState(materializedTupleIds.size() > 0);
        return materializedTupleIds;
    }

    public QueryStmt getViewStmt() {
        return queryStmt;
    }

    public void setViewStmt(QueryStmt queryStmt) {
        this.queryStmt = queryStmt;
    }

    public Analyzer getAnalyzer() {
        Preconditions.checkState(isAnalyzed);
        return inlineViewAnalyzer;
    }

    public ExprSubstitutionMap getSmap() {
        Preconditions.checkState(isAnalyzed);
        return sMap;
    }

    public ExprSubstitutionMap getBaseTblSmap() {
        Preconditions.checkState(isAnalyzed);
        return baseTblSmap;
    }

    public boolean isLocalView() {
        return view == null || view.isLocalView();
    }

    public View getView() {
        return view;
    }

    public QueryStmt getQueryStmt() {
        return queryStmt;
    }

    public void setExternalCtl(String externalCtl) {
        this.externalCtl = externalCtl;
    }

    public String getExternalCtl() {
        return this.externalCtl;
    }

    @Override
    public String tableNameToSql() {
        // Enclose the alias in quotes if Hive cannot parse it without quotes.
        // This is needed for view compatibility between Impala and Hive.
        if (view != null) {
            // FIXME: this may result in a sql cache problem
            // See pr #6736 and issue #6735
            return super.tableNameToSql();
        }

        String aliasSql = null;
        String alias = getExplicitAlias();
        if (alias != null) {
            aliasSql = ToSqlUtils.getIdentSql(alias);
        }
        StringBuilder sb = new StringBuilder();
        sb.append("(").append(queryStmt.toSqlWithSelectList()).append(") ").append(aliasSql);
        return sb.toString();
    }

    @Override
    public String tableRefToDigest() {
        String aliasSql = null;
        String alias = getExplicitAlias();
        if (alias != null) {
            aliasSql = ToSqlUtils.getIdentSql(alias);
        }
        if (view != null) {
            return name.toSql() + (aliasSql == null ? "" : " " + aliasSql);
        }

        StringBuilder sb = new StringBuilder()
                .append("(")
                .append(queryStmt.toDigest())
                .append(") ")
                .append(aliasSql);

        return sb.toString();
    }
}