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 60042611fea [test](protocol) Record a golden baseline of MySQL packets 
and Arrow Flight results (#67789)
60042611fea is described below

commit 60042611fea1b18576470a7e3c49e14cd11243a4
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Fri Sep 11 09:44:01 2026 +0800

    [test](protocol) Record a golden baseline of MySQL packets and Arrow Flight 
results (#67789)
    
    ### What problem does this PR solve?
    
    Issue Number: #67577 -- the tracking issue for the protocol-agnostic
    session and execution
    layer. This is the first PR of its Stage 1 and does not close it.
    
    **In plain terms.** Nothing in this repository checks the bytes the
    server sends to a client. Tests
    stop at "the statement succeeded and the result had two rows"; how many
    packets carried it, in what
    order, with which sequence ids, whether the result set ended in an EOF
    or an OK, and whether it was
    flushed, is checked by nobody. The next PRs move the MySQL and the Arrow
    Flight SQL front ends onto
    one shared session layer, which must not change a single byte on the
    wire -- but if one of them
    drops a packet or reorders two, every existing test still passes and the
    only symptom is a client
    that hangs, weeks later, with no way to tell which refactor did it. So
    this PR records what the
    server sends today into two checked-in files, before anything moves.
    From then on, a change to
    those bytes turns a test red and names the exact line and byte that
    moved. It touches no production
    code -- it photographs the rooms before the renovation starts.
    
    Problem Summary:
    
    Nothing in the tree asserts the bytes a MySQL client actually receives,
    or the Arrow batches an
    Arrow Flight SQL client actually receives. Every test around the
    frontend's result path stops at the
    `QueryState` or at a `ShowResultSet`; the packet framing below that --
    how many packets, in what
    order, with which sequence ids, terminated by an EOF or an OK, flushed
    where -- is only exercised by
    real clients in the regression suites, and only for the handful of
    statements those suites run.
    
    That gap is about to matter. The session and result layers are being
    reorganized so that MySQL and
    Arrow Flight SQL become two front ends over one session (the follow-up
    PRs extract a protocol
    adapter and a result sender out of `ConnectContext`, `ConnectProcessor`
    and `StmtExecutor`). Those
    are pure refactors, and a pure refactor that drops an EOF packet,
    reorders a column definition, or
    shifts a sequence id passes every existing unit test and shows up only
    as a client that hangs.
    
    This PR takes the baseline first, before anything moves. It adds no
    production code.
    
    ### What is changed?
    
    Four test classes under
    `fe/fe-core/src/test/java/org/apache/doris/qe/protocol/` and two golden
    files under `fe/fe-core/src/test/resources/protocol-golden/`:
    
    - `RecordingMysqlChannel` -- a `DummyMysqlChannel` that plays canned
    request packets and records
    every response packet: its payload, the sequence id it was framed with,
    and whether the response
    was flushed at it. The sequence id is advanced the way the real channel
    advances it (once per
    packet read, once per packet written), so what is recorded is what would
    reach the wire.
    - `ProtocolGolden` -- renders the recorded traffic as an annotated
    hexdump and compares it with the
    checked-in file. On a mismatch it writes the current traffic to
    `target/protocol-golden/<name>`
    and names it in the failure message, the same convention
    `AccessControlBehaviorBaselineTest` uses.
    - `MysqlPacketGoldenTest` -- 27 cases: constant selects, a NULL literal,
    an empty result set,
    `SHOW VARIABLES`, `DESC` (the one case whose row stream is longer than a
    single packet), `SET`,
    `USE`, `EXPLAIN`, a syntax error, an unknown table, an error followed by
    a healthy statement on the
    same connection, multi-statement requests with and without
    `CLIENT_MULTI_STATEMENTS`, and the
    connection commands (`COM_FIELD_LIST`, `COM_STMT_PREPARE`,
    `COM_STMT_CLOSE`, `COM_SET_OPTION`,
    `COM_RESET_CONNECTION`, `COM_PING`, `COM_INIT_DB`, `COM_STATISTICS`, an
    unknown command,
    `COM_QUIT`). Two of them run with `CLIENT_DEPRECATE_EOF` negotiated off,
    the capability that
    decides whether a result set ends in an EOF or an OK and whether the
    column definitions get their
      own terminator.
    - `FlightResultGoldenTest` -- the same statements an Arrow Flight SQL
    session can be answered
    frontend-side, recorded as the schema and rows of the `VectorSchemaRoot`
    the session caches, or as
      the error it failed with.
    
    Capability negotiation is replayed rather than mocked: the test derives
    the effective capability the
    way `MysqlProto.negotiate()` does (`server & client`) and sets
    `clientDeprecatedEOF`,
    `clientMultiStatements` and the serializer capability from it, so the
    recorded packets match what a
    real connection with those flags would produce.
    
    Two responses are recorded as shape rather than bytes, because their
    payload legitimately moves with
    changes that have nothing to do with the protocol: a parser error
    carries the entire keyword list of
    the grammar (4881 bytes today), and an `EXPLAIN` carries the current
    plan text. For those the golden
    keeps the packet kinds, plus the error code, SQL state and the start of
    the message.
    
    The statement set is limited to what a frontend answers on its own.
    `COM_STMT_EXECUTE` is
    deliberately absent: `ConnectContext.supportHandleByFe()` is false for
    it, so its result always comes
    from a backend and belongs in `prepared_stmt_p0`, not here. No case in
    either test reaches a backend
    (verified: the mocked backend logs no `exec_plan_fragment` request
    during the run).
    
    The two golden files are added to `.licenserc.yaml`'s `paths-ignore`:
    both tests regenerate the file
    in full and compare it byte for byte, so a license header would be read
    back as unexpected content.
    
    ### Release note
    
    None.
    
    ### Check List (For Author)
    
    - Test
        - [x] Unit Test
        - [ ] Regression test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    
    - Behavior changed:
        - [x] No.
        - [ ] Yes.
    
    - Does this need documentation?
        - [x] No.
        - [ ] Yes.
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .licenserc.yaml                                    |   5 +
 .../doris/qe/protocol/FlightResultGoldenTest.java  | 205 ++++++++++++
 .../doris/qe/protocol/MysqlPacketGoldenTest.java   | 288 +++++++++++++++++
 .../apache/doris/qe/protocol/ProtocolGolden.java   | 226 +++++++++++++
 .../doris/qe/protocol/RecordingMysqlChannel.java   | 141 +++++++++
 .../resources/protocol-golden/flight-results.txt   |  50 +++
 .../resources/protocol-golden/mysql-packets.txt    | 351 +++++++++++++++++++++
 7 files changed, 1266 insertions(+)

diff --git a/.licenserc.yaml b/.licenserc.yaml
index f92ce264daf..ba286646d74 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -64,6 +64,11 @@ header:
     # the whole file and compares it to this one line for line, so a header 
would read back
     # as extra rows; it prints the regenerated path on mismatch instead.
     - "fe/fe-core/src/test/resources/access-control-behavior-baseline.txt"
+    # Golden protocol traffic. MysqlPacketGoldenTest and 
FlightResultGoldenTest regenerate
+    # each file in full and compare it to this one byte for byte, so a header 
would read back
+    # as unexpected leading content; both tests print the regeneration command 
on mismatch.
+    - "fe/fe-core/src/test/resources/protocol-golden/mysql-packets.txt"
+    - "fe/fe-core/src/test/resources/protocol-golden/flight-results.txt"
     # Connector plugin settings templates. build.sh seeds each connector's live
     # <name>.conf from its template verbatim (cp -n), so the template's 
content IS
     # the file an administrator edits in plugins/connector/<dir>/. Matched by 
name
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/FlightResultGoldenTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/FlightResultGoldenTest.java
new file mode 100644
index 00000000000..89a32d876d1
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/FlightResultGoldenTest.java
@@ -0,0 +1,205 @@
+// 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.protocol;
+
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.util.DebugUtil;
+import org.apache.doris.qe.QueryState.MysqlStateType;
+import org.apache.doris.service.arrowflight.FlightSqlConnectProcessor;
+import org.apache.doris.service.arrowflight.results.FlightSqlResultCacheEntry;
+import org.apache.doris.service.arrowflight.sessions.FlightSqlConnectContext;
+import org.apache.doris.utframe.TestWithFeService;
+
+import com.google.common.collect.Lists;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * Records what an Arrow Flight SQL session gets back for the statements a 
frontend answers itself,
+ * and compares it with a golden file.
+ *
+ * <p>Counterpart of {@link MysqlPacketGoldenTest}: the two protocols are 
being moved onto one
+ * session and one result path, so both ends need a baseline taken before the 
move. What a Flight
+ * client sees is the cached {@code VectorSchemaRoot} -- its schema and its 
rows -- or the error the
+ * statement failed with, so that is what this records.
+ *
+ * <p>Only statements a frontend can answer are covered. An Arrow Flight 
session never handles a
+ * query in the frontend ({@code ConnectContext.supportHandleByFe()} is false 
for it), so a
+ * {@code SELECT} reaches a backend and produces endpoints instead of a cached 
result; those belong
+ * in the {@code arrow_flight_sql_p0} regression suite.
+ *
+ * <p>What is covered: {@code SHOW VARIABLES}, {@code SHOW DATABASES} and 
{@code DESC} (results the
+ * frontend materializes and caches), {@code SET} and {@code USE} (no cached 
result, which is how the
+ * producer knows to synthesize its {@code StatusResult=0} row), {@code 
EXPLAIN}, a syntax error and
+ * an unknown table. Every column comes back as {@code Utf8} today -- that is 
the current behavior,
+ * and typing those results is a later step of the same work.
+ *
+ * <p>Regenerate from the {@code fe} directory with:
+ * {@code mvn test -pl fe-common,fe-core -am -Dtest=FlightResultGoldenTest
+ * -Ddoris.protocol.golden.regenerate=true} -- {@code -am} is required, the 
reactor does not
+ * resolve {@code ${revision}} without it.
+ */
+public class FlightResultGoldenTest extends TestWithFeService {
+    private static final String GOLDEN_FILE = "flight-results.txt";
+    private static final String DB_NAME = "protocol_golden_flight_db";
+    private static final String TABLE_NAME = "golden_tbl";
+    private static final String PEER_IDENTITY = "protocol-golden-peer";
+    private static final int MESSAGE_PREFIX_LENGTH = 72;
+
+    @Override
+    protected void runBeforeAll() throws Exception {
+        createDatabaseAndUse(DB_NAME);
+        createTable("create table " + TABLE_NAME + " (k1 int, k2 varchar(32)) 
duplicate key(k1)"
+                + " distributed by hash(k1) buckets 1 
properties('replication_num' = '1');");
+    }
+
+    @Test
+    public void testFlightResultGolden() throws Exception {
+        StringBuilder actual = new StringBuilder();
+        for (FlightCase flightCase : cases()) {
+            actual.append(render(flightCase.statement, flightCase.detail));
+        }
+        ProtocolGolden.verify(GOLDEN_FILE, actual.toString());
+    }
+
+    private List<FlightCase> cases() {
+        return Lists.newArrayList(
+                new FlightCase("show variables like 'wait_timeout'", 
Detail.ROWS),
+                new FlightCase("show databases like '" + DB_NAME + "'", 
Detail.ROWS),
+                new FlightCase("desc " + TABLE_NAME, Detail.ROWS),
+                new FlightCase("set sql_select_limit = 100", Detail.ROWS),
+                new FlightCase("use " + DB_NAME, Detail.ROWS),
+                // The plan text and the node ids inside it move with the 
planner, so only the shape
+                // of the answer is recorded. Same reason as 
ProtocolGolden.Fidelity.SUMMARY.
+                new FlightCase("explain select 1", Detail.SHAPE),
+                // A parser error carries the whole keyword list of the 
grammar.
+                new FlightCase("select from", Detail.SHAPE),
+                new FlightCase("select * from no_such_table", Detail.ROWS));
+    }
+
+    private String render(String statement, Detail detail) throws Exception {
+        FlightSqlConnectContext ctx = newContext();
+        StringBuilder rendered = new StringBuilder();
+        rendered.append("=== statement ").append(statement).append(" ===\n");
+        try (FlightSqlConnectProcessor processor = new 
FlightSqlConnectProcessor(ctx)) {
+            processor.handleQuery(statement);
+            rendered.append(renderOutcome(ctx, detail));
+        } finally {
+            connectContext.setThreadLocalInfo();
+        }
+        rendered.append('\n');
+        return rendered.toString();
+    }
+
+    private String renderOutcome(FlightSqlConnectContext ctx, Detail detail) {
+        StringBuilder rendered = new StringBuilder();
+        if (ctx.getState().getStateType() == MysqlStateType.ERR) {
+            String message = ctx.getState().getErrorMessage().replace("\n", 
"\\n").replace("\r", "\\r");
+            if (detail == Detail.SHAPE && message.length() > 
MESSAGE_PREFIX_LENGTH) {
+                message = message.substring(0, MESSAGE_PREFIX_LENGTH) + "...";
+            }
+            rendered.append("state: ERR 
errorCode=").append(ctx.getState().getErrorCode())
+                    .append(" message=\"").append(message).append("\"\n");
+            return rendered.toString();
+        }
+        rendered.append("state: 
").append(ctx.getState().getStateType()).append('\n');
+        if (ctx.getFlightSqlChannel().resultNum() == 0) {
+            // DorisFlightSqlProducer answers such a statement with a 
synthesized one-row
+            // StatusResult=0; nothing was cached by the frontend itself.
+            rendered.append("result: none, the producer synthesizes 
StatusResult=0\n");
+            return rendered.toString();
+        }
+        FlightSqlResultCacheEntry entry = 
ctx.getFlightSqlChannel().getResult(DebugUtil.printId(ctx.queryId()));
+        if (entry == null) {
+            rendered.append("result: cached under an unexpected query id\n");
+            return rendered.toString();
+        }
+        rendered.append(renderRoot(entry.getVectorSchemaRoot(), detail));
+        return rendered.toString();
+    }
+
+    private String renderRoot(VectorSchemaRoot root, Detail detail) {
+        StringBuilder rendered = new StringBuilder();
+        rendered.append("schema:\n");
+        for (Field field : root.getSchema().getFields()) {
+            rendered.append("  ").append(field.getName()).append(": 
").append(field.getType())
+                    .append(field.isNullable() ? " nullable" : " not 
null").append('\n');
+        }
+        if (detail == Detail.SHAPE) {
+            rendered.append("rows: ").append(root.getRowCount() > 0 ? "some" : 
"none").append('\n');
+            return rendered.toString();
+        }
+        rendered.append("rows: ").append(root.getRowCount()).append('\n');
+        for (int row = 0; row < root.getRowCount(); row++) {
+            StringBuilder line = new StringBuilder();
+            for (FieldVector vector : root.getFieldVectors()) {
+                if (line.length() > 0) {
+                    line.append(" | ");
+                }
+                line.append(valueOf(vector, row));
+            }
+            // Trailing spaces would be an empty last column; keep them out of 
the file so an editor
+            // or a whitespace lint cannot silently rewrite the golden.
+            rendered.append("  
").append(line.toString().stripTrailing()).append('\n');
+        }
+        return rendered.toString();
+    }
+
+    private String valueOf(FieldVector vector, int row) {
+        if (vector.isNull(row)) {
+            return "NULL";
+        }
+        if (vector instanceof VarCharVector) {
+            return new String(((VarCharVector) vector).get(row), 
StandardCharsets.UTF_8);
+        }
+        return String.valueOf(vector.getObject(row));
+    }
+
+    /** How much of a result to keep; see ProtocolGolden.Fidelity for why the 
second mode exists. */
+    private enum Detail {
+        ROWS,
+        SHAPE
+    }
+
+    private static class FlightCase {
+        private final String statement;
+        private final Detail detail;
+
+        FlightCase(String statement, Detail detail) {
+            this.statement = statement;
+            this.detail = detail;
+        }
+    }
+
+    private FlightSqlConnectContext newContext() {
+        FlightSqlConnectContext ctx = new 
FlightSqlConnectContext(PEER_IDENTITY);
+        ctx.setCurrentUserIdentity(UserIdentity.ROOT);
+        ctx.setRemoteIP("127.0.0.1");
+        ctx.setEnv(Env.getCurrentEnv());
+        ctx.setDatabase(DB_NAME);
+        ctx.setThreadLocalInfo();
+        return ctx;
+    }
+}
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
new file mode 100644
index 00000000000..43d4bdf1b97
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/MysqlPacketGoldenTest.java
@@ -0,0 +1,288 @@
+// 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.protocol;
+
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.mysql.MysqlCapability;
+import org.apache.doris.nereids.StatementContext;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.MysqlConnectProcessor;
+import org.apache.doris.utframe.TestWithFeService;
+
+import com.google.common.collect.Lists;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * Records the MySQL packets a set of commands produces and compares them with 
a golden file.
+ *
+ * <p>This is the safety net for the protocol-independent session work: the 
MySQL front end is being
+ * pulled apart into a protocol adapter and a result sender, and nothing else 
in the tree asserts the
+ * bytes that leave the server. A refactor that changes a sequence id, drops 
an EOF, or reorders a
+ * column definition passes every other unit test and only shows up as a hung 
or confused client.
+ *
+ * <p>The statement set is deliberately limited to what a frontend can answer 
on its own. Anything
+ * that needs a backend belongs in the regression suites, not here -- in 
particular
+ * {@code COM_STMT_EXECUTE}, which {@link 
org.apache.doris.qe.ConnectContext#supportHandleByFe()}
+ * always sends to one, so the cursor-fetch packet boundaries live in {@code 
prepared_stmt_p0}.
+ *
+ * <p>What is covered:
+ * <ul>
+ *   <li>result sets a frontend computes: a literal, two columns, a session 
variable, a NULL, and an
+ *       empty result set;</li>
+ *   <li>the same result set with {@code CLIENT_DEPRECATE_EOF} negotiated off, 
which decides whether
+ *       a result set ends in an EOF or an OK, and adds one after the column 
definitions;</li>
+ *   <li>{@code SHOW VARIABLES} (both EOF flavors), {@code DESC} (two data 
rows), {@code SET},
+ *       {@code USE}, {@code EXPLAIN};</li>
+ *   <li>errors: a syntax error, an unknown table, and an error followed by a 
healthy statement on
+ *       the same connection;</li>
+ *   <li>multi-statement requests with and without {@code 
CLIENT_MULTI_STATEMENTS}, which decides
+ *       whether the intermediate result set gets a terminator at all;</li>
+ *   <li>connection commands: {@code COM_FIELD_LIST}, {@code COM_STMT_PREPARE},
+ *       {@code COM_STMT_CLOSE}, {@code COM_SET_OPTION}, {@code 
COM_RESET_CONNECTION},
+ *       {@code COM_PING}, {@code COM_INIT_DB}, {@code COM_STATISTICS}, an 
unknown command, and
+ *       {@code COM_QUIT}.</li>
+ * </ul>
+ *
+ * <p>Regenerate from the {@code fe} directory with:
+ * {@code mvn test -pl fe-common,fe-core -am -Dtest=MysqlPacketGoldenTest
+ * -Ddoris.protocol.golden.regenerate=true} -- {@code -am} is required, the 
reactor does not
+ * resolve {@code ${revision}} without it.
+ */
+public class MysqlPacketGoldenTest extends TestWithFeService {
+    private static final String GOLDEN_FILE = "mysql-packets.txt";
+    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();
+    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 {
+        createDatabaseAndUse(DB_NAME);
+        createTable("create table " + TABLE_NAME + " (k1 int, k2 varchar(32)) 
duplicate key(k1)"
+                + " distributed by hash(k1) buckets 1 
properties('replication_num' = '1');");
+    }
+
+    @Test
+    public void testMysqlPacketGolden() throws Exception {
+        StringBuilder actual = new StringBuilder();
+        for (GoldenCase goldenCase : cases()) {
+            actual.append(render(goldenCase));
+        }
+        ProtocolGolden.verify(GOLDEN_FILE, actual.toString());
+    }
+
+    private List<GoldenCase> cases() {
+        List<GoldenCase> cases = Lists.newArrayList();
+        cases.add(new GoldenCase("select-literal", MODERN_CLIENT)
+                .add(query("select 1")));
+        cases.add(new GoldenCase("select-two-columns", MODERN_CLIENT)
+                .add(query("select 1, 'a'")));
+        cases.add(new GoldenCase("select-session-variable", MODERN_CLIENT)
+                .add(query("select @@wait_timeout")));
+        cases.add(new GoldenCase("select-null-literal", MODERN_CLIENT)
+                .add(query("select null")));
+        cases.add(new GoldenCase("select-empty-result", MODERN_CLIENT)
+                .add(query("select k1 from " + TABLE_NAME + " where 1 = 0")));
+        cases.add(new GoldenCase("select-literal-legacy-eof", 
LEGACY_EOF_CLIENT)
+                .add(query("select 1")));
+        cases.add(new GoldenCase("show-variables", MODERN_CLIENT)
+                .add(query("show variables like 'wait_timeout'")));
+        cases.add(new GoldenCase("show-variables-legacy-eof", 
LEGACY_EOF_CLIENT)
+                .add(query("show variables like 'wait_timeout'")));
+        // Two data rows: the only case here where the row stream is longer 
than one packet.
+        cases.add(new GoldenCase("describe-table", MODERN_CLIENT)
+                .add(query("desc " + TABLE_NAME)));
+        cases.add(new GoldenCase("set-session-variable", MODERN_CLIENT)
+                .add(query("set sql_select_limit = 100")));
+        cases.add(new GoldenCase("use-database", MODERN_CLIENT)
+                .add(query("use " + DB_NAME)));
+        cases.add(new GoldenCase("explain-select", MODERN_CLIENT, 
ProtocolGolden.Fidelity.SUMMARY)
+                .add(query("explain select 1")));
+        cases.add(new GoldenCase("syntax-error", MODERN_CLIENT, 
ProtocolGolden.Fidelity.SUMMARY)
+                .add(query("select from")));
+        cases.add(new GoldenCase("unknown-table", MODERN_CLIENT)
+                .add(query("select * from no_such_table")));
+        cases.add(new GoldenCase("error-then-next-command", MODERN_CLIENT)
+                .add(query("select * from no_such_table"))
+                .add(query("select 1")));
+        cases.add(new GoldenCase("multi-statement-with-capability", 
MULTI_STATEMENT_CLIENT)
+                .add(query("select 1; select 2")));
+        cases.add(new GoldenCase("multi-statement-without-capability", 
MODERN_CLIENT)
+                .add(query("select 1; select 2")));
+        cases.add(new GoldenCase("com-field-list", MODERN_CLIENT)
+                .add(fieldList(TABLE_NAME)));
+        cases.add(new GoldenCase("com-stmt-prepare", MODERN_CLIENT)
+                .add(command("COM_STMT_PREPARE select ?", 0x16, "select ?")));
+        cases.add(new GoldenCase("com-stmt-close", MODERN_CLIENT)
+                .add(stmtClose(1)));
+        cases.add(new GoldenCase("com-set-option", MODERN_CLIENT)
+                .add(setOption(0)));
+        cases.add(new GoldenCase("com-reset-connection", MODERN_CLIENT)
+                .add(command("COM_RESET_CONNECTION", 0x1F, "")));
+        cases.add(new GoldenCase("com-ping", MODERN_CLIENT)
+                .add(command("COM_PING", 0x0E, "")));
+        cases.add(new GoldenCase("com-init-db", MODERN_CLIENT)
+                .add(command("COM_INIT_DB " + DB_NAME, 0x02, DB_NAME)));
+        cases.add(new GoldenCase("com-statistics", MODERN_CLIENT)
+                .add(command("COM_STATISTICS", 0x09, "")));
+        cases.add(new GoldenCase("com-unknown", MODERN_CLIENT)
+                .add(command("unknown command 0x2A", 0x2A, "")));
+        cases.add(new GoldenCase("com-quit", MODERN_CLIENT)
+                .add(command("COM_QUIT", 0x01, "")));
+        return cases;
+    }
+
+    private String render(GoldenCase goldenCase) throws Exception {
+        RecordingMysqlChannel channel = new RecordingMysqlChannel();
+        ConnectContext ctx = newContext(channel, goldenCase.clientFlags);
+        MysqlConnectProcessor processor = new MysqlConnectProcessor(ctx);
+        StringBuilder rendered = new StringBuilder();
+        rendered.append("=== case ").append(goldenCase.name).append(" ===\n");
+        rendered.append("client capability: 
").append(describe(goldenCase.clientFlags)).append('\n');
+        try {
+            for (Command command : goldenCase.commands) {
+                channel.clearOutbound();
+                channel.queueRequest(command.payload);
+                processor.processOnce();
+                rendered.append("--> ").append(command.label).append('\n');
+                
rendered.append(ProtocolGolden.renderPackets(channel.getOutbound(), 
goldenCase.fidelity));
+            }
+        } finally {
+            // Hand the thread back to the context TestWithFeService 
installed, so the helpers it
+            // offers (create table, drop database) keep working after a case 
has run.
+            connectContext.setThreadLocalInfo();
+        }
+        rendered.append('\n');
+        return rendered.toString();
+    }
+
+    private ConnectContext newContext(RecordingMysqlChannel channel, int 
clientFlags) {
+        ConnectContext ctx = new GoldenConnectContext(channel);
+        ctx.setCurrentUserIdentity(UserIdentity.ROOT);
+        ctx.setRemoteIP("127.0.0.1");
+        ctx.setEnv(Env.getCurrentEnv());
+        ctx.setDatabase(DB_NAME);
+        ctx.setStatementContext(new StatementContext());
+        // Replay what MysqlProto.negotiate() derives from the client's 
capability flags: the
+        // effective capability is the intersection with the server's, and the 
two channel flags are
+        // set from it. Everything the golden records downstream depends on 
this.
+        MysqlCapability clientCapability = new MysqlCapability(clientFlags);
+        ctx.setCapability(new 
MysqlCapability(ctx.getServerCapability().getFlags() & 
clientCapability.getFlags()));
+        if (ctx.getCapability().isDeprecatedEOF()) {
+            channel.setClientDeprecatedEOF();
+        }
+        if (clientCapability.isClientMultiStatements()) {
+            channel.setClientMultiStatements();
+        }
+        channel.getSerializer().setCapability(ctx.getCapability());
+        ctx.setThreadLocalInfo();
+        return ctx;
+    }
+
+    private static String describe(int clientFlags) {
+        MysqlCapability capability = new MysqlCapability(clientFlags);
+        return "deprecate_eof=" + capability.isDeprecatedEOF()
+                + " multi_statements=" + capability.isClientMultiStatements();
+    }
+
+    private static Command query(String sql) {
+        return command("COM_QUERY " + sql, 0x03, sql);
+    }
+
+    private static Command fieldList(String table) {
+        ByteArrayOutputStream payload = new ByteArrayOutputStream();
+        payload.write(0x04);
+        byte[] tableBytes = table.getBytes(StandardCharsets.UTF_8);
+        payload.write(tableBytes, 0, tableBytes.length);
+        payload.write(0);
+        return new Command("COM_FIELD_LIST " + table, payload.toByteArray());
+    }
+
+    private static Command stmtClose(int stmtId) {
+        ByteBuffer payload = 
ByteBuffer.allocate(5).order(ByteOrder.LITTLE_ENDIAN);
+        payload.put((byte) 0x19);
+        payload.putInt(stmtId);
+        return new Command("COM_STMT_CLOSE " + stmtId, payload.array());
+    }
+
+    private static Command setOption(int option) {
+        ByteBuffer payload = 
ByteBuffer.allocate(3).order(ByteOrder.LITTLE_ENDIAN);
+        payload.put((byte) 0x1B);
+        payload.putShort((short) option);
+        return new Command("COM_SET_OPTION " + option, payload.array());
+    }
+
+    private static Command command(String label, int commandCode, String 
argument) {
+        ByteArrayOutputStream payload = new ByteArrayOutputStream();
+        payload.write(commandCode);
+        byte[] argumentBytes = argument.getBytes(StandardCharsets.UTF_8);
+        payload.write(argumentBytes, 0, argumentBytes.length);
+        return new Command(label, payload.toByteArray());
+    }
+
+    /** A ConnectContext wired to a channel of our choosing; the field is 
protected, so subclass it. */
+    private static class GoldenConnectContext extends ConnectContext {
+        GoldenConnectContext(RecordingMysqlChannel channel) {
+            super();
+            this.mysqlChannel = channel;
+        }
+    }
+
+    private static class Command {
+        private final String label;
+        private final byte[] payload;
+
+        Command(String label, byte[] payload) {
+            this.label = label;
+            this.payload = payload;
+        }
+    }
+
+    private static class GoldenCase {
+        private final String name;
+        private final int clientFlags;
+        private final ProtocolGolden.Fidelity fidelity;
+        private final List<Command> commands = Lists.newArrayList();
+
+        GoldenCase(String name, int clientFlags) {
+            this(name, clientFlags, ProtocolGolden.Fidelity.BYTES);
+        }
+
+        GoldenCase(String name, int clientFlags, ProtocolGolden.Fidelity 
fidelity) {
+            this.name = name;
+            this.clientFlags = clientFlags;
+            this.fidelity = fidelity;
+        }
+
+        GoldenCase add(Command command) {
+            commands.add(command);
+            return this;
+        }
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/ProtocolGolden.java 
b/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/ProtocolGolden.java
new file mode 100644
index 00000000000..88a67be214a
--- /dev/null
+++ b/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/ProtocolGolden.java
@@ -0,0 +1,226 @@
+// 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.protocol;
+
+import org.apache.doris.qe.protocol.RecordingMysqlChannel.RecordedPacket;
+
+import org.junit.jupiter.api.Assertions;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+/**
+ * Renders recorded protocol traffic as text and compares it with a checked-in 
golden file.
+ *
+ * <p>Run with -Ddoris.protocol.golden.regenerate=true to rewrite the golden 
files from the current
+ * behavior. Every rewritten byte is a behavior change that has to be 
justified in review, which is
+ * the whole point of the file: the session and result layers are being 
refactored protocol by
+ * protocol, and these bytes are the contract that must not move while they 
are.
+ */
+public final class ProtocolGolden {
+    public static final String REGENERATE_PROPERTY = 
"doris.protocol.golden.regenerate";
+
+    private static final String GOLDEN_DIR = 
"src/test/resources/protocol-golden";
+    private static final int BYTES_PER_LINE = 16;
+    private static final int MESSAGE_PREFIX_LENGTH = 72;
+
+    private ProtocolGolden() {
+    }
+
+    /**
+     * Compare the rendered traffic with the golden file of that name, or 
rewrite it when
+     * regeneration is requested.
+     */
+    public static void verify(String goldenName, String actual) throws 
IOException {
+        Path golden = resolve(goldenName);
+        if (Boolean.getBoolean(REGENERATE_PROPERTY)) {
+            Files.createDirectories(golden.getParent());
+            Files.write(golden, actual.getBytes(StandardCharsets.UTF_8));
+            System.out.println("regenerated golden file " + 
golden.toAbsolutePath());
+            return;
+        }
+        Assertions.assertTrue(Files.exists(golden),
+                "golden file " + golden.toAbsolutePath() + " is missing, 
regenerate it with -D"
+                        + REGENERATE_PROPERTY + "=true");
+        String expected = new String(Files.readAllBytes(golden), 
StandardCharsets.UTF_8);
+        if (expected.equals(actual)) {
+            return;
+        }
+        Path dump = Paths.get("target", "protocol-golden", goldenName);
+        Files.createDirectories(dump.getParent());
+        Files.write(dump, actual.getBytes(StandardCharsets.UTF_8));
+        Assertions.fail("protocol golden " + goldenName + " changed. " + 
firstDifference(expected, actual)
+                + "\nEvery difference is a change in what a client receives, 
not a test artifact."
+                + " The current traffic was written to " + 
dump.toAbsolutePath()
+                + "; copy it over " + GOLDEN_DIR + "/" + goldenName
+                + " (or rerun with -D" + REGENERATE_PROPERTY + "=true) once 
every changed byte is"
+                + " explained and intended.");
+    }
+
+    private static Path resolve(String goldenName) {
+        Path fromModule = Paths.get(GOLDEN_DIR, goldenName);
+        if (Files.isDirectory(fromModule.getParent())) {
+            return fromModule;
+        }
+        // Surefire runs with the module directory as the working directory, 
but an IDE may use the
+        // repository root instead.
+        return Paths.get("fe/fe-core", GOLDEN_DIR, goldenName);
+    }
+
+    private static String firstDifference(String expected, String actual) {
+        String[] expectedLines = expected.split("\n", -1);
+        String[] actualLines = actual.split("\n", -1);
+        for (int i = 0; i < Math.max(expectedLines.length, 
actualLines.length); i++) {
+            String expectedLine = i < expectedLines.length ? expectedLines[i] 
: "<end of file>";
+            String actualLine = i < actualLines.length ? actualLines[i] : 
"<end of file>";
+            if (!expectedLine.equals(actualLine)) {
+                return "First difference at line " + (i + 1) + ":\n  golden: " 
+ expectedLine
+                        + "\n  actual: " + actualLine;
+            }
+        }
+        return "Files differ but no differing line was found.";
+    }
+
+    /**
+     * How much of a response to keep in the golden.
+     *
+     * <p>BYTES keeps every byte and is the default: that is the contract a 
client parses. SUMMARY
+     * keeps only the shape -- the sequence of packet kinds, plus the error 
code and the start of the
+     * message when the command failed -- and exists for the two responses 
whose payload legitimately
+     * moves between builds: a parser error carries the full keyword list of 
the grammar, and an
+     * EXPLAIN carries the current plan. Recording those byte for byte would 
break the golden on
+     * changes that have nothing to do with the protocol.
+     */
+    public enum Fidelity {
+        BYTES,
+        SUMMARY
+    }
+
+    /** Render one command's response packets the way they would hit the wire. 
*/
+    public static String renderPackets(List<RecordedPacket> packets, Fidelity 
fidelity) {
+        StringBuilder builder = new StringBuilder();
+        if (packets.isEmpty()) {
+            builder.append("  <no response packet>\n");
+            return builder.toString();
+        }
+        if (fidelity == Fidelity.SUMMARY) {
+            return renderSummary(packets);
+        }
+        for (RecordedPacket packet : packets) {
+            byte[] payload = packet.getPayload();
+            builder.append(String.format("  packet seq=%d len=%d kind=%s%s\n",
+                    packet.getSequenceId(), payload.length, classify(payload),
+                    packet.isFlushed() ? " flushed" : ""));
+            builder.append(hexDump(payload));
+        }
+        return builder.toString();
+    }
+
+    private static String renderSummary(List<RecordedPacket> packets) {
+        StringBuilder shape = new StringBuilder();
+        String previousKind = null;
+        boolean repeatedNoted = false;
+        for (RecordedPacket packet : packets) {
+            String kind = classify(packet.getPayload()) + (packet.isFlushed() 
? " flushed" : "");
+            if (kind.equals(previousKind)) {
+                if (!repeatedNoted) {
+                    // No count: an EXPLAIN emits one row packet per plan 
line, and that number is
+                    // not a protocol property.
+                    shape.append(" (repeated)");
+                    repeatedNoted = true;
+                }
+                continue;
+            }
+            if (previousKind != null) {
+                shape.append(", ");
+            }
+            shape.append(kind);
+            previousKind = kind;
+            repeatedNoted = false;
+        }
+        StringBuilder builder = new StringBuilder();
+        builder.append("  kinds: ").append(shape).append('\n');
+        for (RecordedPacket packet : packets) {
+            if ("ERR".equals(classify(packet.getPayload()))) {
+                builder.append("  
").append(describeError(packet.getPayload())).append('\n');
+            }
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Decode the fixed head of an ERR packet: error code, SQL state, and the 
start of the message.
+     * Only a prefix of the message is kept, for the reason SUMMARY exists at 
all.
+     */
+    private static String describeError(byte[] payload) {
+        if (payload.length < 9) {
+            return "err: malformed, " + payload.length + " bytes";
+        }
+        int errorCode = (payload[1] & 0xFF) | ((payload[2] & 0xFF) << 8);
+        String sqlState = new String(payload, 4, 5, StandardCharsets.UTF_8);
+        String message = new String(payload, 9, payload.length - 9, 
StandardCharsets.UTF_8);
+        message = message.replace("\n", "\\n").replace("\r", "\\r");
+        if (message.length() > MESSAGE_PREFIX_LENGTH) {
+            message = message.substring(0, MESSAGE_PREFIX_LENGTH) + "...";
+        }
+        return "err: errorCode=" + errorCode + " sqlState=" + sqlState + " 
message=\"" + message + "\"";
+    }
+
+    /**
+     * Name the packet by the same first-byte rule a client uses. A result-set 
header carries the
+     * column count, so anything that is not an OK, an ERR or an EOF is left 
as payload: the golden
+     * keeps the bytes either way, the name is only there to make a diff 
readable.
+     */
+    public static String classify(byte[] payload) {
+        if (payload.length == 0) {
+            return "EMPTY";
+        }
+        int first = payload[0] & 0xFF;
+        if (first == 0x00 && payload.length >= 7) {
+            return "OK";
+        }
+        if (first == 0xFF) {
+            return "ERR";
+        }
+        if (first == 0xFE && payload.length < 9) {
+            return "EOF";
+        }
+        return "PAYLOAD";
+    }
+
+    /** Classic hexdump: offset, bytes, printable gutter. */
+    public static String hexDump(byte[] bytes) {
+        StringBuilder builder = new StringBuilder();
+        for (int offset = 0; offset < bytes.length; offset += BYTES_PER_LINE) {
+            int end = Math.min(offset + BYTES_PER_LINE, bytes.length);
+            StringBuilder hex = new StringBuilder();
+            StringBuilder text = new StringBuilder();
+            for (int i = offset; i < end; i++) {
+                hex.append(String.format("%02x ", bytes[i]));
+                int value = bytes[i] & 0xFF;
+                text.append(value >= 0x20 && value < 0x7F ? (char) value : 
'.');
+            }
+            builder.append(String.format("    %04x  %-48s |%s|\n", offset, 
hex.toString().trim(), text));
+        }
+        return builder.toString();
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/RecordingMysqlChannel.java
 
b/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/RecordingMysqlChannel.java
new file mode 100644
index 00000000000..457d29b3298
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/qe/protocol/RecordingMysqlChannel.java
@@ -0,0 +1,141 @@
+// 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.protocol;
+
+import org.apache.doris.mysql.DummyMysqlChannel;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * A MysqlChannel that plays canned request packets to a ConnectProcessor and 
keeps every response
+ * packet the server wrote, so a test can compare the wire bytes against a 
golden file.
+ *
+ * <p>The real channel frames a packet in {@link 
org.apache.doris.mysql.MysqlChannel#sendOnePacket}:
+ * it writes a 4-byte header (3-byte payload length + 1-byte sequence id) into 
the send buffer, then
+ * the payload, then advances the sequence id. There is no send buffer here, 
so the header is not
+ * materialized; the sequence id each packet would have carried is recorded 
next to the payload
+ * instead, and the golden renders the header from the two.
+ */
+public class RecordingMysqlChannel extends DummyMysqlChannel {
+
+    /** One response packet: the payload the server wrote and the sequence id 
it was framed with. */
+    public static class RecordedPacket {
+        private final int sequenceId;
+        private final byte[] payload;
+        private boolean flushed;
+
+        RecordedPacket(int sequenceId, byte[] payload, boolean flushed) {
+            this.sequenceId = sequenceId;
+            this.payload = payload;
+            this.flushed = flushed;
+        }
+
+        public int getSequenceId() {
+            return sequenceId;
+        }
+
+        public byte[] getPayload() {
+            return payload;
+        }
+
+        /**
+         * True when the response was pushed to the client at this packet, 
either by sendAndFlush or
+         * by a later flush(). A response that never gets flushed leaves the 
client waiting, so where
+         * the flush lands is part of the contract.
+         */
+        public boolean isFlushed() {
+            return flushed;
+        }
+
+        void markFlushed() {
+            this.flushed = true;
+        }
+    }
+
+    private final Deque<ByteBuffer> inbound = new ArrayDeque<>();
+    private final List<RecordedPacket> outbound = new ArrayList<>();
+
+    /**
+     * Queue one request packet. The buffer holds the payload only (command 
byte first), the way
+     * fetchOnePacket() hands it to the processor after stripping the header.
+     */
+    public void queueRequest(byte[] payload) {
+        inbound.addLast(ByteBuffer.wrap(payload));
+    }
+
+    public List<RecordedPacket> getOutbound() {
+        return outbound;
+    }
+
+    public void clearOutbound() {
+        outbound.clear();
+    }
+
+    public boolean hasPendingRequest() {
+        return !inbound.isEmpty();
+    }
+
+    @Override
+    public ByteBuffer fetchOnePacket() {
+        ByteBuffer packet = inbound.pollFirst();
+        if (packet == null) {
+            // An empty buffer is how the real channel reports "peer sent 
nothing more"; processOnce()
+            // turns that into ctx.setKilled().
+            return ByteBuffer.allocate(0);
+        }
+        // The real channel advances the sequence id once per packet it reads, 
so the first response
+        // packet is framed with the request's id plus one.
+        accSequenceId();
+        return packet;
+    }
+
+    @Override
+    public void sendOnePacket(ByteBuffer packet) {
+        record(packet, false);
+    }
+
+    @Override
+    public void sendAndFlush(ByteBuffer packet) {
+        record(packet, true);
+    }
+
+    @Override
+    public void flush() {
+        if (!outbound.isEmpty()) {
+            outbound.get(outbound.size() - 1).markFlushed();
+        }
+    }
+
+    private void record(ByteBuffer packet, boolean flushed) {
+        byte[] payload = new byte[packet.remaining()];
+        packet.duplicate().get(payload);
+        outbound.add(new RecordedPacket(sequenceId, payload, flushed));
+        accSequenceId();
+    }
+
+    private void accSequenceId() {
+        sequenceId++;
+        if (sequenceId > 255) {
+            sequenceId = 0;
+        }
+    }
+}
diff --git a/fe/fe-core/src/test/resources/protocol-golden/flight-results.txt 
b/fe/fe-core/src/test/resources/protocol-golden/flight-results.txt
new file mode 100644
index 00000000000..28657ba9254
--- /dev/null
+++ b/fe/fe-core/src/test/resources/protocol-golden/flight-results.txt
@@ -0,0 +1,50 @@
+=== statement show variables like 'wait_timeout' ===
+state: EOF
+schema:
+  Variable_name: Utf8 nullable
+  Value: Utf8 nullable
+  Default_Value: Utf8 nullable
+  Changed: Utf8 nullable
+rows: 1
+  wait_timeout | 28800 | 28800 | 0
+
+=== statement show databases like 'protocol_golden_flight_db' ===
+state: EOF
+schema:
+  Database: Utf8 nullable
+rows: 1
+  protocol_golden_flight_db
+
+=== statement desc golden_tbl ===
+state: EOF
+schema:
+  Field: Utf8 nullable
+  Type: Utf8 nullable
+  Null: Utf8 nullable
+  Key: Utf8 nullable
+  Default: Utf8 nullable
+  Extra: Utf8 nullable
+rows: 2
+  k1 | int | Yes | true | NULL |
+  k2 | varchar(32) | Yes | false | NULL | NONE
+
+=== statement set sql_select_limit = 100 ===
+state: OK
+result: none, the producer synthesizes StatusResult=0
+
+=== statement use protocol_golden_flight_db ===
+state: OK
+result: none, the producer synthesizes StatusResult=0
+
+=== statement explain select 1 ===
+state: EOF
+schema:
+  Explain String(Nereids Planner): Utf8 nullable
+rows: some
+
+=== statement select from ===
+state: ERR errorCode=ERR_UNKNOWN_ERROR message="errCode = 2, detailMessage = 
\nmismatched input 'from' expecting {'(', '..."
+
+=== statement select * from no_such_table ===
+state: ERR errorCode=ERR_UNKNOWN_ERROR message="errCode = 2, detailMessage = 
Table [no_such_table] does not exist in database 
[protocol_golden_flight_db].(line 1, pos 14)"
+
diff --git a/fe/fe-core/src/test/resources/protocol-golden/mysql-packets.txt 
b/fe/fe-core/src/test/resources/protocol-golden/mysql-packets.txt
new file mode 100644
index 00000000000..46792ff5cdd
--- /dev/null
+++ b/fe/fe-core/src/test/resources/protocol-golden/mysql-packets.txt
@@ -0,0 +1,351 @@
+=== case select-literal ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY select 1
+  packet seq=1 len=1 kind=PAYLOAD
+    0000  01                                               |.|
+  packet seq=2 len=23 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 01 31 00 0c 21 00 04 00 00  |.def....1..!....|
+    0010  00 01 00 00 00 00 00                             |.......|
+  packet seq=3 len=2 kind=PAYLOAD
+    0000  01 31                                            |.1|
+  packet seq=4 len=8 kind=EOF flushed
+    0000  fe 00 00 00 00 00 00 00                          |........|
+
+=== case select-two-columns ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY select 1, 'a'
+  packet seq=1 len=1 kind=PAYLOAD
+    0000  02                                               |.|
+  packet seq=2 len=23 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 01 31 00 0c 21 00 04 00 00  |.def....1..!....|
+    0010  00 01 00 00 00 00 00                             |.......|
+  packet seq=3 len=23 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 01 61 00 0c 21 00 ff 00 00  |.def....a..!....|
+    0010  00 fe 00 00 00 00 00                             |.......|
+  packet seq=4 len=4 kind=PAYLOAD
+    0000  01 31 01 61                                      |.1.a|
+  packet seq=5 len=8 kind=EOF flushed
+    0000  fe 00 00 00 00 00 00 00                          |........|
+
+=== case select-session-variable ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY select @@wait_timeout
+  packet seq=1 len=1 kind=PAYLOAD
+    0000  01                                               |.|
+  packet seq=2 len=36 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 0e 40 40 77 61 69 74 5f 74  |.def....@@wait_t|
+    0010  69 6d 65 6f 75 74 00 0c 21 00 0b 00 00 00 03 00  |imeout..!.......|
+    0020  00 00 00 00                                      |....|
+  packet seq=3 len=6 kind=PAYLOAD
+    0000  05 32 38 38 30 30                                |.28800|
+  packet seq=4 len=8 kind=EOF flushed
+    0000  fe 00 00 00 00 00 00 00                          |........|
+
+=== case select-null-literal ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY select null
+  packet seq=1 len=1 kind=PAYLOAD
+    0000  01                                               |.|
+  packet seq=2 len=26 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 04 6e 75 6c 6c 00 0c 21 00  |.def....null..!.|
+    0010  ff 00 00 00 fe 00 00 00 00 00                    |..........|
+  packet seq=3 len=1 kind=PAYLOAD
+    0000  fb                                               |.|
+  packet seq=4 len=8 kind=EOF flushed
+    0000  fe 00 00 00 00 00 00 00                          |........|
+
+=== case select-empty-result ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY select k1 from golden_tbl where 1 = 0
+  packet seq=1 len=1 kind=PAYLOAD
+    0000  01                                               |.|
+  packet seq=2 len=64 kind=PAYLOAD
+    0000  03 64 65 66 12 70 72 6f 74 6f 63 6f 6c 5f 67 6f  |.def.protocol_go|
+    0010  6c 64 65 6e 5f 64 62 0a 67 6f 6c 64 65 6e 5f 74  |lden_db.golden_t|
+    0020  62 6c 0a 67 6f 6c 64 65 6e 5f 74 62 6c 02 6b 31  |bl.golden_tbl.k1|
+    0030  02 6b 31 0c 21 00 0b 00 00 00 03 00 00 00 00 00  |.k1.!...........|
+  packet seq=3 len=8 kind=EOF flushed
+    0000  fe 00 00 00 00 00 00 00                          |........|
+
+=== case select-literal-legacy-eof ===
+client capability: deprecate_eof=false multi_statements=false
+--> COM_QUERY select 1
+  packet seq=1 len=1 kind=PAYLOAD
+    0000  01                                               |.|
+  packet seq=2 len=23 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 01 31 00 0c 21 00 04 00 00  |.def....1..!....|
+    0010  00 01 00 00 00 00 00                             |.......|
+  packet seq=3 len=5 kind=EOF
+    0000  fe 00 00 00 00                                   |.....|
+  packet seq=4 len=2 kind=PAYLOAD
+    0000  01 31                                            |.1|
+  packet seq=5 len=5 kind=EOF flushed
+    0000  fe 00 00 00 00                                   |.....|
+
+=== case show-variables ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY show variables like 'wait_timeout'
+  packet seq=1 len=1 kind=PAYLOAD
+    0000  04                                               |.|
+  packet seq=2 len=48 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 0d 56 61 72 69 61 62 6c 65  |.def....Variable|
+    0010  5f 6e 61 6d 65 0d 56 61 72 69 61 62 6c 65 5f 6e  |_name.Variable_n|
+    0020  61 6d 65 0c 21 00 ff 00 00 00 fe 00 00 00 00 00  |ame.!...........|
+  packet seq=3 len=32 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 05 56 61 6c 75 65 05 56 61  |.def....Value.Va|
+    0010  6c 75 65 0c 21 00 ff 00 00 00 fe 00 00 00 00 00  |lue.!...........|
+  packet seq=4 len=48 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 0d 44 65 66 61 75 6c 74 5f  |.def....Default_|
+    0010  56 61 6c 75 65 0d 44 65 66 61 75 6c 74 5f 56 61  |Value.Default_Va|
+    0020  6c 75 65 0c 21 00 ff 00 00 00 fe 00 00 00 00 00  |lue.!...........|
+  packet seq=5 len=36 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 07 43 68 61 6e 67 65 64 07  |.def....Changed.|
+    0010  43 68 61 6e 67 65 64 0c 21 00 ff 00 00 00 fe 00  |Changed.!.......|
+    0020  00 00 00 00                                      |....|
+  packet seq=6 len=27 kind=PAYLOAD
+    0000  0c 77 61 69 74 5f 74 69 6d 65 6f 75 74 05 32 38  |.wait_timeout.28|
+    0010  38 30 30 05 32 38 38 30 30 01 30                 |800.28800.0|
+  packet seq=7 len=8 kind=EOF flushed
+    0000  fe 00 00 00 00 00 00 00                          |........|
+
+=== case show-variables-legacy-eof ===
+client capability: deprecate_eof=false multi_statements=false
+--> COM_QUERY show variables like 'wait_timeout'
+  packet seq=1 len=1 kind=PAYLOAD
+    0000  04                                               |.|
+  packet seq=2 len=48 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 0d 56 61 72 69 61 62 6c 65  |.def....Variable|
+    0010  5f 6e 61 6d 65 0d 56 61 72 69 61 62 6c 65 5f 6e  |_name.Variable_n|
+    0020  61 6d 65 0c 21 00 ff 00 00 00 fe 00 00 00 00 00  |ame.!...........|
+  packet seq=3 len=32 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 05 56 61 6c 75 65 05 56 61  |.def....Value.Va|
+    0010  6c 75 65 0c 21 00 ff 00 00 00 fe 00 00 00 00 00  |lue.!...........|
+  packet seq=4 len=48 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 0d 44 65 66 61 75 6c 74 5f  |.def....Default_|
+    0010  56 61 6c 75 65 0d 44 65 66 61 75 6c 74 5f 56 61  |Value.Default_Va|
+    0020  6c 75 65 0c 21 00 ff 00 00 00 fe 00 00 00 00 00  |lue.!...........|
+  packet seq=5 len=36 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 07 43 68 61 6e 67 65 64 07  |.def....Changed.|
+    0010  43 68 61 6e 67 65 64 0c 21 00 ff 00 00 00 fe 00  |Changed.!.......|
+    0020  00 00 00 00                                      |....|
+  packet seq=6 len=5 kind=EOF
+    0000  fe 00 00 00 00                                   |.....|
+  packet seq=7 len=27 kind=PAYLOAD
+    0000  0c 77 61 69 74 5f 74 69 6d 65 6f 75 74 05 32 38  |.wait_timeout.28|
+    0010  38 30 30 05 32 38 38 30 30 01 30                 |800.28800.0|
+  packet seq=8 len=5 kind=EOF flushed
+    0000  fe 00 00 00 00                                   |.....|
+
+=== case describe-table ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY desc golden_tbl
+  packet seq=1 len=1 kind=PAYLOAD
+    0000  06                                               |.|
+  packet seq=2 len=32 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 05 46 69 65 6c 64 05 46 69  |.def....Field.Fi|
+    0010  65 6c 64 0c 21 00 ff 00 00 00 fe 00 00 00 00 00  |eld.!...........|
+  packet seq=3 len=30 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 04 54 79 70 65 04 54 79 70  |.def....Type.Typ|
+    0010  65 0c 21 00 ff 00 00 00 fe 00 00 00 00 00        |e.!...........|
+  packet seq=4 len=30 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 04 4e 75 6c 6c 04 4e 75 6c  |.def....Null.Nul|
+    0010  6c 0c 21 00 ff 00 00 00 fe 00 00 00 00 00        |l.!...........|
+  packet seq=5 len=28 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 03 4b 65 79 03 4b 65 79 0c  |.def....Key.Key.|
+    0010  21 00 ff 00 00 00 fe 00 00 00 00 00              |!...........|
+  packet seq=6 len=36 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 07 44 65 66 61 75 6c 74 07  |.def....Default.|
+    0010  44 65 66 61 75 6c 74 0c 21 00 ff 00 00 00 fe 00  |Default.!.......|
+    0020  00 00 00 00                                      |....|
+  packet seq=7 len=32 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 05 45 78 74 72 61 05 45 78  |.def....Extra.Ex|
+    0010  74 72 61 0c 21 00 ff 00 00 00 fe 00 00 00 00 00  |tra.!...........|
+  packet seq=8 len=18 kind=PAYLOAD
+    0000  02 6b 31 03 69 6e 74 03 59 65 73 04 74 72 75 65  |.k1.int.Yes.true|
+    0010  fb 00                                            |..|
+  packet seq=9 len=31 kind=PAYLOAD
+    0000  02 6b 32 0b 76 61 72 63 68 61 72 28 33 32 29 03  |.k2.varchar(32).|
+    0010  59 65 73 05 66 61 6c 73 65 fb 04 4e 4f 4e 45     |Yes.false..NONE|
+  packet seq=10 len=8 kind=EOF flushed
+    0000  fe 00 00 00 00 00 00 00                          |........|
+
+=== case set-session-variable ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY set sql_select_limit = 100
+  packet seq=1 len=8 kind=OK flushed
+    0000  00 00 00 00 00 00 00 00                          |........|
+
+=== case use-database ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY use protocol_golden_db
+  packet seq=1 len=8 kind=OK flushed
+    0000  00 00 00 00 00 00 00 00                          |........|
+
+=== case explain-select ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY explain select 1
+  kinds: PAYLOAD (repeated), EOF flushed
+
+=== case syntax-error ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY select from
+  kinds: ERR flushed
+  err: errorCode=1105 sqlState=HY000 message="errCode = 2, detailMessage = 
\nmismatched input 'from' expecting {'(', '..."
+
+=== case unknown-table ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY select * from no_such_table
+  packet seq=1 len=124 kind=ERR flushed
+    0000  ff 51 04 23 48 59 30 30 30 65 72 72 43 6f 64 65  |.Q.#HY000errCode|
+    0010  20 3d 20 32 2c 20 64 65 74 61 69 6c 4d 65 73 73  | = 2, detailMess|
+    0020  61 67 65 20 3d 20 54 61 62 6c 65 20 5b 6e 6f 5f  |age = Table [no_|
+    0030  73 75 63 68 5f 74 61 62 6c 65 5d 20 64 6f 65 73  |such_table] does|
+    0040  20 6e 6f 74 20 65 78 69 73 74 20 69 6e 20 64 61  | not exist in da|
+    0050  74 61 62 61 73 65 20 5b 70 72 6f 74 6f 63 6f 6c  |tabase [protocol|
+    0060  5f 67 6f 6c 64 65 6e 5f 64 62 5d 2e 28 6c 69 6e  |_golden_db].(lin|
+    0070  65 20 31 2c 20 70 6f 73 20 31 34 29              |e 1, pos 14)|
+
+=== case error-then-next-command ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY select * from no_such_table
+  packet seq=1 len=124 kind=ERR flushed
+    0000  ff 51 04 23 48 59 30 30 30 65 72 72 43 6f 64 65  |.Q.#HY000errCode|
+    0010  20 3d 20 32 2c 20 64 65 74 61 69 6c 4d 65 73 73  | = 2, detailMess|
+    0020  61 67 65 20 3d 20 54 61 62 6c 65 20 5b 6e 6f 5f  |age = Table [no_|
+    0030  73 75 63 68 5f 74 61 62 6c 65 5d 20 64 6f 65 73  |such_table] does|
+    0040  20 6e 6f 74 20 65 78 69 73 74 20 69 6e 20 64 61  | not exist in da|
+    0050  74 61 62 61 73 65 20 5b 70 72 6f 74 6f 63 6f 6c  |tabase [protocol|
+    0060  5f 67 6f 6c 64 65 6e 5f 64 62 5d 2e 28 6c 69 6e  |_golden_db].(lin|
+    0070  65 20 31 2c 20 70 6f 73 20 31 34 29              |e 1, pos 14)|
+--> COM_QUERY select 1
+  packet seq=1 len=1 kind=PAYLOAD
+    0000  01                                               |.|
+  packet seq=2 len=23 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 01 31 00 0c 21 00 04 00 00  |.def....1..!....|
+    0010  00 01 00 00 00 00 00                             |.......|
+  packet seq=3 len=2 kind=PAYLOAD
+    0000  01 31                                            |.1|
+  packet seq=4 len=8 kind=EOF flushed
+    0000  fe 00 00 00 00 00 00 00                          |........|
+
+=== case multi-statement-with-capability ===
+client capability: deprecate_eof=true multi_statements=true
+--> COM_QUERY select 1; select 2
+  packet seq=1 len=1 kind=PAYLOAD
+    0000  01                                               |.|
+  packet seq=2 len=23 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 01 31 00 0c 21 00 04 00 00  |.def....1..!....|
+    0010  00 01 00 00 00 00 00                             |.......|
+  packet seq=3 len=2 kind=PAYLOAD
+    0000  01 31                                            |.1|
+  packet seq=4 len=8 kind=EOF flushed
+    0000  fe 00 00 08 00 00 00 00                          |........|
+  packet seq=5 len=1 kind=PAYLOAD
+    0000  01                                               |.|
+  packet seq=6 len=23 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 01 32 00 0c 21 00 04 00 00  |.def....2..!....|
+    0010  00 01 00 00 00 00 00                             |.......|
+  packet seq=7 len=2 kind=PAYLOAD
+    0000  01 32                                            |.2|
+  packet seq=8 len=8 kind=EOF flushed
+    0000  fe 00 00 00 00 00 00 00                          |........|
+
+=== case multi-statement-without-capability ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUERY select 1; select 2
+  packet seq=1 len=1 kind=PAYLOAD
+    0000  01                                               |.|
+  packet seq=2 len=23 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 01 31 00 0c 21 00 04 00 00  |.def....1..!....|
+    0010  00 01 00 00 00 00 00                             |.......|
+  packet seq=3 len=2 kind=PAYLOAD
+    0000  01 31                                            |.1|
+  packet seq=4 len=1 kind=PAYLOAD
+    0000  01                                               |.|
+  packet seq=5 len=23 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 01 32 00 0c 21 00 04 00 00  |.def....2..!....|
+    0010  00 01 00 00 00 00 00                             |.......|
+  packet seq=6 len=2 kind=PAYLOAD
+    0000  01 32                                            |.2|
+  packet seq=7 len=8 kind=EOF flushed
+    0000  fe 00 00 00 00 00 00 00                          |........|
+
+=== case com-field-list ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_FIELD_LIST golden_tbl
+  packet seq=1 len=65 kind=PAYLOAD
+    0000  03 64 65 66 12 70 72 6f 74 6f 63 6f 6c 5f 67 6f  |.def.protocol_go|
+    0010  6c 64 65 6e 5f 64 62 0a 67 6f 6c 64 65 6e 5f 74  |lden_db.golden_t|
+    0020  62 6c 0a 67 6f 6c 64 65 6e 5f 74 62 6c 02 6b 31  |bl.golden_tbl.k1|
+    0030  02 6b 31 0c 21 00 0b 00 00 00 03 00 00 00 00 00  |.k1.!...........|
+    0040  00                                               |.|
+  packet seq=2 len=65 kind=PAYLOAD
+    0000  03 64 65 66 12 70 72 6f 74 6f 63 6f 6c 5f 67 6f  |.def.protocol_go|
+    0010  6c 64 65 6e 5f 64 62 0a 67 6f 6c 64 65 6e 5f 74  |lden_db.golden_t|
+    0020  62 6c 0a 67 6f 6c 64 65 6e 5f 74 62 6c 02 6b 32  |bl.golden_tbl.k2|
+    0030  02 6b 32 0c 21 00 ff 00 00 00 fe 00 00 00 00 00  |.k2.!...........|
+    0040  00                                               |.|
+  packet seq=3 len=8 kind=EOF flushed
+    0000  fe 00 00 00 00 00 00 00                          |........|
+
+=== case com-stmt-prepare ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_STMT_PREPARE select ?
+  packet seq=1 len=13 kind=OK
+    0000  00 00 00 00 80 01 00 01 00 00 00 00 01           |.............|
+  packet seq=2 len=26 kind=PAYLOAD
+    0000  03 64 65 66 00 00 00 02 24 30 02 24 30 0c 21 00  |.def....$0.$0.!.|
+    0010  ff 00 00 00 fc 00 00 00 00 00                    |..........|
+  packet seq=3 len=44 kind=PAYLOAD flushed
+    0000  03 64 65 66 00 00 00 0b 5f 5f 6c 69 74 65 72 61  |.def....__litera|
+    0010  6c 5f 30 0b 5f 5f 6c 69 74 65 72 61 6c 5f 30 0c  |l_0.__literal_0.|
+    0020  21 00 ff 00 00 00 fc 00 00 00 00 00              |!...........|
+
+=== case com-stmt-close ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_STMT_CLOSE 1
+  <no response packet>
+
+=== case com-set-option ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_SET_OPTION 0
+  packet seq=1 len=8 kind=OK flushed
+    0000  00 00 00 00 00 00 00 00                          |........|
+
+=== case com-reset-connection ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_RESET_CONNECTION
+  packet seq=1 len=8 kind=OK flushed
+    0000  00 00 00 00 00 00 00 00                          |........|
+
+=== case com-ping ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_PING
+  packet seq=1 len=8 kind=OK flushed
+    0000  00 00 00 00 00 00 00 00                          |........|
+
+=== case com-init-db ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_INIT_DB protocol_golden_db
+  packet seq=1 len=8 kind=OK flushed
+    0000  00 00 00 00 00 00 00 00                          |........|
+
+=== case com-statistics ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_STATISTICS
+  packet seq=1 len=8 kind=OK flushed
+    0000  00 00 00 00 00 00 00 00                          |........|
+
+=== case com-unknown ===
+client capability: deprecate_eof=true multi_statements=false
+--> unknown command 0x2A
+  packet seq=1 len=28 kind=ERR flushed
+    0000  ff 17 04 23 30 38 53 30 31 55 6e 6b 6e 6f 77 6e  |...#08S01Unknown|
+    0010  20 63 6f 6d 6d 61 6e 64 28 34 32 29              | command(42)|
+
+=== case com-quit ===
+client capability: deprecate_eof=true multi_statements=false
+--> COM_QUIT
+  packet seq=1 len=8 kind=OK flushed
+    0000  00 00 00 00 00 00 00 00                          |........|
+


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

Reply via email to