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

gian 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 c3262756f1d feat: MSQ exception handling improvements. (#19234)
c3262756f1d is described below

commit c3262756f1d5eb8fd992e93b54a27c3181ed9bfa
Author: Gian Merlino <[email protected]>
AuthorDate: Wed Apr 1 10:37:55 2026 -0700

    feat: MSQ exception handling improvements. (#19234)
    
    This patch makes various changes to MSQ exception handling, with the
    goal of better compatibility with DruidExceptions:
    
    1) Add DruidExceptionFault, which allows including DruidExceptions in
       MSQErrorReports without losing any information.
    
    2) Make MSQFault#toDruidException abstract so all fault implementations
       must reference a specific category and persona. Changed the category
       and persona of various faults, in situations where the inherited
       default was not ideal.
    
    3) QueryRuntimeFault no longer generated, instead we're relying fully
       on DruidException for exceptions that originate in the query runtime.
    
    4) Retain original DruidException, rather than wrapping and re-throwing,
       in ControllerImpl, SegmentGeneratorFrameProcessor, and
       RowBasedFrameWriter.
    
    5) Update Either to throw non-DEVELOPER DruidExceptions as-is, rather
       than wrapping them.
---
 docs/multi-stage-query/reference.md                |   6 +-
 .../dart/worker/DartDataServerQueryHandler.java    |   2 +-
 .../org/apache/druid/msq/exec/ControllerImpl.java  |   4 +-
 .../java/org/apache/druid/msq/exec/MSQTasks.java   |  20 +-
 .../apache/druid/msq/guice/MSQIndexingModule.java  |   2 +
 .../error/BroadcastTablesTooLargeFault.java        |  10 +
 .../druid/msq/indexing/error/CanceledFault.java    |   4 +-
 .../msq/indexing/error/CancellationReason.java     |  33 +-
 .../error/CannotParseExternalDataFault.java        |  10 +
 .../msq/indexing/error/DruidExceptionFault.java    | 152 ++++++++
 .../error/DurableStorageConfigurationFault.java    |  10 +
 .../error/InsertCannotAllocateSegmentFault.java    |  10 +
 .../indexing/error/InsertCannotBeEmptyFault.java   |  10 +
 .../indexing/error/InsertLockPreemptedFault.java   |  10 +
 .../msq/indexing/error/InsertTimeNullFault.java    |  10 +
 .../indexing/error/InsertTimeOutOfBoundsFault.java |  10 +
 .../msq/indexing/error/InvalidFieldFault.java      |  13 +
 .../msq/indexing/error/InvalidNullByteFault.java   |  10 +
 .../druid/msq/indexing/error/MSQErrorReport.java   |  37 +-
 .../apache/druid/msq/indexing/error/MSQFault.java  |  11 +-
 .../msq/indexing/error/NotEnoughMemoryFault.java   |  10 +
 .../error/NotEnoughTemporaryStorageFault.java      |  10 +
 .../msq/indexing/error/QueryNotSupportedFault.java |   2 +-
 .../msq/indexing/error/QueryRuntimeFault.java      |  15 +-
 .../druid/msq/indexing/error/RowTooLargeFault.java |  10 +
 .../msq/indexing/error/TaskStartTimeoutFault.java  |  10 +
 .../msq/indexing/error/TooManyAttemptsForJob.java  |  10 +
 .../indexing/error/TooManyAttemptsForWorker.java   |  10 +
 .../msq/indexing/error/TooManyBucketsFault.java    |  10 +
 .../error/TooManyClusteredByColumnsFault.java      |  10 +
 .../msq/indexing/error/TooManyColumnsFault.java    |  10 +
 .../msq/indexing/error/TooManyInputFilesFault.java |  10 +
 .../msq/indexing/error/TooManyPartitionsFault.java |  10 +
 .../indexing/error/TooManyRowsInAWindowFault.java  |  10 +
 .../error/TooManyRowsWithSameKeyFault.java         |  10 +
 .../error/TooManySegmentsInTimeChunkFault.java     |  10 +
 .../msq/indexing/error/TooManyWarningsFault.java   |  10 +
 .../msq/indexing/error/TooManyWorkersFault.java    |  10 +
 .../druid/msq/indexing/error/UnknownFault.java     |  10 +
 .../msq/indexing/error/WorkerFailedFault.java      |  10 +
 .../msq/indexing/error/WorkerRpcFailedFault.java   |  10 +
 .../processor/SegmentGeneratorFrameProcessor.java  |   2 +
 .../druid/msq/input/RegularLoadableSegment.java    |   2 +-
 .../results/ExportResultsFrameProcessor.java       |   4 +-
 .../msq/sql/resources/SqlStatementResource.java    |   2 +-
 .../dart/controller/http/DartSqlResourceTest.java  |   4 +-
 .../org/apache/druid/msq/exec/MSQFaultsTest.java   |  27 ++
 .../org/apache/druid/msq/exec/MSQSelectTest.java   |   4 +-
 .../msq/indexing/error/MSQErrorReportTest.java     |   6 +-
 .../msq/indexing/error/MSQFaultSerdeTest.java      |  14 +
 .../error/MSQFaultToDruidExceptionTest.java        | 411 +++++++++++++++++++++
 .../druid/frame/write/RowBasedFrameWriter.java     |   5 +-
 .../org/apache/druid/java/util/common/Either.java  |  25 +-
 .../java/org/apache/druid/common/EitherTest.java   |  29 ++
 .../funcs_and_sql_func_json_keys.03.dart.iq        |  18 +-
 .../funcs_and_sql_func_json_paths.02.dart.iq       |   6 +-
 56 files changed, 1066 insertions(+), 74 deletions(-)

diff --git a/docs/multi-stage-query/reference.md 
b/docs/multi-stage-query/reference.md
index 29b044b46b8..b2311382052 100644
--- a/docs/multi-stage-query/reference.md
+++ b/docs/multi-stage-query/reference.md
@@ -582,6 +582,7 @@ The following table lists query limits:
 | Number of workers for any one stage. | Hard limit is 1,000. Memory-dependent 
