UpdateStmt.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.analysis;

import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.KeysType;
import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.Table;
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.UserException;
import org.apache.doris.common.util.Util;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;

import com.google.common.collect.Lists;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;

/**
 * UPDATE is a DML statement that modifies rows in a unique key olap table.
 * The current update syntax only supports updating the filtered data of a single table.
 * <p>
 * UPDATE table_reference
 *     SET assignment_list
 *     [from_clause]
 *     [WHERE where_condition]
 * <p>
 * assignment_list:
 *     assignment [, assignment] ...
 * <p>
 * assignment:
 *     col_name = value
 * <p>
 * value:
 *     {expr}
 */
@Deprecated
public class UpdateStmt extends DdlStmt implements NotFallbackInParser {
    private TableRef targetTableRef;
    private TableName tableName;
    private final List<BinaryPredicate> setExprs;
    private final Expr whereExpr;
    private final FromClause fromClause;
    private InsertStmt insertStmt;
    private TableIf targetTable;
    List<SelectListItem> selectListItems = Lists.newArrayList();
    List<String> cols = Lists.newArrayList();
    private boolean isPartialUpdate = false;

    public UpdateStmt(TableRef targetTableRef, List<BinaryPredicate> setExprs, FromClause fromClause, Expr whereExpr) {
        this.targetTableRef = targetTableRef;
        this.tableName = targetTableRef.getName();
        this.setExprs = setExprs;
        this.fromClause = fromClause;
        this.whereExpr = whereExpr;
    }

    public InsertStmt getInsertStmt() {
        return insertStmt;
    }

    @Override
    public void analyze(Analyzer analyzer) throws UserException {
        super.analyze(analyzer);
        if (ConnectContext.get() != null && ConnectContext.get().getSessionVariable().isInDebugMode()) {
            throw new AnalysisException("Update is forbidden since current session is in debug mode."
                    + " Please check the following session variables: "
                    + ConnectContext.get().getSessionVariable().printDebugModeVariables());
        }
        analyzeTargetTable(analyzer);
        analyzeSetExprs(analyzer);
        constructInsertStmt();
    }

    private void constructInsertStmt() {
        // not use origin from clause, because we need to mod it, and this action will affect toSql().
        FromClause fromUsedInInsert;
        if (fromClause == null) {
            fromUsedInInsert = new FromClause(Lists.newArrayList(targetTableRef));
        } else {
            fromUsedInInsert = fromClause.clone();
            fromUsedInInsert.getTableRefs().add(0, targetTableRef);
        }
        SelectStmt selectStmt = new SelectStmt(
                // select list
                new SelectList(selectListItems, false),
                // from clause
                fromUsedInInsert,
                // where expr
                whereExpr,
                // group by
                null,
                // having
                null,
                // order by
                null,
                // limit
                LimitElement.NO_LIMIT
        );

        insertStmt = new NativeInsertStmt(
                new InsertTarget(tableName, null),
                null,
                cols,
                new InsertSource(selectStmt),
                null,
                isPartialUpdate, NativeInsertStmt.InsertType.UPDATE);
        ((NativeInsertStmt) insertStmt).setIsFromDeleteOrUpdateStmt(true);
    }

    private void analyzeTargetTable(Analyzer analyzer) throws UserException {
        // step1: analyze table name and origin table alias
        targetTableRef = analyzer.resolveTableRef(targetTableRef);
        targetTableRef.analyze(analyzer);
        tableName = targetTableRef.getName();
        // disallow external catalog
        Util.prohibitExternalCatalog(tableName.getCtl(), this.getClass().getSimpleName());
        // check load privilege, select privilege will check when analyze insert stmt
        if (!Env.getCurrentEnv().getAccessManager()
                .checkTblPriv(ConnectContext.get(), tableName.getCtl(), tableName.getDb(), tableName.getTbl(),
                        PrivPredicate.LOAD)) {
            ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, "LOAD");
        }

