Skip to content

Commit ef12d8d

Browse files
authored
fix(query-orchestrator): Fix improper pre-aggregation buildRange construction for non UTC timezones (#9284)
* improve partitionTableName() in PreAggregationPartitionRangeLoader * make loadBuildRange() timezone-aware + align tests * fix loadBuildRange() to use inDbTimeZone() * return back utcToLocalTimeZone * return back utcToLocalTimeZone in replaceQueryBuildRangeParams * rename inDbTimeZone to localTimestampToUtc * small improvement in BaseFilter format*Date * remove unneeded * another round in loadBuildRange() * optimize to avoid doble ts convertion, add ts format to loadBuildRange() * fix/specify some types * fix invalidateKeyQueries not to be in the future * fix correct extractDate parsing * fix localTimestampToUtc() * tests for all time functions * revert change for sealAt * fix tests * fix extractDate and rename it to parseLocalDate * fix tests for parseLocalDate() * align PreAggregationPartitionRangeLoader with changes * fix BaseDbRunner * fix date formatting in replacePartitionSqlAndParams * fix tests * spelling * add more tests for timeSeries() * add tests for reformatUtcTimestamp() * refactor alignToOrigin() * convert to utc only in replacePartitionSqlAndParams() * remove reformatUtcTimestamp() as oblsolete * code polish * remove unused redis-related utils * move widely used ts format literal to const * more tests for PreAggregationPartitionRangeLoader.intersectDateRanges() * some improvements in tests * some tests for PreAggregationPartitionRangeLoader * some test polishment
1 parent 3174be1 commit ef12d8d

File tree

17 files changed

+846
-340
lines changed

17 files changed

+846
-340
lines changed

packages/cubejs-backend-shared/src/time.ts

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -78,31 +78,17 @@ export function subtractInterval(date: moment.Moment, interval: ParsedInterval):
7878
*/
7979
export const alignToOrigin = (startDate: moment.Moment, interval: ParsedInterval, origin: moment.Moment): moment.Moment => {
8080
let alignedDate = startDate.clone();
81-
let intervalOp;
82-
let isIntervalNegative = false;
83-
84-
let offsetDate = addInterval(origin, interval);
85-
86-
// The easiest way to check the interval sign
87-
if (offsetDate.isBefore(origin)) {
88-
isIntervalNegative = true;
89-
}
90-
91-
offsetDate = origin.clone();
81+
let offsetDate = origin.clone();
9282

9383
if (startDate.isBefore(origin)) {
94-
intervalOp = isIntervalNegative ? addInterval : subtractInterval;
95-
9684
while (offsetDate.isAfter(startDate)) {
97-
offsetDate = intervalOp(offsetDate, interval);
85+
offsetDate = subtractInterval(offsetDate, interval);
9886
}
9987
alignedDate = offsetDate;
10088
} else {
101-
intervalOp = isIntervalNegative ? subtractInterval : addInterval;
102-
10389
while (offsetDate.isBefore(startDate)) {
10490
alignedDate = offsetDate.clone();
105-
offsetDate = intervalOp(offsetDate, interval);
91+
offsetDate = addInterval(offsetDate, interval);
10692
}
10793

10894
if (offsetDate.isSame(startDate)) {
@@ -192,7 +178,13 @@ export const BUILD_RANGE_START_LOCAL = '__BUILD_RANGE_START_LOCAL';
192178

193179
export const BUILD_RANGE_END_LOCAL = '__BUILD_RANGE_END_LOCAL';
194180

195-
export const inDbTimeZone = (timezone: string, timestampFormat: string, timestamp: string): string => {
181+
/**
182+
* Takes timestamp, treat it as time in provided timezone and returns the corresponding timestamp in UTC
183+
*/
184+
export const localTimestampToUtc = (timezone: string, timestampFormat: string, timestamp?: string): string | null => {
185+
if (!timestamp) {
186+
return null;
187+
}
196188
if (timestamp.length === 23 || timestamp.length === 26) {
197189
const zone = moment.tz.zone(timezone);
198190
if (!zone) {
@@ -217,8 +209,14 @@ export const inDbTimeZone = (timezone: string, timestampFormat: string, timestam
217209
} else if (timestampFormat === 'YYYY-MM-DDTHH:mm:ss.SSS') {
218210
return inDbTimeZoneDate.toJSON().replace('Z', '');
219211
} else if (timestampFormat === 'YYYY-MM-DDTHH:mm:ss.SSSSSS') {
212+
const value = inDbTimeZoneDate.toJSON();
213+
if (value.endsWith('999Z')) {
214+
// emulate microseconds
215+
return value.replace('Z', '999');
216+
}
217+
220218
// emulate microseconds
221-
return inDbTimeZoneDate.toJSON().replace('Z', '000');
219+
return value.replace('Z', '000');
222220
}
223221
}
224222

@@ -227,7 +225,13 @@ export const inDbTimeZone = (timezone: string, timestampFormat: string, timestam
227225
return moment.tz(timestamp, timezone).utc().format(timestampFormat);
228226
};
229227

230-
export const utcToLocalTimeZone = (timezone: string, timestampFormat: string, timestamp: string): string => {
228+
/**
229+
* Takes timestamp in UTC, shift it into provided timezone and returns the corresponding timestamp in UTC
230+
*/
231+
export const utcToLocalTimeZone = (timezone: string, timestampFormat: string, timestamp?: string): string | null => {
232+
if (!timestamp) {
233+
return null;
234+
}
231235
if (timestamp.length === 23) {
232236
const zone = moment.tz.zone(timezone);
233237
if (!zone) {
@@ -247,16 +251,45 @@ export const utcToLocalTimeZone = (timezone: string, timestampFormat: string, ti
247251
return moment.tz(timestamp, 'UTC').tz(timezone).format(timestampFormat);
248252
};
249253

250-
export const extractDate = (data: any): string | null => {
254+
export const parseLocalDate = (data: any, timezone: string, timestampFormat: string = 'YYYY-MM-DDTHH:mm:ss.SSS'): string | null => {
251255
if (!data) {
252256
return null;
253257
}
254258
data = JSON.parse(JSON.stringify(data));
255259
const value = data[0] && data[0][Object.keys(data[0])[0]];
256260
if (!value) {
257-
return value;
261+
return null;
262+
}
263+
264+
const zone = moment.tz.zone(timezone);
265+
if (!zone) {
266+
throw new Error(`Unknown timezone: ${timezone}`);
267+
}
268+
269+
// Most common formats
270+
const formats = [
271+
moment.ISO_8601,
272+
'YYYY-MM-DD HH:mm:ss',
273+
'YYYY-MM-DD HH:mm:ss.SSS',
274+
'YYYY-MM-DDTHH:mm:ss.SSS',
275+
'YYYY-MM-DDTHH:mm:ss'
276+
];
277+
278+
let parsedMoment;
279+
280+
if (value.includes('Z') || /([+-]\d{2}:?\d{2})$/.test(value.trim())) {
281+
// We have timezone info
282+
parsedMoment = moment(value, formats, true);
283+
} else {
284+
// If no tz info - use provided timezone
285+
parsedMoment = moment.tz(value, formats, true, timezone);
258286
}
259-
return moment.tz(value, 'UTC').utc().format(moment.HTML5_FMT.DATETIME_LOCAL_MS);
287+
288+
if (!parsedMoment.isValid()) {
289+
return null;
290+
}
291+
292+
return parsedMoment.tz(timezone).format(timestampFormat);
260293
};
261294

262295
export const addSecondsToLocalTimestamp = (timestamp: string, timezone: string, seconds: number): Date => {

0 commit comments

Comments
 (0)