in web-console/src/views/workbench-view/result-table-pane/result-table-pane.tsx [157:590]
function getHeaderMenu(column: Column, headerIndex: number) {
const header = column.name;
const type = column.sqlType || column.nativeType;
const ref = C(header);
const prettyRef = prettyPrintSql(ref);
const menuItems: JSX.Element[] = [];
if (parsedQuery) {
const noStar = !parsedQuery.hasStarInSelect();
const selectExpression = parsedQuery.getSelectExpressionForIndex(headerIndex);
const orderByExpression = parsedQuery.isValidSelectIndex(headerIndex)
? SqlLiteral.index(headerIndex)
: ref;
const descOrderBy = orderByExpression.toOrderByExpression('DESC');
const ascOrderBy = orderByExpression.toOrderByExpression('ASC');
const orderBy = parsedQuery.getOrderByForSelectIndex(headerIndex);
if (orderBy) {
const reverseOrderBy = orderBy.reverseDirection();
const reverseOrderByDirection = reverseOrderBy.getEffectiveDirection();
menuItems.push(
<MenuItem
key="order"
icon={reverseOrderByDirection === 'ASC' ? IconNames.SORT_ASC : IconNames.SORT_DESC}
text={`Order ${reverseOrderByDirection === 'ASC' ? 'ascending' : 'descending'}`}
onClick={() => {
handleQueryAction(q => q.changeOrderByExpressions([reverseOrderBy]));
}}
/>,
);
} else {
menuItems.push(
<MenuItem
key="order_desc"
icon={IconNames.SORT_DESC}
text="Order descending"
onClick={() => {
handleQueryAction(q => q.changeOrderByExpressions([descOrderBy]));
}}
/>,
<MenuItem
key="order_asc"
icon={IconNames.SORT_ASC}
text="Order ascending"
onClick={() => {
handleQueryAction(q => q.changeOrderByExpressions([ascOrderBy]));
}}
/>,
);
}
// Expression changers
if (selectExpression) {
const underlyingExpression = selectExpression.getUnderlyingExpression();
const columnValues = queryResult.getColumnByIndex(headerIndex)!;
// Remove outer function
if (underlyingExpression instanceof SqlFunction && underlyingExpression.getArg(0)) {
menuItems.push(
<MenuItem
key="uncast"
icon={IconNames.CROSS}
text={`Remove outer ${underlyingExpression.getEffectiveFunctionName()} function`}
onClick={() => {
if (!selectExpression || !underlyingExpression) return;
handleQueryAction(q =>
q.changeSelect(
headerIndex,
underlyingExpression.getArg(0)!.setAlias(selectExpression.getOutputName()),
),
);
}}
/>,
);
}
// Add a CAST
menuItems.push(
<MenuItem key="cast" icon={IconNames.EXCHANGE} text="Cast to...">
{filterMap(CAST_TARGETS, asType => {
if (asType === column.sqlType) return;
return (
<MenuItem
key={asType}
text={asType}
onClick={() => {
if (!selectExpression) return;
handleQueryAction(q =>
q.changeSelect(
headerIndex,
underlyingExpression
.cast(asType)
.setAlias(selectExpression.getOutputName()),
),
);
}}
/>
);
})}
</MenuItem>,
);
// Parse as JSON
if (column.nativeType === 'STRING' && queryResult.getNumResults()) {
if (columnValues.every(isStringJsonObject)) {
menuItems.push(
<MenuItem
key="parse_json"
icon={IconNames.DIAGRAM_TREE}
text="Parse as JSON"
onClick={() => {
if (!selectExpression) return;
handleQueryAction(q =>
q.changeSelect(
headerIndex,
F('TRY_PARSE_JSON', underlyingExpression).setAlias(
selectExpression.getOutputName(),
),
),
);
}}
/>,
);
} else if (columnValues.every(isStringJsonString)) {
menuItems.push(
<MenuItem
key="unquote_json_string"
icon={IconNames.DOCUMENT_SHARE}
text="Unquote JSON string"
onClick={() => {
if (!selectExpression) return;
handleQueryAction(q =>
q.changeSelect(
headerIndex,
SqlFunction.jsonValue(
F('TRY_PARSE_JSON', underlyingExpression),
'$',
SqlType.VARCHAR,
).setAlias(selectExpression.getOutputName()),
),
);
}}
/>,
);
}
}
// Extract a JSON path
if (column.nativeType === 'COMPLEX<json>' && queryResult.getNumResults()) {
const paths = getJsonPaths(
filterMap(columnValues, (v: any) => {
// Strangely, multi-stage-query-engine and broker deal with JSON differently
if (v && typeof v === 'object') return v;
try {
return JSONBig.parse(v);
} catch {
return;
}
}),
);
if (paths.length) {
menuItems.push(
<MenuItem key="json_value" icon={IconNames.DIAGRAM_TREE} text="Get JSON value for...">
{paths.map(path => {
return (
<MenuItem
key={path}
text={path}
onClick={() => {
if (!selectExpression) return;
handleQueryAction(q =>
q.addSelect(
SqlFunction.jsonValue(underlyingExpression, path).setAlias(
selectExpression.getOutputName() + path.replace(/^\$/, ''),
),
{ insertIndex: headerIndex + 1 },
),
);
}}
/>
);
})}
</MenuItem>,
);
}
}
}
if (parsedQuery.isRealOutputColumnAtSelectIndex(headerIndex)) {
const whereExpression = parsedQuery.getWhereExpression();
if (whereExpression && whereExpression.containsColumnName(header)) {
menuItems.push(
<MenuItem
key="remove_where"
icon={IconNames.FILTER_REMOVE}
text="Remove from WHERE clause"
onClick={() => {
handleQueryAction(q =>
q.changeWhereExpression(whereExpression.removeColumnFromAnd(header)),
);
}}
/>,
);
}
const havingExpression = parsedQuery.getHavingExpression();
if (havingExpression && havingExpression.containsColumnName(header)) {
menuItems.push(
<MenuItem
key="remove_having"
icon={IconNames.FILTER_REMOVE}
text="Remove from HAVING clause"
onClick={() => {
handleQueryAction(q =>
q.changeHavingExpression(havingExpression.removeColumnFromAnd(header)),
);
}}
/>,
);
}
}
if (noStar) {
menuItems.push(
<MenuItem
key="edit_column"
icon={IconNames.EDIT}
text="Edit column"
onClick={() => {
setEditingColumn(headerIndex);
}}
/>,
);
}
if (noStar && selectExpression) {
if (column.isTimeColumn()) {
menuItems.push(
<TimeFloorMenuItem
key="time_floor"
expression={selectExpression}
onChange={expression => {
handleQueryAction(q => q.changeSelect(headerIndex, expression));
}}
/>,
);
} else if (column.sqlType === 'TIMESTAMP') {
menuItems.push(
<MenuItem
key="declare_time"
icon={IconNames.TIME}
text="Use as the primary time column"
onClick={() => {
handleQueryAction(q =>
q.changeSelect(headerIndex, selectExpression.as(TIME_COLUMN)),
);
}}
/>,
);
} else {
// Not a time column -------------------------------------------
const values = queryResult.rows.map(row => row[headerIndex]);
const possibleDruidFormat = possibleDruidFormatForValues(values);
const formatSql = possibleDruidFormat ? timeFormatToSql(possibleDruidFormat) : undefined;
if (formatSql) {
const newSelectExpression = formatSql.fillPlaceholders([
selectExpression.getUnderlyingExpression(),
]);
menuItems.push(
<MenuItem
key="parse_time"
icon={IconNames.TIME}
text={`Time parse as '${possibleDruidFormat}' and use as the primary time column`}
onClick={() => {
handleQueryAction(q =>
q.changeSelect(headerIndex, newSelectExpression.as(TIME_COLUMN)),
);
}}
/>,
);
}
if (parsedQuery.hasGroupBy()) {
if (parsedQuery.isGroupedOutputColumn(header)) {
const convertToAggregate = (aggregate: SqlExpression) => {
handleQueryAction(q =>
q.removeOutputColumn(header).addSelect(aggregate, {
insertIndex: 'last',
}),
);
};
const underlyingSelectExpression = selectExpression.getUnderlyingExpression();
menuItems.push(
<MenuItem
key="convert_to_aggregate"
icon={IconNames.EXCHANGE}
text="Convert to aggregate"
>
{oneOf(type, 'LONG', 'FLOAT', 'DOUBLE', 'BIGINT') && (
<>
<MenuItem
text="Convert to SUM(...)"
onClick={() => {
convertToAggregate(F.sum(underlyingSelectExpression).as(`sum_${header}`));
}}
/>
<MenuItem
text="Convert to MIN(...)"
onClick={() => {
convertToAggregate(F.min(underlyingSelectExpression).as(`min_${header}`));
}}
/>
<MenuItem
text="Convert to MAX(...)"
onClick={() => {
convertToAggregate(F.max(underlyingSelectExpression).as(`max_${header}`));
}}
/>
</>
)}
<MenuItem
text="Convert to COUNT(DISTINCT ...)"
onClick={() => {
convertToAggregate(
F.countDistinct(underlyingSelectExpression).as(`unique_${header}`),
);
}}
/>
<MenuItem
text="Convert to APPROX_COUNT_DISTINCT_DS_HLL(...)"
onClick={() => {
convertToAggregate(
F('APPROX_COUNT_DISTINCT_DS_HLL', underlyingSelectExpression).as(
`unique_${header}`,
),
);
}}
/>
<MenuItem
text="Convert to APPROX_COUNT_DISTINCT_DS_THETA(...)"
onClick={() => {
convertToAggregate(
F('APPROX_COUNT_DISTINCT_DS_THETA', underlyingSelectExpression).as(
`unique_${header}`,
),
);
}}
/>
</MenuItem>,
);
} else {
const groupByExpression = convertToGroupByExpression(selectExpression);
if (groupByExpression) {
menuItems.push(
<MenuItem
key="convert_to_group_by"
icon={IconNames.EXCHANGE}
text="Convert to group by"
onClick={() => {
handleQueryAction(q =>
q.removeOutputColumn(header).addSelect(groupByExpression, {
insertIndex: 'last-grouping',
addToGroupBy: 'end',
}),
);
}}
/>,
);
}
}
}
}
}
if (noStar) {
menuItems.push(
<MenuItem
key="remove_column"
icon={IconNames.CROSS}
text="Remove column"
onClick={() => {
handleQueryAction(q => q.removeOutputColumn(header));
}}
/>,
);
}
} else {
menuItems.push(
<MenuItem
key="copy_ref"
icon={IconNames.CLIPBOARD}
text={`Copy: ${prettyRef}`}
onClick={() => {
copyAndAlert(String(ref), `${prettyRef}' copied to clipboard`);
}}
/>,
);
if (!runeMode) {
const orderByExpression = ref;
const descOrderBy = orderByExpression.toOrderByExpression('DESC');
const ascOrderBy = orderByExpression.toOrderByExpression('ASC');
const descOrderByPretty = prettyPrintSql(descOrderBy);
const ascOrderByPretty = prettyPrintSql(descOrderBy);
menuItems.push(
<MenuItem
key="copy_desc"
icon={IconNames.CLIPBOARD}
text={`Copy: ${descOrderByPretty}`}
onClick={() =>
copyAndAlert(descOrderBy.toString(), `'${descOrderByPretty}' copied to clipboard`)
}
/>,
<MenuItem
key="copy_asc"
icon={IconNames.CLIPBOARD}
text={`Copy: ${ascOrderByPretty}`}
onClick={() =>
copyAndAlert(ascOrderBy.toString(), `'${ascOrderByPretty}' copied to clipboard`)
}
/>,
);
}
}
return <Menu>{menuItems}</Menu>;
}