SqlModeHelper.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.qe;


import org.apache.doris.common.DdlException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Maps;
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.List;
import java.util.Map;
import java.util.function.Supplier;

public class SqlModeHelper {
    private static final Logger LOG = LogManager.getLogger(SqlModeHelper.class);

    // TODO(xuyang): these mode types are copy from MYSQL mode types, which are example
    //  of how they works and to be compatible with MySQL, so for now they are not
    //  really meaningful.
    /* Bits for different SQL MODE modes, you can add custom SQL MODE here */
    /* When a new session is created, its sql mode is set to MODE_DEFAULT */
    public static final long MODE_DEFAULT = 1L;
    public static final long MODE_PIPES_AS_CONCAT = 2L;
    public static final long MODE_ANSI_QUOTES = 4L;
    public static final long MODE_IGNORE_SPACE = 8L;
    public static final long MODE_NOT_USED = 16L;
    public static final long MODE_ONLY_FULL_GROUP_BY = 32L;
    public static final long MODE_NO_UNSIGNED_SUBTRACTION = 64L;
    public static final long MODE_NO_DIR_IN_CREATE = 128L;
    public static final long MODE_NO_AUTO_VALUE_ON_ZERO = 1L << 19;
    public static final long MODE_NO_BACKSLASH_ESCAPES = 1L << 20;
    public static final long MODE_STRICT_TRANS_TABLES = 1L << 21;
    public static final long MODE_STRICT_ALL_TABLES = 1L << 22;
    // NO_ZERO_IN_DATE and NO_ZERO_DATE are removed in mysql 5.7 and merged into STRICT MODE.
    // However, for backward compatibility during upgrade, these modes are kept.
    @Deprecated
    public static final long MODE_NO_ZERO_IN_DATE = 1L << 23;
    @Deprecated
    public static final long MODE_NO_ZERO_DATE = 1L << 24;
    public static final long MODE_INVALID_DATES = 1L << 25;
    public static final long MODE_ERROR_FOR_DIVISION_BY_ZERO = 1L << 26;
    public static final long MODE_HIGH_NOT_PRECEDENCE = 1L << 29;
    public static final long MODE_NO_ENGINE_SUBSTITUTION = 1L << 30;
    public static final long MODE_PAD_CHAR_TO_FULL_LENGTH = 1L << 31;
    public static final long MODE_TIME_TRUNCATE_FRACTIONAL = 1L << 32;

    /* Bits for different COMBINE MODE modes, you can add custom COMBINE MODE here */
    public static final long MODE_ANSI = 1L << 18;
    public static final long MODE_TRADITIONAL = 1L << 27;

    public static final long MODE_LAST = 1L << 33;
    public static final long MODE_REAL_AS_FLOAT = 1L << 34;


    public static final long MODE_ALLOWED_MASK =
            (MODE_REAL_AS_FLOAT | MODE_PIPES_AS_CONCAT | MODE_ANSI_QUOTES | MODE_IGNORE_SPACE | MODE_NOT_USED
                    | MODE_ONLY_FULL_GROUP_BY | MODE_NO_UNSIGNED_SUBTRACTION | MODE_NO_DIR_IN_CREATE
                    | MODE_NO_AUTO_VALUE_ON_ZERO | MODE_NO_BACKSLASH_ESCAPES | MODE_STRICT_TRANS_TABLES
                    | MODE_STRICT_ALL_TABLES | MODE_NO_ZERO_IN_DATE | MODE_NO_ZERO_DATE | MODE_INVALID_DATES
                    | MODE_ERROR_FOR_DIVISION_BY_ZERO | MODE_HIGH_NOT_PRECEDENCE | MODE_NO_ENGINE_SUBSTITUTION
                    | MODE_PAD_CHAR_TO_FULL_LENGTH | MODE_TRADITIONAL | MODE_ANSI | MODE_TIME_TRUNCATE_FRACTIONAL
                    | MODE_DEFAULT);

    public static final long MODE_COMBINE_MASK = (MODE_ANSI | MODE_TRADITIONAL);