soft limit may be lower. | `TooManyWorkers`|
 | Maximum memory occupied by broadcasted tables. | 30% of each [processor 
memory bundle](concepts.md#memory-usage). | `BroadcastTablesTooLarge` |
 | Maximum memory occupied by buffered data during sort-merge join. Only 
relevant when `sqlJoinAlgorithm` is `sortMerge`. | 10 MB | 
`TooManyRowsWithSameKey` |
+| Number of rows materialized in a single window partition. Configurable with 
`maxRowsMaterializedInWindow`. | 100,000 | `TooManyRowsInAWindow` |
 | Maximum relaunch attempts per worker. Initial run is not a relaunch. The 
worker will be spawned 1 + `workerRelaunchLimit` times before the job fails. | 
2 | `TooManyAttemptsForWorker` |
 | Maximum relaunch attempts for a job across all workers. | 100 | 
`TooManyAttemptsForJob` |
 <a name="errors"></a>
@@ -597,6 +598,8 @@ The following table describes error codes you may encounter 
in the `multiStageQu
 | `CannotParseExternalData`| A worker task could not parse data from an 
external datasource. | `errorMessage`: More details on why parsing failed. |
 | `ColumnNameRestricted`| The query uses a restricted column name. | 
`columnName`: The restricted column name. |
 | `ColumnTypeNotSupported` | The column type is not supported. This can be 
because:<br /> <br /><ul><li>Support for writing or reading from a particular 
column type is not supported.</li><li>The query attempted to use a column type 
that is not supported by the frame format. This occurs with ARRAY types, which 
are not yet implemented for frames.</li></ul> | `columnName`: The column name 
with an unsupported type.<br /> <br />`columnType`: The unknown column type. |
+| `DruidException` | An error occurred with structured information such as 
code, persona, and category. It does not fall into one of the other categories. 
| `druidErrorCode`: More specific error code.<br /><br />`persona`: Target 
persona.<br /><br />`category`: Error category.<br /><br />`context`: 
Additional context. |
+| `DurableStorageConfiguration` | Durable storage mode was enabled but is not 
configured correctly. Check the documentation on how to enable durable storage 
mode. | `message`: Details on the configuration error. |
 |`InsertCannotAllocateSegment`| The controller task could not allocate a new 
segment ID due to conflict with existing segments or pending segments. Common 
reasons for such conflicts:<br /> <br /><ul><li>Attempting to mix different 
granularities in the same intervals of the same datasource.</li><li>Prior 
ingestions that used non-extendable shard specs.</li></ul> <br /> <br /> Use 
REPLACE to overwrite the existing data or if the error contains the 
`allocatedInterval` then alternatively rer [...]
 |`InsertCannotBeEmpty`| An INSERT or REPLACE query did not generate any output 
rows when `failOnEmptyInsert` query context is set to true. `failOnEmptyInsert` 
defaults to false, so an INSERT query generating no output rows will be no-op, 
and a REPLACE query generating no output rows will delete all data that matches 
the OVERWRITE clause. | `dataSource` |
 | `InsertLockPreempted` | An INSERT or REPLACE query was canceled by a 
higher-priority ingestion job, such as a real-time ingestion task. | |
@@ -605,7 +608,6 @@ The following table describes error codes you may encounter 
in the `multiStageQu
 | `InvalidField`| An error was encountered while writing a field. | `error`: 
Encountered error. <br /><br /> `source`: Source for the error. <br /><br /> 
`rowNumber`: Row number (1-indexed) for the error. <br /><br /> `column`: 
Column for the error. |
 | `InvalidNullByte`| A string column included a null byte. Null bytes in 
strings are not permitted. |`source`: The source that included the null byte 
<br /><br /> `rowNumber`: The row number (1-indexed) that included the null 
byte <br /><br /> `column`: The column that included the null byte <br /><br /> 
`value`: Actual string containing the null byte <br /><br /> `position`: 
Position (1-indexed) of occurrence of null byte|
 | `QueryNotSupported`| QueryKit could not translate the provided native query 
to a multi-stage query.<br /> <br />This can happen if the query uses features 
that aren't supported, like GROUPING SETS. | |
-| `QueryRuntimeError` | MSQ uses the native query engine to run the leaf 
stages. This error tells MSQ that error is in native query runtime.<br /> <br 
/> Since this is a generic error, the user needs to look at logs for the error 
message and stack trace to figure out the next course of action. If the user is 
stuck, consider raising a `github` issue for assistance. |  `baseErrorMessage` 
error message from the native query runtime. |
 | `RowTooLarge`| The query tried to process a row that was too large to write 
to a single frame. See the [Limits](#limits) table for specific limits on frame 
size. Note that the effective maximum row size is smaller than the maximum 
frame size due to alignment considerations during frame writing. | 
`maxFrameSize`: The limit on the frame size. |
 | `TaskStartTimeout` | Unable to launch `pendingTasks` worker out of total 
`totalTasks` workers tasks within `timeout` seconds of the last successful 
worker launch.<br /><br />There may be insufficient available slots to start 
all the worker tasks simultaneously. Try splitting up your query into smaller 
chunks using a smaller value of [`maxNumTasks`](#context-parameters). Another 
option is to increase capacity. | `pendingTasks`: Number of tasks not yet 
started.<br /><br />`totalTasks`: T [...]
 | `TooManyAttemptsForJob` | Total relaunch attempt count across all workers 
exceeded max relaunch attempt limit. See the [Limits](#limits) table for the 
specific limit. | `maxRelaunchCount`: Max number of relaunches across all the 
workers defined in the [Limits](#limits) section. <br /><br /> 
`currentRelaunchCount`: current relaunch counter for the job across all 
workers. <br /><br /> `taskId`: Latest task id which failed <br /> <br /> 
`rootErrorMessage`: Error message of the latest fail [...]
@@ -615,6 +617,8 @@ The following table describes error codes you may encounter 
in the `multiStageQu
 | `TooManyPartitions`| Exceeded the maximum number of partitions for a stage 
(25,000 partitions).<br /><br />This can occur with INSERT or REPLACE 
statements that generate large numbers of segments, since each segment is 
associated with a partition. If you encounter this limit, consider breaking up 
your INSERT or REPLACE statement into smaller statements that process less data 
per statement. | `maxPartitions`: The limit on partitions which was exceeded |
 | `TooManyClusteredByColumns` | Exceeded the maximum number of clustering 
columns for a stage (1,500 columns).<br /><br />This can occur with `CLUSTERED 
BY`, `ORDER BY`, or `GROUP BY` with a large number of columns. | `numColumns`: 
The number of columns requested.<br /><br />`maxColumns`: The limit on columns 
which was exceeded.`stage`: The stage number exceeding the limit<br /><br /> |
 | `TooManyRowsWithSameKey` | The number of rows for a given key exceeded the 
maximum number of buffered bytes on both sides of a join. See the 
[Limits](#limits) table for the specific limit. Only occurs when join is 
executed via the sort-merge join algorithm. | `key`: The key that had a large 
number of rows.<br /><br />`numBytes`: Number of bytes buffered, which may 
include other keys.<br /><br />`maxBytes`: Maximum number of bytes buffered. |
+| `TooManyRowsInAWindow` | Too many rows materialized in a single window 
partition. Try using a window with a higher cardinality `PARTITION BY` column, 
changing the query shape, or increasing the limit with the 
`maxRowsMaterializedInWindow` query context parameter. | `numRows`: Number of 
rows encountered.<br /><br />`maxRows`: Maximum number of rows allowed. |
+| `TooManySegmentsInTimeChunk` | Too many segments requested to be generated 
in a single time chunk. Try breaking up the query or increase the limit using 
the `maxNumSegments` query context parameter. | `timeChunk`: The time chunk 
that exceeded the limit.<br /><br />`numSegments`: Number of segments 
requested.<br /><br />`maxNumSegments`: Maximum number of segments allowed.<br 
/><br />`segmentGranularity`: The segment granularity. |
 | `TooManyColumns` | Exceeded the maximum number of columns for a stage (2,000 
columns). | `numColumns`: The number of columns requested.<br /><br 
/>`maxColumns`: The limit on columns which was exceeded. |
 | `TooManyWarnings` | Exceeded the maximum allowed number of warnings of a 
particular type. | `rootErrorCode`: The error code corresponding to the 
exception that exceeded the required limit. <br /><br />`maxWarnings`: Maximum 
number of warnings that are allowed for the corresponding `rootErrorCode`. |
 | `TooManyWorkers`| Exceeded the maximum number of simultaneously-running 
workers. See the [Limits](#limits) table for more details. | `workers`: The 
number of simultaneously running workers that exceeded a hard or soft limit. 
This may be larger than the number of workers in any one stage if multiple 
stages are running simultaneously. <br /><br />`maxWorkers`: The hard or soft 
limit on workers that was exceeded. If this is lower than the hard limit (1,000 
workers), then you can increase  [...]
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/worker/DartDataServerQueryHandler.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/worker/DartDataServerQueryHandler.java
index e81f6ee92dc..28601da4a76 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/dart/worker/DartDataServerQueryHandler.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/dart/worker/DartDataServerQueryHandler.java
@@ -130,7 +130,7 @@ public class DartDataServerQueryHandler implements 
DataServerQueryHandler
 
           if (!missingSegments.isEmpty()) {
             throw DruidException
-                .forPersona(DruidException.Persona.USER)
+                .forPersona(DruidException.Persona.OPERATOR)
                 .ofCategory(DruidException.Category.RUNTIME_FAILURE)
                 .build(
                     "Segment[%s]%s not found on server[%s]. Please retry your 
query.",
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/exec/ControllerImpl.java 
b/multi-stage-query/src/main/java/org/apache/druid/msq/exec/ControllerImpl.java
index 958589598e3..082ab512d42 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/exec/ControllerImpl.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/exec/ControllerImpl.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.exec;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
@@ -698,7 +699,7 @@ public class ControllerImpl implements Controller
         }
       }
       catch (IOException e) {
-        throw DruidException.forPersona(DruidException.Persona.USER)
+        throw DruidException.forPersona(DruidException.Persona.OPERATOR)
             .ofCategory(DruidException.Category.RUNTIME_FAILURE)
             .build(e, "Exception occurred while connecting to export 
destination.");
       }
@@ -2253,6 +2254,7 @@ public class ControllerImpl implements Controller
       exec.cancel(RESULT_READER_CANCELLATION_ID);
     }
     catch (Exception e) {
+      Throwables.throwIfUnchecked(e);
       throw new RuntimeException(e);
     }
     finally {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/exec/MSQTasks.java 
b/multi-stage-query/src/main/java/org/apache/druid/msq/exec/MSQTasks.java
index 75dc1271e3a..b47124aef6b 100644
--- a/multi-stage-query/src/main/java/org/apache/druid/msq/exec/MSQTasks.java
+++ b/multi-stage-query/src/main/java/org/apache/druid/msq/exec/MSQTasks.java
@@ -21,17 +21,18 @@ package org.apache.druid.msq.exec;
 
 import com.google.inject.Injector;
 import com.google.inject.Key;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.java.util.common.ISE;
 import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.msq.guice.MultiStageQuery;
 import org.apache.druid.msq.indexing.error.CanceledFault;
+import org.apache.druid.msq.indexing.error.DruidExceptionFault;
 import org.apache.druid.msq.indexing.error.DurableStorageConfigurationFault;
 import org.apache.druid.msq.indexing.error.InsertTimeNullFault;
 import org.apache.druid.msq.indexing.error.MSQErrorReport;
 import org.apache.druid.msq.indexing.error.MSQException;
 import org.apache.druid.msq.indexing.error.MSQFault;
 import org.apache.druid.msq.indexing.error.MSQFaultUtils;
-import org.apache.druid.msq.indexing.error.QueryRuntimeFault;
 import org.apache.druid.msq.indexing.error.TooManyAttemptsForJob;
 import org.apache.druid.msq.indexing.error.TooManyAttemptsForWorker;
 import org.apache.druid.msq.indexing.error.UnknownFault;
@@ -221,8 +222,7 @@ public class MSQTasks
     logMessage.append(": 
").append(MSQFaultUtils.generateMessageWithErrorCode(errorReport.getFault()));
 
     if (errorReport.getExceptionStackTrace() != null) {
-      if (errorReport.getFault() instanceof UnknownFault || 
errorReport.getFault() instanceof QueryRuntimeFault) {
-        // Log full stack trace for UnknownFault and QueryRuntimeFault
+      if (logFullStackTrace(errorReport.getFault())) {
         logMessage.append('\n').append(errorReport.getExceptionStackTrace());
       } else {
         // Log first line only (error class, message) for known faults, to 
avoid polluting logs.
@@ -237,4 +237,18 @@ public class MSQTasks
 
     return logMessage.toString();
   }
+
+  /**
+   * Log full stack trace for UnknownFault and non-USER DruidExceptions.
+   */
+  static boolean logFullStackTrace(final MSQFault fault)
+  {
+    if (fault instanceof UnknownFault) {
+      return true;
+    } else if (fault instanceof DruidExceptionFault) {
+      return !DruidException.Persona.USER.name().equals(((DruidExceptionFault) 
fault).getPersona());
+    } else {
+      return false;
+    }
+  }
 }
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/guice/MSQIndexingModule.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/guice/MSQIndexingModule.java
index 6bcbc04981f..6b100e078b4 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/guice/MSQIndexingModule.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/guice/MSQIndexingModule.java
@@ -47,6 +47,7 @@ import org.apache.druid.msq.indexing.error.CanceledFault;
 import org.apache.druid.msq.indexing.error.CannotParseExternalDataFault;
 import org.apache.druid.msq.indexing.error.ColumnNameRestrictedFault;
 import org.apache.druid.msq.indexing.error.ColumnTypeNotSupportedFault;
+import org.apache.druid.msq.indexing.error.DruidExceptionFault;
 import org.apache.druid.msq.indexing.error.DurableStorageConfigurationFault;
 import org.apache.druid.msq.indexing.error.InsertCannotAllocateSegmentFault;
 import org.apache.druid.msq.indexing.error.InsertCannotBeEmptyFault;
@@ -130,6 +131,7 @@ public class MSQIndexingModule implements DruidModule
       CannotParseExternalDataFault.class,
       ColumnTypeNotSupportedFault.class,
       ColumnNameRestrictedFault.class,
+      DruidExceptionFault.class,
       DurableStorageConfigurationFault.class,
       InsertCannotAllocateSegmentFault.class,
       InsertCannotBeEmptyFault.class,
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/BroadcastTablesTooLargeFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/BroadcastTablesTooLargeFault.java
index 31337cbcfe6..fe561c6063a 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/BroadcastTablesTooLargeFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/BroadcastTablesTooLargeFault.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.query.JoinAlgorithm;
 import org.apache.druid.sql.calcite.planner.PlannerContext;
@@ -65,6 +66,15 @@ public class BroadcastTablesTooLargeFault extends 
BaseMSQFault
     return configuredJoinAlgorithm;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/CanceledFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/CanceledFault.java
index 329a511b0a8..29ec730fa41 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/CanceledFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/CanceledFault.java
@@ -36,7 +36,7 @@ public class CanceledFault extends BaseMSQFault
   @JsonCreator
   public CanceledFault(@JsonProperty("reason") @Nullable CancellationReason 
reason)
   {
-    super(CODE, "Query canceled due to [%s].", reason == null ? 
CancellationReason.UNKNOWN : reason);
+    super(CODE, (reason == null ? CancellationReason.UNKNOWN : 
reason).getMessage());
     this.reason = reason == null ? CancellationReason.UNKNOWN : reason;
   }
 
@@ -70,7 +70,7 @@ public class CanceledFault extends BaseMSQFault
   public DruidException toDruidException()
   {
     return DruidException.forPersona(DruidException.Persona.USER)
-                         .ofCategory(DruidException.Category.CANCELED)
+                         .ofCategory(reason.getCategory())
                          .withErrorCode(getErrorCode())
                          
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
   }
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/CancellationReason.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/CancellationReason.java
index 3e178fa54e6..4d8c5c7968e 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/CancellationReason.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/CancellationReason.java
@@ -20,6 +20,7 @@
 package org.apache.druid.msq.indexing.error;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
+import org.apache.druid.error.DruidException;
 
 /**
  * Enum denoting the reason for query cancellation.
@@ -27,27 +28,39 @@ import com.fasterxml.jackson.annotation.JsonCreator;
 public enum CancellationReason
 {
   /**
-   * Query was cancaled due to the task shutting down.
+   * Query was canceled due to the task shutting down.
    */
-  TASK_SHUTDOWN("Task shutdown"),
+  TASK_SHUTDOWN("Query canceled due to task shutdown", 
DruidException.Category.CANCELED),
   /**
-   * Query was cancaled due to a request from the user.
+   * Query was canceled due to a request from the user.
    */
-  USER_REQUEST("User request"),
+  USER_REQUEST("Query canceled", DruidException.Category.CANCELED),
   /**
    * Query was canceled due to exceeding the configured query timeout.
    */
-  QUERY_TIMEOUT("Query timeout"),
+  QUERY_TIMEOUT("Query timed out", DruidException.Category.TIMEOUT),
   /**
    * Query was canceled due to an unknown reason.
    */
-  UNKNOWN("Unknown");
+  UNKNOWN("Query interrupted", DruidException.Category.CANCELED);
 
-  private final String reason;
+  private final String message;
+  private final DruidException.Category category;
 
-  CancellationReason(String reason)
+  CancellationReason(final String message, final DruidException.Category 
category)
   {
-    this.reason = reason;
+    this.message = message;
+    this.category = category;
+  }
+
+  public String getMessage()
+  {
+    return message;
+  }
+
+  public DruidException.Category getCategory()
+  {
+    return category;
   }
 
   @JsonCreator
@@ -66,6 +79,6 @@ public enum CancellationReason
   @Override
   public String toString()
   {
-    return reason;
+    return message;
   }
 }
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/CannotParseExternalDataFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/CannotParseExternalDataFault.java
index 7668b68ea03..769e592b49c 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/CannotParseExternalDataFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/CannotParseExternalDataFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
 import com.google.common.base.Preconditions;
+import org.apache.druid.error.DruidException;
 
 @JsonTypeName(CannotParseExternalDataFault.CODE)
 public class CannotParseExternalDataFault extends BaseMSQFault
@@ -32,4 +33,13 @@ public class CannotParseExternalDataFault extends 
BaseMSQFault
   {
     super(CODE, Preconditions.checkNotNull(message, "errorMessage"));
   }
+
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.INVALID_INPUT)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
 }
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/DruidExceptionFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/DruidExceptionFault.java
new file mode 100644
index 00000000000..32dded914ee
--- /dev/null
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/DruidExceptionFault.java
@@ -0,0 +1,152 @@
+/*
+ * 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.msq.indexing.error;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Fault that wraps a {@link DruidException}, preserving its fields for 
serialization across workers and controllers.
+ *
+ * Fields are stored as Strings rather than enums so that a newer worker can 
send a fault to an older controller
+ * without causing Jackson deserialization failures from unknown enum values.
+ */
+@JsonTypeName(DruidExceptionFault.CODE)
+public class DruidExceptionFault extends BaseMSQFault
+{
+  public static final String CODE = "DruidException";
+
+  private final String druidErrorCode;
+  private final String persona;
+  private final String category;
+  private final Map<String, String> context;
+
+  @JsonCreator
+  public DruidExceptionFault(
+      @JsonProperty("druidErrorCode") final String druidErrorCode,
+      @JsonProperty("persona") final String persona,
+      @JsonProperty("category") final String category,
+      @JsonProperty("errorMessage") final String errorMessage,
+      @JsonProperty("context") final Map<String, String> context
+  )
+  {
+    super(CODE, errorMessage);
+    this.druidErrorCode = druidErrorCode;
+    this.persona = persona;
+    this.category = category;
+    this.context = context == null ? Map.of() : context;
+  }
+
+  public static DruidExceptionFault fromDruidException(final DruidException 
druidException)
+  {
+    return new DruidExceptionFault(
+        druidException.getErrorCode(),
+        druidException.getTargetPersona().name(),
+        druidException.getCategory().name(),
+        druidException.getMessage(),
+        druidException.getContext()
+    );
+  }
+
+  @JsonProperty
+  public String getDruidErrorCode()
+  {
+    return druidErrorCode;
+  }
+
+  @JsonProperty
+  public String getPersona()
+  {
+    return persona;
+  }
+
+  @JsonProperty
+  public String getCategory()
+  {
+    return category;
+  }
+
+  @JsonProperty
+  @JsonInclude(JsonInclude.Include.NON_EMPTY)
+  public Map<String, String> getContext()
+  {
+    return context;
+  }
+
+  @Override
+  public DruidException toDruidException()
+  {
+    final DruidException.Persona personaEnum =
+        safeValueOf(DruidException.Persona.class, persona, 
DruidException.Persona.DEVELOPER);
+    final DruidException.Category categoryEnum =
+        safeValueOf(DruidException.Category.class, category, 
DruidException.Category.UNCATEGORIZED);
+    return DruidException.forPersona(personaEnum)
+                         .ofCategory(categoryEnum)
+                         .withErrorCode(druidErrorCode)
+                         .build(getErrorMessage())
+                         .withContext(context);
+  }
+
+  @Override
+  public boolean equals(Object o)
+  {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    final DruidExceptionFault that = (DruidExceptionFault) o;
+    return Objects.equals(druidErrorCode, that.druidErrorCode)
+           && Objects.equals(persona, that.persona)
+           && Objects.equals(category, that.category)
+           && Objects.equals(context, that.context);
+  }
+
+  @Override
+  public int hashCode()
+  {
+    return Objects.hash(super.hashCode(), druidErrorCode, persona, category, 
context);
+  }
+
+  /**
+   * Utility function that returns an enum with a particular name, if it 
exists, or a default value otherwise.
+   * This exists to make it possible to add new {@link 
DruidException.Category} without breaking serde during
+   * rolling updates. Older code attempting to deserialize a new value will 
get a default value instead.
+   */
+  private static <T extends Enum<T>> T safeValueOf(final Class<T> enumClass, 
final String name, final T defaultValue)
+  {
+    try {
+      return Enum.valueOf(enumClass, name);
+    }
+    catch (IllegalArgumentException e) {
+      return defaultValue;
+    }
+  }
+}
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/DurableStorageConfigurationFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/DurableStorageConfigurationFault.java
index cd65e58c7b7..d1c1787571f 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/DurableStorageConfigurationFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/DurableStorageConfigurationFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.msq.guice.MSQDurableStorageModule;
 import org.apache.druid.msq.util.MultiStageQueryContext;
 
@@ -56,6 +57,15 @@ public class DurableStorageConfigurationFault extends 
BaseMSQFault
     return errorMessage;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.OPERATOR)
+                         .ofCategory(DruidException.Category.INVALID_INPUT)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertCannotAllocateSegmentFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertCannotAllocateSegmentFault.java
index f632ae67736..5cbfb40da54 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertCannotAllocateSegmentFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertCannotAllocateSegmentFault.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
 import com.google.common.base.Preconditions;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.java.util.common.granularity.DurationGranularity;
 import org.apache.druid.java.util.common.granularity.GranularityType;
@@ -118,6 +119,15 @@ public class InsertCannotAllocateSegmentFault extends 
BaseMSQFault
     }
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertCannotBeEmptyFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertCannotBeEmptyFault.java
index dd2eea9ac7f..83fe3184999 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertCannotBeEmptyFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertCannotBeEmptyFault.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
 import com.google.common.base.Preconditions;
+import org.apache.druid.error.DruidException;
 
 import java.util.Objects;
 
@@ -49,6 +50,15 @@ public class InsertCannotBeEmptyFault extends BaseMSQFault
     return dataSource;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.INVALID_INPUT)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertLockPreemptedFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertLockPreemptedFault.java
index c355dc3ff67..0d57bd6f7c1 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertLockPreemptedFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertLockPreemptedFault.java
@@ -21,6 +21,7 @@ package org.apache.druid.msq.indexing.error;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 @JsonTypeName(InsertLockPreemptedFault.CODE)
 public class InsertLockPreemptedFault extends BaseMSQFault
@@ -37,6 +38,15 @@ public class InsertLockPreemptedFault extends BaseMSQFault
     );
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.CONFLICT)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @JsonCreator
   public static InsertLockPreemptedFault instance()
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertTimeNullFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertTimeNullFault.java
index bcdc592eefc..707b6cebb29 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertTimeNullFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertTimeNullFault.java
@@ -21,6 +21,7 @@ package org.apache.druid.msq.indexing.error;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.segment.column.ColumnHolder;
 
 @JsonTypeName(InsertTimeNullFault.CODE)
@@ -34,6 +35,15 @@ public class InsertTimeNullFault extends BaseMSQFault
     super(CODE, "Null timestamp (%s) encountered during INSERT or REPLACE.", 
ColumnHolder.TIME_COLUMN_NAME);
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.INVALID_INPUT)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @JsonCreator
   public static InsertTimeNullFault instance()
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertTimeOutOfBoundsFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertTimeOutOfBoundsFault.java
index 97638b95708..a433393d9a1 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertTimeOutOfBoundsFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InsertTimeOutOfBoundsFault.java
@@ -21,6 +21,7 @@ package org.apache.druid.msq.indexing.error;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 import org.joda.time.Interval;
 
 import java.util.List;
@@ -63,6 +64,15 @@ public class InsertTimeOutOfBoundsFault extends BaseMSQFault
     return intervalBounds;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.INVALID_INPUT)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InvalidFieldFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InvalidFieldFault.java
index 09834fee20f..a96bb5b6846 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InvalidFieldFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InvalidFieldFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import javax.annotation.Nullable;
 import java.util.Objects;
@@ -100,6 +101,18 @@ public class InvalidFieldFault extends BaseMSQFault
     return logMsg;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    // DEVELOPER since this fault is generated when InvalidFieldException is 
used as a wrapper around
+    // miscellaneous exceptions whose meaning is not clear. Underlying code 
that has something specific
+    // to say should throw DruidException.
+    return DruidException.forPersona(DruidException.Persona.DEVELOPER)
+                         .ofCategory(DruidException.Category.INVALID_INPUT)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InvalidNullByteFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InvalidNullByteFault.java
index 6b88daf7599..44d72b36759 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InvalidNullByteFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/InvalidNullByteFault.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import javax.annotation.Nullable;
 import java.util.Objects;
@@ -122,6 +123,15 @@ public class InvalidNullByteFault extends BaseMSQFault
     return position;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.INVALID_INPUT)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/MSQErrorReport.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/MSQErrorReport.java
index 8c66280294e..025553961d0 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/MSQErrorReport.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/MSQErrorReport.java
@@ -31,7 +31,6 @@ import org.apache.druid.frame.write.InvalidFieldException;
 import org.apache.druid.frame.write.InvalidNullByteException;
 import org.apache.druid.frame.write.UnsupportedColumnTypeException;
 import org.apache.druid.indexing.common.task.batch.TooManyBucketsException;
-import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.java.util.common.parsers.ParseException;
 import 
org.apache.druid.query.groupby.epinephelinae.UnexpectedMultiValueDimensionException;
 import org.apache.druid.sql.calcite.planner.ColumnMappings;
@@ -227,6 +226,11 @@ public class MSQErrorReport
 
     Throwable cause = e;
 
+    // Remember the first DruidException we encounter, but keep walking the 
cause chain in case a more specific
+    // exception type is nested inside (e.g. when Either.valueOrThrow() wraps 
an InvalidNullByteException in a
+    // DruidException).
+    DruidException firstDruidException = null;
+
     // This method will grow as we try to add more faults and exceptions
     // One way of handling this would be to extend the faults to have a method 
like
     // public MSQFault fromException(@Nullable Throwable e) which returns the 
specific fault if it can be reconstructed
@@ -292,22 +296,31 @@ public class MSQErrorReport
         );
 
       } else if (cause instanceof UnexpectedMultiValueDimensionException) {
-        return new QueryRuntimeFault(
-            StringUtils.format(
-                "Column [%s] is a multi-value string. Please wrap the column 
using MV_TO_ARRAY() to proceed further.",
-                ((UnexpectedMultiValueDimensionException) 
cause).getDimensionName()
-            ), cause.getMessage()
+        return DruidExceptionFault.fromDruidException(
+            DruidException
+                .forPersona(DruidException.Persona.USER)
+                .ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                .build(
+                    cause,
+                    "Column [%s] is a multi-value string. "
+                    + "Please wrap the column using MV_TO_ARRAY() to proceed 
further.",
+                    ((UnexpectedMultiValueDimensionException) 
cause).getDimensionName()
+                )
         );
       } else if (cause instanceof InterruptedException) {
         return CanceledFault.unknown();
-      } else if 
(cause.getClass().getPackage().getName().startsWith("org.apache.druid.query")) {
-        // catch all for all query runtime exception faults.
-        return new QueryRuntimeFault(e.getMessage(), null);
-      } else {
-        cause = cause.getCause();
+      } else if (cause instanceof DruidException && firstDruidException == 
null) {
+        firstDruidException = (DruidException) cause;
       }
+
+      cause = cause.getCause();
     }
 
-    return UnknownFault.forException(e);
+    if (firstDruidException != null) {
+      // Didn't find any more specific exception wrapped underneath the first 
DruidException, so go with that one.
+      return DruidExceptionFault.fromDruidException(firstDruidException);
+    } else {
+      return UnknownFault.forException(e);
+    }
   }
 }
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/MSQFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/MSQFault.java
index 39efce9d204..035a524b5bf 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/MSQFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/MSQFault.java
@@ -39,15 +39,6 @@ public interface MSQFault
 
   /**
    * Returns a {@link DruidException} corresponding to this fault.
-   *
-   * The default is a {@link DruidException.Category#RUNTIME_FAILURE} 
targeting {@link DruidException.Persona#USER}.
-   * Faults with different personas and categories should override this method.
    */
-  default DruidException toDruidException()
-  {
-    return DruidException.forPersona(DruidException.Persona.USER)
-                         .ofCategory(DruidException.Category.RUNTIME_FAILURE)
-                         .withErrorCode(getErrorCode())
-                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
-  }
+  DruidException toDruidException();
 }
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/NotEnoughMemoryFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/NotEnoughMemoryFault.java
index d4360a09d1a..2859a2daf36 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/NotEnoughMemoryFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/NotEnoughMemoryFault.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.java.util.common.StringUtils;
 
 import java.util.Objects;
@@ -130,6 +131,15 @@ public class NotEnoughMemoryFault extends BaseMSQFault
     return maxConcurrentStages;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.OPERATOR)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/NotEnoughTemporaryStorageFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/NotEnoughTemporaryStorageFault.java
index 523bde42de4..2a9a6e07b26 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/NotEnoughTemporaryStorageFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/NotEnoughTemporaryStorageFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import java.util.Objects;
 
@@ -63,6 +64,15 @@ public class NotEnoughTemporaryStorageFault extends 
BaseMSQFault
     return configuredTemporaryStorage;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.OPERATOR)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/QueryNotSupportedFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/QueryNotSupportedFault.java
index 7356cc02909..ed893931133 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/QueryNotSupportedFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/QueryNotSupportedFault.java
@@ -37,7 +37,7 @@ public class QueryNotSupportedFault extends BaseMSQFault
   @Override
   public DruidException toDruidException()
   {
-    return DruidException.forPersona(DruidException.Persona.USER)
+    return DruidException.forPersona(DruidException.Persona.DEVELOPER)
                          .ofCategory(DruidException.Category.UNSUPPORTED)
                          .withErrorCode(getErrorCode())
                          
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/QueryRuntimeFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/QueryRuntimeFault.java
index 0950ae46633..b1191593781 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/QueryRuntimeFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/QueryRuntimeFault.java
@@ -22,12 +22,16 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import javax.annotation.Nullable;
 import java.util.Objects;
 
 /**
- * Fault to throw when the error comes from the druid native query runtime 
while running in the MSQ engine.
+ * Currently unused. Remains for compatibility with older workers that may 
still report it, and older reports
+ * that still contain it. Currently, {@link DruidExceptionFault} is preferred 
for {@link DruidException} or
+ * recognized exceptions that are thrown by the query stack. Fault {@link 
UnknownFault} is preferred for
+ * other errors from the query stack.
  */
 @JsonTypeName(QueryRuntimeFault.CODE)
 public class QueryRuntimeFault extends BaseMSQFault
@@ -54,6 +58,15 @@ public class QueryRuntimeFault extends BaseMSQFault
     return baseErrorMessage;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.DEVELOPER)
+                         .ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/RowTooLargeFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/RowTooLargeFault.java
index 790e83ceff8..067513cbce8 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/RowTooLargeFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/RowTooLargeFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.msq.util.MultiStageQueryContext;
 
 import java.util.Objects;
@@ -52,6 +53,15 @@ public class RowTooLargeFault extends BaseMSQFault
     return maxFrameSize;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TaskStartTimeoutFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TaskStartTimeoutFault.java
index 1eba2e8dc77..18b71b30062 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TaskStartTimeoutFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TaskStartTimeoutFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.msq.util.MultiStageQueryContext;
 
 import java.util.Objects;
@@ -77,6 +78,15 @@ public class TaskStartTimeoutFault extends BaseMSQFault
     return timeout;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.OPERATOR)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyAttemptsForJob.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyAttemptsForJob.java
index a2cb9c3d1fb..13118ea4d6b 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyAttemptsForJob.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyAttemptsForJob.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import java.util.Objects;
 
@@ -87,6 +88,15 @@ public class TooManyAttemptsForJob extends BaseMSQFault
     return rootErrorMessage;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.OPERATOR)
+                         .ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyAttemptsForWorker.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyAttemptsForWorker.java
index dbf9b607e23..29267f67d03 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyAttemptsForWorker.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyAttemptsForWorker.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import java.util.Objects;
 
@@ -86,6 +87,15 @@ public class TooManyAttemptsForWorker extends BaseMSQFault
     return rootErrorMessage;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.OPERATOR)
+                         .ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyBucketsFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyBucketsFault.java
index fdad421e649..df70faf8ecd 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyBucketsFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyBucketsFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import java.util.Objects;
 
@@ -53,6 +54,15 @@ public class TooManyBucketsFault extends BaseMSQFault
     return maxBuckets;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyClusteredByColumnsFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyClusteredByColumnsFault.java
index 8fa80082dca..e3ff3b24285 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyClusteredByColumnsFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyClusteredByColumnsFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import java.util.Objects;
 
@@ -65,6 +66,15 @@ public class TooManyClusteredByColumnsFault extends 
BaseMSQFault
     return stage;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyColumnsFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyColumnsFault.java
index 05b9d0d197a..d9f68e2d170 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyColumnsFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyColumnsFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import java.util.Objects;
 
@@ -56,6 +57,15 @@ public class TooManyColumnsFault extends BaseMSQFault
     return maxColumns;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyInputFilesFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyInputFilesFault.java
index cae7a5c36d7..dad156566d1 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyInputFilesFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyInputFilesFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.msq.util.MultiStageQueryContext;
 
 import java.util.Objects;
@@ -77,6 +78,15 @@ public class TooManyInputFilesFault extends BaseMSQFault
     return minNumWorkers;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyPartitionsFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyPartitionsFault.java
index e59b6645f75..f9a6aebb9c3 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyPartitionsFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyPartitionsFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.msq.util.MultiStageQueryContext;
 
 import java.util.Objects;
@@ -52,6 +53,15 @@ public class TooManyPartitionsFault extends BaseMSQFault
     return maxPartitions;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyRowsInAWindowFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyRowsInAWindowFault.java
index b1905fbb226..0b71505c107 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyRowsInAWindowFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyRowsInAWindowFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.msq.util.MultiStageQueryContext;
 
 import java.util.Objects;
@@ -67,6 +68,15 @@ public class TooManyRowsInAWindowFault extends BaseMSQFault
     return maxRows;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyRowsWithSameKeyFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyRowsWithSameKeyFault.java
index be284ae502d..3259a1adbef 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyRowsWithSameKeyFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyRowsWithSameKeyFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import java.util.List;
 import java.util.Objects;
@@ -74,6 +75,15 @@ public class TooManyRowsWithSameKeyFault extends BaseMSQFault
     return maxBytes;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManySegmentsInTimeChunkFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManySegmentsInTimeChunkFault.java
index ac0c2a641da..18e8d272a6d 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManySegmentsInTimeChunkFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManySegmentsInTimeChunkFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.java.util.common.granularity.Granularity;
 import org.apache.druid.java.util.common.granularity.GranularityType;
 import org.apache.druid.msq.util.MultiStageQueryContext;
@@ -103,6 +104,15 @@ public class TooManySegmentsInTimeChunkFault extends 
BaseMSQFault
     return segmentGranularity;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyWarningsFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyWarningsFault.java
index 57663cb0ddb..0682fda6e2a 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyWarningsFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyWarningsFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import java.util.Objects;
 
@@ -56,6 +57,15 @@ public class TooManyWarningsFault extends BaseMSQFault
     return rootErrorCode;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyWorkersFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyWorkersFault.java
index 42376ca8cb8..cefbe0cfe02 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyWorkersFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/TooManyWorkersFault.java
@@ -22,6 +22,7 @@ package org.apache.druid.msq.indexing.error;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import java.util.Objects;
 
@@ -56,6 +57,15 @@ public class TooManyWorkersFault extends BaseMSQFault
     return maxWorkers;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.USER)
+                         .ofCategory(DruidException.Category.CAPACITY_EXCEEDED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/UnknownFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/UnknownFault.java
index 3a14683613d..a3c8d5f096a 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/UnknownFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/UnknownFault.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import javax.annotation.Nullable;
 import java.util.Objects;
@@ -60,6 +61,15 @@ public class UnknownFault extends BaseMSQFault
     return message;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.DEVELOPER)
+                         .ofCategory(DruidException.Category.UNCATEGORIZED)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/WorkerFailedFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/WorkerFailedFault.java
index f2c579abeb5..a10928261ad 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/WorkerFailedFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/WorkerFailedFault.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import javax.annotation.Nullable;
 import java.util.Objects;
@@ -62,6 +63,15 @@ public class WorkerFailedFault extends BaseMSQFault
     return errorMsg;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.OPERATOR)
+                         .ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/WorkerRpcFailedFault.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/WorkerRpcFailedFault.java
index 9754d0ed840..a5cfe113299 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/WorkerRpcFailedFault.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/error/WorkerRpcFailedFault.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.error.DruidException;
 
 import javax.annotation.Nullable;
 import java.util.Objects;
@@ -60,6 +61,15 @@ public class WorkerRpcFailedFault extends BaseMSQFault
     return errorMsg;
   }
 
+  @Override
+  public DruidException toDruidException()
+  {
+    return DruidException.forPersona(DruidException.Persona.OPERATOR)
+                         .ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                         .withErrorCode(getErrorCode())
+                         
.build(MSQFaultUtils.generateMessageWithErrorCode(this));
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/processor/SegmentGeneratorFrameProcessor.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/processor/SegmentGeneratorFrameProcessor.java
index d42ce9455fd..e37c755bdd5 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/processor/SegmentGeneratorFrameProcessor.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/processor/SegmentGeneratorFrameProcessor.java
@@ -19,6 +19,7 @@
 
 package org.apache.druid.msq.indexing.processor;
 
+import com.google.common.base.Throwables;
 import com.google.common.collect.Iterables;
 import com.google.common.util.concurrent.ListenableFuture;
 import it.unimi.dsi.fastutil.ints.IntSet;
@@ -208,6 +209,7 @@ public class SegmentGeneratorFrameProcessor implements 
FrameProcessor<DataSegmen
           segmentGenerationProgressCounter.incrementRowsProcessed(1);
         }
         catch (Exception e) {
+          Throwables.throwIfUnchecked(e);
           throw new RuntimeException(e);
         }
 
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/input/RegularLoadableSegment.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/input/RegularLoadableSegment.java
index 33399e354a1..7582cd2514c 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/input/RegularLoadableSegment.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/input/RegularLoadableSegment.java
@@ -225,7 +225,7 @@ public class RegularLoadableSegment implements 
LoadableSegment
    */
   private DruidException segmentNotFound()
   {
-    return DruidException.forPersona(DruidException.Persona.USER)
+    return DruidException.forPersona(DruidException.Persona.OPERATOR)
                          .ofCategory(DruidException.Category.RUNTIME_FAILURE)
                          .build("Segment[%s] not found on this server. Please 
retry your query.", segmentId);
   }
diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/results/ExportResultsFrameProcessor.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/results/ExportResultsFrameProcessor.java
index 9c9a9028892..7c5558791f9 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/results/ExportResultsFrameProcessor.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/results/ExportResultsFrameProcessor.java
@@ -197,7 +197,7 @@ public class ExportResultsFrameProcessor implements 
FrameProcessor<Object>
       }
     }
     catch (IOException e) {
-      throw DruidException.forPersona(DruidException.Persona.USER)
+      throw DruidException.forPersona(DruidException.Persona.OPERATOR)
                           .ofCategory(DruidException.Category.RUNTIME_FAILURE)
                           .build(
                               e,
@@ -223,7 +223,7 @@ public class ExportResultsFrameProcessor implements 
FrameProcessor<Object>
       if (stream != null) {
         stream.close();
       }
-      throw DruidException.forPersona(DruidException.Persona.USER)
+      throw DruidException.forPersona(DruidException.Persona.OPERATOR)
                           .ofCategory(DruidException.Category.RUNTIME_FAILURE)
                           .build(e, "Exception occurred while opening a stream 
to the export location [%s].", exportFilePath);
     }
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 06adbc74f19..b71d9d2221d 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
@@ -988,7 +988,7 @@ public class SqlStatementResource
   private void checkForDurableStorageConnectorImpl()
   {
     if (storageConnector instanceof NilStorageConnector) {
-      throw DruidException.forPersona(DruidException.Persona.USER)
+      throw DruidException.forPersona(DruidException.Persona.OPERATOR)
                           .ofCategory(DruidException.Category.INVALID_INPUT)
                           .build(
                               StringUtils.format(
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java
 
b/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java
index 1b2d7d8a547..03ebec54087 100644
--- 
a/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java
@@ -581,7 +581,7 @@ public class DartSqlResourceTest extends MSQTestBase
     );
 
     Assertions.assertNull(sqlResource.doPost(sqlQuery, httpServletRequest));
-    
Assertions.assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
asyncResponse.getStatus());
+    Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
asyncResponse.getStatus());
 
     final Map<String, Object> e = objectMapper.readValue(
         asyncResponse.baos.toByteArray(),
@@ -589,7 +589,7 @@ public class DartSqlResourceTest extends MSQTestBase
     );
 
     Assertions.assertEquals("InvalidNullByte", e.get("errorCode"));
-    Assertions.assertEquals("RUNTIME_FAILURE", e.get("category"));
+    Assertions.assertEquals("INVALID_INPUT", e.get("category"));
     assertThat((String) e.get("errorMessage"), 
CoreMatchers.startsWith("InvalidNullByte: "));
   }
 
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQFaultsTest.java 
b/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQFaultsTest.java
index f83b86be77c..b74c76ee766 100644
--- 
a/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQFaultsTest.java
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQFaultsTest.java
@@ -34,6 +34,7 @@ import org.apache.druid.java.util.common.ISE;
 import org.apache.druid.java.util.common.Intervals;
 import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.java.util.common.granularity.Granularities;
+import org.apache.druid.msq.indexing.error.DruidExceptionFault;
 import org.apache.druid.msq.indexing.error.InsertCannotAllocateSegmentFault;
 import org.apache.druid.msq.indexing.error.InsertCannotBeEmptyFault;
 import org.apache.druid.msq.indexing.error.InsertTimeNullFault;
@@ -711,6 +712,32 @@ public class MSQFaultsTest extends MSQTestBase
                      .verifyExecutionError();
   }
 
+  @Test
+  public void testDruidExceptionFault()
+  {
+    // BITWISE_COMPLEMENT(m1 * 1e19) throws because the double value exceeds 
Long range. The expression engine
+    // wraps this in DruidException, which getFaultFromException() converts to 
DruidExceptionFault.
+    final Map<String, Object> context = ImmutableMap.<String, Object>builder()
+                                                    
.putAll(DEFAULT_MSQ_CONTEXT)
+                                                    .put("vectorize", "false")
+                                                    .build();
+
+    testSelectQuery()
+        .setSql("SELECT BITWISE_COMPLEMENT(m1 * 1e19) FROM foo")
+        .setQueryContext(context)
+        .setExpectedMSQFault(
+            new DruidExceptionFault(
+                "general",
+                "USER",
+                "INVALID_INPUT",
+                "Function[bitwiseComplement] Possible data truncation, param 
[10000000000000000000.000000]"
+                + " is out of LONG value range",
+                Collections.emptyMap()
+            )
+        )
+        .verifyResults();
+  }
+
   private void testLockTypes(TaskLockType contextTaskLockType, String sql, 
String errorMessage)
   {
     Map<String, Object> context = new HashMap<>(DEFAULT_MSQ_CONTEXT);
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQSelectTest.java 
b/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQSelectTest.java
index 509e93333a8..7b16ff41e75 100644
--- 
a/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQSelectTest.java
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQSelectTest.java
@@ -2808,9 +2808,7 @@ public class MSQSelectTest extends MSQTestBase
         .setExpectedMSQFault(CanceledFault.timeout())
         .setExpectedExecutionErrorMatcher(CoreMatchers.allOf(
             CoreMatchers.instanceOf(ISE.class),
-            ThrowableMessageMatcher.hasMessage(CoreMatchers.containsString(
-                " Query canceled due to [Configured query timeout].")
-            )
+            
ThrowableMessageMatcher.hasMessage(CoreMatchers.containsString("Query timed 
out"))
         ))
         .verifyExecutionError();
   }
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/error/MSQErrorReportTest.java
 
b/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/error/MSQErrorReportTest.java
index 0570195cdee..f7641938f3c 100644
--- 
a/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/error/MSQErrorReportTest.java
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/error/MSQErrorReportTest.java
@@ -60,11 +60,13 @@ public class MSQErrorReportTest
     Assert.assertEquals(new RowTooLargeFault(10), 
MSQErrorReport.getFaultFromException(tooLargeException));
 
     UnexpectedMultiValueDimensionException mvException = new 
UnexpectedMultiValueDimensionException(ERROR_MESSAGE);
-    Assert.assertEquals(QueryRuntimeFault.CODE, 
MSQErrorReport.getFaultFromException(mvException).getErrorCode());
+    Assert.assertEquals(DruidExceptionFault.CODE, 
MSQErrorReport.getFaultFromException(mvException).getErrorCode());
 
+    // QueryTimeoutException (legacy QueryException, not a DruidException) 
becomes an UnknownFault.
+    // Only DruidExceptions become nice faults.
     QueryTimeoutException queryException = new 
QueryTimeoutException(ERROR_MESSAGE);
     Assert.assertEquals(
-        new QueryRuntimeFault(ERROR_MESSAGE, null),
+        UnknownFault.forException(queryException),
         MSQErrorReport.getFaultFromException(queryException)
     );
 
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/error/MSQFaultSerdeTest.java
 
b/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/error/MSQFaultSerdeTest.java
index 594b7bf244d..e4ef013a515 100644
--- 
a/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/error/MSQFaultSerdeTest.java
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/error/MSQFaultSerdeTest.java
@@ -60,6 +60,20 @@ public class MSQFaultSerdeTest
     assertFaultSerde(new ColumnTypeNotSupportedFault("the column", null));
     assertFaultSerde(new ColumnTypeNotSupportedFault("the column", 
ColumnType.STRING_ARRAY));
     assertFaultSerde(new ColumnNameRestrictedFault("the column"));
+    assertFaultSerde(new DruidExceptionFault(
+        "general",
+        "USER",
+        "INVALID_INPUT",
+        "test message",
+        Collections.emptyMap()
+    ));
+    assertFaultSerde(new DruidExceptionFault(
+        "custom",
+        "ADMIN",
+        "RUNTIME_FAILURE",
+        "another message",
+        Collections.singletonMap("key", "value")
+    ));
     assertFaultSerde(new InsertCannotAllocateSegmentFault("the datasource", 
Intervals.ETERNITY, null));
     assertFaultSerde(new InsertCannotAllocateSegmentFault(
         "the datasource",
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/error/MSQFaultToDruidExceptionTest.java
 
b/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/error/MSQFaultToDruidExceptionTest.java
new file mode 100644
index 00000000000..5391ec21850
--- /dev/null
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/error/MSQFaultToDruidExceptionTest.java
@@ -0,0 +1,411 @@
+/*
+ * 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.msq.indexing.error;
+
+import org.apache.druid.error.DruidException;
+import org.apache.druid.java.util.common.DateTimes;
+import org.apache.druid.java.util.common.Intervals;
+import org.apache.druid.java.util.common.granularity.Granularities;
+import org.apache.druid.query.JoinAlgorithm;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+public class MSQFaultToDruidExceptionTest
+{
+  @Test
+  public void testBroadcastTablesTooLargeFault()
+  {
+    final DruidException e = new BroadcastTablesTooLargeFault(10, 
JoinAlgorithm.SORT_MERGE).toDruidException();
+    Assert.assertEquals("BroadcastTablesTooLarge", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testCanceledFaultShutdown()
+  {
+    final DruidException e = CanceledFault.shutdown().toDruidException();
+    Assert.assertEquals("Canceled", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CANCELED, e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testCanceledFaultTimeout()
+  {
+    final DruidException e = CanceledFault.timeout().toDruidException();
+    Assert.assertEquals("Canceled", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.TIMEOUT, e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testCanceledFaultUserRequest()
+  {
+    final DruidException e = CanceledFault.userRequest().toDruidException();
+    Assert.assertEquals("Canceled", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CANCELED, e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testCanceledFaultUnknown()
+  {
+    final DruidException e = CanceledFault.unknown().toDruidException();
+    Assert.assertEquals("Canceled", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CANCELED, e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testCannotParseExternalDataFault()
+  {
+    final DruidException e = new CannotParseExternalDataFault("the 
message").toDruidException();
+    Assert.assertEquals("CannotParseExternalData", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.INVALID_INPUT, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testDruidExceptionFault()
+  {
+    final DruidException e = new DruidExceptionFault(
+        "custom",
+        "USER",
+        "RUNTIME_FAILURE",
+        "test message",
+        Collections.singletonMap("key", "value")
+    ).toDruidException();
+    Assert.assertEquals("custom", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.RUNTIME_FAILURE, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+    Assert.assertEquals(Collections.singletonMap("key", "value"), 
e.getContext());
+  }
+
+  @Test
+  public void testDruidExceptionFaultUnknownEnums()
+  {
+    final DruidException e = new DruidExceptionFault(
+        "custom",
+        "NONEXISTENT_PERSONA",
+        "NONEXISTENT_CATEGORY",
+        "test message",
+        Collections.emptyMap()
+    ).toDruidException();
+    Assert.assertEquals("custom", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.UNCATEGORIZED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.DEVELOPER, 
e.getTargetPersona());
+  }
+
+  @Test
+  public void testDurableStorageConfigurationFault()
+  {
+    final DruidException e = new DurableStorageConfigurationFault("some 
error").toDruidException();
+    Assert.assertEquals("DurableStorageConfiguration", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.INVALID_INPUT, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.OPERATOR, e.getTargetPersona());
+  }
+
+  @Test
+  public void testInsertCannotAllocateSegmentFault()
+  {
+    final DruidException e = new InsertCannotAllocateSegmentFault(
+        "the datasource",
+        Intervals.ETERNITY,
+        null
+    ).toDruidException();
+    Assert.assertEquals("InsertCannotAllocateSegment", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.RUNTIME_FAILURE, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testInsertCannotBeEmptyFault()
+  {
+    final DruidException e = new InsertCannotBeEmptyFault("the 
datasource").toDruidException();
+    Assert.assertEquals("InsertCannotBeEmpty", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.INVALID_INPUT, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testInsertLockPreemptedFault()
+  {
+    final DruidException e = 
InsertLockPreemptedFault.INSTANCE.toDruidException();
+    Assert.assertEquals("InsertLockPreempted", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CONFLICT, e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testInsertTimeNullFault()
+  {
+    final DruidException e = InsertTimeNullFault.INSTANCE.toDruidException();
+    Assert.assertEquals("InsertTimeNull", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.INVALID_INPUT, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testInsertTimeOutOfBoundsFault()
+  {
+    final DruidException e = new InsertTimeOutOfBoundsFault(
+        Intervals.of("2001/2002"),
+        Collections.singletonList(Intervals.of("2000/2001"))
+    ).toDruidException();
+    Assert.assertEquals("InsertTimeOutOfBounds", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.INVALID_INPUT, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testInvalidFieldFault()
+  {
+    final DruidException e = new InvalidFieldFault(
+        "the source",
+        "the column",
+        1,
+        "the error",
+        "the log msg"
+    ).toDruidException();
+    Assert.assertEquals("InvalidField", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.INVALID_INPUT, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.DEVELOPER, 
e.getTargetPersona());
+  }
+
+  @Test
+  public void testInvalidNullByteFault()
+  {
+    final DruidException e = new InvalidNullByteFault(
+        "the source",
+        1,
+        "the column",
+        "the value",
+        2
+    ).toDruidException();
+    Assert.assertEquals("InvalidNullByte", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.INVALID_INPUT, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testNotEnoughMemoryFault()
+  {
+    final DruidException e = new NotEnoughMemoryFault(
+        1234,
+        1000,
+        1000,
+        900,
+        1,
+        2,
+        2
+    ).toDruidException();
+    Assert.assertEquals("NotEnoughMemory", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.OPERATOR, e.getTargetPersona());
+  }
+
+  @Test
+  public void testNotEnoughTemporaryStorageFault()
+  {
+    final DruidException e = new NotEnoughTemporaryStorageFault(250, 
2).toDruidException();
+    Assert.assertEquals("NotEnoughTemporaryStorageFault", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.OPERATOR, e.getTargetPersona());
+  }
+
+  @Test
+  public void testQueryNotSupportedFault()
+  {
+    final DruidException e = 
QueryNotSupportedFault.INSTANCE.toDruidException();
+    Assert.assertEquals("QueryNotSupported", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.UNSUPPORTED, e.getCategory());
+    Assert.assertEquals(DruidException.Persona.DEVELOPER, 
e.getTargetPersona());
+  }
+
+  @Test
+  public void testQueryRuntimeFault()
+  {
+    final DruidException e = new QueryRuntimeFault("new error", "base 
error").toDruidException();
+    Assert.assertEquals("QueryRuntimeError", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.RUNTIME_FAILURE, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.DEVELOPER, 
e.getTargetPersona());
+  }
+
+  @Test
+  public void testRowTooLargeFault()
+  {
+    final DruidException e = new RowTooLargeFault(1000).toDruidException();
+    Assert.assertEquals("RowTooLarge", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testTaskStartTimeoutFault()
+  {
+    final DruidException e = new TaskStartTimeoutFault(1, 10, 
11).toDruidException();
+    Assert.assertEquals("TaskStartTimeout", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.OPERATOR, e.getTargetPersona());
+  }
+
+  @Test
+  public void testTooManyAttemptsForJob()
+  {
+    final DruidException e = new TooManyAttemptsForJob(2, 2, "taskId", 
"rootError").toDruidException();
+    Assert.assertEquals("TooManyAttemptsForJob", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.RUNTIME_FAILURE, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.OPERATOR, e.getTargetPersona());
+  }
+
+  @Test
+  public void testTooManyAttemptsForWorker()
+  {
+    final DruidException e = new TooManyAttemptsForWorker(2, "taskId", 1, 
"rootError").toDruidException();
+    Assert.assertEquals("TooManyAttemptsForWorker", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.RUNTIME_FAILURE, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.OPERATOR, e.getTargetPersona());
+  }
+
+  @Test
+  public void testTooManyBucketsFault()
+  {
+    final DruidException e = new TooManyBucketsFault(10).toDruidException();
+    Assert.assertEquals("TooManyBuckets", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testTooManyClusteredByColumnsFault()
+  {
+    final DruidException e = new TooManyClusteredByColumnsFault(10, 8, 
1).toDruidException();
+    Assert.assertEquals("TooManyClusteredByColumns", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testTooManyColumnsFault()
+  {
+    final DruidException e = new TooManyColumnsFault(10, 8).toDruidException();
+    Assert.assertEquals("TooManyColumns", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testTooManyInputFilesFault()
+  {
+    final DruidException e = new TooManyInputFilesFault(15, 10, 
5).toDruidException();
+    Assert.assertEquals("TooManyInputFiles", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testTooManyPartitionsFault()
+  {
+    final DruidException e = new TooManyPartitionsFault(10).toDruidException();
+    Assert.assertEquals("TooManyPartitions", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testTooManyRowsInAWindowFault()
+  {
+    final DruidException e = new TooManyRowsInAWindowFault(10, 
20).toDruidException();
+    Assert.assertEquals("TooManyRowsInAWindow", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testTooManyRowsWithSameKeyFault()
+  {
+    final DruidException e = new TooManyRowsWithSameKeyFault(
+        Arrays.asList("foo", 123), 1, 2
+    ).toDruidException();
+    Assert.assertEquals("TooManyRowsWithSameKey", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testTooManySegmentsInTimeChunkFault()
+  {
+    final DruidException e = new TooManySegmentsInTimeChunkFault(
+        DateTimes.nowUtc(), 10, 1, Granularities.ALL
+    ).toDruidException();
+    Assert.assertEquals("TooManySegmentsInTimeChunk", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testTooManyWarningsFault()
+  {
+    final DruidException e = new TooManyWarningsFault(10, "the 
error").toDruidException();
+    Assert.assertEquals("TooManyWarnings", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.RUNTIME_FAILURE, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testTooManyWorkersFault()
+  {
+    final DruidException e = new TooManyWorkersFault(10, 5).toDruidException();
+    Assert.assertEquals("TooManyWorkers", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.CAPACITY_EXCEEDED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.USER, e.getTargetPersona());
+  }
+
+  @Test
+  public void testUnknownFault()
+  {
+    final DruidException e = UnknownFault.forMessage("the 
message").toDruidException();
+    Assert.assertEquals("UnknownError", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.UNCATEGORIZED, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.DEVELOPER, 
e.getTargetPersona());
+  }
+
+  @Test
+  public void testWorkerFailedFault()
+  {
+    final DruidException e = new WorkerFailedFault("the worker task", "the 
error msg").toDruidException();
+    Assert.assertEquals("WorkerFailed", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.RUNTIME_FAILURE, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.OPERATOR, e.getTargetPersona());
+  }
+
+  @Test
+  public void testWorkerRpcFailedFault()
+  {
+    final DruidException e = new WorkerRpcFailedFault("the worker task", "the 
error msg").toDruidException();
+    Assert.assertEquals("WorkerRpcFailed", e.getErrorCode());
+    Assert.assertEquals(DruidException.Category.RUNTIME_FAILURE, 
e.getCategory());
+    Assert.assertEquals(DruidException.Persona.OPERATOR, e.getTargetPersona());
+  }
+}
diff --git 
a/processing/src/main/java/org/apache/druid/frame/write/RowBasedFrameWriter.java
 
b/processing/src/main/java/org/apache/druid/frame/write/RowBasedFrameWriter.java
index aeb9beadba0..7af90bd82b9 100644
--- 
a/processing/src/main/java/org/apache/druid/frame/write/RowBasedFrameWriter.java
+++ 
b/processing/src/main/java/org/apache/druid/frame/write/RowBasedFrameWriter.java
@@ -19,7 +19,6 @@
 
 package org.apache.druid.frame.write;
 
-import com.google.common.base.Throwables;
 import com.google.common.primitives.Ints;
 import org.apache.datasketches.memory.Memory;
 import org.apache.datasketches.memory.WritableMemory;
@@ -304,8 +303,8 @@ public class RowBasedFrameWriter implements FrameWriter
                                       .column(signature.getColumnName(i))
                                       .build();
       }
-      catch (ParseException pe) {
-        throw Throwables.propagate(pe);
+      catch (DruidException | ParseException e) {
+        throw e;
       }
       catch (Exception e) {
         throw 
InvalidFieldException.builder().column(signature.getColumnName(i))
diff --git 
a/processing/src/main/java/org/apache/druid/java/util/common/Either.java 
b/processing/src/main/java/org/apache/druid/java/util/common/Either.java
index 5a2720d0748..a9c0072fe4a 100644
--- a/processing/src/main/java/org/apache/druid/java/util/common/Either.java
+++ b/processing/src/main/java/org/apache/druid/java/util/common/Either.java
@@ -20,6 +20,7 @@
 package org.apache.druid.java.util.common;
 
 import com.google.common.base.Preconditions;
+import org.apache.druid.error.DruidException;
 
 import javax.annotation.Nullable;
 import java.util.Objects;
@@ -79,8 +80,9 @@ public class Either<L, R>
   /**
    * If this Either represents a value, returns it. If this Either represents 
an error, throw an error.
    *
-   * If the error is a {@link Throwable}, it is wrapped in a RuntimeException 
and thrown. If it is not a throwable,
-   * a generic error is thrown containing the string representation of the 
error object.
+   * If the error is a {@link DruidException}, it is thrown. If it is some 
other {@link Throwable}, it is
+   * wrapped in a {@link DruidException} and thrown. If it is not a throwable, 
a generic {@link DruidException}
+   * is thrown containing the string representation of the error object.
    *
    * To retrieve the error as-is, use {@link #isError()} and {@link #error()} 
instead.
    */
@@ -90,11 +92,22 @@ public class Either<L, R>
     if (isValue()) {
       return value;
     } else if (error instanceof Throwable) {
-      // Always wrap Throwable, even if we could throw it directly, to provide 
additional context
-      // about where the exception happened (we want the current stack frame 
in the trace).
-      throw new RuntimeException((Throwable) error);
+      // The exception happened somewhere else, perhaps in another thread 
entirely. If it is a DruidException
+      // targeting a non-DEVELOPER persona, re-throw it as-is so we keep the 
original intent of the error message.
+      // Otherwise, wrap it in a DEVELOPER-oriented DruidException so the 
stack trace of the current thread is
+      // added to the exception details.
+      if (error instanceof DruidException
+          && ((DruidException) error).getTargetPersona() != 
DruidException.Persona.DEVELOPER) {
+        throw (DruidException) error;
+      }
+
+      throw DruidException.forPersona(DruidException.Persona.DEVELOPER)
+                          .ofCategory(DruidException.Category.UNCATEGORIZED)
+                          .build((Throwable) error, ((Throwable) 
error).getMessage());
     } else {
-      throw new RuntimeException(error.toString());
+      throw DruidException.forPersona(DruidException.Persona.DEVELOPER)
+                          .ofCategory(DruidException.Category.UNCATEGORIZED)
+                          .build("%s", error);
     }
   }
 
diff --git a/processing/src/test/java/org/apache/druid/common/EitherTest.java 
b/processing/src/test/java/org/apache/druid/common/EitherTest.java
index bb081a24242..a440e9676fc 100644
--- a/processing/src/test/java/org/apache/druid/common/EitherTest.java
+++ b/processing/src/test/java/org/apache/druid/common/EitherTest.java
@@ -20,6 +20,7 @@
 package org.apache.druid.common;
 
 import nl.jqno.equalsverifier.EqualsVerifier;
+import org.apache.druid.error.DruidException;
 import org.apache.druid.java.util.common.Either;
 import org.apache.druid.java.util.common.StringUtils;
 import org.hamcrest.CoreMatchers;
@@ -104,6 +105,34 @@ public class EitherTest
     Assert.assertEquals("Error[java.lang.AssertionError: oh no]", 
either.toString());
   }
 
+  @Test
+  public void testErrorDruidExceptionUserPersona()
+  {
+    final DruidException original = 
DruidException.forPersona(DruidException.Persona.USER)
+                                                  
.ofCategory(DruidException.Category.INVALID_INPUT)
+                                                  .build("bad input");
+    final Either<DruidException, Object> either = Either.error(original);
+
+    // Non-DEVELOPER DruidExceptions are re-thrown as-is by valueOrThrow.
+    final DruidException e = Assert.assertThrows(DruidException.class, 
either::valueOrThrow);
+    Assert.assertSame(original, e);
+  }
+
+  @Test
+  public void testErrorDruidExceptionDeveloperPersona()
+  {
+    final DruidException original = 
DruidException.forPersona(DruidException.Persona.DEVELOPER)
+                                                  
.ofCategory(DruidException.Category.UNCATEGORIZED)
+                                                  .build("internal error");
+    final Either<DruidException, Object> either = Either.error(original);
+
+    // DEVELOPER DruidExceptions are wrapped to capture the current stack 
trace.
+    final DruidException e = Assert.assertThrows(DruidException.class, 
either::valueOrThrow);
+    Assert.assertNotSame(original, e);
+    Assert.assertSame(original, e.getCause());
+    Assert.assertEquals(DruidException.Persona.DEVELOPER, 
e.getTargetPersona());
+  }
+
   @Test
   public void testEqualsAndHashCode()
   {
diff --git 
a/quidem-ut/src/test/quidem/org.apache.druid.quidem.QTest/qaJsonCols/funcs_and_sql_func_json_keys.03.dart.iq
 
b/quidem-ut/src/test/quidem/org.apache.druid.quidem.QTest/qaJsonCols/funcs_and_sql_func_json_keys.03.dart.iq
index 5e666556a32..d67c29642a9 100644
--- 
a/quidem-ut/src/test/quidem/org.apache.druid.quidem.QTest/qaJsonCols/funcs_and_sql_func_json_keys.03.dart.iq
+++ 
b/quidem-ut/src/test/quidem/org.apache.druid.quidem.QTest/qaJsonCols/funcs_and_sql_func_json_keys.03.dart.iq
@@ -371,7 +371,7 @@ WHERE json_keys(c1, '$.a_obj.a_num_int') = json_keys(c2, 
'$.a_obj.a_num_int');
 SELECT count(*) c
 FROM test_json_cols t1
 INNER JOIN test_json_cols t2 ON json_keys(t1.c1, '$.a_obj')=json_keys(t2.c1, 
'$.a_obj');
-QueryUnsupportedException
+Joining against ARRAY columns is not supported
 !error
 
 #-------------------------------------------------------------------------
@@ -380,7 +380,7 @@ QueryUnsupportedException
 SELECT count(*) c
 FROM test_json_cols t1
 INNER JOIN test_json_cols t2 ON json_keys(t1.c1, 
'$.a_obj.a_str')=json_keys(t2.c1, '$.a_obj.a_str');
-QueryUnsupportedException
+Joining against ARRAY columns is not supported
 !error
 
 #-------------------------------------------------------------------------
@@ -389,7 +389,7 @@ QueryUnsupportedException
 SELECT count(*) c
 FROM test_json_cols t1
 INNER JOIN test_json_cols t2 ON json_keys(t1.c1, 
'$.a_obj.a_num_int')=json_keys(t2.c1, '$.a_obj.a_num_int');
-QueryUnsupportedException
+Joining against ARRAY columns is not supported
 !error
 
 #-------------------------------------------------------------------------
@@ -398,7 +398,7 @@ QueryUnsupportedException
 SELECT count(*) c
 FROM test_json_cols t1
 LEFT JOIN test_json_cols t2 ON json_keys(t1.c1, '$.a_obj')=json_keys(t2.c1, 
'$.a_obj');
-QueryUnsupportedException
+Joining against ARRAY columns is not supported
 !error
 
 #-------------------------------------------------------------------------
@@ -407,7 +407,7 @@ QueryUnsupportedException
 SELECT count(*) c
 FROM test_json_cols t1
 LEFT JOIN test_json_cols t2 ON json_keys(t1.c1, 
'$.a_obj.a_str')=json_keys(t2.c1, '$.a_obj.a_str');
-QueryUnsupportedException
+Joining against ARRAY columns is not supported
 !error
 
 #-------------------------------------------------------------------------
@@ -416,7 +416,7 @@ QueryUnsupportedException
 SELECT count(*) c
 FROM test_json_cols t1
 LEFT JOIN test_json_cols t2 ON json_keys(t1.c1, 
'$.a_obj.a_num_int')=json_keys(t2.c1, '$.a_obj.a_num_int');
-QueryUnsupportedException
+Joining against ARRAY columns is not supported
 !error
 
 #-------------------------------------------------------------------------
@@ -426,7 +426,7 @@ SELECT count(*) c
 FROM test_json_cols t1,
      test_json_cols t2
 WHERE json_keys(t1.c1, '$.a_obj')=json_keys(t2.c1, '$.a_obj');
-QueryUnsupportedException
+Joining against ARRAY columns is not supported
 !error
 
 #-------------------------------------------------------------------------
@@ -436,7 +436,7 @@ SELECT count(*) c
 FROM test_json_cols t1,
      test_json_cols t2
 WHERE json_keys(t1.c1, '$.a_obj.a_str')=json_keys(t2.c1, '$.a_obj.a_str');
-QueryUnsupportedException
+Joining against ARRAY columns is not supported
 !error
 
 #-------------------------------------------------------------------------
@@ -446,7 +446,7 @@ SELECT count(*) c
 FROM test_json_cols t1,
      test_json_cols t2
 WHERE json_keys(t1.c1, '$.a_obj.a_num_int')=json_keys(t2.c1, 
'$.a_obj.a_num_int');
-QueryUnsupportedException
+Joining against ARRAY columns is not supported
 !error
 
 #-------------------------------------------------------------------------
diff --git 
a/quidem-ut/src/test/quidem/org.apache.druid.quidem.QTest/qaJsonCols/funcs_and_sql_func_json_paths.02.dart.iq
 
b/quidem-ut/src/test/quidem/org.apache.druid.quidem.QTest/qaJsonCols/funcs_and_sql_func_json_paths.02.dart.iq
index 530d91cb68f..895b750ad88 100644
--- 
a/quidem-ut/src/test/quidem/org.apache.druid.quidem.QTest/qaJsonCols/funcs_and_sql_func_json_paths.02.dart.iq
+++ 
b/quidem-ut/src/test/quidem/org.apache.druid.quidem.QTest/qaJsonCols/funcs_and_sql_func_json_paths.02.dart.iq
@@ -732,7 +732,7 @@ WHERE json_paths(c1) = json_paths(c2);
 SELECT count(*) c
 FROM test_json_cols t1
 INNER JOIN test_json_cols t2 ON json_paths(t1.c1)=json_paths(t2.c1);
-QueryUnsupportedException
+Joining against ARRAY columns is not supported
 !error
 
 #-------------------------------------------------------------------------
@@ -741,7 +741,7 @@ QueryUnsupportedException
 SELECT count(*) c
 FROM test_json_cols t1
 LEFT JOIN test_json_cols t2 ON json_paths(t1.c1)=json_paths(t2.c1);
-QueryUnsupportedException
+Joining against ARRAY columns is not supported
 !error
 
 #-------------------------------------------------------------------------
@@ -751,7 +751,7 @@ SELECT count(*) c
 FROM test_json_cols t1,
      test_json_cols t2
 WHERE json_paths(t1.c1)=json_paths(t2.c1);
-QueryUnsupportedException
+Joining against ARRAY columns is not supported
 !error
 
 #-------------------------------------------------------------------------


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

Reply via email to