@malloydata/malloy 0.0.179 → 0.0.180-dev240906180931

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.
@@ -35,6 +35,8 @@ import { ExprForRangeContext } from "./MalloyParser";
35
35
  import { ExprAndTreeContext } from "./MalloyParser";
36
36
  import { ExprOrTreeContext } from "./MalloyParser";
37
37
  import { ExprCompareContext } from "./MalloyParser";
38
+ import { ExprWarnLikeContext } from "./MalloyParser";
39
+ import { ExprWarnNullCmpContext } from "./MalloyParser";
38
40
  import { ExprApplyContext } from "./MalloyParser";
39
41
  import { ExprNotContext } from "./MalloyParser";
40
42
  import { ExprLogicalAndContext } from "./MalloyParser";
@@ -646,6 +648,30 @@ export interface MalloyParserListener extends ParseTreeListener {
646
648
  * @param ctx the parse tree
647
649
  */
648
650
  exitExprCompare?: (ctx: ExprCompareContext) => void;
651
+ /**
652
+ * Enter a parse tree produced by the `exprWarnLike`
653
+ * labeled alternative in `MalloyParser.fieldExpr`.
654
+ * @param ctx the parse tree
655
+ */
656
+ enterExprWarnLike?: (ctx: ExprWarnLikeContext) => void;
657
+ /**
658
+ * Exit a parse tree produced by the `exprWarnLike`
659
+ * labeled alternative in `MalloyParser.fieldExpr`.
660
+ * @param ctx the parse tree
661
+ */
662
+ exitExprWarnLike?: (ctx: ExprWarnLikeContext) => void;
663
+ /**
664
+ * Enter a parse tree produced by the `exprWarnNullCmp`
665
+ * labeled alternative in `MalloyParser.fieldExpr`.
666
+ * @param ctx the parse tree
667
+ */
668
+ enterExprWarnNullCmp?: (ctx: ExprWarnNullCmpContext) => void;
669
+ /**
670
+ * Exit a parse tree produced by the `exprWarnNullCmp`
671
+ * labeled alternative in `MalloyParser.fieldExpr`.
672
+ * @param ctx the parse tree
673
+ */
674
+ exitExprWarnNullCmp?: (ctx: ExprWarnNullCmpContext) => void;
649
675
  /**
650
676
  * Enter a parse tree produced by the `exprApply`
651
677
  * labeled alternative in `MalloyParser.fieldExpr`.
@@ -35,6 +35,8 @@ import { ExprForRangeContext } from "./MalloyParser";
35
35
  import { ExprAndTreeContext } from "./MalloyParser";
36
36
  import { ExprOrTreeContext } from "./MalloyParser";
37
37
  import { ExprCompareContext } from "./MalloyParser";
38
+ import { ExprWarnLikeContext } from "./MalloyParser";
39
+ import { ExprWarnNullCmpContext } from "./MalloyParser";
38
40
  import { ExprApplyContext } from "./MalloyParser";
39
41
  import { ExprNotContext } from "./MalloyParser";
40
42
  import { ExprLogicalAndContext } from "./MalloyParser";
@@ -469,6 +471,20 @@ export interface MalloyParserVisitor<Result> extends ParseTreeVisitor<Result> {
469
471
  * @return the visitor result
470
472
  */
471
473
  visitExprCompare?: (ctx: ExprCompareContext) => Result;
474
+ /**
475
+ * Visit a parse tree produced by the `exprWarnLike`
476
+ * labeled alternative in `MalloyParser.fieldExpr`.
477
+ * @param ctx the parse tree
478
+ * @return the visitor result
479
+ */
480
+ visitExprWarnLike?: (ctx: ExprWarnLikeContext) => Result;
481
+ /**
482
+ * Visit a parse tree produced by the `exprWarnNullCmp`
483
+ * labeled alternative in `MalloyParser.fieldExpr`.
484
+ * @param ctx the parse tree
485
+ * @return the visitor result
486
+ */
487
+ visitExprWarnNullCmp?: (ctx: ExprWarnNullCmpContext) => Result;
472
488
  /**
473
489
  * Visit a parse tree produced by the `exprApply`
474
490
  * labeled alternative in `MalloyParser.fieldExpr`.
@@ -9,7 +9,7 @@ import { MalloyParseInfo } from './malloy-parse-info';
9
9
  import { FieldDeclarationConstructor } from './ast';
10
10
  import { HasString, HasID } from './parse-utils';
11
11
  import { CastType } from '../model';
12
- import { DocumentLocation, Note } from '../model/malloy_types';
12
+ import { DocumentLocation, DocumentRange, Note } from '../model/malloy_types';
13
13
  import { Tag } from '../tags';
14
14
  declare class ErrorNode extends ast.SourceQueryElement {
15
15
  elementType: string;
@@ -49,6 +49,7 @@ export declare class MalloyToAST extends AbstractParseTreeVisitor<ast.MalloyElem
49
49
  * Log an error message relative to a parse node
50
50
  */
51
51
  protected contextError(cx: ParserRuleContext, msg: string, sev?: LogSeverity): void;
52
+ protected warnWithReplacement(message: string, range: DocumentRange, replacement: string): void;
52
53
  protected inExperiment(experimentID: string, cx: ParserRuleContext): boolean;
53
54
  protected m4Severity(): LogSeverity | false;
54
55
  protected m4advisory(cx: ParserRuleContext, msg: string): void;
@@ -221,5 +222,7 @@ export declare class MalloyToAST extends AbstractParseTreeVisitor<ast.MalloyElem
221
222
  visitSQTable(pcx: parse.SQTableContext): ast.SQSource | ErrorNode;
222
223
  visitSQSQL(pcx: parse.SQSQLContext): ast.SQSource;
223
224
  visitExperimentalStatementForTesting(pcx: parse.ExperimentalStatementForTestingContext): ast.ExperimentalExperiment;
225
+ visitExprWarnLike(pcx: parse.ExprWarnLikeContext): ast.ExprCompare;
226
+ visitExprWarnNullCmp(pcx: parse.ExprWarnNullCmpContext): ast.ExprCompare;
224
227
  }