    private static final Map<String, Long> sqlModeSet = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);

    private static final Map<String, Long> combineModeSet = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);

    static {
        sqlModeSet.put("DEFAULT", MODE_DEFAULT);
        sqlModeSet.put("REAL_AS_FLOAT", MODE_REAL_AS_FLOAT);
        sqlModeSet.put("PIPES_AS_CONCAT", MODE_PIPES_AS_CONCAT);
        sqlModeSet.put("ANSI_QUOTES", MODE_ANSI_QUOTES);
        sqlModeSet.put("IGNORE_SPACE", MODE_IGNORE_SPACE);
        sqlModeSet.put("NOT_USED", MODE_NOT_USED);
        sqlModeSet.put("ONLY_FULL_GROUP_BY", MODE_ONLY_FULL_GROUP_BY);
        sqlModeSet.put("NO_UNSIGNED_SUBTRACTION", MODE_NO_UNSIGNED_SUBTRACTION);
        sqlModeSet.put("NO_DIR_IN_CREATE", MODE_NO_DIR_IN_CREATE);
        sqlModeSet.put("ANSI", MODE_ANSI);
        sqlModeSet.put("NO_AUTO_VALUE_ON_ZERO", MODE_NO_AUTO_VALUE_ON_ZERO);
        sqlModeSet.put("NO_BACKSLASH_ESCAPES", MODE_NO_BACKSLASH_ESCAPES);
        sqlModeSet.put("STRICT_TRANS_TABLES", MODE_STRICT_TRANS_TABLES);
        sqlModeSet.put("STRICT_ALL_TABLES", MODE_STRICT_ALL_TABLES);
        sqlModeSet.put("NO_ZERO_IN_DATE", MODE_NO_ZERO_IN_DATE);
        sqlModeSet.put("NO_ZERO_DATE", MODE_NO_ZERO_DATE);
        sqlModeSet.put("INVALID_DATES", MODE_INVALID_DATES);
        sqlModeSet.put("ERROR_FOR_DIVISION_BY_ZERO", MODE_ERROR_FOR_DIVISION_BY_ZERO);
        sqlModeSet.put("TRADITIONAL", MODE_TRADITIONAL);
        sqlModeSet.put("HIGH_NOT_PRECEDENCE", MODE_HIGH_NOT_PRECEDENCE);
        sqlModeSet.put("NO_ENGINE_SUBSTITUTION", MODE_NO_ENGINE_SUBSTITUTION);
        sqlModeSet.put("PAD_CHAR_TO_FULL_LENGTH", MODE_PAD_CHAR_TO_FULL_LENGTH);
        sqlModeSet.put("TIME_TRUNCATE_FRACTIONAL", MODE_TIME_TRUNCATE_FRACTIONAL);

        combineModeSet.put("ANSI", (MODE_REAL_AS_FLOAT | MODE_PIPES_AS_CONCAT
                | MODE_ANSI_QUOTES | MODE_IGNORE_SPACE | MODE_ONLY_FULL_GROUP_BY));
        combineModeSet.put("TRADITIONAL", (MODE_STRICT_TRANS_TABLES | MODE_STRICT_ALL_TABLES
                | MODE_NO_ZERO_IN_DATE | MODE_NO_ZERO_DATE | MODE_ERROR_FOR_DIVISION_BY_ZERO
                | MODE_NO_ENGINE_SUBSTITUTION));
    }

    // convert long type SQL MODE to string type that user can read
    public static String decode(Long sqlMode) throws DdlException {
        if (sqlMode == MODE_DEFAULT) {
            //For compatibility with older versions��� return empty string
            return "";
        }
        if ((sqlMode & ~MODE_ALLOWED_MASK) != 0) {
            ErrorReport.reportDdlException(ErrorCode.ERR_WRONG_VALUE_FOR_VAR, SessionVariable.SQL_MODE, sqlMode);
        }

        List<String> names = new ArrayList<String>();
        for (Map.Entry<String, Long> mode : getSupportedSqlMode().entrySet()) {
            if ((sqlMode & mode.getValue()) != 0) {
                names.add(mode.getKey());
            }
        }

        return Joiner.on(',').join(names);
    }

    // convert string type SQL MODE to long type that session can store
    public static Long encode(String sqlMode) throws DdlException {
        List<String> names =
                Splitter.on(',').trimResults().omitEmptyStrings().splitToList(sqlMode);

        // empty string parse to 0
        long resultCode = 0L;
        for (String key : names) {
            long code = 0L;
            if (StringUtils.isNumeric(key)) {
                code |= expand(Long.valueOf(key));
            } else {
                code = getCodeFromString(key);
                if (code == 0) {
                    ErrorReport.reportDdlException(ErrorCode.ERR_WRONG_VALUE_FOR_VAR, SessionVariable.SQL_MODE, key);
                }
            }
            resultCode |= code;
            if ((resultCode & ~MODE_ALLOWED_MASK) != 0) {
                ErrorReport.reportDdlException(ErrorCode.ERR_WRONG_VALUE_FOR_VAR, SessionVariable.SQL_MODE, key);
            }
        }
        return resultCode;
    }

    // expand the combine mode if exists
    public static long expand(long sqlMode) throws DdlException {
        for (String key : getCombineMode().keySet()) {
            if ((sqlMode & getSupportedSqlMode().get(key)) != 0) {
                sqlMode |= getCombineMode().get(key);
            }
        }
        return sqlMode;
    }

    // check if this SQL MODE is supported
    public static boolean isSupportedSqlMode(String sqlMode) {
        if (sqlMode == null || !getSupportedSqlMode().containsKey(sqlMode)) {
            return false;
        }
        return true;
    }

    // encode sqlMode from string to long
    private static long getCodeFromString(String sqlMode) {
        long code = 0L;
        if (isSupportedSqlMode(sqlMode)) {
            if (isCombineMode(sqlMode)) {
                code |= getCombineMode().get(sqlMode);
            }
            code |= getSupportedSqlMode().get(sqlMode);
        }
        return code;
    }

    // check if this SQL MODE is combine mode
    public static boolean isCombineMode(String key) {
        return combineModeSet.containsKey(key);
    }

    public static Map<String, Long> getSupportedSqlMode() {
        return sqlModeSet;
    }

    public static Map<String, Long> getCombineMode() {
        return combineModeSet;
    }

    /**
     * The SQL mode text that is not the caller's own is parsed under.
     *
     * <p>A security policy's predicate - a row filter or a column mask, whether an administrator wrote it in
     * {@code CREATE ROW POLICY} or an authorization source handed it over - is parsed while planning the
     * query it applies to, on the thread of the very user it restricts. {@code sql_mode} is a session
     * variable any user may set with no privilege at all, and two of its bits change what SQL text means:
     * {@code NO_BACKSLASH_ESCAPES} changes how a string literal decodes and {@code PIPES_AS_CONCAT} turns
     * {@code ||} from OR into concatenation. Reading the mode off the caller would therefore let the
     * restricted user decide what their own restriction says. So policy text is parsed under a fixed mode
     * instead - the one a fresh session has, which is what the mode was when nothing said otherwise.
     */
    public static final long MODE_FOR_POLICY_TEXT = MODE_DEFAULT;

    /**
     * Runs {@code parse} with this session's {@code sql_mode} temporarily replaced, and restores it after.
     *
     * <p>An override rather than a parameter because the mode is not threaded through the parser: the lexer
     * reads it when it is built and the plan builder reads it again when it reaches a {@code ||}, both from
     * the session variable. A statement is planned on one thread, and nothing outside that thread reads this
     * session's {@code sql_mode} while it is swapped - the one thing that writes it from elsewhere is
     * {@code WorkloadActionSetSessionVar}, which sets session variables by name on a policy's behalf; a write
     * landing inside this window is restored on the way out, the same as it would be lost to any other
     * concurrent {@code SET}.
     *
     * <p>With no connection on the thread this is a no-op, and what the parser then reads is the <em>global</em>
     * {@code sql_mode} - {@code VariableMgr.newSessionVariable()} clones the defaults an operator can set - not
     * {@link #MODE_FOR_POLICY_TEXT}. That is where a replay thread recovering a stored policy stands, so a
     * global {@code sql_mode} still decides what such a policy's text means until a session reads it again.
     */
    public static <T> T withSqlMode(long sqlMode, Supplier<T> parse) {
        ConnectContext context = ConnectContext.get();
        if (context == null) {
            // Nothing to override: both readers fall back to a fresh session variable when there is no
            // connection, which already holds the default mode.
            return parse.get();
        }
        SessionVariable sessionVariable = context.getSessionVariable();
        long callerMode = sessionVariable.getSqlMode();
        if (callerMode == sqlMode) {
            return parse.get();
        }
        sessionVariable.setSqlMode(sqlMode);
        try {
            return parse.get();
        } finally {
            sessionVariable.setSqlMode(callerMode);
        }
    }

    public static boolean hasNoBackSlashEscapes() {
        SessionVariable sessionVariable = ConnectContext.get() == null
                ? VariableMgr.newSessionVariable()
                : ConnectContext.get().getSessionVariable();
        return ((sessionVariable.getSqlMode() & MODE_ALLOWED_MASK)
                & MODE_NO_BACKSLASH_ESCAPES) != 0;
    }

    public static boolean hasPipeAsConcat() {
        SessionVariable sessionVariable = ConnectContext.get() == null
                ? VariableMgr.newSessionVariable()
                : ConnectContext.get().getSessionVariable();
        return ((sessionVariable.getSqlMode() & MODE_ALLOWED_MASK)
                & MODE_PIPES_AS_CONCAT) != 0;
    }

    public static boolean hasOnlyFullGroupBy() {
        SessionVariable sessionVariable = ConnectContext.get() == null
                ? VariableMgr.newSessionVariable()
                : ConnectContext.get().getSessionVariable();
        return ((sessionVariable.getSqlMode() & MODE_ALLOWED_MASK)
                & MODE_ONLY_FULL_GROUP_BY) != 0;
    }

}