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

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


The following commit(s) were added to refs/heads/master by this push:
     new 71862170ac3 feat: add dynamic default query context (#19144)
71862170ac3 is described below

commit 71862170ac31386e05f68406b9aee7b32be8c86f
Author: jtuglu1 <[email protected]>
AuthorDate: Fri Mar 20 20:59:15 2026 -0700

    feat: add dynamic default query context (#19144)
    
    Add dynamic default query context to brokers. Allows operators to quickly 
tune the default setting for query context values without requiring a 
re-deployment or forcing upstream clients to update their queries.
---
 docs/api-reference/dynamic-configuration-api.md    |  35 +++-
 docs/querying/query-context.md                     |   8 +-
 .../server/EmbeddedBrokerDynamicConfigTest.java    | 188 +++++++++++++++++++++
 .../druid/grpc/server/GrpcEndpointInitializer.java |   7 +-
 .../org/apache/druid/grpc/server/QueryDriver.java  |   9 +-
 .../java/org/apache/druid/grpc/BasicAuthTest.java  |   3 +-
 .../java/org/apache/druid/grpc/DriverTest.java     |   3 +-
 .../java/org/apache/druid/grpc/GrpcQueryTest.java  |   3 +-
 .../msq/dart/controller/sql/DartSqlEngine.java     |  10 +-
 .../druid/msq/dart/guice/DartControllerModule.java |   6 +
 .../msq/sql/resources/SqlStatementResource.java    |  10 +-
 .../druid/msq/sql/resources/SqlTaskResource.java   |  10 +-
 .../org/apache/druid/query/DefaultQueryConfig.java |   3 +-
 .../apache/druid/query/QueryConfigProvider.java    |  34 ++++
 .../druid/client/BaseBrokerViewOfConfig.java       |   2 +-
 .../druid/client/BrokerViewOfBrokerConfig.java     |  47 +++++-
 .../apache/druid/guice/QueryToolChestModule.java   |   4 +
 .../org/apache/druid/server/QueryLifecycle.java    |  10 +-
 .../apache/druid/server/QueryLifecycleFactory.java |  22 +--
 .../druid/server/broker/BrokerDynamicConfig.java   |  41 ++++-
 .../druid/client/BrokerViewOfBrokerConfigTest.java |  33 +++-
 .../metadata/SegmentMetadataCacheTestBase.java     |   3 +-
 .../apache/druid/server/QueryLifecycleTest.java    |  11 +-
 .../org/apache/druid/server/QueryResourceTest.java |  27 ++-
 .../server/broker/BrokerDynamicConfigTest.java     |  27 +++
 .../main/java/org/apache/druid/cli/CliBroker.java  |   6 +-
 .../org/apache/druid/sql/avatica/DruidMeta.java    |  18 +-
 .../org/apache/druid/sql/http/SqlResource.java     |  10 +-
 .../druid/sql/avatica/DruidAvaticaHandlerTest.java |   2 +
 .../sql/calcite/util/QueryFrameworkUtils.java      |   3 +-
 .../org/apache/druid/sql/guice/SqlModuleTest.java  |   6 +-
 .../apache/druid/sql/http/SqlHttpModuleTest.java   |   2 +
 .../broker-dynamic-config-completions.ts           |  26 +++
 .../broker-dynamic-config.tsx                      |  24 ++-
 34 files changed, 539 insertions(+), 114 deletions(-)

diff --git a/docs/api-reference/dynamic-configuration-api.md 
b/docs/api-reference/dynamic-configuration-api.md
index 1c37fa91e29..80433a54103 100644
--- a/docs/api-reference/dynamic-configuration-api.md
+++ b/docs/api-reference/dynamic-configuration-api.md
@@ -306,7 +306,13 @@ Host: http://ROUTER_IP:ROUTER_PORT
 ## Broker dynamic configuration
 
 Broker dynamic configuration is managed through the Coordinator but consumed 
by Brokers.
-These settings control broker behavior such as query blocking rules.
+These settings control broker behavior such as query blocking rules and 
default query context values.
+
+> **Note:** Broker dynamic configuration is best-effort. Settings may not be 
applied in certain
+> cases, such as when a Broker has recently started and hasn't received the 
config yet, or if the
+> Broker cannot contact the Coordinator. Brokers poll the configuration 
periodically (default every
+> 1 minute) and also receive push updates from the Coordinator for immediate 
propagation. If a
+> setting is critical and must always be applied, use the equivalent static 
runtime property instead.
 
 ### Get broker dynamic configuration
 
@@ -366,7 +372,11 @@ Host: http://ROUTER_IP:ROUTER_PORT
       "dataSources": ["large_table"],
       "queryTypes": ["scan"]
     }
-  ]
+  ],
+  "queryContext": {
+    "priority": 0,
+    "timeout": 300000
+  }
 }
 ```
 
@@ -417,7 +427,7 @@ The endpoint supports a set of optional header parameters 
to populate the audit
 curl -X POST "http://ROUTER_IP:ROUTER_PORT/druid/coordinator/v1/broker/config"; 
\
 -H "Content-Type: application/json" \
 -H "X-Druid-Author: admin" \
--H "X-Druid-Comment: Add query blocklist rules" \
+-H "X-Druid-Comment: Add query blocklist rules and set default context" \
 -d '{
   "queryBlocklist": [
     {
@@ -431,7 +441,11 @@ curl -X POST 
"http://ROUTER_IP:ROUTER_PORT/druid/coordinator/v1/broker/config"; \
         "debug": "true"
       }
     }
-  ]
+  ],
+  "queryContext": {
+    "priority": 0,
+    "timeout": 300000
+  }
 }'
 ```
 
@@ -444,7 +458,7 @@ POST /druid/coordinator/v1/broker/config HTTP/1.1
 Host: http://ROUTER_IP:ROUTER_PORT
 Content-Type: application/json
 X-Druid-Author: admin
-X-Druid-Comment: Add query blocklist rules
+X-Druid-Comment: Add query blocklist rules and set default context
 
 {
   "queryBlocklist": [
@@ -459,7 +473,11 @@ X-Druid-Comment: Add query blocklist rules
         "debug": "true"
       }
     }