225
228
  export {};
@@ -120,6 +120,14 @@ class MalloyToAST extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
120
120
  severity: sev,
121
121
  });
122
122
  }
123
+ warnWithReplacement(message, range, replacement) {
124
+ this.msgLog.log({
125
+ message,
126
+ at: { url: this.parseInfo.sourceURL, range },
127
+ severity: 'warn',
128
+ replacement,
129
+ });
130
+ }
123
131
  inExperiment(experimentID, cx) {
124
132
  const experimental = this.compilerFlags.tag('experimental');
125
133
  if (experimental &&
@@ -1303,6 +1311,34 @@ class MalloyToAST extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
1303
1311
  this.inExperiment('compilerTestExperimentParse', pcx);
1304
1312
  return this.astAt(new ast.ExperimentalExperiment('compilerTestExperimentTranslate'), pcx);
1305
1313
  }
1314
+ visitExprWarnLike(pcx) {
1315
+ let op = '~';
1316
+ const left = pcx.fieldExpr(0);
1317
+ const right = pcx.fieldExpr(1);
1318
+ const wholeRange = this.parseInfo.rangeFromContext(pcx);
1319
+ if (pcx.NOT()) {
1320
+ op = '!~';
1321
+ this.warnWithReplacement("Use Malloy operator '!~' instead of 'NOT LIKE'", wholeRange, `${left.text} !~ ${right.text}`);
1322
+ }
1323
+ else {
1324
+ this.warnWithReplacement("Use Malloy operator '~' instead of 'LIKE'", wholeRange, `${left.text} ~ ${right.text}`);
1325
+ }
1326
+ return this.astAt(new ast.ExprCompare(this.getFieldExpr(left), op, this.getFieldExpr(right)), pcx);
1327
+ }
1328
+ visitExprWarnNullCmp(pcx) {
1329
+ let op = '=';
1330
+ const expr = pcx.fieldExpr();
1331
+ const wholeRange = this.parseInfo.rangeFromContext(pcx);
1332
+ if (pcx.NOT()) {
1333
+ op = '!=';
1334
+ this.warnWithReplacement("Use '!= NULL' to check for NULL instead of 'IS NOT NULL'", wholeRange, `${expr.text} != null`);
1335
+ }
1336
+ else {
1337
+ this.warnWithReplacement("Use '= NULL' to check for NULL instead of 'IS NULL'", wholeRange, `${expr.text} = null`);
1338
+ }
1339
+ const nullExpr = new ast.ExprNULL();
1340
+ return this.astAt(new ast.ExprCompare(this.getFieldExpr(expr), op, nullExpr), pcx);
1341
+ }
1306
1342
  }
