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

vogievetsky 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 a23170e1586 Web console: support SET syntax in queries (#17966)
a23170e1586 is described below

commit a23170e15867382d4f42d6ad17237d2286cc69cd
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Thu May 1 11:14:27 2025 -0700

    Web console: support SET syntax in queries (#17966)
    
    * fix explain and semicolon suggestion
    
    * move Edit context
    
    * update for CodeQL feedback
    
    * removed left over old comment
---
 licenses.yaml                                      |   2 +-
 web-console/package-lock.json                      |  26 +--
 web-console/package.json                           |   2 +-
 .../array-ingest-mode-switch.spec.tsx.snap}        |   2 +-
 .../array-ingest-mode-switch.spec.tsx}             |   8 +-
 .../array-ingest-mode-switch.tsx}                  |  18 +-
 web-console/src/components/index.ts                |   2 +-
 .../array-ingest-mode/array-ingest-mode.ts}        |  13 +-
 .../druid-models/dimension-spec/dimension-spec.ts  |   5 +-
 .../external-config/external-config.ts             |   6 +-
 web-console/src/druid-models/index.ts              |   1 +
 .../ingest-query-pattern.spec.ts                   |  10 +
 .../ingest-query-pattern/ingest-query-pattern.ts   |  30 ++-
 .../ingestion-spec/ingestion-spec.spec.ts          |  10 +-
 .../druid-models/ingestion-spec/ingestion-spec.tsx |  25 ++-
 .../druid-models/query-context/query-context.tsx   |   3 +-
 .../workbench-query/workbench-query.ts             |  19 +-
 .../__snapshots__/spec-conversion.spec.ts.snap     |  29 +++
 web-console/src/helpers/spec-conversion.spec.ts    |   6 +-
 web-console/src/helpers/spec-conversion.ts         |  37 ++--
 web-console/src/utils/druid-query.spec.ts          |  12 +-
 web-console/src/utils/druid-query.ts               |  17 +-
 web-console/src/utils/explain.spec.ts              | 145 ++++++++++++++
 .../explain.ts}                                    |  29 ++-
 web-console/src/utils/index.tsx                    |   1 +
 web-console/src/utils/sql.spec.ts                  | 220 +++++++++++++++++++++
 web-console/src/utils/sql.ts                       |  55 ++++--
 .../src/views/load-data-view/load-data-view.tsx    |  46 ++---
 .../schema-step/schema-step.tsx                    |  14 +-
 .../sql-data-loader-view/sql-data-loader-view.tsx  |  20 --
 .../connect-external-data-dialog.tsx               |   9 +-
 .../explain-dialog/explain-dialog.tsx              |  10 +-
 .../input-format-step/input-format-step.tsx        |  26 ++-
 .../views/workbench-view/run-panel/run-panel.tsx   | 180 +++++++++--------
 34 files changed, 747 insertions(+), 291 deletions(-)

diff --git a/licenses.yaml b/licenses.yaml
index 33c09795182..9509f442d24 100644
--- a/licenses.yaml
+++ b/licenses.yaml
@@ -5762,7 +5762,7 @@ license_category: binary
 module: web-console
 license_name: Apache License version 2.0
 copyright: Imply Data
-version: 1.0.2
+version: 1.1.1
 
 ---
 
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 27eef016a1d..006e331466c 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -30,7 +30,7 @@
         "d3-shape": "^3.2.0",
         "d3-time-format": "^4.1.0",
         "date-fns": "^2.28.0",
-        "druid-query-toolkit": "^1.0.2",
+        "druid-query-toolkit": "^1.1.1",
         "echarts": "^5.5.1",
         "file-saver": "^2.0.5",
         "hjson": "^3.2.2",
