This is an automated email from the ASF dual-hosted git repository.

CalvinKirs pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new cbed065d54f [feature](fe) Improve MySQL 9 client and Connector/J 
compatibility (#68824)
cbed065d54f is described below

commit cbed065d54f9bef9d27c7848e43929a19df20fce
Author: Calvin Kirs <[email protected]>
AuthorDate: Mon Oct 12 14:32:20 2026 +0800

    [feature](fe) Improve MySQL 9 client and Connector/J compatibility (#68824)
    
    ### What problem does this PR solve?
    
    Problem Summary: Improve Doris FE compatibility with MySQL 9 clients and
    Connector/J 8/9. Clients can consume multiple statements correctly,
    authenticate through caching_sha2 full authentication, query
    authentication_policy, and bind binary TIME parameters in server-side
    prepared statements.
    
    | Area | Supported behavior | Coverage |
    |---|---|---|
    | B1: Multiple statements | Advertise CLIENT_MULTI_STATEMENTS,
    CLIENT_MULTI_RESULTS and CLIENT_PS_MULTI_RESULTS; allowMultiQueries
    clients receive all results and can continue using the connection. |
    Handshake/capability unit tests, existing raw-protocol regression, JDBC
    multiple result sets and mixed SELECT/SET responses. |
    | A1: caching_sha2 full authentication | Support full authentication
    over TLS or RSA, empty passwords, incorrect-password rejection, and
    COM_CHANGE_USER. Use the established channel's actual TLS state during
    login and change-user. | Password resolver unit tests; forced
    caching_sha2 JDBC login and real changeUser over both TLS and RSA,
    including nonempty → empty → nonempty accounts. |
    | A1: Client selection | Add mysql_caching_sha2_password_clients: auto
    (default, client major version ≥9 via _client_version), all (all clients
    requesting this plugin), none (native fallback). Normalize and validate
    options during startup and dynamic updates. | Client routing and
    configuration unit tests; JDBC auto routing and none/native fallback. |
    | D3: authentication_policy | Expose the read-only variable with value
    `*,,` through SHOW VARIABLES and SELECT @@authentication_policy. |
    Variable unit tests and JDBC SHOW/SELECT checks. |
    | F1: Binary TIME parameters | Decode MYSQL_TYPE_TIME/TIME2 in
    server-side prepared statements, including fractional seconds, signed
    durations, zero and NULL. Reject malformed lengths, truncated payloads
    and out-of-range unsigned days without consuming adjacent parameter
    data. | Literal unit tests for malformed framing, overflow and
    ±838:59:59.999999; genuine JDBC ServerPreparedStatement tests for
    setTime, LocalTime, microseconds, NULL, reuse and a following integer
    parameter. |
    
    
    
    ### Release note
    
    Improve MySQL 9 client and Connector/J 8/9 compatibility for
    authentication, multi-statement results, authentication_policy and
    prepared TIME parameters. Reject invalid authentication client options
    at startup.
---
 .../main/java/org/apache/doris/common/Config.java  |  12 ++
 .../java/org/apache/doris/common/ConfigBase.java   |  28 ++-
 .../java/org/apache/doris/qe/GlobalVariable.java   |   6 +
 .../java/org/apache/doris/common/ConfigTest.java   |  67 ++++++
 .../doris/mysql/CachingSha2PasswordExchange.java   | 236 +++++++++++++++++++++
 .../org/apache/doris/mysql/MysqlCapability.java    |  10 +-
 .../java/org/apache/doris/mysql/MysqlChannel.java  |   4 +
 .../password/NativePasswordResolver.java           |  45 +++-
 .../doris/mysql/protocol/MysqlProtocolAdapter.java |   5 +-
 .../nereids/trees/expressions/literal/Literal.java |  36 ++++
 .../org/apache/doris/qe/MysqlConnectProcessor.java |  40 +++-
 .../apache/doris/mysql/MysqlCapabilityTest.java    |  20 +-
 .../doris/mysql/MysqlHandshakePacketTest.java      |   9 +-
 .../org/apache/doris/mysql/MysqlProtoTest.java     |  33 +++
 .../password/NativePasswordResolverTest.java       | 233 ++++++++++++++++++++
 .../expressions/literal/TimeV2LiteralTest.java     | 110 ++++++++++
 .../plans/commands/ShowConfigCommandTest.java      |   7 +-
 .../java/org/apache/doris/qe/VariableMgrTest.java  |  11 +
 .../doris/qe/protocol/MysqlPacketGoldenTest.java   |   9 +-
 .../query_p0/test_multi_statement_response.groovy  |   8 +-
 20 files changed, 899 insertions(+), 30 deletions(-)

diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java 
b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
index d32763cfa76..cec742afde5 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
@@ -2407,6 +2407,18 @@ public class Config extends ConfigBase {
     @ConfField(mutable = false, masterOnly = false)
     public static boolean ssl_force_client_auth = false;
 
+    /**
+     * Which MySQL clients that announce caching_sha2_password are 
authenticated with it (through its
+     * full-authentication exchange, the password is still checked against the 
stored native hash):
+     * "auto" - clients announcing caching_sha2_password with _client_version 
9 or later
+     * (such as mysql / mysqldump 9.x); every other client is switched to 
mysql_native_password
+     * as before. "all" - every client announcing caching_sha2_password. 
"none" - no client;
+     * every client is switched to mysql_native_password, the behavior before 
this option.
+     */
+    @ConfField(mutable = true, masterOnly = false, options = {"auto", "all", 
"none"},
+            callback = OptionsConfHandler.class)
+    public static String mysql_caching_sha2_password_clients = "auto";
+
     /**
      * ssl connection needs to authenticate client's certificate store type.
      */
diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/ConfigBase.java 
b/fe/fe-common/src/main/java/org/apache/doris/common/ConfigBase.java
index 2fa82c3f715..2e84b399999 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/common/ConfigBase.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/common/ConfigBase.java
@@ -82,6 +82,25 @@ public class ConfigBase {
         }
     }
 
+    // The value must be one of the annotation's options; stored as the 
option's own spelling.
+    public static class OptionsConfHandler implements ConfHandler {
+        @Override
+        public void handle(Field field, String confVal) throws Exception {
+            setConfigField(field, checkedOption(field, confVal));
+        }
+
+        // Pure validation shared by startup loading and dynamic updates.
+        private static String checkedOption(Field field, String confVal) 
throws ConfigException {
+            for (String option : 
field.getAnnotation(ConfField.class).options()) {
+                if (option.equalsIgnoreCase(confVal.trim())) {
+                    return option;
+                }
+            }
+            throw new ConfigException("Config '" + field.getName() + "' must 
be one of "
+                    + String.join(", ", 
field.getAnnotation(ConfField.class).options()));
+        }
+    }
+
     static class CommaSeparatedIntersectConfHandler implements ConfHandler {
         @Override
         public void handle(Field field, String newVal) throws Exception {
@@ -298,11 +317,18 @@ public class ConfigBase {
             // ensure that field has property string
             String confKey = f.getName();
             String confVal = props.getProperty(confKey, 
props.getProperty(anno.varType().getPrefix() + confKey));
-            if (Strings.isNullOrEmpty(confVal)) {
+            if (confVal == null) {
                 continue;
             }
 
             try {
+                // Runtime callbacks can have side effects; only options 
validation is safe here.
+                if (anno.callback() == OptionsConfHandler.class) {
+                    confVal = OptionsConfHandler.checkedOption(f, confVal);
+                }
+                if (confVal.isEmpty()) {
+                    continue;
+                }
                 setConfigField(f, confVal);
             } catch (Exception e) {
                 String msg = String.format("Failed to set config, name: %s, 
value: %s", f.getName(), confVal);
diff --git a/fe/fe-common/src/main/java/org/apache/doris/qe/GlobalVariable.java 
b/fe/fe-common/src/main/java/org/apache/doris/qe/GlobalVariable.java
index 46eceb0ecaf..80078ce767a 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/qe/GlobalVariable.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/qe/GlobalVariable.java
@@ -49,6 +49,7 @@ public final class GlobalVariable {
     public static final String VERSION = "version";
     public static final String LOWER_CASE_TABLE_NAMES = 
"lower_case_table_names";
     public static final String LICENSE = "license";
+    public static final String AUTHENTICATION_POLICY = "authentication_policy";
     public static final String LANGUAGE = "language";
     public static final String INIT_CONNECT = "init_connect";
     public static final String SYSTEM_TIME_ZONE = "system_time_zone";
@@ -112,6 +113,11 @@ public final class GlobalVariable {
     @VarAttrDef.VarAttr(name = LICENSE, flag = VarAttrDef.READ_ONLY)
     public static String license = "Apache License, Version 2.0";
 
+    // MySQL 8.0.27+ reports the multi-factor authentication policy here and 
tools built for
+    // MySQL 8.4+ read it. Doris authenticates with a single factor, which is 
the MySQL default.
+    @VarAttrDef.VarAttr(name = AUTHENTICATION_POLICY, flag = 
VarAttrDef.READ_ONLY)
+    public static String authenticationPolicy = "*,,";
+
     @VarAttrDef.VarAttr(name = LANGUAGE, flag = VarAttrDef.READ_ONLY)
     public static String language = "/palo/share/english/";
 
diff --git a/fe/fe-common/src/test/java/org/apache/doris/common/ConfigTest.java 
b/fe/fe-common/src/test/java/org/apache/doris/common/ConfigTest.java
index 06dbdfbd13e..2a00701a4a6 100644
--- a/fe/fe-common/src/test/java/org/apache/doris/common/ConfigTest.java
+++ b/fe/fe-common/src/test/java/org/apache/doris/common/ConfigTest.java
@@ -142,6 +142,73 @@ public class ConfigTest {
         throw new IllegalStateException("config not found: " + key);
     }
 
+    @Test
+    public void testOptionsConfigAcceptsItsOptionsOnly() throws 
ConfigException {
+        String original = Config.mysql_caching_sha2_password_clients;
+        try {
+            ConfigBase.setMutableConfig("mysql_caching_sha2_password_clients", 
"ALL");
+            Assertions.assertEquals("all", 
Config.mysql_caching_sha2_password_clients);
+            Assertions.assertThrows(ConfigException.class,
+                    () -> 
ConfigBase.setMutableConfig("mysql_caching_sha2_password_clients", "bogus"));
+            Assertions.assertEquals("all", 
Config.mysql_caching_sha2_password_clients);
+        } finally {
+            Config.mysql_caching_sha2_password_clients = original;
+        }
+    }
+
+    @Test
+    public void testOptionsConfigIsValidatedDuringStartup() throws Exception {
+        String original = Config.mysql_caching_sha2_password_clients;
+        int originalInterval = 
Config.cloud_warm_up_job_scheduler_interval_millisecond;
+        try {
+            Config config = new Config();
+            Path file = configFile("mysql_caching_sha2_password_clients= ALL 
\n"
+                    + "cloud_warm_up_job_scheduler_interval_millisecond=0\n");
+            config.init(file.toString());
+            Assertions.assertEquals("all", 
Config.mysql_caching_sha2_password_clients);
+            // This unrelated callback rejects zero at runtime; startup must 
not dispatch it.
+            Assertions.assertEquals(0, 
Config.cloud_warm_up_job_scheduler_interval_millisecond);
+            config.init(configFile("").toString());
+            Assertions.assertEquals("all", 
Config.mysql_caching_sha2_password_clients);
+            for (String value : new String[] {"nnoe", "", " "}) {
+                IllegalArgumentException error = 
Assertions.assertThrows(IllegalArgumentException.class,
+                        () -> 
config.init(configFile("mysql_caching_sha2_password_clients=" + value + "\n")
+                                .toString()));
+                Assertions.assertInstanceOf(ConfigException.class, 
error.getCause());
+                Assertions.assertEquals("all", 
Config.mysql_caching_sha2_password_clients);
+            }
+        } finally {
+            Config.mysql_caching_sha2_password_clients = original;
+            Config.cloud_warm_up_job_scheduler_interval_millisecond = 
originalInterval;
+        }
+    }
+
+    @Test
+    public void testOptionsConfigIsValidatedInCustomStartupFile() throws 
Exception {
+        String original = Config.mysql_caching_sha2_password_clients;
+        try {
+            Config config = new Config();
+            
config.init(configFile("mysql_caching_sha2_password_clients=auto\n").toString());
+            Path custom = configFile("mysql_caching_sha2_password_clients= 
NONE \n");
+            config.initCustom(custom.toString());
+            Assertions.assertEquals("none", 
Config.mysql_caching_sha2_password_clients);
+            Files.writeString(custom, 
"mysql_caching_sha2_password_clients=nnoe\n");
+            IllegalArgumentException error = 
Assertions.assertThrows(IllegalArgumentException.class,
+                    () -> config.initCustom(custom.toString()));
+            Assertions.assertInstanceOf(ConfigException.class, 
error.getCause());
+            Assertions.assertEquals("none", 
Config.mysql_caching_sha2_password_clients);
+        } finally {
+            Config.mysql_caching_sha2_password_clients = original;
+        }
+    }
+
+    private static Path configFile(String contents) throws Exception {
+        Path file = Files.createTempFile("fe_ut_options_", ".conf");
+        file.toFile().deleteOnExit();
+        Files.writeString(file, contents);
+        return file;
+    }
+
     @Test
     public void testSetEmptyArray() throws ConfigException {
         ConfigBase.setMutableConfig("mysql_compat_var_whitelist", "a,b,c");
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/mysql/CachingSha2PasswordExchange.java
 
b/fe/fe-core/src/main/java/org/apache/doris/mysql/CachingSha2PasswordExchange.java
new file mode 100644
index 00000000000..df854ddf291
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/mysql/CachingSha2PasswordExchange.java
@@ -0,0 +1,236 @@
+// 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.mysql;
+
+import org.apache.doris.common.Config;
+
+import com.google.common.base.Strings;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.util.Base64;
+import java.util.Map;
+import javax.crypto.Cipher;
+
+/**
+ * Server side of the caching_sha2_password "full authentication" exchange, 
which yields the
+ * client's plaintext password. Doris keeps only the mysql_native_password 
hash, so the fast path
+ * of the plugin (a SHA-256 scramble) can never be verified here; every client 
that authenticates
+ * with this plugin is taken through full authentication and the password is 
then checked the
+ * native way by the caller.
+ *
+ * Why this exists: MySQL 9 moved mysql_native_password from its client 
library's built-in plugins
+ * to a dynamically loadable plugin. A client without that plugin fails when 
asked to switch to it,
+ * before Doris sees a password. Such a client announces caching_sha2_password 
in its handshake
+ * response, with an empty auth response and the connection attribute 
_client_version.
+ *
+ * The exchange, after the client's handshake response:
+ * <pre>
+ *   server: AuthSwitchRequest(caching_sha2_password, nonce)      (the 
handshake nonce is reused)
+ *   client: 32-byte scramble, or empty for an empty password
+ *   server: AuthMoreData 0x04 (perform full authentication)
+ *   client: over TLS, the NUL-terminated plaintext password;
+ *           otherwise 0x02 to ask for the server's RSA public key, or the 
encrypted password
+ *           right away when it already holds the key 
(--server-public-key-path)
+ *   server: AuthMoreData + PEM public key, when asked
+ *   client: RSA(password XOR nonce, NUL-terminated), OAEP or PKCS#1 padded
+ * </pre>
+ * The RSA key pair lives in this process only; a client fetches the public 
key on every
+ * connection, so frontends need not share one.
+ */
+public class CachingSha2PasswordExchange {
+    private static final Logger LOG = 
LogManager.getLogger(CachingSha2PasswordExchange.class);
+
+    public static final String PLUGIN_NAME = "caching_sha2_password";
+    public static final String CLIENT_VERSION_ATTR = "_client_version";
+
+    private static final int AUTH_SWITCH_REQUEST = 0xfe;
+    private static final int AUTH_MORE_DATA = 0x01;
+    private static final int PERFORM_FULL_AUTHENTICATION = 0x04;
+    private static final int REQUEST_PUBLIC_KEY = 0x02;
+    private static final int SCRAMBLE_LENGTH = 32;
+    // libmysqlclient always encrypts with OAEP. Connector/J picks the padding 
from the server
+    // version it was told, OAEP from 8.0.5 on and PKCS#1 v1.5 before, and 
Doris reports itself as
+    // 5.7, so both are accepted; the padding is only told apart by trying.
+    private static final String[] RSA_TRANSFORMATIONS = 
{"RSA/ECB/OAEPWithSHA-1AndMGF1Padding",
+            "RSA/ECB/PKCS1Padding"};
+
+    private static volatile KeyPair keyPair;
+
+    /**
+     * Whether a client that announced this plugin is served with it instead 
of being switched to
+     * mysql_native_password, per Config.mysql_caching_sha2_password_clients: 
"auto" serves
+     * clients reporting _client_version 9 or later; "all" serves every client 
that asks for the
+     * plugin; "none" keeps switching every client to the native plugin.
+     */
+    public static boolean serves(String pluginName, Map<String, String> 
connectAttributes) {
+        if (!PLUGIN_NAME.equals(pluginName)) {
+            return false;
+        }
+        String mode = Config.mysql_caching_sha2_password_clients;
+        if ("all".equalsIgnoreCase(mode)) {
+            return true;
+        }
+        if ("none".equalsIgnoreCase(mode)) {
+            return false;
+        }
+        return 
isClientVersion9OrLater(connectAttributes.get(CLIENT_VERSION_ATTR));
+    }
+
+    // Only the >= 9 decision matters. Stop once reached so a long numeric 
prefix cannot overflow.
+    static boolean isClientVersion9OrLater(String clientVersion) {
+        if (Strings.isNullOrEmpty(clientVersion)) {
+            return false;
+        }
+        int major = 0;
+        for (int i = 0; i < clientVersion.length(); i++) {
+            char digit = clientVersion.charAt(i);
+            if (!Character.isDigit(digit)) {
+                break;
+            }
+            major = major * 10 + Character.digit(digit, 10);
+            if (major >= 9) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** The client answered with something that is not part of the exchange. */
+    public static class Rejected extends Exception {
+        Rejected(String message) {
+            super(message);
+        }
+    }
+
+    /**
+     * Runs the exchange on the channel and returns the plaintext password, or 
null when the
+     * client closed the connection.
+     */
+    public static String exchange(MysqlChannel channel, MysqlSerializer 
serializer, byte[] nonce,
+            boolean ssl) throws IOException, Rejected {
+        serializer.reset();
+        serializer.writeInt1(AUTH_SWITCH_REQUEST);
+        serializer.writeNulTerminateString(PLUGIN_NAME);
+        serializer.writeBytes(nonce);
+        serializer.writeInt1(0);
+        channel.sendAndFlush(serializer.toByteBuffer());
+
+        ByteBuffer scramble = channel.fetchOnePacket();
+        if (scramble == null) {
+            return null;
+        }
+        if (isEmptyPassword(scramble)) {
+            return "";
+        }
+        if (scramble.remaining() != SCRAMBLE_LENGTH) {
+            throw new Rejected("unexpected scramble length " + 
scramble.remaining());
+        }
+
+        serializer.reset();
+        serializer.writeInt1(AUTH_MORE_DATA);
+        serializer.writeInt1(PERFORM_FULL_AUTHENTICATION);
+        channel.sendAndFlush(serializer.toByteBuffer());
+
+        ByteBuffer response = channel.fetchOnePacket();
+        if (response == null) {
+            return null;
+        }
+        if (ssl) {
+            LOG.debug("caching_sha2_password: full authentication over TLS");
+            return new String(MysqlProto.readNulTerminateString(response), 
StandardCharsets.UTF_8);
+        }
+        if (response.remaining() == 1 && (response.get(response.position()) & 
0xff) == REQUEST_PUBLIC_KEY) {
+            serializer.reset();
+            serializer.writeInt1(AUTH_MORE_DATA);
+            
serializer.writeBytes(publicKeyPem().getBytes(StandardCharsets.US_ASCII));
+            channel.sendAndFlush(serializer.toByteBuffer());
+            response = channel.fetchOnePacket();
+            if (response == null) {
+                return null;
+            }
+        }
+        byte[] encrypted = new byte[response.remaining()];
+        response.get(encrypted);
+        LOG.debug("caching_sha2_password: full authentication over RSA");
+        return decryptPassword(encrypted, nonce);
+    }
+
+    // An empty password is answered with no scramble: libmysqlclient sends a 
single NUL byte, the
+    // empty string with its terminator; nothing more is fetched from such a 
client.
+    private static boolean isEmptyPassword(ByteBuffer scramble) {
+        return scramble.remaining() == 0
+                || (scramble.remaining() == 1 && 
scramble.get(scramble.position()) == 0);
+    }
+
+    // RSA ciphertext of (password + NUL) XOR nonce, as libmysqlclient and 
Connector/J send it
+    static String decryptPassword(byte[] encrypted, byte[] nonce) throws 
Rejected {
+        byte[] xored = null;
+        for (String transformation : RSA_TRANSFORMATIONS) {
+            try {
+                Cipher cipher = Cipher.getInstance(transformation);
+                cipher.init(Cipher.DECRYPT_MODE, keyPair().getPrivate());
+                xored = cipher.doFinal(encrypted);
+                break;
+            } catch (GeneralSecurityException e) {
+                LOG.debug("caching_sha2_password: not {}: {}", transformation, 
e.toString());
+            }
+        }
+        if (xored == null) {
+            throw new Rejected("cannot decrypt the password with either RSA 
padding");
+        }
+        byte[] plain = new byte[xored.length];
+        for (int i = 0; i < xored.length; i++) {
+            plain[i] = (byte) (xored[i] ^ nonce[i % nonce.length]);
+        }
+        int end = plain.length;
+        if (end > 0 && plain[end - 1] == 0) {
+            end--;
+        }
+        return new String(plain, 0, end, StandardCharsets.UTF_8);
+    }
+
+    public static String publicKeyPem() {
+        String base64 = Base64.getMimeEncoder(64, 
"\n".getBytes(StandardCharsets.US_ASCII))
+                .encodeToString(keyPair().getPublic().getEncoded());
+        return "-----BEGIN PUBLIC KEY-----\n" + base64 + "\n-----END PUBLIC 
KEY-----\n";
+    }
+
+    private static KeyPair keyPair() {
+        if (keyPair == null) {
+            synchronized (CachingSha2PasswordExchange.class) {
+                if (keyPair == null) {
+                    try {
+                        KeyPairGenerator generator = 
KeyPairGenerator.getInstance("RSA");
+                        generator.initialize(2048);
+                        keyPair = generator.generateKeyPair();
+                    } catch (GeneralSecurityException e) {
+                        throw new IllegalStateException("cannot generate the 
RSA key pair", e);
+                    }
+                }
+            }
+        }
+        return keyPair;
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlCapability.java 
b/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlCapability.java
index 785d71520fd..7a91b0675e4 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlCapability.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlCapability.java
@@ -72,12 +72,20 @@ public class MysqlCapability {
 
     private static final EnumSet<Flag> FLAG_SET = EnumSet.allOf(Flag.class);
 
+    // What the server advertises in the handshake, and its side of the 
negotiation: a connection
+    // gets the intersection of this set with what the client asks for. The 
multi-statement flags
+    // are advertised as MySQL does because Connector/J 8 and 9 mask the flags 
they ask for with the
+    // advertised ones: without them a driver with allowMultiQueries=true 
never asks for
+    // CLIENT_MULTI_STATEMENTS, and gets only the last statement's response of 
a multi-statement
+    // request (see MysqlProtocolAdapter.finishStatement).
     private static final int DEFAULT_FLAGS = 
Flag.CLIENT_PROTOCOL_41.getFlagBit()
             | Flag.CLIENT_CONNECT_WITH_DB.getFlagBit() | 
Flag.CLIENT_SECURE_CONNECTION.getFlagBit()
             | Flag.CLIENT_PLUGIN_AUTH.getFlagBit() | 
Flag.CLIENT_CONNECT_ATTRS.getFlagBit()
             | Flag.CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA.getFlagBit()
             | Flag.CLIENT_LOCAL_FILES.getFlagBit() | 
Flag.CLIENT_LONG_FLAG.getFlagBit()
-            | Flag.CLIENT_DEPRECATE_EOF.getFlagBit();
+            | Flag.CLIENT_DEPRECATE_EOF.getFlagBit()
+            | Flag.CLIENT_MULTI_STATEMENTS.getFlagBit() | 
Flag.CLIENT_MULTI_RESULTS.getFlagBit()
+            | Flag.CLIENT_PS_MULTI_RESULTS.getFlagBit();
 
     private static final int SSL_FLAGS = DEFAULT_FLAGS | 
Flag.CLIENT_SSL.getFlagBit();
 
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlChannel.java 
b/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlChannel.java
index 033dafae2b2..28c85afeca9 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlChannel.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlChannel.java
@@ -163,6 +163,10 @@ public class MysqlChannel implements BytesChannel {
         encryptNetData = 
ByteBuffer.allocate(sslEngine.getSession().getPacketBufferSize() * 2);
     }
 
+    public boolean isSslMode() {
+        return isSslMode;
+    }
+
     public void setSslMode(boolean sslMode) {
         isSslMode = sslMode;
         if (isSslMode) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/password/NativePasswordResolver.java
 
b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/password/NativePasswordResolver.java
index 344c9903699..2299a8037b0 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/password/NativePasswordResolver.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/password/NativePasswordResolver.java
@@ -19,19 +19,26 @@ package org.apache.doris.mysql.authenticate.password;
 
 import org.apache.doris.authentication.CredentialType;
 import org.apache.doris.common.Config;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.mysql.CachingSha2PasswordExchange;
 import org.apache.doris.mysql.MysqlAuthPacket;
 import org.apache.doris.mysql.MysqlChannel;
 import org.apache.doris.mysql.MysqlHandshakePacket;
+import org.apache.doris.mysql.MysqlPassword;
 import org.apache.doris.mysql.MysqlProto;
 import org.apache.doris.mysql.MysqlSerializer;
 import org.apache.doris.mysql.authenticate.AuthenticateRequest;
 import org.apache.doris.qe.ConnectContext;
 
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.Optional;
 
 public class NativePasswordResolver implements PasswordResolver {
+    private static final Logger LOG = 
LogManager.getLogger(NativePasswordResolver.class);
     public static final String MYSQL_RANDOM_STRING_PROPERTY = 
"mysql.random_string";
 
     @Override
@@ -48,8 +55,35 @@ public class NativePasswordResolver implements 
PasswordResolver {
         // which Doris is using now.
         // Note: Check the authPacket whether support plugin auth firstly,
         // before we check AuthPlugin between doris and client to compatible 
with older version: like mysql 5.1
+        // NOTE: when we behind proxy, we need random string sent by proxy.
+        byte[] randomString = handshakePacket.getAuthPluginData();
+        if (Config.proxy_auth_enable && authPacket.getRandomString() != null) {
+            randomString = authPacket.getRandomString();
+        }
         if (authPacket.getCapability().isPluginAuth()
                 && 
!handshakePacket.checkAuthPluginSameAsDoris(authPacket.getPluginName())) {
+            if (CachingSha2PasswordExchange.serves(authPacket.getPluginName(), 
authPacket.getConnectAttributes())) {
+                // Complete caching_sha2_password full authentication for this 
client, then check
+                // the plaintext the native way so stored hashes, the proxy 
nonce and every check
+                // below stay as they are.
+                String plainPassword;
+                try {
+                    plainPassword = 
CachingSha2PasswordExchange.exchange(channel, serializer,
+                            handshakePacket.getAuthPluginData(), 
channel.isSslMode());
+                } catch (CachingSha2PasswordExchange.Rejected e) {
+                    LOG.warn("caching_sha2_password exchange rejected for 
client {}: {}", channel.getRemoteIp(),
+                            e.getMessage());
+                    
context.getState().setError(ErrorCode.ERR_NOT_SUPPORTED_AUTH_MODE,
+                            
ErrorCode.ERR_NOT_SUPPORTED_AUTH_MODE.formatErrorMsg());
+                    MysqlProto.sendResponsePacket(context);
+                    return Optional.empty();
+                }
+                if (plainPassword == null) {
+                    // the client went away, as with the native switch below
+                    return Optional.empty();
+                }
+                return Optional.of(new 
NativePassword(nativeScramble(plainPassword, randomString), randomString));
+            }
             // 1. clear the serializer
             serializer.reset();
             // 2. build the auth switch request and send to the client
@@ -66,14 +100,15 @@ public class NativePasswordResolver implements 
PasswordResolver {
             authResponse = MysqlProto.readEofString(authSwitchResponse);
         }
 
-        // NOTE: when we behind proxy, we need random string sent by proxy.
-        byte[] randomString = handshakePacket.getAuthPluginData();
-        if (Config.proxy_auth_enable && authPacket.getRandomString() != null) {
-            randomString = authPacket.getRandomString();
-        }
         return Optional.of(new NativePassword(authResponse, randomString));
     }
 
+    // What a mysql_native_password client would have sent for this password: 
an empty auth
+    // response for an empty password, the SHA-1 scramble over the nonce 
otherwise.
+    public static byte[] nativeScramble(String plainPassword, byte[] nonce) {
+        return plainPassword.isEmpty() ? new byte[0] : 
MysqlPassword.scramble(nonce, plainPassword);
+    }
+
     @Override
     public Optional<AuthenticateRequest> resolveAuthenticateRequest(String 
userName, ConnectContext context,
             MysqlChannel channel, MysqlSerializer serializer, MysqlAuthPacket 
authPacket,
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/mysql/protocol/MysqlProtocolAdapter.java
 
b/fe/fe-core/src/main/java/org/apache/doris/mysql/protocol/MysqlProtocolAdapter.java
index 4279566e15c..a65bb00d95a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/mysql/protocol/MysqlProtocolAdapter.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/mysql/protocol/MysqlProtocolAdapter.java
@@ -193,7 +193,10 @@ public class MysqlProtocolAdapter implements 
ProtocolAdapter {
      * client it is answering, as {@link #fillForwardRequest} put them in the 
request. A request
      * from an old frontend carries neither the capability flags nor the 
cursor flag; it gets the
      * default capabilities without CLIENT_DEPRECATE_EOF, plus that flag when 
set separately, and
-     * its ordinary prepared statements are not rejected.
+     * its ordinary prepared statements are not rejected. The defaults carry 
the multi-statement
+     * flags, which an old frontend did not advertise; that is of no 
consequence here, because the
+     * master answers one forwarded statement at a time and the channel's 
multi-statement flag is
+     * set only by the handshake on the frontend the client is connected to.
      */
     public void restoreFromForwardRequest(ConnectContext ctx, TMasterOpRequest 
request) {
         int flags = request.isSetMysqlCapability() ? 
request.getMysqlCapability()
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/Literal.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/Literal.java
index bbe0e32b11c..92de0c55f1f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/Literal.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/Literal.java
@@ -580,6 +580,10 @@ public abstract class Literal extends Expression 
implements LeafExpression {
             case MYSQL_TYPE_TIMESTAMP2:
                 literal = handleDateTimeLiteral(data);
                 break;
+            case MYSQL_TYPE_TIME:
+            case MYSQL_TYPE_TIME2:
+                literal = handleTimeLiteral(data);
+                break;
             case MYSQL_TYPE_STRING:
             case MYSQL_TYPE_VARSTRING:
                 literal = handleStringLiteral(data);
@@ -658,6 +662,38 @@ public abstract class Literal extends Expression 
implements LeafExpression {
         }
     }
 
+    // Protocol::MYSQL_TYPE_TIME: length 0 is 00:00:00; 8 carries is_negative, 
days, hours,
+    // minutes, seconds; 12 adds microseconds.
+    private static Literal handleTimeLiteral(ByteBuffer data) throws 
AnalysisException {
+        if (!data.hasRemaining()) {
+            throw new AnalysisException("Missing binary TIME parameter 
length");
+        }
+        int len = MysqlProto.readInt1(data);
+        if (len != 0 && len != 8 && len != 12) {
+            throw new AnalysisException("Invalid binary TIME parameter length: 
" + len);
+        }
+        if (data.remaining() < len) {
+            throw new AnalysisException("Truncated binary TIME parameter");
+        }
+        if (len == 0) {
+            return new TimeV2Literal(0, 0, 0, 0, 6, false);
+        }
+        int sign = MysqlProto.readInt1(data);
+        if (sign != 0 && sign != 1) {
+            throw new AnalysisException("Invalid binary TIME parameter sign: " 
+ sign);
+        }
+        long days = Integer.toUnsignedLong(data.getInt());
+        long hours = days * 24 + MysqlProto.readInt1(data);
+        int minute = MysqlProto.readInt1(data);
+        int second = MysqlProto.readInt1(data);
+        int microsecond = len == 12 ? data.getInt() : 0;
+        try {
+            return new TimeV2Literal(Math.toIntExact(hours), minute, second, 
microsecond, 6, sign == 1);
+        } catch (ArithmeticException e) {
+            throw new AnalysisException("Binary TIME parameter is out of 
range", e);
+        }
+    }
+
     private static Literal handleStringLiteral(ByteBuffer data) {
         int strLen = getParmLen(data);
         strLen = Math.min(strLen, data.remaining());
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/qe/MysqlConnectProcessor.java 
b/fe/fe-core/src/main/java/org/apache/doris/qe/MysqlConnectProcessor.java
index cf9504d852f..44495a1bf0e 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/MysqlConnectProcessor.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/MysqlConnectProcessor.java
@@ -29,11 +29,13 @@ import org.apache.doris.common.ErrorCode;
 import org.apache.doris.common.ErrorReport;
 import org.apache.doris.common.Pair;
 import org.apache.doris.datasource.InternalCatalog;
+import org.apache.doris.mysql.CachingSha2PasswordExchange;
 import org.apache.doris.mysql.MysqlChannel;
 import org.apache.doris.mysql.MysqlCommand;
 import org.apache.doris.mysql.MysqlHandshakePacket;
 import org.apache.doris.mysql.MysqlProto;
 import org.apache.doris.mysql.MysqlSerializer;
+import org.apache.doris.mysql.authenticate.password.NativePasswordResolver;
 import org.apache.doris.mysql.privilege.Auth;
 import org.apache.doris.mysql.protocol.MysqlProtocolAdapter;
 import org.apache.doris.nereids.StatementContext;
@@ -366,16 +368,34 @@ public class MysqlConnectProcessor extends 
ConnectProcessor {
         if (!MysqlHandshakePacket.AUTH_PLUGIN_NAME.equals(authPluginName)) {
             MysqlChannel channel = ctx.getMysqlChannel();
             MysqlSerializer serializer = MysqlSerializer.newInstance();
-            serializer.writeInt1((byte) 0xfe);
-            
serializer.writeNulTerminateString(MysqlHandshakePacket.AUTH_PLUGIN_NAME);
-            serializer.writeBytes(authPluginData);
-            serializer.writeInt1(0);
-            channel.sendAndFlush(serializer.toByteBuffer());
-            // Server receive auth switch response packet from client.
-            ByteBuffer authSwitchResponse = channel.fetchOnePacket();
-            int length = authSwitchResponse.limit();
-            password = new byte[length];
-            System.arraycopy(authSwitchResponse.array(), 0, password, 0, 
length);
+            if (CachingSha2PasswordExchange.serves(authPluginName, 
ctx.getConnectAttributes())) {
+                // Same client class as at login (see NativePasswordResolver): 
full authentication,
+                // then the plaintext is checked the native way below.
+                String plainPassword;
+                try {
+                    plainPassword = 
CachingSha2PasswordExchange.exchange(channel, serializer,
+                            authPluginData, channel.isSslMode());
+                } catch (CachingSha2PasswordExchange.Rejected e) {
+                    LOG.warn("caching_sha2_password exchange rejected on 
COM_CHANGE_USER: {}", e.getMessage());
+                    plainPassword = null;
+                }
+                if (plainPassword == null) {
+                    ctx.getState().setError(ErrorCode.ERR_ACCESS_DENIED_ERROR, 
"Authentication failed.");
+                    return;
+                }
+                password = 
NativePasswordResolver.nativeScramble(plainPassword, authPluginData);
+            } else {
+                serializer.writeInt1((byte) 0xfe);
+                
serializer.writeNulTerminateString(MysqlHandshakePacket.AUTH_PLUGIN_NAME);
+                serializer.writeBytes(authPluginData);
+                serializer.writeInt1(0);
+                channel.sendAndFlush(serializer.toByteBuffer());
+                // Server receive auth switch response packet from client.
+                ByteBuffer authSwitchResponse = channel.fetchOnePacket();
+                int length = authSwitchResponse.limit();
+                password = new byte[length];
+                System.arraycopy(authSwitchResponse.array(), 0, password, 0, 
length);
+            }
         }
 
         // For safety, not allowed to change to root or admin.
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlCapabilityTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlCapabilityTest.java
index 7b7ee02cda8..f4f6d9e857c 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlCapabilityTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlCapabilityTest.java
@@ -44,12 +44,30 @@ public class MysqlCapabilityTest {
     public void testDefaultFlags() {
         MysqlCapability capability = MysqlCapability.DEFAULT_CAPABILITY;
         Assertions.assertEquals("CLIENT_LONG_FLAG | CLIENT_CONNECT_WITH_DB | 
CLIENT_LOCAL_FILES | CLIENT_PROTOCOL_41"
-                + " | CLIENT_SECURE_CONNECTION | CLIENT_PLUGIN_AUTH | 
CLIENT_CONNECT_ATTRS"
+                + " | CLIENT_SECURE_CONNECTION | CLIENT_MULTI_STATEMENTS | 
CLIENT_MULTI_RESULTS"
+                + " | CLIENT_PS_MULTI_RESULTS | CLIENT_PLUGIN_AUTH | 
CLIENT_CONNECT_ATTRS"
                 + " | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA | 
CLIENT_DEPRECATE_EOF",
                 capability.toString());
         Assertions.assertTrue(capability.supportClientLocalFile());
         Assertions.assertTrue(capability.isConnectAttrs());
         Assertions.assertTrue(capability.isPluginAuthDataLengthEncoded());
         Assertions.assertTrue(capability.isDeprecatedEOF());
+        Assertions.assertTrue(capability.isClientMultiStatements());
+        Assertions.assertFalse(capability.isClientUseSsl());
+    }
+
+    @Test
+    public void testSslFlags() {
+        MysqlCapability capability = MysqlCapability.SSL_CAPABILITY;
+        Assertions.assertEquals(
+                MysqlCapability.DEFAULT_CAPABILITY.getFlags() | 
MysqlCapability.Flag.CLIENT_SSL.getFlagBit(),
+                capability.getFlags());
+        Assertions.assertEquals("CLIENT_LONG_FLAG | CLIENT_CONNECT_WITH_DB | 
CLIENT_LOCAL_FILES | CLIENT_PROTOCOL_41"
+                + " | CLIENT_SSL | CLIENT_SECURE_CONNECTION | 
CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS"
+                + " | CLIENT_PS_MULTI_RESULTS | CLIENT_PLUGIN_AUTH | 
CLIENT_CONNECT_ATTRS"
+                + " | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA | 
CLIENT_DEPRECATE_EOF",
+                capability.toString());
+        Assertions.assertTrue(capability.isClientUseSsl());
+        Assertions.assertTrue(capability.isClientMultiStatements());
     }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlHandshakePacketTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlHandshakePacketTest.java
index 3b262281f24..26ae3cc7997 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlHandshakePacketTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlHandshakePacketTest.java
@@ -28,6 +28,9 @@ import org.mockito.Mockito;
 import java.nio.ByteBuffer;
 
 public class MysqlHandshakePacketTest {
+    private static final int MULTI_RESULT_FLAGS = 
MysqlCapability.Flag.CLIENT_MULTI_RESULTS.getFlagBit()
+            | MysqlCapability.Flag.CLIENT_PS_MULTI_RESULTS.getFlagBit();
+
     private byte[] buf;
     private MysqlCapability capability;
     private MockedStatic<MysqlPassword> mockedMysqlPassword;
@@ -82,6 +85,11 @@ public class MysqlHandshakePacketTest {
         MysqlCapability advertisedCapability = new MysqlCapability(flags);
         Assertions.assertTrue(advertisedCapability.isConnectAttrs());
         
Assertions.assertTrue(advertisedCapability.isPluginAuthDataLengthEncoded());
+        // The multi-statement flags MySQL advertises, in the upper two bytes: 
a driver that masks
+        // what it asks for with the advertised set (Connector/J 8 and 9) 
keeps CLIENT_MULTI_STATEMENTS
+        // only when they are there.
+        Assertions.assertTrue(advertisedCapability.isClientMultiStatements());
+        Assertions.assertEquals(MULTI_RESULT_FLAGS, flags & 
MULTI_RESULT_FLAGS);
         // length of plugin data
         Assertions.assertEquals(21, MysqlProto.readInt1(buffer));
         // length of plugin data
@@ -100,5 +108,4 @@ public class MysqlHandshakePacketTest {
         Assertions.assertEquals(0, MysqlProto.readInt1(buffer));
         Assertions.assertEquals(22, buffer.remaining());
     }
-
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlProtoTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlProtoTest.java
index f8eec649da7..c7931776d71 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlProtoTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlProtoTest.java
@@ -202,6 +202,39 @@ public class MysqlProtoTest {
         Assertions.assertTrue(MysqlProto.negotiate(context));
     }
 
+    @Test
+    public void testNegotiateSetsClientMultiStatementsWhenTheClientAsksForIt() 
throws Exception {
+        // The client echoes the advertised capabilities, which carry 
CLIENT_MULTI_STATEMENTS: what
+        // Connector/J 8 and 9 with allowMultiQueries send, since they mask 
the flags they ask for
+        // with the advertised ones.
+        mockChannel("user", true);
+        mockPassword(true);
+        mockAccess();
+        ConnectContext context = createContext();
+        context.setEnv(env);
+        context.setThreadLocalInfo();
+        Assertions.assertTrue(MysqlProto.negotiate(context));
+        
Assertions.assertTrue(context.getCapability().isClientMultiStatements());
+        Mockito.verify(channel).setClientMultiStatements();
+    }
+
+    @Test
+    public void 
testNegotiateLeavesClientMultiStatementsWhenTheClientDoesNotAskForIt() throws 
Exception {
+        mockChannel("user", true);
+        ByteBuffer handshake = channel.fetchOnePacket();
+        int clientFlags = MysqlCapability.DEFAULT_CAPABILITY.getFlags()
+                & ~MysqlCapability.Flag.CLIENT_MULTI_STATEMENTS.getFlagBit();
+        handshake.putInt(0, Integer.reverseBytes(clientFlags));
+        mockPassword(true);
+        mockAccess();
+        ConnectContext context = createContext();
+        context.setEnv(env);
+        context.setThreadLocalInfo();
+        Assertions.assertTrue(MysqlProto.negotiate(context));
+        
Assertions.assertFalse(context.getCapability().isClientMultiStatements());
+        Mockito.verify(channel, Mockito.never()).setClientMultiStatements();
+    }
+
     @Test
     public void testNegotiateUsesClientServerCapabilityIntersection() throws 
Exception {
         mockChannel("user", true);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/password/NativePasswordResolverTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/password/NativePasswordResolverTest.java
new file mode 100644
index 00000000000..c6c7268a74e
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/password/NativePasswordResolverTest.java
@@ -0,0 +1,233 @@
+// 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.mysql.authenticate.password;
+
+import org.apache.doris.common.Config;
+import org.apache.doris.mysql.CachingSha2PasswordExchange;
+import org.apache.doris.mysql.MysqlAuthPacket;
+import org.apache.doris.mysql.MysqlCapability;
+import org.apache.doris.mysql.MysqlChannel;
+import org.apache.doris.mysql.MysqlHandshakePacket;
+import org.apache.doris.mysql.MysqlPassword;
+import org.apache.doris.mysql.MysqlSerializer;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.QueryState;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+import java.util.Map;
+import javax.crypto.Cipher;
+
+class NativePasswordResolverTest {
+    private static final Map<String, String> MYSQL_9_CLIENT = 
ImmutableMap.of("_client_name", "libmysql",
+            CachingSha2PasswordExchange.CLIENT_VERSION_ATTR, "9.4.0");
+    private static final Map<String, String> MYSQL_8_CLIENT = 
ImmutableMap.of("_client_name", "libmysql",
+            CachingSha2PasswordExchange.CLIENT_VERSION_ATTR, "8.0.44");
+
+    private String previousMode;
+    private final MysqlHandshakePacket handshake = new MysqlHandshakePacket(1);
+    private final MysqlChannel channel = Mockito.mock(MysqlChannel.class);
+
+    @BeforeEach
+    void setUp() {
+        previousMode = Config.mysql_caching_sha2_password_clients;
+        Mockito.when(channel.getRemoteIp()).thenReturn("127.0.0.1");
+    }
+
+    @AfterEach
+    void tearDown() {
+        Config.mysql_caching_sha2_password_clients = previousMode;
+    }
+
+    @Test
+    void testMysql9ClientGetsFullAuthenticationOverRsa() throws Exception {
+        byte[] nonce = handshake.getAuthPluginData();
+        Mockito.when(channel.fetchOnePacket()).thenReturn(
+                ByteBuffer.wrap(new byte[32]),                 // its 
caching_sha2 scramble, ignored
+                ByteBuffer.wrap(new byte[] {0x02}),            // asks for the 
public key
+                ByteBuffer.wrap(encryptLikeClient("secret", nonce, 
"RSA/ECB/OAEPWithSHA-1AndMGF1Padding")));
+
+        NativePassword password = 
resolve(authPacket(MysqlCapability.DEFAULT_CAPABILITY, MYSQL_9_CLIENT));
+
+        Assertions.assertArrayEquals(MysqlPassword.scramble(nonce, "secret"), 
password.getRemotePasswd());
+        Assertions.assertArrayEquals(nonce, password.getRandomString());
+        // AuthSwitchRequest, perform-full-authentication, public key
+        Mockito.verify(channel, Mockito.times(3)).sendAndFlush(Mockito.any());
+    }
+
+    @Test
+    void testConnectorJPkcs1PaddingIsAcceptedToo() throws Exception {
+        // Connector/J takes the padding from the server version and Doris 
reports 5.7: PKCS#1 v1.5
+        byte[] nonce = handshake.getAuthPluginData();
+        Mockito.when(channel.fetchOnePacket()).thenReturn(
+                ByteBuffer.wrap(new byte[32]),
+                ByteBuffer.wrap(new byte[] {0x02}),
+                ByteBuffer.wrap(encryptLikeClient("secret", nonce, 
"RSA/ECB/PKCS1Padding")));
+
+        NativePassword password = 
resolve(authPacket(MysqlCapability.DEFAULT_CAPABILITY, MYSQL_9_CLIENT));
+
+        Assertions.assertArrayEquals(MysqlPassword.scramble(nonce, "secret"), 
password.getRemotePasswd());
+    }
+
+    @Test
+    void testUndecryptableAnswerIsRejectedWithAnError() throws Exception {
+        Mockito.when(channel.fetchOnePacket()).thenReturn(
+                ByteBuffer.wrap(new byte[32]),
+                ByteBuffer.wrap(new byte[] {0x02}),
+                ByteBuffer.wrap(new byte[256]));
+        
Mockito.when(channel.getSerializer()).thenReturn(MysqlSerializer.newInstance());
+        ConnectContext context = Mockito.mock(ConnectContext.class);
+        QueryState state = new QueryState();
+        Mockito.when(context.getState()).thenReturn(state);
+        Mockito.when(context.getMysqlChannel()).thenReturn(channel);
+
+        Assertions.assertFalse(new 
NativePasswordResolver().resolvePassword(context, channel,
+                MysqlSerializer.newInstance(), 
authPacket(MysqlCapability.DEFAULT_CAPABILITY, MYSQL_9_CLIENT),
+                handshake).isPresent());
+        Assertions.assertEquals(QueryState.MysqlStateType.ERR, 
state.getStateType());
+        // AuthSwitchRequest, perform-full-authentication, public key, the 
error packet
+        Mockito.verify(channel, Mockito.times(4)).sendAndFlush(Mockito.any());
+    }
+
+    @Test
+    void testMysql9ClientSendsThePlaintextOverTls() throws Exception {
+        byte[] nonce = handshake.getAuthPluginData();
+        Mockito.when(channel.fetchOnePacket()).thenReturn(
+                ByteBuffer.wrap(new byte[32]),
+                ByteBuffer.wrap("secret\0".getBytes(StandardCharsets.UTF_8)));
+
+        // The negotiated capabilities omit CLIENT_SSL even after TLS has been 
established.
+        Mockito.when(channel.isSslMode()).thenReturn(true);
+        NativePassword password = 
resolve(authPacket(MysqlCapability.DEFAULT_CAPABILITY, MYSQL_9_CLIENT));
+
+        Assertions.assertArrayEquals(MysqlPassword.scramble(nonce, "secret"), 
password.getRemotePasswd());
+        Mockito.verify(channel, Mockito.times(2)).sendAndFlush(Mockito.any());
+    }
+
+    @Test
+    void testEmptyPasswordNeedsNoFullAuthentication() throws Exception {
+        // libmysqlclient answers the switch with the empty string and its 
terminator: one NUL byte
+        Mockito.when(channel.fetchOnePacket()).thenReturn(ByteBuffer.wrap(new 
byte[] {0}));
+
+        NativePassword password = 
resolve(authPacket(MysqlCapability.DEFAULT_CAPABILITY, MYSQL_9_CLIENT));
+
+        Assertions.assertEquals(0, password.getRemotePasswd().length);
+        Mockito.verify(channel, Mockito.times(1)).sendAndFlush(Mockito.any());
+    }
+
+    @Test
+    void testMysql8ClientIsStillSwitchedToNativePassword() throws Exception {
+        byte[] nativeResponse = new byte[20];
+        nativeResponse[0] = 7;
+        
Mockito.when(channel.fetchOnePacket()).thenReturn(ByteBuffer.wrap(nativeResponse));
+
+        NativePassword password = 
resolve(authPacket(MysqlCapability.DEFAULT_CAPABILITY, MYSQL_8_CLIENT));
+
+        Assertions.assertArrayEquals(nativeResponse, 
password.getRemotePasswd());
+        Mockito.verify(channel, Mockito.times(1)).sendAndFlush(Mockito.any());
+    }
+
+    @Test
+    void testOversizedClientVersionStillUsesFullAuthentication() throws 
Exception {
+        Config.mysql_caching_sha2_password_clients = "auto";
+        Map<String, String> attributes = 
ImmutableMap.of(CachingSha2PasswordExchange.CLIENT_VERSION_ATTR,
+                "2147483648.0");
+        byte[] nonce = handshake.getAuthPluginData();
+        Mockito.when(channel.fetchOnePacket()).thenReturn(
+                ByteBuffer.wrap(new byte[32]),
+                ByteBuffer.wrap("secret\0".getBytes(StandardCharsets.UTF_8)));
+        Mockito.when(channel.isSslMode()).thenReturn(true);
+
+        
Assertions.assertTrue(CachingSha2PasswordExchange.serves(CachingSha2PasswordExchange.PLUGIN_NAME,
+                attributes));
+        NativePassword password = 
resolve(authPacket(MysqlCapability.DEFAULT_CAPABILITY, attributes));
+
+        Assertions.assertArrayEquals(MysqlPassword.scramble(nonce, "secret"), 
password.getRemotePasswd());
+        Mockito.verify(channel, Mockito.times(2)).sendAndFlush(Mockito.any());
+    }
+
+    @Test
+    void testModeNoneSwitchesEveryClientAndModeAllServesEveryClient() throws 
Exception {
+        Config.mysql_caching_sha2_password_clients = "none";
+        
Assertions.assertFalse(CachingSha2PasswordExchange.serves(CachingSha2PasswordExchange.PLUGIN_NAME,
+                MYSQL_9_CLIENT));
+        Config.mysql_caching_sha2_password_clients = "all";
+        
Assertions.assertTrue(CachingSha2PasswordExchange.serves(CachingSha2PasswordExchange.PLUGIN_NAME,
+                ImmutableMap.of()));
+        Config.mysql_caching_sha2_password_clients = "auto";
+        
Assertions.assertFalse(CachingSha2PasswordExchange.serves(CachingSha2PasswordExchange.PLUGIN_NAME,
+                ImmutableMap.of()));
+        
Assertions.assertFalse(CachingSha2PasswordExchange.serves(CachingSha2PasswordExchange.PLUGIN_NAME,
+                
ImmutableMap.of(CachingSha2PasswordExchange.CLIENT_VERSION_ATTR, 
"00000000000000000008.0")));
+        
Assertions.assertTrue(CachingSha2PasswordExchange.serves(CachingSha2PasswordExchange.PLUGIN_NAME,
+                
ImmutableMap.of(CachingSha2PasswordExchange.CLIENT_VERSION_ATTR, 
"00000000000000000009.0")));
+        
Assertions.assertFalse(CachingSha2PasswordExchange.serves("mysql_native_password",
 MYSQL_9_CLIENT));
+        
Assertions.assertFalse(CachingSha2PasswordExchange.serves(CachingSha2PasswordExchange.PLUGIN_NAME,
+                
ImmutableMap.of(CachingSha2PasswordExchange.CLIENT_VERSION_ATTR, "x")));
+    }
+
+    @Test
+    void testClientClosingTheConnectionMidExchangeYieldsNoRequest() throws 
Exception {
+        Mockito.when(channel.fetchOnePacket()).thenReturn(ByteBuffer.wrap(new 
byte[32]), (ByteBuffer) null);
+
+        Assertions.assertFalse(new 
NativePasswordResolver().resolvePassword(Mockito.mock(ConnectContext.class), 
channel,
+                MysqlSerializer.newInstance(), 
authPacket(MysqlCapability.DEFAULT_CAPABILITY, MYSQL_9_CLIENT),
+                handshake).isPresent());
+    }
+
+    private NativePassword resolve(MysqlAuthPacket authPacket) throws 
Exception {
+        return (NativePassword) new 
NativePasswordResolver().resolvePassword(Mockito.mock(ConnectContext.class),
+                channel, MysqlSerializer.newInstance(), authPacket, handshake)
+                .orElseThrow(() -> new AssertionError("password is required"));
+    }
+
+    private static MysqlAuthPacket authPacket(MysqlCapability capability, 
Map<String, String> attributes) {
+        MysqlAuthPacket authPacket = Mockito.mock(MysqlAuthPacket.class);
+        Mockito.when(authPacket.getCapability()).thenReturn(capability);
+        
Mockito.when(authPacket.getPluginName()).thenReturn(CachingSha2PasswordExchange.PLUGIN_NAME);
+        Mockito.when(authPacket.getConnectAttributes()).thenReturn(attributes);
+        Mockito.when(authPacket.getAuthResponse()).thenReturn(new byte[0]);
+        return authPacket;
+    }
+
+    // What a client does with the PEM key the server sent: RSA over (password 
+ NUL) XOR nonce
+    private static byte[] encryptLikeClient(String password, byte[] nonce, 
String transformation) throws Exception {
+        String pem = CachingSha2PasswordExchange.publicKeyPem()
+                .replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END 
PUBLIC KEY-----", "").replace("\n", "");
+        PublicKey key = KeyFactory.getInstance("RSA")
+                .generatePublic(new 
X509EncodedKeySpec(Base64.getDecoder().decode(pem)));
+        byte[] plain = (password + "\0").getBytes(StandardCharsets.UTF_8);
+        for (int i = 0; i < plain.length; i++) {
+            plain[i] ^= nonce[i % nonce.length];
+        }
+        Cipher cipher = Cipher.getInstance(transformation);
+        cipher.init(Cipher.ENCRYPT_MODE, key);
+        return cipher.doFinal(plain);
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/TimeV2LiteralTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/TimeV2LiteralTest.java
index 96775d44bfc..a05786857da 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/TimeV2LiteralTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/TimeV2LiteralTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.doris.nereids.trees.expressions.literal;
 
+import org.apache.doris.catalog.MysqlColType;
 import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.types.DateTimeV2Type;
@@ -26,6 +27,9 @@ import org.apache.doris.nereids.types.TimeV2Type;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
 public class TimeV2LiteralTest {
 
     @Test
@@ -186,4 +190,110 @@ public class TimeV2LiteralTest {
         Assertions.assertEquals(59, dateTime.getSecond());
     }
 
+    @Test
+    public void testFromMysqlBinaryTimeParameter() throws AnalysisException {
+        // Protocol::MYSQL_TYPE_TIME, 12 bytes: is_negative, days, hours, 
minutes, seconds, microseconds
+        ByteBuffer data = 
ByteBuffer.allocate(13).order(ByteOrder.LITTLE_ENDIAN);
+        data.put((byte) 12).put((byte) 0).putInt(0).put((byte) 12).put((byte) 
34).put((byte) 56).putInt(123456).flip();
+        Literal literal = 
Literal.getLiteralByMysqlType(MysqlColType.MYSQL_TYPE_TIME, false, data);
+        TimeV2Literal time = Assertions.assertInstanceOf(TimeV2Literal.class, 
literal);
+        Assertions.assertEquals("12:34:56.123456", time.getStringValue());
+
+        // 8 bytes: no microseconds; negative, and the days fold into the hours
+        data = ByteBuffer.allocate(9).order(ByteOrder.LITTLE_ENDIAN);
+        data.put((byte) 8).put((byte) 1).putInt(1).put((byte) 1).put((byte) 
2).put((byte) 3).flip();
+        time = (TimeV2Literal) 
Literal.getLiteralByMysqlType(MysqlColType.MYSQL_TYPE_TIME2, false, data);
+        Assertions.assertEquals("-25:02:03.000000", time.getStringValue());
+
+        // 0 bytes: 00:00:00
+        data = ByteBuffer.allocate(1).order(ByteOrder.LITTLE_ENDIAN);
+        data.put((byte) 0).flip();
+        time = (TimeV2Literal) 
Literal.getLiteralByMysqlType(MysqlColType.MYSQL_TYPE_TIME, false, data);
+        Assertions.assertEquals("00:00:00.000000", time.getStringValue());
+    }
+
+    @Test
+    public void testBinaryTimeRejectsInvalidFraming() {
+        for (MysqlColType type : new MysqlColType[] 
{MysqlColType.MYSQL_TYPE_TIME, MysqlColType.MYSQL_TYPE_TIME2}) {
+            Assertions.assertThrows(AnalysisException.class,
+                    () -> Literal.getLiteralByMysqlType(type, false, 
ByteBuffer.allocate(0)));
+            for (int len : new int[] {1, 7, 9, 11, 13, 252, 253, 254, 255}) {
+                ByteBuffer data = 
ByteBuffer.allocate(17).order(ByteOrder.LITTLE_ENDIAN);
+                data.put((byte) len).position(17);
+                data.flip();
+                Assertions.assertThrows(AnalysisException.class,
+                        () -> Literal.getLiteralByMysqlType(type, false, 
data), "length=" + len);
+                Assertions.assertEquals(1, data.position(), "invalid length 
must not consume parameter data");
+            }
+            for (int len : new int[] {8, 12}) {
+                for (int available = 0; available < len; available++) {
+                    ByteBuffer data = ByteBuffer.allocate(available + 
1).order(ByteOrder.LITTLE_ENDIAN);
+                    data.put((byte) len).position(available + 1);
+                    data.flip();
+                    Assertions.assertThrows(AnalysisException.class,
+                            () -> Literal.getLiteralByMysqlType(type, false, 
data));
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testBinaryTimeKeepsParameterBoundaries() {
+        // The malformed first value used to read the second TIME's length as 
days=8.
+        ByteBuffer malformed = ByteBuffer.wrap(new byte[] {1, 0, 8, 0, 0, 0, 
0, 0, 0, 0, 0})
+                .order(ByteOrder.LITTLE_ENDIAN);
+        Assertions.assertThrows(AnalysisException.class,
+                () -> 
Literal.getLiteralByMysqlType(MysqlColType.MYSQL_TYPE_TIME, false, malformed));
+        Assertions.assertEquals(1, malformed.position());
+
+        ByteBuffer valid = 
ByteBuffer.allocate(13).order(ByteOrder.LITTLE_ENDIAN);
+        valid.put((byte) 8).put((byte) 0).putInt(0).put((byte) 12).put((byte) 
34).put((byte) 56)
+                .putInt(7654321).flip();
+        Assertions.assertEquals("12:34:56.000000",
+                Literal.getLiteralByMysqlType(MysqlColType.MYSQL_TYPE_TIME, 
false, valid).getStringValue());
+        Assertions.assertEquals(7654321,
+                Literal.getLiteralByMysqlType(MysqlColType.MYSQL_TYPE_LONG, 
false, valid).getValue());
+        Assertions.assertFalse(valid.hasRemaining());
+    }
+
+    @Test
+    public void testBinaryTimeRejectsOutOfRangeDurations() {
+        for (MysqlColType type : new MysqlColType[] 
{MysqlColType.MYSQL_TYPE_TIME, MysqlColType.MYSQL_TYPE_TIME2}) {
+            for (int days : new int[] {35, 0x20000000, 0x80000000, 
0xffffffff}) {
+                ByteBuffer data = binaryTime(0, days, 0, 0, 0, 0);
+                Assertions.assertThrows(AnalysisException.class,
+                        () -> Literal.getLiteralByMysqlType(type, false, 
data));
+            }
+            Assertions.assertThrows(AnalysisException.class,
+                    () -> Literal.getLiteralByMysqlType(type, false, 
binaryTime(0, 34, 23, 0, 0, 0)));
+            Assertions.assertThrows(AnalysisException.class,
+                    () -> Literal.getLiteralByMysqlType(type, false, 
binaryTime(0, 0, 0, 0, 0, 1000000)));
+            for (int sign : new int[] {2, 255}) {
+                Assertions.assertThrows(AnalysisException.class,
+                        () -> Literal.getLiteralByMysqlType(type, false, 
binaryTime(sign, 0, 0, 0, 0, 0)));
+            }
+        }
+    }
+
+    @Test
+    public void testBinaryTimeAcceptsMaximumDuration() {
+        for (MysqlColType type : new MysqlColType[] 
{MysqlColType.MYSQL_TYPE_TIME, MysqlColType.MYSQL_TYPE_TIME2}) {
+            Assertions.assertEquals("838:59:59.999999",
+                    Literal.getLiteralByMysqlType(type, false, binaryTime(0, 
34, 22, 59, 59, 999999)).getStringValue());
+            Assertions.assertEquals("-838:59:59.999999",
+                    Literal.getLiteralByMysqlType(type, false, binaryTime(1, 
34, 22, 59, 59, 999999)).getStringValue());
+        }
+    }
+
+    private static ByteBuffer binaryTime(int sign, int days, int hour, int 
minute, int second, int microsecond) {
+        int len = microsecond == 0 ? 8 : 12;
+        ByteBuffer data = ByteBuffer.allocate(len + 
1).order(ByteOrder.LITTLE_ENDIAN);
+        data.put((byte) len).put((byte) sign).putInt(days).put((byte) 
hour).put((byte) minute).put((byte) second);
+        if (len == 12) {
+            data.putInt(microsecond);
+        }
+        data.flip();
+        return data;
+    }
+
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowConfigCommandTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowConfigCommandTest.java
index 8856d7cd846..12d5a4af434 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowConfigCommandTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowConfigCommandTest.java
@@ -122,9 +122,8 @@ public class ShowConfigCommandTest extends 
TestWithFeService {
     public void testMaskAllPasswordConfigsWithPattern() throws Exception {
         ShowResultSet resultSet = runShowFrontendConfig("%password%");
         List<List<String>> rows = resultSet.getResultRows();
-        // The pattern '%password%' matches 6 configs: the 5 sensitive ones 
plus
-        // tls_cert_based_auth_ignore_password which is not a secret.
-        Assertions.assertEquals(SENSITIVE_KEYS.size() + 1, rows.size());
+        // The pattern '%password%' matches the sensitive configs and two 
non-sensitive configs.
+        Assertions.assertEquals(SENSITIVE_KEYS.size() + 2, rows.size());
         Map<String, String> keyToValue = rows.stream()
                 .collect(Collectors.toMap(row -> row.get(0), row -> 
row.get(1)));
         for (String sensitiveKey : SENSITIVE_KEYS) {
@@ -134,6 +133,8 @@ public class ShowConfigCommandTest extends 
TestWithFeService {
         }
         // A non-sensitive config containing "password" in its name keeps its 
real value.
         Assertions.assertEquals("false", 
keyToValue.get("tls_cert_based_auth_ignore_password"));
+        Assertions.assertEquals(Config.mysql_caching_sha2_password_clients,
+                keyToValue.get("mysql_caching_sha2_password_clients"));
     }
 
     @Test
diff --git a/fe/fe-core/src/test/java/org/apache/doris/qe/VariableMgrTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/qe/VariableMgrTest.java
index 2bd7c696de3..455e9801e9a 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/qe/VariableMgrTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/qe/VariableMgrTest.java
@@ -87,6 +87,17 @@ public class VariableMgrTest extends TestWithFeService {
         Assertions.assertThrows(DdlException.class, () -> 
VariableMgr.setVar(null, setVar));
     }
 
+    @Test
+    public void testAuthenticationPolicyIsTheReadOnlyMysqlDefault() throws 
Exception {
+        SessionVariable sessionVariable = new SessionVariable();
+        VariableExpr variableExpr = new 
VariableExpr(GlobalVariable.AUTHENTICATION_POLICY);
+        VariableMgr.fillValue(sessionVariable, variableExpr);
+        Assertions.assertEquals("*,,", VariableMgr.getValue(sessionVariable, 
variableExpr));
+
+        SetVar setVar = new SetVar(SetType.GLOBAL, 
GlobalVariable.AUTHENTICATION_POLICY, new StringLiteral("*,"));
+        Assertions.assertThrows(DdlException.class, () -> 
VariableMgr.setVar(sessionVariable, setVar));
+    }
+
     @Test
     public void testVariableCallback() throws Exception {
         SetOptionsCommand stmt = (SetOptionsCommand) UtFrameUtils.parseStmt(
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/MysqlPacketGoldenTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/MysqlPacketGoldenTest.java
index 871f99b310f..6bd88c3faed 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/MysqlPacketGoldenTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/MysqlPacketGoldenTest.java
@@ -77,11 +77,14 @@ public class MysqlPacketGoldenTest extends 
TestWithFeService {
     private static final String DB_NAME = "protocol_golden_db";
     private static final String TABLE_NAME = "golden_tbl";
 
-    private static final int MODERN_CLIENT = 
MysqlCapability.DEFAULT_CAPABILITY.getFlags();
+    // A client echoing the advertised set, which asks for 
CLIENT_MULTI_STATEMENTS: what Connector/J
+    // with allowMultiQueries sends, since it masks the flags it asks for with 
the advertised ones.
+    private static final int MULTI_STATEMENT_CLIENT = 
MysqlCapability.DEFAULT_CAPABILITY.getFlags();
+    // The same client without CLIENT_MULTI_STATEMENTS (Connector/J without 
allowMultiQueries).
+    private static final int MODERN_CLIENT =
+            MULTI_STATEMENT_CLIENT & 
~MysqlCapability.Flag.CLIENT_MULTI_STATEMENTS.getFlagBit();
     private static final int LEGACY_EOF_CLIENT =
             MODERN_CLIENT & 
~MysqlCapability.Flag.CLIENT_DEPRECATE_EOF.getFlagBit();
-    private static final int MULTI_STATEMENT_CLIENT =
-            MODERN_CLIENT | 
MysqlCapability.Flag.CLIENT_MULTI_STATEMENTS.getFlagBit();
 
     @Override
     protected void runBeforeAll() throws Exception {
diff --git 
a/regression-test/suites/query_p0/test_multi_statement_response.groovy 
b/regression-test/suites/query_p0/test_multi_statement_response.groovy
index 088298fa9d7..0f7dac65753 100644
--- a/regression-test/suites/query_p0/test_multi_statement_response.groovy
+++ b/regression-test/suites/query_p0/test_multi_statement_response.groovy
@@ -33,10 +33,10 @@ import java.sql.Statement
 // statement's OK alone; it used to answer with the packets of the preceding 
query followed by the
 // OK, a stream no client can parse.
 //
-// Connector/J exercises the second case only: it asks for 
CLIENT_MULTI_STATEMENTS only when the
-// server advertises it, which Doris does not, and it always asks for 
CLIENT_DEPRECATE_EOF. The
-// other combinations are driven through a bare protocol client below, which 
also sees the response
-// packet by packet.
+// Connector/J asks for CLIENT_MULTI_STATEMENTS only with 
allowMultiQueries=true, and 8.x and 9.x
+// only when the server advertises it too, which Doris does like MySQL; it 
always asks for
+// CLIENT_DEPRECATE_EOF. The other combinations are driven through a bare 
protocol client below,
+// which also sees the response packet by packet.
 suite("test_multi_statement_response") {
     def tableName = "multi_statement_response"
     sql "DROP TABLE IF EXISTS ${tableName}"


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to