|
| 1 | +import { getEnv } from '@cubejs-backend/shared'; |
| 2 | +import { PostgresQuery } from '../../../src/adapter'; |
| 3 | +import { prepareYamlCompiler } from '../../unit/PrepareCompiler'; |
| 4 | +import { dbRunner } from './PostgresDBRunner'; |
| 5 | + |
| 6 | +describe('Calendar cubes', () => { |
| 7 | + jest.setTimeout(200000); |
| 8 | + |
| 9 | + // language=YAML |
| 10 | + const { compiler, joinGraph, cubeEvaluator } = prepareYamlCompiler(` |
| 11 | +cubes: |
| 12 | + - name: calendar_orders |
| 13 | + sql: > |
| 14 | + SELECT |
| 15 | + gs.id, |
| 16 | + 100 + gs.id AS user_id, |
| 17 | + (ARRAY['new', 'processed', 'shipped'])[(gs.id % 3) + 1] AS status, |
| 18 | + make_timestamp( |
| 19 | + 2025, |
| 20 | + (gs.id % 12) + 1, |
| 21 | + 1 + (gs.id * 7 % 25), |
| 22 | + 0, |
| 23 | + 0, |
| 24 | + 0 |
| 25 | + ) AS created_at |
| 26 | + FROM generate_series(1, 40) AS gs(id) |
| 27 | +
|
| 28 | + joins: |
| 29 | + - name: custom_calendar |
| 30 | + sql: "{CUBE}.created_at = {custom_calendar.date_val}" |
| 31 | + relationship: many_to_one |
| 32 | +
|
| 33 | + dimensions: |
| 34 | + - name: id |
| 35 | + sql: id |
| 36 | + type: number |
| 37 | + primary_key: true |
| 38 | + public: true |
| 39 | +
|
| 40 | + - name: user_id |
| 41 | + sql: user_id |
| 42 | + type: number |
| 43 | +
|
| 44 | + - name: status |
| 45 | + sql: status |
| 46 | + type: string |
| 47 | + meta: |
| 48 | + addDesc: The status of order |
| 49 | + moreNum: 42 |
| 50 | +
|
| 51 | + - name: created_at |
| 52 | + sql: created_at |
| 53 | + type: time |
| 54 | +
|
| 55 | + measures: |
| 56 | + - name: count |
| 57 | + type: count |
| 58 | +
|
| 59 | + - name: count_shifted |
| 60 | + type: count |
| 61 | + multi_stage: true |
| 62 | + sql: "{count}" |
| 63 | + time_shift: |
| 64 | + - time_dimension: created_at |
| 65 | + interval: 1 year |
| 66 | + type: prior |
| 67 | +
|
| 68 | + - name: completed_count |
| 69 | + type: count |
| 70 | + filters: |
| 71 | + - sql: "{CUBE}.status = 'completed'" |
| 72 | +
|
| 73 | + - name: completed_percentage |
| 74 | + sql: "({completed_count} / NULLIF({count}, 0)) * 100.0" |
| 75 | + type: number |
| 76 | + format: percent |
| 77 | +
|
| 78 | + - name: total |
| 79 | + type: count |
| 80 | + rolling_window: |
| 81 | + trailing: unbounded |
| 82 | +
|
| 83 | + - name: custom_calendar |
| 84 | + sql: > |
| 85 | + WITH base AS ( |
| 86 | + SELECT |
| 87 | + gs.n - 1 AS day_offset, |
| 88 | + DATE '2025-02-02' + (gs.n - 1) AS date_val |
| 89 | + FROM generate_series(1, 364) AS gs(n) |
| 90 | + ), |
| 91 | + retail_calc AS ( |
| 92 | + SELECT |
| 93 | + date_val, |
| 94 | + date_val AS retail_date, |
| 95 | + '2025' AS retail_year_name, |
| 96 | + (day_offset / 7) + 1 AS retail_week, |
| 97 | + -- Group of months 4-5-4 (13 weeks = 3 months) |
| 98 | + ((day_offset / 7) / 13) + 1 AS retail_quarter, |
| 99 | + (day_offset / 7) % 13 AS week_in_quarter, |
| 100 | + DATE '2025-02-02' AS retail_year_begin_date |
| 101 | + FROM base |
| 102 | + ), |
| 103 | + final AS ( |
| 104 | + SELECT |
| 105 | + date_val, |
| 106 | + retail_date, |
| 107 | + retail_year_name, |
| 108 | + ('Retail Month ' || ((retail_quarter - 1) * 3 + |
| 109 | + CASE |
| 110 | + WHEN week_in_quarter < 4 THEN 1 |
| 111 | + WHEN week_in_quarter < 9 THEN 2 |
| 112 | + ELSE 3 |
| 113 | + END)) AS retail_month_long_name, |
| 114 | + ('WK' || LPAD(retail_week::text, 2, '0')) AS retail_week_name, |
| 115 | + retail_year_begin_date, |
| 116 | + ('Q' || retail_quarter || ' 2025') AS retail_quarter_year, |
| 117 | + (SELECT MIN(date_val) FROM retail_calc r2 |
| 118 | + WHERE r2.retail_quarter = r.retail_quarter |
| 119 | + AND CASE |
| 120 | + WHEN week_in_quarter < 4 THEN 1 |
| 121 | + WHEN week_in_quarter < 9 THEN 2 |
| 122 | + ELSE 3 |
| 123 | + END = |
| 124 | + CASE |
| 125 | + WHEN r.week_in_quarter < 4 THEN 1 |
| 126 | + WHEN r.week_in_quarter < 9 THEN 2 |
| 127 | + ELSE 3 |
| 128 | + END |
| 129 | + ) AS retail_month_begin_date, |
| 130 | + date_val - (extract(dow from date_val)::int) AS retail_week_begin_date, |
| 131 | + ('2025-WK' || LPAD(retail_week::text, 2, '0')) AS retail_year_week |
| 132 | + FROM retail_calc r |
| 133 | + ) |
| 134 | + SELECT * |
| 135 | + FROM final |
| 136 | + ORDER BY date_val |
| 137 | +
|
| 138 | + calendar: true |
| 139 | +
|
| 140 | + dimensions: |
| 141 | + # Plain date value |
| 142 | + - name: date_val |
| 143 | + sql: "{CUBE}.date_val" |
| 144 | + type: time |
| 145 | + primary_key: true |
| 146 | +
|
| 147 | + ##### Retail Dates #### |
| 148 | + - name: retail_date |
| 149 | + sql: retail_date |
| 150 | + type: time |
| 151 | +
|
| 152 | + granularities: |
| 153 | + - name: year |
| 154 | + sql: "{CUBE.retail_year_begin_date}" |
| 155 | +
|
| 156 | + - name: quarter |
| 157 | + sql: "{CUBE.retail_quarter_year}" |
| 158 | +
|
| 159 | + - name: month |
| 160 | + sql: "{CUBE.retail_month_begin_date}" |
| 161 | +
|
| 162 | + - name: week |
| 163 | + sql: "{CUBE.retail_week_begin_date}" |
| 164 | +
|
| 165 | + # Casually defining custom granularities should also work. |
| 166 | + # While maybe not very sound from a business standpoint, |
| 167 | + # such definition should be allowed in this data model |
| 168 | + - name: fortnight |
| 169 | + interval: 2 week |
| 170 | + origin: "2025-01-01" |
| 171 | +
|
| 172 | + - name: retail_year |
| 173 | + sql: "{CUBE}.retail_year_name" |
| 174 | + type: string |
| 175 | +
|
| 176 | + - name: retail_month_long_name |
| 177 | + sql: "{CUBE}.retail_month_long_name" |
| 178 | + type: string |
| 179 | +
|
| 180 | + - name: retail_week_name |
| 181 | + sql: "{CUBE}.retail_week_name" |
| 182 | + type: string |
| 183 | +
|
| 184 | + - name: retail_year_begin_date |
| 185 | + sql: "{CUBE}.retail_year_begin_date" |
| 186 | + type: time |
| 187 | +
|
| 188 | + - name: retail_quarter_year |
| 189 | + sql: "{CUBE}.retail_quarter_year" |
| 190 | + type: string |
| 191 | +
|
| 192 | + - name: retail_month_begin_date |
| 193 | + sql: "{CUBE}.retail_month_begin_date" |
| 194 | + type: string |
| 195 | +
|
| 196 | + - name: retail_week_begin_date |
| 197 | + sql: "{CUBE}.retail_week_begin_date" |
| 198 | + type: string |
| 199 | +
|
| 200 | + - name: retail_year_week |
| 201 | + sql: "{CUBE}.retail_year_week" |
| 202 | + type: string |
| 203 | + `); |
| 204 | + |
| 205 | + async function runQueryTest(q: any, expectedResult: any) { |
| 206 | + // Calendars are working only with Tesseract SQL planner |
| 207 | + if (!getEnv('nativeSqlPlanner')) { |
| 208 | + return; |
| 209 | + } |
| 210 | + |
| 211 | + await compiler.compile(); |
| 212 | + const query = new PostgresQuery( |
| 213 | + { joinGraph, cubeEvaluator, compiler }, |
| 214 | + { ...q, timezone: 'UTC', preAggregationsSchema: '' } |
| 215 | + ); |
| 216 | + |
| 217 | + const qp = query.buildSqlAndParams(); |
| 218 | + console.log(qp); |
| 219 | + |
| 220 | + const res = await dbRunner.testQuery(qp); |
| 221 | + console.log(JSON.stringify(res)); |
| 222 | + |
| 223 | + expect(res).toEqual( |
| 224 | + expectedResult |
| 225 | + ); |
| 226 | + } |
| 227 | + |
| 228 | + it('Count by retail year', async () => runQueryTest({ |
| 229 | + measures: ['calendar_orders.count'], |
| 230 | + timeDimensions: [{ |
| 231 | + dimension: 'custom_calendar.retail_date', |
| 232 | + granularity: 'year', |
| 233 | + dateRange: ['2025-02-01', '2026-03-01'] |
| 234 | + }], |
| 235 | + order: [{ id: 'custom_calendar.retail_date' }] |
| 236 | + }, [ |
| 237 | + { |
| 238 | + calendar_orders__count: '36', |
| 239 | + custom_calendar__retail_date_year: '2025-02-02T00:00:00.000Z', |
| 240 | + } |
| 241 | + ])); |
| 242 | + |
| 243 | + it('Count by retail month', async () => runQueryTest({ |
| 244 | + measures: ['calendar_orders.count'], |
| 245 | + timeDimensions: [{ |
| 246 | + dimension: 'custom_calendar.retail_date', |
| 247 | + granularity: 'month', |
| 248 | + dateRange: ['2025-02-01', '2026-03-01'] |
| 249 | + }], |
| 250 | + order: [{ id: 'custom_calendar.retail_date' }] |
| 251 | + }, [ |
| 252 | + { |
| 253 | + calendar_orders__count: '3', |
| 254 | + custom_calendar__retail_date_month: '2025-02-02T00:00:00.000Z', |
| 255 | + }, |
| 256 | + { |
| 257 | + calendar_orders__count: '4', |
| 258 | + custom_calendar__retail_date_month: '2025-03-02T00:00:00.000Z', |
| 259 | + }, |
| 260 | + { |
| 261 | + calendar_orders__count: '4', |
| 262 | + custom_calendar__retail_date_month: '2025-04-06T00:00:00.000Z', |
| 263 | + }, |
| 264 | + { |
| 265 | + calendar_orders__count: '4', |
| 266 | + custom_calendar__retail_date_month: '2025-05-04T00:00:00.000Z', |
| 267 | + }, |
| 268 | + { |
| 269 | + calendar_orders__count: '4', |
| 270 | + custom_calendar__retail_date_month: '2025-06-01T00:00:00.000Z', |
| 271 | + }, |
| 272 | + { |
| 273 | + calendar_orders__count: '2', |
| 274 | + custom_calendar__retail_date_month: '2025-07-06T00:00:00.000Z', |
| 275 | + }, |
| 276 | + { |
| 277 | + calendar_orders__count: '3', |
| 278 | + custom_calendar__retail_date_month: '2025-08-03T00:00:00.000Z', |
| 279 | + }, |
| 280 | + { |
| 281 | + calendar_orders__count: '3', |
| 282 | + custom_calendar__retail_date_month: '2025-08-31T00:00:00.000Z', |
| 283 | + }, |
| 284 | + { |
| 285 | + calendar_orders__count: '3', |
| 286 | + custom_calendar__retail_date_month: '2025-10-05T00:00:00.000Z', |
| 287 | + }, |
| 288 | + { |
| 289 | + calendar_orders__count: '3', |
| 290 | + custom_calendar__retail_date_month: '2025-11-02T00:00:00.000Z', |
| 291 | + }, |
| 292 | + { |
| 293 | + calendar_orders__count: '3', |
| 294 | + custom_calendar__retail_date_month: '2025-11-30T00:00:00.000Z', |
| 295 | + } |
| 296 | + ])); |
| 297 | + |
| 298 | + it('Count by retail week', async () => runQueryTest({ |
| 299 | + measures: ['calendar_orders.count'], |
| 300 | + timeDimensions: [{ |
| 301 | + dimension: 'custom_calendar.retail_date', |
| 302 | + granularity: 'week', |
| 303 | + dateRange: ['2025-02-01', '2025-04-01'] |
| 304 | + }], |
| 305 | + order: [{ id: 'custom_calendar.retail_date' }] |
| 306 | + }, [ |
| 307 | + { |
| 308 | + calendar_orders__count: '1', |
| 309 | + custom_calendar__retail_date_week: '2025-02-02T00:00:00.000Z', |
| 310 | + }, |
| 311 | + { |
| 312 | + calendar_orders__count: '1', |
| 313 | + custom_calendar__retail_date_week: '2025-02-09T00:00:00.000Z', |
| 314 | + }, |
| 315 | + { |
| 316 | + calendar_orders__count: '1', |
| 317 | + custom_calendar__retail_date_week: '2025-02-16T00:00:00.000Z', |
| 318 | + }, |
| 319 | + { |
| 320 | + calendar_orders__count: '1', |
| 321 | + custom_calendar__retail_date_week: '2025-03-02T00:00:00.000Z', |
| 322 | + }, |
| 323 | + { |
| 324 | + calendar_orders__count: '1', |
| 325 | + custom_calendar__retail_date_week: '2025-03-09T00:00:00.000Z', |
| 326 | + }, |
| 327 | + { |
| 328 | + calendar_orders__count: '1', |
| 329 | + custom_calendar__retail_date_week: '2025-03-16T00:00:00.000Z', |
| 330 | + }, |
| 331 | + { |
| 332 | + calendar_orders__count: '1', |
| 333 | + custom_calendar__retail_date_week: '2025-03-23T00:00:00.000Z', |
| 334 | + } |
| 335 | + ])); |
| 336 | + |
| 337 | + it('Count by fortnight custom granularity', async () => runQueryTest({ |
| 338 | + measures: ['calendar_orders.count'], |
| 339 | + timeDimensions: [{ |
| 340 | + dimension: 'custom_calendar.retail_date', |
| 341 | + granularity: 'fortnight', |
| 342 | + dateRange: ['2025-02-01', '2025-04-01'] |
| 343 | + }], |
| 344 | + order: [{ id: 'custom_calendar.retail_date' }] |
| 345 | + }, [ |
| 346 | + { |
| 347 | + calendar_orders__count: '2', |
| 348 | + custom_calendar__retail_date_fortnight: '2025-01-29T00:00:00.000Z', // Notice it starts on 2025-01-29, not 2025-02-01 |
| 349 | + }, |
| 350 | + { |
| 351 | + calendar_orders__count: '1', |
| 352 | + custom_calendar__retail_date_fortnight: '2025-02-12T00:00:00.000Z', |
| 353 | + }, |
| 354 | + { |
| 355 | + calendar_orders__count: '1', |
| 356 | + custom_calendar__retail_date_fortnight: '2025-02-26T00:00:00.000Z', |
| 357 | + }, |
| 358 | + { |
| 359 | + calendar_orders__count: '3', |
| 360 | + custom_calendar__retail_date_fortnight: '2025-03-12T00:00:00.000Z', |
| 361 | + } |
| 362 | + ])); |
| 363 | +}); |
0 commit comments