mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
Closes HDX-3154 This PR adds a feature that allows the user to add settings to a source. These settings are then added to the end of every query that is rendered through the `renderChartConfig` function, along with any other chart specific settings. See: https://clickhouse.com/docs/sql-reference/statements/select#settings-in-select-query Most of the work was to pass the `source` or `source.querySettings` value through the code to the `renderChartConfig` calls and to update the related tests. There are also some UI changes in the `SourceForm` components. `SQLParser.Parser` from the `node-sql-parser` throws an error when it encounters a SETTINGS clause in a sql string, so a function was added to remove that clause from any sql that is passed to the parser. It assumes that the SETTINGS clause will always be at the end of the sql string, it removes any part of the string including and after the SETTINGS clause. https://github.com/user-attachments/assets/7ac3b852-2c86-4431-88bc-106f982343bb
1429 lines
48 KiB
TypeScript
1429 lines
48 KiB
TypeScript
import { chSql, ColumnMeta, parameterizedQueryToSql } from '@/clickhouse';
|
|
import { Metadata } from '@/core/metadata';
|
|
import {
|
|
ChartConfigWithOptDateRange,
|
|
DisplayType,
|
|
MetricsDataType,
|
|
QuerySettings,
|
|
} from '@/types';
|
|
|
|
import {
|
|
ChartConfigWithOptDateRangeEx,
|
|
renderChartConfig,
|
|
timeFilterExpr,
|
|
} from '../core/renderChartConfig';
|
|
|
|
describe('renderChartConfig', () => {
|
|
let mockMetadata: jest.Mocked<Metadata>;
|
|
|
|
beforeEach(() => {
|
|
const columns = [
|
|
{ name: 'timestamp', type: 'DateTime' },
|
|
{ name: 'value', type: 'Float64' },
|
|
{ name: 'TraceId', type: 'String' },
|
|
{ name: 'ServiceName', type: 'String' },
|
|
];
|
|
mockMetadata = {
|
|
getColumns: jest.fn().mockResolvedValue([
|
|
{ name: 'timestamp', type: 'DateTime' },
|
|
{ name: 'value', type: 'Float64' },
|
|
]),
|
|
getMaterializedColumnsLookupTable: jest.fn().mockResolvedValue(null),
|
|
getColumn: jest
|
|
.fn()
|
|
.mockImplementation(async ({ column }) =>
|
|
columns.find(col => col.name === column),
|
|
),
|
|
getTableMetadata: jest
|
|
.fn()
|
|
.mockResolvedValue({ primary_key: 'timestamp' }),
|
|
getSkipIndices: jest.fn().mockResolvedValue([]),
|
|
} as unknown as jest.Mocked<Metadata>;
|
|
});
|
|
|
|
const gaugeConfiguration: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
// metricTables is added from the Source object via spread operator
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'quantile',
|
|
aggCondition: '',
|
|
aggConditionLanguage: 'lucene',
|
|
valueExpression: 'Value',
|
|
level: 0.95,
|
|
metricName: 'nodejs.event_loop.utilization',
|
|
metricType: MetricsDataType.Gauge,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'lucene',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
granularity: '1 minute',
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
const querySettings: QuerySettings = [
|
|
{ setting: 'optimize_read_in_order', value: '0' },
|
|
{ setting: 'cast_keep_nullable', value: '1' },
|
|
{ setting: 'additional_result_filter', value: 'x != 2' },
|
|
{ setting: 'count_distinct_implementation', value: 'uniqCombined64' },
|
|
{ setting: 'async_insert_busy_timeout_min_ms', value: '20000' },
|
|
];
|
|
|
|
it('should generate sql for a single gauge metric', async () => {
|
|
const generatedSql = await renderChartConfig(
|
|
gaugeConfiguration,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should generate sql for a single gauge metric with a delta() function applied', async () => {
|
|
const generatedSql = await renderChartConfig(
|
|
{
|
|
...gaugeConfiguration,
|
|
select: [
|
|
{
|
|
aggFn: 'max',
|
|
valueExpression: 'Value',
|
|
metricName: 'nodejs.event_loop.utilization',
|
|
metricType: MetricsDataType.Gauge,
|
|
isDelta: true,
|
|
},
|
|
],
|
|
},
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should generate sql for a single sum metric', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
// metricTables is added from the Source object via spread operator
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'avg',
|
|
aggCondition: '',
|
|
aggConditionLanguage: 'lucene',
|
|
valueExpression: 'Value',
|
|
metricName: 'db.client.connections.usage',
|
|
metricType: MetricsDataType.Sum,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
granularity: '5 minute',
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should throw error for string select on sum metric', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: 'Value',
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
granularity: '5 minute',
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
await expect(
|
|
renderChartConfig(config, mockMetadata, querySettings),
|
|
).rejects.toThrow('multi select or string select on metrics not supported');
|
|
});
|
|
|
|
describe('histogram metric queries', () => {
|
|
describe('quantile', () => {
|
|
it('should generate a query without grouping or time bucketing', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'quantile',
|
|
level: 0.5,
|
|
valueExpression: 'Value',
|
|
metricName: 'http.server.duration',
|
|
metricType: MetricsDataType.Histogram,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should generate a query without grouping but time bucketing', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'quantile',
|
|
level: 0.5,
|
|
valueExpression: 'Value',
|
|
metricName: 'http.server.duration',
|
|
metricType: MetricsDataType.Histogram,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
granularity: '2 minute',
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should generate a query with grouping and time bucketing', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'quantile',
|
|
level: 0.5,
|
|
valueExpression: 'Value',
|
|
metricName: 'http.server.duration',
|
|
metricType: MetricsDataType.Histogram,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
granularity: '2 minute',
|
|
groupBy: `ResourceAttributes['host']`,
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
});
|
|
|
|
describe('count', () => {
|
|
it('should generate a count query without grouping or time bucketing', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'count',
|
|
valueExpression: 'Value',
|
|
metricName: 'http.server.duration',
|
|
metricType: MetricsDataType.Histogram,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should generate a count query without grouping but time bucketing', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'count',
|
|
valueExpression: 'Value',
|
|
metricName: 'http.server.duration',
|
|
metricType: MetricsDataType.Histogram,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
granularity: '2 minute',
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should generate a count query with grouping and time bucketing', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'count',
|
|
valueExpression: 'Value',
|
|
metricName: 'http.server.duration',
|
|
metricType: MetricsDataType.Histogram,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
granularity: '2 minute',
|
|
groupBy: `ResourceAttributes['host']`,
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('containing CTE clauses', () => {
|
|
it('should render a ChSql CTE configuration correctly', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
connection: 'test-connection',
|
|
from: {
|
|
databaseName: '',
|
|
tableName: 'TestCte',
|
|
},
|
|
with: [
|
|
{ name: 'TestCte', sql: chSql`SELECT TimeUnix, Line FROM otel_logs` },
|
|
],
|
|
select: [{ valueExpression: 'Line' }],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should render a chart config CTE configuration correctly', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
connection: 'test-connection',
|
|
with: [
|
|
{
|
|
name: 'Parts',
|
|
chartConfig: {
|
|
connection: 'test-connection',
|
|
timestampValueExpression: '',
|
|
select: '_part, _part_offset',
|
|
from: { databaseName: 'default', tableName: 'some_table' },
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
filters: [
|
|
{
|
|
type: 'sql',
|
|
condition: `FieldA = 'test'`,
|
|
},
|
|
],
|
|
orderBy: [{ ordering: 'DESC', valueExpression: 'rand()' }],
|
|
limit: { limit: 1000 },
|
|
},
|
|
},
|
|
],
|
|
select: '*',
|
|
filters: [
|
|
{
|
|
type: 'sql',
|
|
condition: `FieldA = 'test'`,
|
|
},
|
|
{
|
|
type: 'sql',
|
|
condition: `indexHint((_part, _part_offset) IN (SELECT tuple(_part, _part_offset) FROM Parts))`,
|
|
},
|
|
],
|
|
from: {
|
|
databaseName: '',
|
|
tableName: 'Parts',
|
|
},
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
orderBy: [{ ordering: 'DESC', valueExpression: 'rand()' }],
|
|
limit: { limit: 1000 },
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should throw if the CTE is missing both sql and chartConfig', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
connection: 'test-connection',
|
|
with: [
|
|
{
|
|
name: 'InvalidCTE',
|
|
// Intentionally omitting both sql and chartConfig properties
|
|
},
|
|
],
|
|
select: [{ valueExpression: 'Line' }],
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: 'some_table',
|
|
},
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
};
|
|
|
|
await expect(
|
|
renderChartConfig(config, mockMetadata, querySettings),
|
|
).rejects.toThrow(
|
|
"must specify either 'sql' or 'chartConfig' in with clause",
|
|
);
|
|
});
|
|
|
|
it('should throw if the CTE sql param is invalid', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
connection: 'test-connection',
|
|
with: [
|
|
{
|
|
name: 'InvalidCTE',
|
|
sql: 'SELECT * FROM some_table' as any, // Intentionally not a ChSql object
|
|
},
|
|
],
|
|
select: [{ valueExpression: 'Line' }],
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: 'some_table',
|
|
},
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
};
|
|
|
|
await expect(
|
|
renderChartConfig(config, mockMetadata, querySettings),
|
|
).rejects.toThrow('non-conforming sql object in CTE');
|
|
});
|
|
|
|
it('should throw if the CTE chartConfig param is invalid', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
connection: 'test-connection',
|
|
with: [
|
|
{
|
|
name: 'InvalidCTE',
|
|
chartConfig: {
|
|
// Missing required properties like select, from, etc.
|
|
connection: 'test-connection',
|
|
} as any, // Intentionally invalid chartConfig
|
|
},
|
|
],
|
|
select: [{ valueExpression: 'Line' }],
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: 'some_table',
|
|
},
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
};
|
|
|
|
await expect(
|
|
renderChartConfig(config, mockMetadata, querySettings),
|
|
).rejects.toThrow('non-conforming chartConfig object in CTE');
|
|
});
|
|
});
|
|
|
|
describe('k8s semantic convention migrations', () => {
|
|
it('should generate SQL with metricNameSql for k8s.pod.cpu.utilization gauge metric', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'avg',
|
|
aggCondition: '',
|
|
aggConditionLanguage: 'lucene',
|
|
valueExpression: 'Value',
|
|
metricName: 'k8s.pod.cpu.utilization',
|
|
metricNameSql:
|
|
"MetricName IN ('k8s.pod.cpu.utilization', 'k8s.pod.cpu.usage')",
|
|
metricType: MetricsDataType.Gauge,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'lucene',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
granularity: '1 minute',
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
|
|
// Verify the SQL contains the IN-based metric name condition
|
|
expect(actual).toContain('k8s.pod.cpu.utilization');
|
|
expect(actual).toContain('k8s.pod.cpu.usage');
|
|
expect(actual).toMatch(/MetricName IN /);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should generate SQL with metricNameSql for k8s.node.cpu.utilization sum metric', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'max',
|
|
aggCondition: '',
|
|
aggConditionLanguage: 'lucene',
|
|
valueExpression: 'Value',
|
|
metricName: 'k8s.node.cpu.utilization',
|
|
metricNameSql:
|
|
"MetricName IN ('k8s.node.cpu.utilization', 'k8s.node.cpu.usage')",
|
|
metricType: MetricsDataType.Sum,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
granularity: '5 minute',
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
|
|
expect(actual).toContain('k8s.node.cpu.utilization');
|
|
expect(actual).toContain('k8s.node.cpu.usage');
|
|
expect(actual).toMatch(/MetricName IN /);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should generate SQL with metricNameSql for container.cpu.utilization histogram metric', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'quantile',
|
|
level: 0.95,
|
|
valueExpression: 'Value',
|
|
metricName: 'container.cpu.utilization',
|
|
metricNameSql:
|
|
"MetricName IN ('container.cpu.utilization', 'container.cpu.usage')",
|
|
metricType: MetricsDataType.Histogram,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
granularity: '2 minute',
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
|
|
expect(actual).toContain('container.cpu.utilization');
|
|
expect(actual).toContain('container.cpu.usage');
|
|
expect(actual).toMatch(/MetricName IN /);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should generate SQL with metricNameSql for histogram metric with groupBy', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'quantile',
|
|
level: 0.99,
|
|
valueExpression: 'Value',
|
|
metricName: 'k8s.pod.cpu.utilization',
|
|
metricNameSql:
|
|
"MetricName IN ('k8s.pod.cpu.utilization', 'k8s.pod.cpu.usage')",
|
|
metricType: MetricsDataType.Histogram,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
granularity: '1 minute',
|
|
groupBy: `ResourceAttributes['k8s.pod.name']`,
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
|
|
expect(actual).toContain('k8s.pod.cpu.utilization');
|
|
expect(actual).toContain('k8s.pod.cpu.usage');
|
|
expect(actual).toMatch(/MetricName IN /);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should handle metrics without metricNameSql (backward compatibility)', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
metricTables: {
|
|
gauge: 'otel_metrics_gauge',
|
|
histogram: 'otel_metrics_histogram',
|
|
sum: 'otel_metrics_sum',
|
|
summary: 'otel_metrics_summary',
|
|
'exponential histogram': 'otel_metrics_exponential_histogram',
|
|
},
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: '',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'avg',
|
|
aggCondition: '',
|
|
aggConditionLanguage: 'lucene',
|
|
valueExpression: 'Value',
|
|
metricName: 'some.regular.metric',
|
|
// No metricNameSql provided
|
|
metricType: MetricsDataType.Gauge,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'lucene',
|
|
timestampValueExpression: 'TimeUnix',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-12-14')],
|
|
granularity: '1 minute',
|
|
limit: { limit: 10 },
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
|
|
// Should use the simple string comparison for regular metrics (not IN-based)
|
|
expect(actual).toContain("MetricName = 'some.regular.metric'");
|
|
expect(actual).not.toMatch(/MetricName IN /);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
});
|
|
|
|
describe('HAVING clause', () => {
|
|
it('should render HAVING clause with SQL language', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Table,
|
|
connection: 'test-connection',
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: 'logs',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'count',
|
|
valueExpression: '*',
|
|
aggCondition: '',
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
groupBy: 'severity',
|
|
having: 'count(*) > 100',
|
|
havingLanguage: 'sql',
|
|
timestampValueExpression: 'timestamp',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-02-14')],
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toContain('HAVING');
|
|
expect(actual).toContain('count(*) > 100');
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should render HAVING clause with multiple conditions', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Table,
|
|
connection: 'test-connection',
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: 'metrics',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'avg',
|
|
valueExpression: 'response_time',
|
|
aggCondition: '',
|
|
},
|
|
{
|
|
aggFn: 'count',
|
|
valueExpression: '*',
|
|
aggCondition: '',
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
groupBy: 'endpoint',
|
|
having: 'avg(response_time) > 500 AND count(*) > 10',
|
|
havingLanguage: 'sql',
|
|
timestampValueExpression: 'timestamp',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-02-14')],
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toContain('HAVING');
|
|
expect(actual).toContain('avg(response_time) > 500 AND count(*) > 10');
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should not render HAVING clause when not provided', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Table,
|
|
connection: 'test-connection',
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: 'logs',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'count',
|
|
valueExpression: '*',
|
|
aggCondition: '',
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
groupBy: 'severity',
|
|
timestampValueExpression: 'timestamp',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-02-14')],
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).not.toContain('HAVING');
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should render HAVING clause with granularity and groupBy', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Line,
|
|
connection: 'test-connection',
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: 'events',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'count',
|
|
valueExpression: '*',
|
|
aggCondition: '',
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
groupBy: 'event_type',
|
|
having: 'count(*) > 50',
|
|
havingLanguage: 'sql',
|
|
timestampValueExpression: 'timestamp',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-02-14')],
|
|
granularity: '5 minute',
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toContain('HAVING');
|
|
expect(actual).toContain('count(*) > 50');
|
|
expect(actual).toContain('GROUP BY');
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should not render HAVING clause when having is empty string', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Table,
|
|
connection: 'test-connection',
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: 'logs',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'count',
|
|
valueExpression: '*',
|
|
aggCondition: '',
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
groupBy: 'severity',
|
|
having: '',
|
|
havingLanguage: 'sql',
|
|
timestampValueExpression: 'timestamp',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-02-14')],
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).not.toContain('HAVING');
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
});
|
|
|
|
describe('timeFilterExpr', () => {
|
|
type TimeFilterExprTestCase = {
|
|
timestampValueExpression: string;
|
|
dateRangeStartInclusive?: boolean;
|
|
dateRangeEndInclusive?: boolean;
|
|
dateRange: [Date, Date];
|
|
includedDataInterval?: string;
|
|
expected: string;
|
|
description: string;
|
|
tableName?: string;
|
|
databaseName?: string;
|
|
primaryKey?: string;
|
|
};
|
|
|
|
const testCases: TimeFilterExprTestCase[] = [
|
|
{
|
|
description: 'with basic timestampValueExpression',
|
|
timestampValueExpression: 'timestamp',
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
expected: `(timestamp >= fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}) AND timestamp <= fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}))`,
|
|
},
|
|
{
|
|
description: 'with dateRangeEndInclusive=false',
|
|
timestampValueExpression: 'timestamp',
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
dateRangeEndInclusive: false,
|
|
expected: `(timestamp >= fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}) AND timestamp < fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}))`,
|
|
},
|
|
{
|
|
description: 'with dateRangeStartInclusive=false',
|
|
timestampValueExpression: 'timestamp',
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
dateRangeStartInclusive: false,
|
|
expected: `(timestamp > fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}) AND timestamp <= fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}))`,
|
|
},
|
|
{
|
|
description: 'with includedDataInterval',
|
|
timestampValueExpression: 'timestamp',
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
includedDataInterval: '1 WEEK',
|
|
expected: `(timestamp >= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}), INTERVAL 1 WEEK) - INTERVAL 1 WEEK AND timestamp <= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}), INTERVAL 1 WEEK) + INTERVAL 1 WEEK)`,
|
|
},
|
|
{
|
|
description: 'with date type timestampValueExpression',
|
|
timestampValueExpression: 'date',
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
expected: `(date >= toDate(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()})) AND date <= toDate(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()})))`,
|
|
},
|
|
{
|
|
description: 'with multiple timestampValueExpression parts',
|
|
timestampValueExpression: 'timestamp, date',
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
expected: `(timestamp >= fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}) AND timestamp <= fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}))AND(date >= toDate(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()})) AND date <= toDate(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()})))`,
|
|
},
|
|
{
|
|
description: 'with toStartOfDay() in timestampExpr',
|
|
timestampValueExpression: 'toStartOfDay(timestamp)',
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
expected: `(toStartOfDay(timestamp) >= toStartOfDay(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()})) AND toStartOfDay(timestamp) <= toStartOfDay(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()})))`,
|
|
},
|
|
{
|
|
description: 'with toStartOfDay () in timestampExpr',
|
|
timestampValueExpression: 'toStartOfDay (timestamp)',
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
expected: `(toStartOfDay (timestamp) >= toStartOfDay(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()})) AND toStartOfDay (timestamp) <= toStartOfDay(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()})))`,
|
|
},
|
|
{
|
|
description: 'with toStartOfInterval() in timestampExpr',
|
|
timestampValueExpression:
|
|
'toStartOfInterval(timestamp, INTERVAL 12 MINUTE)',
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
expected: `(toStartOfInterval(timestamp, INTERVAL 12 MINUTE) >= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}), INTERVAL 12 MINUTE) AND toStartOfInterval(timestamp, INTERVAL 12 MINUTE) <= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}), INTERVAL 12 MINUTE))`,
|
|
},
|
|
{
|
|
description:
|
|
'with toStartOfInterval() with lowercase interval in timestampExpr',
|
|
timestampValueExpression:
|
|
'toStartOfInterval(timestamp, interval 1 minute)',
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
expected: `(toStartOfInterval(timestamp, interval 1 minute) >= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}), interval 1 minute) AND toStartOfInterval(timestamp, interval 1 minute) <= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}), interval 1 minute))`,
|
|
},
|
|
{
|
|
description: 'with toStartOfInterval() with timezone and offset',
|
|
timestampValueExpression: `toStartOfInterval(timestamp, INTERVAL 1 MINUTE, toDateTime('2023-01-01 14:35:30'), 'America/New_York')`,
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
expected: `(toStartOfInterval(timestamp, INTERVAL 1 MINUTE, toDateTime('2023-01-01 14:35:30'), 'America/New_York') >= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}), INTERVAL 1 MINUTE, toDateTime('2023-01-01 14:35:30'), 'America/New_York') AND toStartOfInterval(timestamp, INTERVAL 1 MINUTE, toDateTime('2023-01-01 14:35:30'), 'America/New_York') <= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}), INTERVAL 1 MINUTE, toDateTime('2023-01-01 14:35:30'), 'America/New_York'))`,
|
|
},
|
|
{
|
|
description: 'with nonstandard spacing',
|
|
timestampValueExpression: ` toStartOfInterval ( timestamp , INTERVAL 1 MINUTE , toDateTime ( '2023-01-01 14:35:30' ), 'America/New_York' ) `,
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
expected: `(toStartOfInterval ( timestamp , INTERVAL 1 MINUTE , toDateTime ( '2023-01-01 14:35:30' ), 'America/New_York' ) >= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-12 00:12:34Z').getTime()}), INTERVAL 1 MINUTE, toDateTime ( '2023-01-01 14:35:30' ), 'America/New_York') AND toStartOfInterval ( timestamp , INTERVAL 1 MINUTE , toDateTime ( '2023-01-01 14:35:30' ), 'America/New_York' ) <= toStartOfInterval(fromUnixTimestamp64Milli(${new Date('2025-02-14 00:12:34Z').getTime()}), INTERVAL 1 MINUTE, toDateTime ( '2023-01-01 14:35:30' ), 'America/New_York'))`,
|
|
},
|
|
{
|
|
description: 'with optimizable timestampValueExpression',
|
|
timestampValueExpression: `timestamp`,
|
|
primaryKey:
|
|
"toStartOfMinute(timestamp), ServiceName, ResourceAttributes['timestamp'], timestamp",
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
expected: `(timestamp >= fromUnixTimestamp64Milli(1739319154000) AND timestamp <= fromUnixTimestamp64Milli(1739491954000))AND(toStartOfMinute(timestamp) >= toStartOfMinute(fromUnixTimestamp64Milli(1739319154000)) AND toStartOfMinute(timestamp) <= toStartOfMinute(fromUnixTimestamp64Milli(1739491954000)))`,
|
|
},
|
|
{
|
|
description: 'with synthetic timestamp value expression for CTE',
|
|
timestampValueExpression: `__hdx_time_bucket`,
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
databaseName: '',
|
|
tableName: 'Bucketed',
|
|
primaryKey:
|
|
"toStartOfMinute(timestamp), ServiceName, ResourceAttributes['timestamp'], timestamp",
|
|
expected: `(__hdx_time_bucket >= fromUnixTimestamp64Milli(1739319154000) AND __hdx_time_bucket <= fromUnixTimestamp64Milli(1739491954000))`,
|
|
},
|
|
|
|
{
|
|
description: 'with toStartOfMinute in timestampValueExpression',
|
|
timestampValueExpression: `toStartOfMinute(timestamp)`,
|
|
dateRange: [
|
|
new Date('2025-02-12 00:12:34Z'),
|
|
new Date('2025-02-14 00:12:34Z'),
|
|
],
|
|
primaryKey:
|
|
"toStartOfMinute(timestamp), ServiceName, ResourceAttributes['timestamp'], timestamp",
|
|
expected: `(toStartOfMinute(timestamp) >= toStartOfMinute(fromUnixTimestamp64Milli(1739319154000)) AND toStartOfMinute(timestamp) <= toStartOfMinute(fromUnixTimestamp64Milli(1739491954000)))`,
|
|
},
|
|
];
|
|
|
|
beforeEach(() => {
|
|
mockMetadata.getColumn.mockImplementation(async ({ column }) =>
|
|
column === 'date'
|
|
? ({ type: 'Date' } as ColumnMeta)
|
|
: ({ type: 'DateTime' } as ColumnMeta),
|
|
);
|
|
});
|
|
|
|
it.each(testCases)(
|
|
'should generate a time filter expression $description',
|
|
async ({
|
|
timestampValueExpression,
|
|
dateRangeEndInclusive = true,
|
|
dateRangeStartInclusive = true,
|
|
dateRange,
|
|
expected,
|
|
includedDataInterval,
|
|
tableName = 'target_table',
|
|
databaseName = 'default',
|
|
primaryKey,
|
|
}) => {
|
|
if (primaryKey) {
|
|
mockMetadata.getTableMetadata.mockResolvedValue({
|
|
primary_key: primaryKey,
|
|
} as any);
|
|
}
|
|
|
|
const actual = await timeFilterExpr({
|
|
timestampValueExpression,
|
|
dateRangeEndInclusive,
|
|
dateRangeStartInclusive,
|
|
dateRange,
|
|
connectionId: 'test-connection',
|
|
databaseName,
|
|
tableName,
|
|
metadata: mockMetadata,
|
|
includedDataInterval,
|
|
});
|
|
|
|
const actualSql = parameterizedQueryToSql(actual);
|
|
expect(actualSql).toBe(expected);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe('Aggregate Merge Functions', () => {
|
|
it('should generate SQL for an aggregate merge function', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Table,
|
|
connection: 'test-connection',
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: 'logs',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'avgMerge',
|
|
valueExpression: 'Duration',
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
groupBy: 'severity',
|
|
timestampValueExpression: 'timestamp',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-02-14')],
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toContain('avgMerge(Duration)');
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should generate SQL for an aggregate merge function with a condition', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Table,
|
|
connection: 'test-connection',
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: 'logs',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'avgMerge',
|
|
valueExpression: 'Duration',
|
|
aggCondition: 'severity:"ERROR"',
|
|
aggConditionLanguage: 'lucene',
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
groupBy: 'severity',
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toContain(
|
|
"avgMergeIf(Duration, ((severity = 'ERROR')) AND toFloat64OrDefault(toString(Duration)) IS NOT NULL)",
|
|
);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should generate SQL for an quantile merge function with a condition', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Table,
|
|
connection: 'test-connection',
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: 'logs',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'quantileMerge',
|
|
aggCondition: 'severity:"ERROR"',
|
|
aggConditionLanguage: 'lucene',
|
|
valueExpression: 'Duration',
|
|
level: 0.95,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
groupBy: 'severity',
|
|
timestampValueExpression: 'timestamp',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-02-14')],
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toContain(
|
|
"quantileMergeIf(0.95)(Duration, ((severity = 'ERROR')) AND toFloat64OrDefault(toString(Duration)) IS NOT NULL)",
|
|
);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
it('should generate SQL for an histogram merge function', async () => {
|
|
const config: ChartConfigWithOptDateRange = {
|
|
displayType: DisplayType.Table,
|
|
connection: 'test-connection',
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: 'logs',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'histogramMerge',
|
|
valueExpression: 'Duration',
|
|
level: 20,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
groupBy: 'severity',
|
|
timestampValueExpression: 'timestamp',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-02-14')],
|
|
};
|
|
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toContain('histogramMerge(20)(Duration)');
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
});
|
|
|
|
describe('SETTINGS clause', () => {
|
|
const config: ChartConfigWithOptDateRangeEx = {
|
|
displayType: DisplayType.Table,
|
|
connection: 'test-connection',
|
|
from: {
|
|
databaseName: 'default',
|
|
tableName: 'logs',
|
|
},
|
|
select: [
|
|
{
|
|
aggFn: 'histogramMerge',
|
|
valueExpression: 'Duration',
|
|
level: 20,
|
|
},
|
|
],
|
|
where: '',
|
|
whereLanguage: 'sql',
|
|
groupBy: 'severity',
|
|
timestampValueExpression: 'timestamp',
|
|
dateRange: [new Date('2025-02-12'), new Date('2025-02-14')],
|
|
};
|
|
|
|
test('should apply the "query settings" settings to the query', async () => {
|
|
const generatedSql = await renderChartConfig(
|
|
config,
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toContain(
|
|
"SETTINGS optimize_read_in_order = 0, cast_keep_nullable = 1, additional_result_filter = 'x != 2', count_distinct_implementation = 'uniqCombined64', async_insert_busy_timeout_min_ms = 20000",
|
|
);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
test('should apply the "chart config" settings to the query', async () => {
|
|
const generatedSql = await renderChartConfig(
|
|
{
|
|
...config,
|
|
settings: chSql`short_circuit_function_evaluation = 'force_enable'`,
|
|
},
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toContain(
|
|
"SETTINGS short_circuit_function_evaluation = 'force_enable'",
|
|
);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
|
|
test('should concat the "chart config" and "query setting" settings and apply them to the query', async () => {
|
|
const generatedSql = await renderChartConfig(
|
|
{
|
|
...config,
|
|
settings: chSql`short_circuit_function_evaluation = 'force_enable'`,
|
|
},
|
|
mockMetadata,
|
|
querySettings,
|
|
);
|
|
|
|
const actual = parameterizedQueryToSql(generatedSql);
|
|
expect(actual).toContain(
|
|
"SETTINGS short_circuit_function_evaluation = 'force_enable', optimize_read_in_order = 0, cast_keep_nullable = 1, additional_result_filter = 'x != 2', count_distinct_implementation = 'uniqCombined64', async_insert_busy_timeout_min_ms = 20000",
|
|
);
|
|
expect(actual).toMatchSnapshot();
|
|
});
|
|
});
|
|
});
|