github-actions[bot] commented on code in PR #67966:
URL: https://github.com/apache/doris/pull/67966#discussion_r4013282726
##########
fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java:
##########
@@ -1076,7 +1121,13 @@ public void finalizeQuery() {
// received after unregisterQuery(), causing the instance profile to
be lost, so we should wait
// for the profile before unregisterQuery().
updateProfile(true);
- QeProcessorImpl.INSTANCE.unregisterQuery(context.queryId());
+ QeProcessorImpl.INSTANCE.unregisterQuery(queryId());
+ }
+
+ // The id this query runs under: the context's, until the query is
deferred for Arrow Flight and
+ // the context may move on to another statement before the query is
finalized.
+ private TUniqueId queryId() {
Review Comment:
[P1] Freeze every delayed executor's query ID
This fallback to `context.queryId()` is stale for non-deferred remote
executors too. A Flight request containing two backend-result statements
appends both executors to `returnResultFromRemoteExecutor` and finalizes them
only after the shared context holds the second statement's ID; finalizing the
first then unregisters the second ID, and subsequent calls repeat it, leaving
the first coordinator-map entry, per-user instance count, profile completion,
and query-finish callbacks behind. Capture each executor's
execution/registration ID and use it for every delayed finalization, not only
batch-split deferral, and add a two-remote-statement test that proves both IDs
are removed.
##########
fe/fe-core/src/main/java/org/apache/doris/arrowflight/FlightSessionOptions.java:
##########
@@ -0,0 +1,312 @@
+// 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.AnalysisException;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.util.Util;
+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;
+
+/**
+ * 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 {@code SHOW VARIABLES} lists under 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. The names an option
goes by are exactly
+ * the names that are read back, spelled the same: a client looks an option up
in the result of
+ * GetSessionOptions by the name it set it under. And what is read back can be
set back: the empty
+ * value, Flight's way of unsetting an option, puts a variable back to its
default and the session
+ * back into no database, as it started -- the state GetSessionOptions reports
as an empty
+ * {@code schema}, which sets it too. A session is always in some catalog, so
{@code catalog} has no
+ * empty value.
+ *
+ * <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";
+
+ 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
+ * the session is in none, the value that puts it back there) 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)));
Review Comment:
[P2] Preserve session-option scalar types in GET
`get()` wraps every `SHOW VARIABLES` value with the string factory, even
though this implementation accepts Flight's native integer, double, and boolean
variants in SET. The ADBC Flight SQL driver sends those variants from its typed
setters and requires the same exact variants in its typed getters, so
`SetOptionInt(...query_timeout, 77)` succeeds here but `GetOptionInt` receives
`"77"` and returns `StatusNotFound`; the boolean and double paths fail
likewise. Please return a stable native Flight type for typed variables (or
otherwise preserve a consistent SET/GET type contract) and cover the round trip
through the ADBC typed getters rather than only a raw Flight client that
expects strings.
##########
fe/fe-core/src/main/java/org/apache/doris/arrowflight/protocol/FlightProtocolAdapter.java:
##########
@@ -58,6 +61,25 @@
* arrive on its own thread, and nothing in the transport serializes them.
{@link #runCommand}
* does, so that a session's {@link ConnectContext}, which is not thread-safe,
is only ever used
* by one command at a time.
+ *
+ * <p>Nor does a query end where the frontend's command does. The client pulls
the results from the
+ * backends by itself (DoGet), and the frontend hears nothing of it -- not
when it starts, not when
+ * it is done -- so a query may still be running on the backends while the
session runs its next
+ * command here. Two things of a request therefore outlive the command that
made them, and the
+ * contract for both is that the command stream owns them and nothing else
does: the result a
+ * statement materialized on this frontend, cached on the channel until the
client's DoGet takes it
+ * or the next request drops it; and the executors of queries whose
coordinator must stay alive
+ * until the backends are done with it, the {@link #deferredExecutors}. A
deferred executor is a
+ * closed object from the moment it is deferred: it carries what finalizing it
needs and reads
+ * nothing of the session's live state afterwards, since the session moves on
without it (a session
+ * option's SET, a metadata request, the next request) and since it may be
finalized from a thread
+ * that runs no command of the session (the timeout checker, a token expiry).
It is finalized
+ * exactly once, by whoever takes it out of the list under the list's lock --
the next request
Review Comment:
[P1] Prevent teardown from racing deferred publication
Teardown does not take `commandLock` and the adapter never enters a
permanently closed state. If CloseSession/token expiry unregisters the context
after a batch-split query sets `deferredForArrowFlight` but before
`addDeferredExecutor`, teardown drains an empty list and removes the context,
then the late add still succeeds; `FlightSqlConnectProcessor.close()` skips
that executor, and no pool timeout, next request, or token owner remains to
close its coordinator or unregister the query. This permanently retains the
batch SplitSource and workload-group queue slot; teardown just after
publication can also finalize the coordinator while the request is still
fetching its Arrow schema. Please serialize permanent teardown with the command
lifecycle or atomically tombstone the adapter so queued commands and late
publications fail/finalize, and cover the boundary with latch-based tests.
##########
fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java:
##########
@@ -1076,7 +1121,13 @@ public void finalizeQuery() {
// received after unregisterQuery(), causing the instance profile to
be lost, so we should wait
// for the profile before unregisterQuery().
updateProfile(true);
- QeProcessorImpl.INSTANCE.unregisterQuery(context.queryId());
+ QeProcessorImpl.INSTANCE.unregisterQuery(queryId());
Review Comment:
[P1] Unregister failed replan attempts under their own IDs
This still has no ID with which to clean up an earlier attempt of the same
executor. On Flight, `beforeQuery` selects remote results before
`registerQuery`; if `coordBase.exec()` then throws a cloud error recognized by
`needRetryWithReplan`, `handleQueryWithRetry` rethrows and skips
`finalizeQuery()` because result placement is remote. The outer `queryRetry()`
replaces the context ID, while the failed attempt never reaches either
delayed-executor list, so this eventual unregister can remove only the latest
ID. The old Qe map entry, per-user instance count, profile completion, and
query-finish callbacks remain and repeated recoverable churn can falsely
exhaust `max_query_instances`. Unregister/finalize each registered failed
attempt under its exact ID before replacing it, and inject a post-registration
replan error in a Flight test.
##########
fe/fe-core/src/main/java/org/apache/doris/arrowflight/FlightSessionOptions.java:
##########
@@ -0,0 +1,312 @@
+// 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.AnalysisException;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.util.Util;
+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;
+
+/**
+ * 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 {@code SHOW VARIABLES} lists under 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. The names an option
goes by are exactly
+ * the names that are read back, spelled the same: a client looks an option up
in the result of
+ * GetSessionOptions by the name it set it under. And what is read back can be
set back: the empty
+ * value, Flight's way of unsetting an option, puts a variable back to its
default and the session
+ * back into no database, as it started -- the state GetSessionOptions reports
as an empty
+ * {@code schema}, which sets it too. A session is always in some catalog, so
{@code catalog} has no
+ * empty value.
+ *
+ * <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";
+
+ 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
+ * the session is in none, the value that puts it back there) 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);
+ // A session is always in some catalog, so there is no catalog the
empty value could stand for.
+ if (catalog == null || catalog.isEmpty()) {
+ return ErrorValue.INVALID_VALUE;
+ }
+ // SWITCH checks the name's format before anything else and its error
code would not reach
+ // here (see runStatement); the check is one of the value alone, so it
is made here first.
+ try {
+ Util.checkCatalogAllRules(catalog);
+ } catch (AnalysisException e) {
+ LOG.warn("session option {} of Arrow Flight SQL connection {}
could not be set, catalog name {} "
+ + "is malformed: {}", CATALOG, ctx.getConnectionId(),
catalog, e.getMessage());
+ 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) {
+ return ErrorValue.INVALID_VALUE;
+ }
+ // No database: the state the session started in, which
GetSessionOptions reports as the
+ // empty string and the empty value asks for back (Flight's way of
unsetting an option; what
+ // the ADBC driver sends to erase one). No statement leads there --
there is no USE of
+ // nothing -- so it is not one the session runs, and there is nothing
in it to check.
+ if (database.isEmpty()) {
+ ctx.clearDatabase();
Review Comment:
[P2] Reject schema clears inside active transactions
This direct mutation bypasses the command eligibility checks used by every
non-empty schema/catalog/variable option. A Flight client can run `USE db;
BEGIN`, clear `schema` here, and receive success even though `StmtExecutor`
rejects `USE`, `SWITCH`, and `SET` while `isTxnModel()` is true; subsequent
unqualified DML loses its database, and the client cannot restore it because a
non-empty schema correctly goes through the rejected `USE` path until commit or
rollback. Please make schema erasure share the ordinary state-changing-command
fence and return an option error without mutation when it is prohibited, with a
transaction-mode action test for both the empty value and empty string.
##########
fe/fe-core/src/main/java/org/apache/doris/arrowflight/protocol/FlightProtocolAdapter.java:
##########
@@ -356,49 +380,88 @@ public void addDeferredExecutor(StmtExecutor executor) {
}
}
+ /**
+ * Takes every deferred executor out of the list and finalizes it: the
next request of the
+ * session ({@link #beginRequest}), which stands in for the end of the
previous query's DoGet, and
+ * the session's teardown.
+ */
public void closeDeferredExecutors() {
- List<StmtExecutor> toClose;
+ List<StmtExecutor> taken;
synchronized (deferredExecutors) {
if (deferredExecutors.isEmpty()) {
return;
}
- toClose = new ArrayList<>(deferredExecutors);
+ taken = new ArrayList<>(deferredExecutors);
deferredExecutors.clear();
}
- for (StmtExecutor deferredExecutor : toClose) {
- try {
- deferredExecutor.finalizeArrowFlightQuery();
- } catch (Throwable t) {
- LOG.warn("failed to finalize deferred arrow flight executor",
t);
- }
- }
+ finalizeDeferredExecutors(taken);
}
/**
- * How long, in seconds, a sleeping connection may keep its deferred
executors before the
- * timeout checker finalizes them without killing the connection
- * (Config.arrow_flight_deferred_query_idle_timeout_second). A Flight
client that opens a
- * session per query and never closes it would otherwise pin each deferred
query's query queue
- * slot and query registration until wait_timeout (8h by default). The
bound is never shorter
- * than the execution timeout the deferred query was run with: the client
may still be pulling
- * that query's results from the BE, which still needs the batch split
source the coordinator
- * holds. Returns -1 when the bound is disabled or nothing is deferred.
+ * Takes out of the list the deferred executors whose own bound has passed
by {@code now} and
+ * returns them for the caller to finalize: the timeout checker's half of
the exactly-once rule
+ * in the class comment. Each executor is judged and removed in the same
critical section, by
+ * its own deadline -- when its query started plus {@link
#deferredBoundMs} -- so that of the
+ * executors of one multi-statement request, each with a start and an
execution timeout of its
Review Comment:
[P1] Enforce one result for backend-result statements
This treats multiple remote executors in one request as a valid lifecycle
state, but `finishStatement()` enforces the documented
one-result/last-statement rule only with `FlightSqlChannel.resultNum()`, which
never sees results left on backends. Thus `SELECT ...; SELECT ...` appends both
queries' endpoints and, when their schemas match, the producer advertises them
as partitions of one Flight result so clients concatenate unrelated rows.
`SELECT ...; SET ...` instead returns the synthesized SET status and silently
drops the SELECT endpoints. Track backend-result production as well as
FE-channel results and reject any result-producing statement that is not the
sole final result; cover equal/unequal-schema SELECT pairs and SELECT followed
by SET.
##########
fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java:
##########
@@ -1297,8 +1366,22 @@ private void forwardToMaster() throws Exception {
}
}
+ /**
+ * Whether this statement's profile is reported, decided the first time it
is asked and the
+ * same ever after (see {@link #profileEnabled}): the update that
publishes the profile as
+ * RUNNING and the one that finishes it must agree, whatever the session
variable says by then.
+ */
+ private boolean reportsProfile() {
+ Boolean decided = profileEnabled;
+ if (decided == null) {
+ decided = context.getSessionVariable().enableProfile() &&
isProfileSafeStmt();
Review Comment:
[P2] Enable `Profile` from the same effective decision
In a multi-statement Flight request, a later `SELECT /*+
SET_VAR(enable_profile=true) */ ...` is parsed up front, but the first
executor's finally block restores the shared session value before the later
executor is constructed. That executor therefore creates `Profile(false)`,
whose `isQueryFinished` starts true; analysis then reapplies its SET_VAR and
this line freezes `reportsProfile()` as true, yet `Profile.updateSummary()`
immediately returns for both the RUNNING and final updates. The effectively
enabled query never appears in profiles. Please construct/enable `Profile` from
the same per-statement effective decision (or apply the hint before
construction), and cover actual multi-statement parse/revert/reapply ordering
in the profile matrix.
--
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]