Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/platform/packages/shared/kbn-esql-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export {
constructCascadeQuery,
mutateQueryStatsGrouping,
appendFilteringWhereClauseForCascadeLayout,
hasTimeseriesBucketAggregation,
} from './src';

export { ENABLE_ESQL, FEEDBACK_LINK } from './constants';
1 change: 1 addition & 0 deletions src/platform/packages/shared/kbn-esql-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export {
getKqlSearchQueries,
getRemoteClustersFromESQLQuery,
convertTimeseriesCommandToFrom,
hasTimeseriesBucketAggregation,
} from './utils/query_parsing_helpers';
export { getIndexPatternFromESQLQuery } from './utils/get_index_pattern_from_query';
export { queryCannotBeSampled } from './utils/query_cannot_be_sampled';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
convertTimeseriesCommandToFrom,
hasLimitBeforeAggregate,
missingSortBeforeLimit,
hasTimeseriesBucketAggregation,
} from './query_parsing_helpers';

describe('esql query helpers', () => {
Expand Down Expand Up @@ -1052,4 +1053,125 @@ describe('esql query helpers', () => {
expect(missingSortBeforeLimit('FROM index | LIMIT 10 | SORT field')).toBe(true);
});
});

describe('hasTimeseriesBucketAggregation', () => {
const mockColumns = [
{
id: 'BUCKET(@timestamp, 1h)',
isNull: false,
meta: { type: 'date', esType: 'date' },
name: 'BUCKET(@timestamp, 1h)',
},
{
id: 'agent.name',
isNull: false,
meta: { type: 'string', esType: 'keyword' },
name: 'agent.name',
},
{
id: '@timestamp',
isNull: false,
meta: { type: 'date', esType: 'date' },
name: '@timestamp',
},
{
id: 'c3',
isNull: false,
meta: { type: 'number', esType: 'long' },
name: 'c3',
},
] as DatatableColumn[];

it('should return false if the query is empty', () => {
expect(hasTimeseriesBucketAggregation('')).toBe(false);
});
it('should return false if it is not a timeseries command', () => {
expect(
hasTimeseriesBucketAggregation('FROM index | STATS COUNT() BY bucket(@timestamp, 1h)')
).toBe(false);
});
it('should return false if there is no BUCKET aggregation', () => {
expect(hasTimeseriesBucketAggregation('TS index | STATS COUNT() BY @timestamp')).toBe(false);
});
it('should return false if the BUCKET aggregation is not a date column', () => {
expect(
hasTimeseriesBucketAggregation(
'TS index | STATS COUNT() BY bucket(c3, 20, 100, 200), agent.name',
[
{
id: 'agent.name',
isNull: false,
meta: { type: 'string', esType: 'keyword' },
name: 'agent.name',
},
{
id: 'c3',
isNull: false,
meta: { type: 'number', esType: 'long' },
name: 'c3',
},
] as DatatableColumn[]
)
).toBe(false);
});
it('should return false if bucket column cannot be identified', () => {
expect(
hasTimeseriesBucketAggregation('TS index | STATS COUNT() BY bucket(@timestamp, 1h)')
).toBe(false);
});
it('should return false if the last stats command does not contain a BUCKET aggregation', () => {
expect(
hasTimeseriesBucketAggregation(
`TS index
| STATS count_per_day=COUNT(*) BY category=CATEGORIZE(message), @timestamp=BUCKET(@timestamp, 1 day)
| STATS count = SUM(count_per_day), Trend=VALUES(count_per_day) BY category
| KEEP category, count
| STATS sample = SAMPLE(count, 10) BY category`,
mockColumns
)
).toBe(false);
});
it('should return true if the query contains a BUCKET aggregation', () => {
expect(
hasTimeseriesBucketAggregation(
'TS index | STATS COUNT() BY bucket(@timestamp, 1h)',
mockColumns
)
).toBe(true);
});
it('should return true if the query contains aliased BUCKET aggregation', () => {
const columns = [
{
id: 't',
isNull: false,
meta: { type: 'date', esType: 'date' },
name: 't',
},
] as DatatableColumn[];

expect(
hasTimeseriesBucketAggregation(
'TS index | STATS COUNT() BY t=bucket(@timestamp, 1h)',
columns
)
).toBe(true);
});
it('should return true if the query contains a BUCKET aggregation with multiple breakdowns', () => {
expect(
hasTimeseriesBucketAggregation(
'TS index | STATS COUNT() BY bucket(@timestamp, 1h), agent.name',
mockColumns
)
).toBe(true);
});

it('should return true if the query contains a TBUCKET aggregation with multiple breakdowns', () => {
expect(
hasTimeseriesBucketAggregation(
'TS index | STATS COUNT() BY tbucket(@timestamp, 1h), agent.name',
mockColumns
)
).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -591,3 +591,41 @@ export const missingSortBeforeLimit = (esql: string): boolean => {
}
return false;
};

/**
* Checks if the ESQL query contains a timeseries bucket aggregation.
* @param esql: string - The ESQL query string
* @param columns: DatatableColumn[] - The columns of the datatable
* @returns true if the query contains a timeseries bucket aggregation, false otherwise
*/
export function hasTimeseriesBucketAggregation(
esql: string,
columns: DatatableColumn[] = []
): boolean {
const { root } = Parser.parse(esql);

if (!columns.some((column) => column.meta.type === 'date')) {
return false;
}

const isTimeseriesCommand = Walker.commands(root).some(({ name }) => name === 'ts');
if (!isTimeseriesCommand) {
return false;
}

const statsCommands = Walker.commands(root).filter(({ name }) => name === 'stats');
if (statsCommands.length === 0) {
return false;
}

const statsByCommands = Walker.matchAll(statsCommands, { type: 'option', name: 'by' });
if (statsByCommands.length === 0) {
return false;
}

const lastByCommand = statsByCommands[statsByCommands.length - 1];
const bucketFunction = Walker.match(lastByCommand, { type: 'function', name: 'bucket' });
const tbucketFunction = Walker.match(lastByCommand, { type: 'function', name: 'tbucket' });

return !!bucketFunction || !!tbucketFunction;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
hasTransformationalCommand,
getCategorizeField,
convertTimeseriesCommandToFrom,
hasTimeseriesBucketAggregation,
} from '@kbn/esql-utils';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
import type {
Expand Down Expand Up @@ -657,10 +658,15 @@ export class LensVisService {
return [];
}

const preferredChartType = preferredVisAttributes
const mappedPreferredChartType = preferredVisAttributes
? mapVisToChartType(preferredVisAttributes.visualizationType)
: undefined;

const preferredChartType =
!mappedPreferredChartType && hasTimeseriesBucketAggregation(query.esql, columns)
? ChartType.Line
: mappedPreferredChartType;
Comment on lines +665 to +668
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we'd add a test case for this to lens_vis_service.suggestions.test.ts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks. I'll add a test case there

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


let visAttributes = preferredVisAttributes;

if (preferredVisAttributes) {
Expand All @@ -671,7 +677,7 @@ export class LensVisService {
dataViewSpec: dataView?.toSpec(),
fieldName: '',
textBasedColumns: columns,
query: query && isOfAggregateQueryType(query) ? query : undefined,
query,
};

return (
Expand Down