morningman commented on code in PR #69867:
URL: https://github.com/apache/doris/pull/69867#discussion_r4420287000


##########
fe/fe-core/src/main/java/org/apache/doris/arrowflight/FlightSqlQuerySchema.java:
##########
@@ -0,0 +1,415 @@
+// 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.arrowflight;
+
+import org.apache.doris.analysis.StatementBase;
+import org.apache.doris.arrow.DorisArrowTypeMapping;
+import org.apache.doris.catalog.AggStateType;
+import org.apache.doris.catalog.ArrayType;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.MapType;
+import org.apache.doris.catalog.NameSpaceContext;
+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.common.util.TimeUtils;
+import org.apache.doris.datasource.CatalogIf;
+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.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.HelpCommand;
+import org.apache.doris.nereids.trees.plans.commands.KillCommand;
+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.ShowSnapshotCommand;
+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.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;
+import java.util.Objects;
+
+/**
+ * Resolves the result schema of a Flight SQL statement without scheduling 
fragments or evaluating
+ * query expressions: what Prepare advertises and GetSchema answers, and what 
a prepared execution
+ * is checked against.
+ */
+final class FlightSqlQuerySchema {
+    private FlightSqlQuerySchema() {
+    }
+
+    /**
+     * The statement's result schema can only be told by running it: DESCRIBE 
and the other commands
+     * whose result columns depend on what they find, a request whose 
statements before the last are
+     * not USE or SWITCH, a query with parameter placeholders, a result type 
this frontend cannot
+     * describe as the backend sends it. Prepare advertises the placeholder 
schema for such a
+     * statement and does not check its executions; GetSchema answers 
UNIMPLEMENTED.
+     */
+    static final class UndiscoverableSchemaException extends Exception {
+        UndiscoverableSchemaException(String message) {
+            super(message);
+        }
+    }
+
+    /**
+     * The result schema of {@code query} in the session's current namespace 
and settings. It runs as
+     * a command of the session ({@link 
org.apache.doris.arrowflight.protocol.FlightProtocolAdapter#callCommand},
+     * which makes the context the thread's own), and puts back everything of 
the session it borrows.
+     */
+    static Schema analyze(ConnectContext context, String query) throws 
Exception {
+        StatementContext previousStatement = context.getStatementContext();
+        SessionVariable previousSession = context.getSessionVariable();
+        QueryState previousState = context.getState();
+        StmtExecutor previousExecutor = context.getExecutor();
+        NameSpaceContext previousNamespace = context.getNameSpaceContext();
+        List<StatementBase> statements = Collections.emptyList();
+        try {
+            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());
+            }
+            if (statements.isEmpty()) {
+                throw new UndiscoverableSchemaException("Schema discovery 
requires a statement");
+            }
+            Map<String, String> scopedDatabases = new HashMap<>();
+            // 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 new UndiscoverableSchemaException(
+                            "Schema discovery only supports USE or SWITCH 
before the result statement");
+                }
+                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 new UndiscoverableSchemaException("Flight SQL parameter 
binding is not supported");
+            }
+            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 new UndiscoverableSchemaException("Command result 
metadata is unavailable");
+                }
+                // 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 new UndiscoverableSchemaException(
+                                    "Result metadata is unavailable without 
executing this command");
+                    }
+                }
+            } else {
+                // BindResultSink preserves SQL column labels only in query 
mode, just as
+                // StmtExecutor does; command mode would infer synthetic 
aliases instead.
+                context.getState().setIsQuery(true);
+                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());

Review Comment:
   Fixed in `7c7393c8c18`. Confirmed. Prepare checked privileges on the 
analyzed plan, where the CTE is still a `LogicalCTEAnchor` over its producer 
and `ColumnPruning` treats every producer output as needed, so a CTE used once 
was checked on every column it selects. Execution first inlines that CTE 
(`CTEInline`, `inline_cte_referenced_threshold` = 1) and checks only the 
columns the query reads. The same applies to `SELECT *` in the CTE and to 
column-level policies of external catalogs. 
`FlightSqlQuerySchema.checkPrivileges` now runs `PullUpCteAnchor` and 
`CTEInline` on the analyzed plan before `CheckPrivileges`, as the rewriter does 
before its own check. A CTE used more than once is still checked on its full 
output, and execution does the same (`TestCheckPrivileges` pins that). Inlining 
alone does not make the two checks equivalent, because the rewriter normalizes 
the plan further before it checks privileges. For instance, it drops an ORDER 
BY under a derived table, so `SELECT id FR
 OM (SELECT id, secret FROM t ORDER BY secret) d` runs for a user with only 