1307
1343
  exports.MalloyToAST = MalloyToAST;
1308
1344
  //# sourceMappingURL=malloy-to-ast.js.map
@@ -8,6 +8,7 @@ export interface LogMessage {
8
8
  at?: DocumentLocation;
9
9
  severity: LogSeverity;
10
10
  errorTag?: string;
11
+ replacement?: string;
11
12
  }
12
13
  export interface MessageLogger {
13
14
  log(logMsg: LogMessage): void;
@@ -24,41 +24,6 @@
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  const test_translator_1 = require("./test-translator");
26
26
  require("./parse-expects");
27
- /**
28
- * Try and write a generic version of the expression, might some day be the basis for
29
- * a much better expression matcher.
30
- */
31
- function exprToString(e, symbols = {}) {
32
- switch (e.node) {
33
- case '=':
34
- case '>':
35
- case '>=':
36
- case '<':
37
- case '<=':
38
- case '+':
39
- case '-':
40
- case '*':
41
- case '/':
42
- case '%':
43
- return `${exprToString(e.kids.left, symbols)}${e.node}${exprToString(e.kids.right, symbols)}`;
44
- case 'and':
45
- case 'or':
46
- return `(${exprToString(e.kids.left, symbols)})${e.node}(${exprToString(e.kids.right, symbols)})`;
47
- case 'field': {
48
- const ref = e.path.join('.');
49
- if (symbols[ref] === undefined) {
50
- const nSyms = Object.keys(symbols).length;
51
- symbols[ref] = String.fromCharCode('A'.charCodeAt(0) + nSyms);
52
- }
53
- return symbols[ref];
54
- }
55
- case '()':
56
- return `(${exprToString(e.e, symbols)})`;
57
- case 'not':
58
- return `not(${exprToString(e.e, symbols)})`;
59
- }
60
- return `{${e.node}}`;
61
- }
62
27
  describe('expressions', () => {
63
28
  describe('timeframes', () => {
64
29
  const timeframes = [
@@ -91,14 +56,14 @@ describe('expressions', () => {
91
56
  });
92
57
  });
93
58
  test('field name', () => {
94
- expect((0, test_translator_1.expr) `astr`).toTranslate();
59
+ expect((0, test_translator_1.expr) `astr`).compilesTo('astr');
95
60
  });
96
61
  test('function call', () => {
97
62
  expect((0, test_translator_1.expr) `concat('foo')`).toTranslate();
98
63
  });
99
64
  describe('operators', () => {
100
65
  test('addition', () => {
101
- expect((0, test_translator_1.expr) `42 + 7`).toTranslate();
66
+ expect('42 + 7').compilesTo('{42 + 7}');
102
67
  });
103
68
  test('typecheck addition lhs', () => {
104
69
  const wrong = (0, test_translator_1.expr) `${'"string"'} + 1`;
@@ -109,58 +74,58 @@ describe('expressions', () => {
109
74
  expect(wrong).translationToFailWith("The '+' operator requires a number, not a 'string'");
110
75
  });
111
76
  test('subtraction', () => {
112
- expect((0, test_translator_1.expr) `42 - 7`).toTranslate();
77
+ expect('42 - 7').compilesTo('{42 - 7}');
113
78
  });
114
79
  test('multiplication', () => {
115
- expect((0, test_translator_1.expr) `42 * 7`).toTranslate();
80
+ expect('42 * 7').compilesTo('{42 * 7}');
116
81
  });
117
82
  test('mod', () => {
118
- expect((0, test_translator_1.expr) `42 % 7`).toTranslate();
83
+ expect('42 % 7').compilesTo('{42 % 7}');
119
84
  });
120
85
  test('division', () => {
121
- expect((0, test_translator_1.expr) `42 / 7`).toTranslate();
86
+ expect('42 / 7').compilesTo('{42 / 7}');
122
87
  });
123
88
  test('unary negation', () => {
124
- expect((0, test_translator_1.expr) `- ai`).toTranslate();
89
+ expect('- ai').compilesTo('{unary- ai}');
125
90
  });
126
91
  test('equal', () => {
127
- expect((0, test_translator_1.expr) `42 = 7`).toTranslate();
92
+ expect('42 = 7').compilesTo('{42 = 7}');
128
93
  });
129
94
  test('not equal', () => {
130
- expect((0, test_translator_1.expr) `42 != 7`).toTranslate();
95
+ expect('42 != 7').compilesTo('{42 != 7}');
131
96
  });
132
97
  test('greater than', () => {
133
- expect((0, test_translator_1.expr) `42 > 7`).toTranslate();
98
+ expect('42 > 7').compilesTo('{42 > 7}');
134
99
  });
135
100
  test('greater than or equal', () => {
136
- expect((0, test_translator_1.expr) `42 >= 7`).toTranslate();
101
+ expect('42 >= 7').compilesTo('{42 >= 7}');
137
102
  });
138
103
  test('less than or equal', () => {
139
- expect((0, test_translator_1.expr) `42 <= 7`).toTranslate();
104
+ expect('42 <= 7').compilesTo('{42 <= 7}');
140
105
  });
141
106
  test('less than', () => {
142
- expect((0, test_translator_1.expr) `42 < 7`).toTranslate();
107
+ expect('42 < 7').compilesTo('{42 < 7}');
143
108
  });
144
109
  test('match', () => {
145
- expect((0, test_translator_1.expr) `'forty-two' ~ 'fifty-four'`).toTranslate();
110
+ expect("'forty-two' ~ 'fifty-four'").compilesTo('{"forty-two" like "fifty-four"}');
146
111
  });
147
112
  test('not match', () => {
148
- expect((0, test_translator_1.expr) `'forty-two' !~ 'fifty-four'`).toTranslate();
113
+ expect("'forty-two' !~ 'fifty-four'").compilesTo('{"forty-two" !like "fifty-four"}');
149
114
  });
150
- test('apply', () => {
151
- expect((0, test_translator_1.expr) `'forty-two' ? 'fifty-four'`).toTranslate();
115
+ test('apply as equality', () => {
116
+ expect("'forty-two' ? 'fifty-four'").compilesTo('{"forty-two" = "fifty-four"}');
152
117
  });
153
118
  test('not', () => {
154
- expect((0, test_translator_1.expr) `not true`).toTranslate();
119
+ expect('not true').compilesTo('{not true}');
155
120
  });
156
121
  test('and', () => {
157
- expect((0, test_translator_1.expr) `true and false`).toTranslate();
122
+ expect('true and false').compilesTo('{true and false}');
158
123
  });
159
124
  test('or', () => {
160
- expect((0, test_translator_1.expr) `true or false`).toTranslate();
125
+ expect('true or false').compilesTo('{true or false}');
161
126
  });
162
127
  test('null-check (??)', () => {
163
- expect((0, test_translator_1.expr) `ai ?? 7`).toTranslate();
128
+ expect('ai ?? 7').compilesTo('{ai coalesce 7}');
164
129
  });
165
130
  test('coalesce type mismatch', () => {
166
131
  expect(new test_translator_1.BetaExpression('ai ?? @2003')).translationToFailWith('Mismatched types for coalesce (number, date)');
@@ -175,37 +140,16 @@ describe('expressions', () => {
175
140
  expect(new test_translator_1.BetaExpression('days(ad to ats)')).translationToFailWith('Cannot measure from date to timestamp');
176
141
  });
177
142
  test('compare to truncation uses straight comparison', () => {
178
- const compare = (0, test_translator_1.expr) `ad = ad.quarter`;
179
- expect(compare).toTranslate();
180
- const compare_expr = compare.translator.generated().value;
181
- expect(exprToString(compare_expr)).toEqual('A={trunc}');
143
+ expect('ad = ad.quarter').compilesTo('{ad = {timeTrunc-quarter ad}}');
182
144
  });
183
145
  test('compare to granular result expression uses straight comparison', () => {
184
- const compare = (0, test_translator_1.expr) `ad = ad.quarter + 1`;
185
- expect(compare).toTranslate();
186
- const compare_expr = compare.translator.generated().value;
187
- expect(exprToString(compare_expr)).toEqual('A={delta}');
146
+ expect('ad = ad.quarter + 1').compilesTo('{ad = {+quarter {timeTrunc-quarter ad} 1}}');
188
147
  });
189
148
  test('apply granular-truncation uses range', () => {
190
- const compare = (0, test_translator_1.expr) `ad ? ad.quarter`;
191
- expect(compare).toTranslate();
192
- const compare_expr = compare.translator.generated().value;
193
- expect(exprToString(compare_expr)).toEqual('(A>={trunc})and(A<{delta})');
149
+ expect('ad ? ad.quarter').compilesTo('{{ad >= {timeTrunc-quarter ad}} and {ad < {+quarter {timeTrunc-quarter ad} 1}}}');
194
150
  });
195
151
  test('apply granular-literal alternation uses all literals for range', () => {
196
- const compare = (0, test_translator_1.expr) `ad ? @2020 | @2022`;
197
- expect(compare).toTranslate();
198
- const compare_expr = compare.translator.generated().value;
199
- expect(exprToString(compare_expr)).toEqual('((A>={timeLiteral})and(A<{timeLiteral}))or((A>={timeLiteral})and(A<{timeLiteral}))');
200
- });
201
- // this should use range, but it uses = and alternations are
202
- // kind of needing help so this is a placeholder for
203
- // future work
204
- test.skip('apply granular-result alternation uses range', () => {
205
- const compare = (0, test_translator_1.expr) `ad ? ad.year | ad.month`;
206
- expect(compare).toTranslate();
207
- const compare_expr = compare.translator.generated().value;
208
- expect(exprToString(compare_expr)).toEqual('((A>=B)and(A<C))or((A>=D)and(A<E))');
152
+ expect('ad ? @2020 | @2022').compilesTo('{{{ad >= @2020-01-01} and {ad < @2021-01-01}} or {{ad >= @2022-01-01} and {ad < @2023-01-01}}}');
209
153
  });
210
154
  test('comparison promotes date literal to timestamp', () => {
211
155
  expect((0, test_translator_1.expr) `@2001 = ats`).toTranslate();
@@ -220,6 +164,68 @@ describe('expressions', () => {
220
164
  test('apply with parens', () => {
221
165
  expect((0, test_translator_1.expr) `ai ? (> 1 & < 100)`).toTranslate();
222
166
  });
167
+ describe('sql friendly warnings', () => {
168
+ test('is null with warning', () => {
169
+ const warnSrc = (0, test_translator_1.expr) `ai is null`;
170
+ expect(warnSrc).toTranslateWithWarnings("Use '= NULL' to check for NULL instead of 'IS NULL'");
171
+ expect(warnSrc).compilesTo('{is-null ai}');
172
+ const warning = warnSrc.translator.problems()[0];
173
+ expect(warning.replacement).toEqual('ai = null');
174
+ });
175
+ test('is not null with warning', () => {
176
+ const warnSrc = (0, test_translator_1.expr) `ai is not null`;
177
+ expect(warnSrc).toTranslateWithWarnings("Use '!= NULL' to check for NULL instead of 'IS NOT NULL'");
178
+ expect(warnSrc).compilesTo('{is-not-null ai}');
179
+ const warning = warnSrc.translator.problems()[0];
180
+ expect(warning.replacement).toEqual('ai != null');
181
+ });
182
+ test('like with warning', () => {
183
+ const warnSrc = (0, test_translator_1.expr) `astr like 'a'`;
184
+ expect(warnSrc).toTranslateWithWarnings("Use Malloy operator '~' instead of 'LIKE'");
185
+ expect(warnSrc).compilesTo('{astr like "a"}');
186
+ const warning = warnSrc.translator.problems()[0];
187
+ expect(warning.replacement).toEqual("astr ~ 'a'");
188
+ });
189
+ test('NOT LIKE with warning', () => {
190
+ const warnSrc = (0, test_translator_1.expr) `astr not like 'a'`;
191
+ expect(warnSrc).toTranslateWithWarnings("Use Malloy operator '!~' instead of 'NOT LIKE'");
192
+ expect(warnSrc).compilesTo('{astr !like "a"}');
193
+ const warning = warnSrc.translator.problems()[0];
194
+ expect(warning.replacement).toEqual("astr !~ 'a'");
195
+ });
196
+ test('is is-null in a model', () => {
197
+ const isNullSrc = (0, test_translator_1.model) `source: xa is a extend { dimension: x1 is astr is null }`;
198
+ expect(isNullSrc).toTranslateWithWarnings("Use '= NULL' to check for NULL instead of 'IS NULL'");
199
+ });
200
+ test('is not-null in a model', () => {
201
+ const isNullSrc = (0, test_translator_1.model) `source: xa is a extend { dimension: x1 is not null }`;
202
+ expect(isNullSrc).toTranslate();
203
+ });
204
+ test('is not-null is in a model', () => {
205
+ const isNullSrc = (0, test_translator_1.model) `source: xa is a extend { dimension: x1 is not null is null }`;
206
+ expect(isNullSrc).toTranslateWithWarnings("Use '= NULL' to check for NULL instead of 'IS NULL'");
207
+ const warning = isNullSrc.translator.problems()[0];
208
+ expect(warning.replacement).toEqual('null = null');
209
+ });
210
+ test('x is expr y is not null', () => {
211
+ const isNullSrc = (0, test_translator_1.model) `source: xa is a extend { dimension: x is 1 y is not null }`;
212
+ expect(isNullSrc).toTranslate();
213
+ const xaModel = isNullSrc.translator.translate().translated;
214
+ const xa = (0, test_translator_1.getExplore)(xaModel.modelDef, 'xa');
215
+ const x = (0, test_translator_1.getFieldDef)(xa, 'x');
216
+ expect(x).toMatchObject({ e: { node: 'numberLiteral' } });
217
+ const y = (0, test_translator_1.getFieldDef)(xa, 'y');
218
+ expect(y).toMatchObject({ e: { node: 'not' } });
219
+ });
220
+ test('not null::number', () => {
221
+ const notNull = (0, test_translator_1.expr) `not null::number`;
222
+ expect(notNull).translationToFailWith("'not' Can't use type number");
223
+ });
224
+ test('(not null)::number', () => {
225
+ const notNull = (0, test_translator_1.expr) `(not null)::number`;
226
+ expect(notNull).toTranslate();
227
+ });
228
+ });
223
229
  });
224
230
  test('filtered measure', () => {
225
231
  expect((0, test_translator_1.expr) `acount {where: astr = 'why?' }`).toTranslate();
@@ -818,11 +824,7 @@ describe('expressions', () => {
818
824
  });
819
825
  });
820
826
  test('paren and applied div', () => {
821
- const one34 = (0, test_translator_1.expr) `1+(3/4)`;
822
- expect(one34).toTranslate();
823
- const exprVal = one34.translator.generated();
824
- const exprE = exprVal.value;
825
- expect(exprToString(exprE)).toEqual('{numberLiteral}+({numberLiteral}/{numberLiteral})');
827
+ expect('1+(3/4)').compilesTo('{1 + ({3 / 4})}');
826
828
  });
827
829
  test.each([
828
830
  ['ats', 'timestamp'],
@@ -14,9 +14,7 @@ declare global {
14
14
  *
15
15
  * Passes if the source parses to an AST without errors.
16
16
  *
17
- * X can be a MarkedSource, a string, or a model. If it is a marked
18
- * source, the errors which are found must match the locations of
19
- * the markings.
17
+ * X can be a MarkedSource, a string, or a model.
20
18
  */
21
19
  toParse(): R;
22
20
  /**
@@ -25,9 +23,7 @@ declare global {
25
23
  * Passes if the source compiles to code which could be used to
26
24
  * generate SQL.
27
25
  *
28
- * X can be a MarkedSource, a string, or a model. If it is a marked
29
- * source, the errors which are found must match the locations of
30
- * the markings.
26
+ * X can be a MarkedSource, a string, or a model.
31
27
  */
32
28
  toTranslate(): R;
33
29
  /**
@@ -56,6 +52,17 @@ declare global {
56
52
  */
57
53
  translationToFailWith(...expectedErrors: ProblemSpec[]): R;
58
54
  isLocationIn(at: DocumentLocation, txt: string): R;
55
+ /**
56
+ * expect(X).compilesTo('expression-string')
57
+ *
58
+ * X should be a string or an expr`string` or a BetaExpression
59
+ *
60
+ * The string is compiled, and the compiled string is then "translated" into an expression,
61
+ * which can be used to check that the compiler did the right thing.
62
+ *
63
+ * Warnings are ignored, so need to be checked seperately
64
+ */
65
+ compilesTo(exprString: string): R;
59
66
  }
60
67
  }
61
68
  }
@@ -23,6 +23,7 @@
23
23
  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
24
  */
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
+ const model_1 = require("../../model");
26
27
  const test_translator_1 = require("./test-translator");
27
28
  function rangeToStr(loc) {
28
29
  if (loc) {
@@ -134,6 +135,51 @@ function xlated(tt) {
134
135
  tt.translate();
135
136
  return checkForNeededs(tt);
136
137
  }
138
+ function eToStr(e, symbols) {
139
+ function subExpr(e) {
140
+ return eToStr(e, symbols);
141
+ }
142
+ switch (e.node) {
143
+ case 'field': {
144
+ const ref = e.path.join('.');
145
+ if (symbols) {
146
+ if (symbols[ref] === undefined) {
147
+ const nSyms = Object.keys(symbols).length;
148
+ symbols[ref] = String.fromCharCode('A'.charCodeAt(0) + nSyms);
149
+ }
150
+ return symbols[ref];
151
+ }
152
+ else {
153
+ return ref;
154
+ }
155
+ }
156
+ case '()':
157
+ return `(${subExpr(e.e)})`;
158
+ case 'numberLiteral':
159
+ return `${e.literal}`;
160
+ case 'stringLiteral':
161
+ return `"${e.literal}"`;
162
+ case 'timeLiteral':
163
+ return `@${e.literal}`;
164
+ case 'trunc':
165
+ return `{timeTrunc-${e.units} ${subExpr(e.e)}}`;
166
+ case 'delta':
167
+ return `{${e.op}${e.units} ${subExpr(e.kids.base)} ${subExpr(e.kids.delta)}}`;
168
+ case 'true':
169
+ case 'false':
170
+ return e.node;
171
+ }
172
+ if ((0, model_1.exprHasKids)(e) && e.kids['left'] && e.kids['right']) {
173
+ return `{${subExpr(e.kids['left'])} ${e.node} ${subExpr(e.kids['right'])}}`;
174
+ }
175
+ else if ((0, model_1.exprHasE)(e)) {
176
+ return `{${e.node} ${subExpr(e.e)}}`;
177
+ }
178
+ else if ((0, model_1.exprIsLeaf)(e)) {
179
+ return `{${e.node}}`;
180
+ }
181
+ return `{?${e.node}}`;
182
+ }
137
183
  expect.extend({
138
184
  toParse: function (tx) {
139
185
  const x = xlator(tx);
@@ -178,6 +224,40 @@ expect.extend({
178
224
  message: () => errMsg,
179
225
  };
180
226
  },
227
+ compilesTo: function (tx, expr) {
228
+ let bx;
229
+ if (typeof tx === 'string') {
230
+ bx = new test_translator_1.BetaExpression(tx);
231
+ }
232
+ else {
233
+ const x = xlator(tx);
234
+ if (x instanceof test_translator_1.BetaExpression) {
235
+ bx = x;
236
+ }
237
+ else {
238
+ return {
239
+ pass: false,
240
+ message: () => 'Must pass expr`EXPRESSION` to expect(EXPRSSION).compilesTo()',
241
+ };
242
+ }
243
+ }
244
+ bx.compile();
245
+ // Only report errors, callers will need to test for warnings
246
+ if (bx.logger.hasErrors()) {
247
+ return {
248
+ message: () => `Translation problems:\n${bx.prettyErrors()}`,
249
+ pass: false,
250
+ };
251
+ }
252
+ const badRefs = checkForNeededs(bx);
253
+ if (!badRefs.pass) {
254
+ return badRefs;
255
+ }
256
+ const rcvExpr = eToStr(bx.generated().value, undefined);
257
+ const pass = this.equals(rcvExpr, expr);
258
+ const msg = pass ? `Matched: ${rcvExpr}` : this.utils.diff(expr, rcvExpr);
259
+ return { pass, message: () => `${msg}` };
260
+ },
181
261
  });
182
262
  function checkForProblems(context, expectCompiles, s, defaultSeverity, ...msgs) {
183
263
  var _a;
@@ -1,4 +1,4 @@
1
- import { DocumentLocation, FieldDef, ModelDef, NamedModelObject, PipeSegment, Query, QueryFieldDef, SQLBlockSource, SQLBlockStructDef, StructDef, TurtleDef } from '../../model/malloy_types';
1
+ import { DocumentLocation, Expr, FieldDef, ModelDef, NamedModelObject, PipeSegment, Query, QueryFieldDef, SQLBlockSource, SQLBlockStructDef, StructDef, TurtleDef } from '../../model/malloy_types';
2
2
  import { MalloyElement } from '../ast';
3
3
  import { NameSpace } from '../ast/types/name-space';
4
4
  import { ModelEntry } from '../ast/types/model-entry';
@@ -72,4 +72,5 @@ export declare function makeModelFunc(options: {
72
72
  }): (unmarked: TemplateStringsArray, ...marked: string[]) => HasTranslator<TestTranslator>;
73
73
  export declare function markSource(unmarked: TemplateStringsArray, ...marked: string[]): MarkedSource;
74
74
  export declare function getSelectOneStruct(sqlBlock: SQLBlockSource): SQLBlockStructDef;
75
+ export declare function exprToString(e: Expr, symbols?: Record<string, string>): string;
75
76
  export {};
@@ -23,7 +23,7 @@
23
23
  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
24
  */
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.getSelectOneStruct = exports.markSource = exports.makeModelFunc = exports.model = exports.expr = exports.getJoinField = exports.getQueryField = exports.getQueryFieldDef = exports.getFieldDef = exports.getModelQuery = exports.getExplore = exports.BetaExpression = exports.TestTranslator = exports.TestChildTranslator = exports.aTableDef = exports.pretty = void 0;
26
+ exports.exprToString = exports.getSelectOneStruct = exports.markSource = exports.makeModelFunc = exports.model = exports.expr = exports.getJoinField = exports.getQueryField = exports.getQueryFieldDef = exports.getFieldDef = exports.getModelQuery = exports.getExplore = exports.BetaExpression = exports.TestTranslator = exports.TestChildTranslator = exports.aTableDef = exports.pretty = void 0;
27
27
  const util_1 = require("util");
28
28
  const malloy_types_1 = require("../../model/malloy_types");
29
29
  const ast_1 = require("../ast");
@@ -549,4 +549,44 @@ function getSelectOneStruct(sqlBlock) {
549
549
  };
550
550
  }
551
551
  exports.getSelectOneStruct = getSelectOneStruct;
552
+ function exprToString(e, symbols = {}) {
553
+ function subExpr(e) {
554
+ const x = exprToString(e, symbols);
555
+ return x[0] === '{' || (0, malloy_types_1.exprIsLeaf)(e) ? x : `(${x})`;
556
+ }
557
+ switch (e.node) {
558
+ case '=':
559
+ case '>':
560
+ case '>=':
561
+ case '<':
562
+ case '<=':
563
+ case '+':
564
+ case '-':
565
+ case '*':
566
+ case '/':
567
+ case '%':
568
+ return `${subExpr(e.kids.left)}${e.node}${subExpr(e.kids.right)}`;
569
+ case 'and':
570
+ case 'like':
571
+ case '!like':
572
+ case 'or':
573
+ return `${subExpr(e.kids.left)} ${e.node} ${subExpr(e.kids.right)}`;
574
+ case 'field': {
575
+ const ref = e.path.join('.');
576
+ if (symbols[ref] === undefined) {
577
+ const nSyms = Object.keys(symbols).length;
578
+ symbols[ref] = String.fromCharCode('A'.charCodeAt(0) + nSyms);
579
+ }
580
+ return symbols[ref];
581
+ }
582
+ case '()':
583
+ return `(${subExpr(e.e)})`;
584
+ case 'not':
585
+ return `not(${exprToString(e.e, symbols)})`;
586
+ case 'coalesce':
587
+ return `${subExpr(e.kids.left)} ?? ${subExpr(e.kids.right)}`;
588
+ }
589
+ return `{${e.node}}`;
590
+ }
591
+ exports.exprToString = exprToString;
552
592
  //# sourceMappingURL=test-translator.js.map