Gabriel39 commented on code in PR #68631:
URL: https://github.com/apache/doris/pull/68631#discussion_r4134432920


##########
fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/FlightSqlQuerySchema.java:
##########
@@ -0,0 +1,364 @@
+// 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.service.arrowflight;
+
+import org.apache.doris.analysis.StatementBase;
+import org.apache.doris.catalog.AggStateType;
+import org.apache.doris.catalog.ArrayType;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.MapType;
+import org.apache.doris.catalog.PrimitiveType;
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.catalog.StructField;
+import org.apache.doris.catalog.StructType;
+import org.apache.doris.catalog.Type;
+import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.datasource.es.EsExternalCatalog;
+import org.apache.doris.datasource.lance.LanceExternalCatalog;
+import org.apache.doris.mysql.MysqlCommand;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.StatementContext;
+import org.apache.doris.nereids.glue.LogicalPlanAdapter;
+import org.apache.doris.nereids.parser.NereidsParser;
+import org.apache.doris.nereids.parser.SqlDialectHelper;
+import org.apache.doris.nereids.rules.rewrite.CheckPrivileges;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.PrepareCommandPlanner;
+import org.apache.doris.nereids.trees.plans.commands.AlterTableCommand;
+import org.apache.doris.nereids.trees.plans.commands.Command;
+import org.apache.doris.nereids.trees.plans.commands.DeleteFromCommand;
+import org.apache.doris.nereids.trees.plans.commands.DescribeCommand;
+import org.apache.doris.nereids.trees.plans.commands.ExplainCommand;
+import org.apache.doris.nereids.trees.plans.commands.KillCommand;
+import org.apache.doris.nereids.trees.plans.commands.ReplayCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowCreateTableCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowDataCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowPartitionsCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowProcCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowPythonPackagesCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowQueryStatsCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowTableCommand;
+import org.apache.doris.nereids.trees.plans.commands.TransactionCommand;
+import org.apache.doris.nereids.trees.plans.commands.UpdateCommand;
+import org.apache.doris.nereids.trees.plans.commands.info.CreateIndexOp;
+import org.apache.doris.nereids.trees.plans.commands.info.DropIndexOp;
+import 
org.apache.doris.nereids.trees.plans.commands.insert.BatchInsertIntoTableCommand;
+import 
org.apache.doris.nereids.trees.plans.commands.insert.InsertIntoTVFCommand;
+import 
org.apache.doris.nereids.trees.plans.commands.insert.InsertIntoTableCommand;
+import 
org.apache.doris.nereids.trees.plans.commands.insert.InsertOverwriteTableCommand;
+import org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand;
+import org.apache.doris.nereids.trees.plans.commands.use.SwitchCommand;
+import org.apache.doris.nereids.trees.plans.commands.use.UseCommand;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.QueryState;
+import org.apache.doris.qe.ResultSetMetaData;
+import org.apache.doris.qe.SessionVariable;
+import org.apache.doris.qe.ShowResultSetMetaData;
+import org.apache.doris.qe.StmtExecutor;
+import org.apache.doris.qe.VariableMgr;
+
+import org.apache.arrow.flight.CallStatus;
+import org.apache.arrow.util.AutoCloseables;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.arrow.vector.types.pojo.Schema;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Resolves result metadata without scheduling fragments or evaluating query 
expressions. */
+final class FlightSqlQuerySchema {
+    private FlightSqlQuerySchema() {
+    }
+
+    static Schema analyze(ConnectContext context, String query) throws 
Exception {
+        synchronized (context) {
+            ConnectContext previousThreadContext = ConnectContext.get();
+            StatementContext previousStatement = context.getStatementContext();
+            SessionVariable previousSession = context.getSessionVariable();
+            QueryState previousState = context.getState();
+            StmtExecutor previousExecutor = context.getExecutor();
+            String previousCatalog = context.getDefaultCatalog();
+            String previousDatabase = context.getDatabase();
+            List<StatementBase> statements = Collections.emptyList();
+            try {
+                context.setThreadLocalInfo();
+                context.setCommand(MysqlCommand.COM_QUERY);
+                // Parsing SET_VAR hints already mutates session variables. 
Isolate them even when parsing fails.
+                
context.setSessionVariable(VariableMgr.cloneSessionVariable(previousSession));
+                context.setState(new QueryState());
+                context.setExecutor(null);
+                context.setStatementContext(null);
+                // Match execution's HTTP/plugin conversion before the dialect 
parser sees the SQL.
+                String converted = SqlDialectHelper.convertSqlByDialect(query, 
context.getSessionVariable());
+                try {
+                    statements = new NereidsParser().parseSQL(converted, 
context.getSessionVariable());
+                } catch (Exception convertedError) {
+                    if 
(!context.getSessionVariable().isRetryOriginSqlOnConvertFail() || 
converted.equals(query)) {
+                        throw convertedError;
+                    }
+                    // Match execution's parse fallback while discarding any 
failed parser context.
+                    StatementContext failed = context.getStatementContext();
+                    if (failed != null) {
+                        failed.close();
+                        context.setStatementContext(null);
+                    }
+                    statements = new NereidsParser().parseSQL(query, 
context.getSessionVariable());
+                }
+                Map<String, String> scopedDatabases = new HashMap<>();
+                if (statements.isEmpty()) {
+                    throw CallStatus.UNIMPLEMENTED.withDescription(
+                            "Schema discovery requires a 
statement").toRuntimeException();
+                }
+                // JDBC clients commonly prefix their query with USE. Resolve 
that namespace only within this scope.
+                for (int i = 0; i < statements.size() - 1; ++i) {
+                    Plan prefix = ((LogicalPlanAdapter) 
statements.get(i)).getLogicalPlan();
+                    if (!(prefix instanceof UseCommand) && !(prefix instanceof 
SwitchCommand)) {
+                        throw CallStatus.UNIMPLEMENTED.withDescription(
+                                "Schema discovery only supports USE or SWITCH 
before the result statement")
+                                .toRuntimeException();
+                    }
+                    resolveNamespace(context, prefix, scopedDatabases);
+                }
+                LogicalPlanAdapter statement = (LogicalPlanAdapter) 
statements.get(statements.size() - 1);
+                StatementContext statementContext = 
statement.getStatementContext();
+                context.setStatementContext(statementContext);
+                statementContext.setParsedStatement(statement);
+                if (!statementContext.getPlaceholders().isEmpty()) {
+                    throw CallStatus.UNIMPLEMENTED.withDescription(
+                            "Flight SQL parameter binding is not 
supported").toRuntimeException();
+                }
+                List<Field> fields = new ArrayList<>();
+                Plan plan = statement.getLogicalPlan();
+                if (plan instanceof Command) {
+                    resolveNamespace(context, plan, scopedDatabases);
+                    ResultSetMetaData metadata = commandMetadata(context, 
(Command) plan);
+                    if (metadata == null) {
+                        throw 
CallStatus.UNIMPLEMENTED.withDescription("Command result metadata is 
unavailable")
+                                .toRuntimeException();
+                    }
+                    // FE-local result sets are serialized as nullable strings 
by FlightSqlChannel.
+                    for (Column column : metadata.getColumns()) {
+                        fields.add(Field.nullable(column.getName(), new 
ArrowType.Utf8()));
+                    }
+                    if (fields.isEmpty()) {
+                        switch (((Command) plan).stmtType()) {
+                            case SET:
+                            case USE:
+                            case SWITCH:
+                            case CREATE:
+                            case ALTER:
+                            case DROP:
+                            case TRUNCATE:
+                                // Only known no-row command categories have 
the protocol OK schema.
+                                fields.add(Field.nullable("StatusResult", new 
ArrowType.Utf8()));
+                                break;
+                            default:
+                                throw CallStatus.UNIMPLEMENTED.withDescription(

Review Comment:
   Fixed in a781cf92ecf. HELP resolves its topic, keyword, or category header 
from the local help module without reading topic text or producing rows. Unit 
tests cover all result shapes and compare headers with execution; the Flight 
RPC regression also checks Prepare/GetSchema/GetFlightInfo/DoGet parity for 
HELP.



##########
fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/DorisFlightSqlProducer.java:
##########
@@ -318,18 +327,63 @@ public FlightInfo getFlightInfoStatement(final 
CommandStatementQuery request, fi
     @Override
     public FlightInfo getFlightInfoPreparedStatement(final 
CommandPreparedStatementQuery command,
             final CallContext context, final FlightDescriptor descriptor) {
-        String[] handleParts = 
command.getPreparedStatementHandle().toStringUtf8().split(":");
-        String executedPeerIdentity = handleParts[0];
-        String preparedStatementId = handleParts[1];
-        ConnectContext connectContext = 
flightSessionsManager.getConnectContext(executedPeerIdentity);
-        return executeQueryStatement(executedPeerIdentity, connectContext,
-                connectContext.getPreparedQuery(preparedStatementId), 
descriptor);
+        ConnectContext connection = 
flightSessionsManager.getConnectContext(context.peerIdentity());
+        synchronized (connection) {
+            return executeQueryStatement(context.peerIdentity(), connection,
+                    preparedQuery(connection, context, command).getLeft(), 
descriptor);

Review Comment:
   Fixed in a781cf92ecf. After execution replans, compare the actual FlightInfo 
schema with the prepared schema before returning any ticket. On mismatch, 
remove the handle, cancel the query, finalize its deferred executors, and clear 
FE results and endpoints. The new test simulates execution returning a 
different schema after successful reanalysis and verifies cancellation, 
cleanup, handle expiry, and no double-finalization for both BE and FE result 
paths.



##########
fe/fe-core/src/main/java/org/apache/doris/qe/ConnectContext.java:
##########
@@ -907,15 +924,34 @@ public void resetLoginTime() {
         this.loginTime = System.currentTimeMillis();
     }
 
-    public void addPreparedQuery(String preparedStatementId, String 
preparedQuery) {
-        preparedQuerys.put(preparedStatementId, preparedQuery);
+    public synchronized void addPreparedQuery(String preparedStatementId, 
String preparedQuery) {
+        addPreparedQuery(preparedStatementId, preparedQuery, null);
+    }
+
+    public synchronized void addPreparedQuery(String preparedStatementId, 
String preparedQuery, Schema schema) {
+        preparedQuerys.put(preparedStatementId,
+                new PreparedQuery(preparedQuery, getDefaultCatalog(), 
getDatabase(), schema));
     }
 
-    public String getPreparedQuery(String preparedStatementId) {
-        return preparedQuerys.get(preparedStatementId);
+    public synchronized Schema getPreparedQuerySchema(String 
preparedStatementId) {
+        PreparedQuery query = preparedQuerys.get(preparedStatementId);
+        return query == null ? null : query.schema;
+    }
+
+    public synchronized String getPreparedQuery(String preparedStatementId) {
+        PreparedQuery query = preparedQuerys.get(preparedStatementId);
+        if (query == null) {
+            return null;
+        }
+        // A handle must not execute unqualified SQL in a different namespace 
than its advertised schema.
+        if (!Objects.equals(query.catalog, getDefaultCatalog()) || 
!Objects.equals(query.database, getDatabase())) {

Review Comment:
   Fixed in a781cf92ecf. After successful execution with the advertised schema, 
rebind this handle to the resulting namespace under the same session lock. A 
prepared USE or USE-prefixed query can execute repeatedly; unrelated handles 
still expire on a namespace change. Added FE tests and a real Flight RPC 
regression executing prepared USE and USE-prefixed SHOW twice, including 
GetSchema and DoGet after execution.



##########
fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/FlightSqlQuerySchema.java:
##########
@@ -0,0 +1,364 @@
+// 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.service.arrowflight;
+
+import org.apache.doris.analysis.StatementBase;
+import org.apache.doris.catalog.AggStateType;
+import org.apache.doris.catalog.ArrayType;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.MapType;
+import org.apache.doris.catalog.PrimitiveType;
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.catalog.StructField;
+import org.apache.doris.catalog.StructType;
+import org.apache.doris.catalog.Type;
+import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.datasource.es.EsExternalCatalog;
+import org.apache.doris.datasource.lance.LanceExternalCatalog;
+import org.apache.doris.mysql.MysqlCommand;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.StatementContext;
+import org.apache.doris.nereids.glue.LogicalPlanAdapter;
+import org.apache.doris.nereids.parser.NereidsParser;
+import org.apache.doris.nereids.parser.SqlDialectHelper;
+import org.apache.doris.nereids.rules.rewrite.CheckPrivileges;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.PrepareCommandPlanner;
+import org.apache.doris.nereids.trees.plans.commands.AlterTableCommand;
+import org.apache.doris.nereids.trees.plans.commands.Command;
+import org.apache.doris.nereids.trees.plans.commands.DeleteFromCommand;
+import org.apache.doris.nereids.trees.plans.commands.DescribeCommand;
+import org.apache.doris.nereids.trees.plans.commands.ExplainCommand;
+import org.apache.doris.nereids.trees.plans.commands.KillCommand;
+import org.apache.doris.nereids.trees.plans.commands.ReplayCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowCreateTableCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowDataCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowPartitionsCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowProcCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowPythonPackagesCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowQueryStatsCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowTableCommand;
+import org.apache.doris.nereids.trees.plans.commands.TransactionCommand;
+import org.apache.doris.nereids.trees.plans.commands.UpdateCommand;
+import org.apache.doris.nereids.trees.plans.commands.info.CreateIndexOp;
+import org.apache.doris.nereids.trees.plans.commands.info.DropIndexOp;
+import 
org.apache.doris.nereids.trees.plans.commands.insert.BatchInsertIntoTableCommand;
+import 
org.apache.doris.nereids.trees.plans.commands.insert.InsertIntoTVFCommand;
+import 
org.apache.doris.nereids.trees.plans.commands.insert.InsertIntoTableCommand;
+import 
org.apache.doris.nereids.trees.plans.commands.insert.InsertOverwriteTableCommand;
+import org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand;
+import org.apache.doris.nereids.trees.plans.commands.use.SwitchCommand;
+import org.apache.doris.nereids.trees.plans.commands.use.UseCommand;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.QueryState;
+import org.apache.doris.qe.ResultSetMetaData;
+import org.apache.doris.qe.SessionVariable;
+import org.apache.doris.qe.ShowResultSetMetaData;
+import org.apache.doris.qe.StmtExecutor;
+import org.apache.doris.qe.VariableMgr;
+
+import org.apache.arrow.flight.CallStatus;
+import org.apache.arrow.util.AutoCloseables;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.arrow.vector.types.pojo.Schema;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Resolves result metadata without scheduling fragments or evaluating query 
expressions. */
+final class FlightSqlQuerySchema {
+    private FlightSqlQuerySchema() {
+    }
+
+    static Schema analyze(ConnectContext context, String query) throws 
Exception {
+        synchronized (context) {
+            ConnectContext previousThreadContext = ConnectContext.get();
+            StatementContext previousStatement = context.getStatementContext();
+            SessionVariable previousSession = context.getSessionVariable();
+            QueryState previousState = context.getState();
+            StmtExecutor previousExecutor = context.getExecutor();
+            String previousCatalog = context.getDefaultCatalog();
+            String previousDatabase = context.getDatabase();
+            List<StatementBase> statements = Collections.emptyList();
+            try {
+                context.setThreadLocalInfo();
+                context.setCommand(MysqlCommand.COM_QUERY);
+                // Parsing SET_VAR hints already mutates session variables. 
Isolate them even when parsing fails.
+                
context.setSessionVariable(VariableMgr.cloneSessionVariable(previousSession));
+                context.setState(new QueryState());
+                context.setExecutor(null);
+                context.setStatementContext(null);
+                // Match execution's HTTP/plugin conversion before the dialect 
parser sees the SQL.
+                String converted = SqlDialectHelper.convertSqlByDialect(query, 
context.getSessionVariable());
+                try {
+                    statements = new NereidsParser().parseSQL(converted, 
context.getSessionVariable());
+                } catch (Exception convertedError) {
+                    if 
(!context.getSessionVariable().isRetryOriginSqlOnConvertFail() || 
converted.equals(query)) {
+                        throw convertedError;
+                    }
+                    // Match execution's parse fallback while discarding any 
failed parser context.
+                    StatementContext failed = context.getStatementContext();
+                    if (failed != null) {
+                        failed.close();
+                        context.setStatementContext(null);
+                    }
+                    statements = new NereidsParser().parseSQL(query, 
context.getSessionVariable());
+                }
+                Map<String, String> scopedDatabases = new HashMap<>();
+                if (statements.isEmpty()) {
+                    throw CallStatus.UNIMPLEMENTED.withDescription(
+                            "Schema discovery requires a 
statement").toRuntimeException();
+                }
+                // JDBC clients commonly prefix their query with USE. Resolve 
that namespace only within this scope.
+                for (int i = 0; i < statements.size() - 1; ++i) {
+                    Plan prefix = ((LogicalPlanAdapter) 
statements.get(i)).getLogicalPlan();
+                    if (!(prefix instanceof UseCommand) && !(prefix instanceof 
SwitchCommand)) {
+                        throw CallStatus.UNIMPLEMENTED.withDescription(
+                                "Schema discovery only supports USE or SWITCH 
before the result statement")
+                                .toRuntimeException();
+                    }
+                    resolveNamespace(context, prefix, scopedDatabases);
+                }
+                LogicalPlanAdapter statement = (LogicalPlanAdapter) 
statements.get(statements.size() - 1);
+                StatementContext statementContext = 
statement.getStatementContext();
+                context.setStatementContext(statementContext);
+                statementContext.setParsedStatement(statement);
+                if (!statementContext.getPlaceholders().isEmpty()) {
+                    throw CallStatus.UNIMPLEMENTED.withDescription(
+                            "Flight SQL parameter binding is not 
supported").toRuntimeException();
+                }
+                List<Field> fields = new ArrayList<>();
+                Plan plan = statement.getLogicalPlan();
+                if (plan instanceof Command) {
+                    resolveNamespace(context, plan, scopedDatabases);
+                    ResultSetMetaData metadata = commandMetadata(context, 
(Command) plan);
+                    if (metadata == null) {
+                        throw 
CallStatus.UNIMPLEMENTED.withDescription("Command result metadata is 
unavailable")
+                                .toRuntimeException();
+                    }
+                    // FE-local result sets are serialized as nullable strings 
by FlightSqlChannel.
+                    for (Column column : metadata.getColumns()) {
+                        fields.add(Field.nullable(column.getName(), new 
ArrowType.Utf8()));
+                    }
+                    if (fields.isEmpty()) {
+                        switch (((Command) plan).stmtType()) {
+                            case SET:
+                            case USE:
+                            case SWITCH:
+                            case CREATE:
+                            case ALTER:
+                            case DROP:
+                            case TRUNCATE:
+                                // Only known no-row command categories have 
the protocol OK schema.
+                                fields.add(Field.nullable("StatusResult", new 
ArrowType.Utf8()));
+                                break;
+                            default:
+                                throw CallStatus.UNIMPLEMENTED.withDescription(
+                                        "Result metadata is unavailable 
without executing this command")
+                                        .toRuntimeException();
+                        }
+                    }
+                } else {
+                    PrepareCommandPlanner planner = new 
PrepareCommandPlanner(statementContext);
+                    planner.plan(statement, 
context.getSessionVariable().toThrift());
+                    CascadesContext cascades = planner.getCascadesContext();
+                    Plan analyzed = cascades.getRewritePlan();
+                    // PrepareCommandPlanner stops before the rewrite phase 
that normally checks privileges.
+                    new CheckPrivileges().rewriteRoot(analyzed, 
cascades.getCurrentJobContext());
+                    for (Slot slot : analyzed.getOutput()) {
+                        fields.add(field(slot.getName(), 
slot.getDataType().toCatalogDataType(), slot.nullable(),
+                                true, 
context.getSessionVariable().getTimeZone()));
+                    }
+                }
+                return new Schema(fields);
+            } finally {
+                try {
+                    List<AutoCloseable> resources = new ArrayList<>();
+                    for (StatementBase statement : statements) {
+                        if (statement instanceof LogicalPlanAdapter) {
+                            resources.add(((LogicalPlanAdapter) 
statement).getStatementContext());
+                        }
+                    }
+                    // A parser failure can leave a context that was never 
added to the returned list.
+                    StatementContext current = context.getStatementContext();
+                    if (current != null && current != previousStatement && 
!resources.contains(current)) {
+                        resources.add(current);
+                    }
+                    AutoCloseables.close(resources);
+                } finally {
+                    try {
+                        context.setStatementContext(previousStatement);
+                        context.setSessionVariable(previousSession);
+                        context.setState(previousState);
+                        context.setExecutor(previousExecutor);
+                        if 
(!previousCatalog.equals(context.getDefaultCatalog())
+                                || 
!previousDatabase.equals(context.getDatabase())) {
+                            context.changeDefaultCatalog(previousCatalog);
+                            context.setDatabase(previousDatabase);
+                        }
+                    } finally {
+                        context.setCommand(MysqlCommand.COM_SLEEP);
+                        if (previousThreadContext == null) {
+                            ConnectContext.remove();
+                        } else {
+                            previousThreadContext.setThreadLocalInfo();
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private static ResultSetMetaData commandMetadata(ConnectContext context, 
Command command) throws Exception {
+        // These getters depend on execution-time state or remote responses. 
Do not advertise a
+        // guessed schema, or run the command merely to discover it.
+        if (command instanceof ShowPythonPackagesCommand || command instanceof 
DescribeCommand
+                || command instanceof ShowDataCommand || command instanceof 
ShowPartitionsCommand
+                || command instanceof ShowQueryStatsCommand) {
+            throw CallStatus.UNIMPLEMENTED.withDescription("Command schema 
requires execution-time metadata")
+                    .toRuntimeException();
+        }
+        if (command instanceof ShowTableCommand) {
+            ((ShowTableCommand) command).validate(context);
+        } else if (command instanceof ShowCreateTableCommand) {
+            return ((ShowCreateTableCommand) command).getMetaData(context);
+        } else if (command instanceof ShowProcCommand) {
+            return ((ShowProcCommand) command).getMetaData(context);
+        }
+        if (command instanceof ExplainCommand) {
+            // PLAN PROCESS has no Flight serialization path in StmtExecutor.
+            if (((ExplainCommand) command).showPlanProcess()) {
+                throw CallStatus.UNIMPLEMENTED.withDescription("EXPLAIN PLAN 
PROCESS is not supported over Flight SQL")
+                        .toRuntimeException();
+            }
+            return stringMetadata("Explain String(Nereids Planner)");
+        } else if (command instanceof ReplayCommand) {
+            return stringMetadata("Plan Replayer dump url");
+        } else if (command instanceof AlterTableCommand) {
+            AlterTableCommand alter = (AlterTableCommand) command;
+            String catalog = alter.getTbl().getCtl();
+            // Lance index admission returns a JobId header even for an IF 
no-op. Do not run
+            // validation/admission here: those paths can resolve remote 
tables or allocate IDs.
+            if (context.getCatalog(catalog == null ? 
context.getDefaultCatalog() : catalog)
+                    instanceof LanceExternalCatalog && 
alter.getNereidsOps().stream().anyMatch(op ->
+                        (op instanceof CreateIndexOp && !((CreateIndexOp) 
op).isAlter())
+                                || (op instanceof DropIndexOp && 
!((DropIndexOp) op).isAlter()))) {
+                return stringMetadata("JobId");
+            }
+        }
+        ResultSetMetaData metadata = command.getResultSetMetaData();

Review Comment:
   Fixed in a781cf92ecf. SHOW SNAPSHOT now validates its WHERE predicates 
before selecting the three-column summary or five-column detail header; the 
execution path reuses this helper. No repository lookup or snapshot fetch 
occurs during discovery. Unit tests cover summary/detail headers, invalid 
timestamp-only filters, and absence of repository access. The Flight RPC 
regression reproduced the old three-column detail header and passes with the 
fix.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to