This is an automated email from the ASF dual-hosted git repository.

xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new ce2dc49cb0f Add SQL query option policies: legacy OPTION() cluster 
config and per-request sqlOptionsMode (#19570)
ce2dc49cb0f is described below

commit ce2dc49cb0fbe0148b387c1abec9fb87ad8eb1d5
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Wed Sep 16 17:54:04 2026 -0700

    Add SQL query option policies: legacy OPTION() cluster config and 
per-request sqlOptionsMode (#19570)
    
    - Cluster config `pinot.query.legacy.option.syntax.mode` (`ALLOW` default / 
`IGNORE` / `REJECT`) controls the
      legacy PQL-style `OPTION(k=v)` suffix; applied at broker and controller 
startup
    - Query option `sqlOptionsMode` (`ALLOW` default / `IGNORE` / `REJECT`), 
honored only from the request payload,
      controls SQL-embedded `SET` / `OPTION(...)` options so a gateway can 
guarantee its request options win
    - Controller `/sql` now parses once with the exact payload it forwards, so 
engine and database routing use the
      same options the broker applies (also aligns `database` precedence with 
the broker: SQL over request)
    - Broker parse-catch sites and the `IN_SUBQUERY` parse and execution paths 
preserve `QueryException` codes and
      messages instead of forcing `SQL_PARSING` / `QUERY_EXECUTION`
    - Add `QueryErrorCode.fromThrowable(Throwable, QueryErrorCode)` and use it 
wherever a site only needs the code
    
    Co-authored-by: Claude Fable 5.1 <[email protected]>
---
 .../broker/api/resources/PinotClientRequest.java   |   3 +-
 .../broker/broker/helix/BaseBrokerStarter.java     |   8 +-
 .../apache/pinot/broker/grpc/BrokerGrpcServer.java |   3 +-
 .../requesthandler/BaseBrokerRequestHandler.java   |  13 +-
 .../BaseSingleStageBrokerRequestHandler.java       |  14 ++-
 .../BrokerRequestHandlerDelegate.java              |   5 +-
 .../BaseSingleStageBrokerRequestHandlerTest.java   |  61 +++++++++
 .../utils/config/QueryOptionConfigListener.java    |  78 ++++++++++++
 .../common/utils/config/QueryOptionsUtils.java     |  47 ++++++-
 .../pinot/common/utils/request/RequestUtils.java   |  32 +++--
 .../apache/pinot/sql/parsers/CalciteSqlParser.java |  15 ++-
 .../config/QueryOptionConfigListenerTest.java      |  71 +++++++++++
 .../pinot/sql/parsers/SqlOptionsModeTest.java      | 136 +++++++++++++++++++++
 .../api/resources/PinotQueryResource.java          |  54 +++-----
 .../api/resources/PinotQueryResourceTest.java      |  40 ++++++
 .../core/operator/combine/BaseCombineOperator.java |   8 +-
 .../streaming/BaseStreamingCombineOperator.java    |   9 +-
 .../runtime/executor/OpChainSchedulerService.java  |  21 ++--
 .../apache/pinot/spi/exception/QueryErrorCode.java |   6 +
 .../apache/pinot/spi/utils/CommonConstants.java    |  23 +++-
 20 files changed, 546 insertions(+), 101 deletions(-)

diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotClientRequest.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotClientRequest.java
index 729055c9c4e..7b681e815d5 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotClientRequest.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotClientRequest.java
@@ -667,7 +667,8 @@ public class PinotClientRequest {
     try {
       sqlNodeAndOptions = 
RequestUtils.parseQuery(sqlRequestJson.get(Request.SQL).asText(), 
sqlRequestJson);
     } catch (Exception e) {
-      return new BrokerResponseNative(QueryErrorCode.SQL_PARSING, 
e.getMessage());
+      QueryErrorCode errorCode = QueryErrorCode.fromThrowable(e, 
QueryErrorCode.SQL_PARSING);
+      return new BrokerResponseNative(errorCode, e.getMessage());
     }
     if (forceUseMultiStage) {
       
sqlNodeAndOptions.setExtraOptions(Map.of(Request.QueryOptionKey.USE_MULTISTAGE_ENGINE,
 "true"));
diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
index ebdad1eaf06..a4080f8d960 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
@@ -86,6 +86,7 @@ import org.apache.pinot.common.metrics.MseMetrics;
 import org.apache.pinot.common.utils.PinotAppConfigs;
 import org.apache.pinot.common.utils.ServiceStartableUtils;
 import org.apache.pinot.common.utils.ServiceStatus;
+import org.apache.pinot.common.utils.config.QueryOptionConfigListener;
 import org.apache.pinot.common.utils.config.QueryWorkloadConfigUtils;
 import org.apache.pinot.common.utils.config.TagNameUtils;
 import org.apache.pinot.common.utils.helix.HelixHelper;
@@ -672,6 +673,10 @@ public abstract class BaseBrokerStarter implements 
ServiceStartable {
 
     LOGGER.info("Wiring up cluster config change handler with helix");
     
_spectatorHelixManager.addClusterfigChangeListener(_clusterConfigChangeHandler);
+    // Registered before the query endpoints start so that the cluster configs 
are applied before the first query
+    
_clusterConfigChangeHandler.registerClusterConfigChangeListener(ContinuousJfrStarter.INSTANCE);
+    
_clusterConfigChangeHandler.registerClusterConfigChangeListener(_serverRoutingStatsManager);
+    _clusterConfigChangeHandler.registerClusterConfigChangeListener(new 
QueryOptionConfigListener());
 
     LOGGER.info("Starting broker admin application on: {}", 
ListenerConfigUtil.toString(_listenerConfigs));
     _brokerAdminApplication = createBrokerAdminApp();
@@ -742,9 +747,6 @@ public abstract class BaseBrokerStarter implements 
ServiceStartable {
     _brokerMetrics.addTimedValue(BrokerTimer.STARTUP_SUCCESS_DURATION_MS,
         System.currentTimeMillis() - startTimeMs, TimeUnit.MILLISECONDS);
 
-    
_clusterConfigChangeHandler.registerClusterConfigChangeListener(ContinuousJfrStarter.INSTANCE);
-    
_clusterConfigChangeHandler.registerClusterConfigChangeListener(_serverRoutingStatsManager);
-
     NettyInspector.registerMetrics(_brokerMetrics);
 
     LOGGER.info("Finish starting Pinot broker");
diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/grpc/BrokerGrpcServer.java 
b/pinot-broker/src/main/java/org/apache/pinot/broker/grpc/BrokerGrpcServer.java
index 6d6f4fce789..da8e696c489 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/grpc/BrokerGrpcServer.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/grpc/BrokerGrpcServer.java
@@ -228,10 +228,11 @@ public class BrokerGrpcServer extends 
PinotQueryBrokerGrpc.PinotQueryBrokerImplB
     try {
       sqlNodeAndOptions = RequestUtils.parseQuery(query, requestJsonNode);
     } catch (Exception e) {
+      QueryErrorCode errorCode = QueryErrorCode.fromThrowable(e, 
QueryErrorCode.SQL_PARSING);
       BrokerResponse brokerResponse;
       Broker.BrokerResponse errorBlock;
       try {
-        brokerResponse = new BrokerResponseNative(QueryErrorCode.SQL_PARSING, 
e.getMessage());
+        brokerResponse = new BrokerResponseNative(errorCode, e.getMessage());
         errorBlock = 
Broker.BrokerResponse.newBuilder().setPayload(ByteString.copyFrom(
             brokerResponse.toJsonString().getBytes())).build();
       } catch (IOException ex) {
diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseBrokerRequestHandler.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseBrokerRequestHandler.java
index 742cf545d02..fcc86726946 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseBrokerRequestHandler.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseBrokerRequestHandler.java
@@ -24,7 +24,6 @@ import com.google.common.collect.Maps;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.OptionalLong;
 import java.util.Set;
@@ -50,8 +49,6 @@ import org.apache.pinot.common.metrics.BrokerQueryPhase;
 import org.apache.pinot.common.response.BrokerResponse;
 import org.apache.pinot.common.response.broker.BrokerResponseNative;
 import org.apache.pinot.common.response.broker.QueryProcessingException;
-import org.apache.pinot.common.utils.config.QueryOptionsUtils;
-import 
org.apache.pinot.common.utils.config.QueryOptionsUtils.SqlQueryOptionValidationMode;
 import org.apache.pinot.common.utils.request.RequestUtils;
 import org.apache.pinot.core.auth.Actions;
 import org.apache.pinot.core.auth.TargetType;
@@ -137,11 +134,6 @@ public abstract class BaseBrokerRequestHandler implements 
BrokerRequestHandler {
         Broker.DEFAULT_BROKER_ENABLE_QUERY_CANCELLATION);
     _enableAutoRewriteAggregationType =
         
config.getProperty(Broker.CONFIG_OF_BROKER_QUERY_ENABLE_AUTO_REWRITE_AGGREGATION_TYPE);
-    // Process-wide static because the SQL parser has no access to the broker 
config. Set here rather
-    // than parsed per query; a broker restart is needed to pick up a config 
change.
-    
QueryOptionsUtils.setSqlQueryOptionValidationMode(SqlQueryOptionValidationMode.valueOf(
-        
config.getProperty(Broker.CONFIG_OF_BROKER_QUERY_OPTION_VALIDATION_MODE,
-            
Broker.DEFAULT_BROKER_QUERY_OPTION_VALIDATION_MODE).trim().toUpperCase(Locale.ROOT)));
     if (_enableQueryCancellation) {
       _queriesById = new ConcurrentHashMap<>();
       _clientQueryIds = new ConcurrentHashMap<>();
@@ -197,8 +189,9 @@ public abstract class BaseBrokerRequestHandler implements 
BrokerRequestHandler {
         sqlNodeAndOptions = RequestUtils.parseQuery(query, request);
       } catch (Exception e) {
         // Do not log or emit metric here because it is pure user error
-        requestContext.setErrorCode(QueryErrorCode.SQL_PARSING);
-        return new BrokerResponseNative(QueryErrorCode.SQL_PARSING, 
e.getMessage());
+        QueryErrorCode errorCode = QueryErrorCode.fromThrowable(e, 
QueryErrorCode.SQL_PARSING);
+        requestContext.setErrorCode(errorCode);
+        return new BrokerResponseNative(errorCode, e.getMessage());
       }
     }
 
diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandler.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandler.java
index a099861f951..d93ffc1ff70 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandler.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandler.java
@@ -1183,9 +1183,9 @@ public abstract class BaseSingleStageBrokerRequestHandler 
extends BaseBrokerRequ
     } catch (Exception e) {
       LOGGER.info("Caught exception while handling the subquery in request {}: 
{}, {}", requestId,
           _queryLogger.redactQuery(query, 
requestContext.getQueryFingerprint()), e.getMessage());
-      requestContext.setErrorCode(QueryErrorCode.QUERY_EXECUTION);
-      return new CompileResult(
-          new BrokerResponseNative(QueryErrorCode.QUERY_EXECUTION, 
e.getMessage()));
+      QueryErrorCode errorCode = QueryErrorCode.fromThrowable(e, 
QueryErrorCode.QUERY_EXECUTION);
+      requestContext.setErrorCode(errorCode);
+      return new CompileResult(new BrokerResponseNative(errorCode, 
e.getMessage()));
     }
 
     boolean ignoreCase = _tableCache.isIgnoreCase();
@@ -1640,8 +1640,8 @@ public abstract class BaseSingleStageBrokerRequestHandler 
extends BaseBrokerRequ
         sqlNodeAndOptions = RequestUtils.parseQuery(subquery, jsonRequest);
       } catch (Exception e) {
         // Do not log or emit metric here because it is pure user error
-        requestContext.setErrorCode(QueryErrorCode.SQL_PARSING);
-        throw new RuntimeException("Failed to parse subquery: " + subquery, e);
+        QueryErrorCode errorCode = QueryErrorCode.fromThrowable(e, 
QueryErrorCode.SQL_PARSING);
+        throw new QueryException(errorCode, "Failed to parse subquery: " + 
e.getMessage(), e);
       }
 
       // Add null handling option from broker config only if there is no 
override in the query
@@ -1665,7 +1665,9 @@ public abstract class BaseSingleStageBrokerRequestHandler 
extends BaseBrokerRequ
           doHandleRequest(requestId, subquery, sqlNodeAndOptions, jsonRequest, 
requesterIdentity, requestContext,
               httpHeaders, accessControl, false);
       if (response.getExceptionsSize() != 0) {
-        throw new RuntimeException("Caught exception while executing subquery: 
" + subquery);
+        QueryProcessingException exception = response.getExceptions().get(0);
+        throw new 
QueryException(QueryErrorCode.fromErrorCode(exception.getErrorCode()),
+            "Caught exception while executing subquery: " + subquery + ": " + 
exception.getMessage());
       }
       String serializedIdSet = (String) 
response.getResultTable().getRows().get(0)[0];
       function.setOperator(IN_ID_SET);
diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BrokerRequestHandlerDelegate.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BrokerRequestHandlerDelegate.java
index ee0586f2815..07a987ae801 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BrokerRequestHandlerDelegate.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BrokerRequestHandlerDelegate.java
@@ -119,8 +119,9 @@ public class BrokerRequestHandlerDelegate implements 
BrokerRequestHandler {
         sqlNodeAndOptions = 
RequestUtils.parseQuery(request.get(Request.SQL).asText(), request);
       } catch (Exception e) {
         // Do not log or emit metric here because it is pure user error
-        requestContext.setErrorCode(QueryErrorCode.SQL_PARSING);
-        return new BrokerResponseNative(QueryErrorCode.SQL_PARSING, 
e.getMessage());
+        QueryErrorCode errorCode = QueryErrorCode.fromThrowable(e, 
QueryErrorCode.SQL_PARSING);
+        requestContext.setErrorCode(errorCode);
+        return new BrokerResponseNative(errorCode, e.getMessage());
       }
     }
 
diff --git 
a/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandlerTest.java
 
b/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandlerTest.java
index ae661c0107a..6c26207b9ae 100644
--- 
a/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandlerTest.java
+++ 
b/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandlerTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.pinot.broker.requesthandler;
 
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.sun.net.httpserver.HttpServer;
 import java.net.InetSocketAddress;
 import java.util.HashMap;
@@ -31,6 +32,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
+import javax.annotation.Nullable;
 import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
 import org.apache.helix.model.InstanceConfig;
 import org.apache.pinot.broker.api.AccessControl;
@@ -46,6 +48,7 @@ import org.apache.pinot.common.request.Function;
 import org.apache.pinot.common.request.PinotQuery;
 import org.apache.pinot.common.response.BrokerResponse;
 import org.apache.pinot.common.response.broker.BrokerResponseNative;
+import org.apache.pinot.common.response.broker.QueryProcessingException;
 import org.apache.pinot.core.routing.RoutingTable;
 import org.apache.pinot.core.routing.SegmentsToQuery;
 import org.apache.pinot.core.routing.TableRouteInfo;
@@ -73,11 +76,15 @@ import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.env.PinotConfiguration;
 import 
org.apache.pinot.spi.eventlistener.query.BrokerQueryEventListenerFactory;
 import org.apache.pinot.spi.exception.BadQueryRequestException;
+import org.apache.pinot.spi.exception.QueryErrorCode;
 import org.apache.pinot.spi.trace.LoggerConstants;
 import org.apache.pinot.spi.trace.RequestContext;
+import org.apache.pinot.spi.trace.RequestScope;
+import org.apache.pinot.spi.trace.Tracing;
 import org.apache.pinot.spi.utils.CommonConstants;
 import org.apache.pinot.spi.utils.CommonConstants.Broker;
 import org.apache.pinot.spi.utils.CommonConstants.Query.Range;
+import org.apache.pinot.spi.utils.JsonUtils;
 import org.apache.pinot.spi.utils.builder.TableNameBuilder;
 import org.apache.pinot.sql.FilterKind;
 import org.apache.pinot.sql.parsers.CalciteSqlParser;
@@ -548,6 +555,60 @@ public class BaseSingleStageBrokerRequestHandlerTest {
     Assert.assertEquals(operands.get(1).getLiteral().getStringValue(), 
expectedRange);
   }
 
+  @Test
+  public void testRejectedSqlOptionsInSubqueryPreserveErrorCode()
+      throws Exception {
+    BaseSingleStageBrokerRequestHandler handler = 
createHybridHandlerWithTimeBoundary(new AtomicReference<>());
+
+    BrokerResponseNative response = handleRequest(handler, "SELECT * FROM 
myTable WHERE IN_SUBQUERY(created_15min, "
+        + "'SET timeoutMs = ''1''; SELECT ID_SET(created_15min) FROM myTable') 
= 1", "sqlOptionsMode=reject");
+    assertSingleException(response, QueryErrorCode.QUERY_VALIDATION, "Query 
options are not allowed in the SQL");
+  }
+
+  @Test
+  public void testRejectedSqlOptionsInNestedSubqueryPreserveErrorCode()
+      throws Exception {
+    BaseSingleStageBrokerRequestHandler handler = 
createHybridHandlerWithTimeBoundary(new AtomicReference<>());
+
+    // The SET sits two IN_SUBQUERY levels down, so the rejection has to 
survive the inner subquery's response
+    BrokerResponseNative response = handleRequest(handler,
+        "SELECT * FROM myTable WHERE IN_SUBQUERY(created_15min, 'SELECT 
ID_SET(created_15min) FROM myTable WHERE "
+            + "IN_SUBQUERY(created_15min, ''SET timeoutMs = 1; SELECT 
ID_SET(created_15min) FROM myTable'') "
+            + "= 1') = 1", "sqlOptionsMode=reject");
+    assertSingleException(response, QueryErrorCode.QUERY_VALIDATION, "Query 
options are not allowed in the SQL");
+  }
+
+  @Test
+  public void testSubqueryParseFailurePreservesErrorCode()
+      throws Exception {
+    BaseSingleStageBrokerRequestHandler handler = 
createHybridHandlerWithTimeBoundary(new AtomicReference<>());
+
+    BrokerResponseNative response = handleRequest(handler,
+        "SELECT * FROM myTable WHERE IN_SUBQUERY(created_15min, 'SELECT 
ID_SET(created_15min) FROM') = 1", null);
+    assertSingleException(response, QueryErrorCode.SQL_PARSING, "Failed to 
parse subquery");
+  }
+
+  private static BrokerResponseNative 
handleRequest(BaseSingleStageBrokerRequestHandler handler, String sql,
+      @Nullable String queryOptions)
+      throws Exception {
+    ObjectNode request = JsonUtils.newObjectNode().put(Broker.Request.SQL, 
sql);
+    if (queryOptions != null) {
+      request.put(Broker.Request.QUERY_OPTIONS, queryOptions);
+    }
+    try (RequestScope requestContext = 
Tracing.getTracer().createRequestScope()) {
+      requestContext.setRequestArrivalTimeMillis(System.currentTimeMillis());
+      return (BrokerResponseNative) handler.handleRequest(request, null, null, 
requestContext, null);
+    }
+  }
+
+  private static void assertSingleException(BrokerResponseNative response, 
QueryErrorCode errorCode,
+      String messagePart) {
+    Assert.assertEquals(response.getExceptions().size(), 1, 
response.toString());
+    QueryProcessingException exception = response.getExceptions().get(0);
+    Assert.assertEquals(exception.getErrorCode(), errorCode.getId());
+    Assert.assertTrue(exception.getMessage().contains(messagePart), 
exception.getMessage());
+  }
+
   @Test
   public void testTimeBoundaryMergesWithBetween()
       throws Exception {
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionConfigListener.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionConfigListener.java
new file mode 100644
index 00000000000..8203a64d8ec
--- /dev/null
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionConfigListener.java
@@ -0,0 +1,78 @@
+/**
+ * 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.pinot.common.utils.config;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import org.apache.pinot.common.utils.config.QueryOptionsUtils.SqlOptionsMode;
+import 
org.apache.pinot.common.utils.config.QueryOptionsUtils.SqlQueryOptionValidationMode;
+import org.apache.pinot.spi.config.provider.PinotClusterConfigChangeListener;
+import org.apache.pinot.spi.utils.CommonConstants.Broker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/// Applies the `pinot.broker.query.option.*` cluster configs to the 
process-wide SQL query option policies held by
+/// [QueryOptionsUtils]: 
[Broker#CONFIG_OF_BROKER_QUERY_OPTION_VALIDATION_MODE] and
+/// [Broker#CONFIG_OF_BROKER_QUERY_OPTION_LEGACY_SYNTAX_MODE]. Changes apply 
live, and a removed key restores the
+/// default; the broker instance config is not consulted. An invalid value is 
logged and the current mode kept, so
+/// that a typo can neither break query parsing nor the other listeners.
+public class QueryOptionConfigListener implements 
PinotClusterConfigChangeListener {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(QueryOptionConfigListener.class);
+
+  @Override
+  public void onChange(Set<String> changedConfigs, Map<String, String> 
clusterConfigs) {
+    if 
(changedConfigs.contains(Broker.CONFIG_OF_BROKER_QUERY_OPTION_VALIDATION_MODE)) 
{
+      applyValidationMode(clusterConfigs);
+    }
+    if 
(changedConfigs.contains(Broker.CONFIG_OF_BROKER_QUERY_OPTION_LEGACY_SYNTAX_MODE))
 {
+      applyLegacySyntaxMode(clusterConfigs);
+    }
+  }
+
+  private static void applyValidationMode(Map<String, String> clusterConfigs) {
+    apply(clusterConfigs, Broker.CONFIG_OF_BROKER_QUERY_OPTION_VALIDATION_MODE,
+        Broker.DEFAULT_BROKER_QUERY_OPTION_VALIDATION_MODE, 
SqlQueryOptionValidationMode.class,
+        QueryOptionsUtils.getSqlQueryOptionValidationMode(), 
QueryOptionsUtils::setSqlQueryOptionValidationMode);
+  }
+
+  private static void applyLegacySyntaxMode(Map<String, String> 
clusterConfigs) {
+    apply(clusterConfigs, 
Broker.CONFIG_OF_BROKER_QUERY_OPTION_LEGACY_SYNTAX_MODE,
+        Broker.DEFAULT_BROKER_QUERY_OPTION_LEGACY_SYNTAX_MODE, 
SqlOptionsMode.class,
+        QueryOptionsUtils.getLegacyOptionSyntaxMode(), 
QueryOptionsUtils::setLegacyOptionSyntaxMode);
+  }
+
+  private static <E extends Enum<E>> void apply(Map<String, String> 
clusterConfigs, String key, String defaultValue,
+      Class<E> enumClass, E currentValue, Consumer<E> setter) {
+    String value = clusterConfigs.getOrDefault(key, defaultValue);
+    E newValue;
+    try {
+      newValue = Enum.valueOf(enumClass, 
value.trim().toUpperCase(Locale.ROOT));
+    } catch (IllegalArgumentException e) {
+      LOGGER.error("Ignoring invalid value '{}' for cluster config: {}, 
keeping: {}", value, key, currentValue);
+      return;
+    }
+    if (newValue != currentValue) {
+      setter.accept(newValue);
+      LOGGER.info("Updated cluster config: {} from {} to {}", key, 
currentValue, newValue);
+    }
+  }
+}
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
index 8a9bdb8639b..f93a4597230 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
@@ -21,9 +21,11 @@ package org.apache.pinot.common.utils.config;
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -31,6 +33,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import javax.annotation.Nullable;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.pinot.spi.config.table.FieldConfig;
+import org.apache.pinot.spi.exception.QueryErrorCode;
 import org.apache.pinot.spi.utils.CommonConstants;
 import 
org.apache.pinot.spi.utils.CommonConstants.Broker.Request.QueryOptionKey;
 import 
org.apache.pinot.spi.utils.CommonConstants.MultiStageQueryRunner.JoinOverFlowMode;
@@ -56,6 +59,19 @@ public class QueryOptionsUtils {
     REJECT
   }
 
+  /// How query options embedded in the SQL text are handled, as opposed to 
options passed through the request
+  /// payload. Shared by the brokers' legacy `OPTION(...)` syntax policy
+  /// 
([CommonConstants.Broker#CONFIG_OF_BROKER_QUERY_OPTION_LEGACY_SYNTAX_MODE]) and 
the per-request
+  /// [QueryOptionKey#SQL_OPTIONS_MODE].
+  public enum SqlOptionsMode {
+    /// The options are applied. Default.
+    ALLOW,
+    /// The options are dropped.
+    IGNORE,
+    /// The statement fails.
+    REJECT
+  }
+
   private static final Logger LOGGER = 
LoggerFactory.getLogger(QueryOptionsUtils.class);
 
   private static final Map<String, String> CONFIG_RESOLVER;
@@ -83,6 +99,7 @@ public class QueryOptionsUtils {
 
   private static volatile SqlQueryOptionValidationMode 
_sqlQueryOptionValidationMode =
       SqlQueryOptionValidationMode.NONE;
+  private static volatile SqlOptionsMode _legacyOptionSyntaxMode = 
SqlOptionsMode.ALLOW;
 
   static {
     // this is a bit hacky, but lots of the code depends directly on usage of
@@ -140,13 +157,37 @@ public class QueryOptionsUtils {
     return _sqlQueryOptionValidationMode;
   }
 
-  /// Sets the validation mode applied to SQL-supplied query option keys. 
Called once per process at
-  /// broker startup from 
[CommonConstants.Broker#CONFIG_OF_BROKER_QUERY_OPTION_VALIDATION_MODE], and
-  /// by tests to restore [SqlQueryOptionValidationMode#NONE].
+  /// Sets the validation mode applied to SQL-supplied query option keys, see
+  /// [CommonConstants.Broker#CONFIG_OF_BROKER_QUERY_OPTION_VALIDATION_MODE].
   public static void 
setSqlQueryOptionValidationMode(SqlQueryOptionValidationMode mode) {
     _sqlQueryOptionValidationMode = mode;
   }
 
+  public static SqlOptionsMode getLegacyOptionSyntaxMode() {
+    return _legacyOptionSyntaxMode;
+  }
+
+  /// Sets how the legacy `OPTION(...)` query option suffix is handled, see
+  /// 
[CommonConstants.Broker#CONFIG_OF_BROKER_QUERY_OPTION_LEGACY_SYNTAX_MODE].
+  public static void setLegacyOptionSyntaxMode(SqlOptionsMode mode) {
+    _legacyOptionSyntaxMode = mode;
+  }
+
+  /// Returns the per-request [QueryOptionKey#SQL_OPTIONS_MODE], `ALLOW` when 
absent. Fails with
+  /// [QueryErrorCode#QUERY_VALIDATION] when the value is not a 
[SqlOptionsMode].
+  public static SqlOptionsMode getSqlOptionsMode(Map<String, String> 
queryOptions) {
+    String mode = queryOptions.get(QueryOptionKey.SQL_OPTIONS_MODE);
+    if (mode == null) {
+      return SqlOptionsMode.ALLOW;
+    }
+    try {
+      return SqlOptionsMode.valueOf(mode.trim().toUpperCase(Locale.ROOT));
+    } catch (IllegalArgumentException e) {
+      throw QueryErrorCode.QUERY_VALIDATION.asException("Invalid value '" + 
mode + "' for query option '"
+          + QueryOptionKey.SQL_OPTIONS_MODE + "', must be one of " + 
Arrays.toString(SqlOptionsMode.values()));
+    }
+  }
+
   /// Registers an option key that [#validateSqlQueryOptions] accepts in 
addition to the keys
   /// declared on [QueryOptionKey]. Meant for plugins that read custom options 
off the
   /// `BrokerRequest`; call it from plugin init. Case-insensitive, thread safe 
and idempotent.
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/utils/request/RequestUtils.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/utils/request/RequestUtils.java
index 0f5a3b4d0c6..a120893c867 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/utils/request/RequestUtils.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/utils/request/RequestUtils.java
@@ -55,6 +55,9 @@ import org.apache.pinot.common.request.Identifier;
 import org.apache.pinot.common.request.Literal;
 import org.apache.pinot.common.request.PinotQuery;
 import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.common.utils.config.QueryOptionsUtils;
+import org.apache.pinot.common.utils.config.QueryOptionsUtils.SqlOptionsMode;
+import org.apache.pinot.spi.exception.QueryErrorCode;
 import org.apache.pinot.spi.utils.BigDecimalUtils;
 import org.apache.pinot.spi.utils.BytesUtils;
 import org.apache.pinot.spi.utils.CommonConstants;
@@ -97,21 +100,34 @@ public class RequestUtils {
     return sqlNodeAndOptions;
   }
 
-  /// Sets extra options for the given query.
+  /// Merges the request payload options (`queryOptions` and `trace`) into the 
options parsed from the SQL. The SQL
+  /// options take precedence, unless the request sets 
[Request.QueryOptionKey#SQL_OPTIONS_MODE] to `IGNORE` (the SQL
+  /// options are dropped) or `REJECT` (the query fails when it carries any).
   @VisibleForTesting
   public static void setOptions(SqlNodeAndOptions sqlNodeAndOptions, JsonNode 
jsonRequest) {
-    Map<String, String> queryOptions = new HashMap<>();
+    Map<String, String> requestOptions = new HashMap<>();
     if (jsonRequest.has(Request.QUERY_OPTIONS)) {
-      
queryOptions.putAll(getOptionsFromString(jsonRequest.get(Request.QUERY_OPTIONS).asText()));
+      
requestOptions.putAll(getOptionsFromString(jsonRequest.get(Request.QUERY_OPTIONS).asText()));
     }
     if (jsonRequest.has(Request.TRACE) && 
jsonRequest.get(Request.TRACE).asBoolean()) {
-      queryOptions.put(Request.TRACE, "true");
+      requestOptions.put(Request.TRACE, "true");
     }
-    if (!queryOptions.isEmpty()) {
-      LOGGER.debug("Query options are set to: {}", queryOptions);
+    if (requestOptions.isEmpty()) {
+      return;
+    }
+    LOGGER.debug("Query options are set to: {}", requestOptions);
+    requestOptions = 
QueryOptionsUtils.resolveCaseInsensitiveOptions(requestOptions);
+    SqlOptionsMode sqlOptionsMode = 
QueryOptionsUtils.getSqlOptionsMode(requestOptions);
+    Map<String, String> sqlOptions = sqlNodeAndOptions.getOptions();
+    if (sqlOptionsMode != SqlOptionsMode.ALLOW && !sqlOptions.isEmpty()) {
+      if (sqlOptionsMode == SqlOptionsMode.REJECT) {
+        throw QueryErrorCode.QUERY_VALIDATION.asException(
+            "Query options are not allowed in the SQL for this request, found: 
" + sqlOptions.keySet());
+      }
+      sqlOptions.clear();
     }
-    // Setting all query options back into SqlNodeAndOptions. The above 
ordering matters due to priority overwrite rule
-    sqlNodeAndOptions.setExtraOptions(queryOptions);
+    // SQL options take precedence over request options
+    requestOptions.forEach(sqlOptions::putIfAbsent);
   }
 
   public static Expression getIdentifierExpression(String identifier) {
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/sql/parsers/CalciteSqlParser.java 
b/pinot-common/src/main/java/org/apache/pinot/sql/parsers/CalciteSqlParser.java
index d82c1a96922..ed16a9d85ad 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/sql/parsers/CalciteSqlParser.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/sql/parsers/CalciteSqlParser.java
@@ -68,6 +68,7 @@ import org.apache.pinot.common.request.JoinType;
 import org.apache.pinot.common.request.PinotQuery;
 import org.apache.pinot.common.request.context.GroupingSets;
 import org.apache.pinot.common.utils.config.QueryOptionsUtils;
+import org.apache.pinot.common.utils.config.QueryOptionsUtils.SqlOptionsMode;
 import org.apache.pinot.common.utils.request.RequestUtils;
 import org.apache.pinot.segment.spi.AggregationFunctionType;
 import org.apache.pinot.sql.FilterKind;
@@ -121,9 +122,19 @@ public class CalciteSqlParser {
     sql = ParserUtils.sanitizeSql(sql);
 
     // extract and remove OPTIONS string
-    List<String> options = extractOptionsFromSql(sql);
-    if (!options.isEmpty()) {
+    List<String> options = List.of();
+    SqlOptionsMode legacyOptionSyntaxMode = 
QueryOptionsUtils.getLegacyOptionSyntaxMode();
+    if (legacyOptionSyntaxMode == SqlOptionsMode.IGNORE) {
       sql = removeOptionsFromSql(sql);
+    } else {
+      options = extractOptionsFromSql(sql);
+      if (!options.isEmpty()) {
+        if (legacyOptionSyntaxMode == SqlOptionsMode.REJECT) {
+          throw new SqlCompilationException("Legacy OPTION(...) query options 
are not allowed on this cluster, use "
+              + "'SET <key> = <value>;' statements instead: " + options);
+        }
+        sql = removeOptionsFromSql(sql);
+      }
     }
 
     try (StringReader inStream = new StringReader(sql)) {
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/utils/config/QueryOptionConfigListenerTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/utils/config/QueryOptionConfigListenerTest.java
new file mode 100644
index 00000000000..73cae4fea65
--- /dev/null
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/utils/config/QueryOptionConfigListenerTest.java
@@ -0,0 +1,71 @@
+/**
+ * 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.pinot.common.utils.config;
+
+import java.util.Map;
+import java.util.Set;
+import org.apache.pinot.common.utils.config.QueryOptionsUtils.SqlOptionsMode;
+import 
org.apache.pinot.common.utils.config.QueryOptionsUtils.SqlQueryOptionValidationMode;
+import org.apache.pinot.spi.utils.CommonConstants.Broker;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+
+
+public class QueryOptionConfigListenerTest {
+  private static final String VALIDATION_MODE_KEY = 
Broker.CONFIG_OF_BROKER_QUERY_OPTION_VALIDATION_MODE;
+  private static final String LEGACY_SYNTAX_MODE_KEY = 
Broker.CONFIG_OF_BROKER_QUERY_OPTION_LEGACY_SYNTAX_MODE;
+
+  @BeforeMethod
+  @AfterMethod
+  public void resetModes() {
+    
QueryOptionsUtils.setSqlQueryOptionValidationMode(SqlQueryOptionValidationMode.NONE);
+    QueryOptionsUtils.setLegacyOptionSyntaxMode(SqlOptionsMode.ALLOW);
+  }
+
+  @Test
+  public void testAppliesClusterConfigChanges() {
+    QueryOptionConfigListener listener = new QueryOptionConfigListener();
+
+    listener.onChange(Set.of(VALIDATION_MODE_KEY, LEGACY_SYNTAX_MODE_KEY),
+        Map.of(VALIDATION_MODE_KEY, "warn", LEGACY_SYNTAX_MODE_KEY, " reject 
"));
+    assertEquals(QueryOptionsUtils.getSqlQueryOptionValidationMode(), 
SqlQueryOptionValidationMode.WARN);
+    assertEquals(QueryOptionsUtils.getLegacyOptionSyntaxMode(), 
SqlOptionsMode.REJECT);
+
+    // Unrelated changes are ignored
+    listener.onChange(Set.of("otherKey"), Map.of("otherKey", "value"));
+    assertEquals(QueryOptionsUtils.getSqlQueryOptionValidationMode(), 
SqlQueryOptionValidationMode.WARN);
+    assertEquals(QueryOptionsUtils.getLegacyOptionSyntaxMode(), 
SqlOptionsMode.REJECT);
+
+    // Removing the keys restores the defaults
+    listener.onChange(Set.of(VALIDATION_MODE_KEY, LEGACY_SYNTAX_MODE_KEY), 
Map.of());
+    assertEquals(QueryOptionsUtils.getSqlQueryOptionValidationMode(), 
SqlQueryOptionValidationMode.NONE);
+    assertEquals(QueryOptionsUtils.getLegacyOptionSyntaxMode(), 
SqlOptionsMode.ALLOW);
+  }
+
+  @Test
+  public void testInvalidValueKeepsCurrentMode() {
+    QueryOptionConfigListener listener = new QueryOptionConfigListener();
+    listener.onChange(Set.of(LEGACY_SYNTAX_MODE_KEY), 
Map.of(LEGACY_SYNTAX_MODE_KEY, "reject"));
+    listener.onChange(Set.of(LEGACY_SYNTAX_MODE_KEY), 
Map.of(LEGACY_SYNTAX_MODE_KEY, "bogus"));
+    assertEquals(QueryOptionsUtils.getLegacyOptionSyntaxMode(), 
SqlOptionsMode.REJECT);
+  }
+}
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/sql/parsers/SqlOptionsModeTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/sql/parsers/SqlOptionsModeTest.java
new file mode 100644
index 00000000000..1fd061b69e8
--- /dev/null
+++ 
b/pinot-common/src/test/java/org/apache/pinot/sql/parsers/SqlOptionsModeTest.java
@@ -0,0 +1,136 @@
+/**
+ * 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.pinot.sql.parsers;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.pinot.common.utils.config.QueryOptionsUtils;
+import org.apache.pinot.common.utils.config.QueryOptionsUtils.SqlOptionsMode;
+import org.apache.pinot.common.utils.request.RequestUtils;
+import org.apache.pinot.spi.exception.QueryErrorCode;
+import org.apache.pinot.spi.exception.QueryException;
+import org.apache.pinot.spi.utils.CommonConstants.Broker.Request;
+import org.apache.pinot.spi.utils.JsonUtils;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.expectThrows;
+
+
+/// Covers the two policies for query options embedded in the SQL text: the 
legacy `OPTION(...)` syntax mode and the
+/// per-request [Request.QueryOptionKey#SQL_OPTIONS_MODE].
+public class SqlOptionsModeTest {
+
+  @AfterMethod
+  public void resetLegacyOptionSyntaxMode() {
+    QueryOptionsUtils.setLegacyOptionSyntaxMode(SqlOptionsMode.ALLOW);
+  }
+
+  @Test
+  public void testLegacyOptionSyntaxAllowedByDefault() {
+    assertEquals(QueryOptionsUtils.getLegacyOptionSyntaxMode(), 
SqlOptionsMode.ALLOW);
+    assertEquals(sqlOptionsOf("select * from vegetables 
OPTION(timeoutMs=1000)"), Map.of("timeoutMs", "1000"));
+  }
+
+  @Test
+  public void testIgnoredLegacyOptionSyntaxIsStrippedAndDropped() {
+    QueryOptionsUtils.setLegacyOptionSyntaxMode(SqlOptionsMode.IGNORE);
+    assertEquals(sqlOptionsOf("select * from vegetables 
OPTION(timeoutMs=1000)"), Map.of());
+    // SET statements are unaffected
+    assertEquals(sqlOptionsOf("SET timeoutMs='2000'; select * from vegetables 
OPTION(timeoutMs=1000, skipUpsert=true)"),
+        Map.of("timeoutMs", "2000"));
+  }
+
+  @Test
+  public void testRejectedLegacyOptionSyntaxFailsEveryStatementType() {
+    QueryOptionsUtils.setLegacyOptionSyntaxMode(SqlOptionsMode.REJECT);
+    for (String sql : List.of("select * from vegetables 
OPTION(timeoutMs=1000)",
+        "INSERT INTO db.tbl FROM FILE 'file:///tmp/file1' 
OPTION(taskName=myTask-1)")) {
+      SqlCompilationException e = expectThrows(SqlCompilationException.class, 
() -> sqlOptionsOf(sql));
+      assertTrue(e.getMessage().contains("OPTION(...)"), e.getMessage());
+      assertTrue(e.getMessage().contains("SET"), e.getMessage());
+    }
+    // SET statements are unaffected
+    assertEquals(sqlOptionsOf("SET timeoutMs='2000'; select * from 
vegetables"), Map.of("timeoutMs", "2000"));
+    assertEquals(sqlOptionsOf("SET taskName='myTask-1'; INSERT INTO db.tbl 
FROM FILE 'file:///tmp/file1'"),
+        Map.of("taskName", "myTask-1"));
+  }
+
+  @Test
+  public void testSqlOptionsAllowedByDefaultWithPrecedenceOverRequestOptions() 
{
+    assertEquals(parse("SET timeoutMs='1000'; select * from vegetables", 
"timeoutMs=2000;maxExecutionThreads=4"),
+        Map.of("timeoutMs", "1000", "maxExecutionThreads", "4"));
+  }
+
+  @Test
+  public void testIgnoredSqlOptionsAreDropped() {
+    assertEquals(parse("SET timeoutMs='1000'; select * from vegetables 
OPTION(skipUpsert=true)",
+        "timeoutMs=2000;sqlOptionsMode=ignore"), Map.of("timeoutMs", "2000", 
"sqlOptionsMode", "ignore"));
+  }
+
+  @Test
+  public void testRejectedSqlOptionsFailTheQuery() {
+    QueryException e = expectThrows(QueryException.class,
+        () -> parse("SET timeoutMs='1000'; select * from vegetables 
OPTION(skipUpsert=true)", "sqlOptionsMode=reject"));
+    assertEquals(e.getErrorCode(), QueryErrorCode.QUERY_VALIDATION);
+    assertTrue(e.getMessage().contains("timeoutMs"), e.getMessage());
+    assertTrue(e.getMessage().contains("skipUpsert"), e.getMessage());
+    // A query without SQL options is unaffected
+    assertEquals(parse("select * from vegetables", 
"timeoutMs=2000;sqlOptionsMode=reject"),
+        Map.of("timeoutMs", "2000", "sqlOptionsMode", "reject"));
+  }
+
+  @Test
+  public void testSqlOptionsModeIsCaseInsensitive() {
+    QueryException e = expectThrows(QueryException.class,
+        () -> parse("SET timeoutMs='1000'; select * from vegetables", 
"SQLOPTIONSMODE=Reject"));
+    assertEquals(e.getErrorCode(), QueryErrorCode.QUERY_VALIDATION);
+  }
+
+  @Test
+  public void testSqlOptionsModeInSqlIsNotHonored() {
+    // Only the request payload can restrict SQL options; the SQL has no say 
over its own options
+    assertEquals(parse("SET sqlOptionsMode='reject'; SET timeoutMs='1000'; 
select * from vegetables", null),
+        Map.of("sqlOptionsMode", "reject", "timeoutMs", "1000"));
+  }
+
+  @Test
+  public void testInvalidSqlOptionsModeFailsEvenWithoutSqlOptions() {
+    QueryException e =
+        expectThrows(QueryException.class, () -> parse("select * from 
vegetables", "sqlOptionsMode=bogus"));
+    assertEquals(e.getErrorCode(), QueryErrorCode.QUERY_VALIDATION);
+    assertTrue(e.getMessage().contains("bogus"), e.getMessage());
+  }
+
+  private static Map<String, String> sqlOptionsOf(String sql) {
+    return CalciteSqlParser.compileToSqlNodeAndOptions(sql).getOptions();
+  }
+
+  private static Map<String, String> parse(String sql, @Nullable String 
queryOptions) {
+    ObjectNode request = JsonUtils.newObjectNode().put(Request.SQL, sql);
+    if (queryOptions != null) {
+      request.put(Request.QUERY_OPTIONS, queryOptions);
+    }
+    return RequestUtils.parseQuery(sql, request).getOptions();
+  }
+}
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotQueryResource.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotQueryResource.java
index ee2a6bd5a1c..7c3aaafa9d8 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotQueryResource.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotQueryResource.java
@@ -57,7 +57,6 @@ import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.StreamingOutput;
-import org.apache.calcite.sql.SqlNode;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.io.IOUtils;
 import org.apache.hc.core5.net.URIBuilder;
@@ -91,7 +90,6 @@ import org.apache.pinot.spi.utils.CommonConstants;
 import 
org.apache.pinot.spi.utils.CommonConstants.Broker.Request.QueryOptionKey;
 import org.apache.pinot.spi.utils.JsonUtils;
 import org.apache.pinot.spi.utils.builder.TableNameBuilder;
-import org.apache.pinot.sql.parsers.CalciteSqlCompiler;
 import org.apache.pinot.sql.parsers.CalciteSqlParser;
 import org.apache.pinot.sql.parsers.PinotSqlType;
 import org.apache.pinot.sql.parsers.SqlNodeAndOptions;
@@ -389,13 +387,11 @@ public class PinotQueryResource {
       @Nullable String queryOptions)
       throws Exception {
     LOGGER.debug("Trace: {}, Running query: {}", traceEnabled, sqlQuery);
-    SqlNodeAndOptions sqlNodeAndOptions;
-    sqlNodeAndOptions = CalciteSqlParser.compileToSqlNodeAndOptions(sqlQuery);
+    // Parse with the exact payload forwarded to the broker, so that the 
options used to route the query (engine,
+    // database) are the ones the broker will apply
+    ObjectNode requestJson = getRequestJson(sqlQuery, traceEnabled, 
queryOptions);
+    SqlNodeAndOptions sqlNodeAndOptions = RequestUtils.parseQuery(sqlQuery, 
requestJson);
     Map<String, String> options = sqlNodeAndOptions.getOptions();
-    if (queryOptions != null) {
-      Map<String, String> optionsFromString = 
RequestUtils.getOptionsFromString(queryOptions);
-      sqlNodeAndOptions.setExtraOptions(optionsFromString);
-    }
     PinotSqlType sqlType = sqlNodeAndOptions.getSqlType();
     if (sqlType == PinotSqlType.DDL) {
       throw QueryErrorCode.QUERY_VALIDATION.asException(
@@ -414,8 +410,8 @@ public class PinotQueryResource {
     switch (sqlType) {
       case DQL:
         return isMse
-            ? getMultiStageQueryResponse(sqlQuery, queryOptions, httpHeaders, 
traceEnabled)
-            : getQueryResponse(sqlQuery, sqlNodeAndOptions.getSqlNode(), 
traceEnabled, queryOptions, httpHeaders);
+            ? getMultiStageQueryResponse(sqlQuery, sqlNodeAndOptions, 
requestJson, httpHeaders)
+            : getQueryResponse(sqlQuery, sqlNodeAndOptions, requestJson, 
httpHeaders);
       case DML:
         Map<String, String> headers = extractHeaders(httpHeaders);
         return output -> {
@@ -428,8 +424,8 @@ public class PinotQueryResource {
     }
   }
 
-  private StreamingOutput getMultiStageQueryResponse(String query, String 
queryOptions, HttpHeaders httpHeaders,
-      String traceEnabled) {
+  private StreamingOutput getMultiStageQueryResponse(String query, 
SqlNodeAndOptions sqlNodeAndOptions,
+      ObjectNode requestJson, HttpHeaders httpHeaders) {
 
     // Validate data access
     // we don't have a cross table access control rule so only ADMIN can make 
request to multi-stage engine.
@@ -438,23 +434,19 @@ public class PinotQueryResource {
       throw new WebApplicationException("Permission denied", 
Response.Status.FORBIDDEN);
     }
 
-    Map<String, String> queryOptionsMap = 
RequestUtils.parseQuery(query).getOptions();
-    if (queryOptions != null) {
-      queryOptionsMap.putAll(RequestUtils.getOptionsFromString(queryOptions));
-    }
-    String database = 
DatabaseUtils.extractDatabaseFromQueryRequest(queryOptionsMap, httpHeaders);
-    List<String> tableNames = getTableNames(query, database);
+    String database = 
DatabaseUtils.extractDatabaseFromQueryRequest(sqlNodeAndOptions.getOptions(), 
httpHeaders);
+    List<String> tableNames = getTableNames(query, sqlNodeAndOptions, 
database);
     List<String> instanceIds = getInstanceIds(query, tableNames, database);
     String instanceId = selectRandomInstanceId(instanceIds);
-    return sendRequestToBroker(query, instanceId, traceEnabled, queryOptions, 
httpHeaders);
+    return sendRequestToBroker(query, instanceId, requestJson, httpHeaders);
   }
 
-  private List<String> getTableNames(String query, String database) {
+  private List<String> getTableNames(String query, SqlNodeAndOptions 
sqlNodeAndOptions, String database) {
     QueryEnvironment queryEnvironment =
         new QueryEnvironment(database, 
_pinotHelixResourceManager.getTableCache(), null);
     List<String> tableNames;
 
-    try (QueryEnvironment.CompiledQuery compiledQuery = 
queryEnvironment.compile(query)) {
+    try (QueryEnvironment.CompiledQuery compiledQuery = 
queryEnvironment.compile(query, sqlNodeAndOptions)) {
       tableNames = new ArrayList<>(compiledQuery.getTableNames());
     } catch (QueryException e) {
       if (e.getErrorCode() != QueryErrorCode.UNKNOWN) {
@@ -493,24 +485,19 @@ public class PinotQueryResource {
     return instanceIds;
   }
 
-  private StreamingOutput getQueryResponse(String query, @Nullable SqlNode 
sqlNode, String traceEnabled,
-      String queryOptions, HttpHeaders httpHeaders) {
+  private StreamingOutput getQueryResponse(String query, SqlNodeAndOptions 
sqlNodeAndOptions, ObjectNode requestJson,
+      HttpHeaders httpHeaders) {
     // Get resource table name.
     String tableName;
-    Map<String, String> queryOptionsMap = 
RequestUtils.parseQuery(query).getOptions();
-    if (queryOptions != null) {
-      queryOptionsMap.putAll(RequestUtils.getOptionsFromString(queryOptions));
-    }
     String database;
     try {
-      database = 
DatabaseUtils.extractDatabaseFromQueryRequest(queryOptionsMap, httpHeaders);
+      database = 
DatabaseUtils.extractDatabaseFromQueryRequest(sqlNodeAndOptions.getOptions(), 
httpHeaders);
     } catch (DatabaseConflictException e) {
       throw QueryErrorCode.QUERY_VALIDATION.asException(e);
     }
     try {
-      String inputTableName =
-          sqlNode != null ? 
RequestUtils.getTableNames(CalciteSqlParser.compileSqlNodeToPinotQuery(sqlNode)).iterator()
-              .next() : 
CalciteSqlCompiler.compileToBrokerRequest(query).getQuerySource().getTableName();
+      String inputTableName = RequestUtils.getTableNames(
+          
CalciteSqlParser.compileSqlNodeToPinotQuery(sqlNodeAndOptions.getSqlNode())).iterator().next();
       tableName = 
_pinotHelixResourceManager.getActualTableName(inputTableName, database);
     } catch (Exception e) {
       LOGGER.error("Caught exception while compiling query: {}", query, e);
@@ -536,7 +523,7 @@ public class PinotQueryResource {
     // Get brokers for the resource table.
     List<String> instanceIds = 
_pinotHelixResourceManager.getBrokerInstancesFor(rawTableName);
     String instanceId = selectRandomInstanceId(instanceIds);
-    return sendRequestToBroker(query, instanceId, traceEnabled, queryOptions, 
httpHeaders);
+    return sendRequestToBroker(query, instanceId, requestJson, httpHeaders);
   }
 
   // given a list of tables, returns the list of tableConfigs
@@ -602,14 +589,13 @@ public class PinotQueryResource {
     return 
brokerInstanceConfigs.map(InstanceConfig::getInstanceName).collect(Collectors.toList());
   }
 
-  private StreamingOutput sendRequestToBroker(String query, String instanceId, 
String traceEnabled, String queryOptions,
+  private StreamingOutput sendRequestToBroker(String query, String instanceId, 
ObjectNode requestJson,
       HttpHeaders httpHeaders) {
     InstanceConfig instanceConfig = getInstanceConfig(instanceId);
     String hostName = getHost(instanceConfig);
     String protocol = _controllerConf.getControllerBrokerProtocol();
     int port = getPort(instanceConfig);
     String url = getQueryURL(protocol, hostName, port);
-    ObjectNode requestJson = getRequestJson(query, traceEnabled, queryOptions);
 
     // Forward client-supplied headers
     Map<String, String> headers = extractHeaders(httpHeaders);
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotQueryResourceTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotQueryResourceTest.java
index 70f7f02bce6..c516c58bd1a 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotQueryResourceTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotQueryResourceTest.java
@@ -22,7 +22,9 @@ import java.io.ByteArrayOutputStream;
 import javax.ws.rs.core.StreamingOutput;
 import org.apache.pinot.common.config.provider.TableCache;
 import org.apache.pinot.controller.ControllerConf;
+import org.apache.pinot.controller.api.access.AccessControl;
 import org.apache.pinot.controller.api.access.AccessControlFactory;
+import org.apache.pinot.controller.api.access.AccessType;
 import org.apache.pinot.controller.helix.core.PinotHelixResourceManager;
 import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.exception.QueryErrorCode;
@@ -35,6 +37,8 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 
@@ -108,6 +112,42 @@ public class PinotQueryResourceTest {
     Assert.assertTrue(response.contains("/sql/ddl"));
   }
 
+  @Test
+  public void testSqlOptionsDecideTheEngine() {
+    // The mocked controller conf reports the multi-stage engine as disabled, 
so routing to it fails with INTERNAL
+    String response = streamingOutputToString(
+        _pinotQueryResource.handleGetSql("SET useMultistageEngine = 'true'; 
SELECT * FROM a", null, null, null));
+    
Assert.assertTrue(response.contains(String.valueOf(QueryErrorCode.INTERNAL.getId())),
 response);
+    Assert.assertTrue(response.contains("Multi-Stage query engine not 
enabled"), response);
+  }
+
+  @Test
+  public void testIgnoredSqlOptionsDoNotDecideTheEngine() {
+    mockSingleStageBrokerSelection();
+    String response = streamingOutputToString(
+        _pinotQueryResource.handleGetSql("SET useMultistageEngine = 'true'; 
SELECT * FROM a", null,
+            "sqlOptionsMode=ignore", null));
+    
Assert.assertTrue(response.contains(String.valueOf(QueryErrorCode.BROKER_RESOURCE_MISSING.getId())),
 response);
+  }
+
+  @Test
+  public void testRejectedSqlOptionsFailBeforeRouting() {
+    String response = streamingOutputToString(
+        _pinotQueryResource.handleGetSql("SET useMultistageEngine = 'true'; 
SELECT * FROM a", null,
+            "sqlOptionsMode=reject", null));
+    
Assert.assertTrue(response.contains(String.valueOf(QueryErrorCode.QUERY_VALIDATION.getId())),
 response);
+    Assert.assertTrue(response.contains("useMultistageEngine"), response);
+  }
+
+  /// Lets a single-stage query get as far as broker selection, which fails 
with BROKER_RESOURCE_MISSING because no
+  /// broker serves the table. Reaching that point proves the query was routed 
to the single-stage engine.
+  private void mockSingleStageBrokerSelection() {
+    when(_resourceManager.getActualTableName(any(), 
any())).then(AdditionalAnswers.returnsFirstArg());
+    AccessControl accessControl = mock(AccessControl.class);
+    when(accessControl.hasAccess(any(), eq(AccessType.READ), any(), 
any())).thenReturn(true);
+    when(_accessControlFactory.create()).thenReturn(accessControl);
+  }
+
   public static String streamingOutputToString(StreamingOutput 
streamingOutput) {
     try (ByteArrayOutputStream byteArrayOutputStream = new 
ByteArrayOutputStream()) {
       streamingOutput.write(byteArrayOutputStream);
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/combine/BaseCombineOperator.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/combine/BaseCombineOperator.java
index 1c56a538d82..f4e4f67f6c0 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/combine/BaseCombineOperator.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/combine/BaseCombineOperator.java
@@ -286,13 +286,7 @@ public abstract class BaseCombineOperator<T extends 
BaseResultsBlock> extends Ba
             + " on segment " + operator.getIndexSegment().getSegmentName()
         : "Caught exception while doing operator: " + operator.getClass();
 
-    QueryErrorCode errorCode;
-    if (e instanceof QueryException) {
-      QueryException queryException = (QueryException) e;
-      errorCode = queryException.getErrorCode();
-    } else {
-      errorCode = QueryErrorCode.QUERY_EXECUTION;
-    }
+    QueryErrorCode errorCode = QueryErrorCode.fromThrowable(e, 
QueryErrorCode.QUERY_EXECUTION);
     // TODO: Only include exception message if it is a QueryException. 
Otherwise, it might expose sensitive information
     throw errorCode.asException(errorMessage + ": " + e.getMessage(), e);
   }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/streaming/BaseStreamingCombineOperator.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/streaming/BaseStreamingCombineOperator.java
index e49b4fb1272..ca1b5930bbe 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/streaming/BaseStreamingCombineOperator.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/streaming/BaseStreamingCombineOperator.java
@@ -35,7 +35,6 @@ import 
org.apache.pinot.core.query.request.context.QueryContext;
 import org.apache.pinot.spi.exception.EarlyTerminationException;
 import org.apache.pinot.spi.exception.QueryErrorCode;
 import org.apache.pinot.spi.exception.QueryErrorMessage;
-import org.apache.pinot.spi.exception.QueryException;
 import org.apache.pinot.spi.query.QueryThreadContext;
 import org.apache.pinot.spi.utils.CommonConstants;
 import org.slf4j.Logger;
@@ -172,12 +171,8 @@ public abstract class BaseStreamingCombineOperator<T 
extends BaseResultsBlock> e
     _processingException.compareAndSet(null, t);
     // Clear the blocking queue and add the exception results block to 
terminate the main thread
     _blockingQueue.clear();
-    QueryErrorMessage errorMsg;
-    if (t instanceof QueryException) {
-      errorMsg = QueryErrorMessage.safeMsg(((QueryException) 
t).getErrorCode(), t.getMessage());
-    } else {
-      errorMsg = QueryErrorMessage.safeMsg(QueryErrorCode.QUERY_EXECUTION, 
t.getMessage());
-    }
+    QueryErrorMessage errorMsg =
+        QueryErrorMessage.safeMsg(QueryErrorCode.fromThrowable(t, 
QueryErrorCode.QUERY_EXECUTION), t.getMessage());
     _blockingQueue.offer(new ExceptionResultsBlock(errorMsg));
   }
 
diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/executor/OpChainSchedulerService.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/executor/OpChainSchedulerService.java
index cc20b3d8408..24cbdef09ec 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/executor/OpChainSchedulerService.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/executor/OpChainSchedulerService.java
@@ -48,7 +48,6 @@ import 
org.apache.pinot.query.runtime.plan.MultiStageQueryStats;
 import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.exception.QueryCancelledException;
 import org.apache.pinot.spi.exception.QueryErrorCode;
-import org.apache.pinot.spi.exception.QueryException;
 import org.apache.pinot.spi.exception.TerminationException;
 import org.apache.pinot.spi.metrics.PinotMeter;
 import org.apache.pinot.spi.query.QueryExecutionContext;
@@ -213,18 +212,14 @@ public class OpChainSchedulerService {
       public void onFailure(Throwable t) {
         String logMsg = "Failed to execute operator chain: " + t.getMessage();
         _metrics.onOpChainFinished(rootOperator);
-        if (t instanceof QueryException) {
-          switch (((QueryException) t).getErrorCode()) {
-            case UNKNOWN:
-            case INTERNAL:
-              LOGGER.error(logMsg, t);
-              break;
-            default:
-              LOGGER.warn(logMsg);
-              break;
-          }
-        } else {
-          LOGGER.error(logMsg, t);
+        switch (QueryErrorCode.fromThrowable(t, QueryErrorCode.UNKNOWN)) {
+          case UNKNOWN:
+          case INTERNAL:
+            LOGGER.error(logMsg, t);
+            break;
+          default:
+            LOGGER.warn(logMsg);
+            break;
         }
         decrementActiveOpChains(requestId);
         notifyCompletionListener(opChainId, operatorChain, statsRef.get(), t);
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/exception/QueryErrorCode.java 
b/pinot-spi/src/main/java/org/apache/pinot/spi/exception/QueryErrorCode.java
index cfee7557e4c..78286d4f44f 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/exception/QueryErrorCode.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/exception/QueryErrorCode.java
@@ -142,6 +142,12 @@ public enum QueryErrorCode {
     return queryErrorCode;
   }
 
+  /// Returns the error code carried by the given throwable when it is a 
[QueryException], or the given default
+  /// otherwise.
+  public static QueryErrorCode fromThrowable(Throwable t, QueryErrorCode 
defaultErrorCode) {
+    return t instanceof QueryException ? ((QueryException) t).getErrorCode() : 
defaultErrorCode;
+  }
+
   public static <T> Map<QueryErrorCode, T> fromKeyMap(Map<Integer, T> 
originalMap) {
     EnumMap<QueryErrorCode, T> newMap = new EnumMap<>(QueryErrorCode.class);
     for (Map.Entry<Integer, T> entry : originalMap.entrySet()) {
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java 
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
index 7fbf935c687..e0de1210a65 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
@@ -384,14 +384,22 @@ public class CommonConstants {
     public static final String DEFAULT_BROKER_QUERY_LOG_SQL_REDACTION = "none";
     public static final String CONFIG_OF_BROKER_QUERY_ENABLE_NULL_HANDLING = 
"pinot.broker.query.enable.null.handling";
     /// How query option keys supplied through SQL `SET` / `OPTION(...)` on 
DQL queries are validated.
-    /// Broker config key: `pinot.broker.query.option.validationMode`.
-    /// One of `QueryOptionsUtils.SqlQueryOptionValidationMode`: `NONE` 
(default, unknown keys are
-    /// preserved silently, as they always have been), `WARN` (preserved, 
logged once per distinct
-    /// unknown key) or `REJECT` (query fails). Plugins can allowlist their 
own keys for `REJECT` via
+    /// Cluster config key: `pinot.broker.query.option.validationMode`, 
applied live and not read from the broker
+    /// instance config. One of 
`QueryOptionsUtils.SqlQueryOptionValidationMode`: `NONE` (default, unknown keys 
are
+    /// preserved silently, as they always have been), `WARN` (preserved, 
logged once per distinct unknown key) or
+    /// `REJECT` (query fails). Plugins can allowlist their own keys for 
`REJECT` via
     /// `QueryOptionsUtils.registerSqlQueryOptionKey`.
     public static final String CONFIG_OF_BROKER_QUERY_OPTION_VALIDATION_MODE =
         "pinot.broker.query.option.validationMode";
     public static final String DEFAULT_BROKER_QUERY_OPTION_VALIDATION_MODE = 
"NONE";
+    /// How the legacy PQL-style `OPTION(key=value)` query option suffix is 
handled.
+    /// Cluster config key: `pinot.broker.query.option.legacySyntaxMode`, 
applied live and not read from the broker
+    /// instance config. One of `QueryOptionsUtils.SqlOptionsMode`: `ALLOW` 
(default) applies the options as always,
+    /// `IGNORE` strips the suffix and drops its options, `REJECT` fails the 
statement with an error pointing at
+    /// `SET`. Applies to every statement type, since `SET` covers them all.
+    public static final String 
CONFIG_OF_BROKER_QUERY_OPTION_LEGACY_SYNTAX_MODE =
+        "pinot.broker.query.option.legacySyntaxMode";
+    public static final String DEFAULT_BROKER_QUERY_OPTION_LEGACY_SYNTAX_MODE 
= "ALLOW";
     /// When true, the broker initializes the materialized view metadata cache 
and query rewrite
     /// engine.  When false (default), MV rewrite is disabled regardless of 
per-MV
     /// `rewriteEnabled` setting.
@@ -902,6 +910,13 @@ public class CommonConstants {
         public static final String USE_FIXED_REPLICA = "useFixedReplica";
         public static final String EXPLAIN_PLAN_VERBOSE = "explainPlanVerbose";
         public static final String USE_MULTISTAGE_ENGINE = 
"useMultistageEngine";
+        /// How query options embedded in the SQL text (`SET` statements and 
the legacy `OPTION(...)` suffix) are
+        /// handled for this request, one of 
`QueryOptionsUtils.SqlOptionsMode`: `ALLOW` (default) merges them with
+        /// precedence over the request options, as always; `IGNORE` drops 
them so that only the request options
+        /// apply; `REJECT` fails the query when it carries any. Only honored 
from the request payload
+        /// (`queryOptions`, gRPC metadata), never from the SQL itself, so a 
gateway that sets request options on
+        /// behalf of its users can guarantee the query text cannot override 
them.
+        public static final String SQL_OPTIONS_MODE = "sqlOptionsMode";
         public static final String INFER_PARTITION_HINT = "inferPartitionHint";
         public static final String ENABLE_NULL_HANDLING = "enableNullHandling";
         public static final String APPLICATION_NAME = "applicationName";


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

Reply via email to