        // step2: resolve table name with catalog, only unique olap table could be updated
        targetTable = targetTableRef.getTable();
        if (targetTable.getType() != Table.TableType.OLAP
                || ((OlapTable) targetTable).getKeysType() != KeysType.UNIQUE_KEYS) {
            throw new AnalysisException("Only unique table could be updated.");
        }
    }

    private void analyzeSetExprs(Analyzer analyzer) throws AnalysisException {
        // step1: analyze set exprs
        Set<String> columnMappingNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
        // the column expr only support binary predicate which child(0) must be a SloRef.
        // the duplicate column name of SloRef is forbidden.
        for (BinaryPredicate predicate : setExprs) {
            if (predicate.getOp() != BinaryPredicate.Operator.EQ) {
                throw new AnalysisException("Set function expr only support eq binary predicate. "
                        + "The predicate operator error, op: " + predicate.getOp());
            }
            Expr lhs = predicate.getChild(0);
            if (!(lhs instanceof SlotRef)) {
                throw new AnalysisException("Set function expr only support eq binary predicate "
                        + "which child(0) must be a column name. "
                        + "The child(0) expr error. expr: " + lhs.toSql());
            }
            String column = ((SlotRef) lhs).getColumnName();
            if (!columnMappingNames.add(column)) {
                throw new AnalysisException("Duplicate column setting: " + column);
            }
        }
        // step2: resolve target columns with catalog,
        //        only value columns which belong to target table could be updated.
        for (BinaryPredicate setExpr : setExprs) {
            // check target column
            // 1. columns must belong to target table
            // 2. only value columns could be updated
            Expr lhs = setExpr.getChild(0);
            if (!(lhs instanceof SlotRef)) {
                throw new AnalysisException("The left side of the set expr must be the column name");
            }
            lhs.analyze(analyzer);
            if (((SlotRef) lhs).getColumn().isKey()) {
                throw new AnalysisException("Only value columns of unique table could be updated");
            }
        }

        // step3: generate select list and insert column name list in insert stmt
        boolean isMow = ((OlapTable) targetTable).getEnableUniqueKeyMergeOnWrite();
        int setExprCnt = 0;
        for (Column column : targetTable.getColumns()) {
            for (BinaryPredicate setExpr : setExprs) {
                Expr lhs = setExpr.getChild(0);
                if (((SlotRef) lhs).getColumn().equals(column)) {
                    setExprCnt++;
                }
            }
        }
        // table with sequence col cannot use partial update cause in MOW, we encode pk
        // with seq column but we don't know which column is sequence in update
        if (isMow && ((OlapTable) targetTable).getSequenceCol() == null
                && setExprCnt <= targetTable.getColumns().size() * 3 / 10) {
            isPartialUpdate = true;
        }
        Optional<Column> sequenceMapCol = Optional.empty();
        OlapTable olapTable = (OlapTable) targetTable;
        if (olapTable.hasSequenceCol() && olapTable.getSequenceMapCol() != null) {
            sequenceMapCol = olapTable.getFullSchema().stream()
                    .filter(col -> col.getName().equalsIgnoreCase(olapTable.getSequenceMapCol())).findFirst();
        }
        for (Column column : targetTable.getColumns()) {
            Expr expr = new SlotRef(targetTableRef.getAliasAsName(), column.getName());
            boolean existInExpr = false;
            for (BinaryPredicate setExpr : setExprs) {
                Expr lhs = setExpr.getChild(0);
                Column exprColumn = ((SlotRef) lhs).getColumn();
                // when updating the sequence map column, the real sequence column need to set with the same value.
                boolean isSequenceMapColumn = sequenceMapCol.isPresent()
                        && exprColumn.equals(sequenceMapCol.get());
                if (exprColumn.equals(column) || (olapTable.hasSequenceCol()
                        && column.equals(olapTable.getSequenceCol()) && isSequenceMapColumn)) {
                    expr = setExpr.getChild(1);
                    existInExpr = true;
                }
            }
            if (column.isKey() || existInExpr || !isPartialUpdate) {
                selectListItems.add(new SelectListItem(expr, null));
                cols.add(column.getName());
            }
        }
    }

    @Override
    public String toSql() {
        StringBuilder sb = new StringBuilder("UPDATE ");
        sb.append(targetTableRef.toSql()).append("\n");
        sb.append("  ").append("SET ");
        for (Expr setExpr : setExprs) {
            sb.append(setExpr.toSql()).append(", ");
        }
        if (fromClause != null) {
            sb.append("\n").append(fromClause.toSql());
        }
        sb.append("\n");
        if (whereExpr != null) {
            sb.append("  ").append("WHERE ").append(whereExpr.toSql());
        }
        return sb.toString();
    }

    @Override
    public StmtType stmtType() {
        return StmtType.UPDATE;
    }
}