github-actions[bot] commented on code in PR #67966:
URL: https://github.com/apache/doris/pull/67966#discussion_r4010615498


##########
fe/fe-core/src/main/java/org/apache/doris/arrowflight/DorisFlightSqlProducer.java:
##########
@@ -632,20 +638,72 @@ public void 
getStreamCrossReference(CommandGetCrossReference command, CallContex
         throw 
CallStatus.UNIMPLEMENTED.withDescription("getStreamCrossReference 
unimplemented").toRuntimeException();
     }
 
+    /**
+     * Sets session options: the current catalog, the current database and 
session variables, see
+     * {@link FlightSessionOptions}. The ADBC Flight SQL driver sends these 
for the connection's
+     * {@code adbc.connection.catalog} / {@code adbc.connection.db_schema} and 
for its
+     * {@code adbc.flight.sql.session.option.*} options; the Flight SQL JDBC 
driver for its
+     * {@code catalog} property. Each option is set on its own and answered on 
its own: the result
+     * names the ones that could not be set and why, and the action itself 
only fails when the
+     * session cannot be reached.
+     */
+    @Override
+    public void setSessionOptions(final SetSessionOptionsRequest request, 
final CallContext context,
+            final StreamListener<SetSessionOptionsResult> listener) {
+        try {
+            ConnectContext connectContext = 
flightSessionsManager.getConnectContext(context.peerIdentity());
+            Map<String, SetSessionOptionsResult.Error> errors = 
FlightProtocolAdapter.of(connectContext)

Review Comment:
   [P1] Bound prior-request resources independently of option traffic
   
   Neither new option action calls beginRequest, so the preceding request's 
cached FE result/endpoints remain and any deferred external-query executor 
stays registered. Accepted SET already refreshes startTime, and fixing GET's 
missing activity refresh would make GET do so too; periodic option actions can 
therefore retain old buffers plus the coordinator, query registration, and 
query-queue slot indefinitely unless resource age is separate. Please either 
release prior-request state at a safe boundary or age/reap it independently of 
option activity, and test both actions after cached-local and deferred queries.



##########
fe/fe-core/src/main/java/org/apache/doris/arrowflight/DorisFlightSqlProducer.java:
##########
@@ -632,20 +638,72 @@ public void 
getStreamCrossReference(CommandGetCrossReference command, CallContex
         throw 
CallStatus.UNIMPLEMENTED.withDescription("getStreamCrossReference 
unimplemented").toRuntimeException();
     }
 
+    /**
+     * Sets session options: the current catalog, the current database and 
session variables, see
+     * {@link FlightSessionOptions}. The ADBC Flight SQL driver sends these 
for the connection's
+     * {@code adbc.connection.catalog} / {@code adbc.connection.db_schema} and 
for its
+     * {@code adbc.flight.sql.session.option.*} options; the Flight SQL JDBC 
driver for its
+     * {@code catalog} property. Each option is set on its own and answered on 
its own: the result
+     * names the ones that could not be set and why, and the action itself 
only fails when the
+     * session cannot be reached.
+     */
+    @Override
+    public void setSessionOptions(final SetSessionOptionsRequest request, 
final CallContext context,

Review Comment:
   [P2] Advertise the newly supported session actions
   
   This class still inherits Arrow Java 19's FlightSqlProducer.listActions, 
whose FLIGHT_SQL_ACTIONS list omits SetSessionOptions, GetSessionOptions, and 
CloseSession even though this PR implements all three FlightConstants actions. 
Capability-discovering clients are therefore told these actions are unavailable 
while direct calls happen to work. Please override listActions to emit the 
inherited SQL actions plus these three constants, and add a discovery assertion.



##########
fe/fe-core/src/main/java/org/apache/doris/arrowflight/DorisFlightSqlProducer.java:
##########
@@ -632,20 +638,72 @@ public void 
getStreamCrossReference(CommandGetCrossReference command, CallContex
         throw 
CallStatus.UNIMPLEMENTED.withDescription("getStreamCrossReference 
unimplemented").toRuntimeException();
     }
 
+    /**
+     * Sets session options: the current catalog, the current database and 
session variables, see
+     * {@link FlightSessionOptions}. The ADBC Flight SQL driver sends these 
for the connection's
+     * {@code adbc.connection.catalog} / {@code adbc.connection.db_schema} and 
for its
+     * {@code adbc.flight.sql.session.option.*} options; the Flight SQL JDBC 
driver for its
+     * {@code catalog} property. Each option is set on its own and answered on 
its own: the result
+     * names the ones that could not be set and why, and the action itself 
only fails when the
+     * session cannot be reached.
+     */
+    @Override
+    public void setSessionOptions(final SetSessionOptionsRequest request, 
final CallContext context,
+            final StreamListener<SetSessionOptionsResult> listener) {
+        try {
+            ConnectContext connectContext = 
flightSessionsManager.getConnectContext(context.peerIdentity());
+            Map<String, SetSessionOptionsResult.Error> errors = 
FlightProtocolAdapter.of(connectContext)
+                    .callCommand(connectContext,
+                            () -> FlightSessionOptions.set(connectContext, 
request.getSessionOptions()));
+            listener.onNext(new SetSessionOptionsResult(errors));
+            listener.onCompleted();
+        } catch (FlightRuntimeException e) {
+            // Same as in getFlightInfoStatement: keep the status the 
session's command lock chose.
+            LOG.error("set session options failed", e);
+            listener.onError(e);
+        } catch (Throwable e) {
+            String errMsg = "set session options failed, " + e.getMessage();
+            LOG.error(errMsg, e);
+            
listener.onError(CallStatus.INTERNAL.withDescription(errMsg).withCause(e).toRuntimeException());
+        }
+    }
+
+    /** The session's options, see {@link FlightSessionOptions#get}. */
+    @Override
+    public void getSessionOptions(final GetSessionOptionsRequest request, 
final CallContext context,
+            final StreamListener<GetSessionOptionsResult> listener) {
+        try {
+            ConnectContext connectContext = 
flightSessionsManager.getConnectContext(context.peerIdentity());
+            Map<String, SessionOptionValue> options = 
FlightProtocolAdapter.of(connectContext)

Review Comment:
   [P1] Refresh session activity for option-only commands
   
   This GET runs only through callCommand, which does not set COM_QUERY or 
refresh ConnectContext.startTime; empty or wholly rejected SET requests have 
the same path because they never reach runStatement. Therefore a client can 
keep successfully issuing these session actions and still be reaped at 
wait_timeout based on the context's old timestamp. Once the context is 
unregistered, the same authenticated token cannot recreate it because 
createdSession is already true. Please give every session-option action the 
normal active/sleep activity lifecycle and cover repeated GET plus 
empty/invalid-only SET across the timeout boundary.



##########
fe/fe-core/src/main/java/org/apache/doris/arrowflight/FlightSessionOptions.java:
##########
@@ -0,0 +1,282 @@
+// 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.SetType;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.nereids.util.SqlLiteralUtils;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.QueryState.MysqlStateType;
+import org.apache.doris.qe.SqlModeHelper;
+import org.apache.doris.qe.VarAttrDef;
+import org.apache.doris.qe.VariableMgr;
+import org.apache.doris.qe.VariableMgr.VarContext;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.arrow.flight.SessionOptionValue;
+import org.apache.arrow.flight.SessionOptionValueFactory;
+import org.apache.arrow.flight.SessionOptionValueVisitor;
+import org.apache.arrow.flight.SetSessionOptionsResult;
+import org.apache.arrow.flight.SetSessionOptionsResult.ErrorValue;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * The session options of an Arrow Flight SQL session, as the 
SetSessionOptions and GetSessionOptions
+ * actions see them.
+ *
+ * <p>An option stands for the statement of the session that sets it: {@code 
catalog} is the current
+ * catalog ({@code SWITCH}), {@code schema} the current database ({@code 
USE}), and any other name is
+ * the session variable of that name ({@code SET SESSION}). Those two names 
are what the ADBC Flight
+ * SQL driver puts on the wire for {@code adbc.connection.catalog} and
+ * {@code adbc.connection.db_schema}, and what the Flight SQL JDBC driver 
sends for its
+ * {@code catalog} property; every other option name the drivers pass through 
as given. Setting an
+ * option runs its statement as a command of the session, so it is checked, 
audited and takes effect
+ * exactly as if the client had sent the statement. Reading the options back 
gives what
+ * {@code SHOW VARIABLES} shows, every value as a string: the one 
representation {@code SET} accepts
+ * back, whatever the Java type of the variable behind it.
+ *
+ * <p>The result of setting an option is one of the three {@link ErrorValue}s 
per name and nothing
+ * else, so the reason a value was refused only reaches the frontend log.
+ */
+public final class FlightSessionOptions {
+    private static final Logger LOG = 
LogManager.getLogger(FlightSessionOptions.class);
+
+    /** The current catalog; set with {@code SWITCH}. */
+    public static final String CATALOG = "catalog";
+    /** The current database; set with {@code USE}. */
+    public static final String SCHEMA = "schema";
+
+    // What a session variable is called in SET: VariableMgr looks the name up 
case-insensitively,
+    // and only a name made of these characters is one the parser reads as the 
variable's identifier.
+    private static final Pattern VARIABLE_NAME = 
Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
+
+    private FlightSessionOptions() {
+    }
+
+    /**
+     * Sets the options of one request, each on its own, and returns the error 
of every option that
+     * could not be set; an option absent from the result was set. {@code 
catalog} goes first and
+     * {@code schema} second, since {@code USE} names a database of the 
current catalog; the others
+     * follow in name order, so that a request applies the same way every 
time. Runs as a command of
+     * the session, under its command lock.
+     */
+    public static Map<String, SetSessionOptionsResult.Error> 
set(ConnectContext ctx,
+            Map<String, SessionOptionValue> options) {
+        List<String> names = new ArrayList<>(options.keySet());
+        Collections.sort(names);
+        names.remove(SCHEMA);
+        names.remove(CATALOG);
+        if (options.containsKey(SCHEMA)) {
+            names.add(0, SCHEMA);
+        }
+        if (options.containsKey(CATALOG)) {
+            names.add(0, CATALOG);
+        }
+        Map<String, SetSessionOptionsResult.Error> errors = new 
LinkedHashMap<>();
+        for (String name : names) {
+            ErrorValue error = setOne(ctx, name, options.get(name));
+            if (error != null) {
+                errors.put(name, new SetSessionOptionsResult.Error(error));
+            }
+        }
+        return errors;
+    }
+
+    /** Sets one option and returns why it could not be, or null when it was 
set. */
+    @VisibleForTesting
+    static ErrorValue setOne(ConnectContext ctx, String name, 
SessionOptionValue value) {
+        if (CATALOG.equals(name)) {
+            return switchCatalog(ctx, value);
+        }
+        if (SCHEMA.equals(name)) {
+            return useDatabase(ctx, value);
+        }
+        return setVariable(ctx, name, value);
+    }
+
+    /**
+     * The options of the session: the current catalog, the current database 
(an empty string when
+     * none has been chosen) and every session variable {@code SHOW VARIABLES} 
would list, in its text.
+     */
+    public static Map<String, SessionOptionValue> get(ConnectContext ctx) {
+        Map<String, SessionOptionValue> options = new LinkedHashMap<>();
+        options.put(CATALOG, 
SessionOptionValueFactory.makeSessionOptionValue(ctx.getDefaultCatalog()));
+        String database = ctx.getDatabase();
+        options.put(SCHEMA, 
SessionOptionValueFactory.makeSessionOptionValue(database == null ? "" : 
database));
+        for (List<String> row : VariableMgr.dump(SetType.SESSION, 
ctx.getSessionVariable(), null)) {
+            // A row is name, value, default value, changed.
+            options.put(row.get(0), 
SessionOptionValueFactory.makeSessionOptionValue(row.get(1)));
+        }
+        return options;
+    }
+
+    private static ErrorValue switchCatalog(ConnectContext ctx, 
SessionOptionValue value) {
+        String catalog = value.acceptVisitor(STRING_VALUE);
+        if (catalog == null || catalog.isEmpty()) {
+            return ErrorValue.INVALID_VALUE;
+        }
+        return runStatement(ctx, CATALOG, "SWITCH " + 
quoteIdentifier(catalog), ErrorCode.ERR_UNKNOWN_CATALOG);
+    }
+
+    private static ErrorValue useDatabase(ConnectContext ctx, 
SessionOptionValue value) {
+        String database = value.acceptVisitor(STRING_VALUE);
+        if (database == null || database.isEmpty()) {
+            return ErrorValue.INVALID_VALUE;
+        }
+        return runStatement(ctx, SCHEMA, "USE " + quoteIdentifier(database), 
ErrorCode.ERR_BAD_DB_ERROR);
+    }
+
+    private static ErrorValue setVariable(ConnectContext ctx, String name, 
SessionOptionValue value) {
+        // Only a session variable is a session option. A name SET quietly 
ignores for the sake of
+        // MySQL clients (a removed variable, the MySQL compatibility 
whitelist) is no variable of
+        // this session either: setting it would set nothing, and reading the 
options back would not
+        // list it.
+        if (!VARIABLE_NAME.matcher(name).matches()) {
+            return ErrorValue.INVALID_NAME;
+        }
+        VarContext varCtx = VariableMgr.getVarContext(name);

Review Comment:
   [P1] Restrict SET to GetSessionOptions' key namespace
   
   getVarContext accepts names that GET never exposes unchanged: retained 
enable_nereids_dml (REMOVED) and enable_local_exchange (INVISIBLE) are omitted 
entirely; Query_Timeout comes back only as query_timeout; and bare 
enable_shared_scan comes back as experimental_enable_shared_scan. SET returns 
no per-name error in each case, but the ADBC getter performs an exact lookup of 
the key the client requested and therefore reports it missing. Please derive 
accepted option keys from the same visible/display namespace as GET (or 
canonicalize both sides consistently), and cover retained hidden variables plus 
case/prefix spellings; use_v2_rollup only covers a context removed entirely.



##########
fe/fe-core/src/main/java/org/apache/doris/arrowflight/FlightSessionOptions.java:
##########
@@ -0,0 +1,282 @@
+// 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.SetType;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.nereids.util.SqlLiteralUtils;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.QueryState.MysqlStateType;
+import org.apache.doris.qe.SqlModeHelper;
+import org.apache.doris.qe.VarAttrDef;
+import org.apache.doris.qe.VariableMgr;
+import org.apache.doris.qe.VariableMgr.VarContext;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.arrow.flight.SessionOptionValue;
+import org.apache.arrow.flight.SessionOptionValueFactory;
+import org.apache.arrow.flight.SessionOptionValueVisitor;
+import org.apache.arrow.flight.SetSessionOptionsResult;
+import org.apache.arrow.flight.SetSessionOptionsResult.ErrorValue;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * The session options of an Arrow Flight SQL session, as the 
SetSessionOptions and GetSessionOptions
+ * actions see them.
+ *
+ * <p>An option stands for the statement of the session that sets it: {@code 
catalog} is the current
+ * catalog ({@code SWITCH}), {@code schema} the current database ({@code 
USE}), and any other name is
+ * the session variable of that name ({@code SET SESSION}). Those two names 
are what the ADBC Flight
+ * SQL driver puts on the wire for {@code adbc.connection.catalog} and
+ * {@code adbc.connection.db_schema}, and what the Flight SQL JDBC driver 
sends for its
+ * {@code catalog} property; every other option name the drivers pass through 
as given. Setting an
+ * option runs its statement as a command of the session, so it is checked, 
audited and takes effect
+ * exactly as if the client had sent the statement. Reading the options back 
gives what
+ * {@code SHOW VARIABLES} shows, every value as a string: the one 
representation {@code SET} accepts
+ * back, whatever the Java type of the variable behind it.
+ *
+ * <p>The result of setting an option is one of the three {@link ErrorValue}s 
per name and nothing
+ * else, so the reason a value was refused only reaches the frontend log.
+ */
+public final class FlightSessionOptions {
+    private static final Logger LOG = 
LogManager.getLogger(FlightSessionOptions.class);
+
+    /** The current catalog; set with {@code SWITCH}. */
+    public static final String CATALOG = "catalog";
+    /** The current database; set with {@code USE}. */
+    public static final String SCHEMA = "schema";
+
+    // What a session variable is called in SET: VariableMgr looks the name up 
case-insensitively,
+    // and only a name made of these characters is one the parser reads as the 
variable's identifier.
+    private static final Pattern VARIABLE_NAME = 
Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
+
+    private FlightSessionOptions() {
+    }
+
+    /**
+     * Sets the options of one request, each on its own, and returns the error 
of every option that
+     * could not be set; an option absent from the result was set. {@code 
catalog} goes first and
+     * {@code schema} second, since {@code USE} names a database of the 
current catalog; the others
+     * follow in name order, so that a request applies the same way every 
time. Runs as a command of
+     * the session, under its command lock.
+     */
+    public static Map<String, SetSessionOptionsResult.Error> 
set(ConnectContext ctx,
+            Map<String, SessionOptionValue> options) {
+        List<String> names = new ArrayList<>(options.keySet());
+        Collections.sort(names);
+        names.remove(SCHEMA);
+        names.remove(CATALOG);
+        if (options.containsKey(SCHEMA)) {
+            names.add(0, SCHEMA);
+        }
+        if (options.containsKey(CATALOG)) {
+            names.add(0, CATALOG);
+        }
+        Map<String, SetSessionOptionsResult.Error> errors = new 
LinkedHashMap<>();
+        for (String name : names) {
+            ErrorValue error = setOne(ctx, name, options.get(name));
+            if (error != null) {
+                errors.put(name, new SetSessionOptionsResult.Error(error));
+            }
+        }
+        return errors;
+    }
+
+    /** Sets one option and returns why it could not be, or null when it was 
set. */
+    @VisibleForTesting
+    static ErrorValue setOne(ConnectContext ctx, String name, 
SessionOptionValue value) {
+        if (CATALOG.equals(name)) {
+            return switchCatalog(ctx, value);
+        }
+        if (SCHEMA.equals(name)) {
+            return useDatabase(ctx, value);
+        }
+        return setVariable(ctx, name, value);
+    }
+
+    /**
+     * The options of the session: the current catalog, the current database 
(an empty string when
+     * none has been chosen) and every session variable {@code SHOW VARIABLES} 
would list, in its text.
+     */
+    public static Map<String, SessionOptionValue> get(ConnectContext ctx) {
+        Map<String, SessionOptionValue> options = new LinkedHashMap<>();
+        options.put(CATALOG, 
SessionOptionValueFactory.makeSessionOptionValue(ctx.getDefaultCatalog()));
+        String database = ctx.getDatabase();
+        options.put(SCHEMA, 
SessionOptionValueFactory.makeSessionOptionValue(database == null ? "" : 
database));
+        for (List<String> row : VariableMgr.dump(SetType.SESSION, 
ctx.getSessionVariable(), null)) {
+            // A row is name, value, default value, changed.
+            options.put(row.get(0), 
SessionOptionValueFactory.makeSessionOptionValue(row.get(1)));
+        }
+        return options;
+    }
+
+    private static ErrorValue switchCatalog(ConnectContext ctx, 
SessionOptionValue value) {
+        String catalog = value.acceptVisitor(STRING_VALUE);
+        if (catalog == null || catalog.isEmpty()) {
+            return ErrorValue.INVALID_VALUE;
+        }
+        return runStatement(ctx, CATALOG, "SWITCH " + 
quoteIdentifier(catalog), ErrorCode.ERR_UNKNOWN_CATALOG);
+    }
+
+    private static ErrorValue useDatabase(ConnectContext ctx, 
SessionOptionValue value) {
+        String database = value.acceptVisitor(STRING_VALUE);
+        if (database == null || database.isEmpty()) {
+            return ErrorValue.INVALID_VALUE;
+        }
+        return runStatement(ctx, SCHEMA, "USE " + quoteIdentifier(database), 
ErrorCode.ERR_BAD_DB_ERROR);
+    }
+
+    private static ErrorValue setVariable(ConnectContext ctx, String name, 
SessionOptionValue value) {
+        // Only a session variable is a session option. A name SET quietly 
ignores for the sake of
+        // MySQL clients (a removed variable, the MySQL compatibility 
whitelist) is no variable of
+        // this session either: setting it would set nothing, and reading the 
options back would not
+        // list it.
+        if (!VARIABLE_NAME.matcher(name).matches()) {
+            return ErrorValue.INVALID_NAME;
+        }
+        VarContext varCtx = VariableMgr.getVarContext(name);
+        if (varCtx == null) {
+            return ErrorValue.INVALID_NAME;
+        }
+        // SET SESSION refuses a read-only variable and one that exists once 
per frontend rather than
+        // per session (SET GLOBAL sets that one); neither can be set as a 
session option.
+        if ((varCtx.getFlag() & (VarAttrDef.READ_ONLY | VarAttrDef.GLOBAL)) != 
0) {
+            return ErrorValue.ERROR;
+        }
+        // A string is quoted for the session's sql_mode, the mode the 
statement is then parsed under.
+        String literal = 
SqlModeHelper.withSqlMode(ctx.getSessionVariable().getSqlMode(),
+                () -> value.acceptVisitor(SET_LITERAL));
+        if (literal == null) {
+            return ErrorValue.INVALID_VALUE;
+        }
+        // The name and the scope were checked above, so what is left for SET 
to refuse is the value:
+        // its type, its range, or what a variable's own checker makes of it.
+        return runStatement(ctx, name, "SET SESSION " + name + " = " + 
literal, null);
+    }
+
+    /**
+     * Runs the statement an option stands for as a command of the session. A 
failure is
+     * {@code INVALID_VALUE} when the value was refused and {@code ERROR} 
otherwise. For SWITCH and
+     * USE the two are told apart by the error code the statement left on the 
session: an unknown
+     * catalog or database is written there by the command itself, while a 
statement that fails in
+     * validation, e.g. for lack of privilege, leaves the executor's generic 
code. For SET every
+     * failure is the value's (see {@link #setVariable}), so {@code 
refusedValueCode} is null there.
+     */
+    private static ErrorValue runStatement(ConnectContext ctx, String option, 
String statement,
+            ErrorCode refusedValueCode) {
+        try (FlightSqlConnectProcessor processor = new 
FlightSqlConnectProcessor(ctx)) {
+            processor.handleQuery(statement);
+            if (ctx.getState().getStateType() != MysqlStateType.ERR) {
+                return null;
+            }
+            LOG.warn("session option {} of Arrow Flight SQL connection {} 
could not be set, statement: {}, "
+                    + "error code: {}, error message: {}", option, 
ctx.getConnectionId(), statement,
+                    ctx.getState().getErrorCode(), 
ctx.getState().getErrorMessage());
+            if (refusedValueCode == null || refusedValueCode == 
ctx.getState().getErrorCode()) {

Review Comment:
   [P2] Preserve non-value SET failures as ERROR
   
   refusedValueCode is always null for variables, so every ERR from handleQuery 
is returned as INVALID_VALUE even when the name and value are valid. For 
example, a non-root cloud session in OVERDUE state rejects SET SESSION 
query_timeout = 10 before execution; transaction mode and block_sql_ast_names 
can similarly reject SetOptionsCommand. Those are server/session failures, for 
which the Flight enum provides ERROR, not invalid option values. Please 
classify only known conversion/checker failures as INVALID_VALUE and return 
ERROR for the other execution paths, with a non-value failure test.



##########
fe/fe-core/src/main/java/org/apache/doris/arrowflight/FlightSessionOptions.java:
##########
@@ -0,0 +1,282 @@
+// 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.SetType;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.nereids.util.SqlLiteralUtils;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.QueryState.MysqlStateType;
+import org.apache.doris.qe.SqlModeHelper;
+import org.apache.doris.qe.VarAttrDef;
+import org.apache.doris.qe.VariableMgr;
+import org.apache.doris.qe.VariableMgr.VarContext;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.arrow.flight.SessionOptionValue;
+import org.apache.arrow.flight.SessionOptionValueFactory;
+import org.apache.arrow.flight.SessionOptionValueVisitor;
+import org.apache.arrow.flight.SetSessionOptionsResult;
+import org.apache.arrow.flight.SetSessionOptionsResult.ErrorValue;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * The session options of an Arrow Flight SQL session, as the 
SetSessionOptions and GetSessionOptions
+ * actions see them.
+ *
+ * <p>An option stands for the statement of the session that sets it: {@code 
catalog} is the current
+ * catalog ({@code SWITCH}), {@code schema} the current database ({@code 
USE}), and any other name is
+ * the session variable of that name ({@code SET SESSION}). Those two names 
are what the ADBC Flight
+ * SQL driver puts on the wire for {@code adbc.connection.catalog} and
+ * {@code adbc.connection.db_schema}, and what the Flight SQL JDBC driver 
sends for its
+ * {@code catalog} property; every other option name the drivers pass through 
as given. Setting an
+ * option runs its statement as a command of the session, so it is checked, 
audited and takes effect
+ * exactly as if the client had sent the statement. Reading the options back 
gives what
+ * {@code SHOW VARIABLES} shows, every value as a string: the one 
representation {@code SET} accepts
+ * back, whatever the Java type of the variable behind it.
+ *
+ * <p>The result of setting an option is one of the three {@link ErrorValue}s 
per name and nothing
+ * else, so the reason a value was refused only reaches the frontend log.
+ */
+public final class FlightSessionOptions {
+    private static final Logger LOG = 
LogManager.getLogger(FlightSessionOptions.class);
+
+    /** The current catalog; set with {@code SWITCH}. */
+    public static final String CATALOG = "catalog";
+    /** The current database; set with {@code USE}. */
+    public static final String SCHEMA = "schema";
+
+    // What a session variable is called in SET: VariableMgr looks the name up 
case-insensitively,
+    // and only a name made of these characters is one the parser reads as the 
variable's identifier.
+    private static final Pattern VARIABLE_NAME = 
Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
+
+    private FlightSessionOptions() {
+    }
+
+    /**
+     * Sets the options of one request, each on its own, and returns the error 
of every option that
+     * could not be set; an option absent from the result was set. {@code 
catalog} goes first and
+     * {@code schema} second, since {@code USE} names a database of the 
current catalog; the others
+     * follow in name order, so that a request applies the same way every 
time. Runs as a command of
+     * the session, under its command lock.
+     */
+    public static Map<String, SetSessionOptionsResult.Error> 
set(ConnectContext ctx,
+            Map<String, SessionOptionValue> options) {
+        List<String> names = new ArrayList<>(options.keySet());
+        Collections.sort(names);
+        names.remove(SCHEMA);
+        names.remove(CATALOG);
+        if (options.containsKey(SCHEMA)) {
+            names.add(0, SCHEMA);
+        }
+        if (options.containsKey(CATALOG)) {
+            names.add(0, CATALOG);
+        }
+        Map<String, SetSessionOptionsResult.Error> errors = new 
LinkedHashMap<>();
+        for (String name : names) {
+            ErrorValue error = setOne(ctx, name, options.get(name));
+            if (error != null) {
+                errors.put(name, new SetSessionOptionsResult.Error(error));
+            }
+        }
+        return errors;
+    }
+
+    /** Sets one option and returns why it could not be, or null when it was 
set. */
+    @VisibleForTesting
+    static ErrorValue setOne(ConnectContext ctx, String name, 
SessionOptionValue value) {
+        if (CATALOG.equals(name)) {
+            return switchCatalog(ctx, value);
+        }
+        if (SCHEMA.equals(name)) {
+            return useDatabase(ctx, value);
+        }
+        return setVariable(ctx, name, value);
+    }
+
+    /**
+     * The options of the session: the current catalog, the current database 
(an empty string when
+     * none has been chosen) and every session variable {@code SHOW VARIABLES} 
would list, in its text.
+     */
+    public static Map<String, SessionOptionValue> get(ConnectContext ctx) {
+        Map<String, SessionOptionValue> options = new LinkedHashMap<>();
+        options.put(CATALOG, 
SessionOptionValueFactory.makeSessionOptionValue(ctx.getDefaultCatalog()));
+        String database = ctx.getDatabase();
+        options.put(SCHEMA, 
SessionOptionValueFactory.makeSessionOptionValue(database == null ? "" : 
database));
+        for (List<String> row : VariableMgr.dump(SetType.SESSION, 
ctx.getSessionVariable(), null)) {
+            // A row is name, value, default value, changed.
+            options.put(row.get(0), 
SessionOptionValueFactory.makeSessionOptionValue(row.get(1)));
+        }
+        return options;
+    }
+
+    private static ErrorValue switchCatalog(ConnectContext ctx, 
SessionOptionValue value) {
+        String catalog = value.acceptVisitor(STRING_VALUE);
+        if (catalog == null || catalog.isEmpty()) {
+            return ErrorValue.INVALID_VALUE;
+        }
+        return runStatement(ctx, CATALOG, "SWITCH " + 
quoteIdentifier(catalog), ErrorCode.ERR_UNKNOWN_CATALOG);

Review Comment:
   [P2] Classify malformed catalog names as INVALID_VALUE
   
   A catalog value such as bad.name parses safely as one quoted identifier but 
fails SwitchCommand's checkCatalogAllRules with ERR_WRONG_NAME_FORMAT. Because 
this call recognizes only ERR_UNKNOWN_CATALOG as a refused value, the result is 
generic ERROR, even though the failure is entirely caused by the supplied 
catalog option value. Please include catalog name-format validation in the 
INVALID_VALUE path and cover a malformed name alongside a well-formed 
nonexistent catalog.



##########
regression-test/suites/arrow_flight_sql_p0/test_session_options.groovy:
##########
@@ -0,0 +1,194 @@
+// 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 java.sql.DriverManager
+import java.sql.SQLException
+
+// The Flight SQL JDBC driver on the classpath shades Arrow Flight; its 
FlightSqlClient is the
+// same client the ADBC and JDBC drivers speak the session actions with.
+import 
org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.CloseSessionRequest
+import org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.FlightClient
+import 
org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.FlightRuntimeException
+import 
org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.FlightStatusCode
+import 
org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.GetSessionOptionsRequest
+import org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.Location
+import 
org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.NoOpSessionOptionValueVisitor
+import 
org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.SessionOptionValueFactory
+import 
org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.SetSessionOptionsRequest
+import 
org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.sql.FlightSqlClient
+import 
org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.memory.RootAllocator
+
+// The session actions of Arrow Flight SQL on a Doris session: 
SetSessionOptions sets the current
+// catalog (`catalog`, what the ADBC driver sends for adbc.connection.catalog 
and the JDBC driver for
+// its catalog property), the current database (`schema`, 
adbc.connection.db_schema) and session
+// variables (any other name), each answered on its own with INVALID_NAME / 
INVALID_VALUE / ERROR;
+// GetSessionOptions reads them back as SHOW VARIABLES text; CloseSession 
invalidates the bearer
+// token at once.
+//
+// Not in the 'arrow_flight_sql' group on purpose: `sql` stays the MySQL 
control connection, and the
+// session under test is a raw Flight SQL client of its own.
+suite("test_session_options") {
+    String host = context.config.otherConfigs.get("extArrowFlightSqlHost")
+    int port = context.config.otherConfigs.get("extArrowFlightSqlPort") as int
+    String user = context.config.otherConfigs.get("extArrowFlightSqlUser")
+    String password = 
context.config.otherConfigs.get("extArrowFlightSqlPassword")
+    String tableName = "session_options_tbl"

Review Comment:
   [P2] Keep the regression table available for debugging
   
   This is an ordinary single-table case, so the repository test contract 
requires hardcoding session_options_tbl in the SQL rather than routing it 
through tableName; it also requires dropping before setup, not after the test, 
so the final state remains available when debugging a failure. Please remove 
this variable/interpolation and the trailing DROP, keeping only the pre-test 
cleanup.



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