mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: Add a minimum date to MV configuration (#1549)
Closes HDX-3129 # Summary This PR adds a "minimum date" setting to Materialized view configurations. The minimum date is optional. When provided, the materialized view will not be used for any query that includes date ranges prior to the minimum date. ## Demo https://github.com/user-attachments/assets/de0efb45-786e-4b45-b053-f1dc7c322f2e
This commit is contained in:
parent
ac1a2f7768
commit
ab7645deb6
6 changed files with 204 additions and 8 deletions
6
.changeset/calm-dragons-lie.md
Normal file
6
.changeset/calm-dragons-lie.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@hyperdx/common-utils": patch
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
feat: Add a minimum date to MV configuration
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import { useMemo } from 'react';
|
||||
import { splitAndTrimWithBracket } from '@hyperdx/common-utils/dist/core/utils';
|
||||
import { MaterializedViewConfiguration } from '@hyperdx/common-utils/dist/types';
|
||||
import { Group, Pill, Stack, Table, Text } from '@mantine/core';
|
||||
import { Grid, Group, Pill, Stack, Table, Text } from '@mantine/core';
|
||||
|
||||
import { FormatTime } from '@/useFormatTime';
|
||||
|
||||
export default function MVConfigSummary({
|
||||
config,
|
||||
|
|
@ -29,12 +31,24 @@ export default function MVConfigSummary({
|
|||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs">
|
||||
Minimum Granularity
|
||||
</Text>
|
||||
<Pill>{config.minGranularity}</Pill>
|
||||
</div>
|
||||
<Grid columns={2}>
|
||||
<Grid.Col span={1}>
|
||||
<Text size="sm" fw={500} mb="xs">
|
||||
Granularity
|
||||
</Text>
|
||||
<Pill>{config.minGranularity}</Pill>
|
||||
</Grid.Col>
|
||||
{config.minDate && (
|
||||
<Grid.Col span={1}>
|
||||
<Text size="sm" fw={500} mb="xs">
|
||||
Minimum Date
|
||||
</Text>
|
||||
<Pill>
|
||||
<FormatTime value={config.minDate} format="withYear" />
|
||||
</Pill>
|
||||
</Grid.Col>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs">
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import {
|
|||
Text,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { DateInput } from '@mantine/dates';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
IconCirclePlus,
|
||||
|
|
@ -402,7 +403,7 @@ function MaterializedViewFormSection({
|
|||
</Group>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={1}>
|
||||
<Grid.Col span={2}>
|
||||
<Text size="xs" fw={500} mb={4}>
|
||||
Timestamp Column
|
||||
</Text>
|
||||
|
|
@ -445,6 +446,38 @@ function MaterializedViewFormSection({
|
|||
)}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={1}>
|
||||
<Text size="xs" fw={500} mb={4}>
|
||||
Minimum Date
|
||||
<Tooltip
|
||||
label="(Optional) The earliest date and time (in the local timezone) for which the materialized view contains data. If not provided, then HyperDX will assume that the materialized view contains data for all dates for which the source table contains data."
|
||||
color="dark"
|
||||
c="white"
|
||||
multiline
|
||||
maw={600}
|
||||
>
|
||||
<IconHelpCircle size={14} className="cursor-pointer ms-1" />
|
||||
</Tooltip>
|
||||
</Text>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`materializedViews.${mvIndex}.minDate`}
|
||||
render={({ field }) => (
|
||||
<DateInput
|
||||
{...field}
|
||||
value={field.value ? new Date(field.value) : undefined}
|
||||
onChange={dateStr =>
|
||||
field.onChange(dateStr ? dateStr.toISOString() : null)
|
||||
}
|
||||
clearable
|
||||
highlightToday
|
||||
placeholder="YYYY-MM-DD HH:mm:ss"
|
||||
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Box>
|
||||
|
|
|
|||
|
|
@ -781,6 +781,122 @@ describe('materializedViews', () => {
|
|||
'Custom count() expressions are not supported with materialized views.',
|
||||
]);
|
||||
});
|
||||
|
||||
it("should not use the materialized view when the chart config references a date range prior to the MV's date range", async () => {
|
||||
const chartConfig: ChartConfigWithOptDateRange = {
|
||||
from: {
|
||||
databaseName: 'default',
|
||||
tableName: 'otel_spans',
|
||||
},
|
||||
select: [
|
||||
{
|
||||
valueExpression: '',
|
||||
aggFn: 'count',
|
||||
},
|
||||
],
|
||||
where: '',
|
||||
connection: 'test-connection',
|
||||
dateRange: [
|
||||
new Date('2022-12-01T00:00:00Z'),
|
||||
new Date('2022-12-31T23:59:59Z'),
|
||||
],
|
||||
};
|
||||
|
||||
const result = await tryConvertConfigToMaterializedViewSelect(
|
||||
chartConfig,
|
||||
{
|
||||
...MV_CONFIG_METRIC_ROLLUP_1M,
|
||||
minDate: '2023-01-01T00:00:00Z',
|
||||
},
|
||||
metadata,
|
||||
);
|
||||
|
||||
expect(result.optimizedConfig).toBeUndefined();
|
||||
expect(result.errors).toEqual([
|
||||
'The selected date range includes dates for which this view does not contain data.',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not use the materialized view when the chart config has no date range and the MV has a minimum date', async () => {
|
||||
const chartConfig: ChartConfigWithOptDateRange = {
|
||||
from: {
|
||||
databaseName: 'default',
|
||||
tableName: 'otel_spans',
|
||||
},
|
||||
select: [
|
||||
{
|
||||
valueExpression: '',
|
||||
aggFn: 'count',
|
||||
},
|
||||
],
|
||||
where: '',
|
||||
connection: 'test-connection',
|
||||
};
|
||||
|
||||
const result = await tryConvertConfigToMaterializedViewSelect(
|
||||
chartConfig,
|
||||
{
|
||||
...MV_CONFIG_METRIC_ROLLUP_1M,
|
||||
minDate: '2023-01-01T00:00:00Z',
|
||||
},
|
||||
metadata,
|
||||
);
|
||||
|
||||
expect(result.optimizedConfig).toBeUndefined();
|
||||
expect(result.errors).toEqual([
|
||||
'The selected date range includes dates for which this view does not contain data.',
|
||||
]);
|
||||
});
|
||||
|
||||
it("should use the materialized view when the chart config's date range is after the MV's minimum date", async () => {
|
||||
const chartConfig: ChartConfigWithOptDateRange = {
|
||||
from: {
|
||||
databaseName: 'default',
|
||||
tableName: 'otel_spans',
|
||||
},
|
||||
select: [
|
||||
{
|
||||
valueExpression: '',
|
||||
aggFn: 'count',
|
||||
},
|
||||
],
|
||||
where: '',
|
||||
connection: 'test-connection',
|
||||
dateRange: [
|
||||
new Date('2023-12-01T00:00:00Z'),
|
||||
new Date('2023-12-31T23:59:59Z'),
|
||||
],
|
||||
};
|
||||
|
||||
const result = await tryConvertConfigToMaterializedViewSelect(
|
||||
chartConfig,
|
||||
{
|
||||
...MV_CONFIG_METRIC_ROLLUP_1M,
|
||||
minDate: '2023-01-01T00:00:00Z',
|
||||
},
|
||||
metadata,
|
||||
);
|
||||
|
||||
expect(result.optimizedConfig).toEqual({
|
||||
from: {
|
||||
databaseName: 'default',
|
||||
tableName: 'metrics_rollup_1m',
|
||||
},
|
||||
dateRange: [
|
||||
new Date('2023-12-01T00:00:00Z'),
|
||||
new Date('2023-12-31T23:59:59Z'),
|
||||
],
|
||||
select: [
|
||||
{
|
||||
valueExpression: 'count',
|
||||
aggFn: 'sum',
|
||||
},
|
||||
],
|
||||
where: '',
|
||||
connection: 'test-connection',
|
||||
});
|
||||
expect(result.errors).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUnsupportedCountFunction', () => {
|
||||
|
|
|
|||
|
|
@ -153,6 +153,24 @@ function mvConfigSupportsGranularity(
|
|||
return chartGranularitySeconds >= mvGranularitySeconds;
|
||||
}
|
||||
|
||||
function mvConfigSupportsDateRange(
|
||||
mvConfig: MaterializedViewConfiguration,
|
||||
chartConfig: ChartConfigWithOptDateRange,
|
||||
) {
|
||||
if (mvConfig.minDate && !chartConfig.dateRange) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mvConfig.minDate || !chartConfig.dateRange) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const [startDate] = chartConfig.dateRange;
|
||||
const minDate = new Date(mvConfig.minDate);
|
||||
|
||||
return startDate >= minDate;
|
||||
}
|
||||
|
||||
const COUNT_FUNCTION_PATTERN = /\bcount(If)?\s*\(/i;
|
||||
export function isUnsupportedCountFunction(selectItem: SelectItem): boolean {
|
||||
return COUNT_FUNCTION_PATTERN.test(selectItem.valueExpression);
|
||||
|
|
@ -268,6 +286,14 @@ export async function tryConvertConfigToMaterializedViewSelect<
|
|||
};
|
||||
}
|
||||
|
||||
if (mvConfig.minDate && !mvConfigSupportsDateRange(mvConfig, chartConfig)) {
|
||||
return {
|
||||
errors: [
|
||||
'The selected date range includes dates for which this view does not contain data.',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (!mvConfigSupportsGranularity(mvConfig, chartConfig)) {
|
||||
const error = chartConfig.granularity
|
||||
? `Granularity must be at least ${mvConfig.minGranularity}.`
|
||||
|
|
|
|||
|
|
@ -646,6 +646,7 @@ export const MaterializedViewConfigurationSchema = z.object({
|
|||
tableName: z.string().min(1, 'Materialized View Table is required'),
|
||||
dimensionColumns: z.string(),
|
||||
minGranularity: SQLIntervalSchema,
|
||||
minDate: z.string().datetime().nullish(),
|
||||
timestampColumn: z
|
||||
.string()
|
||||
.min(1, 'Materialized View Timestamp column is required'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue