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 */
    /* The mode a session carries when nothing has said otherwise. Not what a new session starts with:
     * SessionVariable initialises sql_mode to MODE_ONLY_FULL_GROUP_BY. */
    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 - {@link #MODE_DEFAULT}, the mode a session carries when nothing has said otherwise. It is
     * not the mode a fresh session starts with: {@code SessionVariable.sqlMode} is initialised to
     * {@code MODE_ONLY_FULL_GROUP_BY}, a bit that changes nothing about what SQL text means.
     */
    public static final long MODE_FOR_POLICY_TEXT = MODE_DEFAULT;

    /**
     * The mode {@link #withSqlMode} is currently running under, or null outside such a window.
     *
     * <p>An override held beside the session rather than written into it: the parse it wraps may itself set
     * {@code sql_mode}, and a {@code SET_VAR} hint in the text does exactly that. Writing the fixed mode into
     * the live {@code SessionVariable} let such a hint snapshot the swapped-in value as the caller's own and
     * restore it as permanent when the statement ended, silently dropping the caller's {@code sql_mode} for
     * the rest of the connection. Holding it here instead makes the window immune to anything that writes the
     * session variable, and gives it effect on threads that have no connection at all - the ones that replay
     * a journal or load an image and have to recover a stored policy under the same mode that will execute it.
     */
    private static final ThreadLocal<Long> MODE_OVERRIDE = new ThreadLocal<>();

    /**
     * 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
     * through {@link #hasNoBackSlashEscapes} and {@link #hasPipeAsConcat}. Those readers take the override
     * ahead of any session, so the window holds whatever the parse does to the session variable - a
     * {@code SET_VAR} hint in the text included - and holds on threads that have no connection.
     *
     * <p>Windows nest: an inner one restores the outer mode rather than dropping back to the session.
     */
    public static <T> T withSqlMode(long sqlMode, Supplier<T> parse) {
        Long enclosing = MODE_OVERRIDE.get();
        MODE_OVERRIDE.set(sqlMode);
        try {
            return parse.get();
        } finally {
            if (enclosing == null) {
                MODE_OVERRIDE.remove();
            } else {
                MODE_OVERRIDE.set(enclosing);
            }
        }
    }

    /**
     * The {@code sql_mode} that decides what the text being read right now means: the mode
     * {@link #withSqlMode} put in force if one is, this session's otherwise, and the global one when there is
     * no session on the thread.
     */
    private static long readingMode() {
        Long override = MODE_OVERRIDE.get();
        if (override != null) {
            return override;
        }
        SessionVariable sessionVariable = ConnectContext.get() == null
                ? VariableMgr.newSessionVariable()
                : ConnectContext.get().getSessionVariable();
        return sessionVariable.getSqlMode();
    }

    public static boolean hasNoBackSlashEscapes() {
        return ((readingMode() & MODE_ALLOWED_MASK) & MODE_NO_BACKSLASH_ESCAPES) != 0;
    }

    public static boolean hasPipeAsConcat() {
        return ((readingMode() & MODE_ALLOWED_MASK) & MODE_PIPES_AS_CONCAT) != 0;
    }

    public static boolean hasOnlyFullGroupBy() {
        return ((readingMode() & MODE_ALLOWED_MASK) & MODE_ONLY_FULL_GROUP_BY) != 0;
    }

}