@@ -7067,9 +7067,9 @@
       }
     },
     "node_modules/druid-query-toolkit": {
-      "version": "1.0.2",
-      "resolved": 
"https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-1.0.2.tgz";,
-      "integrity": 
"sha512-TXu8io3oF04g0xhiGeXBB3xsGt2kJtp+95jrSja1a5Db8NFpkso6UPbfc6vlok24bky1aUKObTPJVVKOwSaBIw==",
+      "version": "1.1.1",
+      "resolved": 
"https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-1.1.1.tgz";,
+      "integrity": 
"sha512-+DtPaCf7WPitr/G1YKUWsYGy4vrDzybQUqbFpyGMKIkqwcmTdCS10qfb9+eGKUu/3EIx4FVBoXFeRcMCdJ73Ow==",
       "license": "Apache-2.0",
       "dependencies": {
         "tslib": "^2.5.2"
@@ -9273,9 +9273,9 @@
       "dev": true
     },
     "node_modules/http-proxy-middleware": {
-      "version": "2.0.7",
-      "resolved": 
"https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz";,
-      "integrity": 
"sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
+      "version": "2.0.9",
+      "resolved": 
"https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz";,
+      "integrity": 
"sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -23311,9 +23311,9 @@
       }
     },
     "druid-query-toolkit": {
-      "version": "1.0.2",
-      "resolved": 
"https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-1.0.2.tgz";,
-      "integrity": 
"sha512-TXu8io3oF04g0xhiGeXBB3xsGt2kJtp+95jrSja1a5Db8NFpkso6UPbfc6vlok24bky1aUKObTPJVVKOwSaBIw==",
+      "version": "1.1.1",
+      "resolved": 
"https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-1.1.1.tgz";,
+      "integrity": 
"sha512-+DtPaCf7WPitr/G1YKUWsYGy4vrDzybQUqbFpyGMKIkqwcmTdCS10qfb9+eGKUu/3EIx4FVBoXFeRcMCdJ73Ow==",
       "requires": {
         "tslib": "^2.5.2"
       }
@@ -24922,9 +24922,9 @@
       }
     },
     "http-proxy-middleware": {
-      "version": "2.0.7",
-      "resolved": 
"https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz";,
-      "integrity": 
"sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
+      "version": "2.0.9",
+      "resolved": 
"https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz";,
+      "integrity": 
"sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
       "dev": true,
       "requires": {
         "@types/http-proxy": "^1.17.8",
diff --git a/web-console/package.json b/web-console/package.json
index 038c42962ef..ac3f4891b17 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -71,7 +71,7 @@
     "d3-shape": "^3.2.0",
     "d3-time-format": "^4.1.0",
     "date-fns": "^2.28.0",
-    "druid-query-toolkit": "^1.0.2",
+    "druid-query-toolkit": "^1.1.1",
     "echarts": "^5.5.1",
     "file-saver": "^2.0.5",
     "hjson": "^3.2.2",
diff --git 
a/web-console/src/components/array-mode-switch/__snapshots__/array-mode-swtich.spec.tsx.snap
 
b/web-console/src/components/array-ingest-mode-switch/__snapshots__/array-ingest-mode-switch.spec.tsx.snap
similarity index 94%
rename from 
web-console/src/components/array-mode-switch/__snapshots__/array-mode-swtich.spec.tsx.snap
rename to 
web-console/src/components/array-ingest-mode-switch/__snapshots__/array-ingest-mode-switch.spec.tsx.snap
index 278de27a359..12c7ddf5b9f 100644
--- 
a/web-console/src/components/array-mode-switch/__snapshots__/array-mode-swtich.spec.tsx.snap
+++ 
b/web-console/src/components/array-ingest-mode-switch/__snapshots__/array-ingest-mode-switch.spec.tsx.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`ArrayModeSwitch matches snapshot 1`] = `
+exports[`ArrayIngestModeSwitch matches snapshot 1`] = `
 <div
   class="bp5-form-group form-group-with-info"
 >
diff --git 
a/web-console/src/components/array-mode-switch/array-mode-swtich.spec.tsx 
b/web-console/src/components/array-ingest-mode-switch/array-ingest-mode-switch.spec.tsx
similarity index 81%
copy from 
web-console/src/components/array-mode-switch/array-mode-swtich.spec.tsx
copy to 
web-console/src/components/array-ingest-mode-switch/array-ingest-mode-switch.spec.tsx
index a9ac2320c2d..db2737e7ebe 100644
--- a/web-console/src/components/array-mode-switch/array-mode-swtich.spec.tsx
+++ 
b/web-console/src/components/array-ingest-mode-switch/array-ingest-mode-switch.spec.tsx
@@ -18,11 +18,13 @@
 
 import { render } from '@testing-library/react';
 
-import { ArrayModeSwitch } from './array-mode-switch';
+import { ArrayIngestModeSwitch } from './array-ingest-mode-switch';
 
-describe('ArrayModeSwitch', () => {
+describe('ArrayIngestModeSwitch', () => {
   it('matches snapshot', () => {
-    const arrayInput = <ArrayModeSwitch arrayMode="multi-values" 
changeArrayMode={() => {}} />;
+    const arrayInput = (
+      <ArrayIngestModeSwitch arrayIngestMode="mvd" changeArrayIngestMode={() 
=> {}} />
+    );
 
     const { container } = render(arrayInput);
     expect(container.firstChild).toMatchSnapshot();
diff --git a/web-console/src/components/array-mode-switch/array-mode-switch.tsx 
b/web-console/src/components/array-ingest-mode-switch/array-ingest-mode-switch.tsx
similarity index 79%
rename from web-console/src/components/array-mode-switch/array-mode-switch.tsx
rename to 
web-console/src/components/array-ingest-mode-switch/array-ingest-mode-switch.tsx
index 4d9de1274b9..5f7cb4d9c93 100644
--- a/web-console/src/components/array-mode-switch/array-mode-switch.tsx
+++ 
b/web-console/src/components/array-ingest-mode-switch/array-ingest-mode-switch.tsx
@@ -19,19 +19,21 @@
 import { Switch } from '@blueprintjs/core';
 import React from 'react';
 
-import type { ArrayMode } from '../../druid-models';
+import type { ArrayIngestMode } from '../../druid-models';
 import { getLink } from '../../links';
 import { ExternalLink } from '../external-link/external-link';
 import { FormGroupWithInfo } from 
'../form-group-with-info/form-group-with-info';
 import { PopoverText } from '../popover-text/popover-text';
 
-export interface ArrayModeSwitchProps {
-  arrayMode: ArrayMode;
-  changeArrayMode(newArrayMode: ArrayMode): void;
+export interface ArrayIngestModeSwitchProps {
+  arrayIngestMode: ArrayIngestMode;
+  changeArrayIngestMode(arrayIngestMode: ArrayIngestMode): void;
 }
 
-export const ArrayModeSwitch = React.memo(function ArrayModeSwitch(props: 
ArrayModeSwitchProps) {
-  const { arrayMode, changeArrayMode } = props;
+export const ArrayIngestModeSwitch = React.memo(function ArrayIngestModeSwitch(
+  props: ArrayIngestModeSwitchProps,
+) {
+  const { arrayIngestMode, changeArrayIngestMode } = props;
 
   return (
     <FormGroupWithInfo
@@ -54,8 +56,8 @@ export const ArrayModeSwitch = React.memo(function 
ArrayModeSwitch(props: ArrayM
       <Switch
         label="Store ARRAYs as MVDs"
         className="legacy-switch"
-        checked={arrayMode === 'multi-values'}
-        onChange={() => changeArrayMode(arrayMode === 'arrays' ? 
'multi-values' : 'arrays')}
+        checked={arrayIngestMode === 'mvd'}
+        onChange={() => changeArrayIngestMode(arrayIngestMode === 'array' ? 
'mvd' : 'array')}
       />
     </FormGroupWithInfo>
   );
diff --git a/web-console/src/components/index.ts 
b/web-console/src/components/index.ts
index 60f35c51191..88c0981645c 100644
--- a/web-console/src/components/index.ts
+++ b/web-console/src/components/index.ts
@@ -18,8 +18,8 @@
 
 export * from './action-cell/action-cell';
 export * from './action-icon/action-icon';
+export * from './array-ingest-mode-switch/array-ingest-mode-switch';
 export * from './array-input/array-input';
-export * from './array-mode-switch/array-mode-switch';
 export * from './auto-form/auto-form';
 export * from './braced-text/braced-text';
 export * from './center-message/center-message';
diff --git 
a/web-console/src/components/array-mode-switch/array-mode-swtich.spec.tsx 
b/web-console/src/druid-models/array-ingest-mode/array-ingest-mode.ts
similarity index 68%
copy from 
web-console/src/components/array-mode-switch/array-mode-swtich.spec.tsx
copy to web-console/src/druid-models/array-ingest-mode/array-ingest-mode.ts
index a9ac2320c2d..beef8521c8c 100644
--- a/web-console/src/components/array-mode-switch/array-mode-swtich.spec.tsx
+++ b/web-console/src/druid-models/array-ingest-mode/array-ingest-mode.ts
@@ -16,15 +16,6 @@
  * limitations under the License.
  */
 
-import { render } from '@testing-library/react';
+export type ArrayIngestMode = 'array' | 'mvd';
 
-import { ArrayModeSwitch } from './array-mode-switch';
-
-describe('ArrayModeSwitch', () => {
-  it('matches snapshot', () => {
-    const arrayInput = <ArrayModeSwitch arrayMode="multi-values" 
changeArrayMode={() => {}} />;
-
-    const { container } = render(arrayInput);
-    expect(container.firstChild).toMatchSnapshot();
-  });
-});
+export const DEFAULT_ARRAY_INGEST_MODE: ArrayIngestMode = 'array';
diff --git a/web-console/src/druid-models/dimension-spec/dimension-spec.ts 
b/web-console/src/druid-models/dimension-spec/dimension-spec.ts
index 3037cbe0838..85933bdeb54 100644
--- a/web-console/src/druid-models/dimension-spec/dimension-spec.ts
+++ b/web-console/src/druid-models/dimension-spec/dimension-spec.ts
@@ -20,6 +20,7 @@ import type { Field } from '../../components';
 import { filterMap, typeIsKnown } from '../../utils';
 import type { SampleResponse, TimeColumnAction } from '../../utils/sampler';
 import { getHeaderNamesFromSampleResponse } from '../../utils/sampler';
+import type { ArrayIngestMode } from '../array-ingest-mode/array-ingest-mode';
 import { guessColumnTypeFromSampleResponse } from 
'../ingestion-spec/ingestion-spec';
 import { TIME_COLUMN } from '../timestamp-spec/timestamp-spec';
 
@@ -165,7 +166,7 @@ export function getDimensionSpecs(
   sampleResponse: SampleResponse,
   columnTypeHints: Record<string, string>,
   guessNumericStringsAsNumbers: boolean,
-  forceMvdInsteadOfArray: boolean,
+  arrayIngestMode: ArrayIngestMode,
   hasRollup: boolean,
   timeColumnAction: TimeColumnAction,
 ): (string | DimensionSpec)[] {
@@ -182,7 +183,7 @@ export function getDimensionSpecs(
     );
     let columnType = columnTypeHint || guessedColumnType;
 
-    if (forceMvdInsteadOfArray) {
+    if (arrayIngestMode === 'mvd') {
       if (columnType.startsWith('ARRAY')) {
         columnType = MADE_UP_MV_COLUMN_TYPE;
       }
diff --git a/web-console/src/druid-models/external-config/external-config.ts 
b/web-console/src/druid-models/external-config/external-config.ts
index 6fa74eb038b..dc4dfbcf496 100644
--- a/web-console/src/druid-models/external-config/external-config.ts
+++ b/web-console/src/druid-models/external-config/external-config.ts
@@ -32,7 +32,7 @@ import {
 import * as JSONBig from 'json-bigint-native';
 
 import { nonEmptyArray } from '../../utils';
-import type { ArrayMode } from '../ingestion-spec/ingestion-spec';
+import type { ArrayIngestMode } from '../array-ingest-mode/array-ingest-mode';
 import type { InputFormat } from '../input-format/input-format';
 import type { InputSource } from '../input-source/input-source';
 
@@ -129,7 +129,7 @@ export function externalConfigToTableExpression(config: 
ExternalConfig): SqlExpr
 export function externalConfigToInitDimensions(
   config: ExternalConfig,
   timeExpression: SqlExpression | undefined,
-  arrayMode: ArrayMode,
+  arrayIngestMode: ArrayIngestMode,
 ): SqlExpression[] {
   return (timeExpression ? [timeExpression.setAlias('__time')] : [])
     .concat(
@@ -137,7 +137,7 @@ export function externalConfigToInitDimensions(
         const columnName = columnDeclaration.getColumnName();
         if (timeExpression && timeExpression.containsColumnName(columnName)) 
return;
         return C(columnName).applyIf(
-          arrayMode === 'multi-values' && 
columnDeclaration.columnType.isArray(),
+          arrayIngestMode === 'mvd' && columnDeclaration.columnType.isArray(),
           ex => F('ARRAY_TO_MV', ex).as(columnName),
         );
       }),
diff --git a/web-console/src/druid-models/index.ts 
b/web-console/src/druid-models/index.ts
index 2e23cf7ee2c..6031c8ef624 100644
--- a/web-console/src/druid-models/index.ts
+++ b/web-console/src/druid-models/index.ts
@@ -16,6 +16,7 @@
  * limitations under the License.
  */
 
+export * from './array-ingest-mode/array-ingest-mode';
 export * from './async-query/async-query';
 export * from './compaction-config/compaction-config';
 export * from './compaction-dynamic-config/compaction-dynamic-config';
diff --git 
a/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.spec.ts
 
b/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.spec.ts
index cf108c2b389..00c6a2bb54b 100644
--- 
a/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.spec.ts
+++ 
b/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.spec.ts
@@ -23,6 +23,9 @@ import { fitIngestQueryPattern, ingestQueryPatternToQuery } 
from './ingest-query
 describe('ingest-query-pattern', () => {
   it('works with no group by', () => {
     const query = SqlQuery.parse(sane`
+      SET arrayIngestMode = 'array';
+      SET finalizeAggregations = FALSE;
+      SET groupByEnableMultiValueUnnesting = FALSE;
       INSERT INTO "kttm-2019"
       WITH "ext" AS (
         SELECT *
@@ -48,6 +51,7 @@ describe('ingest-query-pattern', () => {
 
     expect(insertQueryPattern).toMatchInlineSnapshot(`
       {
+        "arrayIngestMode": "array",
         "clusteredBy": [
           3,
         ],
@@ -60,6 +64,7 @@ describe('ingest-query-pattern', () => {
           "browser_version",
         ],
         "filters": [],
+        "forceSegmentSortByTime": true,
         "mainExternalConfig": {
           "inputFormat": {
             "type": "json",
@@ -117,6 +122,9 @@ describe('ingest-query-pattern', () => {
 
   it('works with group by', () => {
     const query = SqlQuery.parse(sane`
+      SET arrayIngestMode = 'array';
+      SET finalizeAggregations = FALSE;
+      SET groupByEnableMultiValueUnnesting = FALSE;
       REPLACE INTO "inline_data" OVERWRITE ALL
       WITH "ext" AS (
         SELECT *
@@ -143,6 +151,7 @@ describe('ingest-query-pattern', () => {
 
     expect(insertQueryPattern).toMatchInlineSnapshot(`
       {
+        "arrayIngestMode": "array",
         "clusteredBy": [],
         "destinationTableName": "inline_data",
         "dimensions": [
@@ -153,6 +162,7 @@ describe('ingest-query-pattern', () => {
           ""floats"",
         ],
         "filters": [],
+        "forceSegmentSortByTime": true,
         "mainExternalConfig": {
           "inputFormat": {
             "type": "json",
diff --git 
a/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.ts 
b/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.ts
index d2009ad2e5e..00e4f7c608c 100644
--- a/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.ts
+++ b/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.ts
@@ -29,13 +29,13 @@ import {
 } from 'druid-query-toolkit';
 
 import { filterMap, oneOf } from '../../utils';
+import type { ArrayIngestMode } from '../array-ingest-mode/array-ingest-mode';
 import type { ExternalConfig } from '../external-config/external-config';
 import {
   externalConfigToInitDimensions,
   externalConfigToTableExpression,
   fitExternalConfigPattern,
 } from '../external-config/external-config';
-import type { ArrayMode } from '../ingestion-spec/ingestion-spec';
 import { guessDataSourceNameFromInputSource } from 
'../ingestion-spec/ingestion-spec';
 
 export type IngestMode = 'insert' | 'replace';
@@ -50,6 +50,7 @@ function removeTopLevelArrayToMvOrUndefined(dimension: 
SqlExpression): SqlExpres
 export interface IngestQueryPattern {
   destinationTableName: string;
   mode: IngestMode;
+  arrayIngestMode: ArrayIngestMode;
   overwriteWhere?: SqlExpression;
   mainExternalName: string;
   mainExternalConfig: ExternalConfig;
@@ -58,23 +59,26 @@ export interface IngestQueryPattern {
   metrics?: readonly SqlExpression[];
   partitionedBy: string;
   clusteredBy: readonly number[];
+  forceSegmentSortByTime: boolean;
 }
 
 export function externalConfigToIngestQueryPattern(
   config: ExternalConfig,
   timeExpression: SqlExpression | undefined,
   partitionedByHint: string | undefined,
-  arrayMode: ArrayMode,
+  arrayIngestMode: ArrayIngestMode,
 ): IngestQueryPattern {
   return {
     destinationTableName: 
guessDataSourceNameFromInputSource(config.inputSource) || 'data',
     mode: 'replace',
+    arrayIngestMode,
     mainExternalName: 'ext',
     mainExternalConfig: config,
     filters: [],
-    dimensions: externalConfigToInitDimensions(config, timeExpression, 
arrayMode),
+    dimensions: externalConfigToInitDimensions(config, timeExpression, 
arrayIngestMode),
     partitionedBy: partitionedByHint || (timeExpression ? 'day' : 'all'),
     clusteredBy: [],
+    forceSegmentSortByTime: true,
   };
 }
 
@@ -147,6 +151,10 @@ export function fitIngestQueryPattern(query: SqlQuery): 
IngestQueryPattern {
     );
   }
 
+  const queryContext = query.getContext();
+  const arrayIngestMode = queryContext['arrayIngestMode'] ?? 'array';
+  const forceSegmentSortByTime = queryContext['forceSegmentSortByTime'] ?? 
true;
+
   let destinationTableName: string;
   let mode: IngestMode;
   let overwriteWhere: SqlExpression | undefined;
@@ -229,6 +237,7 @@ export function fitIngestQueryPattern(query: SqlQuery): 
IngestQueryPattern {
   return {
     destinationTableName,
     mode,
+    arrayIngestMode,
     overwriteWhere,
     mainExternalName,
     mainExternalConfig,
@@ -237,6 +246,7 @@ export function fitIngestQueryPattern(query: SqlQuery): 
IngestQueryPattern {
     metrics,
     partitionedBy,
     clusteredBy,
+    forceSegmentSortByTime,
   };
 }
 
@@ -248,6 +258,7 @@ export function ingestQueryPatternToQuery(
   const {
     destinationTableName,
     mode,
+    arrayIngestMode,
     overwriteWhere,
     mainExternalName,
     mainExternalConfig,
@@ -256,8 +267,21 @@ export function ingestQueryPatternToQuery(
     metrics,
     partitionedBy,
     clusteredBy,
+    forceSegmentSortByTime,
   } = ingestQueryPattern;
+
+  const queryContext: Record<string, any> = {
+    arrayIngestMode,
+    finalizeAggregations: false,
+    groupByEnableMultiValueUnnesting: false,
+  };
+
+  if (!forceSegmentSortByTime) {
+    queryContext.forceSegmentSortByTime = forceSegmentSortByTime;
+  }
+
   return SqlQuery.from(T(mainExternalName))
+    .changeContext(queryContext)
     .applyIf(!preview, q =>
       mode === 'insert'
         ? q.changeInsertIntoTable(destinationTableName)
diff --git a/web-console/src/druid-models/ingestion-spec/ingestion-spec.spec.ts 
b/web-console/src/druid-models/ingestion-spec/ingestion-spec.spec.ts
index 1371001e980..5b4a43aaaa3 100644
--- a/web-console/src/druid-models/ingestion-spec/ingestion-spec.spec.ts
+++ b/web-console/src/druid-models/ingestion-spec/ingestion-spec.spec.ts
@@ -864,7 +864,7 @@ describe('spec utils', () => {
         JSON_SAMPLE,
         false,
         'fixed',
-        'arrays',
+        'array',
         true,
       );
       expect(updateSpec.spec).toMatchInlineSnapshot(`
@@ -947,7 +947,7 @@ describe('spec utils', () => {
         JSON_SAMPLE,
         DEFAULT_FORCE_SEGMENT_SORT_BY_TIME,
         'fixed',
-        'arrays',
+        'array',
         true,
       );
       expect(updateSpec.spec).toMatchInlineSnapshot(`
@@ -1025,7 +1025,7 @@ describe('spec utils', () => {
         JSON_SAMPLE,
         DEFAULT_FORCE_SEGMENT_SORT_BY_TIME,
         'fixed',
-        'multi-values',
+        'mvd',
         true,
       );
       expect(updateSpec.spec).toMatchInlineSnapshot(`
@@ -1103,7 +1103,7 @@ describe('spec utils', () => {
         JSON_SAMPLE,
         DEFAULT_FORCE_SEGMENT_SORT_BY_TIME,
         'fixed',
-        'arrays',
+        'array',
         false,
       );
       expect(updatedSpec.spec).toMatchInlineSnapshot(`
@@ -1172,7 +1172,7 @@ describe('spec utils', () => {
         JSON_SAMPLE,
         DEFAULT_FORCE_SEGMENT_SORT_BY_TIME,
         'fixed',
-        'multi-values',
+        'mvd',
         false,
       );
       expect(updatedSpec.spec).toMatchInlineSnapshot(`
diff --git a/web-console/src/druid-models/ingestion-spec/ingestion-spec.tsx 
b/web-console/src/druid-models/ingestion-spec/ingestion-spec.tsx
index f0f0e7e281f..0100cc34c04 100644
--- a/web-console/src/druid-models/ingestion-spec/ingestion-spec.tsx
+++ b/web-console/src/druid-models/ingestion-spec/ingestion-spec.tsx
@@ -43,6 +43,8 @@ import {
   typeIsKnown,
 } from '../../utils';
 import type { SampleResponse } from '../../utils/sampler';
+import type { ArrayIngestMode } from '../array-ingest-mode/array-ingest-mode';
+import { DEFAULT_ARRAY_INGEST_MODE } from 
'../array-ingest-mode/array-ingest-mode';
 import type { DimensionSpec, DimensionsSpec } from 
'../dimension-spec/dimension-spec';
 import {
   getDimensionSpecColumnType,
@@ -297,11 +299,8 @@ export interface DataSchema {
 
 export type SchemaMode = 'fixed' | 'string-only-discovery' | 
'type-aware-discovery';
 
-export type ArrayMode = 'arrays' | 'multi-values';
-
 export const DEFAULT_FORCE_SEGMENT_SORT_BY_TIME = true;
 export const DEFAULT_SCHEMA_MODE: SchemaMode = 'fixed';
-export const DEFAULT_ARRAY_MODE: ArrayMode = 'arrays';
 
 export function getForceSegmentSortByTime(spec: Partial<IngestionSpec>): 
boolean {
   return (
@@ -320,17 +319,17 @@ export function getSchemaMode(spec: 
Partial<IngestionSpec>): SchemaMode {
   return Array.isArray(dimensions) && dimensions.length === 0 ? 
'string-only-discovery' : 'fixed';
 }
 
-export function getArrayMode(
+export function getArrayIngestMode(
   spec: Partial<IngestionSpec>,
-  whenUnclear: ArrayMode = 'arrays',
-): ArrayMode {
+  whenUnclear: ArrayIngestMode = DEFAULT_ARRAY_INGEST_MODE,
+): ArrayIngestMode {
   const schemaMode = getSchemaMode(spec);
   switch (schemaMode) {
     case 'type-aware-discovery':
-      return 'arrays';
+      return 'array';
 
     case 'string-only-discovery':
-      return 'multi-values';
+      return 'mvd';
 
     default: {
       const dimensions: (DimensionSpec | string)[] = deepGet(
@@ -344,7 +343,7 @@ export function getArrayMode(
             typeof d === 'object' && d.type === 'auto' && 
String(d.castToType).startsWith('ARRAY'),
         )
       ) {
-        return 'arrays';
+        return 'array';
       }
 
       if (
@@ -355,7 +354,7 @@ export function getArrayMode(
             typeof d.multiValueHandling === 'string',
         )
       ) {
-        return 'multi-values';
+        return 'mvd';
       }
 
       return whenUnclear;
@@ -363,7 +362,7 @@ export function getArrayMode(
   }
 }
 
-export function showArrayModeToggle(spec: Partial<IngestionSpec>): boolean {
+export function showArrayIngestModeToggle(spec: Partial<IngestionSpec>): 
boolean {
   const schemaMode = getSchemaMode(spec);
   if (schemaMode !== 'fixed') return false;
 
@@ -2771,7 +2770,7 @@ export function updateSchemaWithSample(
   sampleResponse: SampleResponse,
   forceSegmentSortByTime: boolean,
   schemaMode: SchemaMode,
-  arrayMode: ArrayMode,
+  arrayIngestMode: ArrayIngestMode,
   rollup: boolean,
   forcePartitionInitialization = false,
 ): Partial<IngestionSpec> {
@@ -2817,7 +2816,7 @@ export function updateSchemaWithSample(
           sampleResponse,
           columnTypeHints,
           guessNumericStringsAsNumbers,
-          arrayMode === 'multi-values',
+          arrayIngestMode,
           rollup,
           forceSegmentSortByTime ?? DEFAULT_FORCE_SEGMENT_SORT_BY_TIME ? 
'ignore' : 'preserve',
         ),
diff --git a/web-console/src/druid-models/query-context/query-context.tsx 
b/web-console/src/druid-models/query-context/query-context.tsx
index b702c233457..ae6ffb27e1c 100644
--- a/web-console/src/druid-models/query-context/query-context.tsx
+++ b/web-console/src/druid-models/query-context/query-context.tsx
@@ -16,8 +16,9 @@
  * limitations under the License.
  */
 
+import type { ArrayIngestMode } from '../array-ingest-mode/array-ingest-mode';
+
 export type SelectDestination = 'taskReport' | 'durableStorage';
-export type ArrayIngestMode = 'array' | 'mvd';
 export type TaskAssignment = 'auto' | 'max';
 export type SqlJoinAlgorithm = 'broadcast' | 'sortMerge';
 
diff --git a/web-console/src/druid-models/workbench-query/workbench-query.ts 
b/web-console/src/druid-models/workbench-query/workbench-query.ts
index 6ac6bba3c2a..44ec5e70f0a 100644
--- a/web-console/src/druid-models/workbench-query/workbench-query.ts
+++ b/web-console/src/druid-models/workbench-query/workbench-query.ts
@@ -30,6 +30,7 @@ import {
   SqlOrderByClause,
   SqlOrderByExpression,
   SqlQuery,
+  SqlSetStatement,
 } from 'druid-query-toolkit';
 import Hjson from 'hjson';
 import * as JSONBig from 'json-bigint-native';
@@ -37,6 +38,7 @@ import { v4 as uuidv4 } from 'uuid';
 
 import type { RowColumn } from '../../utils';
 import { caseInsensitiveEquals, deleteKeys } from '../../utils';
+import type { ArrayIngestMode } from '../array-ingest-mode/array-ingest-mode';
 import type { DruidEngine } from '../druid-engine/druid-engine';
 import { validDruidEngine } from '../druid-engine/druid-engine';
 import type { LastExecution } from '../execution/execution';
@@ -46,7 +48,6 @@ import {
   externalConfigToIngestQueryPattern,
   ingestQueryPatternToQuery,
 } from '../ingest-query-pattern/ingest-query-pattern';
-import type { ArrayMode } from '../ingestion-spec/ingestion-spec';
 import type { QueryContext } from '../query-context/query-context';
 
 const ISSUE_MARKER = '--:ISSUE:';
@@ -93,10 +94,8 @@ export class WorkbenchQuery {
     externalConfig: ExternalConfig,
     timeExpression: SqlExpression | undefined,
     partitionedByHint: string | undefined,
-    arrayMode: ArrayMode,
+    arrayMode: ArrayIngestMode,
   ): WorkbenchQuery {
-    const queryContext: QueryContext = {};
-    if (arrayMode === 'arrays') queryContext.arrayIngestMode = 'array';
     return new WorkbenchQuery({
       queryString: ingestQueryPatternToQuery(
         externalConfigToIngestQueryPattern(
@@ -106,7 +105,7 @@ export class WorkbenchQuery {
           arrayMode,
         ),
       ).toString(),
-      queryContext,
+      queryContext: {},
     });
   }
 
@@ -286,6 +285,16 @@ export class WorkbenchQuery {
     return new WorkbenchQuery({ ...this.valueOf(), queryString });
   }
 
+  public changeQueryStringContext(queryContext: QueryContext): WorkbenchQuery {
+    const { queryString } = this;
+    return 
this.changeQueryString(SqlSetStatement.setContextInText(queryString, 
queryContext));
+  }
+
+  public getQueryStringContext(): QueryContext {
+    const { queryString } = this;
+    return SqlSetStatement.getContextFromText(queryString);
+  }
+
   public changeQueryContext(queryContext: QueryContext): WorkbenchQuery {
     return new WorkbenchQuery({ ...this.valueOf(), queryContext });
   }
diff --git a/web-console/src/helpers/__snapshots__/spec-conversion.spec.ts.snap 
b/web-console/src/helpers/__snapshots__/spec-conversion.spec.ts.snap
index 11f2a388ad2..235e36da062 100644
--- a/web-console/src/helpers/__snapshots__/spec-conversion.spec.ts.snap
+++ b/web-console/src/helpers/__snapshots__/spec-conversion.spec.ts.snap
@@ -2,6 +2,9 @@
 
 exports[`spec conversion converts index_hadoop spec (with rollup) 1`] = `
 -- This SQL query was auto generated from an ingestion spec
+SET arrayIngestMode = 'array';
+SET finalizeAggregations = FALSE;
+SET groupByEnableMultiValueUnnesting = FALSE;
 REPLACE INTO "newSource" OVERWRITE ALL
 WITH "source" AS (SELECT * FROM TABLE(
   EXTERN(
@@ -30,6 +33,9 @@ PARTITIONED BY HOUR
 
 exports[`spec conversion converts index_parallel spec (with rollup) 1`] = `
 -- This SQL query was auto generated from an ingestion spec
+SET arrayIngestMode = 'array';
+SET finalizeAggregations = FALSE;
+SET groupByEnableMultiValueUnnesting = FALSE;
 REPLACE INTO "wikipedia_rollup" OVERWRITE ALL
 WITH "source" AS (SELECT * FROM TABLE(
   EXTERN(
@@ -70,6 +76,11 @@ PARTITIONED BY HOUR
 
 exports[`spec conversion converts index_parallel spec (without rollup) 1`] = `
 -- This SQL query was auto generated from an ingestion spec
+SET arrayIngestMode = 'array';
+SET maxNumTasks = 5;
+SET maxParseExceptions = 3;
+SET finalizeAggregations = FALSE;
+SET groupByEnableMultiValueUnnesting = FALSE;
 REPLACE INTO "wikipedia" OVERWRITE ALL
 WITH "source" AS (SELECT * FROM TABLE(
   EXTERN(
@@ -111,6 +122,11 @@ CLUSTERED BY "isRobot"
 
 exports[`spec conversion converts with issue when there is a __time transform 
1`] = `
 -- This SQL query was auto generated from an ingestion spec
+SET arrayIngestMode = 'array';
+SET maxNumTasks = 5;
+SET maxParseExceptions = 3;
+SET finalizeAggregations = FALSE;
+SET groupByEnableMultiValueUnnesting = FALSE;
 REPLACE INTO "wikipedia" OVERWRITE ALL
 WITH "source" AS (SELECT * FROM TABLE(
   EXTERN(
@@ -151,6 +167,11 @@ CLUSTERED BY "isRobot"
 
 exports[`spec conversion converts with issue when there is a dimension 
transform and strange filter 1`] = `
 -- This SQL query was auto generated from an ingestion spec
+SET arrayIngestMode = 'array';
+SET maxNumTasks = 5;
+SET maxParseExceptions = 3;
+SET finalizeAggregations = FALSE;
+SET groupByEnableMultiValueUnnesting = FALSE;
 REPLACE INTO "wikipedia" OVERWRITE ALL
 WITH "source" AS (SELECT * FROM TABLE(
   EXTERN(
@@ -192,6 +213,11 @@ CLUSTERED BY "isRobot"
 
 exports[`spec conversion converts with when the __time column is used as the 
__time column 1`] = `
 -- This SQL query was auto generated from an ingestion spec
+SET arrayIngestMode = 'array';
+SET maxNumTasks = 5;
+SET maxParseExceptions = 3;
+SET finalizeAggregations = FALSE;
+SET groupByEnableMultiValueUnnesting = FALSE;
 REPLACE INTO "wikipedia" OVERWRITE ALL
 WITH "source" AS (SELECT * FROM TABLE(
   EXTERN(
@@ -211,6 +237,9 @@ CLUSTERED BY "isRobot"
 
 exports[`spec conversion works with ARRAY mode 1`] = `
 -- This SQL query was auto generated from an ingestion spec
+SET arrayIngestMode = 'array';
+SET finalizeAggregations = FALSE;
+SET groupByEnableMultiValueUnnesting = FALSE;
 REPLACE INTO "lol" OVERWRITE ALL
 WITH "source" AS (SELECT * FROM TABLE(
   EXTERN(
diff --git a/web-console/src/helpers/spec-conversion.spec.ts 
b/web-console/src/helpers/spec-conversion.spec.ts
index 9271196dafa..32580a4b487 100644
--- a/web-console/src/helpers/spec-conversion.spec.ts
+++ b/web-console/src/helpers/spec-conversion.spec.ts
@@ -123,8 +123,6 @@ describe('spec conversion', () => {
     expect(converted.queryString).toMatchSnapshot();
 
     expect(converted.queryContext).toEqual({
-      maxParseExceptions: 3,
-      maxNumTasks: 5,
       indexSpec: {
         dimensionCompression: 'lzf',
       },
@@ -653,8 +651,6 @@ describe('spec conversion', () => {
 
     expect(converted.queryString).toMatchSnapshot();
 
-    expect(converted.queryContext).toEqual({
-      arrayIngestMode: 'array',
-    });
+    expect(converted.queryContext).toEqual({});
   });
 });
diff --git a/web-console/src/helpers/spec-conversion.ts 
b/web-console/src/helpers/spec-conversion.ts
index 9fc95d3b60f..84ccb7bcb38 100644
--- a/web-console/src/helpers/spec-conversion.ts
+++ b/web-console/src/helpers/spec-conversion.ts
@@ -38,7 +38,8 @@ import type {
   Transform,
 } from '../druid-models';
 import {
-  getArrayMode,
+  DEFAULT_ARRAY_INGEST_MODE,
+  getArrayIngestMode,
   inflateDimensionSpec,
   NO_SUCH_COLUMN,
   TIME_COLUMN,
@@ -82,24 +83,32 @@ export function convertSpecToSql(spec: any): 
QueryWithContext {
 
   const context: QueryContext = {};
 
-  if (getArrayMode(spec, 'multi-values') === 'arrays') {
-    context.arrayIngestMode = 'array';
+  const indexSpec = deepGet(spec, 'spec.tuningConfig.indexSpec');
+  if (indexSpec) {
+    context.indexSpec = indexSpec;
   }
 
+  const lines: string[] = [`-- This SQL query was auto generated from an 
ingestion spec`];
+
+  lines.push(`SET arrayIngestMode = ${L(getArrayIngestMode(spec, 
DEFAULT_ARRAY_INGEST_MODE))};`);
+
   const forceSegmentSortByTime = deepGet(
     spec,
     'spec.dataSchema.dimensionsSpec.forceSegmentSortByTime',
   );
   if (typeof forceSegmentSortByTime !== 'undefined') {
-    context.forceSegmentSortByTime = forceSegmentSortByTime;
+    lines.push(`SET forceSegmentSortByTime = ${L(forceSegmentSortByTime)};`);
   }
 
-  const indexSpec = deepGet(spec, 'spec.tuningConfig.indexSpec');
-  if (indexSpec) {
-    context.indexSpec = indexSpec;
+  const maxNumConcurrentSubTasks = deepGet(spec, 
'spec.tuningConfig.maxNumConcurrentSubTasks');
+  if (maxNumConcurrentSubTasks > 1) {
+    lines.push(`SET maxNumTasks = ${maxNumConcurrentSubTasks + 1};`);
   }
 
-  const lines: string[] = [];
+  const maxParseExceptions = deepGet(spec, 
'spec.tuningConfig.maxParseExceptions');
+  if (typeof maxParseExceptions === 'number') {
+    lines.push(`SET maxParseExceptions = ${maxParseExceptions};`);
+  }
 
   const rollup = deepGet(spec, 'spec.dataSchema.granularitySpec.rollup') ?? 
true;
 
@@ -212,17 +221,7 @@ export function convertSpecToSql(spec: any): 
QueryWithContext {
     );
   }
 
-  lines.push(`-- This SQL query was auto generated from an ingestion spec`);
-
-  const maxNumConcurrentSubTasks = deepGet(spec, 
'spec.tuningConfig.maxNumConcurrentSubTasks');
-  if (maxNumConcurrentSubTasks > 1) {
-    context.maxNumTasks = maxNumConcurrentSubTasks + 1;
-  }
-
-  const maxParseExceptions = deepGet(spec, 
'spec.tuningConfig.maxParseExceptions');
-  if (typeof maxParseExceptions === 'number') {
-    context.maxParseExceptions = maxParseExceptions;
-  }
+  lines.push(`SET finalizeAggregations = FALSE;`, `SET 
groupByEnableMultiValueUnnesting = FALSE;`);
 
   const dataSource = deepGet(spec, 'spec.dataSchema.dataSource');
   if (typeof dataSource !== 'string') throw new 
Error(`spec.dataSchema.dataSource is not a string`);
diff --git a/web-console/src/utils/druid-query.spec.ts 
b/web-console/src/utils/druid-query.spec.ts
index a940b8ac7bb..bf0f6558f1b 100644
--- a/web-console/src/utils/druid-query.spec.ts
+++ b/web-console/src/utils/druid-query.spec.ts
@@ -214,15 +214,13 @@ describe('DruidQuery', () => {
       );
     });
 
-    it('removes trailing semicolon (;)', () => {
-      const sql = `SELECT page FROM wikipedia WHERE channel = 
'#ar.wikipedia';`;
+    it('adds missing semicolon (;)', () => {
+      const sql = `SET x = 1 SELECT * FROM wikipedia`;
       const suggestion = DruidError.getSuggestion(
-        `Received an unexpected token [;] (line [1], column [59]), acceptable 
options:`,
-      );
-      expect(suggestion!.label).toEqual(`Remove trailing semicolon (;)`);
-      expect(suggestion!.fn(sql)).toEqual(
-        `SELECT page FROM wikipedia WHERE channel = '#ar.wikipedia'`,
+        `Received an unexpected token [SELECT] (line [1], column [11]), 
acceptable options: [<EOF>, ";"]`,
       );
+      expect(suggestion!.label).toEqual(`Add semicolon (;) after SET 
statement`);
+      expect(suggestion!.fn(sql)).toEqual(`SET x = 1; SELECT * FROM 
wikipedia`);
     });
 
     it('does nothing there there is nothing to do', () => {
diff --git a/web-console/src/utils/druid-query.ts 
b/web-console/src/utils/druid-query.ts
index d83cf5eb9b6..72813889034 100644
--- a/web-console/src/utils/druid-query.ts
+++ b/web-console/src/utils/druid-query.ts
@@ -236,20 +236,23 @@ export class DruidError extends Error {
       };
     }
 
-    // Semicolon (;) at the end. https://bit.ly/1n1yfkJ
-    // ex: SELECT 1;
-    // ex: Received an unexpected token [;] (line [1], column [9]), acceptable 
options:
+    // Missing (;) after SET statement
+    // ex: SET sqlTimeZone = 'America/Los_Angeles' SELECT * FROM "kttm_simple"
+    // ex: Received an unexpected token [SELECT] (line [1], column [41]), 
acceptable options: [<EOF>, <QUOTED_STRING>, ";", "UESCAPE"]
     const matchSemicolon =
-      /Received an unexpected token \[;] \(line \[(\d+)], column 
\[(\d+)]\),/i.exec(errorMessage);
+      /Received an unexpected token \[(?:SET|SELECT)] \(line \[(\d+)], column 
\[(\d+)]\), acceptable options: \[[^;]*";"/i.exec(
+        errorMessage,
+      );
     if (matchSemicolon) {
       const line = Number(matchSemicolon[1]);
       const column = Number(matchSemicolon[2]);
       return {
-        label: `Remove trailing semicolon (;)`,
+        label: `Add semicolon (;) after SET statement`,
         fn: str => {
           const index = DruidError.positionToIndex(str, line, column);
-          if (str[index] !== ';') return;
-          return str.slice(0, index) + str.slice(index + 1);
+          const prefix = str.slice(0, index).trimEnd();
+          if (prefix.endsWith(';')) return;
+          return prefix + ';' + str.slice(prefix.length);
         },
       };
     }
diff --git a/web-console/src/utils/explain.spec.ts 
b/web-console/src/utils/explain.spec.ts
new file mode 100644
index 00000000000..f23a412c10d
--- /dev/null
+++ b/web-console/src/utils/explain.spec.ts
@@ -0,0 +1,145 @@
+/*
+ * 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.
+ */
+
+import { sane } from 'druid-query-toolkit';
+
+import {
+  wrapInExplainAsParsedIfNeeded,
+  wrapInExplainAsStringIfNeeded,
+  wrapInExplainIfNeeded,
+} from './explain';
+
+describe('explain utils', () => {
+  describe('wrapInExplain*', () => {
+    const queries: [string, string][] = [
+      [
+        sane`
+          SELECT *
+          FROM (
+            select * from wikipedia
+          )
+        `,
+        sane`
+          EXPLAIN PLAN FOR
+          SELECT *
+          FROM (
+            select * from wikipedia
+          )
+        `,
+      ],
+      [
+        sane`
+          WITH "wikipedia" AS (select * from wikipedia2)
+          SELECT *
+          FROM (
+            select * from wikipedia
+          )
+        `,
+        sane`
+          EXPLAIN PLAN FOR
+          WITH "wikipedia" AS (select * from wikipedia2)
+          SELECT *
+          FROM (
+            select * from wikipedia
+          )
+        `,
+      ],
+      [
+        sane`
+          INSERT INTO "table"
+          SELECT *
+          FROM (
+            select * from wikipedia
+          )
+        `,
+        sane`
+          EXPLAIN PLAN FOR
+          INSERT INTO "table"
+          SELECT *
+          FROM (
+            select * from wikipedia
+          )
+        `,
+      ],
+      [
+        sane`
+          REPLACE INTO "table" OVERWRITE ALL
+          SELECT *
+          FROM (
+            select * from wikipedia
+          )
+        `,
+        sane`
+          EXPLAIN PLAN FOR
+          REPLACE INTO "table" OVERWRITE ALL
+          SELECT *
+          FROM (
+            select * from wikipedia
+          )
+        `,
+      ],
+      [
+        sane`
+          REPLACE INTO "table" OVERWRITE ALL
+          WITH "wikipedia" AS (select * from wikipedia2)
+          SELECT *
+          FROM (
+            select * from wikipedia
+          )
+        `,
+        sane`
+          EXPLAIN PLAN FOR
+          REPLACE INTO "table" OVERWRITE ALL
+          WITH "wikipedia" AS (select * from wikipedia2)
+          SELECT *
+          FROM (
+            select * from wikipedia
+          )
+        `,
+      ],
+    ];
+
+    const sets = "-- A select comment\nSET a = 1;\nset b = 'EXPLAIN PLAN 
FOR';\n";
+
+    it('works with queries', () => {
+      for (const [before, after] of queries) {
+        expect(wrapInExplainIfNeeded(before)).toEqual(after);
+        expect(wrapInExplainAsParsedIfNeeded(before)).toEqual(after);
+        expect(wrapInExplainAsStringIfNeeded(before)).toEqual(after);
+
+        const beforeWithSets = sets + before;
+        const afterWithSets = sets + after;
+        expect(wrapInExplainIfNeeded(beforeWithSets)).toEqual(afterWithSets);
+        
expect(wrapInExplainAsParsedIfNeeded(beforeWithSets)).toEqual(afterWithSets);
+        
expect(wrapInExplainAsStringIfNeeded(beforeWithSets)).toEqual(afterWithSets);
+
+        const beforeUnparsable = before + '~';
+        const afterUnparsable = after + '~';
+        
expect(wrapInExplainIfNeeded(beforeUnparsable)).toEqual(afterUnparsable);
+        
expect(wrapInExplainAsStringIfNeeded(beforeUnparsable)).toEqual(afterUnparsable);
+
+        const beforeUnparsableWithSets = beforeWithSets + '~';
+        const afterUnparsableWithSets = afterWithSets + '~';
+        
expect(wrapInExplainIfNeeded(beforeUnparsableWithSets)).toEqual(afterUnparsableWithSets);
+        
expect(wrapInExplainAsStringIfNeeded(beforeUnparsableWithSets)).toEqual(
+          afterUnparsableWithSets,
+        );
+      }
+    });
+  });
+});
diff --git 
a/web-console/src/components/array-mode-switch/array-mode-swtich.spec.tsx 
b/web-console/src/utils/explain.ts
similarity index 50%
rename from 
web-console/src/components/array-mode-switch/array-mode-swtich.spec.tsx
rename to web-console/src/utils/explain.ts
index a9ac2320c2d..bb98409c35b 100644
--- a/web-console/src/components/array-mode-switch/array-mode-swtich.spec.tsx
+++ b/web-console/src/utils/explain.ts
@@ -16,15 +16,26 @@
  * limitations under the License.
  */
 
-import { render } from '@testing-library/react';
+import { SqlQuery, SqlSetStatement } from 'druid-query-toolkit';
 
-import { ArrayModeSwitch } from './array-mode-switch';
+export function wrapInExplainIfNeeded(query: string): string {
+  try {
+    return wrapInExplainAsParsedIfNeeded(query);
+  } catch {
+    return wrapInExplainAsStringIfNeeded(query);
+  }
+}
 
-describe('ArrayModeSwitch', () => {
-  it('matches snapshot', () => {
-    const arrayInput = <ArrayModeSwitch arrayMode="multi-values" 
changeArrayMode={() => {}} />;
+export function wrapInExplainAsParsedIfNeeded(query: string): string {
+  const parsed = SqlQuery.parse(query);
+  if (parsed.explain) return query;
+  return parsed.makeExplain().toString();
+}
 
-    const { container } = render(arrayInput);
-    expect(container.firstChild).toMatchSnapshot();
-  });
-});
+export function wrapInExplainAsStringIfNeeded(query: string): string {
+  const [setPart, queryPart] = SqlSetStatement.partitionSetStatements(query, 
true);
+  if (/^\s*EXPLAIN\sPLAN\sFOR/im.test(queryPart)) return query;
+
+  // Only replace the first occurrence
+  return setPart + queryPart.replace(/REPLACE|INSERT|SELECT|WITH/i, 'EXPLAIN 
PLAN FOR\n$&');
+}
diff --git a/web-console/src/utils/index.tsx b/web-console/src/utils/index.tsx
index 760e79b6f8d..a4dee7a62fc 100644
--- a/web-console/src/utils/index.tsx
+++ b/web-console/src/utils/index.tsx
@@ -25,6 +25,7 @@ export * from './download';
 export * from './download-query-detail-archive';
 export * from './druid-lookup';
 export * from './druid-query';
+export * from './explain';
 export * from './formatter';
 export * from './general';
 export * from './local-storage-backed-visibility';
diff --git a/web-console/src/utils/sql.spec.ts 
b/web-console/src/utils/sql.spec.ts
index 57e7bce95fe..85d01816409 100644
--- a/web-console/src/utils/sql.spec.ts
+++ b/web-console/src/utils/sql.spec.ts
@@ -619,5 +619,225 @@ describe('sql', () => {
         ]
       `);
     });
+
+    it('works with SET statements', () => {
+      const text = sane`
+        SET timeout = 100;
+        SET timeout = 50;
+        SELECT * FROM wikipedia
+      `;
+
+      const found = findAllSqlQueriesInText(text);
+
+      expect(found).toMatchInlineSnapshot(`
+        [
+          {
+            "endOffset": 60,
+            "endRowColumn": {
+              "column": 23,
+              "row": 2,
+            },
+            "index": 0,
+            "sql": "SET timeout = 100;
+        SET timeout = 50;
+        SELECT * FROM wikipedia",
+            "startOffset": 0,
+            "startRowColumn": {
+              "column": 0,
+              "row": 0,
+            },
+          },
+        ]
+      `);
+    });
+
+    it('works with multiple SET statement queries', () => {
+      const text = sane`
+        SET timeout = 100;
+        SELECT * FROM wikipedia
+
+
+        SET timeout = 50;
+        SET sqlTimeZone = 'Etc/UTC';
+        SELECT * FROM wikipedia
+      `;
+
+      const found = findAllSqlQueriesInText(text);
+
+      expect(found).toMatchInlineSnapshot(`
+        [
+          {
+            "endOffset": 42,
+            "endRowColumn": {
+              "column": 23,
+              "row": 1,
+            },
+            "index": 0,
+            "sql": "SET timeout = 100;
+        SELECT * FROM wikipedia",
+            "startOffset": 0,
+            "startRowColumn": {
+              "column": 0,
+              "row": 0,
+            },
+          },
+          {
+            "endOffset": 115,
+            "endRowColumn": {
+              "column": 23,
+              "row": 6,
+            },
+            "index": 1,
+            "sql": "SET timeout = 50;
+        SET sqlTimeZone = 'Etc/UTC';
+        SELECT * FROM wikipedia",
+            "startOffset": 45,
+            "startRowColumn": {
+              "column": 0,
+              "row": 4,
+            },
+          },
+        ]
+      `);
+    });
+
+    it('test', () => {
+      const text = sane`
+        SET finalizeAggregations = FALSE;
+        SET groupByEnableMultiValueUnnesting = FALSE;
+        REPLACE INTO "kttm-v2-2019-08-25" OVERWRITE ALL
+        SELECT
+          TIME_PARSE("timestamp") AS "__time",
+          "agent_category",
+          "agent_type",
+          "browser",
+          "browser_version",
+          "city",
+          "continent",
+          "country",
+          "version",
+          "event_type",
+          "event_subtype",
+          "loaded_image",
+          "adblock_list",
+          "forwarded_for",
+          ARRAY_TO_MV("language") AS "language",
+          "number",
+          "os",
+          "path",
+          "platform",
+          "referrer",
+          "referrer_host",
+          "region",
+          "remote_address",
+          "screen",
+          "session",
+          "session_length",
+          "timezone",
+          "timezone_offset",
+          "window"
+        FROM "ext"
+        PARTITIONED BY DAY
+      `;
+
+      const found = findAllSqlQueriesInText(text);
+
+      expect(found).toMatchInlineSnapshot(`
+        [
+          {
+            "endOffset": 655,
+            "endRowColumn": {
+              "column": 18,
+              "row": 34,
+            },
+            "index": 0,
+            "sql": "SET finalizeAggregations = FALSE;
+        SET groupByEnableMultiValueUnnesting = FALSE;
+        REPLACE INTO "kttm-v2-2019-08-25" OVERWRITE ALL
+        SELECT
+          TIME_PARSE("timestamp") AS "__time",
+          "agent_category",
+          "agent_type",
+          "browser",
+          "browser_version",
+          "city",
+          "continent",
+          "country",
+          "version",
+          "event_type",
+          "event_subtype",
+          "loaded_image",
+          "adblock_list",
+          "forwarded_for",
+          ARRAY_TO_MV("language") AS "language",
+          "number",
+          "os",
+          "path",
+          "platform",
+          "referrer",
+          "referrer_host",
+          "region",
+          "remote_address",
+          "screen",
+          "session",
+          "session_length",
+          "timezone",
+          "timezone_offset",
+          "window"
+        FROM "ext"
+        PARTITIONED BY DAY",
+            "startOffset": 0,
+            "startRowColumn": {
+              "column": 0,
+              "row": 0,
+            },
+          },
+          {
+            "endOffset": 636,
+            "endRowColumn": {
+              "column": 10,
+              "row": 33,
+            },
+            "index": 1,
+            "sql": "SELECT
+          TIME_PARSE("timestamp") AS "__time",
+          "agent_category",
+          "agent_type",
+          "browser",
+          "browser_version",
+          "city",
+          "continent",
+          "country",
+          "version",
+          "event_type",
+          "event_subtype",
+          "loaded_image",
+          "adblock_list",
+          "forwarded_for",
+          ARRAY_TO_MV("language") AS "language",
+          "number",
+          "os",
+          "path",
+          "platform",
+          "referrer",
+          "referrer_host",
+          "region",
+          "remote_address",
+          "screen",
+          "session",
+          "session_length",
+          "timezone",
+          "timezone_offset",
+          "window"
+        FROM "ext"",
+            "startOffset": 128,
+            "startRowColumn": {
+              "column": 0,
+              "row": 3,
+            },
+          },
+        ]
+      `);
+    });
   });
 });
diff --git a/web-console/src/utils/sql.ts b/web-console/src/utils/sql.ts
index 6c168002078..c7768dc62a9 100644
--- a/web-console/src/utils/sql.ts
+++ b/web-console/src/utils/sql.ts
@@ -23,6 +23,7 @@ import {
   SqlFunction,
   SqlLiteral,
   SqlQuery,
+  SqlSetStatement,
   SqlStar,
 } from 'druid-query-toolkit';
 
@@ -136,26 +137,44 @@ export function findAllSqlQueriesInText(text: string): 
QuerySlice[] {
   let remainingText = text;
   let offset = 0;
   let m: RegExpExecArray | null = null;
-  do {
-    m = /SELECT|WITH|INSERT|REPLACE|EXPLAIN/i.exec(remainingText);
-    if (m) {
-      const sql = findSqlQueryPrefix(remainingText.slice(m.index));
-      const advanceBy = m.index + m[0].length; // Skip the initial word
-      if (sql) {
-        const endIndex = m.index + sql.length;
-        found.push({
-          index: found.length,
-          startOffset: offset + m.index,
-          startRowColumn: offsetToRowColumn(text, offset + m.index)!,
-          endOffset: offset + endIndex,
-          endRowColumn: offsetToRowColumn(text, offset + endIndex)!,
-          sql: cleanSqlQueryPrefix(sql),
-        });
+  // Have an upper bound to how many queries we might extract from a long text
+  for (let i = 0; i < 1e3; i++) {
+    m = /SET|SELECT|WITH|INSERT|REPLACE|EXPLAIN/i.exec(remainingText);
+    if (!m) break;
+
+    const sql = findSqlQueryPrefix(remainingText.slice(m.index));
+    if (sql) {
+      const endIndex = m.index + sql.length;
+      found.push({
+        index: found.length,
+        startOffset: offset + m.index,
+        startRowColumn: offsetToRowColumn(text, offset + m.index)!,
+        endOffset: offset + endIndex,
+        endRowColumn: offsetToRowColumn(text, offset + endIndex)!,
+        sql: cleanSqlQueryPrefix(sql),
+      });
+    }
+
+    let advanceBy = m.index;
+    const initialWord = m[0].toUpperCase();
+    if (sql && initialWord === 'SET') {
+      const startOfSetStatements = remainingText.slice(m.index);
+      const [setPart, queryPart] = SqlSetStatement.partitionSetStatements(
+        startOfSetStatements,
+        true,
+      );
+      advanceBy += setPart.length;
+      const nextWord = /SELECT|WITH|INSERT|REPLACE/i.exec(queryPart);
+      if (nextWord) {
+        advanceBy += nextWord[0].length;
       }
-      remainingText = remainingText.slice(advanceBy);
-      offset += advanceBy;
+    } else {
+      advanceBy += initialWord.length; // Skip the initial word only
     }
-  } while (m);
+
+    remainingText = remainingText.slice(advanceBy);
+    offset += advanceBy;
+  }
 
   return found;
 }
diff --git a/web-console/src/views/load-data-view/load-data-view.tsx 
b/web-console/src/views/load-data-view/load-data-view.tsx
index 74c23027aaa..0df1689a73f 100644
--- a/web-console/src/views/load-data-view/load-data-view.tsx
+++ b/web-console/src/views/load-data-view/load-data-view.tsx
@@ -46,7 +46,7 @@ import type { JSX } from 'react';
 import React from 'react';
 
 import {
-  ArrayModeSwitch,
+  ArrayIngestModeSwitch,
   AutoForm,
   CenterMessage,
   ClearableInput,
@@ -58,7 +58,7 @@ import {
 } from '../../components';
 import { AlertDialog, AsyncActionDialog, DiffDialog } from '../../dialogs';
 import type {
-  ArrayMode,
+  ArrayIngestMode,
   DimensionSpec,
   DruidFilter,
   FlattenField,
@@ -83,7 +83,7 @@ import {
   computeFlattenPathsForData,
   CONSTANT_TIMESTAMP_SPEC,
   CONSTANT_TIMESTAMP_SPEC_FIELDS,
-  DEFAULT_ARRAY_MODE,
+  DEFAULT_ARRAY_INGEST_MODE,
   DEFAULT_FORCE_SEGMENT_SORT_BY_TIME,
   DEFAULT_SCHEMA_MODE,
   DIMENSION_SPEC_FIELDS,
@@ -92,7 +92,7 @@ import {
   FILTER_FIELDS,
   FILTERS_FIELDS,
   FLATTEN_FIELD_FIELDS,
-  getArrayMode,
+  getArrayIngestMode,
   getDimensionSpecName,
   getFlattenSpec,
   getForceSegmentSortByTime,
@@ -132,7 +132,7 @@ import {
   possibleDruidFormatForValues,
   PRIMARY_PARTITION_RELATED_FORM_FIELDS,
   removeTimestampTransform,
-  showArrayModeToggle,
+  showArrayIngestModeToggle,
   splitFilter,
   STREAMING_INPUT_FORMAT_FIELDS,
   TIME_COLUMN,
@@ -323,7 +323,7 @@ function initializeSchemaWithSampleIfNeeded(
     sample,
     DEFAULT_FORCE_SEGMENT_SORT_BY_TIME,
     DEFAULT_SCHEMA_MODE,
-    DEFAULT_ARRAY_MODE,
+    DEFAULT_ARRAY_INGEST_MODE,
     getRollup(spec, false),
   );
 }
@@ -410,7 +410,7 @@ export interface LoadDataViewState {
   newRollup?: boolean;
   newForceSegmentSortByTime?: boolean;
   newSchemaMode?: SchemaMode;
-  newArrayMode?: ArrayMode;
+  newArrayIngestMode?: ArrayIngestMode;
 
   // welcome
   overlordModules?: string[];
@@ -2387,7 +2387,7 @@ export class LoadDataView extends 
React.PureComponent<LoadDataViewProps, LoadDat
     );
     const forceSegmentSortByTime = getForceSegmentSortByTime(spec);
     const schemaMode = getSchemaMode(spec);
-    const arrayMode = getArrayMode(spec);
+    const arrayMode = getArrayIngestMode(spec);
 
     let mainFill: JSX.Element | string;
     if (schemaQueryState.isInit()) {
@@ -2496,12 +2496,12 @@ export class LoadDataView extends 
React.PureComponent<LoadDataViewProps, LoadDat
                   label="Explicitly specify schema"
                 />
               </FormGroupWithInfo>
-              {showArrayModeToggle(spec) && (
-                <ArrayModeSwitch
-                  arrayMode={arrayMode}
-                  changeArrayMode={newArrayMode => {
+              {showArrayIngestModeToggle(spec) && (
+                <ArrayIngestModeSwitch
+                  arrayIngestMode={arrayMode}
+                  changeArrayIngestMode={newArrayIngestMode => {
                     this.setState({
-                      newArrayMode,
+                      newArrayIngestMode: newArrayIngestMode,
                     });
                   }}
                 />
@@ -2635,7 +2635,7 @@ export class LoadDataView extends 
React.PureComponent<LoadDataViewProps, LoadDat
           {this.renderChangeForceSegmentSortByTime()}
           {this.renderChangeRollupAction()}
           {this.renderChangeSchemaModeAction()}
-          {this.renderChangeArrayModeAction()}
+          {this.renderChangeArrayIngestModeAction()}
         </div>
         {this.renderNextBar({
           disabled: !schemaQueryState.data,
@@ -2739,7 +2739,7 @@ export class LoadDataView extends 
React.PureComponent<LoadDataViewProps, LoadDat
               sampleResponse,
               newForceSegmentSortByTime,
               getSchemaMode(spec),
-              getArrayMode(spec),
+              getArrayIngestMode(spec),
               getRollup(spec),
               true,
             ),
@@ -2777,7 +2777,7 @@ export class LoadDataView extends 
React.PureComponent<LoadDataViewProps, LoadDat
               sampleResponse,
               getForceSegmentSortByTime(spec),
               getSchemaMode(spec),
-              getArrayMode(spec),
+              getArrayIngestMode(spec),
               newRollup,
               true,
             ),
@@ -2814,7 +2814,7 @@ export class LoadDataView extends 
React.PureComponent<LoadDataViewProps, LoadDat
               sampleResponse,
               getForceSegmentSortByTime(spec),
               newSchemaMode,
-              getArrayMode(spec),
+              getArrayIngestMode(spec),
               getRollup(spec),
             ),
           );
@@ -2867,10 +2867,10 @@ export class LoadDataView extends 
React.PureComponent<LoadDataViewProps, LoadDat
     );
   }
 
-  renderChangeArrayModeAction() {
-    const { newArrayMode, spec, cacheRows } = this.state;
-    if (!newArrayMode || !cacheRows) return;
-    const multiValues = newArrayMode === 'multi-values';
+  renderChangeArrayIngestModeAction() {
+    const { newArrayIngestMode, spec, cacheRows } = this.state;
+    if (!newArrayIngestMode || !cacheRows) return;
+    const multiValues = newArrayIngestMode === 'mvd';
 
     return (
       <AsyncActionDialog
@@ -2886,7 +2886,7 @@ export class LoadDataView extends 
React.PureComponent<LoadDataViewProps, LoadDat
               sampleResponse,
               getForceSegmentSortByTime(spec),
               getSchemaMode(spec),
-              newArrayMode,
+              newArrayIngestMode,
               getRollup(spec),
             ),
           );
@@ -2895,7 +2895,7 @@ export class LoadDataView extends 
React.PureComponent<LoadDataViewProps, LoadDat
         successText={`Array mode changed to ${multiValues ? 'multi-values' : 
'arrays'}.`}
         failText="Could not change array mode"
         intent={Intent.WARNING}
-        onClose={() => this.setState({ newArrayMode: undefined })}
+        onClose={() => this.setState({ newArrayIngestMode: undefined })}
       >
         <p>
           {multiValues
diff --git 
a/web-console/src/views/sql-data-loader-view/schema-step/schema-step.tsx 
b/web-console/src/views/sql-data-loader-view/schema-step/schema-step.tsx
index f3ee9fa2adc..dd5bdd89da5 100644
--- a/web-console/src/views/sql-data-loader-view/schema-step/schema-step.tsx
+++ b/web-console/src/views/sql-data-loader-view/schema-step/schema-step.tsx
@@ -255,8 +255,6 @@ interface EditorColumn {
 export interface SchemaStepProps {
   queryString: string;
   onQueryStringChange(queryString: string): void;
-  forceSegmentSortByTime: boolean;
-  changeForceSegmentSortByTime(forceSegmentSortByTime: boolean): void;
   enableAnalyze: boolean;
   goToQuery: () => void;
   onBack(): void;
@@ -268,8 +266,6 @@ export const SchemaStep = function SchemaStep(props: 
SchemaStepProps) {
   const {
     queryString,
     onQueryStringChange,
-    forceSegmentSortByTime,
-    changeForceSegmentSortByTime,
     enableAnalyze,
     goToQuery,
     onBack,
@@ -674,7 +670,9 @@ export const SchemaStep = function SchemaStep(props: 
SchemaStepProps) {
                         <MenuItem
                           key={i}
                           text={outputName}
-                          disabled={outputName === TIME_COLUMN && 
forceSegmentSortByTime}
+                          disabled={
+                            outputName === TIME_COLUMN && 
ingestQueryPattern.forceSegmentSortByTime
+                          }
                           onClick={() =>
                             updatePattern({
                               ...ingestQueryPattern,
@@ -690,8 +688,10 @@ export const SchemaStep = function SchemaStep(props: 
SchemaStepProps) {
                   <MenuBoolean
                     icon={IconNames.GEOTIME}
                     text="Force segment sort by time"
-                    value={forceSegmentSortByTime}
-                    onValueChange={v => 
changeForceSegmentSortByTime(Boolean(v))}
+                    value={ingestQueryPattern.forceSegmentSortByTime}
+                    onValueChange={v =>
+                      updatePattern({ ...ingestQueryPattern, 
forceSegmentSortByTime: Boolean(v) })
+                    }
                     optionsText={ENABLED_DISABLED_OPTIONS_TEXT}
                     optionsLabelElement={{ false: EXPERIMENTAL_ICON }}
                   />
diff --git 
a/web-console/src/views/sql-data-loader-view/sql-data-loader-view.tsx 
b/web-console/src/views/sql-data-loader-view/sql-data-loader-view.tsx
index 84e3901a566..d692c229441 100644
--- a/web-console/src/views/sql-data-loader-view/sql-data-loader-view.tsx
+++ b/web-console/src/views/sql-data-loader-view/sql-data-loader-view.tsx
@@ -33,7 +33,6 @@ import {
   DEFAULT_SERVER_QUERY_CONTEXT,
   Execution,
   externalConfigToIngestQueryPattern,
-  getQueryContextKey,
   ingestQueryPatternToQuery,
 } from '../../druid-models';
 import type { Capabilities } from '../../helpers';
@@ -52,11 +51,6 @@ import { TitleFrame } from './title-frame/title-frame';
 
 import './sql-data-loader-view.scss';
 
-const INITIAL_QUERY_CONTEXT: QueryContext = {
-  finalizeAggregations: false,
-  groupByEnableMultiValueUnnesting: false,
-};
-
 interface LoaderContent extends QueryWithContext {
   id?: string;
 }
@@ -148,17 +142,6 @@ export const SqlDataLoaderView = React.memo(function 
SqlDataLoaderView(
         <SchemaStep
           queryString={content.queryString}
           onQueryStringChange={queryString => setContent({ ...content, 
queryString })}
-          forceSegmentSortByTime={getQueryContextKey(
-            'forceSegmentSortByTime',
-            content.queryContext || {},
-            serverQueryContext,
-          )}
-          changeForceSegmentSortByTime={forceSegmentSortByTime =>
-            setContent({
-              ...content,
-              queryContext: { ...content?.queryContext, forceSegmentSortByTime 
},
-            })
-          }
           enableAnalyze={false}
           goToQuery={() => goToQuery(content)}
           onBack={() => setContent(undefined)}
@@ -220,8 +203,6 @@ export const SqlDataLoaderView = React.memo(function 
SqlDataLoaderView(
             initInputFormat={inputFormat}
             doneButton={false}
             onSet={({ inputSource, inputFormat, signature, timeExpression, 
arrayMode }) => {
-              const queryContext: QueryContext = { ...INITIAL_QUERY_CONTEXT };
-              if (arrayMode === 'arrays') queryContext.arrayIngestMode = 
'array';
               setContent({
                 queryString: ingestQueryPatternToQuery(
                   externalConfigToIngestQueryPattern(
@@ -231,7 +212,6 @@ export const SqlDataLoaderView = React.memo(function 
SqlDataLoaderView(
                     arrayMode,
                   ),
                 ).toString(),
-                queryContext,
               });
             }}
             altText="Skip the wizard and continue with custom SQL"
diff --git 
a/web-console/src/views/workbench-view/connect-external-data-dialog/connect-external-data-dialog.tsx
 
b/web-console/src/views/workbench-view/connect-external-data-dialog/connect-external-data-dialog.tsx
index 79515810262..eaccb498be9 100644
--- 
a/web-console/src/views/workbench-view/connect-external-data-dialog/connect-external-data-dialog.tsx
+++ 
b/web-console/src/views/workbench-view/connect-external-data-dialog/connect-external-data-dialog.tsx
@@ -20,7 +20,12 @@ import { Classes, Dialog } from '@blueprintjs/core';
 import type { SqlExpression } from 'druid-query-toolkit';
 import React, { useState } from 'react';
 
-import type { ArrayMode, ExternalConfig, InputFormat, InputSource } from 
'../../../druid-models';
+import type {
+  ArrayIngestMode,
+  ExternalConfig,
+  InputFormat,
+  InputSource,
+} from '../../../druid-models';
 import { InputFormatStep } from '../input-format-step/input-format-step';
 import { InputSourceStep } from '../input-source-step/input-source-step';
 
@@ -32,7 +37,7 @@ export interface ConnectExternalDataDialogProps {
     config: ExternalConfig,
     timeExpression: SqlExpression | undefined,
     partitionedByHint: string | undefined,
-    arrayMode: ArrayMode,
+    arrayMode: ArrayIngestMode,
   ): void;
   onClose(): void;
 }
diff --git 
a/web-console/src/views/workbench-view/explain-dialog/explain-dialog.tsx 
b/web-console/src/views/workbench-view/explain-dialog/explain-dialog.tsx
index 9b81edd6d83..d6eed657d09 100644
--- a/web-console/src/views/workbench-view/explain-dialog/explain-dialog.tsx
+++ b/web-console/src/views/workbench-view/explain-dialog/explain-dialog.tsx
@@ -46,19 +46,11 @@ import {
   nonEmptyArray,
   queryDruidSql,
   queryDruidSqlDart,
+  wrapInExplainIfNeeded,
 } from '../../../utils';
 
 import './explain-dialog.scss';
 
-function isExplainQuery(query: string): boolean {
-  return /^\s*EXPLAIN\sPLAN\sFOR/im.test(query);
-}
-
-function wrapInExplainIfNeeded(query: string): string {
-  if (isExplainQuery(query)) return query;
-  return `EXPLAIN PLAN FOR ${query}`;
-}
-
 export interface QueryContextEngine extends QueryWithContext {
   engine: DruidEngine;
 }
diff --git 
a/web-console/src/views/workbench-view/input-format-step/input-format-step.tsx 
b/web-console/src/views/workbench-view/input-format-step/input-format-step.tsx
index 781a0196680..cd9e961ec0c 100644
--- 
a/web-console/src/views/workbench-view/input-format-step/input-format-step.tsx
+++ 
b/web-console/src/views/workbench-view/input-format-step/input-format-step.tsx
@@ -22,12 +22,18 @@ import type { SqlExpression } from 'druid-query-toolkit';
 import { C, SqlColumnDeclaration, SqlType } from 'druid-query-toolkit';
 import React, { useState } from 'react';
 
-import { ArrayModeSwitch, AutoForm, CenterMessage, LearnMore, Loader } from 
'../../../components';
-import type { ArrayMode, InputFormat, InputSource } from 
'../../../druid-models';
+import {
+  ArrayIngestModeSwitch,
+  AutoForm,
+  CenterMessage,
+  LearnMore,
+  Loader,
+} from '../../../components';
+import type { ArrayIngestMode, InputFormat, InputSource } from 
'../../../druid-models';
 import {
   BATCH_INPUT_FORMAT_FIELDS,
   chooseByBestTimestamp,
-  DEFAULT_ARRAY_MODE,
+  DEFAULT_ARRAY_INGEST_MODE,
   DETECTION_TIMESTAMP_SPEC,
   getPossibleSystemFieldsForInputSource,
   guessColumnTypeFromSampleResponse,
@@ -55,7 +61,7 @@ export interface InputSourceFormatAndMore {
   inputFormat: InputFormat;
   signature: SqlColumnDeclaration[];
   timeExpression: SqlExpression | undefined;
-  arrayMode: ArrayMode;
+  arrayMode: ArrayIngestMode;
 }
 
 interface InputSourceAndFormat {
@@ -94,7 +100,8 @@ export const InputFormatStep = React.memo(function 
InputFormatStep(props: InputF
     InputSourceAndFormat | undefined
   >(isValidInputFormat(initInputFormat) ? inputSourceAndFormat : undefined);
   const [selectTimestamp, setSelectTimestamp] = useState(true);
-  const [arrayMode, setArrayMode] = useState<ArrayMode>(DEFAULT_ARRAY_MODE);
+  const [arrayIngestMode, setArrayIngestMode] =
+    useState<ArrayIngestMode>(DEFAULT_ARRAY_INGEST_MODE);
 
   const [previewState] = useQueryManager<InputSourceAndFormat, 
SampleResponse>({
     query: inputSourceAndFormatToSample,
@@ -189,7 +196,7 @@ export const InputFormatStep = React.memo(function 
InputFormatStep(props: InputF
             ),
           ),
           timeExpression: selectTimestamp ? 
possibleTimeExpression?.timeExpression : undefined,
-          arrayMode,
+          arrayMode: arrayIngestMode,
         }
       : undefined;
 
@@ -282,7 +289,12 @@ export const InputFormatStep = React.memo(function 
InputFormatStep(props: InputF
           )}
         </div>
         <div className="bottom-controls">
-          {hasArrays && <ArrayModeSwitch arrayMode={arrayMode} 
changeArrayMode={setArrayMode} />}
+          {hasArrays && (
+            <ArrayIngestModeSwitch
+              arrayIngestMode={arrayIngestMode}
+              changeArrayIngestMode={setArrayIngestMode}
+            />
+          )}
           {possibleTimeExpression && (
             <FormGroup>
               <Callout>
diff --git a/web-console/src/views/workbench-view/run-panel/run-panel.tsx 
b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
index 9ca4f300402..1c2d0b75108 100644
--- a/web-console/src/views/workbench-view/run-panel/run-panel.tsx
+++ b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
@@ -207,35 +207,35 @@ export const RunPanel = React.memo(function 
RunPanel(props: RunPanelProps) {
 
   const emptyQuery = query.isEmptyQuery();
   const ingestMode = query.isIngestQuery();
-  const queryContext = query.queryContext;
-  const numContextKeys = Object.keys(queryContext).length;
+  const queryContext = query.getQueryStringContext();
   const queryParameters = query.queryParameters;
+  const effectiveDefaultContext = { ...defaultQueryContext, 
...query.queryContext };
 
   // Extract the context parts that have UI
   const sqlTimeZone = queryContext.sqlTimeZone;
 
-  const useCache = getQueryContextKey('useCache', queryContext, 
defaultQueryContext);
+  const useCache = getQueryContextKey('useCache', queryContext, 
effectiveDefaultContext);
   const useApproximateTopN = getQueryContextKey(
     'useApproximateTopN',
     queryContext,
-    defaultQueryContext,
+    effectiveDefaultContext,
   );
   const useApproximateCountDistinct = getQueryContextKey(
     'useApproximateCountDistinct',
     queryContext,
-    defaultQueryContext,
+    effectiveDefaultContext,
   );
 
   const arrayIngestMode = queryContext.arrayIngestMode;
   const maxParseExceptions = getQueryContextKey(
     'maxParseExceptions',
     queryContext,
-    defaultQueryContext,
+    effectiveDefaultContext,
   );
   const failOnEmptyInsert = getQueryContextKey(
     'failOnEmptyInsert',
     queryContext,
-    defaultQueryContext,
+    effectiveDefaultContext,
   );
   const finalizeAggregations = queryContext.finalizeAggregations;
   const waitUntilSegmentsLoad = queryContext.waitUntilSegmentsLoad;
@@ -243,20 +243,20 @@ export const RunPanel = React.memo(function 
RunPanel(props: RunPanelProps) {
   const sqlJoinAlgorithm = getQueryContextKey(
     'sqlJoinAlgorithm',
     queryContext,
-    defaultQueryContext,
+    effectiveDefaultContext,
   );
   const selectDestination = getQueryContextKey(
     'selectDestination',
     queryContext,
-    defaultQueryContext,
+    effectiveDefaultContext,
   );
   const durableShuffleStorage = getQueryContextKey(
     'durableShuffleStorage',
     queryContext,
-    defaultQueryContext,
+    effectiveDefaultContext,
   );
 
-  const indexSpec: IndexSpec | undefined = deepGet(queryContext, 'indexSpec');
+  const indexSpec: IndexSpec | undefined = deepGet(query.queryContext, 
'indexSpec');
 
   const handleRun = useCallback(() => {
     if (!onRun) return;
@@ -297,6 +297,10 @@ export const RunPanel = React.memo(function 
RunPanel(props: RunPanelProps) {
     
onQueryChange(query.changeQueryContext(removeUndefinedValues(queryContext)));
   }
 
+  function changeQueryStringContext(queryContext: QueryContext) {
+    
onQueryChange(query.changeQueryStringContext(removeUndefinedValues(queryContext)));
+  }
+
   const overloadWarning =
     query.unlimited &&
     (queryEngine === 'sql-native' ||
@@ -360,41 +364,22 @@ export const RunPanel = React.memo(function 
RunPanel(props: RunPanelProps) {
                         />
                       );
                     })}
-
                     <MenuDivider />
                   </>
                 )}
-                {show('edit-query-context') && (
-                  <MenuItem
-                    icon={IconNames.PROPERTIES}
-                    text="Edit query context..."
-                    onClick={() => setEditContextDialogOpen(true)}
-                    label={pluralIfNeeded(numContextKeys, 'key')}
-                  />
-                )}
-                {show('define-parameters') && (
-                  <MenuItem
-                    icon={IconNames.HELP}
-                    text="Define parameters..."
-                    onClick={() => setEditParametersDialogOpen(true)}
-                    label={
-                      queryParameters ? pluralIfNeeded(queryParameters.length, 
'parameter') : ''
-                    }
-                  />
-                )}
                 {show('timezone') && (
                   <MenuItem
                     icon={IconNames.GLOBE_NETWORK}
                     text="Timezone"
-                    label={sqlTimeZone ?? defaultQueryContext.sqlTimeZone}
+                    label={sqlTimeZone ?? effectiveDefaultContext.sqlTimeZone}
                   >
                     <MenuDivider title="Timezone type" />
                     <TimezoneMenuItems
                       sqlTimeZone={sqlTimeZone}
                       setSqlTimeZone={sqlTimeZone =>
-                        changeQueryContext({ ...queryContext, sqlTimeZone })
+                        changeQueryStringContext({ ...queryContext, 
sqlTimeZone })
                       }
-                      defaultSqlTimeZone={defaultQueryContext.sqlTimeZone}
+                      defaultSqlTimeZone={effectiveDefaultContext.sqlTimeZone}
                     />
                     <MenuItem
                       icon={IconNames.BLANK}
@@ -409,7 +394,7 @@ export const RunPanel = React.memo(function RunPanel(props: 
RunPanelProps) {
                     text="Approximate COUNT(DISTINCT)"
                     value={useApproximateCountDistinct}
                     onValueChange={useApproximateCountDistinct =>
-                      changeQueryContext({
+                      changeQueryStringContext({
                         ...queryContext,
                         useApproximateCountDistinct,
                       })
@@ -423,7 +408,7 @@ export const RunPanel = React.memo(function RunPanel(props: 
RunPanelProps) {
                     text="Approximate TopN"
                     value={useApproximateTopN}
                     onValueChange={useApproximateTopN =>
-                      changeQueryContext({
+                      changeQueryStringContext({
                         ...queryContext,
                         useApproximateTopN,
                       })
@@ -446,7 +431,9 @@ export const RunPanel = React.memo(function RunPanel(props: 
RunPanelProps) {
                         icon={tickIcon(sqlJoinAlgorithm === o)}
                         text={SQL_JOIN_ALGORITHM_LABEL[o]}
                         shouldDismissPopover={false}
-                        onClick={() => changeQueryContext({ ...queryContext, 
sqlJoinAlgorithm: o })}
+                        onClick={() =>
+                          changeQueryStringContext({ ...queryContext, 
sqlJoinAlgorithm: o })
+                        }
                       />
                     ))}
                   </MenuItem>
@@ -457,13 +444,49 @@ export const RunPanel = React.memo(function 
RunPanel(props: RunPanelProps) {
                     icon={IconNames.BRING_DATA}
                     text="INSERT / REPLACE / EXTERN specific context"
                   >
+                    <MenuItem
+                      text="Array ingest mode"
+                      label={
+                        arrayIngestMode
+                          ? ARRAY_INGEST_MODE_LABEL[arrayIngestMode]
+                          : '(server default)'
+                      }
+                    >
+                      {([undefined, 'array', 'mvd'] as (ArrayIngestMode | 
undefined)[]).map(
+                        (m, i) => (
+                          <MenuItem
+                            key={i}
+                            icon={tickIcon(m === arrayIngestMode)}
+                            text={
+                              m
+                                ? ARRAY_INGEST_MODE_DESCRIPTION[m]
+                                : `(server default${
+                                    effectiveDefaultContext.arrayIngestMode
+                                      ? `: 
${effectiveDefaultContext.arrayIngestMode}`
+                                      : ''
+                                  })`
+                            }
+                            onClick={() =>
+                              changeQueryStringContext({ ...queryContext, 
arrayIngestMode: m })
+                            }
+                          />
+                        ),
+                      )}
+                      <MenuDivider />
+                      <MenuItem
+                        icon={IconNames.HELP}
+                        text="Documentation"
+                        
href={`${getLink('DOCS')}/querying/arrays#arrayingestmode`}
+                        target="_blank"
+                      />
+                    </MenuItem>
                     <MenuBoolean
                       text="Fail on empty insert"
                       value={failOnEmptyInsert}
                       showUndefined
                       undefinedEffectiveValue={false}
                       onValueChange={failOnEmptyInsert =>
-                        changeQueryContext({ ...queryContext, 
failOnEmptyInsert })
+                        changeQueryStringContext({ ...queryContext, 
failOnEmptyInsert })
                       }
                       optionsText={ENABLED_DISABLED_OPTIONS_TEXT}
                     />
@@ -473,7 +496,7 @@ export const RunPanel = React.memo(function RunPanel(props: 
RunPanelProps) {
                       showUndefined
                       undefinedEffectiveValue={ingestMode}
                       onValueChange={waitUntilSegmentsLoad =>
-                        changeQueryContext({ ...queryContext, 
waitUntilSegmentsLoad })
+                        changeQueryStringContext({ ...queryContext, 
waitUntilSegmentsLoad })
                       }
                       optionsText={ENABLED_DISABLED_OPTIONS_TEXT}
                     />
@@ -484,7 +507,7 @@ export const RunPanel = React.memo(function RunPanel(props: 
RunPanelProps) {
                           icon={tickIcon(v === maxParseExceptions)}
                           text={v === -1 ? '∞ (-1)' : String(v)}
                           onClick={() =>
-                            changeQueryContext({ ...queryContext, 
maxParseExceptions: v })
+                            changeQueryStringContext({ ...queryContext, 
maxParseExceptions: v })
                           }
                           shouldDismissPopover={false}
                         />
@@ -509,7 +532,7 @@ export const RunPanel = React.memo(function RunPanel(props: 
RunPanelProps) {
                     showUndefined
                     undefinedEffectiveValue={!ingestMode}
                     onValueChange={finalizeAggregations =>
-                      changeQueryContext({ ...queryContext, 
finalizeAggregations })
+                      changeQueryStringContext({ ...queryContext, 
finalizeAggregations })
                     }
                     optionsText={ENABLED_DISABLED_OPTIONS_TEXT}
                   />
@@ -522,7 +545,10 @@ export const RunPanel = React.memo(function 
RunPanel(props: RunPanelProps) {
                     showUndefined
                     undefinedEffectiveValue={!ingestMode}
                     onValueChange={groupByEnableMultiValueUnnesting =>
-                      changeQueryContext({ ...queryContext, 
groupByEnableMultiValueUnnesting })
+                      changeQueryStringContext({
+                        ...queryContext,
+                        groupByEnableMultiValueUnnesting,
+                      })
                     }
                     optionsText={ENABLED_DISABLED_OPTIONS_TEXT}
                   />
@@ -534,7 +560,7 @@ export const RunPanel = React.memo(function RunPanel(props: 
RunPanelProps) {
                     text="Use cache"
                     value={useCache}
                     onValueChange={useCache =>
-                      changeQueryContext({
+                      changeQueryStringContext({
                         ...queryContext,
                         useCache,
                         populateCache: useCache,
@@ -563,7 +589,7 @@ export const RunPanel = React.memo(function RunPanel(props: 
RunPanelProps) {
                     text="Durable shuffle storage"
                     value={durableShuffleStorage}
                     onValueChange={durableShuffleStorage =>
-                      changeQueryContext({
+                      changeQueryStringContext({
                         ...queryContext,
                         durableShuffleStorage,
                       })
@@ -588,7 +614,7 @@ export const RunPanel = React.memo(function RunPanel(props: 
RunPanelProps) {
                         text={SELECT_DESTINATION_LABEL[o]}
                         shouldDismissPopover={false}
                         onClick={() =>
-                          changeQueryContext({ ...queryContext, 
selectDestination: o })
+                          changeQueryStringContext({ ...queryContext, 
selectDestination: o })
                         }
                       />
                     ))}
@@ -607,6 +633,25 @@ export const RunPanel = React.memo(function 
RunPanel(props: RunPanelProps) {
                     />
                   </MenuItem>
                 )}
+                {(show('edit-query-context') || show('define-parameters')) && 
<MenuDivider />}
+                {show('edit-query-context') && (
+                  <MenuItem
+                    icon={IconNames.PROPERTIES}
+                    text="Edit query context..."
+                    onClick={() => setEditContextDialogOpen(true)}
+                    
label={pluralIfNeeded(Object.keys(query.queryContext).length, 'key')}
+                  />
+                )}
+                {show('define-parameters') && (
+                  <MenuItem
+                    icon={IconNames.HELP}
+                    text="Define parameters..."
+                    onClick={() => setEditParametersDialogOpen(true)}
+                    label={
+                      queryParameters ? pluralIfNeeded(queryParameters.length, 
'parameter') : ''
+                    }
+                  />
+                )}
               </Menu>
             }
           >
@@ -624,53 +669,14 @@ export const RunPanel = React.memo(function 
RunPanel(props: RunPanelProps) {
             <MaxTasksButton
               clusterCapacity={clusterCapacity}
               queryContext={queryContext}
-              changeQueryContext={changeQueryContext}
-              defaultQueryContext={defaultQueryContext}
+              changeQueryContext={changeQueryStringContext}
+              defaultQueryContext={effectiveDefaultContext}
               menuHeader={maxTasksMenuHeader}
               maxTasksLabelFn={maxTasksLabelFn}
               maxTasksOptions={maxTasksOptions}
               fullClusterCapacityLabelFn={fullClusterCapacityLabelFn}
             />
           )}
-          {ingestMode && (
-            <Popover
-              position={Position.BOTTOM_LEFT}
-              content={
-                <Menu>
-                  {([undefined, 'array', 'mvd'] as (ArrayIngestMode | 
undefined)[]).map((m, i) => (
-                    <MenuItem
-                      key={i}
-                      icon={tickIcon(m === arrayIngestMode)}
-                      text={
-                        m
-                          ? ARRAY_INGEST_MODE_DESCRIPTION[m]
-                          : `(server default${
-                              defaultQueryContext.arrayIngestMode
-                                ? `: ${defaultQueryContext.arrayIngestMode}`
-                                : ''
-                            })`
-                      }
-                      onClick={() => changeQueryContext({ ...queryContext, 
arrayIngestMode: m })}
-                    />
-                  ))}
-                  <MenuDivider />
-                  <MenuItem
-                    icon={IconNames.HELP}
-                    text="Documentation"
-                    href={`${getLink('DOCS')}/querying/arrays#arrayingestmode`}
-                    target="_blank"
-                  />
-                </Menu>
-              }
-            >
-              <Button
-                text={`Array ingest mode: ${
-                  arrayIngestMode ? ARRAY_INGEST_MODE_LABEL[arrayIngestMode] : 
'(server default)'
-                }`}
-                rightIcon={IconNames.CARET_DOWN}
-              />
-            </Popover>
-          )}
         </ButtonGroup>
       )}
       {moreMenu && (
@@ -680,7 +686,7 @@ export const RunPanel = React.memo(function RunPanel(props: 
RunPanelProps) {
       )}
       {editContextDialogOpen && (
         <EditContextDialog
-          initQueryContext={queryContext}
+          initQueryContext={query.queryContext}
           onQueryContextChange={changeQueryContext}
           onClose={() => {
             setEditContextDialogOpen(false);
@@ -708,7 +714,7 @@ export const RunPanel = React.memo(function RunPanel(props: 
RunPanelProps) {
       {indexSpecDialogSpec && (
         <IndexSpecDialog
           onClose={() => setIndexSpecDialogSpec(undefined)}
-          onSave={indexSpec => changeQueryContext({ ...queryContext, indexSpec 
})}
+          onSave={indexSpec => changeQueryContext({ ...query.queryContext, 
indexSpec })}
           indexSpec={indexSpecDialogSpec}
         />
       )}


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


Reply via email to