@malloydata/malloy 0.0.80-dev230907183434 → 0.0.80

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.
Files changed (32) hide show
  1. package/dist/lang/ast/expressions/expr-aggregate-function.d.ts +8 -2
  2. package/dist/lang/ast/expressions/expr-aggregate-function.js +211 -12
  3. package/dist/lang/ast/expressions/expr-asymmetric.d.ts +2 -1
  4. package/dist/lang/ast/expressions/expr-asymmetric.js +5 -2
  5. package/dist/lang/ast/expressions/expr-avg.d.ts +1 -1
  6. package/dist/lang/ast/expressions/expr-avg.js +2 -2
  7. package/dist/lang/ast/expressions/expr-sum.d.ts +1 -1
  8. package/dist/lang/ast/expressions/expr-sum.js +2 -2
  9. package/dist/lang/ast/field-space/static-space.js +19 -2
  10. package/dist/lang/ast/field-space/struct-space-field-base.d.ts +2 -1
  11. package/dist/lang/ast/field-space/struct-space-field-base.js +3 -0
  12. package/dist/lang/ast/types/lookup-result.d.ts +5 -0
  13. package/dist/lang/ast/types/malloy-element.d.ts +1 -0
  14. package/dist/lang/ast/types/malloy-element.js +4 -0
  15. package/dist/lang/lib/Malloy/MalloyLexer.d.ts +75 -74
  16. package/dist/lang/lib/Malloy/MalloyLexer.js +1101 -1094
  17. package/dist/lang/lib/Malloy/MalloyParser.d.ts +91 -78
  18. package/dist/lang/lib/Malloy/MalloyParser.js +866 -801
  19. package/dist/lang/lib/Malloy/MalloyParserListener.d.ts +13 -0
  20. package/dist/lang/lib/Malloy/MalloyParserVisitor.d.ts +8 -0
  21. package/dist/lang/malloy-to-ast.d.ts +3 -0
  22. package/dist/lang/malloy-to-ast.js +76 -25
  23. package/dist/lang/parse-malloy.d.ts +1 -0
  24. package/dist/lang/parse-malloy.js +13 -0
  25. package/dist/lang/test/expressions.spec.js +241 -25
  26. package/dist/lang/test/parse.spec.js +1 -127
  27. package/dist/lang/test/query.spec.js +1 -1
  28. package/dist/lang/test/test-translator.d.ts +7 -1
  29. package/dist/lang/test/test-translator.js +40 -13
  30. package/dist/model/utils.d.ts +1 -0
  31. package/dist/model/utils.js +101 -1
  32. package/package.json +1 -1
@@ -42,6 +42,7 @@ import { ExprLogicalAndContext } from "./MalloyParser";
42
42
  import { ExprLogicalOrContext } from "./MalloyParser";
43
43
  import { ExprCoalesceContext } from "./MalloyParser";
44
44
  import { ExprCountDisinctContext } from "./MalloyParser";
45
+ import { ExprPathlessAggregateContext } from "./MalloyParser";
45
46
  import { ExprAggregateContext } from "./MalloyParser";
46
47
  import { ExprExprContext } from "./MalloyParser";
47
48
  import { ExprAggFuncContext } from "./MalloyParser";
