mirror of https://github.com/apache/druid.git
Web console: updated the explain dialog to use new explain output (#12009)
This is the UI followup to the work done in #11908 Updated the Explain dialog to use the new output format.
This commit is contained in:
parent
ffa553593f
commit
1f95a42bb8
|
@ -18,13 +18,7 @@
|
||||||
|
|
||||||
import { sane } from 'druid-query-toolkit/build/test-utils';
|
import { sane } from 'druid-query-toolkit/build/test-utils';
|
||||||
|
|
||||||
import {
|
import { DruidError, getDruidErrorMessage, parseHtmlError, trimSemicolon } from './druid-query';
|
||||||
DruidError,
|
|
||||||
getDruidErrorMessage,
|
|
||||||
parseHtmlError,
|
|
||||||
parseQueryPlan,
|
|
||||||
trimSemicolon,
|
|
||||||
} from './druid-query';
|
|
||||||
|
|
||||||
describe('DruidQuery', () => {
|
describe('DruidQuery', () => {
|
||||||
describe('DruidError.parsePosition', () => {
|
describe('DruidError.parsePosition', () => {
|
||||||
|
@ -227,10 +221,6 @@ describe('DruidQuery', () => {
|
||||||
it('parseHtmlError', () => {
|
it('parseHtmlError', () => {
|
||||||
expect(getDruidErrorMessage({})).toMatchInlineSnapshot(`undefined`);
|
expect(getDruidErrorMessage({})).toMatchInlineSnapshot(`undefined`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parseQueryPlan', () => {
|
|
||||||
expect(parseQueryPlan('start')).toMatchInlineSnapshot(`"start"`);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.trimSemicolon', () => {
|
describe('.trimSemicolon', () => {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import { SqlRef } from 'druid-query-toolkit';
|
||||||
|
|
||||||
import { Api } from '../singletons';
|
import { Api } from '../singletons';
|
||||||
|
|
||||||
|
@ -296,73 +297,15 @@ export async function queryDruidSql<T = any>(sqlQueryPayload: Record<string, any
|
||||||
return sqlResultResp.data;
|
return sqlResultResp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BasicQueryExplanation {
|
export interface QueryExplanation {
|
||||||
query: any;
|
query: any;
|
||||||
signature: string | null;
|
signature: { name: string; type: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SemiJoinQueryExplanation {
|
export function formatSignature(queryExplanation: QueryExplanation): string {
|
||||||
mainQuery: BasicQueryExplanation;
|
return queryExplanation.signature
|
||||||
subQueryRight: BasicQueryExplanation;
|
.map(({ name, type }) => `${SqlRef.column(name)}::${type}`)
|
||||||
}
|
.join(', ');
|
||||||
|
|
||||||
function parseQueryPlanResult(queryPlanResult: string): BasicQueryExplanation {
|
|
||||||
if (!queryPlanResult) {
|
|
||||||
return {
|
|
||||||
query: null,
|
|
||||||
signature: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryAndSignature = queryPlanResult.split(', signature=');
|
|
||||||
const queryValue = new RegExp(/query=(.+)/).exec(queryAndSignature[0]);
|
|
||||||
const signatureValue = queryAndSignature[1];
|
|
||||||
|
|
||||||
let parsedQuery: any;
|
|
||||||
|
|
||||||
if (queryValue && queryValue[1]) {
|
|
||||||
try {
|
|
||||||
parsedQuery = JSON.parse(queryValue[1]);
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
query: parsedQuery || queryPlanResult,
|
|
||||||
signature: signatureValue || null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseQueryPlan(
|
|
||||||
raw: string,
|
|
||||||
): BasicQueryExplanation | SemiJoinQueryExplanation | string {
|
|
||||||
let plan: string = raw;
|
|
||||||
plan = plan.replace(/\n/g, '');
|
|
||||||
|
|
||||||
if (plan.includes('DruidOuterQueryRel(')) {
|
|
||||||
return plan; // don't know how to parse this
|
|
||||||
}
|
|
||||||
|
|
||||||
let queryArgs: string;
|
|
||||||
const queryRelFnStart = 'DruidQueryRel(';
|
|
||||||
const semiJoinFnStart = 'DruidSemiJoin(';
|
|
||||||
|
|
||||||
if (plan.startsWith(queryRelFnStart)) {
|
|
||||||
queryArgs = plan.substring(queryRelFnStart.length, plan.length - 1);
|
|
||||||
} else if (plan.startsWith(semiJoinFnStart)) {
|
|
||||||
queryArgs = plan.substring(semiJoinFnStart.length, plan.length - 1);
|
|
||||||
const leftExpressionsArgs = ', leftExpressions=';
|
|
||||||
const keysArgumentIdx = queryArgs.indexOf(leftExpressionsArgs);
|
|
||||||
if (keysArgumentIdx !== -1) {
|
|
||||||
return {
|
|
||||||
mainQuery: parseQueryPlanResult(queryArgs.substring(0, keysArgumentIdx)),
|
|
||||||
subQueryRight: parseQueryPlan(queryArgs.substring(queryArgs.indexOf(queryRelFnStart))),
|
|
||||||
} as SemiJoinQueryExplanation;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return plan;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseQueryPlanResult(queryArgs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function trimSemicolon(query: string): string {
|
export function trimSemicolon(query: string): string {
|
||||||
|
|
|
@ -86,7 +86,175 @@ exports[`ExplainDialog matches snapshot on loading 1`] = `
|
||||||
</Blueprint3.Dialog>
|
</Blueprint3.Dialog>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`ExplainDialog matches snapshot on some data 1`] = `
|
exports[`ExplainDialog matches snapshot on some data (many queries) 1`] = `
|
||||||
|
<Blueprint3.Dialog
|
||||||
|
canOutsideClickClose={true}
|
||||||
|
className="explain-dialog"
|
||||||
|
isOpen={true}
|
||||||
|
onClose={[Function]}
|
||||||
|
title="Query plan"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bp3-dialog-body"
|
||||||
|
>
|
||||||
|
<Blueprint3.Tabs
|
||||||
|
animate={true}
|
||||||
|
large={false}
|
||||||
|
renderActiveTabPanelOnly={true}
|
||||||
|
vertical={true}
|
||||||
|
>
|
||||||
|
<Blueprint3.Tab
|
||||||
|
disabled={false}
|
||||||
|
id={0}
|
||||||
|
key="0"
|
||||||
|
panel={
|
||||||
|
<div
|
||||||
|
className="query-explanation"
|
||||||
|
>
|
||||||
|
<Blueprint3.FormGroup
|
||||||
|
className="query-group"
|
||||||
|
label="Query"
|
||||||
|
>
|
||||||
|
<Blueprint3.TextArea
|
||||||
|
readOnly={true}
|
||||||
|
value="{
|
||||||
|
\\"queryType\\": \\"scan\\",
|
||||||
|
\\"dataSource\\": {
|
||||||
|
\\"type\\": \\"table\\",
|
||||||
|
\\"name\\": \\"wikipedia\\"
|
||||||
|
},
|
||||||
|
\\"intervals\\": {
|
||||||
|
\\"type\\": \\"intervals\\",
|
||||||
|
\\"intervals\\": [
|
||||||
|
\\"-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z\\"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
\\"virtualColumns\\": [],
|
||||||
|
\\"resultFormat\\": \\"compactedList\\",
|
||||||
|
\\"batchSize\\": 20480,
|
||||||
|
\\"limit\\": 101,
|
||||||
|
\\"filter\\": null,
|
||||||
|
\\"columns\\": [
|
||||||
|
\\"channel\\"
|
||||||
|
],
|
||||||
|
\\"legacy\\": false,
|
||||||
|
\\"context\\": {},
|
||||||
|
\\"descending\\": false,
|
||||||
|
\\"granularity\\": {
|
||||||
|
\\"type\\": \\"all\\"
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</Blueprint3.FormGroup>
|
||||||
|
<Blueprint3.FormGroup
|
||||||
|
className="signature-group"
|
||||||
|
label="Signature"
|
||||||
|
>
|
||||||
|
<Blueprint3.InputGroup
|
||||||
|
defaultValue="channel::STRING"
|
||||||
|
readOnly={true}
|
||||||
|
/>
|
||||||
|
</Blueprint3.FormGroup>
|
||||||
|
<Blueprint3.Button
|
||||||
|
className="open-query"
|
||||||
|
intent="primary"
|
||||||
|
minimal={true}
|
||||||
|
onClick={[Function]}
|
||||||
|
rightIcon="arrow-top-right"
|
||||||
|
text="Open query"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
title="Query 1"
|
||||||
|
/>
|
||||||
|
<Blueprint3.Tab
|
||||||
|
disabled={false}
|
||||||
|
id={1}
|
||||||
|
key="1"
|
||||||
|
panel={
|
||||||
|
<div
|
||||||
|
className="query-explanation"
|
||||||
|
>
|
||||||
|
<Blueprint3.FormGroup
|
||||||
|
className="query-group"
|
||||||
|
label="Query"
|
||||||
|
>
|
||||||
|
<Blueprint3.TextArea
|
||||||
|
readOnly={true}
|
||||||
|
value="{
|
||||||
|
\\"queryType\\": \\"scan\\",
|
||||||
|
\\"dataSource\\": {
|
||||||
|
\\"type\\": \\"table\\",
|
||||||
|
\\"name\\": \\"wikipedia\\"
|
||||||
|
},
|
||||||
|
\\"intervals\\": {
|
||||||
|
\\"type\\": \\"intervals\\",
|
||||||
|
\\"intervals\\": [
|
||||||
|
\\"-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z\\"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
\\"virtualColumns\\": [],
|
||||||
|
\\"resultFormat\\": \\"compactedList\\",
|
||||||
|
\\"batchSize\\": 20480,
|
||||||
|
\\"limit\\": 101,
|
||||||
|
\\"filter\\": {
|
||||||
|
\\"type\\": \\"selector\\",
|
||||||
|
\\"dimension\\": \\"channel\\",
|
||||||
|
\\"value\\": \\"#en.wikipedia\\",
|
||||||
|
\\"extractionFn\\": null
|
||||||
|
},
|
||||||
|
\\"columns\\": [
|
||||||
|
\\"channel\\"
|
||||||
|
],
|
||||||
|
\\"legacy\\": false,
|
||||||
|
\\"context\\": {},
|
||||||
|
\\"descending\\": false,
|
||||||
|
\\"granularity\\": {
|
||||||
|
\\"type\\": \\"all\\"
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</Blueprint3.FormGroup>
|
||||||
|
<Blueprint3.FormGroup
|
||||||
|
className="signature-group"
|
||||||
|
label="Signature"
|
||||||
|
>
|
||||||
|
<Blueprint3.InputGroup
|
||||||
|
defaultValue="channel::STRING"
|
||||||
|
readOnly={true}
|
||||||
|
/>
|
||||||
|
</Blueprint3.FormGroup>
|
||||||
|
<Blueprint3.Button
|
||||||
|
className="open-query"
|
||||||
|
intent="primary"
|
||||||
|
minimal={true}
|
||||||
|
onClick={[Function]}
|
||||||
|
rightIcon="arrow-top-right"
|
||||||
|
text="Open query"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
title="Query 2"
|
||||||
|
/>
|
||||||
|
<Expander />
|
||||||
|
</Blueprint3.Tabs>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="bp3-dialog-footer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bp3-dialog-footer-actions"
|
||||||
|
>
|
||||||
|
<Blueprint3.Button
|
||||||
|
onClick={[Function]}
|
||||||
|
text="Close"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Blueprint3.Dialog>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`ExplainDialog matches snapshot on some data (one query) 1`] = `
|
||||||
<Blueprint3.Dialog
|
<Blueprint3.Dialog
|
||||||
canOutsideClickClose={true}
|
canOutsideClickClose={true}
|
||||||
className="explain-dialog"
|
className="explain-dialog"
|
||||||
|
@ -98,9 +266,10 @@ exports[`ExplainDialog matches snapshot on some data 1`] = `
|
||||||
className="bp3-dialog-body"
|
className="bp3-dialog-body"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="one-query"
|
className="query-explanation"
|
||||||
>
|
>
|
||||||
<Blueprint3.FormGroup
|
<Blueprint3.FormGroup
|
||||||
|
className="query-group"
|
||||||
label="Query"
|
label="Query"
|
||||||
>
|
>
|
||||||
<Blueprint3.TextArea
|
<Blueprint3.TextArea
|
||||||
|
@ -108,19 +277,71 @@ exports[`ExplainDialog matches snapshot on some data 1`] = `
|
||||||
value="{
|
value="{
|
||||||
\\"queryType\\": \\"topN\\",
|
\\"queryType\\": \\"topN\\",
|
||||||
\\"dataSource\\": {
|
\\"dataSource\\": {
|
||||||
\\"type\\": \\"table\\",
|
\\"type\\": \\"join\\",
|
||||||
\\"name\\": \\"kttm-multi-day\\"
|
\\"left\\": {
|
||||||
|
\\"type\\": \\"table\\",
|
||||||
|
\\"name\\": \\"wikipedia\\"
|
||||||
|
},
|
||||||
|
\\"right\\": {
|
||||||
|
\\"type\\": \\"query\\",
|
||||||
|
\\"query\\": {
|
||||||
|
\\"queryType\\": \\"groupBy\\",
|
||||||
|
\\"dataSource\\": {
|
||||||
|
\\"type\\": \\"table\\",
|
||||||
|
\\"name\\": \\"wikipedia\\"
|
||||||
|
},
|
||||||
|
\\"intervals\\": {
|
||||||
|
\\"type\\": \\"intervals\\",
|
||||||
|
\\"intervals\\": [
|
||||||
|
\\"-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z\\"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
\\"virtualColumns\\": [],
|
||||||
|
\\"filter\\": {
|
||||||
|
\\"type\\": \\"selector\\",
|
||||||
|
\\"dimension\\": \\"channel\\",
|
||||||
|
\\"value\\": \\"#en.wikipedia\\",
|
||||||
|
\\"extractionFn\\": null
|
||||||
|
},
|
||||||
|
\\"granularity\\": {
|
||||||
|
\\"type\\": \\"all\\"
|
||||||
|
},
|
||||||
|
\\"dimensions\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": \\"default\\",
|
||||||
|
\\"dimension\\": \\"channel\\",
|
||||||
|
\\"outputName\\": \\"d0\\",
|
||||||
|
\\"outputType\\": \\"STRING\\"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"aggregations\\": [],
|
||||||
|
\\"postAggregations\\": [],
|
||||||
|
\\"having\\": null,
|
||||||
|
\\"limitSpec\\": {
|
||||||
|
\\"type\\": \\"NoopLimitSpec\\"
|
||||||
|
},
|
||||||
|
\\"context\\": {},
|
||||||
|
\\"descending\\": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
\\"rightPrefix\\": \\"j0.\\",
|
||||||
|
\\"condition\\": \\"(\\\\\\"channel\\\\\\" == \\\\\\"j0.d0\\\\\\")\\",
|
||||||
|
\\"joinType\\": \\"LEFT\\",
|
||||||
|
\\"leftFilter\\": null
|
||||||
},
|
},
|
||||||
\\"virtualColumns\\": [],
|
\\"virtualColumns\\": [],
|
||||||
\\"dimension\\": {
|
\\"dimension\\": {
|
||||||
\\"type\\": \\"default\\",
|
\\"type\\": \\"default\\",
|
||||||
\\"dimension\\": \\"browser\\",
|
\\"dimension\\": \\"channel\\",
|
||||||
\\"outputName\\": \\"d0\\",
|
\\"outputName\\": \\"d0\\",
|
||||||
\\"outputType\\": \\"STRING\\"
|
\\"outputType\\": \\"STRING\\"
|
||||||
},
|
},
|
||||||
\\"metric\\": {
|
\\"metric\\": {
|
||||||
\\"type\\": \\"numeric\\",
|
\\"type\\": \\"dimension\\",
|
||||||
\\"metric\\": \\"a0\\"
|
\\"previousStop\\": null,
|
||||||
|
\\"ordering\\": {
|
||||||
|
\\"type\\": \\"lexicographic\\"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
\\"threshold\\": 101,
|
\\"threshold\\": 101,
|
||||||
\\"intervals\\": {
|
\\"intervals\\": {
|
||||||
|
@ -140,22 +361,28 @@ exports[`ExplainDialog matches snapshot on some data 1`] = `
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"postAggregations\\": [],
|
\\"postAggregations\\": [],
|
||||||
\\"context\\": {
|
\\"context\\": {},
|
||||||
\\"sqlOuterLimit\\": 101,
|
|
||||||
\\"sqlQueryId\\": \\"5905fe8d-9a91-41e0-8f3a-d7a8ac21dce6\\"
|
|
||||||
},
|
|
||||||
\\"descending\\": false
|
\\"descending\\": false
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</Blueprint3.FormGroup>
|
</Blueprint3.FormGroup>
|
||||||
<Blueprint3.FormGroup
|
<Blueprint3.FormGroup
|
||||||
|
className="signature-group"
|
||||||
label="Signature"
|
label="Signature"
|
||||||
>
|
>
|
||||||
<Blueprint3.InputGroup
|
<Blueprint3.InputGroup
|
||||||
defaultValue="[{d0:STRING, a0:LONG}]"
|
defaultValue="d0::STRING, a0::LONG"
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
/>
|
/>
|
||||||
</Blueprint3.FormGroup>
|
</Blueprint3.FormGroup>
|
||||||
|
<Blueprint3.Button
|
||||||
|
className="open-query"
|
||||||
|
intent="primary"
|
||||||
|
minimal={true}
|
||||||
|
onClick={[Function]}
|
||||||
|
rightIcon="arrow-top-right"
|
||||||
|
text="Open query"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -168,11 +395,6 @@ exports[`ExplainDialog matches snapshot on some data 1`] = `
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
text="Close"
|
text="Close"
|
||||||
/>
|
/>
|
||||||
<Blueprint3.Button
|
|
||||||
intent="primary"
|
|
||||||
onClick={[Function]}
|
|
||||||
text="Open query"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Blueprint3.Dialog>
|
</Blueprint3.Dialog>
|
||||||
|
|
|
@ -18,30 +18,54 @@
|
||||||
|
|
||||||
.explain-dialog {
|
.explain-dialog {
|
||||||
&.bp3-dialog {
|
&.bp3-dialog {
|
||||||
width: 600px;
|
width: 800px;
|
||||||
|
height: 90vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bp3-dialog-body {
|
.bp3-dialog-body {
|
||||||
min-height: 70vh;
|
.bp3-tabs {
|
||||||
}
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.one-query {
|
|
||||||
textarea {
|
|
||||||
height: 50vh !important;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.two-queries {
|
.bp3-tab-panel {
|
||||||
textarea {
|
flex: 1;
|
||||||
height: 25vh !important;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.generic-result {
|
.query-explanation {
|
||||||
overflow: scroll;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-group {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.bp3-form-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-query {
|
||||||
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.generic-result {
|
||||||
|
overflow: scroll;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,17 +19,11 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import { QueryExplanation, QueryState } from '../../../utils';
|
||||||
BasicQueryExplanation,
|
|
||||||
parseQueryPlan,
|
|
||||||
QueryState,
|
|
||||||
SemiJoinQueryExplanation,
|
|
||||||
} from '../../../utils';
|
|
||||||
|
|
||||||
import { ExplainDialog } from './explain-dialog';
|
import { ExplainDialog } from './explain-dialog';
|
||||||
|
|
||||||
let explainState: QueryState<BasicQueryExplanation | SemiJoinQueryExplanation | string> =
|
let explainState: QueryState<QueryExplanation[] | string> = QueryState.INIT;
|
||||||
QueryState.INIT;
|
|
||||||
|
|
||||||
jest.mock('../../../hooks', () => {
|
jest.mock('../../../hooks', () => {
|
||||||
return {
|
return {
|
||||||
|
@ -64,11 +58,184 @@ describe('ExplainDialog', () => {
|
||||||
expect(shallow(makeExplainDialog())).toMatchSnapshot();
|
expect(shallow(makeExplainDialog())).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches snapshot on some data', () => {
|
it('matches snapshot on some data (one query)', () => {
|
||||||
explainState = new QueryState({
|
explainState = new QueryState({
|
||||||
data: parseQueryPlan(
|
data: [
|
||||||
`DruidQueryRel(query=[{"queryType":"topN","dataSource":{"type":"table","name":"kttm-multi-day"},"virtualColumns":[],"dimension":{"type":"default","dimension":"browser","outputName":"d0","outputType":"STRING"},"metric":{"type":"numeric","metric":"a0"},"threshold":101,"intervals":{"type":"intervals","intervals":["-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z"]},"filter":null,"granularity":{"type":"all"},"aggregations":[{"type":"count","name":"a0"}],"postAggregations":[],"context":{"sqlOuterLimit":101,"sqlQueryId":"5905fe8d-9a91-41e0-8f3a-d7a8ac21dce6"},"descending":false}], signature=[{d0:STRING, a0:LONG}])`,
|
{
|
||||||
),
|
query: {
|
||||||
|
queryType: 'topN',
|
||||||
|
dataSource: {
|
||||||
|
type: 'join',
|
||||||
|
left: {
|
||||||
|
type: 'table',
|
||||||
|
name: 'wikipedia',
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
type: 'query',
|
||||||
|
query: {
|
||||||
|
queryType: 'groupBy',
|
||||||
|
dataSource: {
|
||||||
|
type: 'table',
|
||||||
|
name: 'wikipedia',
|
||||||
|
},
|
||||||
|
intervals: {
|
||||||
|
type: 'intervals',
|
||||||
|
intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'],
|
||||||
|
},
|
||||||
|
virtualColumns: [],
|
||||||
|
filter: {
|
||||||
|
type: 'selector',
|
||||||
|
dimension: 'channel',
|
||||||
|
value: '#en.wikipedia',
|
||||||
|
extractionFn: null,
|
||||||
|
},
|
||||||
|
granularity: {
|
||||||
|
type: 'all',
|
||||||
|
},
|
||||||
|
dimensions: [
|
||||||
|
{
|
||||||
|
type: 'default',
|
||||||
|
dimension: 'channel',
|
||||||
|
outputName: 'd0',
|
||||||
|
outputType: 'STRING',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
aggregations: [],
|
||||||
|
postAggregations: [],
|
||||||
|
having: null,
|
||||||
|
limitSpec: {
|
||||||
|
type: 'NoopLimitSpec',
|
||||||
|
},
|
||||||
|
context: {},
|
||||||
|
descending: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rightPrefix: 'j0.',
|
||||||
|
condition: '("channel" == "j0.d0")',
|
||||||
|
joinType: 'LEFT',
|
||||||
|
leftFilter: null,
|
||||||
|
},
|
||||||
|
virtualColumns: [],
|
||||||
|
dimension: {
|
||||||
|
type: 'default',
|
||||||
|
dimension: 'channel',
|
||||||
|
outputName: 'd0',
|
||||||
|
outputType: 'STRING',
|
||||||
|
},
|
||||||
|
metric: {
|
||||||
|
type: 'dimension',
|
||||||
|
previousStop: null,
|
||||||
|
ordering: {
|
||||||
|
type: 'lexicographic',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
threshold: 101,
|
||||||
|
intervals: {
|
||||||
|
type: 'intervals',
|
||||||
|
intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'],
|
||||||
|
},
|
||||||
|
filter: null,
|
||||||
|
granularity: {
|
||||||
|
type: 'all',
|
||||||
|
},
|
||||||
|
aggregations: [
|
||||||
|
{
|
||||||
|
type: 'count',
|
||||||
|
name: 'a0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
postAggregations: [],
|
||||||
|
context: {},
|
||||||
|
descending: false,
|
||||||
|
},
|
||||||
|
signature: [
|
||||||
|
{
|
||||||
|
name: 'd0',
|
||||||
|
type: 'STRING',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'a0',
|
||||||
|
type: 'LONG',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(shallow(makeExplainDialog())).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches snapshot on some data (many queries)', () => {
|
||||||
|
explainState = new QueryState({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
queryType: 'scan',
|
||||||
|
dataSource: {
|
||||||
|
type: 'table',
|
||||||
|
name: 'wikipedia',
|
||||||
|
},
|
||||||
|
intervals: {
|
||||||
|
type: 'intervals',
|
||||||
|
intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'],
|
||||||
|
},
|
||||||
|
virtualColumns: [],
|
||||||
|
resultFormat: 'compactedList',
|
||||||
|
batchSize: 20480,
|
||||||
|
limit: 101,
|
||||||
|
filter: null,
|
||||||
|
columns: ['channel'],
|
||||||
|
legacy: false,
|
||||||
|
context: {},
|
||||||
|
descending: false,
|
||||||
|
granularity: {
|
||||||
|
type: 'all',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
signature: [
|
||||||
|
{
|
||||||
|
name: 'channel',
|
||||||
|
type: 'STRING',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
queryType: 'scan',
|
||||||
|
dataSource: {
|
||||||
|
type: 'table',
|
||||||
|
name: 'wikipedia',
|
||||||
|
},
|
||||||
|
intervals: {
|
||||||
|
type: 'intervals',
|
||||||
|
intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'],
|
||||||
|
},
|
||||||
|
virtualColumns: [],
|
||||||
|
resultFormat: 'compactedList',
|
||||||
|
batchSize: 20480,
|
||||||
|
limit: 101,
|
||||||
|
filter: {
|
||||||
|
type: 'selector',
|
||||||
|
dimension: 'channel',
|
||||||
|
value: '#en.wikipedia',
|
||||||
|
extractionFn: null,
|
||||||
|
},
|
||||||
|
columns: ['channel'],
|
||||||
|
legacy: false,
|
||||||
|
context: {},
|
||||||
|
descending: false,
|
||||||
|
granularity: {
|
||||||
|
type: 'all',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
signature: [
|
||||||
|
{
|
||||||
|
name: 'channel',
|
||||||
|
type: 'STRING',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(shallow(makeExplainDialog())).toMatchSnapshot();
|
expect(shallow(makeExplainDialog())).toMatchSnapshot();
|
||||||
|
|
|
@ -23,20 +23,22 @@ import {
|
||||||
FormGroup,
|
FormGroup,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
Intent,
|
Intent,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
TextArea,
|
TextArea,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
import * as JSONBig from 'json-bigint-native';
|
import * as JSONBig from 'json-bigint-native';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Loader } from '../../../components';
|
import { Loader } from '../../../components';
|
||||||
import { useQueryManager } from '../../../hooks';
|
import { useQueryManager } from '../../../hooks';
|
||||||
import {
|
import {
|
||||||
BasicQueryExplanation,
|
formatSignature,
|
||||||
getDruidErrorMessage,
|
getDruidErrorMessage,
|
||||||
parseQueryPlan,
|
|
||||||
queryDruidSql,
|
queryDruidSql,
|
||||||
|
QueryExplanation,
|
||||||
QueryWithContext,
|
QueryWithContext,
|
||||||
SemiJoinQueryExplanation,
|
|
||||||
trimSemicolon,
|
trimSemicolon,
|
||||||
} from '../../../utils';
|
} from '../../../utils';
|
||||||
import { isEmptyContext } from '../../../utils/query-context';
|
import { isEmptyContext } from '../../../utils/query-context';
|
||||||
|
@ -63,16 +65,17 @@ export interface ExplainDialogProps {
|
||||||
export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDialogProps) {
|
export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDialogProps) {
|
||||||
const { queryWithContext, onClose, setQueryString, mandatoryQueryContext } = props;
|
const { queryWithContext, onClose, setQueryString, mandatoryQueryContext } = props;
|
||||||
|
|
||||||
const [explainState] = useQueryManager<
|
const [explainState] = useQueryManager<QueryWithContext, QueryExplanation[] | string>({
|
||||||
QueryWithContext,
|
|
||||||
BasicQueryExplanation | SemiJoinQueryExplanation | string
|
|
||||||
>({
|
|
||||||
processQuery: async (queryWithContext: QueryWithContext) => {
|
processQuery: async (queryWithContext: QueryWithContext) => {
|
||||||
const { queryString, queryContext, wrapQueryLimit } = queryWithContext;
|
const { queryString, queryContext, wrapQueryLimit } = queryWithContext;
|
||||||
|
|
||||||
let context: Record<string, any> | undefined;
|
let context: Record<string, any> | undefined;
|
||||||
if (!isEmptyContext(queryContext) || wrapQueryLimit || mandatoryQueryContext) {
|
if (!isEmptyContext(queryContext) || wrapQueryLimit || mandatoryQueryContext) {
|
||||||
context = { ...queryContext, ...(mandatoryQueryContext || {}) };
|
context = {
|
||||||
|
...queryContext,
|
||||||
|
...(mandatoryQueryContext || {}),
|
||||||
|
useNativeQueryExplain: true,
|
||||||
|
};
|
||||||
if (typeof wrapQueryLimit !== 'undefined') {
|
if (typeof wrapQueryLimit !== 'undefined') {
|
||||||
context.sqlOuterLimit = wrapQueryLimit + 1;
|
context.sqlOuterLimit = wrapQueryLimit + 1;
|
||||||
}
|
}
|
||||||
|
@ -88,88 +91,73 @@ export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDia
|
||||||
throw new Error(getDruidErrorMessage(e));
|
throw new Error(getDruidErrorMessage(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseQueryPlan(result[0]['PLAN']);
|
const plan = result[0]['PLAN'];
|
||||||
|
if (typeof plan !== 'string') {
|
||||||
|
throw new Error(`unexpected result from server`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSONBig.parse(plan);
|
||||||
|
} catch {
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
initQuery: queryWithContext,
|
initQuery: queryWithContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
let content: JSX.Element;
|
let content: JSX.Element;
|
||||||
let queryString: string | undefined;
|
|
||||||
|
|
||||||
const { loading, error: explainError, data: explainResult } = explainState;
|
const { loading, error: explainError, data: explainResult } = explainState;
|
||||||
|
|
||||||
|
function renderQueryExplanation(queryExplanation: QueryExplanation) {
|
||||||
|
const queryString = JSONBig.stringify(queryExplanation.query, undefined, 2);
|
||||||
|
return (
|
||||||
|
<div className="query-explanation">
|
||||||
|
<FormGroup className="query-group" label="Query">
|
||||||
|
<TextArea readOnly value={queryString} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup className="signature-group" label="Signature">
|
||||||
|
<InputGroup defaultValue={formatSignature(queryExplanation)} readOnly />
|
||||||
|
</FormGroup>
|
||||||
|
<Button
|
||||||
|
className="open-query"
|
||||||
|
text="Open query"
|
||||||
|
rightIcon={IconNames.ARROW_TOP_RIGHT}
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
minimal
|
||||||
|
onClick={() => {
|
||||||
|
setQueryString(queryString);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
content = <Loader />;
|
content = <Loader />;
|
||||||
} else if (explainError) {
|
} else if (explainError) {
|
||||||
content = <div>{explainError.message}</div>;
|
content = <div>{explainError.message}</div>;
|
||||||
} else if (!explainResult) {
|
} else if (!explainResult) {
|
||||||
content = <div />;
|
content = <div />;
|
||||||
} else if ((explainResult as BasicQueryExplanation).query) {
|
} else if (Array.isArray(explainResult) && explainResult.length) {
|
||||||
queryString = JSONBig.stringify(
|
if (explainResult.length === 1) {
|
||||||
(explainResult as BasicQueryExplanation).query[0],
|
content = renderQueryExplanation(explainResult[0]);
|
||||||
undefined,
|
} else {
|
||||||
2,
|
content = (
|
||||||
);
|
<Tabs animate renderActiveTabPanelOnly vertical>
|
||||||
content = (
|
{explainResult.map((queryExplanation, i) => (
|
||||||
<div className="one-query">
|
<Tab
|
||||||
<FormGroup label="Query">
|
id={i}
|
||||||
<TextArea readOnly value={queryString} />
|
key={i}
|
||||||
</FormGroup>
|
title={`Query ${i + 1}`}
|
||||||
{(explainResult as BasicQueryExplanation).signature && (
|
panel={renderQueryExplanation(queryExplanation)}
|
||||||
<FormGroup label="Signature">
|
|
||||||
<InputGroup
|
|
||||||
defaultValue={(explainResult as BasicQueryExplanation).signature || ''}
|
|
||||||
readOnly
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
))}
|
||||||
)}
|
<Tabs.Expander />
|
||||||
</div>
|
</Tabs>
|
||||||
);
|
);
|
||||||
} else if (
|
}
|
||||||
(explainResult as SemiJoinQueryExplanation).mainQuery &&
|
|
||||||
(explainResult as SemiJoinQueryExplanation).subQueryRight
|
|
||||||
) {
|
|
||||||
content = (
|
|
||||||
<div className="two-queries">
|
|
||||||
<FormGroup label="Main query">
|
|
||||||
<TextArea
|
|
||||||
readOnly
|
|
||||||
value={JSONBig.stringify(
|
|
||||||
(explainResult as SemiJoinQueryExplanation).mainQuery.query,
|
|
||||||
undefined,
|
|
||||||
2,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
{(explainResult as SemiJoinQueryExplanation).mainQuery.signature && (
|
|
||||||
<FormGroup label="Signature">
|
|
||||||
<InputGroup
|
|
||||||
defaultValue={(explainResult as SemiJoinQueryExplanation).mainQuery.signature || ''}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
<FormGroup label="Sub query">
|
|
||||||
<TextArea
|
|
||||||
readOnly
|
|
||||||
value={JSONBig.stringify(
|
|
||||||
(explainResult as SemiJoinQueryExplanation).subQueryRight.query,
|
|
||||||
undefined,
|
|
||||||
2,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
{(explainResult as SemiJoinQueryExplanation).subQueryRight.signature && (
|
|
||||||
<FormGroup label="Signature">
|
|
||||||
<InputGroup
|
|
||||||
defaultValue={
|
|
||||||
(explainResult as SemiJoinQueryExplanation).subQueryRight.signature || ''
|
|
||||||
}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
content = <div className="generic-result">{explainResult}</div>;
|
content = <div className="generic-result">{explainResult}</div>;
|
||||||
}
|
}
|
||||||
|
@ -180,16 +168,6 @@ export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDia
|
||||||
<div className={Classes.DIALOG_FOOTER}>
|
<div className={Classes.DIALOG_FOOTER}>
|
||||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||||
<Button text="Close" onClick={onClose} />
|
<Button text="Close" onClick={onClose} />
|
||||||
{queryString && (
|
|
||||||
<Button
|
|
||||||
text="Open query"
|
|
||||||
intent={Intent.PRIMARY}
|
|
||||||
onClick={() => {
|
|
||||||
if (queryString) setQueryString(queryString);
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
Loading…
Reference in New Issue