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 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;

    private Map<List<String>, Boolean> useMvTableColumnMap = new HashMap<>();

    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;
    }

    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();
    }
}