DescribeCommand.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.trees.plans.commands;

import org.apache.doris.analysis.Expr;
import org.apache.doris.analysis.TableValuedFunctionRef;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.DatabaseIf;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.JdbcTable;
import org.apache.doris.catalog.MaterializedIndexMeta;
import org.apache.doris.catalog.MysqlTable;
import org.apache.doris.catalog.OdbcTable;
import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.ScalarType;
import org.apache.doris.catalog.TableIf;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.FeConstants;
import org.apache.doris.common.Pair;
import org.apache.doris.common.UserException;
import org.apache.doris.common.proc.IndexSchemaProcNode;
import org.apache.doris.common.proc.ProcNodeInterface;
import org.apache.doris.common.proc.ProcService;
import org.apache.doris.common.proc.TableProcDir;
import org.apache.doris.common.util.Util;
import org.apache.doris.datasource.CatalogIf;
import org.apache.doris.datasource.systable.SysTable;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.commands.info.PartitionNamesInfo;
import org.apache.doris.nereids.trees.plans.commands.info.TableNameInfo;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.ShowResultSet;
import org.apache.doris.qe.ShowResultSetMetaData;
import org.apache.doris.qe.StmtExecutor;
import org.apache.doris.tablefunction.BackendsTableValuedFunction;
import org.apache.doris.tablefunction.LocalTableValuedFunction;

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
 * Describe command, support
 *   describe tbl
 *   describe tbl all
 *   describe tbl partition p1
 *   describe tbl partition (p1, p2)
 *   describe function tvf
 */
public class DescribeCommand extends ShowCommand {
    private static final Logger LOG = LogManager.getLogger(DescribeCommand.class);

    private TableNameInfo dbTableName;
    private boolean isAllTables = false;
    private boolean isOlapTable = false;
    private boolean showComment = false;

    private PartitionNamesInfo partitionNames;

    private TableValuedFunctionRef tableValuedFunctionRef;
    private boolean isTableValuedFunction;

    private List<List<String>> rows = new LinkedList<List<String>>();

    public DescribeCommand(TableNameInfo dbTableName, boolean isAllTables, PartitionNamesInfo partitionNames) {
        super(PlanType.DESCRIBE);
        this.dbTableName = dbTableName;
        this.isAllTables = isAllTables;
        this.partitionNames = partitionNames;
    }

    public DescribeCommand(TableValuedFunctionRef tableValuedFunctionRef) {
        super(PlanType.DESCRIBE);
        this.tableValuedFunctionRef = tableValuedFunctionRef;
        this.isTableValuedFunction = true;
        this.isAllTables = false;
    }

    /**
     * getAllMetaData
     */
    private static ShowResultSetMetaData getAllMetaData() {
        ShowResultSetMetaData.Builder builder = ShowResultSetMetaData.builder();
        builder.addColumn(new Column("IndexName", ScalarType.createVarchar(20)));
        builder.addColumn(new Column("IndexKeysType", ScalarType.createVarchar(20)));
        builder.addColumn(new Column("Field", ScalarType.createVarchar(20)));
        builder.addColumn(new Column("Type", ScalarType.createVarchar(20)));
        builder.addColumn(new Column("InternalType", ScalarType.createVarchar(20)));
        builder.addColumn(new Column("Null", ScalarType.createVarchar(10)));
        builder.addColumn(new Column("Key", ScalarType.createVarchar(10)));
        builder.addColumn(new Column("Default", ScalarType.createVarchar(30)));
        builder.addColumn(new Column("Extra", ScalarType.createVarchar(30)));
        builder.addColumn(new Column("Visible", ScalarType.createVarchar(10)));
        builder.addColumn(new Column("DefineExpr", ScalarType.createVarchar(30)));
        builder.addColumn(new Column("WhereClause", ScalarType.createVarchar(30)));
        return builder.build();
    }

    /**
     * getMysqlMetaData
     */
    private static ShowResultSetMetaData getMysqlMetaData() {
        ShowResultSetMetaData.Builder builder = ShowResultSetMetaData.builder();
        builder.addColumn(new Column("Host", ScalarType.createVarchar(30)));
        builder.addColumn(new Column("Port", ScalarType.createVarchar(10)));
        builder.addColumn(new Column("User", ScalarType.createVarchar(30)));
        builder.addColumn(new Column("Password", ScalarType.createVarchar(30)));
        builder.addColumn(new Column("Database", ScalarType.createVarchar(30)));
        builder.addColumn(new Column("Table", ScalarType.createVarchar(30)));
        return builder.build();
    }

