Skip to content

Commit 2c1bfb1

Browse files
committed
'Add case for non-aligned origin'
1 parent a5f6729 commit 2c1bfb1

File tree

2 files changed

+180
-47
lines changed

2 files changed

+180
-47
lines changed

packages/cubejs-client-core/src/time.ts

Lines changed: 59 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -123,53 +123,6 @@ export function addInterval(date: dayjs.Dayjs, interval: ParsedInterval): dayjs.
123123
return res;
124124
}
125125

126-
export const dayRange = (from: any, to: any, annotations?: Record<string, { granularity?: Granularity }>): DayRange => ({
127-
by: (value: any) => {
128-
const results = [];
129-
130-
let start = internalDayjs(from);
131-
const end = internalDayjs(to);
132-
133-
while (start.startOf(value).isBefore(end) || start.isSame(end)) {
134-
results.push(start);
135-
start = start.add(1, value);
136-
}
137-
138-
return results;
139-
},
140-
snapTo: (value: any): DayRange => {
141-
// Check if this is a custom granularity
142-
if (!isPredefinedGranularity(value) && annotations) {
143-
// Try to find the custom granularity metadata
144-
// The annotation key might be in format "Cube.dimension.granularity"
145-
// So we need to search through all annotations
146-
let customGranularity: Granularity | undefined;
147-
148-
for (const key of Object.keys(annotations)) {
149-
if (key.endsWith(`.${value}`) && annotations[key].granularity) {
150-
customGranularity = annotations[key].granularity;
151-
break;
152-
}
153-
}
154-
155-
if (customGranularity && customGranularity.interval) {
156-
// For custom granularities, calculate the range for the bucket
157-
const intervalParsed = parseSqlInterval(customGranularity.interval);
158-
const intervalStart = internalDayjs(from);
159-
// End is start + interval - 1 millisecond (to stay within the bucket)
160-
const intervalEnd = addInterval(intervalStart, intervalParsed).subtract(1, 'millisecond');
161-
162-
return dayRange(intervalStart, intervalEnd, annotations);
163-
}
164-
}
165-
166-
// Default behavior for predefined granularities
167-
return dayRange(internalDayjs(from).startOf(value), internalDayjs(to).endOf(value), annotations);
168-
},
169-
start: internalDayjs(from),
170-
end: internalDayjs(to),
171-
});
172-
173126
/**
174127
* Adds interval to provided date.
175128
* TODO: It's copy/paste of subtractInterval from @cubejs-backend/shared [time.ts]
@@ -230,6 +183,65 @@ function alignToOrigin(startDate: dayjs.Dayjs, interval: ParsedInterval, origin:
230183
return alignedDate;
231184
}
232185

186+
export const dayRange = (from: any, to: any, annotations?: Record<string, { granularity?: Granularity }>): DayRange => ({
187+
by: (value: any) => {
188+
const results = [];
189+
190+
let start = internalDayjs(from);
191+
const end = internalDayjs(to);
192+
193+
while (start.startOf(value).isBefore(end) || start.isSame(end)) {
194+
results.push(start);
195+
start = start.add(1, value);
196+
}
197+
198+
return results;
199+
},
200+
snapTo: (value: any): DayRange => {
201+
// Check if this is a custom granularity
202+
if (!isPredefinedGranularity(value) && annotations) {
203+
// Try to find the custom granularity metadata
204+
// The annotation key might be in format "Cube.dimension.granularity"
205+
// So we need to search through all annotations
206+
let customGranularity: Granularity | undefined;
207+
208+
for (const key of Object.keys(annotations)) {
209+
if (key.endsWith(`.${value}`) && annotations[key].granularity) {
210+
customGranularity = annotations[key].granularity;
211+
break;
212+
}
213+
}
214+
215+
if (customGranularity?.interval) {
216+
// For custom granularities, calculate the range for the bucket
217+
const intervalParsed = parseSqlInterval(customGranularity.interval);
218+
let intervalStart = internalDayjs(from);
219+
220+
// If custom granularity has an origin, align to it
221+
if (customGranularity.origin) {
222+
let origin = internalDayjs(customGranularity.origin);
223+
if (customGranularity.offset) {
224+
origin = addInterval(origin, parseSqlInterval(customGranularity.offset));
225+
}
226+
227+
// Align the value to the origin to find the actual bucket start
228+
intervalStart = alignToOrigin(intervalStart, intervalParsed, origin);
229+
}
230+
231+
// End is start + interval - 1 millisecond (to stay within the bucket)
232+
const intervalEnd = addInterval(intervalStart, intervalParsed).subtract(1, 'millisecond');
233+
234+
return dayRange(intervalStart, intervalEnd, annotations);
235+
}
236+
}
237+
238+
// Default behavior for predefined granularities
239+
return dayRange(internalDayjs(from).startOf(value), internalDayjs(to).endOf(value), annotations);
240+
},
241+
start: internalDayjs(from),
242+
end: internalDayjs(to),
243+
});
244+
233245
/**
234246
* Returns the time series points for the custom interval
235247
* TODO: It's almost a copy/paste of timeSeriesFromCustomInterval from

packages/cubejs-client-core/test/drill-down.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,4 +518,125 @@ describe('drill down query', () => {
518518
timezone: 'UTC',
519519
});
520520
});
521+
522+
it('handles custom granularity with non-aligned origin', () => {
523+
const EVALUATION_PERIOD = 5;
524+
const NON_ALIGNED_ORIGIN = '2020-08-01T00:02:00.000'; // Origin at 00:02 instead of 00:00
525+
526+
const customGranularityResponse = {
527+
queryType: 'regularQuery',
528+
results: [
529+
{
530+
query: {
531+
measures: ['Transactions.count'],
532+
timeDimensions: [
533+
{
534+
dimension: 'Transactions.createdAt',
535+
granularity: 'alerting_monitor',
536+
dateRange: ['2020-08-01T00:00:00.000', '2020-08-01T01:00:00.000'],
537+
},
538+
],
539+
filters: [],
540+
timezone: 'UTC',
541+
order: [],
542+
dimensions: [],
543+
},
544+
data: [
545+
{
546+
// First bucket starts at 00:02:00 (origin)
547+
'Transactions.createdAt.alerting_monitor': '2020-08-01T00:02:00.000',
548+
'Transactions.createdAt': '2020-08-01T00:02:00.000',
549+
'Transactions.count': 10,
550+
},
551+
{
552+
// Second bucket starts at 00:07:00 (origin + 5 minutes)
553+
'Transactions.createdAt.alerting_monitor': '2020-08-01T00:07:00.000',
554+
'Transactions.createdAt': '2020-08-01T00:07:00.000',
555+
'Transactions.count': 15,
556+
},
557+
{
558+
// Third bucket starts at 00:12:00 (origin + 10 minutes)
559+
'Transactions.createdAt.alerting_monitor': '2020-08-01T00:12:00.000',
560+
'Transactions.createdAt': '2020-08-01T00:12:00.000',
561+
'Transactions.count': 8,
562+
},
563+
],
564+
annotation: {
565+
measures: {
566+
'Transactions.count': {
567+
title: 'Transactions Count',
568+
shortTitle: 'Count',
569+
type: 'number',
570+
drillMembers: ['Transactions.id', 'Transactions.createdAt'],
571+
drillMembersGrouped: {
572+
measures: [],
573+
dimensions: ['Transactions.id', 'Transactions.createdAt'],
574+
},
575+
},
576+
},
577+
dimensions: {},
578+
segments: {},
579+
timeDimensions: {
580+
'Transactions.createdAt.alerting_monitor': {
581+
title: 'Transaction created at',
582+
shortTitle: 'Created at',
583+
type: 'time',
584+
granularity: {
585+
name: 'alerting_monitor',
586+
title: 'Alerting Monitor',
587+
interval: `${EVALUATION_PERIOD} minutes`,
588+
origin: NON_ALIGNED_ORIGIN,
589+
},
590+
},
591+
'Transactions.createdAt': {
592+
title: 'Transaction created at',
593+
shortTitle: 'Created at',
594+
type: 'time',
595+
},
596+
},
597+
},
598+
},
599+
],
600+
pivotQuery: {
601+
measures: ['Transactions.count'],
602+
timeDimensions: [
603+
{
604+
dimension: 'Transactions.createdAt',
605+
granularity: 'alerting_monitor',
606+
dateRange: ['2020-08-01T00:00:00.000', '2020-08-01T01:00:00.000'],
607+
},
608+
],
609+
filters: [],
610+
timezone: 'UTC',
611+
order: [],
612+
dimensions: [],
613+
},
614+
};
615+
616+
const resultSet = new ResultSet(customGranularityResponse as any);
617+
618+
// Test drilling down on the second data point (00:07:00)
619+
// Since origin is 00:02:00, the bucket is 00:07:00 - 00:11:59
620+
expect(
621+
resultSet.drillDown({ xValues: ['2020-08-01T00:07:00.000'] })
622+
).toEqual({
623+
measures: [],
624+
segments: [],
625+
dimensions: ['Transactions.id', 'Transactions.createdAt'],
626+
filters: [
627+
{
628+
member: 'Transactions.count',
629+
operator: 'measureFilter',
630+
},
631+
],
632+
timeDimensions: [
633+
{
634+
dimension: 'Transactions.createdAt',
635+
// Should align to origin: bucket starts at 00:07:00 (origin + 5min)
636+
dateRange: ['2020-08-01T00:07:00.000', '2020-08-01T00:11:59.999'],
637+
},
638+
],
639+
timezone: 'UTC',
640+
});
641+
});
521642
});

0 commit comments

Comments
 (0)