@malloydata/malloy 0.0.321 → 0.0.323

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,43 +46,53 @@ function sqlSumDistinct(dialect, sqlExp, sqlDistintKey) {
46
46
  return ret;
47
47
  }
48
48
  /**
49
- * Converts an expression to SQL.
50
- * This function was extracted from QueryField.exprToSQL to break circular dependencies.
49
+ * Deep copies an expression tree, preserving structure and types.
51
50
  */
52
- function exprToSQL(resultSet, context, exprToTranslate, state = new utils_1.GenerateState()) {
53
- var _a;
54
- // Wrap non leaf sub expressions in parenthesis
55
- const subExpr = function (e) {
56
- const sql = exprToSQL(resultSet, context, e, state);
57
- if ((0, malloy_types_1.exprHasKids)(e)) {
58
- return `(${sql})`;
51
+ function deepCopyExpr(expr) {
52
+ if ((0, malloy_types_1.exprHasE)(expr)) {
53
+ return { ...expr, e: deepCopyExpr(expr.e) };
54
+ }
55
+ else if ((0, malloy_types_1.exprHasKids)(expr)) {
56
+ const copiedKids = {};
57
+ for (const [name, kidExpr] of Object.entries(expr.kids)) {
58
+ if (kidExpr === null) {
59
+ copiedKids[name] = null;
60
+ }
61
+ else if (Array.isArray(kidExpr)) {
62
+ copiedKids[name] = kidExpr.map(e => deepCopyExpr(e));
63
+ }
64
+ else {
65
+ copiedKids[name] = deepCopyExpr(kidExpr);
66
+ }
59
67
  }
60
- return sql;
61
- };
68
+ return { ...expr, kids: copiedKids };
69
+ }
70
+ return { ...expr };
71
+ }
72
+ /**
73
+ * Compiles an expression tree by mutating it in-place to set all .sql fields.
74
+ * Assumes the expression tree is already a copy that can be mutated.
75
+ */
76
+ function compileExpr(resultSet, context, expr, state = new utils_1.GenerateState(), wrap = true) {
62
77
  /*
63
78
  * Translate the children first, and stash the translation
64
79
  * in the nodes themselves, so that if we call into the dialect
65
80
  * it will have access to the translated children.
66
81
  */
67
- let expr = exprToTranslate;
68
- if ((0, malloy_types_1.exprHasE)(exprToTranslate)) {
69
- expr = { ...exprToTranslate };
70
- const eSql = subExpr(expr.e);
71
- expr.e = { ...expr.e, sql: eSql };
72
- }
73
- else if ((0, malloy_types_1.exprHasKids)(exprToTranslate)) {
74
- expr = { ...exprToTranslate };
75
- const oldKids = exprToTranslate.kids;
76
- for (const [name, kidExpr] of Object.entries(oldKids)) {
82
+ if ((0, malloy_types_1.exprHasE)(expr)) {
83
+ compileExpr(resultSet, context, expr.e, state);
84
+ }
85
+ else if ((0, malloy_types_1.exprHasKids)(expr)) {
86
+ for (const kidExpr of Object.values(expr.kids)) {
77
87
  if (kidExpr === null)
78
88
  continue;
79
89
  if (Array.isArray(kidExpr)) {
80
- expr.kids[name] = kidExpr.map(e => {
81
- return { ...e, sql: subExpr(e) };
82
- });
90
+ for (const e of kidExpr) {
91
+ compileExpr(resultSet, context, e, state);
92
+ }
83
93
  }
84
94
  else {
85
- expr.kids[name] = { ...oldKids[name], sql: subExpr(kidExpr) };
95
+ compileExpr(resultSet, context, kidExpr, state);
86
96
  }
87
97
  }
88
98
  }
@@ -92,132 +102,149 @@ function exprToSQL(resultSet, context, exprToTranslate, state = new utils_1.Gene
92
102
  const qi = resultSet.getQueryInfo();
93
103
  const dialectSQL = context.dialect.exprToSQL(qi, expr);
94
104
  if (dialectSQL) {
95
- return dialectSQL;
96
- }
97
- switch (expr.node) {
98
- case 'field':
99
- return generateFieldFragment(resultSet, context, expr, state);
100
- case 'parameter':
101
- return generateParameterFragment(resultSet, context, expr, state);
102
- case 'filteredExpr':
103
- return generateFilterFragment(resultSet, context, expr, state);
104
- case 'all':
105
- case 'exclude':
106
- return generateUngroupedFragment(resultSet, context, expr, state);
107
- case 'genericSQLExpr':
108
- return Array.from(stringsFromSQLExpression(resultSet, context, expr, state)).join('');
109
- case 'aggregate': {
110
- let agg = '';
111
- if (expr.function === 'sum') {
112
- agg = generateSumFragment(resultSet, context, expr, state);
113
- }
114
- else if (expr.function === 'avg') {
115
- agg = generateAvgFragment(resultSet, context, expr, state);
105
+ expr.sql = wrap && (0, malloy_types_1.exprHasKids)(expr) ? `(${dialectSQL})` : dialectSQL;
106
+ return expr;
107
+ }
108
+ const sql = (() => {
109
+ var _a;
110
+ switch (expr.node) {
111
+ case 'field':
112
+ return generateFieldFragment(resultSet, context, expr, state);
113
+ case 'parameter':
114
+ return generateParameterFragment(resultSet, context, expr, state);
115
+ case 'filteredExpr':
116
+ return generateFilterFragment(resultSet, context, expr, state);
117
+ case 'all':
118
+ case 'exclude':
119
+ return generateUngroupedFragment(resultSet, context, expr, state);
120
+ case 'genericSQLExpr':
121
+ return Array.from(stringsFromSQLExpression(resultSet, context, expr, state)).join('');
122
+ case 'aggregate': {
123
+ let agg = '';
124
+ if (expr.function === 'sum') {
125
+ agg = generateSumFragment(resultSet, context, expr, state);
126
+ }
127
+ else if (expr.function === 'avg') {
128
+ agg = generateAvgFragment(resultSet, context, expr, state);
129
+ }
130
+ else if (expr.function === 'count') {
131
+ agg = generateCountFragment(resultSet, context, expr, state);
132
+ }
133
+ else if (expr.function === 'min' ||
134
+ expr.function === 'max' ||
135
+ expr.function === 'distinct') {
136
+ agg = generateSymmetricFragment(resultSet, context, expr, state);
137
+ }
138
+ else {
139
+ throw new Error(`Internal Error: Unknown aggregate function ${expr.function}`);
140
+ }
141
+ if (resultSet.root().isComplexQuery) {
142
+ let groupSet = resultSet.groupSet;
143
+ if (state.totalGroupSet !== -1) {
144
+ groupSet = state.totalGroupSet;
145
+ }
146
+ return (0, utils_1.caseGroup)([groupSet], agg);
147
+ }
148
+ return agg;
116
149
  }
117
- else if (expr.function === 'count') {
118
- agg = generateCountFragment(resultSet, context, expr, state);
150
+ case 'function_parameter':
151
+ throw new Error('Internal Error: Function parameter fragment remaining during SQL generation');
152
+ case 'outputField':
153
+ return generateOutputFieldFragment(resultSet, context, expr, state);
154
+ case 'function_call':
155
+ return generateFunctionCallExpression(resultSet, context, expr, state);
156
+ case 'spread':
157
+ throw new Error("Internal Error: expandFunctionCall() failed to process node: 'spread'");
158
+ case 'source-reference':
159
+ return generateSourceReference(resultSet, context, expr);
160
+ case '+':
161
+ case '-':
162
+ case '*':
163
+ case '%':
164
+ case '/':
165
+ case '>':
166
+ case '<':
167
+ case '>=':
168
+ case '<=':
169
+ case '=':
170
+ return `${expr.kids.left.sql}${expr.node}${expr.kids.right.sql}`;
171
+ // Malloy inequality comparisons always return a boolean
172
+ case '!=': {
173
+ const notEqual = `${expr.kids.left.sql}!=${expr.kids.right.sql}`;
174
+ return `COALESCE(${notEqual},true)`;
119
175
  }
120
- else if (expr.function === 'min' ||
121
- expr.function === 'max' ||
122
- expr.function === 'distinct') {
123
- agg = generateSymmetricFragment(resultSet, context, expr, state);
176
+ case 'and':
177
+ case 'or':
178
+ return `${expr.kids.left.sql} ${expr.node} ${expr.kids.right.sql}`;
179
+ case 'coalesce':
180
+ return `COALESCE(${expr.kids.left.sql},${expr.kids.right.sql})`;
181
+ case 'in': {
182
+ const oneOf = expr.kids.oneOf.map(o => o.sql).join(',');
183
+ return `${expr.kids.e.sql} ${expr.not ? 'NOT IN' : 'IN'} (${oneOf})`;
124
184
  }
125
- else {
126
- throw new Error(`Internal Error: Unknown aggregate function ${expr.function}`);
185
+ case 'like':
186
+ case '!like': {
187
+ const likeIt = expr.node === 'like' ? 'LIKE' : 'NOT LIKE';
188
+ const compare = expr.kids.right.node === 'stringLiteral'
189
+ ? context.dialect.sqlLike(likeIt, (_a = expr.kids.left.sql) !== null && _a !== void 0 ? _a : '', expr.kids.right.literal)
190
+ : `${expr.kids.left.sql} ${likeIt} ${expr.kids.right.sql}`;
191
+ return expr.node === 'like' ? compare : `COALESCE(${compare},true)`;
127
192
  }
128
- if (resultSet.root().isComplexQuery) {
129
- let groupSet = resultSet.groupSet;
130
- if (state.totalGroupSet !== -1) {
131
- groupSet = state.totalGroupSet;
193
+ case '()':
194
+ return `(${expr.e.sql})`;
195
+ case 'not':
196
+ // Malloy not operator always returns a boolean
197
+ return `COALESCE(NOT ${expr.e.sql},TRUE)`;
198
+ case 'unary-':
199
+ return `-${expr.e.sql}`;
200
+ case 'is-null':
201
+ return `${expr.e.sql} IS NULL`;
202
+ case 'is-not-null':
203
+ return `${expr.e.sql} IS NOT NULL`;
204
+ case 'true':
205
+ case 'false':
206
+ return expr.node;
207
+ case 'null':
208
+ return 'NULL';
209
+ case 'case':
210
+ return generateCaseSQL(expr);
211
+ case '':
212
+ return '';
213
+ case 'filterCondition':
214
+ // our child will be translated at the top of this function
215
+ if (expr.e.sql) {
216
+ expr.sql = expr.e.sql;
217
+ return expr.sql;
132
218
  }
133
- return (0, utils_1.caseGroup)([groupSet], agg);
134
- }
135
- return agg;
136
- }
137
- case 'function_parameter':
138
- throw new Error('Internal Error: Function parameter fragment remaining during SQL generation');
139
- case 'outputField':
140
- return generateOutputFieldFragment(resultSet, context, expr, state);
141
- case 'function_call':
142
- return generateFunctionCallExpression(resultSet, context, expr, state);
143
- case 'spread':
144
- throw new Error("Internal Error: expandFunctionCall() failed to process node: 'spread'");
145
- case 'source-reference':
146
- return generateSourceReference(resultSet, context, expr);
147
- case '+':
148
- case '-':
149
- case '*':
150
- case '%':
151
- case '/':
152
- case '>':
153
- case '<':
154
- case '>=':
155
- case '<=':
156
- case '=':
157
- return `${expr.kids.left.sql}${expr.node}${expr.kids.right.sql}`;
158
- // Malloy inequality comparisons always return a boolean
159
- case '!=': {
160
- const notEqual = `${expr.kids.left.sql}!=${expr.kids.right.sql}`;
161
- return `COALESCE(${notEqual},true)`;
162
- }
163
- case 'and':
164
- case 'or':
165
- return `${expr.kids.left.sql} ${expr.node} ${expr.kids.right.sql}`;
166
- case 'coalesce':
167
- return `COALESCE(${expr.kids.left.sql},${expr.kids.right.sql})`;
168
- case 'in': {
169
- const oneOf = expr.kids.oneOf.map(o => o.sql).join(',');
170
- return `${expr.kids.e.sql} ${expr.not ? 'NOT IN' : 'IN'} (${oneOf})`;
171
- }
172
- case 'like':
173
- case '!like': {
174
- const likeIt = expr.node === 'like' ? 'LIKE' : 'NOT LIKE';
175
- const compare = expr.kids.right.node === 'stringLiteral'
176
- ? context.dialect.sqlLike(likeIt, (_a = expr.kids.left.sql) !== null && _a !== void 0 ? _a : '', expr.kids.right.literal)
177
- : `${expr.kids.left.sql} ${likeIt} ${expr.kids.right.sql}`;
178
- return expr.node === 'like' ? compare : `COALESCE(${compare},true)`;
219
+ return '';
220
+ case 'functionDefaultOrderBy':
221
+ case 'functionOrderBy':
222
+ return '';
223
+ // TODO: throw an error here; not simple because we call into this
224
+ // code currently before the composite source is resolved in some cases
225
+ case 'compositeField':
226
+ return '{COMPOSITE_FIELD}';
227
+ case 'filterMatch':
228
+ return generateAppliedFilter(context, expr, qi);
229
+ case 'filterLiteral':
230
+ return 'INTERNAL ERROR FILTER EXPRESSION VALUE SHOULD NOT BE USED';
231
+ default:
232
+ throw new Error(`Internal Error: Unknown expression node '${expr.node}' ${JSON.stringify(expr, undefined, 2)}`);
179
233
  }
180
- case '()':
181
- return `(${expr.e.sql})`;
182
- case 'not':
183
- // Malloy not operator always returns a boolean
184
- return `COALESCE(NOT ${expr.e.sql},TRUE)`;
185
- case 'unary-':
186
- return `-${expr.e.sql}`;
187
- case 'is-null':
188
- return `${expr.e.sql} IS NULL`;
189
- case 'is-not-null':
190
- return `${expr.e.sql} IS NOT NULL`;
191
- case 'true':
192
- case 'false':
193
- return expr.node;
194
- case 'null':
195
- return 'NULL';
196
- case 'case':
197
- return generateCaseSQL(expr);
198
- case '':
199
- return '';
200
- case 'filterCondition':
201
- // our child will be translated at the top of this function
202
- if (expr.e.sql) {
203
- expr.sql = expr.e.sql;
204
- return expr.sql;
205
- }
206
- return '';
207
- case 'functionDefaultOrderBy':
208
- case 'functionOrderBy':
209
- return '';
210
- // TODO: throw an error here; not simple because we call into this
211
- // code currently before the composite source is resolved in some cases
212
- case 'compositeField':
213
- return '{COMPOSITE_FIELD}';
214
- case 'filterMatch':
215
- return generateAppliedFilter(context, expr, qi);
216
- case 'filterLiteral':
217
- return 'INTERNAL ERROR FILTER EXPRESSION VALUE SHOULD NOT BE USED';
218
- default:
219
- throw new Error(`Internal Error: Unknown expression node '${expr.node}' ${JSON.stringify(expr, undefined, 2)}`);
220
- }
234
+ })();
235
+ expr.sql = wrap && (0, malloy_types_1.exprHasKids)(expr) ? `(${sql})` : sql;
236
+ return expr;
237
+ }
238
+ /**
239
+ * Converts an expression to SQL.
240
+ * This function was extracted from QueryField.exprToSQL to break circular dependencies.
241
+ */
242
+ function exprToSQL(resultSet, context, exprToTranslate, state = new utils_1.GenerateState()) {
243
+ // Make a deep copy that we can mutate during compilation
244
+ const exprCopy = deepCopyExpr(exprToTranslate);
245
+ // Compile the copy, setting .sql on all nodes
246
+ const compiled = compileExpr(resultSet, context, exprCopy, state, false);
247
+ return compiled.sql;
221
248
  }
222
249
  function generateAppliedFilter(context, filterMatchExpr, qi) {
223
250
  var _a;
@@ -25,9 +25,7 @@ export declare class TemporalFilterCompiler {
25
25
  private literalNode;
26
26
  private nowExpr;
27
27
  private n;
28
- private delta;
29
28
  private dayofWeek;
30
- private nowDot;
31
29
  private thisUnit;
32
30
  private lastUnit;
33
31
  private nextUnit;
@@ -340,18 +340,32 @@ class TemporalFilterCompiler {
340
340
  }
341
341
  case 'for': {
342
342
  const start = this.moment(tc.begin);
343
- const end = this.delta(start.begin, '+', tc.n, tc.units);
344
- return this.isIn(tc.not, start.begin.sql, end.sql);
343
+ // start.begin could be any moment (literal, "last monday", etc.)
344
+ // so we can't optimize through its already-generated SQL
345
+ const endSql = this.d.sqlTruncAndOffset((0, malloy_types_1.mkTemporal)(start.begin, 'timestamp'), this.qi, undefined, {
346
+ op: '+',
347
+ magnitude: tc.n,
348
+ unit: tc.units,
349
+ });
350
+ return this.isIn(tc.not, start.begin.sql, endSql);
345
351
  }
346
352
  case 'in_last': {
347
353
  // last N units means "N - 1 UNITS AGO FOR N UNITS"
348
354
  const back = Number(tc.n) - 1;
349
- const thisUnit = this.nowDot(tc.units);
355
+ const now = this.nowExpr();
350
356
  const start = back > 0
351
- ? this.delta(thisUnit, '-', back.toString(), tc.units)
352
- : thisUnit;
353
- const end = this.delta(thisUnit, '+', '1', tc.units);
354
- return this.isIn(tc.not, start.sql, end.sql);
357
+ ? this.d.sqlTruncAndOffset(now, this.qi, tc.units, {
358
+ op: '-',
359
+ magnitude: back.toString(),
360
+ unit: tc.units,
361
+ })
362
+ : this.d.sqlTruncAndOffset(now, this.qi, tc.units);
363
+ const end = this.d.sqlTruncAndOffset(now, this.qi, tc.units, {
364
+ op: '+',
365
+ magnitude: '1',
366
+ unit: tc.units,
367
+ });
368
+ return this.isIn(tc.not, start, end);
355
369
  }
356
370
  case 'to': {
357
371
  const firstMoment = this.moment(tc.fromMoment);
@@ -359,15 +373,28 @@ class TemporalFilterCompiler {
359
373
  return this.isIn(tc.not, firstMoment.begin.sql, lastMoment.begin.sql);
360
374
  }
361
375
  case 'last': {
362
- const thisUnit = this.nowDot(tc.units);
363
- const start = this.delta(thisUnit, '-', tc.n, tc.units);
364
- return this.isIn(tc.not, start.sql, thisUnit.sql);
376
+ const now = this.nowExpr();
377
+ const start = this.d.sqlTruncAndOffset(now, this.qi, tc.units, {
378
+ op: '-',
379
+ magnitude: tc.n,
380
+ unit: tc.units,
381
+ });
382
+ const end = this.d.sqlTruncAndOffset(now, this.qi, tc.units);
383
+ return this.isIn(tc.not, start, end);
365
384
  }
366
385
  case 'next': {
367
- const thisUnit = this.nowDot(tc.units);
368
- const start = this.delta(thisUnit, '+', '1', tc.units);
369
- const end = this.delta(thisUnit, '+', (Number(tc.n) + 1).toString(), tc.units);
370
- return this.isIn(tc.not, start.sql, end.sql);
386
+ const now = this.nowExpr();
387
+ const start = this.d.sqlTruncAndOffset(now, this.qi, tc.units, {
388
+ op: '+',
389
+ magnitude: '1',
390
+ unit: tc.units,
391
+ });
392
+ const end = this.d.sqlTruncAndOffset(now, this.qi, tc.units, {
393
+ op: '+',
394
+ magnitude: (Number(tc.n) + 1).toString(),
395
+ unit: tc.units,
396
+ });
397
+ return this.isIn(tc.not, start, end);
371
398
  }
372
399
  case 'null':
373
400
  return tc.not ? `${x} IS NOT NULL` : `${x} IS NULL`;
@@ -476,18 +503,6 @@ class TemporalFilterCompiler {
476
503
  n(literal) {
477
504
  return { node: 'numberLiteral', literal, sql: literal };
478
505
  }
479
- delta(from, op, n, units) {
480
- const ret = {
481
- node: 'delta',
482
- op,
483
- units,
484
- kids: {
485
- base: (0, malloy_types_1.mkTemporal)(from, 'timestamp'),
486
- delta: this.n(n),
487
- },
488
- };
489
- return { ...ret, sql: this.d.sqlAlterTimeExpr(ret) };
490
- }
491
506
  dayofWeek(e) {
492
507
  const t = {
493
508
  node: 'extract',
@@ -496,29 +511,52 @@ class TemporalFilterCompiler {
496
511
  };
497
512
  return { ...t, sql: this.d.sqlTimeExtractExpr(this.qi, t) };
498
513
  }
499
- nowDot(units) {
500
- const nowTruncExpr = {
501
- node: 'trunc',
502
- e: this.nowExpr(),
503
- units,
504
- };
505
- return { ...nowTruncExpr, sql: this.d.sqlTruncExpr(this.qi, nowTruncExpr) };
506
- }
507
514
  thisUnit(units) {
508
- const thisUnit = this.nowDot(units);
509
- const nextUnit = this.delta(thisUnit, '+', '1', units);
510
- return { begin: thisUnit, end: nextUnit.sql };
515
+ const now = this.nowExpr();
516
+ const beginSql = this.d.sqlTruncAndOffset(now, this.qi, units);
517
+ const endSql = this.d.sqlTruncAndOffset(now, this.qi, units, {
518
+ op: '+',
519
+ magnitude: '1',
520
+ unit: units,
521
+ });
522
+ const beginNode = { node: 'trunc', e: now, units };
523
+ return { begin: { ...beginNode, sql: beginSql }, end: endSql };
511
524
  }
512
525
  lastUnit(units) {
513
- const thisUnit = this.nowDot(units);
514
- const lastUnit = this.delta(thisUnit, '-', '1', units);
515
- return { begin: lastUnit, end: thisUnit.sql };
526
+ const now = this.nowExpr();
527
+ const beginSql = this.d.sqlTruncAndOffset(now, this.qi, units, {
528
+ op: '-',
529
+ magnitude: '1',
530
+ unit: units,
531
+ });
532
+ const endSql = this.d.sqlTruncAndOffset(now, this.qi, units);
533
+ const beginNode = {
534
+ node: 'delta',
535
+ op: '-',
536
+ units,
537
+ kids: { base: now, delta: this.n('1') },
538
+ };
539
+ return { begin: { ...beginNode, sql: beginSql }, end: endSql };
516
540
  }
517
541
  nextUnit(units) {
518
- const thisUnit = this.nowDot(units);
519
- const nextUnit = this.delta(thisUnit, '+', '1', units);
520
- const next2Unit = this.delta(thisUnit, '+', '2', units);
521
- return { begin: nextUnit, end: next2Unit.sql };
542
+ const now = this.nowExpr();
543
+ const beginSql = this.d.sqlTruncAndOffset(now, this.qi, units, {
544
+ op: '+',
545
+ magnitude: '1',
546
+ unit: units,
547
+ });
548
+ const endSql = this.d.sqlTruncAndOffset(now, this.qi, units, {
549
+ op: '+',
550
+ magnitude: '2',
551
+ unit: units,
552
+ });
553
+ const beginNode = {
554
+ node: 'delta',
555
+ op: '+',
556
+ units,
557
+ kids: { base: now, delta: this.n('1') },
558
+ };
559
+ return { begin: { ...beginNode, sql: beginSql }, end: endSql };
522
560
  }
523
561
  mod7(n) {
524
562
  return this.d.hasModOperator ? `(${n})%7` : `MOD(${n},7)`;
@@ -533,19 +571,36 @@ class TemporalFilterCompiler {
533
571
  return this.expandLiteral(m);
534
572
  case 'ago':
535
573
  case 'from_now': {
536
- const nowTruncExpr = this.nowDot(m.units);
537
- const nowTrunc = (0, malloy_types_1.mkTemporal)(nowTruncExpr, 'timestamp');
538
- const beginExpr = this.delta(nowTrunc, m.moment === 'ago' ? '-' : '+', m.n, m.units);
574
+ const now = this.nowExpr();
575
+ const op = m.moment === 'ago' ? '-' : '+';
576
+ const beginSql = this.d.sqlTruncAndOffset(now, this.qi, m.units, {
577
+ op,
578
+ magnitude: m.n,
579
+ unit: m.units,
580
+ });
581
+ const beginNode = {
582
+ node: 'delta',
583
+ op,
584
+ units: m.units,
585
+ kids: { base: now, delta: this.n(m.n) },
586
+ };
539
587
  // Now the end is one unit after that .. either n-1 units ago or n+1 units from now
588
+ let endSql;
540
589
  if (m.moment === 'ago' && m.n === '1') {
541
- return { begin: beginExpr, end: nowTruncExpr.sql };
590
+ endSql = this.d.sqlTruncAndOffset(now, this.qi, m.units);
591
+ }
592
+ else {
593
+ const oneDifferent = Number(m.n) + (m.moment === 'ago' ? -1 : 1);
594
+ endSql = this.d.sqlTruncAndOffset(now, this.qi, m.units, {
595
+ op,
596
+ magnitude: oneDifferent.toString(),
597
+ unit: m.units,
598
+ });
542
599
  }
543
- const oneDifferent = Number(m.n) + (m.moment === 'ago' ? -1 : 1);
544
- const endExpr = {
545
- ...beginExpr,
546
- kids: { base: nowTrunc, delta: this.n(oneDifferent.toString()) },
600
+ return {
601
+ begin: { ...beginNode, sql: beginSql },
602
+ end: endSql,
547
603
  };
548
- return { begin: beginExpr, end: this.d.sqlAlterTimeExpr(endExpr) };
549
604
  }
550
605
  case 'today':
551
606
  return this.thisUnit('day');
@@ -591,7 +646,7 @@ class TemporalFilterCompiler {
591
646
  weekdayMoment(destDay, which) {
592
647
  const direction = which || 'last';
593
648
  const dow = this.dayofWeek(this.nowExpr());
594
- const todayBegin = this.thisUnit('day').begin;
649
+ const now = this.nowExpr();
595
650
  // destDay comes in as 1-7 (Malloy format), convert to 0-6
596
651
  const destDayZeroBased = destDay - 1;
597
652
  // dow is 1-7, convert to 0-6 for the arithmetic
@@ -612,9 +667,29 @@ class TemporalFilterCompiler {
612
667
  // End offset is one day less (closer to today)
613
668
  endOffset = `${this.mod7(`${dowZeroBased}-${destDayZeroBased}+6`)}`;
614
669
  }
615
- const begin = this.delta(todayBegin, direction === 'next' ? '+' : '-', beginOffset, 'day');
616
- const end = this.delta(todayBegin, direction === 'next' ? '+' : '-', endOffset, 'day');
617
- return { begin, end: end.sql };
670
+ const op = direction === 'next' ? '+' : '-';
671
+ const beginSql = this.d.sqlTruncAndOffset(now, this.qi, 'day', {
672
+ op,
673
+ magnitude: beginOffset,
674
+ unit: 'day',
675
+ });
676
+ const endSql = this.d.sqlTruncAndOffset(now, this.qi, 'day', {
677
+ op,
678
+ magnitude: endOffset,
679
+ unit: 'day',
680
+ });
681
+ // Build an Expr node for begin (truncate now to day, then add offset)
682
+ const truncatedNow = { node: 'trunc', e: now, units: 'day' };
683
+ const beginNode = {
684
+ node: 'delta',
685
+ op,
686
+ units: 'day',
687
+ kids: {
688
+ base: (0, malloy_types_1.mkTemporal)(truncatedNow, 'timestamp'),
689
+ delta: this.n(beginOffset),
690
+ },
691
+ };
692
+ return { begin: { ...beginNode, sql: beginSql }, end: endSql };
618
693
  }
619
694
  }
620
695
  exports.TemporalFilterCompiler = TemporalFilterCompiler;