Skip to content
Merged
39 changes: 37 additions & 2 deletions packages/cubejs-client-core/src/ResultSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {

import { aliasSeries } from './utils';
import {
addInterval,
DateRegex,
dayRange,
internalDayjs,
isPredefinedGranularity,
LocalDateRegex,
parseSqlInterval,
TIME_SERIES,
timeSeriesFromCustomInterval
} from './time';
Expand Down Expand Up @@ -215,7 +217,7 @@ export default class ResultSet<T extends Record<string, any> = any> {
normalizedPivotConfig?.y.forEach((member, currentIndex) => values.push([member, yValues[currentIndex]]));

const { filters: parentFilters = [], segments = [] } = this.query();
const { measures } = this.loadResponses[0].annotation;
const { measures, timeDimensions: timeDimensionsAnnotation } = this.loadResponses[0].annotation;
let [, measureName] = values.find(([member]) => member === 'measures') || [];

if (measureName === undefined) {
Expand All @@ -240,7 +242,40 @@ export default class ResultSet<T extends Record<string, any> = any> {
const [cubeName, dimension, granularity] = member.split('.');

if (granularity !== undefined) {
const range = dayRange(value, value).snapTo(granularity);
let range: { start: dayjs.Dayjs; end: dayjs.Dayjs };
Copy link
Member

Choose a reason for hiding this comment

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

I assume that we need to tweak the dayRange function and pass timeDimensionAnnotation, because it's used in time-series, and without that, custom time dimension logic will not work.

Copy link
Member

Choose a reason for hiding this comment

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

friendly ping to @vasilev-alex for advices here :)


// Check if this is a custom granularity
if (!isPredefinedGranularity(granularity)) {
// Get custom granularity metadata from annotations
const customGranularity = timeDimensionsAnnotation?.[member]?.granularity;

if (customGranularity && customGranularity.interval) {
// Parse the interval (e.g., "5 minutes")
const intervalParsed = parseSqlInterval(customGranularity.interval);

// The value is the start of the interval bucket
const intervalStart = internalDayjs(value);

// Calculate the end of the interval bucket
// End is start + interval - 1 millisecond
const intervalEnd = addInterval(intervalStart, intervalParsed).subtract(1, 'millisecond');

range = {
start: intervalStart,
end: intervalEnd
};
} else {
// Fallback to point-in-time if no custom granularity metadata found
range = {
start: internalDayjs(value),
end: internalDayjs(value)
};
}
} else {
// Use existing logic for predefined granularities
range = dayRange(value, value).snapTo(granularity);
}

const originalTimeDimension = query.timeDimensions?.find((td) => td.dimension);

let dateRange = [
Expand Down
117 changes: 117 additions & 0 deletions packages/cubejs-client-core/test/drill-down.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,4 +401,121 @@ describe('drill down query', () => {
timezone: 'UTC',
});
});

it('handles custom granularity with interval and origin', () => {
const EVALUATION_PERIOD = 5;
const LAST_EVALUATED_AT = '2020-08-01T00:00:00.000';

const customGranularityResponse = {
queryType: 'regularQuery',
results: [
{
query: {
measures: ['Transactions.count'],
timeDimensions: [
{
dimension: 'Transactions.createdAt',
granularity: 'alerting_monitor',
dateRange: ['2020-08-01T00:00:00.000', '2020-08-01T01:00:00.000'],
},
],
filters: [],
timezone: 'UTC',
order: [],
dimensions: [],
},
data: [
{
'Transactions.createdAt.alerting_monitor': '2020-08-01T00:00:00.000',
'Transactions.createdAt': '2020-08-01T00:00:00.000',
'Transactions.count': 10,
},
{
'Transactions.createdAt.alerting_monitor': '2020-08-01T00:05:00.000',
'Transactions.createdAt': '2020-08-01T00:05:00.000',
'Transactions.count': 15,
},
{
'Transactions.createdAt.alerting_monitor': '2020-08-01T00:10:00.000',
'Transactions.createdAt': '2020-08-01T00:10:00.000',
'Transactions.count': 8,
},
],
annotation: {
measures: {
'Transactions.count': {
title: 'Transactions Count',
shortTitle: 'Count',
type: 'number',
drillMembers: ['Transactions.id', 'Transactions.createdAt'],
drillMembersGrouped: {
measures: [],
dimensions: ['Transactions.id', 'Transactions.createdAt'],
},
},
},
dimensions: {},
segments: {},
timeDimensions: {
'Transactions.createdAt.alerting_monitor': {
title: 'Transaction created at',
shortTitle: 'Created at',
type: 'time',
granularity: {
name: 'alerting_monitor',
title: 'Alerting Monitor',
interval: `${EVALUATION_PERIOD} minutes`,
origin: LAST_EVALUATED_AT,
},
},
'Transactions.createdAt': {
title: 'Transaction created at',
shortTitle: 'Created at',
type: 'time',
},
},
},
},
],
pivotQuery: {
measures: ['Transactions.count'],
timeDimensions: [
{
dimension: 'Transactions.createdAt',
granularity: 'alerting_monitor',
dateRange: ['2020-08-01T00:00:00.000', '2020-08-01T01:00:00.000'],
},
],
filters: [],
timezone: 'UTC',
order: [],
dimensions: [],
},
};

const resultSet = new ResultSet(customGranularityResponse as any);

// Test drilling down on the second data point (00:05:00)
expect(
resultSet.drillDown({ xValues: ['2020-08-01T00:05:00.000'] })
).toEqual({
measures: [],
segments: [],
dimensions: ['Transactions.id', 'Transactions.createdAt'],
filters: [
{
member: 'Transactions.count',
operator: 'measureFilter',
},
],
timeDimensions: [
{
dimension: 'Transactions.createdAt',
// Should create a date range for the 5-minute interval starting at 00:05:00
dateRange: ['2020-08-01T00:05:00.000', '2020-08-01T00:09:59.999'],
},
],
timezone: 'UTC',
});
});
});
Loading