@@ -727,6 +728,18 @@ export interface MalloyParserListener extends ParseTreeListener {
727
728
  * @param ctx the parse tree
728
729
  */
729
730
  exitExprCountDisinct?: (ctx: ExprCountDisinctContext) => void;
731
+ /**
732
+ * Enter a parse tree produced by the `exprPathlessAggregate`
733
+ * labeled alternative in `MalloyParser.fieldExpr`.
734
+ * @param ctx the parse tree
735
+ */
736
+ enterExprPathlessAggregate?: (ctx: ExprPathlessAggregateContext) => void;
737
+ /**
738
+ * Exit a parse tree produced by the `exprPathlessAggregate`
739
+ * labeled alternative in `MalloyParser.fieldExpr`.
740
+ * @param ctx the parse tree
741
+ */
742
+ exitExprPathlessAggregate?: (ctx: ExprPathlessAggregateContext) => void;
730
743
  /**
731
744
  * Enter a parse tree produced by the `exprAggregate`
732
745
  * labeled alternative in `MalloyParser.fieldExpr`.
@@ -42,6 +42,7 @@ import { ExprLogicalAndContext } from "./MalloyParser";
42
42
  import { ExprLogicalOrContext } from "./MalloyParser";
43
43
  import { ExprCoalesceContext } from "./MalloyParser";
44
44
  import { ExprCountDisinctContext } from "./MalloyParser";
45
+ import { ExprPathlessAggregateContext } from "./MalloyParser";
45
46
  import { ExprAggregateContext } from "./MalloyParser";
46
47
  import { ExprExprContext } from "./MalloyParser";
47
48
  import { ExprAggFuncContext } from "./MalloyParser";
@@ -515,6 +516,13 @@ export interface MalloyParserVisitor<Result> extends ParseTreeVisitor<Result> {
515
516
  * @return the visitor result
516
517
  */
517
518
  visitExprCountDisinct?: (ctx: ExprCountDisinctContext) => Result;
519
+ /**
520
+ * Visit a parse tree produced by the `exprPathlessAggregate`
521
+ * labeled alternative in `MalloyParser.fieldExpr`.
522
+ * @param ctx the parse tree
523
+ * @return the visitor result
524
+ */
525
+ visitExprPathlessAggregate?: (ctx: ExprPathlessAggregateContext) => Result;
518
526
  /**
519
527
  * Visit a parse tree produced by the `exprAggregate`
520
528
  * labeled alternative in `MalloyParser.fieldExpr`.
@@ -175,7 +175,10 @@ export declare class MalloyToAST extends AbstractParseTreeVisitor<ast.MalloyElem
175
175
  visitExprCompare(pcx: parse.ExprCompareContext): ast.ExprCompare;
176
176
  visitExprCountDisinct(pcx: parse.ExprCountDisinctContext): ast.ExprCountDistinct;
177
177
  visitExprUngroup(pcx: parse.ExprUngroupContext): ast.ExprUngroup;
178
+ symmetricAggregateUsageError(aggFunc: string): string;
179
+ asymmetricAggregateUsageError(aggFunc: string): string;
178
180
  visitExprAggregate(pcx: parse.ExprAggregateContext): ast.ExpressionDef;
181
+ visitExprPathlessAggregate(pcx: parse.ExprPathlessAggregateContext): ast.ExpressionDef;
179
182
  visitExprApply(pcx: parse.ExprApplyContext): ast.Apply;
180
183
  visitExprRange(pcx: parse.ExprRangeContext): ast.Range;
181
184
  visitExprCast(pcx: parse.ExprCastContext): ast.ExpressionDef;
@@ -1006,6 +1006,9 @@ class MalloyToAST extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
1006
1006
  throw this.internalError(pcx, `untranslatable comparison operator '${op}'`);
1007
1007
  }
1008
1008
  visitExprCountDisinct(pcx) {
1009
+ if (this.m4WarningsEnabled()) {
1010
+ this.contextError(pcx, "'count(DISTINCT expression)' deprecated, use 'count(expression)' instead");
1011
+ }
1009
1012
  return this.astAt(new ast.ExprCountDistinct(this.getFieldExpr(pcx.fieldExpr())), pcx);
1010
1013
  }
1011
1014
  visitExprUngroup(pcx) {
@@ -1013,53 +1016,101 @@ class MalloyToAST extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
1013
1016
  const kw = pcx.ungroup().text.toLowerCase();
1014
1017
  return this.astAt(new ast.ExprUngroup(kw === 'all' ? kw : 'exclude', this.getFieldExpr(pcx.fieldExpr()), flist), pcx);
1015
1018
  }
1019
+ symmetricAggregateUsageError(aggFunc) {
1020
+ return `Symmetric aggregate function \`${aggFunc}\` must be written as \`${aggFunc}(expression)\` or \`path.to.field.${aggFunc}()\``;
1021
+ }
1022
+ asymmetricAggregateUsageError(aggFunc) {
1023
+ return `Asymmetric aggregate function \`${aggFunc}\` must be written as \`path.to.field.${aggFunc}()\`, \`path.to.join.${aggFunc}(expression)\`, or \`${aggFunc}(expression)\``;
1024
+ }
1016
1025
  visitExprAggregate(pcx) {
1017
1026
  const pathCx = pcx.fieldPath();
1018
- const path = pathCx
1019
- ? this.getFieldPath(pathCx, ast.ExpressionFieldReference)
1020
- : undefined;
1027
+ const path = this.getFieldPath(pathCx, ast.ExpressionFieldReference);
1021
1028
  const source = pathCx && path ? this.astAt(path, pathCx) : undefined;
1029
+ const aggFunc = pcx.aggregate().text.toLowerCase();
1022
1030
  const exprDef = pcx.fieldExpr();
1023
1031
  if (pcx.aggregate().COUNT()) {
1024
1032
  if (exprDef) {
1025
- this.contextError(exprDef, 'Ignored expression inside COUNT()');
1033
+ this.contextError(exprDef, 'Expression illegal inside path.count()');
1026
1034
  }
1027
1035
  return new ast.ExprCount(source);
1028
1036
  }
1029
- // * was ok in count, not ok now ... this should be in grammer but at
1030
- // the moment while things are still changing its right to be caught here
1031
- const star = pcx.STAR();
1032
- if (star) {
1033
- this.contextError(pcx, "'*' is not a valid expression");
1037
+ const expr = exprDef ? this.getFieldExpr(exprDef) : undefined;
1038
+ if (aggFunc === 'min' || aggFunc === 'max') {
1039
+ if (expr) {
1040
+ this.contextError(pcx, this.symmetricAggregateUsageError(aggFunc));
1041
+ }
1042
+ else {
1043
+ const idRef = this.astAt(new ast.ExprIdReference(path), pathCx);
1044
+ return aggFunc === 'min'
1045
+ ? new ast.ExprMin(idRef)
1046
+ : new ast.ExprMax(idRef);
1047
+ }
1048
+ }
1049
+ else if (aggFunc === 'avg') {
1050
+ return new ast.ExprAvg(expr, source);
1051
+ }
1052
+ else if (aggFunc === 'sum') {
1053
+ return new ast.ExprSum(expr, source);
1054
+ }
1055
+ else {
1056
+ this.contextError(pcx, `Cannot parse aggregate function ${aggFunc}`);
1034
1057
  }
1058
+ return new ast.ExprNULL();
1059
+ }
1060
+ /*
1061
+ * error toodos
1062
+ * for an aggregate function being called with no path predeeding ...
1063
+ * [X] sum/avg() -- always an error
1064
+ * [X] sum/avg(fieldName) -- suggest fieldName.sum
1065
+ * [X] sum/avg(expr) -- suggest source.sum(expr)
1066
+ * [X] count() -- OK
1067
+ * [X] count(anything) OK
1068
+ * ?? source.count()
1069
+ * [X] min/max() -- always an error
1070
+ * [X] min/max(expr) -- OK
1071
+ * ?? source.min/max(expr)
1072
+ */
1073
+ visitExprPathlessAggregate(pcx) {
1074
+ const exprDef = pcx.fieldExpr();
1035
1075
  const expr = exprDef ? this.getFieldExpr(exprDef) : undefined;
1036
- if (pcx.aggregate().MIN()) {
1037
- if (path) {
1038
- this.contextError(pcx, 'Path not legal for min()');
1076
+ const source = undefined;
1077
+ const aggFunc = pcx.aggregate().text.toLowerCase();
1078
+ if (this.m4WarningsEnabled()) {
1079
+ if (pcx.STAR()) {
1080
+ this.contextError(pcx, `* illegal inside ${aggFunc}()`, 'warn');
1039
1081
  }
1040
- else if (expr) {
1082
+ }
1083
+ if (aggFunc === 'count') {
1084
+ return this.astAt(expr ? new ast.ExprCountDistinct(expr) : new ast.ExprCount(), pcx);
1085
+ }
1086
+ else if (aggFunc === 'min') {
1087
+ if (expr) {
1041
1088
  return new ast.ExprMin(expr);
1042
1089
  }
1043
1090
  else {
1044
- this.contextError(pcx, 'Missing expression for min');
1091
+ this.contextError(pcx, this.symmetricAggregateUsageError(aggFunc));
1045
1092
  }
1046
1093
  }
1047
- else if (pcx.aggregate().MAX()) {
1048
- if (path) {
1049
- this.contextError(pcx, 'Path not legal for max()');
1050
- }
1051
- else if (expr) {
1094
+ else if (aggFunc === 'max') {
1095
+ if (expr) {
1052
1096
  return new ast.ExprMax(expr);
1053
1097
  }
1054
1098
  else {
1055
- this.contextError(pcx, 'Missing expression for max');
1099
+ this.contextError(pcx, this.symmetricAggregateUsageError(aggFunc));
1056
1100
  }
1057
1101
  }
1058
- else if (pcx.aggregate().AVG()) {
1059
- return new ast.ExprAvg(expr, source);
1060
- }
1061
- else if (pcx.aggregate().SUM()) {
1062
- return new ast.ExprSum(expr, source);
1102
+ else {
1103
+ if (expr === undefined) {
1104
+ this.contextError(pcx, this.asymmetricAggregateUsageError(aggFunc));
1105
+ return new ast.ExprNULL();
1106
+ }
1107
+ const explicitSource = pcx.SOURCE_KW() !== undefined;
1108
+ if (aggFunc === 'avg') {
1109
+ return new ast.ExprAvg(expr, source, explicitSource);
1110
+ }
1111
+ else if (aggFunc === 'sum') {
1112
+ return new ast.ExprSum(expr, source, explicitSource);
1113
+ }
1063
1114
  }
1064
1115
  return new ast.ExprNULL();
1065
1116
  }
@@ -124,6 +124,7 @@ export declare abstract class MalloyTranslation {
124
124
  problemResponse(): ProblemResponse;
125
125
  problems(): LogMessage[];
126
126
  getLineMap(url: string): string[] | undefined;
127
+ codeAtLocation(location: DocumentLocation): string;
127
128
  prettyErrors(): string;
128
129
  childRequest(importURL: string): ModelDataRequest;
129
130
  getChildExports(importURL: string): Record<string, NamedModelObject>;
@@ -575,6 +575,19 @@ class MalloyTranslation {
575
575
  return (_b = theChild.parseStep.sourceInfo) === null || _b === void 0 ? void 0 : _b.lines;
576
576
  }
577
577
  }
578
+ codeAtLocation(location) {
579
+ const lines = this.getLineMap(location.url) || [];
580
+ if (location.range.start.line === location.range.end.line) {
581
+ return lines[location.range.start.line].slice(location.range.start.character, location.range.end.character);
582
+ }
583
+ let result = '';
584
+ result += lines[location.range.start.line].slice(location.range.start.character);
585
+ for (let lineNumber = location.range.start.line + 1; lineNumber < location.range.end.line; lineNumber++) {
586
+ result += lines[lineNumber];
587
+ }
588
+ result += lines[location.range.end.line].slice(0, location.range.end.character);
589
+ return result;
590
+ }
578
591
  prettyErrors() {
579
592
  let lovely = '';
580
593
  let inFile = '';
@@ -172,31 +172,247 @@ describe('expressions', () => {
172
172
  `).translationToFailWith('Filtered expression requires an aggregate computation');
173
173
  });
174
174
  describe('aggregate forms', () => {
175
- test('count', () => {
176
- expect((0, test_translator_1.expr) `count()`).toTranslate();
177
- });
178
- test('count distinct', () => {
179
- expect((0, test_translator_1.expr) `count(distinct astr)`).toTranslate();
180
- });
181
- test('join.count()', () => {
182
- expect((0, test_translator_1.expr) `b.count()`).toTranslate();
183
- });
184
- for (const f of ['sum', 'min', 'max', 'avg']) {
185
- const fOfT = `${f}(af)`;
186
- test(fOfT, () => {
187
- expect(new test_translator_1.BetaExpression(fOfT)).toTranslate();
188
- });
189
- if (f !== 'min' && f !== 'max') {
190
- const joinDot = `b.af.${f}()`;
191
- test(joinDot, () => {
192
- expect(new test_translator_1.BetaExpression(joinDot)).toTranslate();
193
- });
194
- const joinAgg = `b.${f}(af)`;
195
- test(joinAgg, () => {
196
- expect(new test_translator_1.BetaExpression(joinAgg)).toTranslate();
197
- });
198
- }
199
- }
175
+ const m = (0, test_translator_1.model) `
176
+ source: root is a {
177
+ rename: column is ai
178
+ rename: nested is astruct
179
+ dimension: field is column * 2
180
+ dimension: many_field is many.column * 2
181
+ dimension: many_one_field is many.column + one.column
182
+ join_one: one is a {
183
+ rename: column is ai
184
+ dimension: field is column * 2
185
+ dimension: many_field is many.column * 2
186
+ join_many: many is a {
187
+ rename: column is ai
188
+ dimension: field is column * 2
189
+ dimension: constant is 1
190
+ } on true
191
+ } on true
192
+ join_many: many is a {
193
+ rename: column is ai
194
+ dimension: field is column * 2
195
+ dimension: constant is 1
196
+ join_one: one is a {
197
+ rename: column is ai
198
+ dimension: field is column * 2
199
+ join_one: one is a {
200
+ rename: column is ai
201
+ dimension: field is column * 2
202
+ } on true
203
+ } on true
204
+ } on true
205
+ join_cross: cross is a {
206
+ rename: column is ai
207
+ dimension: field is column * 2
208
+ } on true
209
+ }
210
+ `;
211
+ m.translator.translate();
212
+ const modelX = (0, test_translator_1.makeModelFunc)({
213
+ model: m.translator.modelDef,
214
+ wrap: x => `run: root -> { aggregate: x is ${x} }`,
215
+ });
216
+ test('one.column.min()', () => {
217
+ expect(modelX `one.column.min()`).toTranslate();
218
+ });
219
+ test('one.min(one.column)', () => {
220
+ expect(modelX `one.min(one.column)`).translationToFailWith('Symmetric aggregate function `min` must be written as `min(expression)` or `path.to.field.min()`');
221
+ });
222
+ test('min(one.column)', () => {
223
+ expect(modelX `min(one.column)`).toTranslate();
224
+ });
225
+ test('min(many.column)', () => {
226
+ expect(modelX `min(many.column)`).toTranslate();
227
+ });
228
+ test('min()', () => {
229
+ expect(modelX `min()`).translationToFailWith('Symmetric aggregate function `min` must be written as `min(expression)` or `path.to.field.min()`');
230
+ });
231
+ test('source.min(column)', () => {
232
+ expect(modelX `source.min(column)`).toTranslate();
233
+ });
234
+ test('many.column.max()', () => {
235
+ expect(modelX `many.column.max()`).toTranslate();
236
+ });
237
+ test('max(many.column)', () => {
238
+ expect(modelX `max(many.column)`).toTranslate();
239
+ });
240
+ test('max()', () => {
241
+ expect(modelX `max()`).translationToFailWith('Symmetric aggregate function `max` must be written as `max(expression)` or `path.to.field.max()`');
242
+ });
243
+ test('source.max(many.column)', () => {
244
+ expect(modelX `source.max(many.column)`).toTranslate();
245
+ });
246
+ test('many.column.count()', () => {
247
+ expect((0, test_translator_1.expr) `many.column.count()`).toTranslate();
248
+ });
249
+ test('count()', () => {
250
+ expect(modelX `count()`).toTranslate();
251
+ });
252
+ test('count(many.column)', () => {
253
+ expect(modelX `count(many.column)`).toTranslate();
254
+ });
255
+ test('source.count()', () => {
256
+ expect(modelX `source.count()`).toTranslate();
257
+ });
258
+ test('many.count()', () => {
259
+ expect(modelX `many.count()`).toTranslate();
260
+ });
261
+ test('sum()', () => {
262
+ expect(modelX `sum()`).translationToFailWith('Asymmetric aggregate function `sum` must be written as `path.to.field.sum()`, `path.to.join.sum(expression)`, or `sum(expression)`');
263
+ });
264
+ test('sum(column)', () => {
265
+ expect(modelX `sum(column)`).toTranslate();
266
+ });
267
+ test('sum(column * 2)', () => {
268
+ expect(modelX `sum(column * 2)`).toTranslate();
269
+ });
270
+ test('column.sum()', () => {
271
+ expect(modelX `column.sum()`).toTranslate();
272
+ });
273
+ test('source.sum(column)', () => {
274
+ expect(modelX `source.sum(column)`).toTranslate();
275
+ });
276
+ test('sum(many.column)', () => {
277
+ expect(modelX `sum(many.column)`).translationToFailWith('Join path is required for this calculation; use `many.column.sum()`');
278
+ });
279
+ test('source.sum(many.column)', () => {
280
+ expect(modelX `source.sum(many.column)`).translationToFailWith('Cannot compute asymmetric aggregate across forward `join_many` relationship `many`; use `many.column.sum()`');
281
+ });
282
+ test('many.column.sum()', () => {
283
+ expect(modelX `many.column.sum()`).toTranslate();
284
+ });
285
+ test('many.sum(many.column)', () => {
286
+ expect(modelX `many.sum(many.column)`).toTranslate();
287
+ });
288
+ test('sum(one.column)', () => {
289
+ expect(modelX `sum(one.column)`).toTranslateWithWarnings('Join path is required for this calculation; use `one.column.sum()` or `source.sum(one.column)` to get a result weighted with respect to `source`');
290
+ });
291
+ test('sum(many.constant)', () => {
292
+ expect(modelX `sum(many.constant)`).toTranslate();
293
+ });
294
+ test('source.sum(many.constant)', () => {
295
+ expect(modelX `source.sum(many.constant)`).toTranslate();
296
+ });
297
+ test('sum(nested.column)', () => {
298
+ expect(modelX `sum(nested.column)`).toTranslateWithWarnings('Join path is required for this calculation; use `nested.column.sum()` or `source.sum(nested.column)` to get a result weighted with respect to `source`');
299
+ });
300
+ test('nested.column.sum()', () => {
301
+ expect(modelX `nested.column.sum()`).toTranslate();
302
+ });
303
+ test('source.sum(nested.column)', () => {
304
+ expect(modelX `source.sum(nested.column)`).toTranslate();
305
+ });
306
+ test('sum(many.field)', () => {
307
+ expect(modelX `sum(many.field)`).translationToFailWith('Join path is required for this calculation; use `many.field.sum()`');
308
+ });
309
+ test('source.sum(many.field)', () => {
310
+ expect(modelX `source.sum(many.field)`).translationToFailWith('Cannot compute asymmetric aggregate across forward `join_many` relationship `many`; use `many.field.sum()`');
311
+ });
312
+ test('many.field.sum()', () => {
313
+ expect(modelX `many.field.sum()`).toTranslate();
314
+ });
315
+ test('many.sum(many.field)', () => {
316
+ expect(modelX `many.sum(many.field)`).toTranslate();
317
+ });
318
+ test('sum(many.field + many.field)', () => {
319
+ expect(modelX `sum(many.field + many.field)`).translationToFailWith('Join path is required for this calculation; use `many.sum(many.field + many.field)`');
320
+ });
321
+ test('source.sum(many.field + many.field)', () => {
322
+ expect(modelX `source.sum(many.field + many.field)`).translationToFailWith('Cannot compute asymmetric aggregate across forward `join_many` relationship `many`; use `many.sum(many.field + many.field)`');
323
+ });
324
+ test('many.field + many.field.sum()', () => {
325
+ expect(modelX `many.field + many.field.sum()`).toTranslate();
326
+ });
327
+ test('many.sum(many.field + many.field)', () => {
328
+ expect(modelX `many.sum(many.field + many.field)`).toTranslate();
329
+ });
330
+ test('sum(many_field)', () => {
331
+ expect(modelX `sum(many_field)`).translationToFailWith('Join path is required for this calculation; use `many_field.sum()`');
332
+ });
333
+ test('source.sum(many_field)', () => {
334
+ expect(modelX `source.sum(many_field)`).translationToFailWith('Cannot compute asymmetric aggregate across forward `join_many` relationship `many`; use `many_field.sum()`');
335
+ });
336
+ test('many_field.sum()', () => {
337
+ expect(modelX `many_field.sum()`).toTranslate();
338
+ });
339
+ test('many.sum(many_field)', () => {
340
+ expect(modelX `many.sum(many_field)`).toTranslate();
341
+ });
342
+ test('sum(one.many_field)', () => {
343
+ expect(modelX `sum(one.many_field)`).translationToFailWith('Join path is required for this calculation; use `one.many_field.sum()`');
344
+ });
345
+ test('source.sum(one.many_field)', () => {
346
+ expect(modelX `source.sum(one.many_field)`).translationToFailWith('Cannot compute asymmetric aggregate across forward `join_many` relationship `many`; use `one.many_field.sum()`');
347
+ });
348
+ test('one.many_field.sum()', () => {
349
+ expect(modelX `one.many_field.sum()`).toTranslate();
350
+ });
351
+ test('many.sum(one.many_field)', () => {
352
+ expect(modelX `one.many.sum(one.many_field)`).toTranslate();
353
+ });
354
+ test('sum(many.field + one.field)', () => {
355
+ expect(modelX `sum(many.field + one.field)`).translationToFailWith('Aggregated dimensional expression contains multiple join paths; rewrite, for example `sum(first_join.field + second_join.field)` as `first_join.field.sum() + second_join.field.sum()`');
356
+ });
357
+ test('source.sum(many.field + one.field)', () => {
358
+ expect(modelX `source.sum(many.field + one.field)`).translationToFailWith('Aggregated dimensional expression contains multiple join paths; rewrite, for example `sum(first_join.field + second_join.field)` as `first_join.field.sum() + second_join.field.sum()`');
359
+ });
360
+ test('many.sum(many.field + one.field)', () => {
361
+ expect(modelX `many.sum(many.field + one.field)`).toTranslate();
362
+ });
363
+ test('sum(many_one_field)', () => {
364
+ expect(modelX `sum(many_one_field)`).translationToFailWith('Aggregated dimensional expression contains multiple join paths; rewrite, for example `sum(first_join.field + second_join.field)` as `first_join.field.sum() + second_join.field.sum()`');
365
+ });
366
+ test('source.sum(many_one_field)', () => {
367
+ expect(modelX `source.sum(many_one_field)`).translationToFailWith('Aggregated dimensional expression contains multiple join paths; rewrite, for example `sum(first_join.field + second_join.field)` as `first_join.field.sum() + second_join.field.sum()`');
368
+ });
369
+ test('many.sum(many_one_field)', () => {
370
+ expect(modelX `many.sum(many_one_field)`).toTranslate();
371
+ });
372
+ test('sum(many.one.field)', () => {
373
+ expect(modelX `sum(many.one.field)`).translationToFailWith('Join path is required for this calculation; use `many.one.field.sum()` or `many.sum(many.one.field)` to get a result weighted with respect to `many`');
374
+ });
375
+ test('sum(many.one.one.field)', () => {
376
+ expect(modelX `sum(many.one.one.field)`).translationToFailWith('Join path is required for this calculation; use `many.one.one.field.sum()` or `many.sum(many.one.one.field)` to get a result weighted with respect to `many`');
377
+ });
378
+ test('many.avg(field)', () => {
379
+ expect(modelX `many.avg(field)`).toTranslate();
380
+ });
381
+ test('one.avg(field)', () => {
382
+ expect(modelX `one.avg(field)`).toTranslate();
383
+ });
384
+ test('cross.avg(field)', () => {
385
+ expect(modelX `cross.avg(field)`).translationToFailWith('Cannot compute asymmetric aggregate across `join_cross` relationship `cross`; use `field.avg()`');
386
+ });
387
+ test('cross.avg(cross.field)', () => {
388
+ expect(modelX `cross.avg(cross.field)`).toTranslate();
389
+ });
390
+ test('one.column.sum()', () => {
391
+ expect(modelX `one.column.sum()`).toTranslate();
392
+ });
393
+ test('one.sum(one.column)', () => {
394
+ expect(modelX `one.sum(one.column)`).toTranslate();
395
+ });
396
+ test('source.sum(one.column)', () => {
397
+ expect(modelX `source.sum(one.column)`).toTranslate();
398
+ });
399
+ test('sum(one.column + one.column)', () => {
400
+ expect(modelX `sum(one.column + one.column)`).toTranslateWithWarnings('Join path is required for this calculation; use `one.sum(one.column + one.column)` or `source.sum(one.column + one.column)` to get a result weighted with respect to `source`');
401
+ });
402
+ test('one.sum(one.column + one.column)', () => {
403
+ expect(modelX `one.sum(one.column + one.column)`).toTranslate();
404
+ });
405
+ test('source.sum(one.column + one.column)', () => {
406
+ expect(modelX `source.sum(one.column + one.column)`).toTranslate();
407
+ });
408
+ test('lag(sum(output))', () => {
409
+ expect((0, test_translator_1.model) `
410
+ ##! m4warnings
411
+ run: a -> {
412
+ group_by: output is 1
413
+ calculate: bar is lag(sum(output))
414
+ }`).translationToFailWith("'output' is not defined");
415
+ });
200
416
  });
201
417
  describe('pick statements', () => {
202
418
  test('full', () => {
@@ -301,133 +301,7 @@ describe('error cascading', () => {
301
301
  'days(@2003 to err)': 'number',
302
302
  };
303
303
  const scalars = Object.keys(typedScalars);
304
- const aggregates = [
305
- 'measure_err { where: true }',
306
- 'count(distinct err)',
307
- 'b.sum(err)',
308
- 'b.stddev(err)',
309
- ];
310
- const ungroupedAggregates = ['all(measure_err)'];
311
- test('dependent errors do not cascade', () => {
312
- expect(`
313
- source: a1 is a {
314
- join_one: b with astr
315
- dimension:
316
- ${'err is null'}
317
- ${scalars.map((d, i) => `e${i} is ${d}`).join('\n ')}
318
- measure:
319
- measure_err is count(distinct foo),
320
- ${[...aggregates, ...ungroupedAggregates]
321
- .map((m, i) => `e${i + scalars.length} is ${m}`)
322
- .join('\n ')}
323
- }
324
- `).translationToFailWith("Cannot define 'err', unexpected type: null", "'foo' is not defined");
325
- });
326
- test('error type inference is good', () => {
327
- for (const scalar of scalars) {
328
- const source = `
329
- source: a1 is a {
330
- dimension:
331
- ${'err is null'}
332
- dim is length(${scalar}, 1)
333
- }
334
- `;
335
- expect(source).translationToFailWith("Cannot define 'err', unexpected type: null", `No matching overload for function length(${typedScalars[scalar]}, number)`);
336
- }
337
- });
338
- test('eval space of errors is preserved', () => {
339
- expect(`
340
- source: a1 is a {
341
- join_one: b with astr
342
- }
343
- query: a1 -> {
344
- group_by:
345
- ${'err is null'}
346
- aggregate:
347
- measure_err is count(distinct foo)
348
- calculate:
349
- ${scalars
350
- .map((d, i) => `e${i} is lag(${d})`)
351
- .join('\n ')}
352
- ${aggregates
353
- .map((m, i) => `e${i + scalars.length} is lag(${m})`)
354
- .join('\n ')}
355
- }
356
- `).translationToFailWith("Cannot define 'err', unexpected type: null", "'foo' is not defined");
357
- });
358
- });
359
- describe('error cascading', () => {
360
- test('errors can appear in multiple top level objects', () => {
361
- expect((0, test_translator_1.markSource) `
362
- source: a1 is a { dimension: ${'x is count()'} }
363
- source: a2 is a { dimension: ${'x is count()'} }
364
- `).translationToFailWith('Cannot use an aggregate field in a dimension declaration, did you mean to use a measure declaration instead?', 'Cannot use an aggregate field in a dimension declaration, did you mean to use a measure declaration instead?');
365
- });
366
- const typedScalars = {
367
- 'err': 'error',
368
- '@2003 ~ @2003 for err hours': 'boolean',
369
- 'err.hour': 'timestamp',
370
- 'err.month': 'date',
371
- 'day_of_week(err)': 'number',
372
- 'err::string': 'string',
373
- '-err': 'number',
374
- 'err * 1': 'number',
375
- '1 * err': 'number',
376
- 'err / 1': 'number',
377
- '1 / err': 'number',
378
- 'err % 1': 'number',
379
- '1 % err': 'number',
380
- 'err + 1': 'number',
381
- '1 + err': 'number',
382
- 'err - 1': 'number',
383
- '1 - err': 'number',
384
- '@2003 ? err for 1 minute': 'boolean',
385
- '@2003 ? @2003 for err minutes': 'boolean',
386
- '3 ? > err & > 3': 'boolean',
387
- '3 ? > 3 & > err': 'boolean',
388
- '3 ? > err | > 3': 'boolean',
389
- '3 ? > 3 | > err': 'boolean',
390
- 'err ? > 3': 'boolean',
391
- '3 ? > err': 'boolean',
392
- '1 > err': 'boolean',
393
- 'err > 1': 'boolean',
394
- '1 >= err': 'boolean',
395
- 'err >= 1': 'boolean',
396
- '1 < err': 'boolean',
397
- 'err < 1': 'boolean',
398
- '1 <= err': 'boolean',
399
- 'err <= 1': 'boolean',
400
- '1 = err': 'boolean',
401
- 'err = 1': 'boolean',
402
- '1 != err': 'boolean',
403
- 'err != 1': 'boolean',
404
- '1 ~ err': 'boolean',
405
- 'err ~ 1': 'boolean',
406
- '1 !~ err': 'boolean',
407
- 'err !~ 1': 'boolean',
408
- 'not err': 'boolean',
409
- 'err and true': 'boolean',
410
- 'true and err': 'boolean',
411
- 'err or true': 'boolean',
412
- 'true or err': 'boolean',
413
- 'err ?? 1': 'number',
414
- '1 ?? err': 'number',
415
- 'cast(err as number)': 'number',
416
- '(err)': 'error',
417
- 'length(err)': 'number',
418
- 'pick err when true else false': 'boolean',
419
- 'pick true when err else false': 'boolean',
420
- 'pick true when true else err': 'boolean',
421
- 'days(err to @2003)': 'number',
422
- 'days(@2003 to err)': 'number',
423
- };
424
- const scalars = Object.keys(typedScalars);
425
- const aggregates = [
426
- 'measure_err { where: true }',
427
- 'count(distinct err)',
428
- 'b.sum(err)',
429
- 'b.stddev(err)',
430
- ];
304
+ const aggregates = ['measure_err { where: true }'];
431
305
  const ungroupedAggregates = ['all(measure_err)'];
432
306
  test('dependent errors do not cascade', () => {
433
307
  expect(`
@@ -579,7 +579,7 @@ describe('query:', () => {
579
579
  query: a extend { join_one: aaa on astr=aaa.astr } -> {
580
580
  group_by: aaa.big_i
581
581
  aggregate: s is aaa.big_i.sum()
582
- calculate: a is lag(big_i.sum())
582
+ calculate: a is lag(big_i)
583
583
  }`).toTranslate();
584
584
  });
585
585
  });