Repository navigation
Description
Search before asking
- I had searched in the issues and found no similar issues.
Version
Apache Doris 4.1.3-rc02, commit 31263df4dc1d4d3a27517d264802cd4d6b92c874
Client: Python + ADBC Flight SQL driver (adbc_driver_flightsql), FE arrow_flight_sql_port = 41070.
The MySQL/JDBC protocol is used as the control path for comparison.
What's Wrong?
Executing a PL/SQL CALL that contains an INSERT once over Python ADBC Flight SQL returns an error to the client:
INTERNAL: getMysqlChannel not in mysql connection
but the procedure's INSERT side effect is actually executed four times. A single call must execute its side effect at most once.
Two independent ADBC calls (keys 444 and 666) each produced exactly four rows. The JDBC control call (key 555) succeeded and produced exactly one row.
What You Expected?
A single CALL should succeed and insert exactly one row. If CALL is not supported over the Flight SQL protocol, the server must reject it before executing anything, and must never repeat DML side effects after returning an error.
How to Reproduce?
- Create the table and the stored procedure below.
- Execute
CALL plsql_variable_insert(444,'adbc444')once over Python ADBC. - Observe the
INTERNALerror returned by ADBC. - Over MySQL/JDBC, count the rows for
id=444;COUNT(*)is 4. - Run the same
CALLfor key555over JDBC;COUNT(*)is 1.
DROP TABLE IF EXISTS plsql_variable;
CREATE TABLE plsql_variable (
id INT,
name VARCHAR(20)
) DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 4
PROPERTIES ("replication_num"="1");
CREATE OR REPLACE PROCEDURE plsql_variable_insert(IN id INT, IN name STRING)
BEGIN
INSERT INTO plsql_variable VALUES(id,name);
END;
-- Execute this ONCE through Python ADBC Flight SQL.
CALL plsql_variable_insert(444,'adbc444');
-- Inspect through MySQL/JDBC after the ADBC error.
SELECT id, name, COUNT(*)
FROM plsql_variable WHERE id=444 GROUP BY id, name;Client side:
import adbc_driver_flightsql.dbapi as flight_sql
conn = flight_sql.connect(uri="grpc://127.0.0.1:41070",
db_kwargs={"username": "root", "password": ""})
cur = conn.cursor()
cur.execute("CALL plsql_variable_insert(444,'adbc444')")Anything Else?
The error message (getMysqlChannel not in mysql connection) shows that the PL/SQL executor assumes a MySQL channel is available on the session. On a Flight SQL connection that assumption does not hold, and the failure apparently happens after the statement has already been executed, and is then retried, so the DML lands multiple times.
Data correctness impact: this can silently duplicate writes.
Workaround: run PL/SQL CALL over the MySQL/JDBC protocol. Clients must not auto-retry on this error, since the server may already have applied the side effect.
Tracking issue: #65615
Are you willing to submit PR?
- Yes I am willing to submit a PR!
Code of Conduct
- I agree to follow this project's Code of Conduct
Activity
Doris-Breakwater commentedon Sep 1, 2026
Breakwater-GitHub-Analysis-Slot: slot_d813a37c5b64
Preliminary maintainer analysis
Judgment: This is a valid, high-severity FE / Arrow Flight SQL correctness defect. The exact 4x multiplication is not yet proven from the supplied evidence, but the reported
INTERNALerror-after-side-effect is directly explained by the 4.1.3-rc02 source. Until fixed, PL/SQL stored-procedureCALLshould be treated as unsafe over Flight SQL. The issue currently has no labels; please add the repository's bug and Arrow Flight SQL / FE area labels.Verified from commit
31263df4dc1d4d3a27517d264802cd4d6b92c874GetFlightInfo:DorisFlightSqlProducer.executeQueryStatement()callsFlightSqlConnectProcessor.handleQuery()and turns an error inConnectContextinto anINTERNALresponse (source).CallProcedure.run(), which invokesPlSqlOperation.execute()(source). There is no Arrow Flight protocol gate inCallCommandbefore that invocation.PlSqlOperation.execute()evaluates the procedure viaexec.parseAndEval(args)first, then unconditionally executesctx.getMysqlChannel().reset()infinally(source). On a Flight session,FlightSqlConnectContext.getMysqlChannel()always throws exactlygetMysqlChannel not in mysql connection(source). The exception is caught and converted to an error state only after the procedure body may already have committed its DML. This confirms the partial-success ambiguity and the observed client error.reset():PlsqlResultalso sends rows, metadata, and EOF throughMysqlChannel(source). Removing or guarding only the final reset would not establish Flight SQL support for PL/SQL results.max_query_retry_timeto 3, which makes "initial attempt + 3 retries = 4" a plausible clue (source). It is not sufficient proof of the four executions: this particular MySQL-channel exception is caught insidePlSqlOperationand returned as query state, whileStmtExecutor.queryRetry()retries only specific thrown failures. Static inspection therefore does not establish whether the repeated execution is caused by an outer Flight/ADBCGetFlightInforeplay, an independent retry while running the innerINSERT, or another layer.Information needed to prove the 4x path
adbc_driver_flightsql,adbc_driver_manager, andpyarrow(for example, the output ofpython -m pip freeze | grep -E 'adbc|pyarrow'). The DB-API path prepares statements automatically in current ADBC implementations, so the driver version matters.127.0.0.1:41070is the master FE, and the effectivemax_query_retry_timevalue.fe.logfor one fresh unique key, including the outerCALL, innerINSERT, query IDs, timestamps, and the full server stack trace. On an isolated test cluster, enablingenable_print_request_before_executionwould make attempt counting clearer.CreatePreparedStatement,GetFlightInfoPreparedStatement, and/orGetFlightInfoStatementrequests for the singlecur.execute(). Please also compare the DB-API prepared path with a low-level ADBC statement executed without callingprepare().max_query_retry_time = 0. A change from four rows to one would implicate a Doris retry path; no change would shift attention to Flight/ADBC replay. Do not use this as a production mitigation because one write can still commit before the error.The audit pattern will localize the replay boundary:
CALLaudit entries plus four innerINSERTentries: the Flight request is being replayed/resubmitted.CALLentry plus four innerINSERTentries: retry occurs inside procedure/DML execution.Recommended next steps
CALLonConnectType.ARROW_FLIGHT_SQLbeforePlSqlOperation.execute()is entered. This is the only currently evidenced way to guarantee no side effect before an unsupported-protocol error. Keep the documented MySQL/JDBC workaround and the warning not to retry ambiguous Flight errors.getMysqlChannel().reset(). Define how repeatedGetFlightInforequests are deduplicated or otherwise prevented from re-running side-effecting statements.morningman commentedon Sep 4, 2026
Scope: this only affects
branch-4.0,branch-4.1andbranch-3.1. Not reproducible on master.PL/SQL was removed from master by #58700 (
ec4014ec268, 2025-12-08), which deleted the wholeorg.apache.doris.plsqlpackage, thePLLexer.g4/PLParser.g4grammars,CallProcedure, and theCREATE|DROP|SHOW CREATE PROCEDUREgrammar rules.branch-4.0,branch-4.1andbranch-3.1stillcarry that code, so the bug lives only there.
Verified on a master build (both protocols, MySQL on 9033 and Arrow Flight SQL on 8077):
CREATE [OR REPLACE] PROCEDURE ...errCode = 2, no viable alternative at input 'CREATE OR REPLACE PROCEDURE'(line 1, pos 18)get flight info statement failedDROP PROCEDURE ...no viable alternative at input 'DROP PROCEDURE'SHOW CREATE PROCEDURE ...no viable alternative at input 'SHOW CREATE PROCEDURE'CALL p(444,'x')do not support call function PSHOW PROCEDURE STATUS/SHOW FUNCTION STATUSSELECT * FROM information_schema.routinesThe last two rows are the deliberate compatibility shims restored by #66659 — the SQL entry points parse
and return empty, the stored-procedure functionality itself stays removed. On master
CALLonly acceptsthe built-ins
EXECUTE_STMTandFLUSH_AUDIT_LOG; anything else is rejected inCallFunc.getFunc()before anything is executed, so no DML can be duplicated.Root cause on 4.x
PlSqlOperation.execute()ends with an unconditionalin its
finallyblock, andFlightSqlConnectContext.getMysqlChannel()throwsgetMysqlChannel not in mysql connection. TheINSERThas already run by then, the throw is swallowedinto an ERR state,
executeQueryStatementturns that intoafter executeQueryStatement handleQuery,and the client sees
INTERNAL— which matches the report exactly: the error is raised after the sideeffect.
The deeper reason is that PL/SQL assumes a MySQL channel throughout:
PlsqlQueryExecutor.executeQuery()runs every inner statement throughConnectContext.get().cloneContext(), andcloneContext()builds a plainConnectContext(
connectType = MYSQL) while copyingmysqlChannel— which isnullon a Flight SQL connection.About "four times"
I could not find a server-side loop that would execute the body four times:
getFlightInfoStatementexecutes the statement once perGetFlightInfo;StmtExecutor.queryRetry()only retries the three
SystemInfoService.NEED_REPLAN_ERRORSmessages;handleQueryWithRetry()additionally requires
RpcExceptionandConnectType.MYSQL; and theretryTimesloop inInsertIntoTableCommand.initPlan()only re-plans before the transaction begins. If anyone reproducesthis on 4.x,
fe.audit.logsettles it: fourCALLentries means the client retriedGetFlightInfo,while one
CALLplus fourINSERTs would mean a server-side loop.Next step
A guard for
branch-4.1is on the way: rejectCALL <procedure>on a non-MySQL connection inCallProcedure.create(), i.e. beforeexec.parseAndEval()runs, so the statement fails cleanly withno side effect.
branch-4.0/branch-3.1can take the same cherry-pick./cc — please set the affected-version labels (4.0 / 4.1 / 3.1) and drop any
masterlabel.morningman commentedon Sep 4, 2026
Correction to the "next step" in my previous comment: there will be no fix on the 4.x branches. #67521 has been closed — PL/SQL is not being developed further on the release branches, and it is already removed from master by #58700.
The workaround stands for
branch-4.0/branch-4.1/branch-3.1: run PL/SQLCALLover the MySQL protocol, and do not auto-retry ongetMysqlChannel not in mysql connection, because the server may already have applied the side effect.morningman commentedon Sep 4, 2026
Closing as not planned. PL/SQL was removed from master by #58700, so this is not reproducible there, and the release branches (
branch-4.0/branch-4.1/branch-3.1) will not get a fix — see the comments above for the root cause and the workaround.