UseMvHint.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.nereids.hint;
import org.apache.doris.datasource.CatalogIf;
import org.apache.doris.qe.ConnectContext;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* rule hint.
*/
public class UseMvHint extends Hint {
private final boolean isUseMv;
private final boolean isAllMv;
private final List<List<String>> tables;
// Key: MV qualifier (e.g., [catalog, db, mvName] for async MTMV,
// or [catalog, db, table, indexName] for sync MV).
// Value: true if the MV is finally chosen in the physical plan,
// false if not chosen (either rewrite failed or plan selector chose another path).
private Map<List<String>, Boolean> useMvTableColumnMap = new HashMap<>();
// Key: MV qualifier for NO_USE_MV hint.
// Value: true if the MV was excluded from rewrite consideration.
private Map<List<String>, Boolean> noUseMvTableColumnMap = new HashMap<>();
/**
* constructor of use mv hint
* @param hintName use mv
* @param tables original parameters
* @param isUseMv use_mv hint or no_use_mv hint
* @param isAllMv should all mv be controlled
*/
public UseMvHint(String hintName, List<List<String>> tables, boolean isUseMv, boolean isAllMv, List<Hint> hints) {
super(hintName);
this.isUseMv = isUseMv;
this.isAllMv = isAllMv;
this.tables = tables;
this.useMvTableColumnMap = initMvTableColumnMap(tables, true);
this.noUseMvTableColumnMap = initMvTableColumnMap(tables, false);
checkConflicts(hints);
}
public Map<List<String>, Boolean> getNoUseMvTableColumnMap() {
return noUseMvTableColumnMap;
}
public Map<List<String>, Boolean> getUseMvTableColumnMap() {
return useMvTableColumnMap;
}
/**
* Check if a given MV qualifier is specified in the USE_MV hint parameters.
*/
public boolean isUseMvQualifier(List<String> mvQualifier) {
return useMvTableColumnMap.containsKey(mvQualifier);
}
/**
* Find the USE_MV hint parameter which references the given materialization identifier.
* <p>
* The identifier of an async materialization view is [catalog, db, mvName], which is also the
* parameter form of USE_MV(mvName). The identifier of a sync materialization view is
* [catalog, db, tableName, indexName], so besides USE_MV(tableName.mvName) the bare name form
* USE_MV(mvName) also references it, i.e. the parameter [catalog, db, mvName].
*
* @param mvIdentifier the materialization identifier generated by MaterializationContext
* @return the matched USE_MV hint parameter, or empty if the hint does not reference the MV
*/
public Optional<List<String>> findUseMvParameter(List<String> mvIdentifier) {
if (useMvTableColumnMap.containsKey(mvIdentifier)) {
return Optional.of(mvIdentifier);
}
if (mvIdentifier.size() == 4) {
List<String> bareNameParameter = ImmutableList.of(
mvIdentifier.get(0), mvIdentifier.get(1), mvIdentifier.get(3));
if (useMvTableColumnMap.containsKey(bareNameParameter)) {
return Optional.of(bareNameParameter);
}
}
return Optional.empty();
}
/**
* Check if a given MV qualifier is finally chosen in the physical plan.
* This is only meaningful after plan selection has completed and
* {@link org.apache.doris.nereids.NereidsPlanner#finalizeUseMvHintStatus} has been called.
*/
public boolean isQualifierChosen(List<String> mvQualifier) {
return Boolean.TRUE.equals(useMvTableColumnMap.get(mvQualifier));
}
private void checkConflicts(List<Hint> hints) {
String otherUseMv = isUseMv ? "NO_USE_MV" : "USE_MV";
Optional<UseMvHint> otherUseMvHint = Optional.empty();
for (Hint hint : hints) {
if (hint.getHintName().equals(otherUseMv)) {
otherUseMvHint = Optional.of((UseMvHint) hint);
}
}
if (!otherUseMvHint.isPresent()) {
return;
}
Map<List<String>, Boolean> otherUseMvTableColumnMap = isUseMv
? otherUseMvHint.get().getNoUseMvTableColumnMap() : otherUseMvHint.get().getUseMvTableColumnMap();
Map<List<String>, Boolean> thisUseMvTableColumnMap = isUseMv ? useMvTableColumnMap : noUseMvTableColumnMap;
for (Map.Entry<List<String>, Boolean> entry : thisUseMvTableColumnMap.entrySet()) {
List<String> mv = entry.getKey();
if (otherUseMvTableColumnMap.get(mv) != null) {
String errorMsg = "conflict mv exist in use_mv and no_use_mv in the same time. Mv name: "
+ mv;
super.setStatus(Hint.HintStatus.SYNTAX_ERROR);
super.setErrorMessage(errorMsg);
otherUseMvHint.get().setStatus(Hint.HintStatus.SYNTAX_ERROR);
otherUseMvHint.get().setErrorMessage(errorMsg);
}
}
}
private Map<List<String>, Boolean> initMvTableColumnMap(List<List<String>> parameters, boolean initUseMv) {
Map<List<String>, Boolean> mvTableColumnMap;
if (initUseMv && !isUseMv) {
return useMvTableColumnMap;
} else if (!initUseMv && isUseMv) {
return noUseMvTableColumnMap;
} else if (initUseMv && isUseMv) {
mvTableColumnMap = useMvTableColumnMap;
} else {
mvTableColumnMap = noUseMvTableColumnMap;
}
for (List<String> table : parameters) {
// materialize view qualifier should have length between 1 and 4
// which 1 and 3 represent of async materialize view, 2 and 4 represent of sync materialize view
// number of parameters meaning
// 1 async materialize view, mvName
// 2 sync materialize view, tableName.mvName
// 3 async materialize view, catalogName.dbName.mvName
// 3 sync materialize view, catalogName.dbName.tableName.mvName
if (table.size() < 1 || table.size() > 4) {
this.setStatus(HintStatus.SYNTAX_ERROR);
this.setErrorMessage("parameters number of no_use_mv hint must between 1 and 4");
return mvTableColumnMap;
}
String mvName = table.get(table.size() - 1);
if (mvName.equals("`*`") && isUseMv) {
this.setStatus(Hint.HintStatus.SYNTAX_ERROR);
this.setErrorMessage("use_mv hint should only have one mv in one table");
return mvTableColumnMap;
}
List<String> dbQualifier = new ArrayList<>();
if (table.size() == 3 || table.size() == 4) {
mvTableColumnMap.put(table, false);
return mvTableColumnMap;
}
CatalogIf catalogIf = ConnectContext.get().getCurrentCatalog();
if (catalogIf == null) {
this.setStatus(HintStatus.SYNTAX_ERROR);
this.setErrorMessage("Current catalog is not set.");
return mvTableColumnMap;
}
String catalogName = catalogIf.getName();
String dbName = ConnectContext.get().getDatabase();
if (Strings.isNullOrEmpty(dbName)) {
this.setStatus(HintStatus.SYNTAX_ERROR);
this.setErrorMessage("Current database is not set.");
return mvTableColumnMap;
}
dbQualifier.add(catalogName);
dbQualifier.add(dbName);
if (table.size() == 2) {
dbQualifier.add(table.get(0));
}
dbQualifier.add(mvName);
if (mvTableColumnMap.containsKey(dbQualifier)) {
this.setStatus(HintStatus.SYNTAX_ERROR);
this.setErrorMessage("repeated parameters in use_mv hint: " + dbQualifier);
return mvTableColumnMap;
} else {
mvTableColumnMap.put(dbQualifier, false);
}
}
return mvTableColumnMap;
}
public boolean isUseMv() {
return isUseMv;
}
public boolean isAllMv() {
return isAllMv;
}
@Override
public String getExplainString() {
StringBuilder out = new StringBuilder();
if (isUseMv) {
out.append("use_mv");
} else {
out.append("no_use_mv");
}
if (!tables.isEmpty()) {
out.append("(");
for (int i = 0; i < tables.size(); i++) {
if (i % 2 == 0) {
out.append(tables.get(i));
} else {
out.append(".");
out.append(tables.get(i));
out.append(" ");
}
}
out.append(")");
}
return out.toString();
}
}