`SELECT_PRIV(id)`, while its analyzed plan still reads `secret`. A denial on 
the analyzed plan therefore no longer fails Prepare. The statement keeps the 
placeholder schema and runs unchecked, so execution decides as it did before 
this PR, and GetSchema answers UNIMPLEMENTED with the denial and no schema. 
`DorisFlightSqlSchemaTest#columnGrantsAreCheckedAsExecutionChecksThem` grants 
`SELECT_PRIV(id)` and checks each statement against the rewriter 
(`PlanChecker`). Four single-use CTE shapes return `[id]` from GetSchema and 
Prepare. `SELECT name`, the CTE used twice and the derived-table ORDER BY keep 
the placeholder. The test fails without the inlining, and 
`schemaDiscoveryChecksTablePrivileges` fails if a denial fails Prepare again.
   



##########
fe/fe-core/src/main/java/org/apache/doris/arrowflight/FlightSqlQuerySchema.java:
##########
@@ -0,0 +1,415 @@
+// 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.arrowflight;
+
+import org.apache.doris.analysis.StatementBase;
+import org.apache.doris.arrow.DorisArrowTypeMapping;
+import org.apache.doris.catalog.AggStateType;
+import org.apache.doris.catalog.ArrayType;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.MapType;
+import org.apache.doris.catalog.NameSpaceContext;
+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.common.util.TimeUtils;
+import org.apache.doris.datasource.CatalogIf;
+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.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.HelpCommand;
+import org.apache.doris.nereids.trees.plans.commands.KillCommand;
+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.ShowSnapshotCommand;
+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.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;
+import java.util.Objects;
+
+/**
+ * Resolves the result schema of a Flight SQL statement without scheduling 
fragments or evaluating
+ * query expressions: what Prepare advertises and GetSchema answers, and what 
a prepared execution
+ * is checked against.
+ */
+final class FlightSqlQuerySchema {
+    private FlightSqlQuerySchema() {
+    }
+
+    /**
+     * The statement's result schema can only be told by running it: DESCRIBE 
and the other commands
+     * whose result columns depend on what they find, a request whose 
statements before the last are
+     * not USE or SWITCH, a query with parameter placeholders, a result type 
this frontend cannot
+     * describe as the backend sends it. Prepare advertises the placeholder 
schema for such a
+     * statement and does not check its executions; GetSchema answers 
UNIMPLEMENTED.
+     */
+    static final class UndiscoverableSchemaException extends Exception {
+        UndiscoverableSchemaException(String message) {
+            super(message);
+        }
+    }
+
+    /**
+     * The result schema of {@code query} in the session's current namespace 
and settings. It runs as
+     * a command of the session ({@link 
org.apache.doris.arrowflight.protocol.FlightProtocolAdapter#callCommand},
+     * which makes the context the thread's own), and puts back everything of 
the session it borrows.
+     */
+    static Schema analyze(ConnectContext context, String query) throws 
Exception {
+        StatementContext previousStatement = context.getStatementContext();
+        SessionVariable previousSession = context.getSessionVariable();
+        QueryState previousState = context.getState();
+        StmtExecutor previousExecutor = context.getExecutor();
+        NameSpaceContext previousNamespace = context.getNameSpaceContext();
+        List<StatementBase> statements = Collections.emptyList();
+        try {
+            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());
+            }
+            if (statements.isEmpty()) {
+                throw new UndiscoverableSchemaException("Schema discovery 
requires a statement");
+            }
+            Map<String, String> scopedDatabases = new HashMap<>();
+            // 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 new UndiscoverableSchemaException(
+                            "Schema discovery only supports USE or SWITCH 
before the result statement");
+                }
+                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 new UndiscoverableSchemaException("Flight SQL parameter 
binding is not supported");
+            }
+            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 new UndiscoverableSchemaException("Command result 
metadata is unavailable");
+                }
+                // 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 new UndiscoverableSchemaException(
+                                    "Result metadata is unavailable without 
executing this command");
+                    }
+                }
+            } else {
+                // BindResultSink preserves SQL column labels only in query 
mode, just as
+                // StmtExecutor does; command mode would infer synthetic 
aliases instead.
+                context.getState().setIsQuery(true);
+                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());
+                // The zone BE stamps on TIMESTAMPTZ: the one it is sent, see 
CoordinatorContext.
+                String timezone = 
TimeUtils.getCanonicalTimeZone(context.getSessionVariable().getTimeZone());
+                for (Slot slot : analyzed.getOutput()) {
+                    fields.add(field(slot.getName(), 
slot.getDataType().toCatalogDataType(), slot.nullable(),
+                            timezone));
+                }
+            }
+            return new Schema(fields);
+        } finally {
+            try {
+                List<AutoCloseable> resources = new ArrayList<>();
+                for (StatementBase statement : statements) {
+                    addAnalysisResources(resources, ((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)) {
+                    addAnalysisResources(resources, current);
+                }
+                AutoCloseables.close(resources);
+            } finally {
+                context.setStatementContext(previousStatement);
+                context.setSessionVariable(previousSession);
+                context.setState(previousState);
+                context.setExecutor(previousExecutor);
+                // Restoring the captured namespace does not contact a catalog.
+                context.restoreNameSpaceContext(previousNamespace);
+                context.setCommand(MysqlCommand.COM_SLEEP);
+            }
+        }
+    }
+
+    private static void addAnalysisResources(List<AutoCloseable> resources, 
StatementContext statementContext) {
+        resources.add(statementContext);
+        // close() leaves the connector statement scope of a Flight statement 
to the query-finish
+        // cleanup of the query that runs it. Nothing runs an analysis, so its 
scope is closed here.
+        resources.add(statementContext::closeConnectorStatementScope);
+    }
+
+    static boolean matchesExecutionSchema(Schema prepared, Schema actual, 
List<String> columnLabels) {
+        List<Field> expectedFields = prepared.getFields();
+        List<Field> actualFields = actual.getFields();
+        if (columnLabels == null || expectedFields.size() != 
actualFields.size()
+                || expectedFields.size() != columnLabels.size()
+                || 
!prepared.getCustomMetadata().equals(actual.getCustomMetadata())) {
+            return false;
+        }
+        for (int i = 0; i < expectedFields.size(); ++i) {
+            Field expected = expectedFields.get(i);
+            // Compare the final planner's semantic labels: BE labels may 
instead contain SQL
+            // text or type_name_index. Ignoring all names would hide a 
concurrent column rename.
+            if (!expected.getName().equals(columnLabels.get(i))
+                    || !matchesExecutionField(expected, actualFields.get(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static boolean matchesExecutionField(Field expected, Field actual) 
{
+        // Rewrites can prove an expression non-null (e.g. a folded CAST). 
Such narrowing is
+        // compatible with Prepare's nullable field; the reverse violates its 
advertised contract.
+        if ((!expected.isNullable() && actual.isNullable()) || 
!expected.getType().equals(actual.getType())
+                || !expected.getMetadata().equals(actual.getMetadata())
+                || !Objects.equals(expected.getFieldType().getDictionary(), 
actual.getFieldType().getDictionary())
+                || expected.getChildren().size() != 
actual.getChildren().size()) {
+            return false;
+        }
+        for (int i = 0; i < expected.getChildren().size(); ++i) {
+            Field child = expected.getChildren().get(i);
+            Field actualChild = actual.getChildren().get(i);
+            if (!child.getName().equals(actualChild.getName()) || 
!matchesExecutionField(child, actualChild)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    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 new UndiscoverableSchemaException("Command schema requires 
execution-time metadata");
+        }
+        if (command instanceof HelpCommand) {
+            return ((HelpCommand) command).getMetaData(context);
+        } else if (command instanceof ShowSnapshotCommand) {
+            return ((ShowSnapshotCommand) command).getMetaData(context);
+        } else 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) {
+            // The headers StmtExecutor sends for the plan process and for the 
plan text.
+            if (((ExplainCommand) command).showPlanProcess()) {
+                return ShowResultSetMetaData.builder()
+                        .addColumn(new Column("Rule", Type.STRING))
+                        .addColumn(new Column("Before", Type.STRING))
+                        .addColumn(new Column("After", Type.STRING))
+                        .build();
+            }
+            return stringMetadata("Explain String(Nereids Planner)");
+        }
+        ResultSetMetaData metadata = command.getResultSetMetaData();

Review Comment:
   Fixed in `7c7393c8c18`. Confirmed, and the wrong header predates this PR. 
Since #44393, `ShowStoragePolicyCommand.getMetaData()` has returned the 
four-column USING header for every form. Plain SHOW STORAGE POLICY returns 
`PolicyMgr.showStoragePolicy()`, whose header is the seven-column 
`StoragePolicy.STORAGE_META_DATA` whether or not there are rows. Nothing 
compared the two until now: MySQL `COM_STMT_PREPARE` has reported the four 
columns as well, but MySQL clients use the column definitions that come with 
the execution. `getMetaData()` now returns the header `doRun` answers with for 
each form, so Prepare advertises seven columns for the plain form and four for 
USING and USING FOR. I also went through every `ShowCommand` subclass looking 
for the same mismatch, a `getMetaData()` that differs from the header built by 
`doRun` or by the manager it delegates to, and found no other. The headers that 
depend on more than the syntax are the ones this PR already resolves (HELP, 
SHOW SNAPSHO
 T, SHOW CREATE TABLE, SHOW PROC) or keeps on the placeholder. 
`SchemaOnlyCommandMetadataTest#storagePolicyHeadersMatchExecution` compares the 
header of `doRun` with `getMetaData()` for all three forms and fails without 
the fix. `test_schema_only` now also executes its side-effect-free 
frontend-side statements as prepared statements, SHOW STORAGE POLICY among 
them, and compares the executed schema with the advertised one. Until now it 
only compared Prepare with GetSchema, and both come from the same analysis.
   



-- 
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