-  ]
+  ],
+  "queryContext": {
+    "priority": 0,
+    "timeout": 300000
+  }
 }
 ```
 
@@ -477,6 +495,7 @@ The following table shows the dynamic configuration 
properties for the Broker.
 |Property|Description|Default|
 |--------|-----------|-------|
 |`queryBlocklist`| List of rules to block queries based on datasource, query 
type, and/or query context parameters. Each rule defines criteria that are 
combined with AND logic. Blocked queries return an HTTP 403 error. See [Query 
blocklist rules](#query-blocklist-rules) for details.|none|
+|`queryContext`| Map of default query context key-value pairs applied to all 
queries on this broker. These values override static defaults set via runtime 
properties (`druid.query.default.context.*`) but are overridden by context 
values supplied in individual query payloads. Useful for setting cluster-wide 
defaults such as `priority` or `timeout` without restarting. See [Query context 
reference](../querying/query-context-reference.md) for available keys.|none|
 
 #### Query blocklist rules
 
@@ -499,8 +518,6 @@ Each rule in the `queryBlocklist` array is a JSON object 
with the following prop
 - At least one criterion must be specified per rule to prevent accidentally 
blocking all queries
 - A query is blocked if it matches ANY rule in the blocklist (OR logic between 
rules)
 
-> **Note:** Query blocking is best-effort. Queries may not be blocked in 
certain cases, such as when a Broker has recently started and hasn't received 
the config yet, or if the Broker cannot contact the Coordinator. Brokers poll 
the configuration periodically (default every 1 minute) and also receive push 
updates from the Coordinator for immediate propagation.
-
 **Error response:**
 
 When a query is blocked, the Broker returns an HTTP 403 error with a message 
indicating the query ID and the rule that blocked it:
@@ -586,7 +603,7 @@ Host: http://ROUTER_IP:ROUTER_PORT
       "comment": "Add query blocklist rules",
       "ip": "127.0.0.1"
     },
-    "payload": 
"{\"queryBlocklist\":[{\"ruleName\":\"block-expensive-scans\",\"dataSources\":[\"large_table\"],\"queryTypes\":[\"scan\"]}]}",
+    "payload": 
"{\"queryBlocklist\":[{\"ruleName\":\"block-expensive-scans\",\"dataSources\":[\"large_table\"],\"queryTypes\":[\"scan\"]}],\"queryContext\":{\"priority\":0,\"timeout\":300000}}",
     "auditTime": "2024-03-06T12:00:00.000Z"
   }
 ]
diff --git a/docs/querying/query-context.md b/docs/querying/query-context.md
index 2f423945beb..076508a4df3 100644
--- a/docs/querying/query-context.md
+++ b/docs/querying/query-context.md
@@ -275,15 +275,17 @@ For more information, see [Configuration 
reference](../configuration/index.md#ov
 
 ## Query context precedence
 
-For a given context query, Druid determines the final query context value to 
use based on the following order of precedence, from lowest to highest:
+For a given query context, Druid determines the final query context value to 
use based on the following order of precedence, from lowest to highest:
 
 1. **Built-in defaults**: Druid uses the documented default values if you 
don’t specify anything.
 
 2. **Runtime properties**: If you configure parameters as 
`druid.query.default.context.{PARAMETER}` in the configuration files, these 
override the built-in defaults and act as your system-wide defaults.
 
-3. **Context object in HTTP request**: Parameters passed within the JSON 
`context` object override both built-in defaults and runtime properties.
+3. **Broker Dynamic Config**: If you configure query context parameters in the 
Broker's dynamic config (see [Broker Dynamic 
Config](../api-reference/dynamic-configuration-api.md)). Overrides built-in 
defaults and runtime properties.
 
-4. **SET statements**: Parameters set in Druid SQL using `SET key=value;` take 
the highest precedence and override all other settings.
+4. **Context object in HTTP request**: Parameters passed within the JSON 
`context` object override built-in defaults, runtime properties, and Broker 
dynamic configs.
+
+5. **SET statements**: Parameters set in Druid SQL using `SET key=value;` take 
the highest precedence and override all other settings.
 
 
 ## Learn more
diff --git 
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/EmbeddedBrokerDynamicConfigTest.java
 
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/EmbeddedBrokerDynamicConfigTest.java
new file mode 100644
index 00000000000..34020229434
--- /dev/null
+++ 
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/EmbeddedBrokerDynamicConfigTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.druid.testing.embedded.server;
+
+import org.apache.druid.audit.AuditInfo;
+import org.apache.druid.common.config.JacksonConfigManager;
+import org.apache.druid.common.utils.IdUtils;
+import org.apache.druid.indexing.common.task.TaskBuilder;
+import org.apache.druid.query.QueryContext;
+import org.apache.druid.server.QueryBlocklistRule;
+import org.apache.druid.server.broker.BrokerDynamicConfig;
+import org.apache.druid.server.http.BrokerDynamicConfigSyncer;
+import org.apache.druid.testing.embedded.EmbeddedBroker;
+import org.apache.druid.testing.embedded.EmbeddedClusterApis;
+import org.apache.druid.testing.embedded.EmbeddedCoordinator;
+import org.apache.druid.testing.embedded.EmbeddedDruidCluster;
+import org.apache.druid.testing.embedded.EmbeddedHistorical;
+import org.apache.druid.testing.embedded.EmbeddedIndexer;
+import org.apache.druid.testing.embedded.EmbeddedOverlord;
+import org.apache.druid.testing.embedded.indexing.Resources;
+import org.apache.druid.testing.embedded.junit5.EmbeddedClusterTestBase;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Integration test for broker dynamic configuration
+ */
+public class EmbeddedBrokerDynamicConfigTest extends EmbeddedClusterTestBase
+{
+  // Fixed datasource ingested once for all tests; restored before each test 
since the
+  // base class @BeforeEach would otherwise assign a fresh name.
+  private String fixedDataSource;
+
+  private final EmbeddedCoordinator coordinator = new EmbeddedCoordinator();
+  private final EmbeddedOverlord overlord = new EmbeddedOverlord();
+  private final EmbeddedIndexer indexer = new EmbeddedIndexer();
+  private final EmbeddedHistorical historical = new EmbeddedHistorical();
+  private final EmbeddedBroker broker = new EmbeddedBroker();
+
+  @Override
+  protected EmbeddedDruidCluster createCluster()
+  {
+    indexer.addProperty("druid.segment.handoff.pollDuration", "PT0.1s");
+
+    return EmbeddedDruidCluster.withEmbeddedDerbyAndZookeeper()
+                               .useLatchableEmitter()
+                               .addServer(overlord)
+                               .addServer(coordinator)
+                               .addServer(indexer)
+                               .addServer(historical)
+                               .addServer(broker);
+  }
+
+  @BeforeAll
+  @Override
+  public void setup() throws Exception
+  {
+    fixedDataSource = EmbeddedClusterApis.createTestDatasourceName();
+    dataSource = fixedDataSource;
+    super.setup();
+    ingestData();
+    cluster.callApi().waitForAllSegmentsToBeAvailable(fixedDataSource, 
coordinator, broker);
+  }
+
+  @BeforeEach
+  @Override
+  protected void refreshDatasourceName()
+  {
+    dataSource = fixedDataSource;
+  }
+
+  @Test
+  @Timeout(30)
+  public void testQueryBlocklistBlocksMatchingQueries()
+  {
+    // Baseline: query succeeds before blocklist is applied
+    String initialResult = cluster.callApi().runSql("SELECT COUNT(*) FROM %s", 
dataSource);
+    Assertions.assertFalse(initialResult.isBlank());
+
+    // Apply blocklist rule that matches all queries on this datasource
+    QueryBlocklistRule blockRule = new QueryBlocklistRule(
+        "block-test-datasource",
+        Set.of(dataSource),
+        null,
+        null
+    );
+    updateBrokerDynamicConfig(
+        BrokerDynamicConfig.builder()
+                           .withQueryBlocklist(List.of(blockRule))
+                           .build()
+    );
+
+    // Query should now throw due to FORBIDDEN blocklist rule
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> cluster.callApi().runSql("SELECT COUNT(*) FROM %s", dataSource)
+    );
+
+    // Clear the blocklist and verify queries resume
+    updateBrokerDynamicConfig(BrokerDynamicConfig.builder().build());
+    String finalResult = cluster.callApi().runSql("SELECT COUNT(*) FROM %s", 
dataSource);
+    Assertions.assertFalse(finalResult.isBlank());
+  }
+
+  @Test
+  @Timeout(30)
+  public void testDynamicQueryContextTimeoutCausesQueryToFail()
+  {
+    // Baseline: query succeeds before a timeout context is applied
+    String initialResult = cluster.callApi().runSql("SELECT COUNT(*) FROM %s", 
dataSource);
+    Assertions.assertFalse(initialResult.isBlank());
+
+    // Apply a 1ms timeout via dynamic query context to force timeout
+    updateBrokerDynamicConfig(
+        BrokerDynamicConfig.builder()
+                           .withQueryContext(QueryContext.of(Map.of("timeout", 
1)))
+                           .build()
+    );
+
+    // Query should now throw due to the timeout being exceeded
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> cluster.callApi().runSql("SELECT COUNT(*) FROM %s", dataSource)
+    );
+
+    // Clear the dynamic context and verify queries resume
+    updateBrokerDynamicConfig(BrokerDynamicConfig.builder().build());
+    String finalResult = cluster.callApi().runSql("SELECT COUNT(*) FROM %s", 
dataSource);
+    Assertions.assertFalse(finalResult.isBlank());
+  }
+
+  private void ingestData()
+  {
+    cluster.callApi().runTask(
+        TaskBuilder.ofTypeIndex()
+                   .dataSource(dataSource)
+                   .isoTimestampColumn("time")
+                   .csvInputFormatWithColumns("time", "item", "value")
+                   .inlineInputSourceWithData(Resources.InlineData.CSV_10_DAYS)
+                   .segmentGranularity("DAY")
+                   .dimensions()
+                   .withId(IdUtils.getRandomId()),
+        overlord
+    );
+  }
+
+  /**
+   * Updates the broker dynamic config on the coordinator and synchronously 
broadcasts it
+   * to all brokers.
+   *
+   * Uses {@link JacksonConfigManager} directly to avoid the HTTP endpoint's 
builder-merge
+   * semantics, which cannot distinguish "clear to empty" from "not specified" 
when fields
+   * are omitted via {@code @JsonInclude(NON_EMPTY)}.
+   */
+  private void updateBrokerDynamicConfig(BrokerDynamicConfig config)
+  {
+    coordinator.bindings()
+               .getInstance(JacksonConfigManager.class)
+               .set(BrokerDynamicConfig.CONFIG_KEY, config, new 
AuditInfo("test", "[email protected]", "Testing", "127.0.0.1"));
+    coordinator.bindings()
+               .getInstance(BrokerDynamicConfigSyncer.class)
+               .broadcastConfigToBrokers();
+  }
+}
diff --git 
a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/GrpcEndpointInitializer.java
 
b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/GrpcEndpointInitializer.java
index af80e3d928d..ed69719c2fd 100644
--- 
a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/GrpcEndpointInitializer.java
+++ 
b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/GrpcEndpointInitializer.java
@@ -27,14 +27,13 @@ import org.apache.druid.java.util.common.ISE;
 import org.apache.druid.java.util.common.lifecycle.LifecycleStart;
 import org.apache.druid.java.util.common.lifecycle.LifecycleStop;
 import org.apache.druid.java.util.common.logger.Logger;
-import org.apache.druid.query.DefaultQueryConfig;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.server.QueryLifecycleFactory;
 import org.apache.druid.server.QueryScheduler;
 import org.apache.druid.server.security.AuthenticatorMapper;
 import org.apache.druid.sql.SqlStatementFactory;
 
 import javax.inject.Inject;
-
 import java.io.IOException;
 
 /**
@@ -66,7 +65,7 @@ public class GrpcEndpointInitializer
       final @Json ObjectMapper jsonMapper,
       final @NativeQuery SqlStatementFactory sqlStatementFactory,
       final QueryLifecycleFactory queryLifecycleFactory,
-      final DefaultQueryConfig defaultQueryConfig,
+      final QueryConfigProvider queryConfigProvider,
       final AuthenticatorMapper authMapper,
       final QueryScheduler queryScheduler
   )
@@ -76,7 +75,7 @@ public class GrpcEndpointInitializer
     this.driver = new QueryDriver(
         jsonMapper,
         sqlStatementFactory,
-        defaultQueryConfig.getContext(),
+        queryConfigProvider,
         queryLifecycleFactory,
         queryScheduler
     );
diff --git 
a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java
 
b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java
index c30b822648b..df457cf7d14 100644
--- 
a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java
+++ 
b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java
@@ -41,6 +41,7 @@ import org.apache.druid.java.util.common.guava.Accumulator;
 import org.apache.druid.java.util.common.guava.Sequence;
 import org.apache.druid.java.util.common.logger.Logger;
 import org.apache.druid.query.Query;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryInterruptedException;
 import org.apache.druid.query.QueryToolChest;
 import org.apache.druid.segment.column.ColumnHolder;
@@ -99,21 +100,21 @@ public class QueryDriver
 
   private final ObjectMapper jsonMapper;
   private final SqlStatementFactory sqlStatementFactory;
-  private final Map<String, Object> defaultContext;
+  private final QueryConfigProvider queryConfigProvider;
   private final QueryLifecycleFactory queryLifecycleFactory;
   private final QueryScheduler queryScheduler;
 
   public QueryDriver(
       final ObjectMapper jsonMapper,
       final SqlStatementFactory sqlStatementFactory,
-      final Map<String, Object> defaultContext,
+      final QueryConfigProvider queryConfigProvider,
       final QueryLifecycleFactory queryLifecycleFactory,
       final QueryScheduler queryScheduler
   )
   {
     this.jsonMapper = Preconditions.checkNotNull(jsonMapper, "jsonMapper");
     this.sqlStatementFactory = Preconditions.checkNotNull(sqlStatementFactory, 
"sqlStatementFactory");
-    this.defaultContext = defaultContext;
+    this.queryConfigProvider = queryConfigProvider;
     this.queryLifecycleFactory = queryLifecycleFactory;
     this.queryScheduler = queryScheduler;
   }
@@ -314,7 +315,7 @@ public class QueryDriver
   {
     return SqlQueryPlus.builder()
                        .sql(request.getQuery())
-                       .systemDefaultContext(defaultContext)
+                       .systemDefaultContext(queryConfigProvider.getContext())
                        .queryContext(translateContext(request))
                        .sqlParameters(translateParameters(request))
                        .auth(authResult)
diff --git 
a/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/BasicAuthTest.java
 
b/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/BasicAuthTest.java
index f04af5370a3..167bef17f54 100644
--- 
a/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/BasicAuthTest.java
+++ 
b/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/BasicAuthTest.java
@@ -31,6 +31,7 @@ import org.apache.druid.grpc.server.GrpcQueryConfig;
 import org.apache.druid.grpc.server.QueryDriver;
 import org.apache.druid.grpc.server.QueryServer;
 import org.apache.druid.metadata.DefaultPasswordProvider;
+import org.apache.druid.query.DefaultQueryConfig;
 import org.apache.druid.security.basic.authentication.BasicHTTPAuthenticator;
 import 
org.apache.druid.security.basic.authentication.validator.CredentialsValidator;
 import org.apache.druid.server.QueryStackTests;
@@ -72,7 +73,7 @@ public class BasicAuthTest extends BaseCalciteQueryTest
     QueryDriver driver = new QueryDriver(
         sqlTestFramework.queryJsonMapper(),
         plannerFixture.statementFactory(),
-        Map.of("forbiddenKey", "system-default-value"), // systen default 
forbidden key, only superuser can change it
+        new DefaultQueryConfig(Map.of("forbiddenKey", 
"system-default-value")), // system default forbidden key, only superuser can 
change it
         sqlTestFramework.queryLifecycleFactory(),
         QueryStackTests.DEFAULT_NOOP_SCHEDULER
     );
diff --git 
a/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/DriverTest.java
 
b/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/DriverTest.java
index 0bf7aa2c0e3..8e2e99406a9 100644
--- 
a/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/DriverTest.java
+++ 
b/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/DriverTest.java
@@ -27,6 +27,7 @@ import 
org.apache.druid.grpc.proto.QueryOuterClass.QueryResponse;
 import org.apache.druid.grpc.proto.QueryOuterClass.QueryResultFormat;
 import org.apache.druid.grpc.proto.QueryOuterClass.QueryStatus;
 import org.apache.druid.grpc.server.QueryDriver;
+import org.apache.druid.query.DefaultQueryConfig;
 import org.apache.druid.server.QueryStackTests;
 import org.apache.druid.server.security.AuthConfig;
 import org.apache.druid.sql.calcite.BaseCalciteQueryTest;
@@ -58,7 +59,7 @@ public class DriverTest extends BaseCalciteQueryTest
     driver = new QueryDriver(
         sqlTestFramework.queryJsonMapper(),
         plannerFixture.statementFactory(),
-        Map.of(),
+        new DefaultQueryConfig(Map.of()),
         sqlTestFramework.queryLifecycleFactory(),
         QueryStackTests.DEFAULT_NOOP_SCHEDULER
     );
diff --git 
a/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/GrpcQueryTest.java
 
b/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/GrpcQueryTest.java
index 06cbbdd6afe..46c2df5a7b5 100644
--- 
a/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/GrpcQueryTest.java
+++ 
b/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/GrpcQueryTest.java
@@ -34,6 +34,7 @@ import org.apache.druid.grpc.proto.TestResults.QueryResult;
 import org.apache.druid.grpc.server.GrpcQueryConfig;
 import org.apache.druid.grpc.server.QueryDriver;
 import org.apache.druid.grpc.server.QueryServer;
+import org.apache.druid.query.DefaultQueryConfig;
 import org.apache.druid.server.QueryStackTests;
 import org.apache.druid.server.security.AllowAllAuthenticator;
 import org.apache.druid.server.security.AuthConfig;
@@ -77,7 +78,7 @@ public class GrpcQueryTest extends BaseCalciteQueryTest
     QueryDriver driver = new QueryDriver(
         sqlTestFramework.queryJsonMapper(),
         plannerFixture.statementFactory(),
-        Map.of(),
+        new DefaultQueryConfig(Map.of()),
         sqlTestFramework.queryLifecycleFactory(),
         QueryStackTests.DEFAULT_NOOP_SCHEDULER
     );
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartSqlEngine.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartSqlEngine.java
index 2410ea8486d..82a2479a392 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartSqlEngine.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartSqlEngine.java
@@ -46,7 +46,7 @@ import org.apache.druid.msq.indexing.error.CancellationReason;
 import org.apache.druid.msq.querykit.MultiQueryKit;
 import org.apache.druid.msq.sql.DartQueryKitSpecFactory;
 import org.apache.druid.msq.sql.MSQTaskSqlEngine;
-import org.apache.druid.query.DefaultQueryConfig;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryContext;
 import org.apache.druid.query.QueryContexts;
 import org.apache.druid.server.QueryScheduler;
@@ -87,7 +87,7 @@ public class DartSqlEngine implements SqlEngine
   private final ServerConfig serverConfig;
   private final QueryKitSpecFactory queryKitSpecFactory;
   private final MultiQueryKit queryKit;
-  private final DefaultQueryConfig dartQueryConfig;
+  private final QueryConfigProvider queryConfigProvider;
   private final SqlToolbox toolbox;
   private final DartSqlClients sqlClients;
 
@@ -100,7 +100,7 @@ public class DartSqlEngine implements SqlEngine
       DartQueryKitSpecFactory queryKitSpecFactory,
       MultiQueryKit queryKit,
       ServerConfig serverConfig,
-      @Dart DefaultQueryConfig dartQueryConfig,
+      @Dart QueryConfigProvider queryConfigProvider,
       SqlToolbox toolbox,
       DartSqlClients sqlClients
   )
@@ -112,7 +112,7 @@ public class DartSqlEngine implements SqlEngine
     this.queryKitSpecFactory = queryKitSpecFactory;
     this.queryKit = queryKit;
     this.serverConfig = serverConfig;
-    this.dartQueryConfig = dartQueryConfig;
+    this.queryConfigProvider = queryConfigProvider;
     this.toolbox = toolbox;
     this.sqlClients = sqlClients;
   }
@@ -223,7 +223,7 @@ public class DartSqlEngine implements SqlEngine
   public void initContextMap(Map<String, Object> contextMap)
   {
     // Default context keys from dartQueryConfig.
-    for (Map.Entry<String, Object> entry : 
dartQueryConfig.getContext().entrySet()) {
+    for (Map.Entry<String, Object> entry : 
queryConfigProvider.getContext().entrySet()) {
       contextMap.putIfAbsent(entry.getKey(), entry.getValue());
     }
     /**
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/guice/DartControllerModule.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/guice/DartControllerModule.java
index 8219baa03aa..3b86b5e8b70 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/guice/DartControllerModule.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/guice/DartControllerModule.java
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.inject.Binder;
 import com.google.inject.Inject;
+import com.google.inject.Key;
 import com.google.inject.Module;
 import com.google.inject.Provides;
 import com.google.inject.multibindings.Multibinder;
@@ -52,6 +53,7 @@ import 
org.apache.druid.msq.dart.controller.sql.DartSqlClients;
 import org.apache.druid.msq.dart.controller.sql.DartSqlEngine;
 import org.apache.druid.msq.rpc.ResourcePermissionMapper;
 import org.apache.druid.query.DefaultQueryConfig;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.sql.SqlStatementFactory;
 import org.apache.druid.sql.SqlToolbox;
 import org.apache.druid.sql.calcite.run.SqlEngine;
@@ -84,6 +86,10 @@ public class DartControllerModule implements DruidModule
     {
       JsonConfigProvider.bind(binder, DartModules.DART_PROPERTY_BASE + 
".controller", DartControllerConfig.class);
       JsonConfigProvider.bind(binder, DartModules.DART_PROPERTY_BASE + 
".query", DefaultQueryConfig.class, Dart.class);
+      // Dart uses its own static DefaultQueryConfig rather than 
BrokerViewOfBrokerConfig because
+      // DartSqlEngine.initContextMap() manages context merging independently 
for Dart queries.
+      binder.bind(Key.get(QueryConfigProvider.class, Dart.class))
+            .to(Key.get(DefaultQueryConfig.class, Dart.class));
 
       LifecycleModule.register(binder, DartSqlClients.class);
       LifecycleModule.register(binder, DartMessageRelays.class);
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java
index d3363b25498..06adbc74f19 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java
@@ -69,8 +69,8 @@ import org.apache.druid.msq.sql.entity.ResultSetInformation;
 import org.apache.druid.msq.sql.entity.SqlStatementResult;
 import org.apache.druid.msq.util.MultiStageQueryContext;
 import org.apache.druid.msq.util.SqlStatementResourceHelper;
-import org.apache.druid.query.DefaultQueryConfig;
 import org.apache.druid.query.ExecutionMode;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryContext;
 import org.apache.druid.query.QueryContexts;
 import org.apache.druid.query.QueryException;
@@ -136,7 +136,7 @@ public class SqlStatementResource
   private final OverlordClient overlordClient;
   private final StorageConnector storageConnector;
   private final AuthorizerMapper authorizerMapper;
-  private final DefaultQueryConfig defaultQueryConfig;
+  private final QueryConfigProvider queryConfigProvider;
   private final ServerConfig serverConfig;
   private final WireTransferableContext wireTransferableContext;
 
@@ -147,7 +147,7 @@ public class SqlStatementResource
       final OverlordClient overlordClient,
       final @MultiStageQuery StorageConnectorProvider storageConnectorProvider,
       final AuthorizerMapper authorizerMapper,
-      final DefaultQueryConfig defaultQueryConfig,
+      final QueryConfigProvider queryConfigProvider,
       final ServerConfig serverConfig,
       final WireTransferableContext wireTransferableContext
   )
@@ -157,7 +157,7 @@ public class SqlStatementResource
     this.overlordClient = overlordClient;
     this.storageConnector = 
storageConnectorProvider.createStorageConnector(null);
     this.authorizerMapper = authorizerMapper;
-    this.defaultQueryConfig = defaultQueryConfig;
+    this.queryConfigProvider = queryConfigProvider;
     this.serverConfig = serverConfig;
     this.wireTransferableContext = wireTransferableContext;
   }
@@ -202,7 +202,7 @@ public class SqlStatementResource
           sqlQuery,
           req,
           ImmutableMap.<String, Object>builder()
-                      .putAll(defaultQueryConfig.getContext())
+                      .putAll(queryConfigProvider.getContext())
                       .put(RESULT_FORMAT, sqlQuery.getResultFormat())
                       .build()
       );
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlTaskResource.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlTaskResource.java
index 75c064b30bf..37714b6f906 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlTaskResource.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlTaskResource.java
@@ -33,7 +33,7 @@ import org.apache.druid.java.util.common.guava.Yielders;
 import org.apache.druid.java.util.common.logger.Logger;
 import org.apache.druid.msq.guice.MultiStageQuery;
 import org.apache.druid.msq.sql.MSQTaskSqlEngine;
-import org.apache.druid.query.DefaultQueryConfig;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryException;
 import org.apache.druid.query.http.SqlTaskStatus;
 import org.apache.druid.server.QueryResponse;
@@ -81,20 +81,20 @@ public class SqlTaskResource
   private static final Logger log = new Logger(SqlTaskResource.class);
 
   private final SqlStatementFactory sqlStatementFactory;
-  private final DefaultQueryConfig defaultQueryConfig;
+  private final QueryConfigProvider queryConfigProvider;
   private final ObjectMapper jsonMapper;
   private final ServerConfig serverConfig;
 
   @Inject
   public SqlTaskResource(
       final @MultiStageQuery SqlStatementFactory sqlStatementFactory,
-      final DefaultQueryConfig defaultQueryConfig,
+      final QueryConfigProvider queryConfigProvider,
       final ObjectMapper jsonMapper,
       final ServerConfig serverConfig
   )
   {
     this.sqlStatementFactory = sqlStatementFactory;
-    this.defaultQueryConfig = defaultQueryConfig;
+    this.queryConfigProvider = queryConfigProvider;
     this.jsonMapper = jsonMapper;
     this.serverConfig = serverConfig;
   }
@@ -131,7 +131,7 @@ public class SqlTaskResource
     final SqlQueryPlus sqlQueryPlus;
     final HttpStatement stmt;
     try {
-      sqlQueryPlus = SqlResource.makeSqlQueryPlus(sqlQuery, req, 
defaultQueryConfig.getContext());
+      sqlQueryPlus = SqlResource.makeSqlQueryPlus(sqlQuery, req, 
queryConfigProvider.getContext());
       stmt = sqlStatementFactory.httpStatement(sqlQueryPlus, req);
     }
     catch (Exception e) {
diff --git 
a/processing/src/main/java/org/apache/druid/query/DefaultQueryConfig.java 
b/processing/src/main/java/org/apache/druid/query/DefaultQueryConfig.java
index d831fa2d840..0924c5017d0 100644
--- a/processing/src/main/java/org/apache/druid/query/DefaultQueryConfig.java
+++ b/processing/src/main/java/org/apache/druid/query/DefaultQueryConfig.java
@@ -37,7 +37,7 @@ import java.util.Map;
  * @see org.apache.druid.query.scan.ScanQueryConfig
  *
  */
-public class DefaultQueryConfig
+public class DefaultQueryConfig implements QueryConfigProvider
 {
   /**
    * Config that does nothing.
@@ -54,6 +54,7 @@ public class DefaultQueryConfig
   @JsonProperty
   private final Map<String, Object> context;
 
+  @Override
   @Nonnull
   public Map<String, Object> getContext()
   {
diff --git 
a/processing/src/main/java/org/apache/druid/query/QueryConfigProvider.java 
b/processing/src/main/java/org/apache/druid/query/QueryConfigProvider.java
new file mode 100644
index 00000000000..6b533ab6a37
--- /dev/null
+++ b/processing/src/main/java/org/apache/druid/query/QueryConfigProvider.java
@@ -0,0 +1,34 @@
+/*
+ * 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.druid.query;
+
+import java.util.Map;
+
+/**
+ * Provides the default query config applied to all incoming queries before 
per-query overrides are merged in.
+ *
+ * <p>On non-broker nodes this is backed by static runtime properties ({@link 
DefaultQueryConfig}).
+ * On brokers, it is backed by {@code BrokerViewOfBrokerConfig}, which merges 
the static defaults with
+ * operator-supplied overrides pushed dynamically from the Coordinator.
+ */
+public interface QueryConfigProvider
+{
+  Map<String, Object> getContext();
+}
diff --git 
a/server/src/main/java/org/apache/druid/client/BaseBrokerViewOfConfig.java 
b/server/src/main/java/org/apache/druid/client/BaseBrokerViewOfConfig.java
index ca483d1d46f..2f733f1a82d 100644
--- a/server/src/main/java/org/apache/druid/client/BaseBrokerViewOfConfig.java
+++ b/server/src/main/java/org/apache/druid/client/BaseBrokerViewOfConfig.java
@@ -71,7 +71,7 @@ public abstract class BaseBrokerViewOfConfig<DynamicConfig>
   public synchronized void setDynamicConfig(@NotNull DynamicConfig 
updatedConfig)
   {
     config = updatedConfig;
-    log.info("Updated %s to [%s]", getConfigTypeName(), updatedConfig);
+    log.info("Updated [%s] dynamic config to [%s]", getConfigTypeName(), 
updatedConfig);
   }
 
   /**
diff --git 
a/server/src/main/java/org/apache/druid/client/BrokerViewOfBrokerConfig.java 
b/server/src/main/java/org/apache/druid/client/BrokerViewOfBrokerConfig.java
index c85f09e828b..b67b4bc46da 100644
--- a/server/src/main/java/org/apache/druid/client/BrokerViewOfBrokerConfig.java
+++ b/server/src/main/java/org/apache/druid/client/BrokerViewOfBrokerConfig.java
@@ -28,27 +28,48 @@ import 
org.apache.druid.client.coordinator.CoordinatorClientImpl;
 import org.apache.druid.discovery.NodeRole;
 import org.apache.druid.guice.annotations.EscalatedGlobal;
 import org.apache.druid.guice.annotations.Json;
+import org.apache.druid.query.DefaultQueryConfig;
+import org.apache.druid.query.QueryConfigProvider;
+import org.apache.druid.query.QueryContext;
+import org.apache.druid.query.QueryContexts;
 import org.apache.druid.rpc.ServiceClientFactory;
 import org.apache.druid.rpc.ServiceLocator;
 import org.apache.druid.rpc.StandardRetryPolicy;
 import org.apache.druid.server.broker.BrokerDynamicConfig;
 
 import javax.validation.constraints.NotNull;
+import java.util.Map;
 
 /**
  * Broker view of broker dynamic configuration.
+ *
+ * <p>Also implements {@link QueryConfigProvider} to expose the effective 
default query context: the
+ * merge of static defaults (from {@link DefaultQueryConfig}) and 
operator-supplied overrides
+ * (from {@link BrokerDynamicConfig#getQueryContext()}). Dynamic values take 
precedence.
  */
 public class BrokerViewOfBrokerConfig extends 
BaseBrokerViewOfConfig<BrokerDynamicConfig>
+    implements QueryConfigProvider
 {
   private final CoordinatorClient coordinatorClient;
+  private final DefaultQueryConfig defaultQueryConfig;
+
+  /**
+   * Pre-computed merge of {@link DefaultQueryConfig#getContext()} and
+   * {@link BrokerDynamicConfig#getQueryContext()}, recomputed on each config 
sync.
+   * Dynamic config values override static defaults. {@link QueryContext} 
provides immutability.
+   */
+  private volatile QueryContext resolvedDefaultQueryContext;
 
   @Inject
   public BrokerViewOfBrokerConfig(
       @Json final ObjectMapper jsonMapper,
       @EscalatedGlobal final ServiceClientFactory clientFactory,
-      @Coordinator final ServiceLocator serviceLocator
+      @Coordinator final ServiceLocator serviceLocator,
+      final DefaultQueryConfig defaultQueryConfig
   )
   {
+    this.defaultQueryConfig = defaultQueryConfig;
+    this.resolvedDefaultQueryContext = 
QueryContext.of(defaultQueryConfig.getContext());
     this.coordinatorClient =
         new CoordinatorClientImpl(
             clientFactory.makeClient(
@@ -61,9 +82,14 @@ public class BrokerViewOfBrokerConfig extends 
BaseBrokerViewOfConfig<BrokerDynam
   }
 
   @VisibleForTesting
-  public BrokerViewOfBrokerConfig(CoordinatorClient coordinatorClient)
+  public BrokerViewOfBrokerConfig(
+      final CoordinatorClient coordinatorClient,
+      final DefaultQueryConfig defaultQueryConfig
+  )
   {
     this.coordinatorClient = coordinatorClient;
+    this.defaultQueryConfig = defaultQueryConfig;
+    this.resolvedDefaultQueryContext = 
QueryContext.of(defaultQueryConfig.getContext());
   }
 
   @Override
@@ -79,11 +105,26 @@ public class BrokerViewOfBrokerConfig extends 
BaseBrokerViewOfConfig<BrokerDynam
   }
 
   /**
-   * Update the config view with a new broker dynamic config snapshot.
+   * Update the config view with a new broker dynamic config snapshot, and 
recompute the
+   * resolved default query context by merging static defaults with dynamic 
overrides.
    */
   @Override
   public synchronized void setDynamicConfig(@NotNull BrokerDynamicConfig 
updatedConfig)
   {
     super.setDynamicConfig(updatedConfig);
+    resolvedDefaultQueryContext = QueryContext.of(QueryContexts.override(
+        defaultQueryConfig.getContext(),
+        updatedConfig.getQueryContext().asMap()
+    ));
+  }
+
+  /**
+   * Returns the pre-computed merge of static {@link DefaultQueryConfig} 
context and dynamic
+   * {@link BrokerDynamicConfig#getQueryContext()}. Dynamic values take 
precedence over static defaults.
+   */
+  @Override
+  public Map<String, Object> getContext()
+  {
+    return resolvedDefaultQueryContext.asMap();
   }
 }
diff --git 
a/server/src/main/java/org/apache/druid/guice/QueryToolChestModule.java 
b/server/src/main/java/org/apache/druid/guice/QueryToolChestModule.java
index c8466f887d7..72f0e142b29 100644
--- a/server/src/main/java/org/apache/druid/guice/QueryToolChestModule.java
+++ b/server/src/main/java/org/apache/druid/guice/QueryToolChestModule.java
@@ -28,6 +28,7 @@ import 
org.apache.druid.query.DefaultGenericQueryMetricsFactory;
 import org.apache.druid.query.DefaultQueryConfig;
 import org.apache.druid.query.GenericQueryMetricsFactory;
 import org.apache.druid.query.Query;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryToolChest;
 import org.apache.druid.query.QueryToolChestWarehouse;
 import org.apache.druid.query.datasourcemetadata.DataSourceMetadataQuery;
@@ -61,6 +62,7 @@ import org.apache.druid.query.topn.TopNQuery;
 import org.apache.druid.query.topn.TopNQueryConfig;
 import org.apache.druid.query.topn.TopNQueryMetricsFactory;
 import org.apache.druid.query.topn.TopNQueryQueryToolChest;
+
 import java.util.Map;
 
 /**
@@ -99,6 +101,8 @@ public class QueryToolChestModule implements Module
     
binder.bind(QueryToolChestWarehouse.class).to(ConglomerateBackedToolChestWarehouse.class);
 
     JsonConfigProvider.bind(binder, "druid.query.default", 
DefaultQueryConfig.class);
+    // DefaultQueryContext defaults to the static DefaultQueryConfig; brokers 
override this binding.
+    binder.bind(QueryConfigProvider.class).to(DefaultQueryConfig.class);
     JsonConfigProvider.bind(binder, "druid.query.groupBy", 
GroupByQueryConfig.class);
     JsonConfigProvider.bind(binder, "druid.query.search", 
SearchQueryConfig.class);
     JsonConfigProvider.bind(binder, "druid.query.topN", TopNQueryConfig.class);
diff --git a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java 
b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java
index 6add90ace1f..def4bb5b831 100644
--- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java
+++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java
@@ -34,10 +34,10 @@ import org.apache.druid.java.util.common.guava.Sequences;
 import org.apache.druid.java.util.common.logger.Logger;
 import org.apache.druid.java.util.emitter.service.ServiceEmitter;
 import org.apache.druid.query.BaseQuery;
-import org.apache.druid.query.DefaultQueryConfig;
 import org.apache.druid.query.DruidMetrics;
 import org.apache.druid.query.GenericQueryMetricsFactory;
 import org.apache.druid.query.Query;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryContext;
 import org.apache.druid.query.QueryContexts;
 import org.apache.druid.query.QueryInterruptedException;
@@ -97,7 +97,7 @@ public class QueryLifecycle
   private final ServiceEmitter emitter;
   private final RequestLogger requestLogger;
   private final AuthorizerMapper authorizerMapper;
-  private final DefaultQueryConfig defaultQueryConfig;
+  private final QueryConfigProvider queryConfigProvider;
   private final AuthConfig authConfig;
   private final PolicyEnforcer policyEnforcer;
   private final List<QueryBlocklistRule> queryBlocklist;
@@ -120,7 +120,7 @@ public class QueryLifecycle
       final ServiceEmitter emitter,
       final RequestLogger requestLogger,
       final AuthorizerMapper authorizerMapper,
-      final DefaultQueryConfig defaultQueryConfig,
+      final QueryConfigProvider queryConfigProvider,
       final AuthConfig authConfig,
       final PolicyEnforcer policyEnforcer,
       final List<QueryBlocklistRule> queryBlocklist,
@@ -134,7 +134,7 @@ public class QueryLifecycle
     this.emitter = emitter;
     this.requestLogger = requestLogger;
     this.authorizerMapper = authorizerMapper;
-    this.defaultQueryConfig = defaultQueryConfig;
+    this.queryConfigProvider = queryConfigProvider;
     this.authConfig = authConfig;
     this.policyEnforcer = policyEnforcer;
     this.queryBlocklist = queryBlocklist;
@@ -217,7 +217,7 @@ public class QueryLifecycle
     }
 
     Map<String, Object> mergedUserAndConfigContext = QueryContexts.override(
-        defaultQueryConfig.getContext(),
+        queryConfigProvider.getContext(),
         baseQuery.getContext()
     );
     mergedUserAndConfigContext.put(BaseQuery.QUERY_ID, queryId);
diff --git 
a/server/src/main/java/org/apache/druid/server/QueryLifecycleFactory.java 
b/server/src/main/java/org/apache/druid/server/QueryLifecycleFactory.java
index 66d9ed4f629..b32a5aafffb 100644
--- a/server/src/main/java/org/apache/druid/server/QueryLifecycleFactory.java
+++ b/server/src/main/java/org/apache/druid/server/QueryLifecycleFactory.java
@@ -19,13 +19,12 @@
 
 package org.apache.druid.server;
 
-import com.google.common.base.Supplier;
 import com.google.inject.Inject;
 import org.apache.druid.client.BrokerViewOfBrokerConfig;
 import org.apache.druid.guice.LazySingleton;
 import org.apache.druid.java.util.emitter.service.ServiceEmitter;
-import org.apache.druid.query.DefaultQueryConfig;
 import org.apache.druid.query.GenericQueryMetricsFactory;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryRunnerFactoryConglomerate;
 import org.apache.druid.query.QuerySegmentWalker;
 import org.apache.druid.query.policy.PolicyEnforcer;
@@ -46,7 +45,7 @@ public class QueryLifecycleFactory
   private final ServiceEmitter emitter;
   private final RequestLogger requestLogger;
   private final AuthorizerMapper authorizerMapper;
-  private final DefaultQueryConfig defaultQueryConfig;
+  private final QueryConfigProvider queryConfigProvider;
   private final AuthConfig authConfig;
   private final PolicyEnforcer policyEnforcer;
   private final BrokerViewOfBrokerConfig brokerViewOfBrokerConfig;
@@ -61,7 +60,7 @@ public class QueryLifecycleFactory
       final AuthConfig authConfig,
       final PolicyEnforcer policyEnforcer,
       final AuthorizerMapper authorizerMapper,
-      final Supplier<DefaultQueryConfig> queryConfigSupplier,
+      final QueryConfigProvider queryConfigProvider,
       @Nullable final BrokerViewOfBrokerConfig brokerViewOfBrokerConfig
   )
   {
@@ -71,7 +70,7 @@ public class QueryLifecycleFactory
     this.emitter = emitter;
     this.requestLogger = requestLogger;
     this.authorizerMapper = authorizerMapper;
-    this.defaultQueryConfig = queryConfigSupplier.get();
+    this.queryConfigProvider = queryConfigProvider;
     this.authConfig = authConfig;
     this.policyEnforcer = policyEnforcer;
     this.brokerViewOfBrokerConfig = brokerViewOfBrokerConfig;
@@ -79,13 +78,10 @@ public class QueryLifecycleFactory
 
   public QueryLifecycle factorize()
   {
-    // Extract query blocklist from broker config, or use empty list if not on 
broker
-    final List<QueryBlocklistRule> queryBlocklist;
-    if (brokerViewOfBrokerConfig != null && 
brokerViewOfBrokerConfig.getDynamicConfig() != null) {
-      queryBlocklist = 
brokerViewOfBrokerConfig.getDynamicConfig().getQueryBlocklist();
-    } else {
-      queryBlocklist = Collections.emptyList();
-    }
+    final List<QueryBlocklistRule> queryBlocklist =
+        brokerViewOfBrokerConfig != null && 
brokerViewOfBrokerConfig.getDynamicConfig() != null
+        ? brokerViewOfBrokerConfig.getDynamicConfig().getQueryBlocklist()
+        : Collections.emptyList();
 
     return new QueryLifecycle(
         conglomerate,
@@ -94,7 +90,7 @@ public class QueryLifecycleFactory
         emitter,
         requestLogger,
         authorizerMapper,
-        defaultQueryConfig,
+        queryConfigProvider,
         authConfig,
         policyEnforcer,
         queryBlocklist,
diff --git 
a/server/src/main/java/org/apache/druid/server/broker/BrokerDynamicConfig.java 
b/server/src/main/java/org/apache/druid/server/broker/BrokerDynamicConfig.java
index e5156bbb889..45c646be8a7 100644
--- 
a/server/src/main/java/org/apache/druid/server/broker/BrokerDynamicConfig.java
+++ 
b/server/src/main/java/org/apache/druid/server/broker/BrokerDynamicConfig.java
@@ -20,9 +20,9 @@
 package org.apache.druid.server.broker;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import org.apache.druid.common.config.Configs;
+import org.apache.druid.query.QueryContext;
 import org.apache.druid.server.QueryBlocklistRule;
 
 import javax.annotation.Nullable;
@@ -46,21 +46,34 @@ public class BrokerDynamicConfig
    */
   private final List<QueryBlocklistRule> queryBlocklist;
 
+  /**
+   * Default query context values set dynamically by operators. These override 
static defaults from
+   * {@link org.apache.druid.query.DefaultQueryConfig} but are overridden by 
per-query context.
+   */
+  private final QueryContext queryContext;
+
   @JsonCreator
   public BrokerDynamicConfig(
-      @JsonProperty("queryBlocklist") @Nullable List<QueryBlocklistRule> 
queryBlocklist
+      @JsonProperty("queryBlocklist") @Nullable List<QueryBlocklistRule> 
queryBlocklist,
+      @JsonProperty("queryContext") @Nullable QueryContext queryContext
   )
   {
     this.queryBlocklist = Configs.valueOrDefault(queryBlocklist, 
Collections.emptyList());
+    this.queryContext = Configs.valueOrDefault(queryContext, 
QueryContext.empty());
   }
 
   @JsonProperty
-  @JsonInclude(JsonInclude.Include.NON_EMPTY)
   public List<QueryBlocklistRule> getQueryBlocklist()
   {
     return queryBlocklist;
   }
 
+  @JsonProperty
+  public QueryContext getQueryContext()
+  {
+    return queryContext;
+  }
+
   @Override
   public boolean equals(Object o)
   {
@@ -71,13 +84,14 @@ public class BrokerDynamicConfig
       return false;
     }
     BrokerDynamicConfig that = (BrokerDynamicConfig) o;
-    return Objects.equals(queryBlocklist, that.queryBlocklist);
+    return Objects.equals(queryBlocklist, that.queryBlocklist)
+           && Objects.equals(queryContext, that.queryContext);
   }
 
   @Override
   public int hashCode()
   {
-    return Objects.hash(queryBlocklist);
+    return Objects.hash(queryBlocklist, queryContext);
   }
 
   @Override
@@ -85,6 +99,7 @@ public class BrokerDynamicConfig
   {
     return "BrokerDynamicConfig{" +
            "queryBlocklist=" + queryBlocklist +
+           ", queryContext=" + queryContext +
            '}';
   }
 
@@ -96,6 +111,7 @@ public class BrokerDynamicConfig
   public static class Builder
   {
     private List<QueryBlocklistRule> queryBlocklist;
+    private QueryContext queryContext;
 
     public Builder()
     {
@@ -103,10 +119,12 @@ public class BrokerDynamicConfig
 
     @JsonCreator
     public Builder(
-        @JsonProperty("queryBlocklist") @Nullable List<QueryBlocklistRule> 
queryBlocklist
+        @JsonProperty("queryBlocklist") @Nullable List<QueryBlocklistRule> 
queryBlocklist,
+        @JsonProperty("queryContext") @Nullable QueryContext queryContext
     )
     {
       this.queryBlocklist = queryBlocklist;
+      this.queryContext = queryContext;
     }
 
     public Builder withQueryBlocklist(List<QueryBlocklistRule> queryBlocklist)
@@ -115,9 +133,15 @@ public class BrokerDynamicConfig
       return this;
     }
 
+    public Builder withQueryContext(QueryContext queryContext)
+    {
+      this.queryContext = queryContext;
+      return this;
+    }
+
     public BrokerDynamicConfig build()
     {
-      return new BrokerDynamicConfig(queryBlocklist);
+      return new BrokerDynamicConfig(queryBlocklist, queryContext);
     }
 
     /**
@@ -127,7 +151,8 @@ public class BrokerDynamicConfig
     public BrokerDynamicConfig build(@Nullable BrokerDynamicConfig defaults)
     {
       return new BrokerDynamicConfig(
-        Configs.valueOrDefault(queryBlocklist, defaults != null ? 
defaults.getQueryBlocklist() : null)
+          Configs.valueOrDefault(queryBlocklist, defaults != null ? 
defaults.getQueryBlocklist() : null),
+          Configs.valueOrDefault(queryContext, defaults != null ? 
defaults.getQueryContext() : null)
       );
     }
   }
diff --git 
a/server/src/test/java/org/apache/druid/client/BrokerViewOfBrokerConfigTest.java
 
b/server/src/test/java/org/apache/druid/client/BrokerViewOfBrokerConfigTest.java
index a685abadafa..c4877220ffc 100644
--- 
a/server/src/test/java/org/apache/druid/client/BrokerViewOfBrokerConfigTest.java
+++ 
b/server/src/test/java/org/apache/druid/client/BrokerViewOfBrokerConfigTest.java
@@ -19,29 +19,36 @@
 
 package org.apache.druid.client;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.common.util.concurrent.Futures;
 import org.apache.druid.client.coordinator.CoordinatorClient;
+import org.apache.druid.query.DefaultQueryConfig;
+import org.apache.druid.query.QueryContext;
 import org.apache.druid.server.broker.BrokerDynamicConfig;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mockito;
 
+import java.util.Map;
+
 public class BrokerViewOfBrokerConfigTest
 {
   private BrokerViewOfBrokerConfig target;
 
   private CoordinatorClient coordinatorClient;
   private BrokerDynamicConfig config;
+  private DefaultQueryConfig defaultQueryConfig;
 
 
   @Before
   public void setUp() throws Exception
   {
     config = BrokerDynamicConfig.builder().build();
+    defaultQueryConfig = new DefaultQueryConfig(ImmutableMap.of("timeout", 
30000, "useCache", true));
     coordinatorClient = Mockito.mock(CoordinatorClient.class);
     
Mockito.when(coordinatorClient.getBrokerDynamicConfig()).thenReturn(Futures.immediateFuture(config));
-    target = new BrokerViewOfBrokerConfig(coordinatorClient);
+    target = new BrokerViewOfBrokerConfig(coordinatorClient, 
defaultQueryConfig);
   }
 
   @Test
@@ -51,4 +58,28 @@ public class BrokerViewOfBrokerConfigTest
     Mockito.verify(coordinatorClient, 
Mockito.times(1)).getBrokerDynamicConfig();
     Assert.assertEquals(config, target.getDynamicConfig());
   }
+
+  @Test
+  public void testResolvedContextMergesDynamicOverStaticDefaults()
+  {
+    // Dynamic config overrides "useCache" and adds "priority"; static 
"timeout" is preserved.
+    final BrokerDynamicConfig dynamicConfig = BrokerDynamicConfig.builder()
+                                                                 
.withQueryContext(
+                                                                     
QueryContext.of(ImmutableMap.of("useCache", false, "priority", 5))
+                                                                 )
+                                                                 .build();
+    target.setDynamicConfig(dynamicConfig);
+
+    final Map<String, Object> resolved = target.getContext();
+    Assert.assertEquals(30000, resolved.get("timeout"));
+    Assert.assertEquals(false, resolved.get("useCache"));
+    Assert.assertEquals(5, resolved.get("priority"));
+  }
+
+  @Test
+  public void 
testResolvedContextEqualsStaticDefaultsWhenDynamicContextIsEmpty()
+  {
+    target.setDynamicConfig(BrokerDynamicConfig.builder().build());
+    Assert.assertEquals(defaultQueryConfig.getContext(), target.getContext());
+  }
 }
diff --git 
a/server/src/test/java/org/apache/druid/segment/metadata/SegmentMetadataCacheTestBase.java
 
b/server/src/test/java/org/apache/druid/segment/metadata/SegmentMetadataCacheTestBase.java
index d36d4a7c191..10ec3992b97 100644
--- 
a/server/src/test/java/org/apache/druid/segment/metadata/SegmentMetadataCacheTestBase.java
+++ 
b/server/src/test/java/org/apache/druid/segment/metadata/SegmentMetadataCacheTestBase.java
@@ -19,7 +19,6 @@
 
 package org.apache.druid.segment.metadata;
 
-import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import org.apache.druid.data.input.InputRow;
@@ -292,7 +291,7 @@ public abstract class SegmentMetadataCacheTestBase extends 
InitializedNullHandli
         new AuthConfig(),
         NoopPolicyEnforcer.instance(),
         AuthTestUtils.TEST_AUTHORIZER_MAPPER,
-        Suppliers.ofInstance(new DefaultQueryConfig(Map.of())),
+        new DefaultQueryConfig(Map.of()),
         null
     );
   }
diff --git 
a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java 
b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java
index 1f21c065b18..b478ab9befc 100644
--- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java
+++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java
@@ -19,7 +19,6 @@
 
 package org.apache.druid.server;
 
-import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -37,10 +36,10 @@ import org.apache.druid.java.util.common.Intervals;
 import org.apache.druid.java.util.common.guava.Sequences;
 import org.apache.druid.java.util.emitter.service.ServiceEmitter;
 import org.apache.druid.query.DataSource;
-import org.apache.druid.query.DefaultQueryConfig;
 import org.apache.druid.query.Druids;
 import org.apache.druid.query.GenericQueryMetricsFactory;
 import org.apache.druid.query.Query;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryContextTest;
 import org.apache.druid.query.QueryMetrics;
 import org.apache.druid.query.QueryRunner;
@@ -118,9 +117,9 @@ public class QueryLifecycleTest
   @Bind
   AuthorizerMapper authzMapper;
 
-  DefaultQueryConfig queryConfig;
+  QueryConfigProvider queryConfig;
   @Bind(lazy = true)
-  Supplier<DefaultQueryConfig> queryConfigSupplier;
+  QueryConfigProvider queryConfigProvider;
 
   @Bind(lazy = true)
   AuthConfig authConfig;
@@ -151,8 +150,8 @@ public class QueryLifecycleTest
     requestLogger = EasyMock.createNiceMock(RequestLogger.class);
     authorizer = EasyMock.createMock(Authorizer.class);
     authzMapper = new AuthorizerMapper(ImmutableMap.of(AUTHORIZER, 
authorizer));
-    queryConfig = EasyMock.createMock(DefaultQueryConfig.class);
-    queryConfigSupplier = () -> queryConfig;
+    queryConfig = EasyMock.createMock(QueryConfigProvider.class);
+    queryConfigProvider = queryConfig;
 
     toolChest = EasyMock.createMock(QueryToolChest.class);
     runner = EasyMock.createMock(QueryRunner.class);
diff --git 
a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java 
b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
index 9705e3cbb58..bf16044943e 100644
--- a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
+++ b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
@@ -23,7 +23,6 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.jaxrs.smile.SmileMediaTypes;
-import com.google.common.base.Suppliers;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -274,7 +273,7 @@ public class QueryResourceTest
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
-            Suppliers.ofInstance(new DefaultQueryConfig(Map.of())),
+            new DefaultQueryConfig(Map.of()),
             null
         ),
         jsonMapper,
@@ -324,7 +323,7 @@ public class QueryResourceTest
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
-            Suppliers.ofInstance(overrideConfig),
+            overrideConfig,
             null
         ),
         jsonMapper,
@@ -405,7 +404,7 @@ public class QueryResourceTest
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
-            Suppliers.ofInstance(overrideConfig),
+            overrideConfig,
             null
         ),
         jsonMapper,
@@ -506,7 +505,7 @@ public class QueryResourceTest
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
-            Suppliers.ofInstance(new DefaultQueryConfig(Map.of())),
+            new DefaultQueryConfig(Map.of()),
             null
         ),
         jsonMapper,
@@ -592,7 +591,7 @@ public class QueryResourceTest
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
-            Suppliers.ofInstance(new DefaultQueryConfig(Map.of())),
+            new DefaultQueryConfig(Map.of()),
             null
         ),
         jsonMapper,
@@ -691,7 +690,7 @@ public class QueryResourceTest
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
-            Suppliers.ofInstance(new DefaultQueryConfig(Map.of())),
+            new DefaultQueryConfig(Map.of()),
             null
         ),
         jsonMapper,
@@ -761,7 +760,7 @@ public class QueryResourceTest
 
     queryResource = new QueryResource(
 
-        new QueryLifecycleFactory(null, null, null, null, null, null, 
NoopPolicyEnforcer.instance(), null, Suppliers.ofInstance(overrideConfig), null)
+        new QueryLifecycleFactory(null, null, null, null, null, null, 
NoopPolicyEnforcer.instance(), null, overrideConfig, null)
         {
           @Override
           public QueryLifecycle factorize()
@@ -838,7 +837,7 @@ public class QueryResourceTest
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
-            Suppliers.ofInstance(overrideConfig),
+            overrideConfig,
             null
         ),
         jsonMapper,
@@ -1110,7 +1109,7 @@ public class QueryResourceTest
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
             authMapper,
-            Suppliers.ofInstance(new DefaultQueryConfig(Map.of())),
+            new DefaultQueryConfig(Map.of()),
             null
         ),
         jsonMapper,
@@ -1192,7 +1191,7 @@ public class QueryResourceTest
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
-            Suppliers.ofInstance(new DefaultQueryConfig(Map.of())),
+            new DefaultQueryConfig(Map.of()),
             null
         ),
         jsonMapper,
@@ -1297,7 +1296,7 @@ public class QueryResourceTest
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
             authMapper,
-            Suppliers.ofInstance(new DefaultQueryConfig(Map.of())),
+            new DefaultQueryConfig(Map.of()),
             null
         ),
         jsonMapper,
@@ -1411,7 +1410,7 @@ public class QueryResourceTest
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
             authMapper,
-            Suppliers.ofInstance(new DefaultQueryConfig(Map.of())),
+            new DefaultQueryConfig(Map.of()),
             null
         ),
         jsonMapper,
@@ -1801,7 +1800,7 @@ public class QueryResourceTest
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
             AuthTestUtils.TEST_AUTHORIZER_MAPPER,
-            Suppliers.ofInstance(new DefaultQueryConfig(Map.of())),
+            new DefaultQueryConfig(Map.of()),
             null
         ),
         jsonMapper,
diff --git 
a/server/src/test/java/org/apache/druid/server/broker/BrokerDynamicConfigTest.java
 
b/server/src/test/java/org/apache/druid/server/broker/BrokerDynamicConfigTest.java
index ad57a1ca4e1..06e4df77c0a 100644
--- 
a/server/src/test/java/org/apache/druid/server/broker/BrokerDynamicConfigTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/broker/BrokerDynamicConfigTest.java
@@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import nl.jqno.equalsverifier.EqualsVerifier;
+import org.apache.druid.query.QueryContext;
 import org.apache.druid.segment.TestHelper;
 import org.apache.druid.server.QueryBlocklistRule;
 import org.junit.Assert;
@@ -115,6 +116,32 @@ public class BrokerDynamicConfigTest
     Assert.assertEquals(ImmutableMap.of("priority", "0"), 
rule2.getContextMatches());
   }
 
+  @Test
+  public void testSerdeWithQueryContext() throws Exception
+  {
+    String jsonStr = "{\n"
+                     + "  \"queryContext\": {\n"
+                     + "    \"priority\": 10,\n"
+                     + "    \"useCache\": false\n"
+                     + "  }\n"
+                     + "}\n";
+
+    BrokerDynamicConfig actual = mapper.readValue(
+        mapper.writeValueAsString(mapper.readValue(jsonStr, 
BrokerDynamicConfig.class)),
+        BrokerDynamicConfig.class
+    );
+
+    Assert.assertEquals(QueryContext.of(ImmutableMap.of("priority", 10, 
"useCache", false)), actual.getQueryContext());
+  }
+
+  @Test
+  public void testNullQueryContextDefaultsToEmptyMap() throws Exception
+  {
+    BrokerDynamicConfig actual = mapper.readValue("{}", 
BrokerDynamicConfig.class);
+    Assert.assertNotNull(actual.getQueryContext());
+    Assert.assertTrue(actual.getQueryContext().isEmpty());
+  }
+
   @Test
   public void testEquals()
   {
diff --git a/services/src/main/java/org/apache/druid/cli/CliBroker.java 
b/services/src/main/java/org/apache/druid/cli/CliBroker.java
index b9e39479b06..c622abc4fcc 100644
--- a/services/src/main/java/org/apache/druid/cli/CliBroker.java
+++ b/services/src/main/java/org/apache/druid/cli/CliBroker.java
@@ -26,6 +26,7 @@ import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.Module;
 import com.google.inject.name.Names;
+import com.google.inject.util.Modules;
 import org.apache.druid.client.BrokerSegmentWatcherConfig;
 import org.apache.druid.client.BrokerServerView;
 import org.apache.druid.client.BrokerViewOfBrokerConfig;
@@ -66,6 +67,7 @@ import org.apache.druid.msq.guice.MSQExternalDataSourceModule;
 import org.apache.druid.msq.guice.MSQIndexingModule;
 import org.apache.druid.msq.guice.MSQSqlModule;
 import org.apache.druid.msq.guice.SqlTaskModule;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QuerySegmentWalker;
 import org.apache.druid.query.RetryQueryRunnerConfig;
 import org.apache.druid.query.lookup.LookupModule;
@@ -131,7 +133,9 @@ public class CliBroker extends ServerRunnable
     return ImmutableList.of(
         new BrokerProcessingModule(),
         new QueryableModule(),
-        new QueryRunnerFactoryModule(),
+        Modules.override(new QueryRunnerFactoryModule()).with(
+            overrideBinder -> 
overrideBinder.bind(QueryConfigProvider.class).to(BrokerViewOfBrokerConfig.class)
+        ),
         new SegmentWranglerModule(),
         new JoinableFactoryModule(),
         new BrokerServiceModule(),
diff --git a/sql/src/main/java/org/apache/druid/sql/avatica/DruidMeta.java 
b/sql/src/main/java/org/apache/druid/sql/avatica/DruidMeta.java
index 0410326687b..ff27f020832 100644
--- a/sql/src/main/java/org/apache/druid/sql/avatica/DruidMeta.java
+++ b/sql/src/main/java/org/apache/druid/sql/avatica/DruidMeta.java
@@ -43,7 +43,7 @@ import org.apache.druid.java.util.common.ISE;
 import org.apache.druid.java.util.common.UOE;
 import org.apache.druid.java.util.common.lifecycle.LifecycleStop;
 import org.apache.druid.java.util.common.logger.Logger;
-import org.apache.druid.query.DefaultQueryConfig;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryContexts;
 import org.apache.druid.server.security.Access;
 import org.apache.druid.server.security.AuthenticationResult;
@@ -129,7 +129,7 @@ public class DruidMeta extends MetaImpl
   );
 
   private final SqlStatementFactory sqlStatementFactory;
-  private final DefaultQueryConfig defaultQueryConfig;
+  private final QueryConfigProvider queryConfigProvider;
   private final ScheduledExecutorService exec;
   private final AvaticaServerConfig config;
   private final List<Authenticator> authenticators;
@@ -150,7 +150,7 @@ public class DruidMeta extends MetaImpl
   @Inject
   public DruidMeta(
       final @NativeQuery SqlStatementFactory sqlStatementFactory,
-      final DefaultQueryConfig defaultQueryConfig,
+      final QueryConfigProvider queryConfigProvider,
       final AvaticaServerConfig config,
       final ErrorHandler errorHandler,
       final AuthenticatorMapper authMapper
@@ -158,7 +158,7 @@ public class DruidMeta extends MetaImpl
   {
     this(
         sqlStatementFactory,
-        defaultQueryConfig,
+        queryConfigProvider,
         config,
         errorHandler,
         Executors.newSingleThreadScheduledExecutor(
@@ -174,7 +174,7 @@ public class DruidMeta extends MetaImpl
 
   public DruidMeta(
       final SqlStatementFactory sqlStatementFactory,
-      final DefaultQueryConfig defaultQueryConfig,
+      final QueryConfigProvider queryConfigProvider,
       final AvaticaServerConfig config,
       final ErrorHandler errorHandler,
       final ScheduledExecutorService exec,
@@ -184,7 +184,7 @@ public class DruidMeta extends MetaImpl
   {
     super(null);
     this.sqlStatementFactory = sqlStatementFactory;
-    this.defaultQueryConfig = defaultQueryConfig;
+    this.queryConfigProvider = queryConfigProvider;
     this.config = config;
     this.errorHandler = errorHandler;
     this.exec = exec;
@@ -267,7 +267,7 @@ public class DruidMeta extends MetaImpl
   {
     try {
       final DruidJdbcStatement druidStatement = getDruidConnection(ch.id)
-          .createStatement(sqlStatementFactory, 
defaultQueryConfig.getContext(), fetcherFactory);
+          .createStatement(sqlStatementFactory, 
queryConfigProvider.getContext(), fetcherFactory);
       return new StatementHandle(ch.id, druidStatement.getStatementId(), null);
     }
     catch (Throwable t) {
@@ -291,14 +291,14 @@ public class DruidMeta extends MetaImpl
       final DruidConnection druidConnection = getDruidConnection(ch.id);
       final SqlQueryPlus sqlReq = SqlQueryPlus.builder()
                                               .sql(sql)
-                                              
.systemDefaultContext(defaultQueryConfig.getContext())
+                                              
.systemDefaultContext(queryConfigProvider.getContext())
                                               
.queryContext(druidConnection.sessionContext())
                                               
.auth(doAuthenticate(druidConnection))
                                               .buildJdbc();
       final DruidJdbcPreparedStatement stmt = 
getDruidConnection(ch.id).createPreparedStatement(
           sqlStatementFactory,
           sqlReq,
-          defaultQueryConfig.getContext(),
+          queryConfigProvider.getContext(),
           maxRowCount,
           fetcherFactory
       );
diff --git a/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java 
b/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java
index d5b6876129b..bac387f3d18 100644
--- a/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java
+++ b/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java
@@ -28,7 +28,7 @@ import 
org.apache.druid.common.exception.ErrorResponseTransformStrategy;
 import org.apache.druid.error.DruidException;
 import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.java.util.common.logger.Logger;
-import org.apache.druid.query.DefaultQueryConfig;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryContext;
 import org.apache.druid.query.QueryContexts;
 import org.apache.druid.server.QueryResource;
@@ -86,7 +86,7 @@ public class SqlResource
   private final SqlResourceQueryResultPusherFactory resultPusherFactory;
   private final SqlLifecycleManager sqlLifecycleManager;
   private final SqlEngineRegistry sqlEngineRegistry;
-  private final DefaultQueryConfig defaultQueryConfig;
+  private final QueryConfigProvider queryConfigProvider;
   private final ServerConfig serverConfig;
 
   @VisibleForTesting
@@ -96,7 +96,7 @@ public class SqlResource
       final SqlLifecycleManager sqlLifecycleManager,
       final SqlEngineRegistry sqlEngineRegistry,
       final SqlResourceQueryResultPusherFactory resultPusherFactory,
-      final DefaultQueryConfig defaultQueryConfig,
+      final QueryConfigProvider queryConfigProvider,
       final ServerConfig serverConfig
   )
   {
@@ -104,7 +104,7 @@ public class SqlResource
     this.sqlEngineRegistry = Preconditions.checkNotNull(sqlEngineRegistry, 
"sqlEngineRegistry");
     this.authorizerMapper = Preconditions.checkNotNull(authorizerMapper, 
"authorizerMapper");
     this.sqlLifecycleManager = Preconditions.checkNotNull(sqlLifecycleManager, 
"sqlLifecycleManager");
-    this.defaultQueryConfig = Preconditions.checkNotNull(defaultQueryConfig, 
"defaultQueryConfig");
+    this.queryConfigProvider = Preconditions.checkNotNull(queryConfigProvider, 
"queryConfigProvider");
     this.serverConfig = serverConfig;
   }
 
@@ -241,7 +241,7 @@ public class SqlResource
     final QueryContext queryContext;
 
     try {
-      SqlQueryPlus sqlQueryPlus = makeSqlQueryPlus(sqlQuery, req, 
defaultQueryConfig.getContext());
+      SqlQueryPlus sqlQueryPlus = makeSqlQueryPlus(sqlQuery, req, 
queryConfigProvider.getContext());
 
       // Redefine queryContext to include SET parameters and default context.
       queryContext = new QueryContext(sqlQueryPlus.context());
diff --git 
a/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java 
b/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java
index 00d2e771e78..2c963468ef8 100644
--- 
a/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java
+++ 
b/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java
@@ -56,6 +56,7 @@ import 
org.apache.druid.java.util.emitter.service.ServiceEmitter;
 import org.apache.druid.math.expr.ExprMacroTable;
 import org.apache.druid.query.BaseQuery;
 import org.apache.druid.query.DefaultQueryConfig;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryRunnerFactoryConglomerate;
 import org.apache.druid.query.http.ClientSqlParameter;
 import org.apache.druid.query.policy.NoopPolicyEnforcer;
@@ -286,6 +287,7 @@ public class DruidAvaticaHandlerTest extends CalciteTestBase
                     
.toInstance(AuthConfig.newBuilder().setAuthorizeQueryContextParams(true).build());
               binder.bind(DefaultQueryConfig.class)
                     .toInstance(new 
DefaultQueryConfig(ImmutableMap.of("forbidden-key", "system-default-value")));
+              
binder.bind(QueryConfigProvider.class).to(DefaultQueryConfig.class);
               binder.bind(RequestLogger.class).toInstance(testRequestLogger);
               binder.bind(DruidSchemaCatalog.class).toInstance(rootSchema);
               for (NamedSchema schema : rootSchema.getNamedSchemas().values()) 
{
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/util/QueryFrameworkUtils.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/util/QueryFrameworkUtils.java
index e3f3a60c6a8..2ba1238cd2b 100644
--- 
a/sql/src/test/java/org/apache/druid/sql/calcite/util/QueryFrameworkUtils.java
+++ 
b/sql/src/test/java/org/apache/druid/sql/calcite/util/QueryFrameworkUtils.java
@@ -19,7 +19,6 @@
 
 package org.apache.druid.sql.calcite.util;
 
-import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.inject.Injector;
@@ -103,7 +102,7 @@ public class QueryFrameworkUtils
         new AuthConfig(),
         NoopPolicyEnforcer.instance(),
         authorizerMapper,
-        Suppliers.ofInstance(new DefaultQueryConfig(Map.of())),
+        new DefaultQueryConfig(Map.of()),
         null  // BrokerConfigManager - null for tests
     );
   }
diff --git a/sql/src/test/java/org/apache/druid/sql/guice/SqlModuleTest.java 
b/sql/src/test/java/org/apache/druid/sql/guice/SqlModuleTest.java
index 91440ee83a6..8c86d913d67 100644
--- a/sql/src/test/java/org/apache/druid/sql/guice/SqlModuleTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/guice/SqlModuleTest.java
@@ -19,14 +19,11 @@
 
 package org.apache.druid.sql.guice;
 
-import com.google.common.base.Supplier;
-import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
 import com.google.inject.Binder;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.Key;
-import com.google.inject.TypeLiteral;
 import org.apache.druid.client.BrokerViewOfCoordinatorConfig;
 import org.apache.druid.client.FilteredServerInventoryView;
 import org.apache.druid.client.TimelineServerView;
@@ -49,6 +46,7 @@ import org.apache.druid.java.util.http.client.HttpClient;
 import org.apache.druid.math.expr.ExprMacroTable;
 import org.apache.druid.query.DefaultQueryConfig;
 import org.apache.druid.query.GenericQueryMetricsFactory;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.query.QueryRunnerFactoryConglomerate;
 import org.apache.druid.query.QuerySegmentWalker;
 import org.apache.druid.query.QueryToolChestWarehouse;
@@ -200,7 +198,7 @@ public class SqlModuleTest
               binder.bind(Escalator.class).toInstance(new NoopEscalator());
               binder.bind(ServiceEmitter.class).toInstance(serviceEmitter);
               
binder.bind(RequestLogger.class).toInstance(NoopRequestLogger.instance());
-              binder.bind(new 
TypeLiteral<Supplier<DefaultQueryConfig>>(){}).toInstance(Suppliers.ofInstance(new
 DefaultQueryConfig(null)));
+              
binder.bind(QueryConfigProvider.class).to(DefaultQueryConfig.class);
               
binder.bind(FilteredServerInventoryView.class).toInstance(inventoryView);
               
binder.bind(TimelineServerView.class).toInstance(timelineServerView);
               
binder.bind(DruidNodeDiscoveryProvider.class).toInstance(druidNodeDiscoveryProvider);
diff --git a/sql/src/test/java/org/apache/druid/sql/http/SqlHttpModuleTest.java 
b/sql/src/test/java/org/apache/druid/sql/http/SqlHttpModuleTest.java
index efed7674620..87a501f00a8 100644
--- a/sql/src/test/java/org/apache/druid/sql/http/SqlHttpModuleTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/http/SqlHttpModuleTest.java
@@ -32,6 +32,7 @@ import org.apache.druid.guice.annotations.Json;
 import org.apache.druid.guice.annotations.NativeQuery;
 import org.apache.druid.guice.annotations.Self;
 import org.apache.druid.query.DefaultQueryConfig;
+import org.apache.druid.query.QueryConfigProvider;
 import org.apache.druid.server.DruidNode;
 import org.apache.druid.server.ResponseContextConfig;
 import org.apache.druid.server.security.AuthorizerMapper;
@@ -74,6 +75,7 @@ public class SqlHttpModuleTest
                 .annotatedWith(NativeQuery.class)
                 .toInstance(EasyMock.mock(SqlStatementFactory.class));
           
binder.bind(DefaultQueryConfig.class).toInstance(DefaultQueryConfig.NIL);
+          binder.bind(QueryConfigProvider.class).to(DefaultQueryConfig.class);
         },
         target
     );
diff --git 
a/web-console/src/dialogs/broker-dynamic-config-dialog/broker-dynamic-config-completions.ts
 
b/web-console/src/dialogs/broker-dynamic-config-dialog/broker-dynamic-config-completions.ts
index 7572d838e9e..6620dc721f0 100644
--- 
a/web-console/src/dialogs/broker-dynamic-config-dialog/broker-dynamic-config-completions.ts
+++ 
b/web-console/src/dialogs/broker-dynamic-config-dialog/broker-dynamic-config-completions.ts
@@ -29,6 +29,32 @@ export const BROKER_DYNAMIC_CONFIG_COMPLETIONS: 
JsonCompletionRule[] = [
         documentation:
           'List of rules to block queries on brokers. Each rule can match by 
datasource, query type, and/or context parameters.',
       },
+      {
+        value: 'queryContext',
+        documentation:
+          'Default query context values applied to all queries on this broker. 
These override static defaults from runtime properties but are overridden by 
per-query context values. Useful for setting cluster-wide defaults like 
priority or timeout without restarting.',
+      },
+    ],
+  },
+  // Query context key suggestions
+  {
+    path: '$.queryContext',
+    isObject: true,
+    completions: [
+      { value: 'priority', documentation: 'Query scheduling priority' },
+      { value: 'timeout', documentation: 'Query timeout in milliseconds' },
+      { value: 'useCache', documentation: 'Whether to use the query cache' },
+      { value: 'populateCache', documentation: 'Whether to populate the query 
cache' },
+      { value: 'useResultLevelCache', documentation: 'Whether to use 
result-level caching' },
+      {
+        value: 'populateResultLevelCache',
+        documentation: 'Whether to populate the result-level cache',
+      },
+      { value: 'maxScatterGatherBytes', documentation: 'Max bytes to gather 
across segments' },
+      {
+        value: 'maxQueuedBytes',
+        documentation: 'Max bytes queued before backpressure kicks in',
+      },
     ],
   },
   // Query blocklist rule properties
diff --git 
a/web-console/src/druid-models/broker-dynamic-config/broker-dynamic-config.tsx 
b/web-console/src/druid-models/broker-dynamic-config/broker-dynamic-config.tsx
index 63ce036d3cb..9fc66cd4fa1 100644
--- 
a/web-console/src/druid-models/broker-dynamic-config/broker-dynamic-config.tsx
+++ 
b/web-console/src/druid-models/broker-dynamic-config/broker-dynamic-config.tsx
@@ -20,6 +20,7 @@ import type { Field } from '../../components';
 
 export interface BrokerDynamicConfig {
   queryBlocklist?: QueryBlocklistRule[];
+  queryContext?: Record<string, unknown>;
 }
 
 export interface QueryBlocklistRule {
@@ -29,4 +30,25 @@ export interface QueryBlocklistRule {
   contextMatches?: Record<string, string>;
 }
 
-export const BROKER_DYNAMIC_CONFIG_FIELDS: Field<BrokerDynamicConfig>[] = [];
+export const BROKER_DYNAMIC_CONFIG_FIELDS: Field<BrokerDynamicConfig>[] = [
+  {
+    name: 'queryContext',
+    type: 'json',
+    info: (
+      <>
+        Default query context values applied to all queries on this broker. 
These override static
+        defaults from runtime properties but are overridden by per-query 
context values.
+      </>
+    ),
+  },
+  {
+    name: 'queryBlocklist',
+    type: 'json',
+    info: (
+      <>
+        List of rules to block queries on brokers. Each rule can match by 
datasource, query type,
+        and/or context parameters.
+      </>
+    ),
+  },
+];


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

Reply via email to