mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-30 08:17:06 +02:00
fix(core): Insights fix same day queries (#21574)
This commit is contained in:
parent
8504beb154
commit
c100736745
|
|
@ -1,86 +1,10 @@
|
|||
import type { DatabaseConfig } from '@n8n/config';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { getDateRangesCommonTableExpressionQuery } from '../insights-by-period-query.helper';
|
||||
|
||||
function expectLastXDaysDateRangeQuery(params: {
|
||||
result: string;
|
||||
dbType: DatabaseConfig['type'];
|
||||
prevStartDateOffset: number;
|
||||
startDateOffset: number;
|
||||
}) {
|
||||
const { result, dbType, prevStartDateOffset: prev, startDateOffset: start } = params;
|
||||
|
||||
if (dbType === 'sqlite') {
|
||||
if (prev === 0) {
|
||||
expect(result).toContain("datetime('now') AS prev_start_date");
|
||||
} else {
|
||||
expect(result).toContain(`datetime('now', '-${prev} days') AS prev_start_date`);
|
||||
}
|
||||
if (start === 0) {
|
||||
expect(result).toContain("datetime('now') AS start_date");
|
||||
} else {
|
||||
expect(result).toContain(`datetime('now', '-${start} days') AS start_date`);
|
||||
}
|
||||
expect(result).toContain("datetime('now') AS end_date");
|
||||
} else if (dbType === 'postgresdb') {
|
||||
if (prev === 0) {
|
||||
expect(result).toContain('NOW() AS prev_start_date');
|
||||
} else {
|
||||
expect(result).toContain(`NOW() - INTERVAL '${prev} days' AS prev_start_date`);
|
||||
}
|
||||
if (start === 0) {
|
||||
expect(result).toContain('NOW() AS start_date');
|
||||
} else {
|
||||
expect(result).toContain(`NOW() - INTERVAL '${start} days' AS start_date`);
|
||||
}
|
||||
expect(result).toContain('NOW() AS end_date');
|
||||
} else {
|
||||
if (prev === 0) {
|
||||
expect(result).toContain('NOW() AS prev_start_date');
|
||||
} else {
|
||||
expect(result).toContain(`DATE_SUB(NOW(), INTERVAL ${prev} DAY) AS prev_start_date`);
|
||||
}
|
||||
if (start === 0) {
|
||||
expect(result).toContain('NOW() AS start_date');
|
||||
} else {
|
||||
expect(result).toContain(`DATE_SUB(NOW(), INTERVAL ${start} DAY) AS start_date`);
|
||||
}
|
||||
expect(result).toContain('NOW() AS end_date');
|
||||
}
|
||||
}
|
||||
|
||||
function expectStartOfDayDateRangeQuery(params: {
|
||||
result: string;
|
||||
dbType: DatabaseConfig['type'];
|
||||
prevStartDateOffset: number;
|
||||
startDateOffset: number;
|
||||
endDateOffset: number;
|
||||
}) {
|
||||
const {
|
||||
result,
|
||||
dbType,
|
||||
prevStartDateOffset: prev,
|
||||
startDateOffset: start,
|
||||
endDateOffset: end,
|
||||
} = params;
|
||||
|
||||
if (dbType === 'sqlite') {
|
||||
expect(result).toContain(`datetime('now', '-${prev} days', 'start of day') AS prev_start_date`);
|
||||
expect(result).toContain(`datetime('now', '-${start} days', 'start of day') AS start_date`);
|
||||
expect(result).toContain(`datetime('now', '-${end} days', 'start of day') AS end_date`);
|
||||
} else if (dbType === 'postgresdb') {
|
||||
expect(result).toContain(
|
||||
`DATE_TRUNC('day', NOW() - INTERVAL '${prev} days') AS prev_start_date`,
|
||||
);
|
||||
expect(result).toContain(`DATE_TRUNC('day', NOW() - INTERVAL '${start} days') AS start_date`);
|
||||
expect(result).toContain(`DATE_TRUNC('day', NOW() - INTERVAL '${end} days') AS end_date`);
|
||||
} else {
|
||||
expect(result).toContain(`DATE(DATE_SUB(NOW(), INTERVAL ${prev} DAY)) AS prev_start_date`);
|
||||
expect(result).toContain(`DATE(DATE_SUB(NOW(), INTERVAL ${start} DAY)) AS start_date`);
|
||||
expect(result).toContain(`DATE(DATE_SUB(NOW(), INTERVAL ${end} DAY)) AS end_date`);
|
||||
}
|
||||
}
|
||||
import {
|
||||
getDateRangesCommonTableExpressionQuery,
|
||||
getDateRangesSelectQuery,
|
||||
} from '../insights-by-period-query.helper';
|
||||
|
||||
describe('getDateRangesCommonTableExpressionQuery', () => {
|
||||
const now = DateTime.utc(2025, 10, 8, 8, 51, 27);
|
||||
|
|
@ -105,63 +29,83 @@ describe('getDateRangesCommonTableExpressionQuery', () => {
|
|||
const startDate = now.minus({ days: 1 }).startOf('day').toJSDate();
|
||||
const endDate = now.startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
});
|
||||
|
||||
if (dbType === 'sqlite') {
|
||||
expect(result).toContain("datetime('now', '-2 days')"); // prev_start_date
|
||||
expect(result).toContain("datetime('now', '-1 days')"); // start_date
|
||||
expect(result).toContain("datetime('now')"); // end_date
|
||||
} else if (dbType === 'postgresdb') {
|
||||
expect(result).toContain("NOW() - INTERVAL '2 days'"); // prev_start_date
|
||||
expect(result).toContain("NOW() - INTERVAL '1 days'"); // start_date
|
||||
expect(result).toContain('NOW()'); // end_date
|
||||
} else {
|
||||
expect(result).toContain('DATE_SUB(NOW(), INTERVAL 2 DAY)'); // prev_start_date
|
||||
expect(result).toContain('DATE_SUB(NOW(), INTERVAL 1 DAY)'); // start_date
|
||||
expect(result).toContain('NOW()'); // end_date
|
||||
}
|
||||
// endDate is today but different day from startDate, so dates stay as-is
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 2 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 1 }).startOf('day'),
|
||||
endDateTime: now.startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('day before yesterday (specific day)', () => {
|
||||
const startDate = now.minus({ days: 2 }).startOf('day').toJSDate();
|
||||
const endDate = now.minus({ days: 1 }).startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectStartOfDayDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 3,
|
||||
startDateOffset: 2,
|
||||
endDateOffset: 1,
|
||||
});
|
||||
|
||||
// Past range: end+1 day startOf('day')
|
||||
// Duration = 2 days, so prev starts 2 days before start
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 4 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 2 }).startOf('day'),
|
||||
endDateTime: now.startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('7 days ago (specific day)', () => {
|
||||
const startDate = now.minus({ days: 7 }).startOf('day').toJSDate();
|
||||
const endDate = now.minus({ days: 6 }).startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectStartOfDayDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 8,
|
||||
startDateOffset: 7,
|
||||
endDateOffset: 6, // the end of the range is the start of the next day
|
||||
});
|
||||
|
||||
// Past range: end+1 day startOf('day')
|
||||
// Duration = 2 days, so prev starts 2 days before start
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 9 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 7 }).startOf('day'),
|
||||
endDateTime: now.minus({ days: 5 }).startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('14 days ago (specific day)', () => {
|
||||
const startDate = now.minus({ days: 14 }).startOf('day').toJSDate();
|
||||
const endDate = now.minus({ days: 13 }).startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectStartOfDayDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 15,
|
||||
startDateOffset: 14,
|
||||
endDateOffset: 13, // the end of the range is the start of the next day
|
||||
});
|
||||
|
||||
// Past range: end+1 day startOf('day')
|
||||
// Duration = 2 days, so prev starts 2 days before start
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 16 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 14 }).startOf('day'),
|
||||
endDateTime: now.minus({ days: 12 }).startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('X days ago (specific day far in the past)', () => {
|
||||
|
|
@ -169,14 +113,21 @@ describe('getDateRangesCommonTableExpressionQuery', () => {
|
|||
const startDate = now.minus({ days: 109 }).startOf('day').toJSDate();
|
||||
const endDate = now.minus({ days: 108 }).startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectStartOfDayDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 110,
|
||||
startDateOffset: 109,
|
||||
endDateOffset: 108,
|
||||
});
|
||||
|
||||
// Past range: end+1 day startOf('day')
|
||||
// Duration = 2 days, so prev starts 2 days before start
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 111 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 109 }).startOf('day'),
|
||||
endDateTime: now.minus({ days: 107 }).startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -186,39 +137,60 @@ describe('getDateRangesCommonTableExpressionQuery', () => {
|
|||
const startDate = now.minus({ days: 7 }).startOf('day').toJSDate();
|
||||
const endDate = now.startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectLastXDaysDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 14, // 7 + 7
|
||||
startDateOffset: 7,
|
||||
});
|
||||
|
||||
// endDate is today but different day, dates stay as-is
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 14 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 7 }).startOf('day'),
|
||||
endDateTime: now.startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('last 14 days', () => {
|
||||
const startDate = now.minus({ days: 14 }).startOf('day').toJSDate();
|
||||
const endDate = now.startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectLastXDaysDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 28, // 14 + 14
|
||||
startDateOffset: 14,
|
||||
});
|
||||
|
||||
// endDate is today but different day, dates stay as-is
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 28 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 14 }).startOf('day'),
|
||||
endDateTime: now.startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('last 30 days', () => {
|
||||
const startDate = now.minus({ days: 30 }).startOf('day').toJSDate();
|
||||
const endDate = now.startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectLastXDaysDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 60, // 30 + 30
|
||||
startDateOffset: 30,
|
||||
});
|
||||
|
||||
// endDate is today but different day, dates stay as-is
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 60 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 30 }).startOf('day'),
|
||||
endDateTime: now.startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -227,70 +199,100 @@ describe('getDateRangesCommonTableExpressionQuery', () => {
|
|||
const startDate = now.minus({ days: 3 }).startOf('day').toJSDate();
|
||||
const endDate = now.minus({ days: 1 }).startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectStartOfDayDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 5,
|
||||
startDateOffset: 3,
|
||||
endDateOffset: 1,
|
||||
});
|
||||
|
||||
// Past range: end+1 day, duration = 3 days
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 6 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 3 }).startOf('day'),
|
||||
endDateTime: now.startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('5 days range', () => {
|
||||
const startDate = now.minus({ days: 10 }).startOf('day').toJSDate();
|
||||
const endDate = now.minus({ days: 5 }).startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectStartOfDayDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 15,
|
||||
startDateOffset: 10,
|
||||
endDateOffset: 5,
|
||||
});
|
||||
|
||||
// Past range: end+1 day, duration = 6 days
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 16 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 10 }).startOf('day'),
|
||||
endDateTime: now.minus({ days: 4 }).startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('7 days range', () => {
|
||||
const startDate = now.minus({ days: 14 }).startOf('day').toJSDate();
|
||||
const endDate = now.minus({ days: 7 }).startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectStartOfDayDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 21,
|
||||
startDateOffset: 14,
|
||||
endDateOffset: 7,
|
||||
});
|
||||
|
||||
// Past range: end+1 day, duration = 8 days
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 22 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 14 }).startOf('day'),
|
||||
endDateTime: now.minus({ days: 6 }).startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('14 days range', () => {
|
||||
const startDate = now.minus({ days: 15 }).startOf('day').toJSDate();
|
||||
const endDate = now.minus({ days: 1 }).startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectStartOfDayDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 29,
|
||||
startDateOffset: 15,
|
||||
endDateOffset: 1,
|
||||
});
|
||||
|
||||
// Past range: end+1 day, duration = 15 days
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 30 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 15 }).startOf('day'),
|
||||
endDateTime: now.startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('30 days range', () => {
|
||||
const startDate = now.minus({ days: 53 }).startOf('day').toJSDate();
|
||||
const endDate = now.minus({ days: 23 }).startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectStartOfDayDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 83,
|
||||
startDateOffset: 53,
|
||||
endDateOffset: 23,
|
||||
});
|
||||
|
||||
// Past range: end+1 day, duration = 31 days
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 84 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 53 }).startOf('day'),
|
||||
endDateTime: now.minus({ days: 22 }).startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -301,43 +303,68 @@ describe('getDateRangesCommonTableExpressionQuery', () => {
|
|||
const startDate = now.minus({ days: 90 }).startOf('day').toJSDate();
|
||||
const endDate = now.startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectLastXDaysDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 180,
|
||||
startDateOffset: 90,
|
||||
});
|
||||
|
||||
// endDate is today but different day, dates stay as-is
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 180 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 90 }).startOf('day'),
|
||||
endDateTime: now.startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('last 6 months', () => {
|
||||
const startDate = now.minus({ months: 6 }).startOf('day').toJSDate();
|
||||
const endDate = now.startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
const daysBack = Math.floor(now.diff(DateTime.fromJSDate(startDate), 'days').days);
|
||||
const prevDaysBack = daysBack * 2;
|
||||
expectLastXDaysDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: prevDaysBack,
|
||||
startDateOffset: daysBack,
|
||||
});
|
||||
|
||||
const startDateTime = DateTime.fromJSDate(startDate).toUTC().startOf('day');
|
||||
const endDateTime = now.startOf('day');
|
||||
const duration = endDateTime.diff(startDateTime);
|
||||
|
||||
// endDate is today but different day, dates stay as-is
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: startDateTime.minus(duration),
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('last year', () => {
|
||||
const startDate = now.minus({ years: 1 }).startOf('day').toJSDate();
|
||||
const endDate = now.startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
const daysBack = Math.floor(now.diff(DateTime.fromJSDate(startDate), 'days').days);
|
||||
const prevDaysBack = daysBack * 2;
|
||||
expectLastXDaysDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: prevDaysBack,
|
||||
startDateOffset: daysBack,
|
||||
});
|
||||
|
||||
const startDateTime = DateTime.fromJSDate(startDate).toUTC().startOf('day');
|
||||
const endDateTime = now.startOf('day');
|
||||
const duration = endDateTime.diff(startDateTime);
|
||||
|
||||
// endDate is today but different day, dates stay as-is
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: startDateTime.minus(duration),
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -346,56 +373,80 @@ describe('getDateRangesCommonTableExpressionQuery', () => {
|
|||
const startDate = now.minus({ days: 32 }).startOf('day').toJSDate();
|
||||
const endDate = now.minus({ days: 1 }).startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectStartOfDayDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 63,
|
||||
startDateOffset: 32,
|
||||
endDateOffset: 1,
|
||||
});
|
||||
|
||||
// Past range: end+1 day, duration = 32 days
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 64 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 32 }).startOf('day'),
|
||||
endDateTime: now.startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('90 days range (specific historical range)', () => {
|
||||
const startDate = now.minus({ days: 98 }).startOf('day').toJSDate();
|
||||
const endDate = now.minus({ days: 8 }).startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectStartOfDayDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 188,
|
||||
startDateOffset: 98,
|
||||
endDateOffset: 8,
|
||||
});
|
||||
|
||||
// Past range: end+1 day, duration = 91 days
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 189 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 98 }).startOf('day'),
|
||||
endDateTime: now.minus({ days: 7 }).startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('180 days range (specific historical range)', () => {
|
||||
const startDate = now.minus({ days: 181 }).startOf('day').toJSDate();
|
||||
const endDate = now.minus({ days: 1 }).startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectStartOfDayDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 361,
|
||||
startDateOffset: 181,
|
||||
endDateOffset: 1,
|
||||
});
|
||||
|
||||
// Past range: end+1 day, duration = 181 days
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 362 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 181 }).startOf('day'),
|
||||
endDateTime: now.startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('360 days range (specific historical range)', () => {
|
||||
const startDate = now.minus({ days: 361 }).startOf('day').toJSDate();
|
||||
const endDate = now.minus({ days: 1 }).startOf('day').toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
expectStartOfDayDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 721,
|
||||
startDateOffset: 361,
|
||||
endDateOffset: 1,
|
||||
});
|
||||
|
||||
// Past range: end+1 day, duration = 361 days
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: now.minus({ days: 722 }).startOf('day'),
|
||||
startDateTime: now.minus({ days: 361 }).startOf('day'),
|
||||
endDateTime: now.startOf('day'),
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -403,90 +454,78 @@ describe('getDateRangesCommonTableExpressionQuery', () => {
|
|||
describe('edge cases', () => {
|
||||
test('handles date with time component correctly', () => {
|
||||
// Oct 6 14:30 to Oct 7 18:45
|
||||
// Now is Oct 8 8:51:27, so Oct 7 18:45 is less than 1 full day ago
|
||||
// Therefore useStartOfDay = false (not yet a full day in the past)
|
||||
// daysFromEndDateToToday = Math.round(0.58) = 1
|
||||
// daysDiff = Math.round(1.18) = 1
|
||||
// daysFromStartDateToToday = Math.floor(1.76) = 1
|
||||
// prevStartDaysFromToday = 1 + 1 = 2
|
||||
// Now is Oct 8 8:51:27, so Oct 7 18:45 is in the past
|
||||
const startDate = DateTime.utc(2025, 10, 6, 14, 30, 0).toJSDate();
|
||||
const endDate = DateTime.utc(2025, 10, 7, 18, 45, 30).toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
});
|
||||
|
||||
// Verify the actual output based on the calculation
|
||||
if (dbType === 'sqlite') {
|
||||
expect(result).toContain("datetime('now', '-2 days') AS prev_start_date");
|
||||
expect(result).toContain("datetime('now', '-1 days') AS start_date");
|
||||
expect(result).toContain("datetime('now', '-1 days') AS end_date");
|
||||
} else if (dbType === 'postgresdb') {
|
||||
expect(result).toContain("NOW() - INTERVAL '2 days' AS prev_start_date");
|
||||
expect(result).toContain("NOW() - INTERVAL '1 days' AS start_date");
|
||||
expect(result).toContain("NOW() - INTERVAL '1 days' AS end_date");
|
||||
} else {
|
||||
expect(result).toContain('DATE_SUB(NOW(), INTERVAL 2 DAY) AS prev_start_date');
|
||||
expect(result).toContain('DATE_SUB(NOW(), INTERVAL 1 DAY) AS start_date');
|
||||
expect(result).toContain('DATE_SUB(NOW(), INTERVAL 1 DAY) AS end_date');
|
||||
}
|
||||
// Past range, take full days
|
||||
const startDateTime = DateTime.utc(2025, 10, 6, 14, 30, 0).startOf('day');
|
||||
const endDateTime = DateTime.utc(2025, 10, 7, 18, 45, 30).plus({ days: 1 }).startOf('day');
|
||||
const duration = endDateTime.diff(startDateTime);
|
||||
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: startDateTime.minus(duration),
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('handles same day with different times correctly (hour periodicity)', () => {
|
||||
// Oct 7 9:00 to Oct 7 17:00 (same day)
|
||||
// Now is Oct 8 8:51:27, so Oct 7 17:00 is less than 1 full day ago
|
||||
// useStartOfDay = false
|
||||
// daysDiff = 0 (same day), daysFromEndDateToToday = 1 (rounded)
|
||||
// daysFromStartDateToToday = 0 (floored), prevStartDaysFromToday = 0 + 0 = 0
|
||||
// Now is Oct 8 8:51:27
|
||||
const startDate = DateTime.utc(2025, 10, 7, 9, 0, 0).toJSDate();
|
||||
const endDate = DateTime.utc(2025, 10, 7, 17, 0, 0).toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
|
||||
// Verify the actual output based on the calculation
|
||||
if (dbType === 'sqlite') {
|
||||
expect(result).toContain("datetime('now') AS prev_start_date");
|
||||
expect(result).toContain("datetime('now') AS start_date");
|
||||
expect(result).toContain("datetime('now', '-1 days') AS end_date");
|
||||
} else if (dbType === 'postgresdb') {
|
||||
expect(result).toContain('NOW() AS prev_start_date');
|
||||
expect(result).toContain('NOW() AS start_date');
|
||||
expect(result).toContain("NOW() - INTERVAL '1 days' AS end_date");
|
||||
} else {
|
||||
expect(result).toContain('NOW() AS prev_start_date');
|
||||
expect(result).toContain('NOW() AS start_date');
|
||||
expect(result).toContain('DATE_SUB(NOW(), INTERVAL 1 DAY) AS end_date');
|
||||
}
|
||||
});
|
||||
|
||||
test('handles daylight saving time transition correctly', () => {
|
||||
// Simulate DST transition: Oct 22 (GMT+0200) to Nov 5 (GMT+0100)
|
||||
// Same wall-clock time but different timezone offset
|
||||
// Oct 26 2025 is when DST ends in Europe (clocks go back 1 hour)
|
||||
const startDate = DateTime.fromObject(
|
||||
{ year: 2025, month: 10, day: 22, hour: 12, minute: 37, second: 56 },
|
||||
{ zone: 'Europe/Paris' },
|
||||
).toJSDate();
|
||||
const endDate = DateTime.fromObject(
|
||||
{ year: 2025, month: 11, day: 5, hour: 12, minute: 37, second: 56 },
|
||||
{ zone: 'Europe/Paris' },
|
||||
).toJSDate();
|
||||
|
||||
// Mock current time to be Nov 5
|
||||
jest.setSystemTime(endDate);
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({ dbType, startDate, endDate });
|
||||
|
||||
// With DST normalization: Oct 22 to Nov 5 is 14 calendar days
|
||||
// The function detects same wall-clock time but different timezone offset
|
||||
// and normalizes to calculate correct calendar days
|
||||
expectLastXDaysDateRangeQuery({
|
||||
result,
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
prevStartDateOffset: 28, // 14 + 14
|
||||
startDateOffset: 14,
|
||||
});
|
||||
|
||||
// Restore original mock time
|
||||
jest.setSystemTime(now.toJSDate());
|
||||
// Past range, take full days
|
||||
const startDateTime = DateTime.utc(2025, 10, 7, 9, 0, 0).startOf('day');
|
||||
const endDateTime = DateTime.utc(2025, 10, 7, 17, 0, 0).plus({ days: 1 }).startOf('day');
|
||||
const duration = endDateTime.diff(startDateTime);
|
||||
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: startDateTime.minus(duration),
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('handle current day as both start and end date', () => {
|
||||
const startDate = now.toJSDate();
|
||||
const endDate = now.toJSDate();
|
||||
|
||||
const result = getDateRangesCommonTableExpressionQuery({
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
});
|
||||
|
||||
// startDate and endDate are today, so start is startOf('day'), end is now
|
||||
const startDateTime = now.startOf('day');
|
||||
const endDateTime = now;
|
||||
const duration = endDateTime.diff(startDateTime);
|
||||
|
||||
const expected = getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime: startDateTime.minus(duration),
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,148 +2,83 @@ import type { DatabaseConfig } from '@n8n/config';
|
|||
import { sql } from '@n8n/db';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
/**
|
||||
* Generates database-specific SQL for a datetime value relative to now
|
||||
* @param dbType - The database type
|
||||
* @param daysFromToday - Number of days back from today (0 = now)
|
||||
* @param useStartOfDay - Whether to truncate to start of day (00:00:00)
|
||||
*/
|
||||
const getDatetimeSql = ({
|
||||
dbType,
|
||||
daysFromToday,
|
||||
useStartOfDay = false,
|
||||
}: {
|
||||
dbType: DatabaseConfig['type'];
|
||||
daysFromToday: number;
|
||||
useStartOfDay?: boolean;
|
||||
}): string => {
|
||||
// Handle "now" case
|
||||
if (daysFromToday === 0 && !useStartOfDay) {
|
||||
return dbType === 'sqlite' ? "datetime('now')" : 'NOW()';
|
||||
}
|
||||
|
||||
// SQLite
|
||||
if (dbType === 'sqlite') {
|
||||
if (daysFromToday === 0 && useStartOfDay) {
|
||||
return "datetime('now', 'start of day')";
|
||||
}
|
||||
if (useStartOfDay) {
|
||||
return `datetime('now', '-${daysFromToday} days', 'start of day')`;
|
||||
}
|
||||
return `datetime('now', '-${daysFromToday} days')`;
|
||||
}
|
||||
|
||||
// PostgreSQL
|
||||
if (dbType === 'postgresdb') {
|
||||
if (daysFromToday === 0 && useStartOfDay) {
|
||||
return "DATE_TRUNC('day', NOW())";
|
||||
}
|
||||
if (useStartOfDay) {
|
||||
return `DATE_TRUNC('day', NOW() - INTERVAL '${daysFromToday} days')`;
|
||||
}
|
||||
return `NOW() - INTERVAL '${daysFromToday} days'`;
|
||||
}
|
||||
|
||||
// MySQL/MariaDB
|
||||
if (daysFromToday === 0 && useStartOfDay) {
|
||||
return 'DATE(NOW())';
|
||||
}
|
||||
if (useStartOfDay) {
|
||||
return `DATE(DATE_SUB(NOW(), INTERVAL ${daysFromToday} DAY))`;
|
||||
}
|
||||
return `DATE_SUB(NOW(), INTERVAL ${daysFromToday} DAY)`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a SQL Common Table Expression (CTE) query that provides three date boundaries for insights queries
|
||||
*
|
||||
* Behavior:
|
||||
* - If startDate and endDate are the same and today
|
||||
* - returns the last 24 hours: prev_start_date (2 days ago), start_date (1 day ago), end_date (now).
|
||||
* - Otherwise:
|
||||
* - prev_start_date: start of the day before the range
|
||||
* - start_date: start of the current range
|
||||
* - end_date: "now" if endDate is today, else start of the day after endDate
|
||||
* - If the end date is today and the start date is also today, start date is set to the start of the day to take today's data.
|
||||
* - If the end date is in the past, both start and end dates are set to the start of their respective days, to take full days.
|
||||
*
|
||||
* The SQL CTE can be joined with the insights table for filtering/aggregation.
|
||||
*
|
||||
* @param dbType - The database type ('sqlite', 'postgresdb', 'mysqldb', 'mariadb')
|
||||
* @param startDate - The start date of the range (inclusive)
|
||||
* @param endDate - The end date of the range (inclusive, or "now" if today)
|
||||
* @param dbType - The database type (postgresdb, mysqldb, mariadb, or sqlite)
|
||||
* @returns SQL CTE query with `prev_start_date`, `start_date`, and `end_date` columns
|
||||
* - `prev_start_date`: The start of the previous period (used for comparison)
|
||||
* - `start_date`: The start of the current period (inclusive)
|
||||
* - `end_date`: The end of the current period (exclusive)
|
||||
*/
|
||||
export const getDateRangesCommonTableExpressionQuery = ({
|
||||
dbType,
|
||||
startDate,
|
||||
endDate,
|
||||
dbType,
|
||||
}: {
|
||||
dbType: DatabaseConfig['type'];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
dbType: DatabaseConfig['type'];
|
||||
}) => {
|
||||
let today = DateTime.now();
|
||||
let startDateTime = DateTime.fromJSDate(startDate);
|
||||
let endDateTime = DateTime.fromJSDate(endDate);
|
||||
let startDateTime = DateTime.fromJSDate(startDate).toUTC();
|
||||
let endDateTime = DateTime.fromJSDate(endDate).toUTC();
|
||||
|
||||
// If the end date is in a past day, use start of day for both dates
|
||||
const useStartOfDay = today.diff(endDateTime, 'days').days >= 1;
|
||||
const today = DateTime.now().toUTC();
|
||||
const isEndDateToday = endDateTime.hasSame(today, 'day');
|
||||
|
||||
if (useStartOfDay) {
|
||||
// Past range, take full days
|
||||
if (!isEndDateToday) {
|
||||
startDateTime = startDateTime.startOf('day');
|
||||
endDateTime = endDateTime.startOf('day');
|
||||
today = today.startOf('day');
|
||||
endDateTime = endDateTime.plus({ days: 1 }).startOf('day');
|
||||
}
|
||||
|
||||
// Check if times are exactly the same but timezone differs (DST transition case)
|
||||
const offsetDiff = Math.abs(startDateTime.offset - endDateTime.offset);
|
||||
|
||||
// If same wall-clock time but different timezone offset (max 2 hours), normalize to same timezone
|
||||
if (
|
||||
startDateTime.hour === endDateTime.hour &&
|
||||
startDateTime.minute === endDateTime.minute &&
|
||||
startDateTime.second === endDateTime.second &&
|
||||
offsetDiff > 0 &&
|
||||
offsetDiff <= 120 // Max 2 hours difference in minutes
|
||||
) {
|
||||
// Change the startDateTime to so that time matches
|
||||
startDateTime = startDateTime.plus({ minutes: offsetDiff });
|
||||
// Today range, take all day data starting from the beginning of the day
|
||||
if (isEndDateToday && startDateTime.hasSame(endDateTime, 'day')) {
|
||||
startDateTime = startDateTime.startOf('day');
|
||||
}
|
||||
|
||||
// Convert to UTC to avoid DST issues when calculating day differences
|
||||
const startDateTimeUTC = startDateTime.toUTC();
|
||||
const endDateTimeUTC = endDateTime.toUTC();
|
||||
const todayUTC = today.toUTC();
|
||||
const prevStartDateTime = startDateTime.minus(endDateTime.diff(startDateTime));
|
||||
|
||||
const daysFromEndDateToToday = Math.round(todayUTC.diff(endDateTimeUTC, 'days').days);
|
||||
const daysDiff = Math.round(endDateTimeUTC.diff(startDateTimeUTC, 'days').days);
|
||||
return getDateRangesSelectQuery({ dbType, prevStartDateTime, startDateTime, endDateTime });
|
||||
};
|
||||
|
||||
const daysFromStartDateToToday = Math.floor(todayUTC.diff(startDateTimeUTC, 'days').days);
|
||||
const prevStartDaysFromToday = daysFromStartDateToToday + daysDiff;
|
||||
export function getDateRangesSelectQuery({
|
||||
dbType,
|
||||
prevStartDateTime,
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
}: {
|
||||
dbType: DatabaseConfig['type'];
|
||||
prevStartDateTime: DateTime;
|
||||
startDateTime: DateTime;
|
||||
endDateTime: DateTime;
|
||||
}) {
|
||||
const prevStartStr = prevStartDateTime.toSQL({ includeZone: false, includeOffset: false });
|
||||
const startStr = startDateTime.toSQL({ includeZone: false, includeOffset: false });
|
||||
const endStr = endDateTime.toSQL({ includeZone: false, includeOffset: false });
|
||||
|
||||
const prevStartDateSql = getDatetimeSql({
|
||||
dbType,
|
||||
daysFromToday: prevStartDaysFromToday,
|
||||
useStartOfDay,
|
||||
});
|
||||
|
||||
const startDateSql = getDatetimeSql({
|
||||
dbType,
|
||||
daysFromToday: daysFromStartDateToToday,
|
||||
useStartOfDay,
|
||||
});
|
||||
|
||||
const endDateSql = getDatetimeSql({
|
||||
dbType,
|
||||
daysFromToday: daysFromEndDateToToday,
|
||||
useStartOfDay,
|
||||
});
|
||||
// Database-specific timestamp casting
|
||||
// PostgreSQL requires explicit CAST or :: syntax for timestamp comparisons
|
||||
// SQLite and MySQL/MariaDB can work with string literals in comparisons
|
||||
if (dbType === 'postgresdb') {
|
||||
return sql`SELECT
|
||||
CAST('${prevStartStr}' AS TIMESTAMP) AS prev_start_date,
|
||||
CAST('${startStr}' AS TIMESTAMP) AS start_date,
|
||||
CAST('${endStr}' AS TIMESTAMP) AS end_date
|
||||
`;
|
||||
}
|
||||
|
||||
return sql`SELECT
|
||||
${prevStartDateSql} AS prev_start_date,
|
||||
${startDateSql} AS start_date,
|
||||
${endDateSql} AS end_date
|
||||
'${prevStartStr}' AS prev_start_date,
|
||||
'${startStr}' AS start_date,
|
||||
'${endStr}' AS end_date
|
||||
`;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user