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]