IvmNormalizeDupKeysMTMV.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.doris.mtmv.ivm;
import org.apache.doris.catalog.KeysType;
import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.info.TableNameInfo;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapTableSink;
import org.apache.doris.nereids.trees.plans.logical.LogicalOneRowRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
import org.apache.doris.nereids.trees.plans.logical.LogicalRepeat;
import org.apache.doris.nereids.trees.plans.logical.LogicalResultSink;
import org.apache.doris.nereids.trees.plans.logical.LogicalSubQueryAlias;
import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
import org.apache.doris.nereids.trees.plans.visitor.DefaultPlanRewriter;
import java.util.Set;
/**
* Normalizes a query for a DUP detail IVM MV by checking it, and answers with the query itself.
*
* <p>A detail MV holds the query's own rows: nothing can ever reach a row it already holds, so its
* refresh only appends what its base tables appended, and there is nothing about the query to
* rewrite. What is left to do is check that the query is one such an MV can maintain -- that no node
* of it is a shape whose maintenance would have to correct a row already materialized -- and this
* rewriter is that check, node by node, together with the traversal that reaches every node to check
* it.
*
* <p>Every node type this rewriter does not accept lands in {@link #visit}, which rejects it exactly
* as the unique-key normalize does. That is what keeps the two in step: a shape the normalize does
* not support is rejected here as well, and a shape it supports only through the row-id path is
* rejected by name below.
*
* <p>What such an MV saves over the unique-key model, which is what makes it worth building the query
* this way at all, is in {@link org.apache.doris.catalog.MTMV#isIvmAppendOnlyDupTable}.
*
* <p>The rewriter carries no state of its own: what the traversal needs is the statement's excluded
* trigger tables, which is also all the scan check below reads, so they are the rewriter's context.
*/
public class IvmNormalizeDupKeysMTMV extends DefaultPlanRewriter<Set<TableNameInfo>> {
/**
* Checks that {@code plan} is a query a DUP detail MV can maintain.
*
* @return {@code plan} itself, which is what such an MV's normalized plan is
*/
public Plan normalize(Plan plan, Set<TableNameInfo> excludedTriggerTables) {
return plan.accept(this, excludedTriggerTables);
}
@Override
public Plan visit(Plan plan, Set<TableNameInfo> excludedTriggerTables) {
// Every node type not accepted below lands here, which is where the unique-key normalize
// rejects it too: a detail MV's query may hold no other shape.
throw new IvmException(IvmFailureReason.PLAN_PATTERN_UNSUPPORTED,
"IVM does not support plan node: " + plan.getClass().getSimpleName());
}
@Override
public Plan visitLogicalOlapScan(LogicalOlapScan scan, Set<TableNameInfo> excludedTriggerTables) {
OlapTable table = scan.getTable();
if (table.getKeysType() != KeysType.DUP_KEYS) {
// An AnalysisException rather than an IvmException: a refresh turns every IvmException into a
// fallback, which would express "the premise of this MV is broken" as "try a complete refresh",
// even though that refresh runs this same normalize and fails the same way. The premise being
// broken is not a retry.
throw new AnalysisException(
"IVM detail MV error: table " + table.getName() + " is " + table.getKeysType()
+ ", but a detail MV can only be built over duplicate-key detail tables");
}
IvmUtil.validateBinlogEnabled(excludedTriggerTables, scan);
return scan;
}
@Override
public Plan visitLogicalOneRowRelation(LogicalOneRowRelation oneRowRelation,
Set<TableNameInfo> excludedTriggerTables) {
return visitChildren(this, oneRowRelation, excludedTriggerTables);
}
@Override
public Plan visitLogicalProject(LogicalProject<? extends Plan> project,
Set<TableNameInfo> excludedTriggerTables) {
if (project.isDistinct()) {
throw new IvmException(IvmFailureReason.PLAN_PATTERN_UNSUPPORTED,
"IVM does not support DISTINCT project.");
}
return visitChildren(this, project, excludedTriggerTables);
}
@Override
public Plan visitLogicalFilter(LogicalFilter<? extends Plan> filter,
Set<TableNameInfo> excludedTriggerTables) {
return visitChildren(this, filter, excludedTriggerTables);
}
@Override
public Plan visitLogicalSubQueryAlias(LogicalSubQueryAlias<? extends Plan> alias,
Set<TableNameInfo> excludedTriggerTables) {
return visitChildren(this, alias, excludedTriggerTables);
}
@Override
public Plan visitLogicalJoin(LogicalJoin<? extends Plan, ? extends Plan> join,
Set<TableNameInfo> excludedTriggerTables) {
if (!join.getJoinType().isInnerOrCrossJoin()) {
// An outer join retracts the rows its null side stopped matching, which means reaching
// rows the MV already holds.
throw new IvmException(IvmFailureReason.PLAN_PATTERN_UNSUPPORTED,
"IVM detail MV error: a detail MV can only maintain INNER_JOIN and CROSS_JOIN, "
+ "but the query has " + join.getJoinType());
}
return visitChildren(this, join, excludedTriggerTables);
}
@Override
public Plan visitLogicalUnion(LogicalUnion union, Set<TableNameInfo> excludedTriggerTables) {
if (union.getQualifier() != Qualifier.ALL) {
throw new IvmException(IvmFailureReason.PLAN_PATTERN_UNSUPPORTED,
"IVM does not support UNION DISTINCT. Only UNION ALL is supported.");
}
if (!union.getConstantExprsList().isEmpty()) {
throw new IvmException(IvmFailureReason.PLAN_PATTERN_UNSUPPORTED,
"IVM does not support UNION ALL with constant expressions.");
}
return visitChildren(this, union, excludedTriggerTables);
}
@Override
public Plan visitLogicalResultSink(LogicalResultSink<? extends Plan> sink,
Set<TableNameInfo> excludedTriggerTables) {
// The MV is built with the query's own columns, so a column name Doris reserves for a hidden one
// would leave the MV holding fewer columns than the query produced; the unique-key normalize
// rejects the same names, and both models lay hidden columns on the MV they build.
IvmUtil.validateUserOutputColumnNames(sink.getOutputExprs());
return visitChildren(this, sink, excludedTriggerTables);
}
@Override
public Plan visitLogicalOlapTableSink(LogicalOlapTableSink<? extends Plan> sink,
Set<TableNameInfo> excludedTriggerTables) {
return visitChildren(this, sink, excludedTriggerTables);
}
@Override
public Plan visitLogicalAggregate(LogicalAggregate<? extends Plan> aggregate,
Set<TableNameInfo> excludedTriggerTables) {
// An aggregate's delta changes the row of a group, and DISTINCT folds rows together: both
// mean reaching and correcting rows the MV already holds.
throw new IvmException(IvmFailureReason.PLAN_PATTERN_UNSUPPORTED,
"IVM detail MV error: a detail MV holds no aggregate, so an aggregate cannot be "
+ "maintained in one");
}
@Override
public Plan visitLogicalRepeat(LogicalRepeat<? extends Plan> repeat,
Set<TableNameInfo> excludedTriggerTables) {
// GROUPING SETS is a repeat of aggregates, so it needs each group row corrected as well.
throw new IvmException(IvmFailureReason.PLAN_PATTERN_UNSUPPORTED,
"IVM detail MV error: a detail MV holds no GROUPING SETS, so GROUPING SETS cannot be "
+ "maintained in one");
}
}