diff --git a/web-console/script/druid b/web-console/script/druid index 2edb40439bef..c1f4f4bbcabc 100755 --- a/web-console/script/druid +++ b/web-console/script/druid @@ -70,6 +70,8 @@ function _build_distribution() { && echo -e "\n\ndruid.server.http.allowedHttpMethods=[\"HEAD\"]" >> conf/druid/auto/_common/common.runtime.properties \ && echo -e "\n\ndruid.export.storage.baseDir=/" >> conf/druid/auto/_common/common.runtime.properties \ && echo -e "\n\ndruid.msq.dart.enabled=true" >> conf/druid/auto/_common/common.runtime.properties \ + && echo -e "\n\ndruid.msq.dart.controller.maxRetainedReportCount=100" >> conf/druid/auto/_common/common.runtime.properties \ + && echo -e "\n\ndruid.msq.dart.controller.maxRetainedReportDuration=PT3600S" >> conf/druid/auto/_common/common.runtime.properties \ ) } diff --git a/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts b/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts index e74513c7e28c..9e3cb8dca8dd 100644 --- a/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts +++ b/web-console/src/dialogs/edit-context-dialog/query-context-completions.ts @@ -110,6 +110,7 @@ export const QUERY_CONTEXT_COMPLETIONS: JsonCompletionRule[] = [ { value: 'forceSegmentSortByTime', documentation: 'Force segments to be sorted by time' }, { value: 'includeAllCounters', documentation: 'Include all counters in task reports' }, // SQL specific + { value: 'sqlQueryId', documentation: 'Query ID for SQL queries' }, { value: 'sqlTimeZone', documentation: 'Time zone for SQL queries' }, { value: 'useApproximateCountDistinct', documentation: 'Use approximate COUNT DISTINCT' }, { value: 'useApproximateTopN', documentation: 'Use approximate TOP N queries' }, diff --git a/web-console/src/druid-models/execution/execution-ingest-complete.mock.ts b/web-console/src/druid-models/execution/execution-ingest-complete.mock.ts index 24ba4687ac3f..cf52f2d3c4d4 100644 --- a/web-console/src/druid-models/execution/execution-ingest-complete.mock.ts +++ b/web-console/src/druid-models/execution/execution-ingest-complete.mock.ts @@ -55,6 +55,7 @@ export const EXECUTION_INGEST_COMPLETE = Execution.fromTaskReport({ workerId: 'query-346b9ac6-4912-46e4-9b98-75f11071af87-worker0_0', state: 'SUCCESS', durationMs: 8789, + pendingMs: 123, }, ], }, diff --git a/web-console/src/druid-models/execution/execution-ingest-error.mock.ts b/web-console/src/druid-models/execution/execution-ingest-error.mock.ts index 0c2c5a93ed04..b48002113412 100644 --- a/web-console/src/druid-models/execution/execution-ingest-error.mock.ts +++ b/web-console/src/druid-models/execution/execution-ingest-error.mock.ts @@ -92,6 +92,7 @@ export const EXECUTION_INGEST_ERROR = Execution.fromTaskReport({ workerId: 'query-26d490c6-c06d-4cd2-938f-bc5f7f982754-worker0_0', state: 'FAILED', durationMs: -1, + pendingMs: -1, }, ], }, diff --git a/web-console/src/druid-models/execution/execution.ts b/web-console/src/druid-models/execution/execution.ts index 1237b49e62da..0c327b1d064b 100644 --- a/web-console/src/druid-models/execution/execution.ts +++ b/web-console/src/druid-models/execution/execution.ts @@ -366,8 +366,18 @@ export class Execution { static getProgressDescription(execution: Execution | undefined): string { if (!execution?.stages) return 'Loading...'; - if (!execution.isWaitingForQuery()) - return 'Query complete, waiting for segments to be loaded...'; + if (!execution.isWaitingForQuery()) { + switch (execution.engine) { + case 'sql-msq-task': + return 'Query complete, waiting for segments to be loaded...'; + + case 'sql-msq-dart': + return 'Got a non-running report. Did you reuse a sqlQueryID?'; + + default: + return 'Query not running.'; + } + } let ret = execution.stages.getStage(0)?.phase ? 'Running query...' : 'Starting query...'; if (execution.usageInfo) { @@ -556,6 +566,10 @@ export class Execution { return status !== 'SUCCESS' && status !== 'FAILED'; } + public isWaitingForSegments(): boolean { + return Boolean(this.stages && !this.isWaitingForQuery() && this.engine === 'sql-msq-task'); + } + public getSegmentStatusDescription() { const { segmentStatus } = this; diff --git a/web-console/src/druid-models/task/task.ts b/web-console/src/druid-models/task/task.ts index e1743c116897..0d6cd07266fd 100644 --- a/web-console/src/druid-models/task/task.ts +++ b/web-console/src/druid-models/task/task.ts @@ -81,6 +81,7 @@ export interface WorkerState { workerId: string; state: string; durationMs: number; + pendingMs: number; } export interface SegmentLoadWaiterStatus { diff --git a/web-console/src/druid-models/workbench-query/workbench-query.spec.ts b/web-console/src/druid-models/workbench-query/workbench-query.spec.ts index 82bbb28f69aa..13908bbf2cf6 100644 --- a/web-console/src/druid-models/workbench-query/workbench-query.spec.ts +++ b/web-console/src/druid-models/workbench-query/workbench-query.spec.ts @@ -843,4 +843,822 @@ describe('WorkbenchQuery', () => { }); }); }); + + describe('#getEffectiveEngine', () => { + beforeEach(() => { + // Reset to default engines before each test + WorkbenchQuery.setQueryEngines(['native', 'sql-native', 'sql-msq-task']); + }); + + describe('when engine is explicitly set', () => { + it('returns the explicitly set engine', () => { + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString('SELECT * FROM wikipedia') + .changeEngine('sql-msq-task'); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task'); + }); + + it('returns explicit engine even if query suggests different engine', () => { + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString('INSERT INTO wiki SELECT * FROM wikipedia') + .changeEngine('sql-native'); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('returns explicit engine for JSON queries', () => { + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString('{"queryType": "topN", "dataSource": "test"}') + .changeEngine('sql-native'); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('returns explicit engine even when context has engine set', () => { + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString('SELECT * FROM wikipedia') + .changeQueryContext({ engine: 'native' }) + .changeEngine('sql-msq-task'); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task'); + }); + }); + + describe('when context engine is set', () => { + it('returns sql-native when context engine is native', () => { + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString('SELECT * FROM wikipedia') + .changeQueryContext({ engine: 'native' }); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('returns sql-msq-dart when context engine is msq-dart', () => { + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString('SELECT * FROM wikipedia') + .changeQueryContext({ engine: 'msq-dart' }); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-dart'); + }); + + it('returns sql-native when context engine is native via SET statement', () => { + const queryWithSet = sane` + SET engine = 'native'; + SELECT * FROM wikipedia + `; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(queryWithSet); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('returns sql-msq-dart when context engine is msq-dart via SET statement', () => { + const queryWithSet = sane` + SET engine = 'msq-dart'; + SELECT * FROM wikipedia + `; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(queryWithSet); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-dart'); + }); + + it('returns sql-native when context engine is native via JSON context', () => { + const sqlInJson = sane` + { + "query": "SELECT * FROM wikipedia", + "context": { + "engine": "native" + } + } + `; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(sqlInJson); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('returns sql-msq-dart when context engine is msq-dart via JSON context', () => { + const sqlInJson = sane` + { + "query": "SELECT * FROM wikipedia", + "context": { + "engine": "msq-dart" + } + } + `; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(sqlInJson); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-dart'); + }); + + it('prioritizes SET statement engine over JSON context engine', () => { + const sqlInJson = sane` + { + "query": "SET engine = 'msq-dart'; SELECT * FROM wikipedia", + "context": { + "engine": "native" + } + } + `; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(sqlInJson); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-dart'); + }); + + it('falls through to other logic when context engine is not native or msq-dart', () => { + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString('SELECT * FROM wikipedia') + .changeQueryContext({ engine: 'msq-task' }); + + // Should fall through to normal logic and return sql-native + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('falls through to other logic when context engine is not set', () => { + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString('SELECT * FROM wikipedia') + .changeQueryContext({ maxNumTasks: 3 }); + + // Should fall through to normal logic and return sql-native + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('handles INSERT query with context engine native', () => { + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString('INSERT INTO wiki SELECT * FROM wikipedia') + .changeQueryContext({ engine: 'native' }); + + // Context engine takes priority over task engine detection + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('handles JSON query with context engine msq-dart', () => { + const nativeJson = sane` + { + "queryType": "topN", + "dataSource": "wikipedia", + "context": { + "engine": "msq-dart" + } + } + `; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(nativeJson); + + // Context engine takes priority over JSON-like detection + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-dart'); + }); + }); + + describe('when query is JSON-like', () => { + it('returns sql-native for SQL-in-JSON when sql-native is enabled', () => { + const sqlInJson = sane` + { + "query": "SELECT * FROM wikipedia", + "context": {} + } + `; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(sqlInJson); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('returns native for native JSON query when native is enabled', () => { + const nativeJson = sane` + { + "queryType": "topN", + "dataSource": "wikipedia", + "dimension": "page", + "threshold": 10, + "intervals": ["2015-09-12/2015-09-13"], + "granularity": "all", + "aggregations": [ + {"type": "count", "name": "count"} + ] + } + `; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(nativeJson); + + expect(workbenchQuery.getEffectiveEngine()).toBe('native'); + }); + + it('falls through for SQL-in-JSON when sql-native is not enabled', () => { + WorkbenchQuery.setQueryEngines(['native', 'sql-msq-task']); + + const sqlInJson = sane` + { + "query": "SELECT * FROM wikipedia", + "context": {} + } + `; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(sqlInJson); + + // Falls through JSON-like check, task engine check (no INSERT/EXTERN), sql-native check (not enabled), + // and returns first enabled engine which is 'native' + expect(workbenchQuery.getEffectiveEngine()).toBe('native'); + }); + + it('falls through for native JSON when native is not enabled', () => { + WorkbenchQuery.setQueryEngines(['sql-native', 'sql-msq-task']); + + const nativeJson = sane` + { + "queryType": "topN", + "dataSource": "wikipedia" + } + `; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(nativeJson); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + }); + + describe('when query needs task engine', () => { + it('returns sql-msq-task for INSERT query when sql-msq-task is enabled', () => { + const insertQuery = 'INSERT INTO wiki SELECT * FROM wikipedia'; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(insertQuery); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task'); + }); + + it('returns sql-msq-task for REPLACE query when sql-msq-task is enabled', () => { + const replaceQuery = 'REPLACE INTO wiki OVERWRITE ALL SELECT * FROM wikipedia'; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(replaceQuery); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task'); + }); + + it('returns sql-msq-task for EXTERN query when sql-msq-task is enabled', () => { + const externQuery = sane` + SELECT * + FROM TABLE( + EXTERN( + '{"type":"http","uris":["https://example.com/data.json"]}', + '{"type":"json"}' + ) + ) + `; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(externQuery); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task'); + }); + + it('falls through when sql-msq-task is not enabled for task engine query', () => { + WorkbenchQuery.setQueryEngines(['native', 'sql-native']); + + const insertQuery = 'INSERT INTO wiki SELECT * FROM wikipedia'; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(insertQuery); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + }); + + describe('fallback behavior', () => { + it('falls back to sql-native for regular SQL query', () => { + const regularQuery = 'SELECT * FROM wikipedia'; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(regularQuery); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('falls back to sql-native when it is in enabled engines', () => { + WorkbenchQuery.setQueryEngines(['native', 'sql-msq-task', 'sql-native']); + + const regularQuery = "SELECT * FROM wikipedia WHERE channel = 'en'"; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(regularQuery); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('falls back to first enabled engine when sql-native is not available', () => { + WorkbenchQuery.setQueryEngines(['native', 'sql-msq-task']); + + const regularQuery = 'SELECT * FROM wikipedia'; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(regularQuery); + + expect(workbenchQuery.getEffectiveEngine()).toBe('native'); + }); + + it('falls back to sql-native when no engines are enabled', () => { + WorkbenchQuery.setQueryEngines([]); + + const regularQuery = 'SELECT * FROM wikipedia'; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(regularQuery); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + }); + + describe('complex scenarios', () => { + it('prioritizes explicit engine over task engine detection', () => { + const insertQuery = 'INSERT INTO wiki SELECT * FROM wikipedia'; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(insertQuery) + .changeEngine('sql-native'); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('handles SQL query with different enabled engines order', () => { + WorkbenchQuery.setQueryEngines(['sql-msq-task', 'native', 'sql-native']); + + const regularQuery = 'SELECT COUNT(*) FROM wikipedia'; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(regularQuery); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('returns sql-msq-task for task query even when sql-native is enabled', () => { + WorkbenchQuery.setQueryEngines(['sql-native', 'sql-msq-task', 'native']); + + const insertQuery = sane` + INSERT INTO wiki + SELECT * FROM wikipedia + PARTITIONED BY DAY + `; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(insertQuery); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task'); + }); + + it('handles empty query string', () => { + const workbenchQuery = WorkbenchQuery.blank(); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('handles query with only whitespace', () => { + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(' \n\t '); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-native'); + }); + + it('handles malformed JSON query', () => { + const malformedJson = '{ "queryType": "topN"'; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(malformedJson); + + // Malformed JSON will be treated as JSON-like (starts with {) but will fail isSqlInJson check, + // falling into the native JSON branch which returns 'native' since it's enabled + expect(workbenchQuery.getEffectiveEngine()).toBe('native'); + }); + + it('correctly identifies case-insensitive INSERT keyword', () => { + const insertQuery = 'insert into wiki select * from wikipedia'; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(insertQuery); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task'); + }); + + it('correctly identifies case-insensitive EXTERN keyword', () => { + const externQuery = sane` + SELECT * FROM TABLE( + extern( + '{"type":"http","uris":["https://example.com/data.json"]}', + '{"type":"json"}' + ) + ) + `; + + const workbenchQuery = WorkbenchQuery.blank().changeQueryString(externQuery); + + expect(workbenchQuery.getEffectiveEngine()).toBe('sql-msq-task'); + }); + }); + }); + + describe('#getEffectiveContext', () => { + describe('for regular SQL queries', () => { + it('returns queryContext when no SET statements exist', () => { + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString('SELECT * FROM wikipedia') + .changeQueryContext({ maxNumTasks: 3, useCache: false }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ + maxNumTasks: 3, + useCache: false, + }); + }); + + it('merges queryContext with SET statement context', () => { + const queryWithSets = sane` + SET maxNumTasks = 5; + SET timeout = 30000; + SELECT * FROM wikipedia + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(queryWithSets) + .changeQueryContext({ useCache: false, finalizeAggregations: true }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ + useCache: false, + finalizeAggregations: true, + maxNumTasks: 5, + timeout: 30000, + }); + }); + + it('prioritizes SET statement context over queryContext', () => { + const queryWithSets = sane` + SET maxNumTasks = 10; + SELECT * FROM wikipedia + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(queryWithSets) + .changeQueryContext({ maxNumTasks: 3, useCache: false }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext.maxNumTasks).toBe(10); + expect(effectiveContext.useCache).toBe(false); + }); + + it('handles empty query string', () => { + const workbenchQuery = WorkbenchQuery.blank().changeQueryContext({ + maxNumTasks: 3, + }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ maxNumTasks: 3 }); + }); + + it('handles query with only SET statements', () => { + const queryWithOnlySets = sane` + SET maxNumTasks = 5; + SET useCache = TRUE; + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(queryWithOnlySets) + .changeQueryContext({ timeout: 60000 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ + timeout: 60000, + maxNumTasks: 5, + useCache: true, + }); + }); + }); + + describe('for native JSON queries', () => { + it('returns queryContext when JSON has no context property', () => { + const nativeJson = sane` + { + "queryType": "topN", + "dataSource": "wikipedia" + } + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(nativeJson) + .changeQueryContext({ maxNumTasks: 3 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ maxNumTasks: 3 }); + }); + + it('merges JSON context with queryContext', () => { + const nativeJson = sane` + { + "queryType": "topN", + "dataSource": "wikipedia", + "context": { + "timeout": 30000, + "useCache": false + } + } + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(nativeJson) + .changeQueryContext({ maxNumTasks: 3 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ + maxNumTasks: 3, + timeout: 30000, + useCache: false, + }); + }); + + it('prioritizes JSON context over queryContext', () => { + const nativeJson = sane` + { + "queryType": "topN", + "dataSource": "wikipedia", + "context": { + "maxNumTasks": 10 + } + } + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(nativeJson) + .changeQueryContext({ maxNumTasks: 3, useCache: false }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext.maxNumTasks).toBe(10); + expect(effectiveContext.useCache).toBe(false); + }); + }); + + describe('for SQL-in-JSON queries', () => { + it('returns queryContext when JSON has no context and SQL has no SET statements', () => { + const sqlInJson = sane` + { + "query": "SELECT * FROM wikipedia" + } + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(sqlInJson) + .changeQueryContext({ maxNumTasks: 3 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ maxNumTasks: 3 }); + }); + + it('merges JSON context with queryContext', () => { + const sqlInJson = sane` + { + "query": "SELECT * FROM wikipedia", + "context": { + "timeout": 30000 + } + } + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(sqlInJson) + .changeQueryContext({ maxNumTasks: 3 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ + maxNumTasks: 3, + timeout: 30000, + }); + }); + + it('merges SQL SET statements context with queryContext', () => { + const sqlInJson = sane` + { + "query": "SET useCache = FALSE; SELECT * FROM wikipedia" + } + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(sqlInJson) + .changeQueryContext({ maxNumTasks: 3 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ + maxNumTasks: 3, + useCache: false, + }); + }); + + it('merges all three contexts: queryContext, JSON context, and SET statements', () => { + const sqlInJson = sane` + { + "query": "SET timeout = 60000; SET finalizeAggregations = TRUE; SELECT * FROM wikipedia", + "context": { + "maxNumTasks": 5, + "useCache": false + } + } + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(sqlInJson) + .changeQueryContext({ maxNumTasks: 3, priority: 10 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ + priority: 10, + maxNumTasks: 5, + useCache: false, + timeout: 60000, + finalizeAggregations: true, + }); + }); + + it('prioritizes SET statements over JSON context over queryContext', () => { + const sqlInJson = sane` + { + "query": "SET maxNumTasks = 20; SELECT * FROM wikipedia", + "context": { + "maxNumTasks": 10 + } + } + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(sqlInJson) + .changeQueryContext({ maxNumTasks: 3 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext.maxNumTasks).toBe(20); + }); + + it('handles SQL-in-JSON with multiple SET statements', () => { + const sqlInJson = sane` + { + "query": "SET maxNumTasks = 8; SET useCache = TRUE; SET timeout = 45000; SELECT * FROM wikipedia", + "context": { + "finalizeAggregations": false + } + } + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(sqlInJson) + .changeQueryContext({ priority: 5 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ + priority: 5, + finalizeAggregations: false, + maxNumTasks: 8, + useCache: true, + timeout: 45000, + }); + }); + }); + + describe('error handling', () => { + it('handles malformed JSON gracefully', () => { + const malformedJson = '{ "queryType": "topN"'; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(malformedJson) + .changeQueryContext({ maxNumTasks: 3 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + // Should fall back to queryContext only since JSON parsing fails + expect(effectiveContext).toEqual({ maxNumTasks: 3 }); + }); + + it('handles JSON with invalid context property', () => { + const jsonWithInvalidContext = sane` + { + "queryType": "topN", + "context": "not an object" + } + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(jsonWithInvalidContext) + .changeQueryContext({ maxNumTasks: 3 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + // Should merge the context even if it's not an object + expect(effectiveContext).toBeDefined(); + }); + + it('handles SQL-in-JSON with malformed SET statements', () => { + const sqlInJson = sane` + { + "query": "SET maxNumTasks INVALID; SELECT * FROM wikipedia" + } + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(sqlInJson) + .changeQueryContext({ maxNumTasks: 3 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + // Should still return queryContext even if SET statement is invalid + expect(effectiveContext).toBeDefined(); + expect(effectiveContext.maxNumTasks).toBe(3); + }); + }); + + describe('edge cases', () => { + it('handles empty queryContext', () => { + const workbenchQuery = WorkbenchQuery.blank().changeQueryString('SELECT * FROM wikipedia'); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({}); + }); + + it('handles whitespace-only query', () => { + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(' \n\t ') + .changeQueryContext({ maxNumTasks: 3 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ maxNumTasks: 3 }); + }); + + it('handles JSON with null context', () => { + const jsonWithNullContext = sane` + { + "queryType": "topN", + "context": null + } + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(jsonWithNullContext) + .changeQueryContext({ maxNumTasks: 3 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toBeDefined(); + }); + + it('handles complex nested context values', () => { + const sqlInJson = sane` + { + "query": "SELECT * FROM wikipedia", + "context": { + "nestedObject": { + "key1": "value1", + "key2": 42 + }, + "arrayValue": [1, 2, 3] + } + } + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(sqlInJson) + .changeQueryContext({ maxNumTasks: 3 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ + maxNumTasks: 3, + nestedObject: { + key1: 'value1', + key2: 42, + }, + arrayValue: [1, 2, 3], + }); + }); + + it('preserves boolean false values in context', () => { + const queryWithSets = sane` + SET useCache = FALSE; + SET finalizeAggregations = FALSE; + SELECT * FROM wikipedia + `; + + const workbenchQuery = WorkbenchQuery.blank() + .changeQueryString(queryWithSets) + .changeQueryContext({ maxNumTasks: 3 }); + + const effectiveContext = workbenchQuery.getEffectiveContext(); + + expect(effectiveContext).toEqual({ + maxNumTasks: 3, + useCache: false, + finalizeAggregations: false, + }); + }); + }); + }); }); 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 825b6af5b15d..7072d41f729b 100644 --- a/web-console/src/druid-models/workbench-query/workbench-query.ts +++ b/web-console/src/druid-models/workbench-query/workbench-query.ts @@ -309,6 +309,29 @@ export class WorkbenchQuery { return SqlSetStatement.getContextFromText(this.queryString); } + public getEffectiveContext(): QueryContext { + let effectiveContext = this.queryContext; + if (this.isJsonLike()) { + try { + const query = Hjson.parse(this.queryString); + effectiveContext = { ...effectiveContext, ...query.context }; + if (typeof query.query === 'string') { + effectiveContext = { + ...effectiveContext, + ...SqlSetStatement.getContextFromText(query.query), + }; + } + } catch {} + } else { + effectiveContext = { + ...effectiveContext, + ...SqlSetStatement.getContextFromText(this.queryString), + }; + } + + return effectiveContext; + } + public changeQueryContext(queryContext: QueryContext): WorkbenchQuery { return new WorkbenchQuery({ ...this.valueOf(), queryContext }); } @@ -340,6 +363,12 @@ export class WorkbenchQuery { public getEffectiveEngine(): DruidEngine { const { engine } = this; if (engine) return engine; + + // If an engine is set explicitly in the config then respect it + const contextEngine = this.getEffectiveContext().engine; + if (contextEngine === 'native') return 'sql-native'; + if (contextEngine === 'msq-dart') return 'sql-msq-dart'; + const enabledEngines = WorkbenchQuery.getQueryEngines(); if (this.isJsonLike()) { if (this.isSqlInJson()) { @@ -463,7 +492,7 @@ export class WorkbenchQuery { } public getMaxNumTasks(): number | undefined { - return this.getQueryStringContext().maxNumTasks ?? this.queryContext.maxNumTasks; + return this.getEffectiveContext().maxNumTasks; } public setMaxNumTasksIfUnset(maxNumTasks: number | undefined): WorkbenchQuery { @@ -552,13 +581,21 @@ export class WorkbenchQuery { ...queryContext, }; + // Effective context lets us see the context which can also include the set statements from the SetStatements + const effectiveContext = { + ...apiQuery.context, + ...SqlSetStatement.getContextFromText(apiQuery.query || ''), + }; + if (engine === 'sql-native') { - apiQuery.context.engine ??= 'native'; + if (typeof effectiveContext.engine === 'undefined') { + apiQuery.context.engine = 'native'; + } } let cancelQueryId: string | undefined; if (engine === 'sql-native' || engine === 'sql-msq-dart') { - cancelQueryId = apiQuery.context.sqlQueryId; + cancelQueryId = effectiveContext.sqlQueryId; if (!cancelQueryId) { // If the sqlQueryId is not explicitly set on the context generate one, so it is possible to cancel the query. apiQuery.context.sqlQueryId = cancelQueryId = makeQueryId(); @@ -566,22 +603,40 @@ export class WorkbenchQuery { } if (engine === 'sql-msq-task') { - apiQuery.context.executionMode ??= 'async'; + if (typeof effectiveContext.executionMode === 'undefined') { + apiQuery.context.executionMode = 'async'; + } + if (ingestQuery) { // Alter these defaults for ingest queries if unset - apiQuery.context.finalizeAggregations ??= false; - apiQuery.context.groupByEnableMultiValueUnnesting ??= false; - apiQuery.context.waitUntilSegmentsLoad ??= true; + if (typeof effectiveContext.finalizeAggregations === 'undefined') { + apiQuery.context.finalizeAggregations = false; + } + if (typeof effectiveContext.groupByEnableMultiValueUnnesting === 'undefined') { + apiQuery.context.groupByEnableMultiValueUnnesting = false; + } + if (typeof effectiveContext.waitUntilSegmentsLoad === 'undefined') { + apiQuery.context.waitUntilSegmentsLoad = true; + } } } if (engine === 'sql-native' || engine === 'sql-msq-task') { - apiQuery.context.sqlStringifyArrays ??= false; + if (typeof effectiveContext.sqlStringifyArrays === 'undefined') { + apiQuery.context.sqlStringifyArrays = false; + } } if (engine === 'sql-msq-dart') { - apiQuery.context.engine = 'msq-dart'; - apiQuery.context.fullReport ??= true; + if (typeof effectiveContext.engine === 'undefined') { + apiQuery.context.engine = 'msq-dart'; + } + if (typeof effectiveContext.fullReport === 'undefined') { + apiQuery.context.fullReport = true; + } + if (typeof effectiveContext.liveReportCounters === 'undefined') { + apiQuery.context.liveReportCounters = true; + } } if (Array.isArray(queryParameters) && queryParameters.length) { diff --git a/web-console/src/utils/query-manager/query-manager.ts b/web-console/src/utils/query-manager/query-manager.ts index 5c1a859758cd..c802b8014ce7 100644 --- a/web-console/src/utils/query-manager/query-manager.ts +++ b/web-console/src/utils/query-manager/query-manager.ts @@ -25,12 +25,19 @@ import { IntermediateQueryState } from './intermediate-query-state'; import { QueryState } from './query-state'; import { ResultWithAuxiliaryWork } from './result-with-auxiliary-work'; +export interface ProcessQueryExtra { + setIntermediateQuery: (intermediateQuery: any) => void; + setIntermediateStateCallback: ( + intermediateStateCallback: (signal: AbortSignal) => Promise, + ) => void; +} + export interface QueryManagerOptions { initState?: QueryState; processQuery: ( query: Q, signal: AbortSignal, - setIntermediateQuery: (intermediateQuery: any) => void, + extra: ProcessQueryExtra, ) => Promise | ResultWithAuxiliaryWork>; backgroundStatusCheck?: ( state: I, @@ -56,7 +63,7 @@ export class QueryManager { private readonly processQuery: ( query: Q, signal: AbortSignal, - setIntermediateQuery: (intermediateQuery: any) => void, + extra: ProcessQueryExtra, ) => Promise | ResultWithAuxiliaryWork>; private readonly backgroundStatusCheck?: ( @@ -130,8 +137,56 @@ export class QueryManager { const query = this.lastQuery; let data: R | IntermediateQueryState | ResultWithAuxiliaryWork; try { - data = await this.processQuery(query, signal, (intermediateQuery: any) => { - this.lastIntermediateQuery = intermediateQuery; + data = await this.processQuery(query, signal, { + setIntermediateQuery: (intermediateQuery: any) => { + this.lastIntermediateQuery = intermediateQuery; + }, + setIntermediateStateCallback: intermediateStateCallback => { + let backgroundChecks = 0; + let intermediateError: Error | undefined; + + void (async () => { + while (!signal.aborted && this.currentQueryId === myQueryId) { + try { + const delay = + backgroundChecks > 0 + ? this.backgroundStatusCheckDelay + : this.backgroundStatusCheckInitDelay; + + if (delay) { + await wait(delay); + if (signal.aborted || this.currentQueryId !== myQueryId) return; + } + + const intermediate = await intermediateStateCallback(signal); + + if (signal.aborted || this.currentQueryId !== myQueryId || !this.state.loading) { + return; + } + + this.setState( + new QueryState({ + loading: true, + intermediate, + intermediateError, + lastData: this.state.getSomeData(), + }), + ); + + intermediateError = undefined; // Clear the intermediate error if there was one + } catch (e) { + if (signal.aborted || this.currentQueryId !== myQueryId) return; + if (this.swallowBackgroundError?.(e)) { + intermediateError = e; + } else { + return; // Stop the loop on unrecoverable error + } + } + + backgroundChecks++; + } + })(); + }, }); } catch (e) { if (this.currentQueryId !== myQueryId) return; diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx index 463991a806a6..49e0f0e29966 100644 --- a/web-console/src/views/datasources-view/datasources-view.tsx +++ b/web-console/src/views/datasources-view/datasources-view.tsx @@ -435,7 +435,7 @@ GROUP BY 1, 2`; processQuery: async ( { capabilities, visibleColumns, showUnused }, signal, - setIntermediateQuery, + { setIntermediateQuery }, ) => { let datasources: DatasourceQueryResultRow[]; if (capabilities.hasSql()) { diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx index 67e6bbba5695..11e89818d34b 100644 --- a/web-console/src/views/segments-view/segments-view.tsx +++ b/web-console/src/views/segments-view/segments-view.tsx @@ -308,7 +308,7 @@ export class SegmentsView extends React.PureComponent { + processQuery: async (query: SegmentsQuery, signal, { setIntermediateQuery }) => { const { page, pageSize, filtered, sorted, visibleColumns, capabilities, groupByInterval } = query; diff --git a/web-console/src/views/supervisors-view/supervisors-view.tsx b/web-console/src/views/supervisors-view/supervisors-view.tsx index 498362469683..294e86d81e9a 100644 --- a/web-console/src/views/supervisors-view/supervisors-view.tsx +++ b/web-console/src/views/supervisors-view/supervisors-view.tsx @@ -282,7 +282,7 @@ export class SupervisorsView extends React.PureComponent< processQuery: async ( { capabilities, visibleColumns, filtered, sorted, page, pageSize }, signal, - setIntermediateQuery, + { setIntermediateQuery }, ) => { let supervisors: SupervisorQueryResultRow[]; let count = -1; diff --git a/web-console/src/views/tasks-view/tasks-view.tsx b/web-console/src/views/tasks-view/tasks-view.tsx index bb064c7dab4d..0a866194faa6 100644 --- a/web-console/src/views/tasks-view/tasks-view.tsx +++ b/web-console/src/views/tasks-view/tasks-view.tsx @@ -715,6 +715,7 @@ ORDER BY )} {executionDialogOpen && ( { onFiltersChange(TableFilters.eq({ task_id: taskId })); diff --git a/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.tsx b/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.tsx index 2160bbbf654c..007e3eca1032 100644 --- a/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.tsx +++ b/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.tsx @@ -29,7 +29,6 @@ import { useClock, useInterval, useQueryManager } from '../../../hooks'; import { Api, AppToaster } from '../../../singletons'; import { formatDuration, prettyFormatIsoDate } from '../../../utils'; import { CancelQueryDialog } from '../cancel-query-dialog/cancel-query-dialog'; -import { DartDetailsDialog } from '../dart-details-dialog/dart-details-dialog'; import { getMsqDartVersion, WORK_STATE_STORE } from '../work-state-store'; import './current-dart-panel.scss'; @@ -48,24 +47,22 @@ function stateToIconAndColor(status: DartQueryEntry['state']): [IconName, string } export interface CurrentViberPanelProps { + onExecutionDetails(id: string): void; onClose(): void; } export const CurrentDartPanel = React.memo(function CurrentViberPanel( props: CurrentViberPanelProps, ) { - const { onClose } = props; + const { onExecutionDetails, onClose } = props; - const [showSql, setShowSql] = useState(); const [confirmCancelId, setConfirmCancelId] = useState(); const [dartQueryEntriesState, queryManager] = useQueryManager({ query: useStore(WORK_STATE_STORE, getMsqDartVersion), processQuery: async (_, signal) => { - return ( - (await Api.instance.get('/druid/v2/sql/queries', { signal })).data - .queries as DartQueryEntry[] - ).filter(q => q.engine === 'msq-dart'); + return (await Api.instance.get('/druid/v2/sql/queries', { signal })).data + .queries as DartQueryEntry[]; }, }); @@ -89,9 +86,9 @@ export const CurrentDartPanel = React.memo(function CurrentViberPanel( { - setShowSql(w.sql); + onExecutionDetails(w.sqlQueryId); }} /> -
setShowSql(w.sql)}> -
+
onExecutionDetails(w.sqlQueryId)}> +
setConfirmCancelId(undefined)} /> )} - {showSql && setShowSql(undefined)} />}
); }); diff --git a/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.scss b/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.scss deleted file mode 100644 index f1f380dc4ec8..000000000000 --- a/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.scss +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 '../../../variables'; - -.dart-details-dialog { - &.#{$bp-ns}-dialog { - width: 95vw; - } - - .#{$bp-ns}-dialog-body { - height: 70vh; - position: relative; - margin: 0; - - .flexible-query-input { - height: 100%; - } - } -} diff --git a/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.tsx b/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.tsx deleted file mode 100644 index 0637d6b9644b..000000000000 --- a/web-console/src/views/workbench-view/dart-details-dialog/dart-details-dialog.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 { Button, Classes, Dialog } from '@blueprintjs/core'; -import React from 'react'; - -import { FlexibleQueryInput } from '../flexible-query-input/flexible-query-input'; - -import './dart-details-dialog.scss'; - -export interface DartDetailsDialogProps { - sql: string; - onClose(): void; -} - -export const DartDetailsDialog = React.memo(function DartDetailsDialog( - props: DartDetailsDialogProps, -) { - const { sql, onClose } = props; - - return ( - -
- -
-
-
-
-
-
- ); -}); diff --git a/web-console/src/views/workbench-view/execution-details-dialog/execution-details-dialog.tsx b/web-console/src/views/workbench-view/execution-details-dialog/execution-details-dialog.tsx index 8c9de297c6ad..9f390fac2ca3 100644 --- a/web-console/src/views/workbench-view/execution-details-dialog/execution-details-dialog.tsx +++ b/web-console/src/views/workbench-view/execution-details-dialog/execution-details-dialog.tsx @@ -26,6 +26,7 @@ import { ExecutionDetailsPaneLoader } from '../execution-details-pane-loader/exe import './execution-details-dialog.scss'; export interface ExecutionDetailsDialogProps { + type: 'task' | 'dart'; id: string; initTab?: ExecutionDetailsTab; initExecution?: Execution; @@ -36,12 +37,13 @@ export interface ExecutionDetailsDialogProps { export const ExecutionDetailsDialog = React.memo(function ExecutionDetailsDialog( props: ExecutionDetailsDialogProps, ) { - const { id, initTab, initExecution, goToTask, onClose } = props; + const { type, id, initTab, initExecution, goToTask, onClose } = props; return (
{ + const { data } = await Api.instance.get( + `/druid/v2/sql/queries/${Api.encodePath(sqlQueryId)}/reports`, + { signal }, + ); + + return Execution.fromDartReport(data.report).changeSqlQuery(data.query.sql); +} + export interface ExecutionDetailsPaneLoaderProps { + type: 'task' | 'dart'; id: string; initTab?: ExecutionDetailsTab; initExecution?: Execution; @@ -36,13 +47,17 @@ export interface ExecutionDetailsPaneLoaderProps { export const ExecutionDetailsPaneLoader = React.memo(function ExecutionDetailsPaneLoader( props: ExecutionDetailsPaneLoaderProps, ) { - const { id, initTab, initExecution, goToTask } = props; + const { type, id, initTab, initExecution, goToTask } = props; const [executionState, queryManager] = useQueryManager({ initQuery: initExecution ? undefined : id, initState: initExecution ? new QueryState({ data: initExecution }) : undefined, processQuery: (id, signal) => { - return getTaskExecution(id, undefined, signal); + if (type === 'task') { + return getTaskExecution(id, undefined, signal); + } else { + return getDartExecution(id, signal); + } }, }); @@ -50,7 +65,7 @@ export const ExecutionDetailsPaneLoader = React.memo(function ExecutionDetailsPa const execution = executionState.data; if (!execution) return; if (execution.isWaitingForQuery()) { - queryManager.runQuery(execution.id); + queryManager.rerunLastQuery(); } }, 1000); diff --git a/web-console/src/views/workbench-view/execution-details-pane/__snapshots__/execution-details-pane.spec.tsx.snap b/web-console/src/views/workbench-view/execution-details-pane/__snapshots__/execution-details-pane.spec.tsx.snap index 308b030d19f1..4a27ae98ca24 100644 --- a/web-console/src/views/workbench-view/execution-details-pane/__snapshots__/execution-details-pane.spec.tsx.snap +++ b/web-console/src/views/workbench-view/execution-details-pane/__snapshots__/execution-details-pane.spec.tsx.snap @@ -77,8 +77,8 @@ exports[`ExecutionDetailsPane matches snapshot no init tab 1`] = ` > 0:00:04 + (starting at - (starting at {execution.startTime && !!execution.duration && (

- Query took {formatDurationWithMsIfNeeded(execution.duration)}{' '} - (starting at {prettyFormatIsoDate(execution.startTime)}) + {execution.status === 'RUNNING' ? 'Query is running for ' : 'Query took '} + {formatDurationWithMsIfNeeded(execution.duration)} (starting at{' '} + {prettyFormatIsoDate(execution.startTime)})

)} {execution.destination && ( diff --git a/web-console/src/views/workbench-view/execution-progress-bar-pane/execution-progress-bar-pane.tsx b/web-console/src/views/workbench-view/execution-progress-bar-pane/execution-progress-bar-pane.tsx index 4520c20a39b1..b8edf41ff698 100644 --- a/web-console/src/views/workbench-view/execution-progress-bar-pane/execution-progress-bar-pane.tsx +++ b/web-console/src/views/workbench-view/execution-progress-bar-pane/execution-progress-bar-pane.tsx @@ -49,7 +49,7 @@ export const ExecutionProgressBarPane = React.memo(function ExecutionProgressBar } const idx = stages ? stages.currentStageIndex() : -1; - const waitingForSegments = stages && !execution.isWaitingForQuery(); + const waitingForSegments = execution?.isWaitingForSegments(); const segmentStatusDescription = execution?.getSegmentStatusDescription(); diff --git a/web-console/src/views/workbench-view/query-tab/query-tab.tsx b/web-console/src/views/workbench-view/query-tab/query-tab.tsx index 0bbc26024d9f..8ec233b6a817 100644 --- a/web-console/src/views/workbench-view/query-tab/query-tab.tsx +++ b/web-console/src/views/workbench-view/query-tab/query-tab.tsx @@ -200,7 +200,7 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) { >({ initQuery: cachedExecutionState ? undefined : currentRunningPromise || query.getLastExecution(), initState: cachedExecutionState, - processQuery: async (q, signal) => { + processQuery: async (q, signal, { setIntermediateStateCallback }) => { if (q instanceof WorkbenchQuery) { ExecutionStateCache.deleteState(id); const { engine, query, prefixLines, cancelQueryId } = q.getApiQuery(); @@ -280,6 +280,15 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) { .delete(`/druid/v2/sql/${Api.encodePath(cancelQueryId)}`) .catch(() => {}); }); + + setIntermediateStateCallback(async signal => { + const { data } = await Api.instance.get( + `/druid/v2/sql/queries/${Api.encodePath(cancelQueryId)}/reports`, + { signal }, + ); + + return Execution.fromDartReport(data.report); + }); } onQueryChange(props.query.changeLastExecution(undefined)); diff --git a/web-console/src/views/workbench-view/workbench-view.tsx b/web-console/src/views/workbench-view/workbench-view.tsx index 16a9f77caae2..9fdd778b9c13 100644 --- a/web-console/src/views/workbench-view/workbench-view.tsx +++ b/web-console/src/views/workbench-view/workbench-view.tsx @@ -150,7 +150,12 @@ export interface WorkbenchViewState { columnMetadataState: QueryState; - details?: { id: string; initTab?: ExecutionDetailsTab; initExecution?: Execution }; + details?: { + type: 'task' | 'dart'; + id: string; + initTab?: ExecutionDetailsTab; + initExecution?: Execution; + }; connectExternalDataDialogOpen: boolean; explainDialogOpen: boolean; @@ -293,9 +298,15 @@ export class WorkbenchView extends React.PureComponent { + private readonly handleDetailsWithTaskId = (id: string, initTab?: ExecutionDetailsTab) => { this.setState({ - details: { id, initTab }, + details: { type: 'task', id, initTab }, + }); + }; + + private readonly handleDetailsWithSqlId = (id: string, initTab?: ExecutionDetailsTab) => { + this.setState({ + details: { type: 'dart', id, initTab }, }); }; @@ -308,7 +319,12 @@ export class WorkbenchView extends React.PureComponent { this.setState({ - details: { id: execution.id, initExecution: execution, initTab }, + details: { + type: execution.engine === 'sql-msq-dart' ? 'dart' : 'task', + id: execution.id, + initExecution: execution, + initTab, + }, }); }; @@ -344,6 +360,7 @@ export class WorkbenchView extends React.PureComponent { this.setState({ details: { + type: 'task', id: execution.id, initExecution: execution, }, @@ -968,13 +986,16 @@ export class WorkbenchView extends React.PureComponent )} {showCurrentDartPanel && ( - + )}
)}