@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.
- package/dist/lang/ast/expressions/expr-not.js +1 -2
- package/dist/lang/ast/index.d.ts +0 -1
- package/dist/lang/ast/index.js +0 -1
- package/dist/lang/ast/types/expression-def.js +2 -10
- package/dist/lang/lib/Malloy/MalloyLexer.d.ts +91 -90
- package/dist/lang/lib/Malloy/MalloyLexer.js +1122 -1116
- package/dist/lang/lib/Malloy/MalloyParser.d.ts +111 -90
- package/dist/lang/lib/Malloy/MalloyParser.js +987 -855
- package/dist/lang/lib/Malloy/MalloyParserListener.d.ts +26 -0
- package/dist/lang/lib/Malloy/MalloyParserVisitor.d.ts +16 -0
- package/dist/lang/malloy-to-ast.d.ts +4 -1
- package/dist/lang/malloy-to-ast.js +36 -0
- package/dist/lang/parse-log.d.ts +1 -0
- package/dist/lang/test/expressions.spec.js +88 -86
- package/dist/lang/test/parse-expects.d.ts +13 -6
- package/dist/lang/test/parse-expects.js +80 -0
- package/dist/lang/test/test-translator.d.ts +2 -1
- package/dist/lang/test/test-translator.js +41 -1
- package/dist/model/malloy_query.js +13 -5
- package/package.json +1 -1
- package/dist/lang/ast/expressions/utils.d.ts +0 -3
- package/dist/lang/ast/expressions/utils.js +0 -39
|
@@ -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
|
package/dist/lang/parse-log.d.ts
CHANGED
|
@@ -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`).
|
|
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(
|
|
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(
|
|
77
|
+
expect('42 - 7').compilesTo('{42 - 7}');
|
|
113
78
|
});
|
|
114
79
|
test('multiplication', () => {
|
|
115
|
-
expect(
|
|
80
|
+
expect('42 * 7').compilesTo('{42 * 7}');
|
|
116
81
|
});
|
|
117
82
|
test('mod', () => {
|
|
118
|
-
expect(
|
|
83
|
+
expect('42 % 7').compilesTo('{42 % 7}');
|
|
119
84
|
});
|
|
120
85
|
test('division', () => {
|
|
121
|
-
expect(
|
|
86
|
+
expect('42 / 7').compilesTo('{42 / 7}');
|
|
122
87
|
});
|
|
123
88
|
test('unary negation', () => {
|
|
124
|
-
expect(
|
|
89
|
+
expect('- ai').compilesTo('{unary- ai}');
|
|
125
90
|
});
|
|
126
91
|
test('equal', () => {
|
|
127
|
-
expect(
|
|
92
|
+
expect('42 = 7').compilesTo('{42 = 7}');
|
|
128
93
|
});
|
|
129
94
|
test('not equal', () => {
|
|
130
|
-
expect(
|
|
95
|
+
expect('42 != 7').compilesTo('{42 != 7}');
|
|
131
96
|
});
|
|
132
97
|
test('greater than', () => {
|
|
133
|
-
expect(
|
|
98
|
+
expect('42 > 7').compilesTo('{42 > 7}');
|
|
134
99
|
});
|
|
135
100
|
test('greater than or equal', () => {
|
|
136
|
-
expect(
|
|
101
|
+
expect('42 >= 7').compilesTo('{42 >= 7}');
|
|
137
102
|
});
|
|
138
103
|
test('less than or equal', () => {
|
|
139
|
-
expect(
|
|
104
|
+
expect('42 <= 7').compilesTo('{42 <= 7}');
|
|
140
105
|
});
|
|
141
106
|
test('less than', () => {
|
|
142
|
-
expect(
|
|
107
|
+
expect('42 < 7').compilesTo('{42 < 7}');
|
|
143
108
|
});
|
|
144
109
|
test('match', () => {
|
|
145
|
-
expect(
|
|
110
|
+
expect("'forty-two' ~ 'fifty-four'").compilesTo('{"forty-two" like "fifty-four"}');
|
|
146
111
|
});
|
|
147
112
|
test('not match', () => {
|
|
148
|
-
expect(
|
|
113
|
+
expect("'forty-two' !~ 'fifty-four'").compilesTo('{"forty-two" !like "fifty-four"}');
|
|
149
114
|
});
|
|
150
|
-
test('apply', () => {
|
|
151
|
-
expect(
|
|
115
|
+
test('apply as equality', () => {
|
|
116
|
+
expect("'forty-two' ? 'fifty-four'").compilesTo('{"forty-two" = "fifty-four"}');
|
|
152
117
|
});
|
|
153
118
|
test('not', () => {
|
|
154
|
-
expect(
|
|
119
|
+
expect('not true').compilesTo('{not true}');
|
|
155
120
|
});
|
|
156
121
|
test('and', () => {
|
|
157
|
-
expect(
|
|
122
|
+
expect('true and false').compilesTo('{true and false}');
|
|
158
123
|
});
|
|
159
124
|
test('or', () => {
|
|
160
|
-
expect(
|
|
125
|
+
expect('true or false').compilesTo('{true or false}');
|
|
161
126
|
});
|
|
162
127
|
test('null-check (??)', () => {
|
|
163
|
-
expect(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|