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

aho135 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 13749ce5336 feat: Add a timeout for SQL query planning on the Broker 
(#20314)
13749ce5336 is described below

commit 13749ce5336cd52864378371df5a17358196a7f0
Author: aho135 <[email protected]>
AuthorDate: Sat Sep 26 17:50:21 2026 -0700

    feat: Add a timeout for SQL query planning on the Broker (#20314)
    
    * Add a timeout for SQL query planning on the Broker
    
    An extremely complex SQL query (for example one with a very large IN
    filter) can spend tens of seconds in the Calcite planning phase on the
    Broker. A flood of such queries can occupy all of the Broker's request
    threads and effectively freeze the process, degrading the whole cluster.
    
    This adds a wall-clock timeout on the planning phase (the phase measured
    by the sqlQuery/planningTimeMs metric):
    
    - New runtime property druid.sql.planner.maxPlanningTimeMs (default 0,
      meaning no timeout), also overridable per query via the
      maxPlanningTimeMs query context key.
    - PlannerContext now owns a Calcite CancelFlag, which PlannerFactory
      wires into the framework Context so that Calcite aborts at its next
      cancellation checkpoint when the flag is tripped.
    - A new SqlPlanningTimeout watchdog is armed in DirectStatement.plan().
      When the deadline is exceeded it trips the CancelFlag and interrupts
      the planning thread; the failure is surfaced as a QueryTimeoutException
      (HTTP 504). The watchdog clears the interrupt on close so it cannot
      leak to a pooled request thread.
    
    Adds unit tests for the watchdog, the new config (including the query
    context override), and an end-to-end DirectStatement planning-timeout
    test, plus documentation.
    
    * Make planning-timeout code comments more concise
    
    * Remove cancelled planning-timeout tasks from the scheduler queue
    
    Address review feedback: build the scheduler as a 
ScheduledThreadPoolExecutor
    with setRemoveOnCancelPolicy(true) so that cancelled watchdog tasks (the 
common
    case, when planning finishes before the deadline) are removed from the queue
    immediately instead of lingering until their delay elapses.
    
    * Address review: propagate cancel flag to Hep/view planners, tighten 
timeout
    
    Addresses PR review feedback beyond the scheduler queue fix:
    
    - Propagate the per-query CancelFlag to the Hep-based planning stages
      (pre/reduction/pre-Volcano/cleanup). Calcite's Programs.of builds its
      HepPlanner with a null Context, so it never observed the flag; the new
      CalciteRulesManager#hepProgram helper builds the HepPlanner with a
      Context carrying the flag, otherwise identical to Programs.of. This is
      the stage where RexSimplify (CALCITE-3178) runs, so it must be
      cancellable.
    
    - Share the top-level query's CancelFlag with nested planners created
      during view expansion (DruidViewMacro), via
      PlannerContext#withInheritedCancelFlag, so one planning timeout governs
      the whole session.
    
    - Reject a plan that completes after the watchdog fired (e.g. in a
      non-cancellable section that swallowed the interrupt) instead of
      returning it: check isTimedOut() on the success path too.
    
    - Measure the planning budget from planningStartNanos so time spent
      constructing the planner counts against maxPlanningTimeMs and a query
      cannot get a fresh full budget afterwards.
    
    - Emit maxPlanningTimeMs from PlannerConfig#getNonDefaultAsQueryContext()
      so the defensive config<->context round-trip check passes for a
      non-default value; add a round-trip test.
    
    * Enforce planning deadline across construction; drop deprecated Calcite 
call
    
    Address review follow-ups:
    
    - Enforce the planning deadline against wall-clock time elapsed since
      planningStartNanos, which includes createPlanner() (planner/schema
      construction). If construction alone exhausts maxPlanningTimeMs, fail
      with QueryTimeoutException before arming the watchdog instead of
      scheduling a 1ms timer and letting a fast follow-on plan slip through.
      Also re-check the wall-clock deadline (not just the watchdog flag) on
      the success path, so a plan that completes after the deadline under
      scheduler jitter is still rejected. Adds a unit test that exhausts the
      budget during planner construction.
    
    - Drop the deprecated HepPlanner#registerMetadataProviders call from the
      cancel-flag-aware hepProgram helper; modern Calcite routes rule
      metadata through the cluster provider (cluster.setMetadataProvider),
      which the helper already sets, matching Programs.of behavior. Verified
      against the decoupled/join/array/select Calcite query tests.
    
    * Close watchdog race, cover subquery rewriting, restore Hep metadata 
provider
    
    Address the second automated review round (3x P2):
    
    - Watchdog last-millisecond race (DirectStatement): compute the remaining
      budget once and, if the timeout is enabled but already exhausted, fail
      immediately. Previously the deadline check and the budget recompute were
      separate steps; if time crossed the deadline in between, arm() received a
      non-positive budget and returned the disabled (no-op) watchdog, so a query
      past the deadline got no cancel-flag trip or interrupt and was only caught
      after planning returned.
    
    - Subquery rewriting cancellation (CalciteRulesManager): buildPreProgram 
used
      Programs.subQuery(...), whose HepPlanners are built with a null context 
and
      never see the query CancelFlag. Replaced with buildSubQueryProgram(), a
      cancel-aware replica of Programs.subQuery's two Hep stages, so a query 
that
      spends its budget in subquery rewriting is interruptible.
    
    - Restore Hep metadata provider (CalciteRulesManager): the previous change
      dropped HepPlanner#registerMetadataProviders, which also dropped the
      HepRelMetadataProvider that method prepends (needed for metadata lookups 
on
      HepRelVertex during Hep planning). Restored it and suppressed the
      deprecation warning with a comment explaining why it must match 
Programs.of.
    
    * docs: clarify that a non-positive maxPlanningTimeMs disables the timeout
    
    A negative value is coalesced to disabled everywhere the setting is consumed
    (all guards use <= 0 / > 0), same as 0. Document that non-positive disables 
the
    timeout, in the runtime-property table, the query-context table, and the
    PlannerConfig javadoc.
    
    * test: use imports instead of fully-qualified class names in 
SqlStatementTest
    
    Address review: import DruidPlanner, PlannerResult, CancelFlag, and JUnit
    Timeout in the planning-timeout tests rather than referencing them by
    fully-qualified name inline.
    
    * Fix merge conflict resolution in PlannerConfig
    
    The merge of master (#19830, catalog DDL) dropped the closing brace of
    isPlanningTimeLimited() and the opening Javadoc delimiter of the following
    isEnableCatalogDdl() method, so PlannerConfig.java no longer compiled. 
Restore
    both.
    
    * Drop druid.sql.planner.maxPlanningTimeMs in favor of default query context
    
    Per review: since dynamic query-context configuration exists, a per-query
    setting should not also be exposed as a dedicated druid.sql.planner.* 
runtime
    property (the existing planner properties predate that feature). Remove the
    @JsonProperty binding so maxPlanningTimeMs is configured only via the query
    context; a cluster-wide default is set with 
druid.query.default.context.maxPlanningTimeMs
    (or the Broker's dynamic queryContext config). The context key, watchdog, 
and
    per-query override are unchanged.
    
    Docs: remove the runtime-property row from the Broker SQL config table, 
update
    the maxPlanningTimeMs query-context entry to point at "Overriding default 
query
    context values", and add that link to the sql-query-context "Learn more" 
list.
    
    * Trim verbose javadoc on PlannerConfig#maxPlanningTimeMs
    
    * Trim PlannerConfig#maxPlanningTimeMs javadoc to what/why only
    
    * test: use assertThrows in planning-timeout tests
    
    Replace the try/plan()/fail()/catch pattern with Assertions.assertThrows in
    testPlanningTimeout and testPlanningTimeoutDuringPlannerConstruction, 
matching
    the file's existing assertThrows usages. Behavior unchanged; still asserts 
the
    QueryTimeoutException message and closes the statement in a finally.
    
    * Drop Hep cancel-flag duplication; rely on Volcano coverage for the timeout
    
    Thread-dumping a planning-bound large-IN query (forced OR path) showed the 
cost
    is in VolcanoPlanner.findBestExp (Druid-convention conversion + memo digest
    computation), not the Hep reduction stage as originally assumed. The Volcano
    planner already receives the per-query CancelFlag via the framework 
Context, so
    the timeout covers that path without any custom Hep plumbing.
    
    Revert the hepProgram/buildSubQueryProgram helpers that reimplemented 
Calcite's
    Programs.of/Programs.subQuery just to inject the CancelFlag into the 
internally
    constructed HepPlanners. Those duplicated Calcite internals (and a 
deprecated
    API) to cover a Hep-reduction-bound scenario that could not be reproduced. 
Back
    to plain Programs.of/Programs.subQuery. The framework-Context wiring and the
    view-planner flag inheritance (PlannerContext#withInheritedCancelFlag) 
remain.
    
    * Trim verbose comments across the planning-timeout changes
---
 docs/querying/sql-query-context.md                 |   2 +
 .../java/org/apache/druid/sql/DirectStatement.java |  97 ++++++++++++++---
 .../org/apache/druid/sql/SqlPlanningTimeout.java   | 117 +++++++++++++++++++++
 .../druid/sql/calcite/planner/PlannerConfig.java   |  48 +++++++++
 .../druid/sql/calcite/planner/PlannerContext.java  |  46 ++++++++
 .../druid/sql/calcite/planner/PlannerFactory.java  |   5 +
 .../apache/druid/sql/SqlPlanningTimeoutTest.java   |  98 +++++++++++++++++
 .../org/apache/druid/sql/SqlStatementTest.java     | 106 +++++++++++++++++--
 .../sql/calcite/planner/PlannerConfigTest.java     |  51 +++++++++
 9 files changed, 547 insertions(+), 23 deletions(-)

diff --git a/docs/querying/sql-query-context.md 
b/docs/querying/sql-query-context.md
index 23e9f41347a..88cafe1ced7 100644
--- a/docs/querying/sql-query-context.md
+++ b/docs/querying/sql-query-context.md
@@ -56,8 +56,10 @@ The table below lists the query context parameters you can 
use with Druid SQL.
 |`inFunctionThreshold`| At or beyond this threshold number of values, Druid 
converts SQL `IN` to [`SCALAR_IN_ARRAY`](sql-functions.md#scalar_in_array). A 
threshold of 0 forces this conversion in all cases. A threshold of 
`Integer.MAX_VALUE` disables this conversion. The converted function is 
eligible for fewer planning-time optimizations, which speeds up planning, but 
may prevent certain planning-time optimizations.| `100`|
 |`inFunctionExprThreshold`|At or beyond this threshold number of values, SQL 
`IN` is eligible for execution using the native function `scalar_in_array` 
rather than an <code>&#124;&#124;</code> of `==`, even if the number of values 
is below `inFunctionThreshold`. This property only affects translation of SQL 
`IN` to a [native expression](math-expr.md). It doesn't affect translation of 
SQL `IN` to a [native filter](filters.md). This property is provided for 
backwards compatibility purposes [...]
 |`inSubQueryThreshold`|At or beyond this threshold number of values, Druid 
converts SQL `IN` to `JOIN` on an inline table. `inFunctionThreshold` takes 
priority over this setting. A threshold of 0 forces usage of an inline table in 
all cases where the size of a SQL `IN` is larger than `inFunctionThreshold`. A 
threshold of `2147483647` disables the rewrite of SQL `IN` to `JOIN`. 
|`2147483647`|
+|`maxPlanningTimeMs`|Maximum wall-clock time, in milliseconds, allowed for 
planning this query on the Broker. When planning exceeds this budget, planning 
is aborted and the query fails with an HTTP 504 `Query timeout` error. Use this 
to guard against pathological queries, such as one with a very large `IN` 
filter, whose planning time can spike. A non-positive value disables the 
timeout. To apply a cluster-wide default, set the 
`druid.query.default.context.maxPlanningTimeMs` runtime prope [...]
 
 ## Learn more
 - [Set query context](../querying/query-context.md) for how to set the query 
context.
 - [Query context reference](query-context-reference.md)  for available query 
context parameters.
+- [Overriding default query context 
values](../configuration/index.md#overriding-default-query-context-values) for 
how to set cluster-wide defaults for any query context parameter with 
`druid.query.default.context.*`.
 - [MSQ context 
parameters](../multi-stage-query/reference.md#context-parameters) for how to 
set context parameters for Multi-Stage Queries.
diff --git a/sql/src/main/java/org/apache/druid/sql/DirectStatement.java 
b/sql/src/main/java/org/apache/druid/sql/DirectStatement.java
index ee50d0b8e18..88c9b079b79 100644
--- a/sql/src/main/java/org/apache/druid/sql/DirectStatement.java
+++ b/sql/src/main/java/org/apache/druid/sql/DirectStatement.java
@@ -27,15 +27,18 @@ import org.apache.druid.java.util.common.ISE;
 import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.query.QueryException;
 import org.apache.druid.query.QueryInterruptedException;
+import org.apache.druid.query.QueryTimeoutException;
 import org.apache.druid.server.QueryResponse;
 import org.apache.druid.server.security.ResourceAction;
 import org.apache.druid.sql.SqlLifecycleManager.Cancelable;
 import org.apache.druid.sql.calcite.planner.DruidPlanner;
+import org.apache.druid.sql.calcite.planner.PlannerContext;
 import org.apache.druid.sql.calcite.planner.PlannerResult;
 import org.apache.druid.sql.calcite.planner.PrepareResult;
 
 import javax.annotation.Nullable;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Lifecycle for direct SQL statement execution, which means that the query
@@ -196,20 +199,52 @@ public class DirectStatement extends AbstractStatement 
implements Cancelable
     }
     long planningStartNanos = System.nanoTime();
     try (DruidPlanner planner = createPlanner()) {
-      validate(planner);
-      authorize(planner, authorizer());
-
-      // Adding the statement to the lifecycle manager allows cancellation.
-      // Tests cancel during this call; real clients might do so if the plan
-      // or execution prep stages take too long for some unexpected reason.
-      sqlToolbox.sqlLifecycleManager.add(sqlQueryId(), this);
-      transition(State.PREPARED);
-      resultSet = createResultSet(createPlan(planner));
-      prepareResult = planner.prepareResult();
-      // Double check needed by SqlResourceTest
-      transition(State.PREPARED);
-      reporter.planningTimeNanos(System.nanoTime() - planningStartNanos);
-      return resultSet;
+      // Bound planning wall-clock. Budget is measured from planningStartNanos 
(before createPlanner), so planner
+      // construction counts against it; if already exhausted, fail now rather 
than arm a no-op (non-positive) watchdog.
+      final long maxPlanningTimeMs = 
planner.getPlannerContext().getPlannerConfig().getMaxPlanningTimeMs();
+      final long remainingBudgetMs = 
remainingPlanningBudgetMs(maxPlanningTimeMs, planningStartNanos);
+      if (maxPlanningTimeMs > 0 && remainingBudgetMs <= 0) {
+        throw planningTimedOut(maxPlanningTimeMs);
+      }
+      try (SqlPlanningTimeout timeout = SqlPlanningTimeout.arm(
+          remainingBudgetMs,
+          planner.getPlannerContext().getCancelFlag(),
+          Thread.currentThread()
+      )) {
+        // Nested planners created during view expansion inherit this flag 
(PlannerContext#withInheritedCancelFlag).
+        return PlannerContext.withInheritedCancelFlag(
+            planner.getPlannerContext().getCancelFlag(),
+            () -> {
+              try {
+                validate(planner);
+                authorize(planner, authorizer());
+
+                // Adding the statement to the lifecycle manager allows 
cancellation.
+                // Tests cancel during this call; real clients might do so if 
the plan
+                // or execution prep stages take too long for some unexpected 
reason.
+                sqlToolbox.sqlLifecycleManager.add(sqlQueryId(), this);
+                transition(State.PREPARED);
+                resultSet = createResultSet(createPlan(planner));
+                prepareResult = planner.prepareResult();
+                // Double check needed by SqlResourceTest
+                transition(State.PREPARED);
+                reporter.planningTimeNanos(System.nanoTime() - 
planningStartNanos);
+              }
+              catch (RuntimeException | AssertionError e) {
+                // On timeout, the failure is a side effect of aborting the 
planner; surface it as a timeout.
+                if (timeout.isTimedOut()) {
+                  throw planningTimedOut(maxPlanningTimeMs);
+                }
+                throw e;
+              }
+              // Reject a plan that completed past the deadline 
(non-cancellable section, or watchdog scheduler jitter).
+              if (timeout.isTimedOut() || 
planningDeadlineExceeded(maxPlanningTimeMs, planningStartNanos)) {
+                throw planningTimedOut(maxPlanningTimeMs);
+              }
+              return resultSet;
+            }
+        );
+      }
     }
     catch (RelOptPlanner.CannotPlanException e) {
       // Not sure if this is even thrown here.
@@ -229,6 +264,40 @@ public class DirectStatement extends AbstractStatement 
implements Cancelable
     }
   }
 
+  /**
+   * Budget left in the planning timeout, given elapsed time since {@code 
planningStartNanos}. Returns
+   * {@code maxPlanningTimeMs} unchanged when non-positive (disabled); may be 
non-positive if already overrun.
+   */
+  private static long remainingPlanningBudgetMs(long maxPlanningTimeMs, long 
planningStartNanos)
+  {
+    if (maxPlanningTimeMs <= 0) {
+      return maxPlanningTimeMs;
+    }
+    final long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - 
planningStartNanos);
+    return maxPlanningTimeMs - elapsedMs;
+  }
+
+  /**
+   * Whether the planning deadline has been exceeded by elapsed wall-clock 
time. False when disabled.
+   */
+  private static boolean planningDeadlineExceeded(long maxPlanningTimeMs, long 
planningStartNanos)
+  {
+    return maxPlanningTimeMs > 0
+           && TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - 
planningStartNanos) >= maxPlanningTimeMs;
+  }
+
+  private QueryTimeoutException planningTimedOut(long maxPlanningTimeMs)
+  {
+    return new QueryTimeoutException(
+        StringUtils.format(
+            "Query planning for [%s] exceeded the configured maximum planning 
time of [%,d] ms. This may "
+            + "indicate an overly complex query, for example one with a very 
large IN filter.",
+            sqlQueryId(),
+            maxPlanningTimeMs
+        )
+    );
+  }
+
   protected DruidPlanner createPlanner()
   {
     return sqlToolbox.plannerFactory.createPlanner(
diff --git a/sql/src/main/java/org/apache/druid/sql/SqlPlanningTimeout.java 
b/sql/src/main/java/org/apache/druid/sql/SqlPlanningTimeout.java
new file mode 100644
index 00000000000..34bc00631ef
--- /dev/null
+++ b/sql/src/main/java/org/apache/druid/sql/SqlPlanningTimeout.java
@@ -0,0 +1,117 @@
+/*
+ * 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.sql;
+
+import org.apache.calcite.util.CancelFlag;
+import org.apache.druid.java.util.common.concurrent.Execs;
+
+import java.io.Closeable;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Bounds the wall-clock time spent planning a single SQL query (see
+ * {@link 
org.apache.druid.sql.calcite.planner.PlannerConfig#getMaxPlanningTimeMs()}). On 
deadline it trips the query's
+ * Calcite {@link CancelFlag} and interrupts the planning thread; {@link 
#close()} cancels the watchdog and clears any
+ * leaked interrupt. Check {@link #isTimedOut()} to translate a planning 
failure into a timeout.
+ */
+public class SqlPlanningTimeout implements Closeable
+{
+  private static final ScheduledThreadPoolExecutor SCHEDULER = 
createScheduler();
+
+  private static ScheduledThreadPoolExecutor createScheduler()
+  {
+    // One daemon thread suffices; remove cancelled tasks (the common case) 
from the queue so it can't grow under load.
+    final ScheduledThreadPoolExecutor scheduler =
+        new ScheduledThreadPoolExecutor(1, 
Execs.makeThreadFactory("sql-planning-timeout-%d"));
+    scheduler.setRemoveOnCancelPolicy(true);
+    return scheduler;
+  }
+
+  // No-op instance returned when no timeout is configured, so callers need no 
null checks.
+  private static final SqlPlanningTimeout DISABLED = new SqlPlanningTimeout();
+
+  private final Object lock = new Object();
+  private final ScheduledFuture<?> future;
+  private volatile boolean timedOut;
+  private boolean closed;
+
+  private SqlPlanningTimeout()
+  {
+    this.future = null;
+  }
+
+  private SqlPlanningTimeout(long maxPlanningTimeMs, CancelFlag cancelFlag, 
Thread planningThread)
+  {
+    this.future = SCHEDULER.schedule(
+        () -> fire(cancelFlag, planningThread),
+        maxPlanningTimeMs,
+        TimeUnit.MILLISECONDS
+    );
+  }
+
+  /**
+   * Arm a watchdog for {@code planningThread}. A non-positive {@code 
maxPlanningTimeMs} returns a no-op instance.
+   */
+  public static SqlPlanningTimeout arm(long maxPlanningTimeMs, CancelFlag 
cancelFlag, Thread planningThread)
+  {
+    if (maxPlanningTimeMs <= 0) {
+      return DISABLED;
+    }
+    return new SqlPlanningTimeout(maxPlanningTimeMs, cancelFlag, 
planningThread);
+  }
+
+  private void fire(CancelFlag cancelFlag, Thread planningThread)
+  {
+    synchronized (lock) {
+      if (closed) {
+        // Planning already finished; do not interrupt a possibly-recycled 
thread.
+        return;
+      }
+      timedOut = true;
+      cancelFlag.requestCancel();
+      planningThread.interrupt();
+    }
+  }
+
+  public boolean isTimedOut()
+  {
+    return timedOut;
+  }
+
+  @Override
+  public void close()
+  {
+    if (future == null) {
+      return;
+    }
+    boolean wasTimedOut;
+    synchronized (lock) {
+      closed = true;
+      future.cancel(false);
+      wasTimedOut = timedOut;
+    }
+    if (wasTimedOut) {
+      // Clear the watchdog's interrupt so it isn't leaked to a pooled request 
thread (close() runs on that thread).
+      Thread.interrupted();
+    }
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerConfig.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerConfig.java
index 3afdbe88ea0..bf6a3f20c0b 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerConfig.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerConfig.java
@@ -40,7 +40,12 @@ public class PlannerConfig
   public static final String CTX_KEY_FORCE_EXPRESSION_VIRTUAL_COLUMNS = 
"forceExpressionVirtualColumns";
   public static final String CTX_MAX_NUMERIC_IN_FILTERS = 
"maxNumericInFilters";
   public static final String CTX_REQUIRE_TIME_CONDITION = 
"requireTimeCondition";
+  public static final String CTX_KEY_MAX_PLANNING_TIME_MS = 
"maxPlanningTimeMs";
   public static final int NUM_FILTER_NOT_USED = -1;
+  /**
+   * Sentinel value for {@link #maxPlanningTimeMs} meaning "no planning 
timeout".
+   */
+  public static final long PLANNING_TIME_NOT_LIMITED = 0;
   @JsonProperty
   private int maxTopNLimit = 100_000;
 
@@ -86,6 +91,13 @@ public class PlannerConfig
   @JsonProperty
   private boolean authorizeTableVisibility = true;
 
+  /**
+   * Maximum wall-clock time, in milliseconds, allowed for planning a SQL 
query. When exceeded, planning is aborted and
+   * the query fails with a {@link 
org.apache.druid.query.QueryTimeoutException}. A non-positive value (default
+   * {@link #PLANNING_TIME_NOT_LIMITED}) disables the timeout.
+   */
+  private long maxPlanningTimeMs = PLANNING_TIME_NOT_LIMITED;
+
   @JsonProperty
   private boolean enableCatalogDdl = false;
 
@@ -180,6 +192,19 @@ public class PlannerConfig
     return authorizeTableVisibility;
   }
 
+  /**
+   * See {@link #maxPlanningTimeMs}. Returns {@link 
#PLANNING_TIME_NOT_LIMITED} when planning time is unbounded.
+   */
+  public long getMaxPlanningTimeMs()
+  {
+    return maxPlanningTimeMs;
+  }
+
+  public boolean isPlanningTimeLimited()
+  {
+    return maxPlanningTimeMs > PLANNING_TIME_NOT_LIMITED;
+  }
+
   /**
    * Whether catalog DDL statements (CREATE TABLE, ALTER TABLE) may be 
executed. Off by default: the Broker's SQL
    * endpoint is typically reachable by far more people than the Coordinator's 
catalog API, so enabling this widens
@@ -218,6 +243,7 @@ public class PlannerConfig
            && useNativeQueryExplain == that.useNativeQueryExplain
            && forceExpressionVirtualColumns == 
that.forceExpressionVirtualColumns
            && maxNumericInFilters == that.maxNumericInFilters
+           && maxPlanningTimeMs == that.maxPlanningTimeMs
            && enableSysQueriesTable == that.enableSysQueriesTable
            && authorizeTableVisibility == that.authorizeTableVisibility
            && enableCatalogDdl == that.enableCatalogDdl
@@ -241,6 +267,7 @@ public class PlannerConfig
         useNativeQueryExplain,
         forceExpressionVirtualColumns,
         maxNumericInFilters,
+        maxPlanningTimeMs,
         nativeQuerySqlPlanningMode,
         enableSysQueriesTable,
         authorizeTableVisibility,
@@ -259,6 +286,7 @@ public class PlannerConfig
            ", requireTimeCondition=" + requireTimeCondition +
            ", sqlTimeZone=" + sqlTimeZone +
            ", useNativeQueryExplain=" + useNativeQueryExplain +
+           ", maxPlanningTimeMs=" + maxPlanningTimeMs +
            ", nativeQuerySqlPlanningMode=" + nativeQuerySqlPlanningMode +
            ", enableSysQueriesTable=" + enableSysQueriesTable +
            ", authorizeTableVisibility=" + authorizeTableVisibility +
@@ -296,6 +324,7 @@ public class PlannerConfig
     private boolean useNativeQueryExplain;
     private boolean forceExpressionVirtualColumns;
     private int maxNumericInFilters;
+    private long maxPlanningTimeMs;
     private String nativeQuerySqlPlanningMode;
     private boolean enableSysQueriesTable;
     private boolean authorizeTableVisibility;
@@ -318,6 +347,7 @@ public class PlannerConfig
       useNativeQueryExplain = base.isUseNativeQueryExplain();
       forceExpressionVirtualColumns = base.isForceExpressionVirtualColumns();
       maxNumericInFilters = base.getMaxNumericInFilters();
+      maxPlanningTimeMs = base.getMaxPlanningTimeMs();
       nativeQuerySqlPlanningMode = base.getNativeQuerySqlPlanningMode();
       enableSysQueriesTable = base.isEnableSysQueriesTable();
       authorizeTableVisibility = base.isAuthorizeTableVisibility();
@@ -342,6 +372,12 @@ public class PlannerConfig
       return this;
     }
 
+    public Builder maxPlanningTimeMs(long value)
+    {
+      this.maxPlanningTimeMs = value;
+      return this;
+    }
+
     public Builder useApproximateCountDistinct(boolean option)
     {
       this.useApproximateCountDistinct = option;
@@ -469,6 +505,11 @@ public class PlannerConfig
           CTX_REQUIRE_TIME_CONDITION,
           requireTimeCondition
       );
+      maxPlanningTimeMs = QueryContexts.parseLong(
+          queryContext,
+          CTX_KEY_MAX_PLANNING_TIME_MS,
+          maxPlanningTimeMs
+      );
       return this;
     }
 
@@ -508,6 +549,7 @@ public class PlannerConfig
       config.authorizeSystemTablesDirectly = authorizeSystemTablesDirectly;
       config.useNativeQueryExplain = useNativeQueryExplain;
       config.maxNumericInFilters = maxNumericInFilters;
+      config.maxPlanningTimeMs = maxPlanningTimeMs;
       config.forceExpressionVirtualColumns = forceExpressionVirtualColumns;
       config.nativeQuerySqlPlanningMode = nativeQuerySqlPlanningMode;
       config.enableSysQueriesTable = enableSysQueriesTable;
@@ -544,6 +586,12 @@ public class PlannerConfig
           String.valueOf(requireTimeCondition)
       );
     }
+    if (def.maxPlanningTimeMs != maxPlanningTimeMs) {
+      overrides.put(
+          CTX_KEY_MAX_PLANNING_TIME_MS,
+          maxPlanningTimeMs
+      );
+    }
 
     PlannerConfig newConfig = 
PlannerConfig.builder().withOverrides(overrides).build();
     if (!equals(newConfig)) {
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java
index e9826af5dd4..923bc1d198a 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java
@@ -30,6 +30,7 @@ import org.apache.calcite.linq4j.QueryProvider;
 import org.apache.calcite.schema.SchemaPlus;
 import org.apache.calcite.schema.Table;
 import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.util.CancelFlag;
 import org.apache.druid.error.InvalidSqlInput;
 import org.apache.druid.java.util.common.DateTimes;
 import org.apache.druid.java.util.common.ISE;
@@ -80,6 +81,8 @@ import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
 
 /**
  * Like {@link PlannerConfig}, but that has static configuration and this class
@@ -151,6 +154,18 @@ public class PlannerContext
   private final PlannerHook hook;
   private final Set<String> lookupsToLoad = new HashSet<>();
 
+  /**
+   * Cancel flag inherited by any {@link PlannerContext} created on this 
thread while set, so nested view-expansion
+   * planners share the top-level query's {@link #cancelFlag}. Managed by 
{@link #withInheritedCancelFlag}.
+   */
+  private static final ThreadLocal<CancelFlag> INHERITED_CANCEL_FLAG = new 
ThreadLocal<>();
+
+  /**
+   * Calcite cancellation flag, wired into the planner via the framework 
{@link org.apache.calcite.plan.Context} in
+   * {@link PlannerFactory}; tripping it enforces the planning timeout ({@link 
PlannerConfig#getMaxPlanningTimeMs()}).
+   */
+  private final CancelFlag cancelFlag = inheritedOrNewCancelFlag();
+
   private PlannerConfig plannerConfig;
   /**
    * Root schema created with {@link 
DruidSchemaCatalogProvider#createEscalatedRootSchema()}, for use in
@@ -280,6 +295,37 @@ public class PlannerContext
     return plannerConfig;
   }
 
+  public CancelFlag getCancelFlag()
+  {
+    return cancelFlag;
+  }
+
+  private static CancelFlag inheritedOrNewCancelFlag()
+  {
+    final CancelFlag inherited = INHERITED_CANCEL_FLAG.get();
+    return inherited != null ? inherited : new CancelFlag(new 
AtomicBoolean(false));
+  }
+
+  /**
+   * Runs {@code action} with {@code cancelFlag} installed as the flag that 
{@link PlannerContext}s created on this
+   * thread inherit, so one planning timeout governs nested view-expansion 
planners. Restores the prior value after.
+   */
+  public static <T> T withInheritedCancelFlag(final CancelFlag cancelFlag, 
final Supplier<T> action)
+  {
+    final CancelFlag previous = INHERITED_CANCEL_FLAG.get();
+    INHERITED_CANCEL_FLAG.set(cancelFlag);
+    try {
+      return action.get();
+    }
+    finally {
+      if (previous != null) {
+        INHERITED_CANCEL_FLAG.set(previous);
+      } else {
+        INHERITED_CANCEL_FLAG.remove();
+      }
+    }
+  }
+
   public DateTime getLocalNow()
   {
     return localNow;
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java
index c232108f65f..7ae657ec454 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java
@@ -40,6 +40,7 @@ import org.apache.calcite.sql.validate.SqlConformance;
 import org.apache.calcite.sql2rel.SqlToRelConverter;
 import org.apache.calcite.tools.FrameworkConfig;
 import org.apache.calcite.tools.Frameworks;
+import org.apache.calcite.util.CancelFlag;
 import org.apache.druid.guice.annotations.Json;
 import org.apache.druid.math.expr.ExprMacroTable;
 import org.apache.druid.query.QueryContexts;
@@ -326,6 +327,10 @@ public class PlannerFactory extends PlannerToolbox
             if (aClass.equals(PlannerContext.class)) {
               return (C) plannerContext;
             }
+            if (aClass.equals(CancelFlag.class)) {
+              // Hand Calcite the per-query cancellation flag so planning can 
be aborted (e.g. on planning timeout).
+              return (C) plannerContext.getCancelFlag();
+            }
 
             return null;
           }
diff --git a/sql/src/test/java/org/apache/druid/sql/SqlPlanningTimeoutTest.java 
b/sql/src/test/java/org/apache/druid/sql/SqlPlanningTimeoutTest.java
new file mode 100644
index 00000000000..80577ac186d
--- /dev/null
+++ b/sql/src/test/java/org/apache/druid/sql/SqlPlanningTimeoutTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.sql;
+
+import org.apache.calcite.util.CancelFlag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SqlPlanningTimeoutTest
+{
+  private static CancelFlag newCancelFlag()
+  {
+    return new CancelFlag(new AtomicBoolean(false));
+  }
+
+  @Test
+  public void testDisabledWhenTimeoutNotPositive()
+  {
+    final CancelFlag cancelFlag = newCancelFlag();
+    try (SqlPlanningTimeout timeout = SqlPlanningTimeout.arm(0, cancelFlag, 
Thread.currentThread())) {
+      assertFalse(timeout.isTimedOut());
+      assertFalse(cancelFlag.isCancelRequested());
+    }
+    assertFalse(cancelFlag.isCancelRequested());
+    assertFalse(Thread.currentThread().isInterrupted());
+  }
+
+  @Test
+  public void testDisabledWhenTimeoutNegative()
+  {
+    final CancelFlag cancelFlag = newCancelFlag();
+    try (SqlPlanningTimeout timeout = SqlPlanningTimeout.arm(-100, cancelFlag, 
Thread.currentThread())) {
+      assertFalse(timeout.isTimedOut());
+    }
+    assertFalse(cancelFlag.isCancelRequested());
+  }
+
+  /**
+   * On deadline the watchdog must trip the cancel flag and interrupt the 
thread, and {@link #close()} must clear it.
+   */
+  @Test
+  @Timeout(30)
+  public void testTimeoutTripsCancelFlagAndInterrupts()
+  {
+    final CancelFlag cancelFlag = newCancelFlag();
+    boolean sawInterruptOrCancel;
+    try (SqlPlanningTimeout timeout = SqlPlanningTimeout.arm(50, cancelFlag, 
Thread.currentThread())) {
+      // Mimic CPU-bound planning that periodically checks for cancellation.
+      while (!cancelFlag.isCancelRequested() && 
!Thread.currentThread().isInterrupted()) {
+        // spin
+      }
+      sawInterruptOrCancel = true;
+      assertTrue(timeout.isTimedOut());
+      assertTrue(cancelFlag.isCancelRequested());
+    }
+    assertTrue(sawInterruptOrCancel);
+    assertFalse(Thread.currentThread().isInterrupted());
+  }
+
+  /**
+   * Planning that finishes before the deadline must not trip the flag or 
leave the thread interrupted.
+   */
+  @Test
+  @Timeout(30)
+  public void testNoTimeoutWhenPlanningFinishesEarly() throws 
InterruptedException
+  {
+    final CancelFlag cancelFlag = newCancelFlag();
+    try (SqlPlanningTimeout timeout = SqlPlanningTimeout.arm(5000, cancelFlag, 
Thread.currentThread())) {
+      Thread.sleep(10);
+      assertFalse(timeout.isTimedOut());
+      assertFalse(cancelFlag.isCancelRequested());
+    }
+    assertFalse(cancelFlag.isCancelRequested());
+    assertFalse(Thread.currentThread().isInterrupted());
+  }
+}
diff --git a/sql/src/test/java/org/apache/druid/sql/SqlStatementTest.java 
b/sql/src/test/java/org/apache/druid/sql/SqlStatementTest.java
index 4abb504c524..956a550f254 100644
--- a/sql/src/test/java/org/apache/druid/sql/SqlStatementTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/SqlStatementTest.java
@@ -26,6 +26,7 @@ import 
com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.util.CancelFlag;
 import org.apache.druid.error.DruidException;
 import org.apache.druid.error.DruidExceptionMatcher;
 import org.apache.druid.java.util.common.ISE;
@@ -38,6 +39,7 @@ import org.apache.druid.query.DefaultQueryConfig;
 import org.apache.druid.query.Query;
 import org.apache.druid.query.QueryContexts;
 import org.apache.druid.query.QueryRunnerFactoryConglomerate;
+import org.apache.druid.query.QueryTimeoutException;
 import org.apache.druid.query.policy.NoopPolicyEnforcer;
 import org.apache.druid.query.policy.PolicyEnforcer;
 import org.apache.druid.query.policy.RestrictAllTablesPolicyEnforcer;
@@ -58,8 +60,10 @@ import org.apache.druid.sql.calcite.BaseCalciteQueryTest;
 import org.apache.druid.sql.calcite.planner.CalciteRulesManager;
 import org.apache.druid.sql.calcite.planner.CatalogResolver;
 import org.apache.druid.sql.calcite.planner.DruidOperatorTable;
+import org.apache.druid.sql.calcite.planner.DruidPlanner;
 import org.apache.druid.sql.calcite.planner.PlannerConfig;
 import org.apache.druid.sql.calcite.planner.PlannerFactory;
+import org.apache.druid.sql.calcite.planner.PlannerResult;
 import org.apache.druid.sql.calcite.planner.PrepareResult;
 import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider;
 import org.apache.druid.sql.calcite.util.CalciteTests;
@@ -71,6 +75,7 @@ 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 org.junit.jupiter.api.io.TempDir;
 
 import javax.servlet.http.HttpServletRequest;
@@ -102,6 +107,7 @@ public class SqlStatementTest
 
   private PolicyEnforcer policyEnforcer;
   private SqlStatementFactory sqlStatementFactory;
+  private SqlToolbox sqlToolbox;
 
   @BeforeAll
   public static void setUpClass()
@@ -568,15 +574,97 @@ public class SqlStatementTest
         new DruidHookDispatcher()
     );
 
-    return new SqlStatementFactory(
-        new SqlToolbox(
-            CalciteTests.createMockSqlEngine(walker, conglomerate),
-            plannerFactory,
-            new NoopServiceEmitter(),
-            testRequestLogger,
-            QueryStackTests.DEFAULT_NOOP_SCHEDULER,
-            new SqlLifecycleManager()
-        )
+    this.sqlToolbox = new SqlToolbox(
+        CalciteTests.createMockSqlEngine(walker, conglomerate),
+        plannerFactory,
+        new NoopServiceEmitter(),
+        testRequestLogger,
+        QueryStackTests.DEFAULT_NOOP_SCHEDULER,
+        new SqlLifecycleManager()
     );
+    return new SqlStatementFactory(sqlToolbox);
+  }
+
+  /**
+   * Planning that exceeds {@code maxPlanningTimeMs} fails with a {@link 
QueryTimeoutException}. Planning is simulated
+   * as a CPU-bound loop that honours the cancel flag (as Calcite does), so 
the watchdog trips it and we time out.
+   */
+  @Test
+  @Timeout(30)
+  public void testPlanningTimeout()
+  {
+    SqlQueryPlus sqlReq = SqlQueryPlus
+        .builder("SELECT COUNT(*) AS cnt, 'foo' AS TheFoo FROM druid.foo")
+        
.queryContext(ImmutableMap.of(PlannerConfig.CTX_KEY_MAX_PLANNING_TIME_MS, 100))
+        .auth(CalciteTests.REGULAR_USER_AUTH_RESULT)
+        .build();
+
+    // A DirectStatement whose planning step blocks until the query is 
cancelled, mimicking a pathological query.
+    DirectStatement stmt = new DirectStatement(sqlToolbox, sqlReq, null)
+    {
+      @Override
+      protected PlannerResult createPlan(DruidPlanner planner)
+      {
+        final CancelFlag cancelFlag = 
planner.getPlannerContext().getCancelFlag();
+        while (!cancelFlag.isCancelRequested() && 
!Thread.currentThread().isInterrupted()) {
+          // spin until the watchdog aborts us
+        }
+        // Calcite throws when it observes a tripped cancel flag; emulate that.
+        throw new RuntimeException("Preparation aborted");
+      }
+    };
+
+    try {
+      QueryTimeoutException e = 
Assertions.assertThrows(QueryTimeoutException.class, stmt::plan);
+      Assertions.assertTrue(
+          e.getMessage().contains("exceeded the configured maximum planning 
time"),
+          "Unexpected message: " + e.getMessage()
+      );
+    }
+    finally {
+      stmt.close();
+    }
+  }
+
+  /**
+   * The budget covers planner construction: if {@link 
DirectStatement#createPlanner()} alone exhausts
+   * {@code maxPlanningTimeMs}, planning fails with a {@link 
QueryTimeoutException} before any further work.
+   */
+  @Test
+  @Timeout(30)
+  public void testPlanningTimeoutDuringPlannerConstruction()
+  {
+    SqlQueryPlus sqlReq = SqlQueryPlus
+        .builder("SELECT COUNT(*) AS cnt, 'foo' AS TheFoo FROM druid.foo")
+        
.queryContext(ImmutableMap.of(PlannerConfig.CTX_KEY_MAX_PLANNING_TIME_MS, 50))
+        .auth(CalciteTests.REGULAR_USER_AUTH_RESULT)
+        .build();
+
+    // Simulate an expensive planner/schema construction that by itself 
exceeds the budget.
+    DirectStatement stmt = new DirectStatement(sqlToolbox, sqlReq, null)
+    {
+      @Override
+      protected DruidPlanner createPlanner()
+      {
+        try {
+          Thread.sleep(300);
+        }
+        catch (InterruptedException e) {
+          Thread.currentThread().interrupt();
+        }
+        return super.createPlanner();
+      }
+    };
+
+    try {
+      QueryTimeoutException e = 
Assertions.assertThrows(QueryTimeoutException.class, stmt::plan);
+      Assertions.assertTrue(
+          e.getMessage().contains("exceeded the configured maximum planning 
time"),
+          "Unexpected message: " + e.getMessage()
+      );
+    }
+    finally {
+      stmt.close();
+    }
   }
 }
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/planner/PlannerConfigTest.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/planner/PlannerConfigTest.java
index 6b3442132aa..5dd6865e13a 100644
--- 
a/sql/src/test/java/org/apache/druid/sql/calcite/planner/PlannerConfigTest.java
+++ 
b/sql/src/test/java/org/apache/druid/sql/calcite/planner/PlannerConfigTest.java
@@ -19,11 +19,14 @@
 
 package org.apache.druid.sql.calcite.planner;
 
+import com.google.common.collect.ImmutableMap;
 import nl.jqno.equalsverifier.EqualsVerifier;
 import nl.jqno.equalsverifier.Warning;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.Map;
+
 public class PlannerConfigTest
 {
   @Test
@@ -35,6 +38,54 @@ public class PlannerConfigTest
     Assertions.assertTrue(config.isUseApproximateCountDistinct());
   }
 
+  @Test
+  public void testMaxPlanningTimeMsDisabledByDefault()
+  {
+    PlannerConfig config = new PlannerConfig();
+    Assertions.assertEquals(PlannerConfig.PLANNING_TIME_NOT_LIMITED, 
config.getMaxPlanningTimeMs());
+    Assertions.assertFalse(config.isPlanningTimeLimited());
+  }
+
+  @Test
+  public void testMaxPlanningTimeMsBuilder()
+  {
+    PlannerConfig config = PlannerConfig.builder()
+                                        .maxPlanningTimeMs(5000)
+                                        .build();
+    Assertions.assertEquals(5000, config.getMaxPlanningTimeMs());
+    Assertions.assertTrue(config.isPlanningTimeLimited());
+  }
+
+  @Test
+  public void testMaxPlanningTimeMsQueryContextOverride()
+  {
+    PlannerConfig base = 
PlannerConfig.builder().maxPlanningTimeMs(10_000).build();
+    PlannerConfig overridden = base.withOverrides(
+        ImmutableMap.of(PlannerConfig.CTX_KEY_MAX_PLANNING_TIME_MS, 2500)
+    );
+    Assertions.assertEquals(2500, overridden.getMaxPlanningTimeMs());
+    // The base config is untouched.
+    Assertions.assertEquals(10_000, base.getMaxPlanningTimeMs());
+  }
+
+  @Test
+  public void testMaxPlanningTimeMsInheritedWhenNotOverridden()
+  {
+    PlannerConfig base = 
PlannerConfig.builder().maxPlanningTimeMs(10_000).build();
+    PlannerConfig overridden = 
base.withOverrides(ImmutableMap.of("someOtherKey", "someValue"));
+    Assertions.assertEquals(10_000, overridden.getMaxPlanningTimeMs());
+  }
+
+  @Test
+  public void testMaxPlanningTimeMsRoundTripsThroughQueryContext()
+  {
+    // Non-default maxPlanningTimeMs must round-trip through 
getNonDefaultAsQueryContext()'s config<->context check.
+    PlannerConfig config = 
PlannerConfig.builder().maxPlanningTimeMs(5000).build();
+    Map<String, Object> asContext = config.getNonDefaultAsQueryContext();
+    Assertions.assertEquals(5000L, ((Number) 
asContext.get(PlannerConfig.CTX_KEY_MAX_PLANNING_TIME_MS)).longValue());
+    Assertions.assertEquals(config, 
PlannerConfig.builder().withOverrides(asContext).build());
+  }
+
   @Test
   public void testPlannerConfigBuilder()
   {


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

Reply via email to