This is an automated email from the ASF dual-hosted git repository.
morningman 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 099d5bd71df [fix](arrow-flight) Do not take the point-query short
circuit on an Arrow Flight connection (#67487)
099d5bd71df is described below
commit 099d5bd71dffb4f515c417e75227c15c4fcea922
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Fri Sep 4 09:48:13 2026 +0800
[fix](arrow-flight) Do not take the point-query short circuit on an Arrow
Flight connection (#67487)
### What problem does this PR solve?
Issue Number: close #67368
Related PR: #67381 (sibling fix in the same tracking series), #62259
Problem Summary:
A `UNIQUE KEY` point query that qualifies for the short-circuit path
returned no Flight endpoint over Arrow Flight SQL. The client failed
with
```
fetch arrow flight schema failed, no FlightSqlEndpointsLocations
```
and the row was silently dropped. The identical query with
`SET_VAR(enable_short_circuit_query=false)` returned it on the same
connection.
**Root cause** — the short circuit produces no Arrow result at either
end, and nothing prevented an Arrow Flight connection from planning one:
* It executes on `PointQueryExecutor`, not a `Coordinator`, and
`Coordinator`/`NereidsCoordinator` are the only places that register a
`FlightSqlEndpointsLocation`. `StmtExecutor.executeAndSendResult` then
returns early through its Arrow Flight branch with nothing registered
and without ever calling `getNext()`, so `GetFlightInfo` found an empty
endpoint list.
* The BE cannot be pointed at either. `tablet_fetch_data` serializes
with `VMysqlResultWriter` into `PTabletKeyLookupResponse.row_batch` and
runs no fragment, so the `ArrowFlightResultBlockBuffer` that
`fetch_arrow_flight_schema` looks up by finst id never exists.
`LogicalResultSinkToShortCircuitPointQuery` did not look at the connect
type, and `enable_short_circuit_query` defaults to `true`, so every ADBC
/ Arrow Flight JDBC point query on a MoW + light-schema-change +
`store_row_column` table hit this. Prepared statements go through the
same `executeQueryStatement` and failed identically.
**Fix** — keep Arrow Flight SQL on the normal execution path.
This has to be decided at plan time rather than when picking the
executor: `OlapScanNode.computeTabletInfo` and several rewrite/property
rules (`ChildOutputPropertyDeriver`, `ShuffleKeyPruner`,
`NestedColumnPruning`, `PruneOlapScanPartition`) read
`StatementContext.isShortCircuitQuery()` while the plan is being built,
so flipping the flag later would run a coordinator over a plan shaped
for a different execution mode. MySQL connections keep the short circuit
unchanged.
Returning the point-query result from the FE instead was considered and
rejected for now: `FlightSqlChannel.addResult` builds varchar vectors
only, so every column would come back as `Utf8`, inconsistent with the
normal Flight path. Full support (Arrow serialization in the BE lookup
RPC plus a result buffer to hand out an endpoint) is a larger change and
out of scope here.
Also refreshes a now-stale comment in `StmtExecutor` that said point
queries reach the Arrow Flight deferral gate.
---
.../LogicalResultSinkToShortCircuitPointQuery.java | 16 ++-
.../java/org/apache/doris/qe/StmtExecutor.java | 6 +-
.../rules/rewrite/ShortCircuitPointQueryTest.java | 32 +++++
.../test_point_query_over_arrow_flight.groovy | 131 +++++++++++++++++++++
4 files changed, 182 insertions(+), 3 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
index f839b81b601..51bdc44b66b 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
@@ -31,6 +31,7 @@ import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.ConnectContext.ConnectType;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
@@ -64,7 +65,20 @@ public class LogicalResultSinkToShortCircuitPointQuery
implements RewriteRuleFac
@VisibleForTesting
boolean scanMatchShortCircuitCondition(LogicalOlapScan olapScan) {
- if
(!ConnectContext.get().getSessionVariable().isEnableShortCircuitQuery()) {
+ ConnectContext connectContext = ConnectContext.get();
+ if (!connectContext.getSessionVariable().isEnableShortCircuitQuery()) {
+ return false;
+ }
+ // The short circuit produces no Arrow result at either end.
PointQueryExecutor is not a
+ // Coordinator, and Coordinator/NereidsCoordinator are the only places
that register a
+ // FlightSqlEndpointsLocation, so GetFlightInfo found none and failed
the query with
+ // "no FlightSqlEndpointsLocations"; the BE side cannot be pointed at
either, since the lookup rpc
+ // serializes with VMysqlResultWriter into
PTabletKeyLookupResponse.row_batch and never creates the
+ // ArrowFlightResultBlockBuffer that fetch_arrow_flight_schema looks
up. Keep Arrow Flight SQL on
+ // the normal execution path. This has to be decided here at plan time
rather than when picking the
+ // executor: OlapScanNode.computeTabletInfo and several rewrite and
property rules read
+ // StatementContext.isShortCircuitQuery() while building the plan. See
#67368.
+ if (connectContext.getConnectType() == ConnectType.ARROW_FLIGHT_SQL) {
return false;
}
// Lazy point-query pruning does not preserve explicit
PARTITION/TABLET restrictions.
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
index a9a38a884d5..8a733fb47ab 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
@@ -1550,8 +1550,10 @@ public class StmtExecutor {
// need deferral (the BE buffers their result independently)
but are captured by the
// same gate; the trade-off is their coordinator, query queue
slot and query
// registration stay held until the next query / teardown
instead of being released
- // at the end of GetFlightInfo. Point queries use a different
coordBase (not
- // deferred). See #62259.
+ // at the end of GetFlightInfo. A short-circuit point query is
the one case with a
+ // different coordBase, and it can no longer reach here: it
has no Arrow result on
+ // either side, so LogicalResultSinkToShortCircuitPointQuery
keeps Arrow Flight SQL
+ // on the normal execution path. See #62259 and #67368.
if (coordBase == coord) {
deferredForArrowFlight = true;
context.addFlightSqlDeferredExecutor(this);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
index 1b25c4728d7..a94f6ae46f2 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
@@ -29,11 +29,15 @@ import
org.apache.doris.nereids.trees.plans.logical.LogicalEmptyRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
import org.apache.doris.nereids.util.MemoPatternMatchSupported;
import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.ConnectContext.ConnectType;
import org.apache.doris.utframe.TestWithFeService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.lang.reflect.Field;
+
/**
* Regression test:
* For short-circuit point query, we should not rewrite LogicalOlapScan to
LogicalEmptyRelation
@@ -154,6 +158,34 @@ class ShortCircuitPointQueryTest extends TestWithFeService
.scanMatchShortCircuitCondition(scan));
}
+ @Test
+ void testArrowFlightSqlConnectionDoesNotUseShortCircuit() throws Exception
{
+ // The short circuit hands its rows back through PointQueryExecutor,
which registers no
+ // FlightSqlEndpointsLocation and leaves no Arrow result on the BE, so
GetFlightInfo used to fail
+ // with "fetch arrow flight schema failed, no
FlightSqlEndpointsLocations" and drop the row.
+ // An Arrow Flight SQL connection has to plan the normal execution
path. See #67368.
+ Field connectTypeField =
ConnectContext.class.getDeclaredField("connectType");
+ connectTypeField.setAccessible(true);
+ ConnectType originConnectType = (ConnectType)
connectTypeField.get(connectContext);
+ try {
+ connectTypeField.set(connectContext, ConnectType.ARROW_FLIGHT_SQL);
+ Plan plan = rewrite("select * from tbl_point_query where `key` =
1");
+
+
Assertions.assertFalse(connectContext.getStatementContext().isShortCircuitQuery());
+ // And the plan really is the ordinary one: tbl_point_query is
empty, so it prunes to a
+ // LogicalEmptyRelation, which is exactly what the short circuit
suppresses in
+ // testShortCircuitPointQueryKeepOlapScanWhenTableEmpty above.
+ Assertions.assertTrue(plan.anyMatch(p -> p instanceof
LogicalEmptyRelation));
+ Assertions.assertFalse(plan.anyMatch(p -> p instanceof
LogicalOlapScan));
+ } finally {
+ connectTypeField.set(connectContext, originConnectType);
+ }
+
+ // The very same statement still short circuits on a MySQL connection.
+ rewrite("select * from tbl_point_query where `key` = 1");
+
Assertions.assertTrue(connectContext.getStatementContext().isShortCircuitQuery());
+ }
+
private long getTabletId(String partitionName) throws Exception {
Database database =
Env.getCurrentInternalCatalog().getDbOrMetaException("test");
OlapTable table = (OlapTable)
database.getTableOrMetaException("tbl_partitioned_point_query");
diff --git
a/regression-test/suites/arrow_flight_sql_p0/test_point_query_over_arrow_flight.groovy
b/regression-test/suites/arrow_flight_sql_p0/test_point_query_over_arrow_flight.groovy
new file mode 100644
index 00000000000..a20b3bc6d45
--- /dev/null
+++
b/regression-test/suites/arrow_flight_sql_p0/test_point_query_over_arrow_flight.groovy
@@ -0,0 +1,131 @@
+// 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.
+
+import org.apache.doris.regression.util.JdbcUtils
+
+// Regression for https://github.com/apache/doris/issues/67368
+//
+// A UNIQUE KEY point query that matches the short circuit is executed by
PointQueryExecutor instead of
+// a Coordinator, and neither end of that path can serve an Arrow Flight
result:
+//
+// * Coordinator and NereidsCoordinator are the only places that register a
FlightSqlEndpointsLocation,
+// so GetFlightInfo found no endpoint and failed with
+// "fetch arrow flight schema failed, no FlightSqlEndpointsLocations",
dropping the row.
+// * The BE could not be pointed at either. tablet_fetch_data serializes with
VMysqlResultWriter into
+// PTabletKeyLookupResponse.row_batch and runs no fragment, so the
ArrowFlightResultBlockBuffer that
+// fetch_arrow_flight_schema looks up by finst id never exists.
+//
+// The fix keeps Arrow Flight SQL connections on the normal execution path,
decided at plan time in
+// LogicalResultSinkToShortCircuitPointQuery. The table below is the one from
the issue.
+suite("test_point_query_over_arrow_flight") {
+ def mysqlConn = context.getConn()
+ def flightConn = context.getArrowFlightSqlConnection()
+
+ def runOnMysql = { String stmt ->
+ def (result, meta) = JdbcUtils.executeToList(mysqlConn, stmt)
+ return result
+ }
+ def runOnFlight = { String stmt ->
+ def (result, meta) = JdbcUtils.executeToList(flightConn, stmt)
+ return result
+ }
+ // The suite level explain{} action always runs on the MySQL connection,
but the whole point here is
+ // which protocol asked for the plan, so read the explain text off each
connection explicitly.
+ def explainOn = { conn, String stmt ->
+ def (rows, meta) = JdbcUtils.executeToList(conn, "explain " + stmt)
+ return rows.collect { row -> row.get(0).toString() }.join("\n")
+ }
+
+ def dbName = context.dbName
+ runOnMysql "USE `${dbName}`"
+ runOnFlight "USE `${dbName}`"
+
+ def tblName = "test_point_query_over_arrow_flight_tbl"
+ runOnMysql "DROP TABLE IF EXISTS ${tblName}"
+ runOnMysql """
+ CREATE TABLE ${tblName} (
+ `col1` SMALLINT NOT NULL,
+ `col2` INT NOT NULL,
+ `loc3` CHAR(10) NOT NULL,
+ `value` CHAR(10) NOT NULL,
+ INDEX col3 (`loc3`) USING INVERTED,
+ INDEX col2_idx (`col2`) USING INVERTED
+ ) ENGINE=OLAP
+ UNIQUE KEY(`col1`, `col2`, `loc3`)
+ DISTRIBUTED BY HASH(`col1`, `col2`, `loc3`) BUCKETS 1
+ PROPERTIES (
+ "replication_allocation" = "tag.location.default: 1",
+ "disable_auto_compaction" = "true",
+ "bloom_filter_columns" = "col1",
+ "store_row_column" = "true",
+ "enable_mow_light_delete" = "false"
+ )
+ """
+ runOnMysql "INSERT INTO ${tblName} VALUES (10, 20, 'aabc', 'value')"
+
+ def pointQuery = "SELECT * FROM ${tblName} WHERE col1 = 10 AND col2 = 20
AND loc3 = 'aabc'"
+
+ // The short circuit is still taken on a MySQL connection: the fix is
scoped to one protocol, it does
+ // not disable the optimization. Assert this first, so a table that
stopped qualifying for the short
+ // circuit (schema or session variable drift) fails loudly here instead of
making the flight
+ // assertions below pass for the wrong reason.
+ def mysqlExplain = explainOn(mysqlConn, pointQuery)
+ assertTrue(mysqlExplain.contains("SHORT-CIRCUIT"),
+ "the point query must still short circuit on a mysql connection,
but got:\n" + mysqlExplain)
+
+ // The same statement must be planned on the normal path over Arrow Flight
SQL.
+ def flightExplain = explainOn(flightConn, pointQuery)
+ assertFalse(flightExplain.contains("SHORT-CIRCUIT"),
+ "the point query must not short circuit on an arrow flight
connection, but got:\n" + flightExplain)
+
+ // This is the call that used to fail with "no
FlightSqlEndpointsLocations".
+ def (flightRows, flightMeta) = JdbcUtils.executeToList(flightConn,
pointQuery)
+ assertEquals(1, flightRows.size())
+ assertEquals(10, flightRows[0][0] as int)
+ assertEquals(20, flightRows[0][1] as int)
+ assertEquals("aabc", flightRows[0][2].toString())
+ assertEquals("value", flightRows[0][3].toString())
+
+ // Both protocols must see the same row, one through the short circuit and
one through the normal
+ // plan.
+ def mysqlRows = runOnMysql(pointQuery)
+ assertEquals(1, mysqlRows.size())
+ assertEquals(mysqlRows[0].collect { it.toString() }, flightRows[0].collect
{ it.toString() })
+
+ // The BE produces the arrow batch, so the column types survive. Serving
the point query result from
+ // the FE instead would hand every column back as a string, because
FlightSqlChannel.addResult builds
+ // varchar vectors only.
+ assertTrue(flightRows[0][0] instanceof Number,
+ "col1 must stay numeric over arrow flight, but got: " +
flightRows[0][0].getClass())
+ assertTrue(flightRows[0][1] instanceof Number,
+ "col2 must stay numeric over arrow flight, but got: " +
flightRows[0][1].getClass())
+ assertEquals(4, flightMeta.getColumnCount())
+
+ // A key that matches no row is planned the same way and used to fail with
the same error, so it is
+ // not enough for the statement above to be the only shape that works.
+ def emptyRows = runOnFlight("SELECT * FROM ${tblName} WHERE col1 = 11 AND
col2 = 20 AND loc3 = 'aabc'")
+ assertEquals(0, emptyRows.size())
+
+ // The workaround reported in the issue keeps working, and a plain non
point query on the same table
+ // is unaffected.
+ def hintRows = runOnFlight("SELECT /*+
SET_VAR(enable_short_circuit_query=false) */ * FROM ${tblName} "
+ + "WHERE col1 = 10 AND col2 = 20 AND loc3 = 'aabc'")
+ assertEquals(1, hintRows.size())
+ assertEquals(1, runOnFlight("SELECT col1 FROM ${tblName} ORDER BY
col1").size())
+
+ runOnMysql "DROP TABLE IF EXISTS ${tblName}"
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]