    /**
     * initEmptyRow
     */
    private static List<String> initEmptyRow() {
        List<String> emptyRow = new ArrayList<>(getAllMetaData().getColumns().size());
        for (int i = 0; i < getAllMetaData().getColumns().size(); i++) {
            emptyRow.add("");
        }
        return emptyRow;
    }

    /**
     * getMetaData
     */
    @Override
    public ShowResultSetMetaData getMetaData() {
        if (!isAllTables) {
            ShowResultSetMetaData.Builder builder = ShowResultSetMetaData.builder();
            for (String col : IndexSchemaProcNode.TITLE_NAMES) {
                builder.addColumn(new Column(col, ScalarType.createVarchar(30)));
            }
            if (showComment) {
                builder.addColumn(new Column(IndexSchemaProcNode.COMMENT_COLUMN_TITLE, ScalarType.createStringType()));
            }
            return builder.build();
        } else {
            if (isOlapTable) {
                return getAllMetaData();
            } else {
                return getMysqlMetaData();
            }
        }
    }

    /**
     * validateTableValuedFunction
     */
    private void validateTableValuedFunction(ConnectContext ctx, String funcName) throws AnalysisException {
        // check privilege for backends/local tvf
        if (funcName.equalsIgnoreCase(BackendsTableValuedFunction.NAME)
                || funcName.equalsIgnoreCase(LocalTableValuedFunction.NAME)) {
            if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, PrivPredicate.ADMIN)
                    && !Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx,
                    PrivPredicate.OPERATOR)) {
                ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, "ADMIN/OPERATOR");
            }
        }
    }

    @Override
    public ShowResultSet doRun(ConnectContext ctx, StmtExecutor executor) throws Exception {
        if (dbTableName != null) {
            dbTableName.analyze(ctx);
            CatalogIf catalog = Env.getCurrentEnv().getCatalogMgr().getCatalogOrAnalysisException(dbTableName.getCtl());
            DatabaseIf db = catalog.getDbOrAnalysisException(dbTableName.getDb());
            Pair<String, String> tableNameWithSysTableName
                    = SysTable.getTableNameWithSysTableName(dbTableName.getTbl());
            if (!Strings.isNullOrEmpty(tableNameWithSysTableName.second)) {
                TableIf table = db.getTableOrDdlException(tableNameWithSysTableName.first);
                isTableValuedFunction = true;
                Optional<TableValuedFunctionRef> optTvfRef = table.getSysTableFunctionRef(
                        dbTableName.getCtl(), dbTableName.getDb(), dbTableName.getTbl());
                if (!optTvfRef.isPresent()) {
                    throw new AnalysisException("sys table not found: " + tableNameWithSysTableName.second);
                }
                tableValuedFunctionRef = optTvfRef.get();
            }
        }

        if (!isAllTables && isTableValuedFunction) {
            validateTableValuedFunction(ctx, tableValuedFunctionRef.getTableFunction().getTableName());
            List<Column> columns = tableValuedFunctionRef.getTableFunction().getTableColumns();
            for (Column column : columns) {
                List<String> row = Arrays.asList(
                        column.getName(),
                        column.getOriginType().hideVersionForVersionColumn(true),
                        column.isAllowNull() ? "Yes" : "No",
                        ((Boolean) column.isKey()).toString(),
                        column.getDefaultValue() == null
                            ? FeConstants.null_string : column.getDefaultValue(),
                        "NONE");
                rows.add(row);
            }
            return new ShowResultSet(getMetaData(), rows);
        }

        if (partitionNames != null) {
            partitionNames.validate();
            if (partitionNames.isTemp()) {
                throw new AnalysisException("Do not support temp partitions");
            }
        }

        if (!Env.getCurrentEnv().getAccessManager()
                .checkTblPriv(ConnectContext.get(),
                        dbTableName.getCtl(),
                        dbTableName.getDb(),
                        dbTableName.getTbl(),
                        PrivPredicate.SHOW)) {
            ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR, "DESCRIBE",
                    ConnectContext.get().getQualifiedUser(), ConnectContext.get().getRemoteIP(),
                    dbTableName.toString());
        }

        CatalogIf catalog = Env.getCurrentEnv().getCatalogMgr().getCatalogOrAnalysisException(dbTableName.getCtl());
        DatabaseIf db = catalog.getDbOrAnalysisException(dbTableName.getDb());
        TableIf table = db.getTableOrDdlException(dbTableName.getTbl());
        table.readLock();
        try {
            if (!isAllTables) {
                // show base table schema only
                String procString = "/catalogs/" + catalog.getId() + "/" + db.getId() + "/" + table.getId() + "/"
                        + TableProcDir.INDEX_SCHEMA + "/";
                if (table instanceof OlapTable) {
                    procString += ((OlapTable) table).getBaseIndexId();
                } else {
                    if (partitionNames != null) {
                        throw new AnalysisException(dbTableName.getTbl()
                            + " is not a OLAP table, describe table failed");
                    }
                    procString += table.getId();
                }
                if (partitionNames != null) {
                    procString += "/";
                    StringBuilder builder = new StringBuilder();
                    for (String str : partitionNames.getPartitionNames()) {
                        // TODO: describe tbl partition p1 can not execute, should fix it.
                        builder.append(str);
                        builder.append(",");
                    }
                    builder.deleteCharAt(builder.length() - 1);
                    procString += builder.toString();
                }
                ProcNodeInterface node = ProcService.getInstance().open(procString);
                if (node == null) {
                    throw new AnalysisException("Describe table[" + dbTableName.getTbl() + "] failed");
                }
                rows.addAll(getResultRows(node));
            } else {
                Util.prohibitExternalCatalog(dbTableName.getCtl(), this.getClass().getSimpleName() + " ALL");
                if (table instanceof OlapTable) {
                    isOlapTable = true;
                    OlapTable olapTable = (OlapTable) table;
                    Set<String> bfColumns = olapTable.getCopiedBfColumns();
                    Map<Long, List<Column>> indexIdToSchema = olapTable.getIndexIdToSchema();

                    // indices order
                    List<Long> indices = Lists.newArrayList();
                    indices.add(olapTable.getBaseIndexId());
                    for (Long indexId : indexIdToSchema.keySet()) {
                        if (indexId != olapTable.getBaseIndexId()) {
                            indices.add(indexId);
                        }
                    }

                    // add all indices
                    for (int i = 0; i < indices.size(); ++i) {
                        long indexId = indices.get(i);
                        List<Column> columns = indexIdToSchema.get(indexId);
                        String indexName = olapTable.getIndexNameById(indexId);
                        MaterializedIndexMeta indexMeta = olapTable.getIndexMetaByIndexId(indexId);
                        for (int j = 0; j < columns.size(); ++j) {
                            Column column = columns.get(j);

                            // Extra string (aggregation and bloom filter)
                            List<String> extras = Lists.newArrayList();
                            if (column.getAggregationType() != null) {
                                extras.add(column.getAggregationString());
                            }
                            if (bfColumns != null && bfColumns.contains(column.getName())) {
                                extras.add("BLOOM_FILTER");
                            }
                            String extraStr = StringUtils.join(extras, ",");

                            String defineExprStr = "";
                            Expr defineExpr = column.getDefineExpr();
                            if (defineExpr != null) {
                                column.getDefineExpr().setDisableTableName(true);
                                defineExprStr = defineExpr.toSql();
                            }

                            List<String> row = Arrays.asList(
                                    "",
                                    "",
                                    column.getName(),
                                    column.getOriginType().toString(),
                                    column.getOriginType().toString(),
                                    column.isAllowNull() ? "Yes" : "No",
                                    ((Boolean) column.isKey()).toString(),
                                    column.getDefaultValue() == null
                                            ? FeConstants.null_string
                                            : column.getDefaultValue(),
                                    extraStr,
                                    ((Boolean) column.isVisible()).toString(),
                                    defineExprStr,
                                    "");

                            if (column.getOriginType().isDatetimeV2()) {
                                StringBuilder typeStr = new StringBuilder("DATETIME");
                                if (((ScalarType) column.getOriginType()).getScalarScale() > 0) {
                                    typeStr.append("(").append(((ScalarType) column.getOriginType()).getScalarScale())
                                        .append(")");
                                }
                                row.set(3, typeStr.toString());
                            } else if (column.getOriginType().isDateV2()) {
                                row.set(3, "DATE");
                            } else if (column.getOriginType().isDecimalV3()) {
                                StringBuilder typeStr = new StringBuilder("DECIMAL");
                                ScalarType sType = (ScalarType) column.getOriginType();
                                int scale = sType.getScalarScale();
                                int precision = sType.getScalarPrecision();
                                // not default
                                if (scale > 0 && precision != 9) {
                                    typeStr.append("(").append(precision).append(", ").append(scale)
                                        .append(")");
                                }
                                row.set(3, typeStr.toString());
                            }

                            if (j == 0) {
                                row.set(0, indexName);
                                row.set(1, indexMeta.getKeysType().name());
                                Expr where = indexMeta.getWhereClause();
                                row.set(getMetaData().getColumns().size() - 1,
                                        where == null ? "" : where.toSqlWithoutTbl());
                            }

                            rows.add(row);
                        } // end for columns

                        if (i != indices.size() - 1) {
                            rows.add(initEmptyRow());
                        }
                    } // end for indices
                } else if (table.getType() == TableIf.TableType.ODBC) {
                    isOlapTable = false;
                    OdbcTable odbcTable = (OdbcTable) table;
                    List<String> row = Arrays.asList(odbcTable.getHost(),
                            odbcTable.getPort(),
                            odbcTable.getUserName(),
                            odbcTable.getPasswd(),
                            odbcTable.getOdbcDatabaseName(),
                            odbcTable.getOdbcTableName(),
                            odbcTable.getOdbcDriver(),
                            odbcTable.getOdbcTableTypeName());
                    rows.add(row);
                } else if (table.getType() == TableIf.TableType.JDBC) {
                    isOlapTable = false;
                    JdbcTable jdbcTable = (JdbcTable) table;
                    List<String> row = Arrays.asList(jdbcTable.getJdbcUrl(),
                            jdbcTable.getJdbcUser(),
                            jdbcTable.getJdbcPasswd(),
                            jdbcTable.getDriverClass(),
                            jdbcTable.getDriverUrl(),
                            jdbcTable.getExternalTableName(),
                            jdbcTable.getResourceName(),
                            jdbcTable.getJdbcTypeName());
                    rows.add(row);
                } else if (table.getType() == TableIf.TableType.MYSQL) {
                    isOlapTable = false;
                    MysqlTable mysqlTable = (MysqlTable) table;
                    List<String> row = Arrays.asList(mysqlTable.getHost(),
                            mysqlTable.getPort(),
                            mysqlTable.getUserName(),
                            mysqlTable.getPasswd(),
                            mysqlTable.getMysqlDatabaseName(),
                            mysqlTable.getMysqlTableName(),
                            mysqlTable.getCharset());
                    rows.add(row);
                } else {
                    ErrorReport.reportAnalysisException(ErrorCode.ERR_UNKNOWN_STORAGE_ENGINE, table.getType());
                }
            }
        } finally {
            table.readUnlock();
        }

        return new ShowResultSet(getMetaData(), rows);
    }

    /**
     * getResultRows
     */
    private List<List<String>> getResultRows(ProcNodeInterface node) throws AnalysisException {
        showComment = ConnectContext.get().getSessionVariable().showColumnCommentInDescribe;
        Preconditions.checkNotNull(node);
        List<List<String>> rows = node.fetchResult().getRows();
        List<List<String>> res = new ArrayList<>();
        for (List<String> row : rows) {
            try {
                Env.getCurrentEnv().getAccessManager()
                        .checkColumnsPriv(ConnectContext.get().getCurrentUserIdentity(), dbTableName.getCtl(),
                                dbTableName.getDb(), dbTableName.getTbl(), Sets.newHashSet(row.get(0)),
                                PrivPredicate.SHOW);
                res.add(row);
            } catch (UserException e) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug(e.getMessage());
                }
            }
        }
        return res;
    }

    @Override
    public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
        return visitor.visitDescribeCommand(this, context);
    }
}