@malloydata/malloy 0.0.197 → 0.0.198-dev241010181909
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/case.d.ts +18 -0
- package/dist/lang/ast/expressions/case.js +114 -0
- package/dist/lang/ast/expressions/pick-when.js +15 -17
- package/dist/lang/ast/index.d.ts +1 -0
- package/dist/lang/ast/index.js +1 -0
- package/dist/lang/lib/Malloy/MalloyParser.d.ts +58 -18
- package/dist/lang/lib/Malloy/MalloyParser.js +1766 -1522
- package/dist/lang/lib/Malloy/MalloyParserListener.d.ts +35 -0
- package/dist/lang/lib/Malloy/MalloyParserVisitor.d.ts +22 -0
- package/dist/lang/malloy-to-ast.d.ts +2 -0
- package/dist/lang/malloy-to-ast.js +23 -4
- package/dist/lang/parse-log.d.ts +17 -1
- package/dist/lang/parse-log.js +4 -0
- package/dist/lang/test/expressions.spec.js +166 -0
- package/dist/lang/test/parse-expects.js +19 -6
- package/dist/model/malloy_query.d.ts +2 -2
- package/dist/model/malloy_query.js +14 -6
- package/dist/model/malloy_types.d.ts +7 -6
- package/dist/model/utils.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -50,6 +50,7 @@ import { ExprExprContext } from "./MalloyParser";
|
|
|
50
50
|
import { ExprAggFuncContext } from "./MalloyParser";
|
|
51
51
|
import { ExprFuncContext } from "./MalloyParser";
|
|
52
52
|
import { ExprPickContext } from "./MalloyParser";
|
|
53
|
+
import { ExprCaseContext } from "./MalloyParser";
|
|
53
54
|
import { ExprUngroupContext } from "./MalloyParser";
|
|
54
55
|
import { SegFieldContext } from "./MalloyParser";
|
|
55
56
|
import { SegOpsContext } from "./MalloyParser";
|
|
@@ -197,6 +198,8 @@ import { FieldExprContext } from "./MalloyParser";
|
|
|
197
198
|
import { PartialAllowedFieldExprContext } from "./MalloyParser";
|
|
198
199
|
import { PickStatementContext } from "./MalloyParser";
|
|
199
200
|
import { PickContext } from "./MalloyParser";
|
|
201
|
+
import { CaseStatementContext } from "./MalloyParser";
|
|
202
|
+
import { CaseWhenContext } from "./MalloyParser";
|
|
200
203
|
import { RecordKeyContext } from "./MalloyParser";
|
|
201
204
|
import { RecordElementContext } from "./MalloyParser";
|
|
202
205
|
import { ArgumentListContext } from "./MalloyParser";
|
|
@@ -832,6 +835,18 @@ export interface MalloyParserListener extends ParseTreeListener {
|
|
|
832
835
|
* @param ctx the parse tree
|
|
833
836
|
*/
|
|
834
837
|
exitExprPick?: (ctx: ExprPickContext) => void;
|
|
838
|
+
/**
|
|
839
|
+
* Enter a parse tree produced by the `exprCase`
|
|
840
|
+
* labeled alternative in `MalloyParser.fieldExpr`.
|
|
841
|
+
* @param ctx the parse tree
|
|
842
|
+
*/
|
|
843
|
+
enterExprCase?: (ctx: ExprCaseContext) => void;
|
|
844
|
+
/**
|
|
845
|
+
* Exit a parse tree produced by the `exprCase`
|
|
846
|
+
* labeled alternative in `MalloyParser.fieldExpr`.
|
|
847
|
+
* @param ctx the parse tree
|
|
848
|
+
*/
|
|
849
|
+
exitExprCase?: (ctx: ExprCaseContext) => void;
|
|
835
850
|
/**
|
|
836
851
|
* Enter a parse tree produced by the `exprUngroup`
|
|
837
852
|
* labeled alternative in `MalloyParser.fieldExpr`.
|
|
@@ -2360,6 +2375,26 @@ export interface MalloyParserListener extends ParseTreeListener {
|
|
|
2360
2375
|
* @param ctx the parse tree
|
|
2361
2376
|
*/
|
|
2362
2377
|
exitPick?: (ctx: PickContext) => void;
|
|
2378
|
+
/**
|
|
2379
|
+
* Enter a parse tree produced by `MalloyParser.caseStatement`.
|
|
2380
|
+
* @param ctx the parse tree
|
|
2381
|
+
*/
|
|
2382
|
+
enterCaseStatement?: (ctx: CaseStatementContext) => void;
|
|
2383
|
+
/**
|
|
2384
|
+
* Exit a parse tree produced by `MalloyParser.caseStatement`.
|
|
2385
|
+
* @param ctx the parse tree
|
|
2386
|
+
*/
|
|
2387
|
+
exitCaseStatement?: (ctx: CaseStatementContext) => void;
|
|
2388
|
+
/**
|
|
2389
|
+
* Enter a parse tree produced by `MalloyParser.caseWhen`.
|
|
2390
|
+
* @param ctx the parse tree
|
|
2391
|
+
*/
|
|
2392
|
+
enterCaseWhen?: (ctx: CaseWhenContext) => void;
|
|
2393
|
+
/**
|
|
2394
|
+
* Exit a parse tree produced by `MalloyParser.caseWhen`.
|
|
2395
|
+
* @param ctx the parse tree
|
|
2396
|
+
*/
|
|
2397
|
+
exitCaseWhen?: (ctx: CaseWhenContext) => void;
|
|
2363
2398
|
/**
|
|
2364
2399
|
* Enter a parse tree produced by `MalloyParser.recordKey`.
|
|
2365
2400
|
* @param ctx the parse tree
|
|
@@ -50,6 +50,7 @@ import { ExprExprContext } from "./MalloyParser";
|
|
|
50
50
|
import { ExprAggFuncContext } from "./MalloyParser";
|
|
51
51
|
import { ExprFuncContext } from "./MalloyParser";
|
|
52
52
|
import { ExprPickContext } from "./MalloyParser";
|
|
53
|
+
import { ExprCaseContext } from "./MalloyParser";
|
|
53
54
|
import { ExprUngroupContext } from "./MalloyParser";
|
|
54
55
|
import { SegFieldContext } from "./MalloyParser";
|
|
55
56
|
import { SegOpsContext } from "./MalloyParser";
|
|
@@ -197,6 +198,8 @@ import { FieldExprContext } from "./MalloyParser";
|
|
|
197
198
|
import { PartialAllowedFieldExprContext } from "./MalloyParser";
|
|
198
199
|
import { PickStatementContext } from "./MalloyParser";
|
|
199
200
|
import { PickContext } from "./MalloyParser";
|
|
201
|
+
import { CaseStatementContext } from "./MalloyParser";
|
|
202
|
+
import { CaseWhenContext } from "./MalloyParser";
|
|
200
203
|
import { RecordKeyContext } from "./MalloyParser";
|
|
201
204
|
import { RecordElementContext } from "./MalloyParser";
|
|
202
205
|
import { ArgumentListContext } from "./MalloyParser";
|
|
@@ -580,6 +583,13 @@ export interface MalloyParserVisitor<Result> extends ParseTreeVisitor<Result> {
|
|
|
580
583
|
* @return the visitor result
|
|
581
584
|
*/
|
|
582
585
|
visitExprPick?: (ctx: ExprPickContext) => Result;
|
|
586
|
+
/**
|
|
587
|
+
* Visit a parse tree produced by the `exprCase`
|
|
588
|
+
* labeled alternative in `MalloyParser.fieldExpr`.
|
|
589
|
+
* @param ctx the parse tree
|
|
590
|
+
* @return the visitor result
|
|
591
|
+
*/
|
|
592
|
+
visitExprCase?: (ctx: ExprCaseContext) => Result;
|
|
583
593
|
/**
|
|
584
594
|
* Visit a parse tree produced by the `exprUngroup`
|
|
585
595
|
* labeled alternative in `MalloyParser.fieldExpr`.
|
|
@@ -1491,6 +1501,18 @@ export interface MalloyParserVisitor<Result> extends ParseTreeVisitor<Result> {
|
|
|
1491
1501
|
* @return the visitor result
|
|
1492
1502
|
*/
|
|
1493
1503
|
visitPick?: (ctx: PickContext) => Result;
|
|
1504
|
+
/**
|
|
1505
|
+
* Visit a parse tree produced by `MalloyParser.caseStatement`.
|
|
1506
|
+
* @param ctx the parse tree
|
|
1507
|
+
* @return the visitor result
|
|
1508
|
+
*/
|
|
1509
|
+
visitCaseStatement?: (ctx: CaseStatementContext) => Result;
|
|
1510
|
+
/**
|
|
1511
|
+
* Visit a parse tree produced by `MalloyParser.caseWhen`.
|
|
1512
|
+
* @param ctx the parse tree
|
|
1513
|
+
* @return the visitor result
|
|
1514
|
+
*/
|
|
1515
|
+
visitCaseWhen?: (ctx: CaseWhenContext) => Result;
|
|
1494
1516
|
/**
|
|
1495
1517
|
* Visit a parse tree produced by `MalloyParser.recordKey`.
|
|
1496
1518
|
* @param ctx the parse tree
|
|
@@ -45,6 +45,7 @@ export declare class MalloyToAST extends AbstractParseTreeVisitor<ast.MalloyElem
|
|
|
45
45
|
*/
|
|
46
46
|
protected astError<T extends MessageCode>(el: ast.MalloyElement, code: T, data: MessageParameterType<T>, options?: LogMessageOptions): void;
|
|
47
47
|
protected getLocation(cx: ParserRuleContext): DocumentLocation;
|
|
48
|
+
protected getSourceString(cx: ParserRuleContext): string;
|
|
48
49
|
/**
|
|
49
50
|
* Log an error message relative to a parse node
|
|
50
51
|
*/
|
|
@@ -187,6 +188,7 @@ export declare class MalloyToAST extends AbstractParseTreeVisitor<ast.MalloyElem
|
|
|
187
188
|
visitExprAggFunc(pcx: parse.ExprAggFuncContext): ast.ExpressionDef;
|
|
188
189
|
visitExprFunc(pcx: parse.ExprFuncContext): ast.ExpressionDef;
|
|
189
190
|
visitExprDuration(pcx: parse.ExprDurationContext): ast.ExprDuration;
|
|
191
|
+
visitCaseStatement(pcx: parse.CaseStatementContext): ast.Case;
|
|
190
192
|
visitPickStatement(pcx: parse.PickStatementContext): ast.Pick;
|
|
191
193
|
visitExprFieldProps(pcx: parse.ExprFieldPropsContext): ast.ExprProps;
|
|
192
194
|
visitPartitionByStatement(pcx: parse.PartitionByStatementContext): ast.PartitionBy;
|
|
@@ -110,6 +110,9 @@ class MalloyToAST extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
|
|
|
110
110
|
range: this.parseInfo.rangeFromContext(cx),
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
+
getSourceString(cx) {
|
|
114
|
+
return this.parseInfo.sourceStream.getText(new Interval_1.Interval(cx.start.startIndex, cx.stop ? cx.stop.stopIndex : cx.start.startIndex));
|
|
115
|
+
}
|
|
113
116
|
/**
|
|
114
117
|
* Log an error message relative to a parse node
|
|
115
118
|
*/
|
|
@@ -1116,6 +1119,22 @@ class MalloyToAST extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
|
|
|
1116
1119
|
visitExprDuration(pcx) {
|
|
1117
1120
|
return new ast.ExprDuration(this.getFieldExpr(pcx.fieldExpr()), this.visitTimeframe(pcx.timeframe()).text);
|
|
1118
1121
|
}
|
|
1122
|
+
visitCaseStatement(pcx) {
|
|
1123
|
+
const valueCx = pcx._valueExpr;
|
|
1124
|
+
const value = valueCx ? this.getFieldExpr(valueCx) : undefined;
|
|
1125
|
+
const whenCxs = pcx.caseWhen();
|
|
1126
|
+
const whens = whenCxs.map(whenCx => {
|
|
1127
|
+
return new ast.CaseWhen(this.getFieldExpr(whenCx._condition), this.getFieldExpr(whenCx._result));
|
|
1128
|
+
});
|
|
1129
|
+
const elseCx = pcx._caseElse;
|
|
1130
|
+
const theElse = elseCx ? this.getFieldExpr(elseCx) : undefined;
|
|
1131
|
+
this.warnWithReplacement('sql-case', 'Use a `pick` statement instead of `case`', this.parseInfo.rangeFromContext(pcx), `${[
|
|
1132
|
+
...(valueCx ? [`${this.getSourceCode(valueCx)} ?`] : []),
|
|
1133
|
+
...whenCxs.map(whenCx => `pick ${this.getSourceCode(whenCx._result)} when ${this.getSourceCode(whenCx._condition)}`),
|
|
1134
|
+
elseCx ? `else ${elseCx.text}` : 'else null',
|
|
1135
|
+
].join(' ')}`);
|
|
1136
|
+
return new ast.Case(value, whens, theElse);
|
|
1137
|
+
}
|
|
1119
1138
|
visitPickStatement(pcx) {
|
|
1120
1139
|
const picks = pcx.pick().map(pwCx => {
|
|
1121
1140
|
let pickExpr;
|
|
@@ -1347,10 +1366,10 @@ class MalloyToAST extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
|
|
|
1347
1366
|
const wholeRange = this.parseInfo.rangeFromContext(pcx);
|
|
1348
1367
|
if (pcx.NOT()) {
|
|
1349
1368
|
op = '!~';
|
|
1350
|
-
this.warnWithReplacement('sql-not-like', "Use Malloy operator '!~' instead of 'NOT LIKE'", wholeRange, `${left
|
|
1369
|
+
this.warnWithReplacement('sql-not-like', "Use Malloy operator '!~' instead of 'NOT LIKE'", wholeRange, `${this.getSourceCode(left)} !~ ${this.getSourceCode(right)}`);
|
|
1351
1370
|
}
|
|
1352
1371
|
else {
|
|
1353
|
-
this.warnWithReplacement('sql-like', "Use Malloy operator '~' instead of 'LIKE'", wholeRange, `${left
|
|
1372
|
+
this.warnWithReplacement('sql-like', "Use Malloy operator '~' instead of 'LIKE'", wholeRange, `${this.getSourceCode(left)} ~ ${this.getSourceCode(right)}`);
|
|
1354
1373
|
}
|
|
1355
1374
|
return this.astAt(new ast.ExprCompare(this.getFieldExpr(left), op, this.getFieldExpr(right)), pcx);
|
|
1356
1375
|
}
|
|
@@ -1360,10 +1379,10 @@ class MalloyToAST extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
|
|
|
1360
1379
|
const wholeRange = this.parseInfo.rangeFromContext(pcx);
|
|
1361
1380
|
if (pcx.NOT()) {
|
|
1362
1381
|
op = '!=';
|
|
1363
|
-
this.warnWithReplacement('sql-is-not-null', "Use '!= NULL' to check for NULL instead of 'IS NOT NULL'", wholeRange, `${expr
|
|
1382
|
+
this.warnWithReplacement('sql-is-not-null', "Use '!= NULL' to check for NULL instead of 'IS NOT NULL'", wholeRange, `${this.getSourceCode(expr)} != null`);
|
|
1364
1383
|
}
|
|
1365
1384
|
else {
|
|
1366
|
-
this.warnWithReplacement('sql-is-null', "Use '= NULL' to check for NULL instead of 'IS NULL'", wholeRange, `${expr
|
|
1385
|
+
this.warnWithReplacement('sql-is-null', "Use '= NULL' to check for NULL instead of 'IS NULL'", wholeRange, `${this.getSourceCode(expr)} = null`);
|
|
1367
1386
|
}
|
|
1368
1387
|
const nullExpr = new ast.ExprNULL();
|
|
1369
1388
|
return this.astAt(new ast.ExprCompare(this.getFieldExpr(expr), op, nullExpr), pcx);
|
package/dist/lang/parse-log.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ type MessageParameterTypes = {
|
|
|
52
52
|
'pick-missing-value': {};
|
|
53
53
|
'pick-illegal-partial': {};
|
|
54
54
|
'pick-when-must-be-boolean': {
|
|
55
|
-
whenType:
|
|
55
|
+
whenType: FieldValueType;
|
|
56
56
|
};
|
|
57
57
|
'experiment-not-enabled': {
|
|
58
58
|
experimentId: string;
|
|
@@ -315,6 +315,22 @@ type MessageParameterTypes = {
|
|
|
315
315
|
'sql-is-not-null': string;
|
|
316
316
|
'sql-is-null': string;
|
|
317
317
|
'illegal-record-property-type': string;
|
|
318
|
+
'sql-case': string;
|
|
319
|
+
'case-then-type-does-not-match': {
|
|
320
|
+
thenType: FieldValueType;
|
|
321
|
+
returnType: FieldValueType;
|
|
322
|
+
};
|
|
323
|
+
'case-else-type-does-not-match': {
|
|
324
|
+
elseType: FieldValueType;
|
|
325
|
+
returnType: FieldValueType;
|
|
326
|
+
};
|
|
327
|
+
'case-when-must-be-boolean': {
|
|
328
|
+
whenType: FieldValueType;
|
|
329
|
+
};
|
|
330
|
+
'case-when-type-does-not-match': {
|
|
331
|
+
whenType: FieldValueType;
|
|
332
|
+
valueType: FieldValueType;
|
|
333
|
+
};
|
|
318
334
|
'dialect-cast-unsafe-only': string;
|
|
319
335
|
};
|
|
320
336
|
export declare const MESSAGE_FORMATTERS: PartialErrorCodeMessageMap;
|
package/dist/lang/parse-log.js
CHANGED
|
@@ -90,6 +90,10 @@ exports.MESSAGE_FORMATTERS = {
|
|
|
90
90
|
'syntax-error': e => e.message,
|
|
91
91
|
'internal-translator-error': e => `Internal Translator Error: ${e.message}`,
|
|
92
92
|
'invalid-timezone': e => `Invalid timezone: ${e.timezone}`,
|
|
93
|
+
'case-then-type-does-not-match': e => `Case then type ${e.thenType} does not match return type ${e.returnType}`,
|
|
94
|
+
'case-else-type-does-not-match': e => `Case else type ${e.elseType} does not match return type ${e.returnType}`,
|
|
95
|
+
'case-when-must-be-boolean': e => `Case when expression must be boolean, not ${e.whenType}`,
|
|
96
|
+
'case-when-type-does-not-match': e => `Case when type ${e.whenType} does not match value type ${e.valueType}`,
|
|
93
97
|
};
|
|
94
98
|
function makeLogMessage(code, parameters, options) {
|
|
95
99
|
var _a, _b, _c, _d, _e;
|
|
@@ -735,6 +735,172 @@ describe('expressions', () => {
|
|
|
735
735
|
}`).toLog((0, test_translator_1.errorMessage)("'output' is not defined"));
|
|
736
736
|
});
|
|
737
737
|
});
|
|
738
|
+
describe('case statements', () => {
|
|
739
|
+
test('full', () => {
|
|
740
|
+
const e = (0, test_translator_1.expr) `
|
|
741
|
+
case
|
|
742
|
+
when ai = 42 then 'the answer'
|
|
743
|
+
when ai = 54 then 'the questionable answer'
|
|
744
|
+
else 'random'
|
|
745
|
+
end
|
|
746
|
+
`;
|
|
747
|
+
expect(e).toLog((0, test_translator_1.warning)('sql-case'));
|
|
748
|
+
expect(e).compilesTo('{case when {ai = 42} then "the answer" when {ai = 54} then "the questionable answer" else "random"}');
|
|
749
|
+
});
|
|
750
|
+
test('with value', () => {
|
|
751
|
+
const e = (0, test_translator_1.expr) `
|
|
752
|
+
case ai
|
|
753
|
+
when 42 then 'the answer'
|
|
754
|
+
when 54 then 'the questionable answer'
|
|
755
|
+
else 'random'
|
|
756
|
+
end
|
|
757
|
+
`;
|
|
758
|
+
expect(e).toLog((0, test_translator_1.warning)('sql-case'));
|
|
759
|
+
expect(e).compilesTo('{case ai when 42 then "the answer" when 54 then "the questionable answer" else "random"}');
|
|
760
|
+
});
|
|
761
|
+
test('no else', () => {
|
|
762
|
+
const e = (0, test_translator_1.expr) `
|
|
763
|
+
case
|
|
764
|
+
when ai = 42 then 'the answer'
|
|
765
|
+
when ai = 54 then 'the questionable answer'
|
|
766
|
+
end
|
|
767
|
+
`;
|
|
768
|
+
expect(e).toLog((0, test_translator_1.warning)('sql-case'));
|
|
769
|
+
expect(e).compilesTo('{case when {ai = 42} then "the answer" when {ai = 54} then "the questionable answer"}');
|
|
770
|
+
});
|
|
771
|
+
test('wrong then type', () => {
|
|
772
|
+
expect((0, test_translator_1.expr) `
|
|
773
|
+
case
|
|
774
|
+
when ai = 42 then 'the answer'
|
|
775
|
+
when ai = 54 then 7
|
|
776
|
+
end
|
|
777
|
+
`).toLog((0, test_translator_1.warning)('sql-case'), (0, test_translator_1.error)('case-then-type-does-not-match', {
|
|
778
|
+
thenType: 'number',
|
|
779
|
+
returnType: 'string',
|
|
780
|
+
}));
|
|
781
|
+
});
|
|
782
|
+
test('wrong when type', () => {
|
|
783
|
+
expect((0, test_translator_1.expr) `
|
|
784
|
+
case ai
|
|
785
|
+
when 42 then 'the answer'
|
|
786
|
+
when 'forty-two' then 'the answer but string'
|
|
787
|
+
end
|
|
788
|
+
`).toLog((0, test_translator_1.warning)('sql-case'), (0, test_translator_1.error)('case-when-type-does-not-match', {
|
|
789
|
+
whenType: 'string',
|
|
790
|
+
valueType: 'number',
|
|
791
|
+
}));
|
|
792
|
+
});
|
|
793
|
+
test('wrong else type', () => {
|
|
794
|
+
expect((0, test_translator_1.expr) `
|
|
795
|
+
case
|
|
796
|
+
when ai = 42 then 'the answer'
|
|
797
|
+
else @2020
|
|
798
|
+
end
|
|
799
|
+
`).toLog((0, test_translator_1.warning)('sql-case'), (0, test_translator_1.error)('case-else-type-does-not-match', {
|
|
800
|
+
elseType: 'date',
|
|
801
|
+
returnType: 'string',
|
|
802
|
+
}));
|
|
803
|
+
});
|
|
804
|
+
test('null then type okay second', () => {
|
|
805
|
+
expect((0, test_translator_1.expr) `
|
|
806
|
+
case
|
|
807
|
+
when ai = 42 then 'the answer'
|
|
808
|
+
when ai = 54 then null
|
|
809
|
+
end
|
|
810
|
+
`).toLog((0, test_translator_1.warning)('sql-case'));
|
|
811
|
+
});
|
|
812
|
+
test('null then type okay first', () => {
|
|
813
|
+
expect((0, test_translator_1.expr) `
|
|
814
|
+
case
|
|
815
|
+
when ai = 54 then null
|
|
816
|
+
when ai = 42 then 'the answer'
|
|
817
|
+
end
|
|
818
|
+
`).toLog((0, test_translator_1.warning)('sql-case'));
|
|
819
|
+
});
|
|
820
|
+
test('null else type okay', () => {
|
|
821
|
+
expect((0, test_translator_1.expr) `
|
|
822
|
+
case
|
|
823
|
+
when ai = 42 then 'the answer'
|
|
824
|
+
else null
|
|
825
|
+
end
|
|
826
|
+
`).toLog((0, test_translator_1.warning)('sql-case'));
|
|
827
|
+
});
|
|
828
|
+
test('null then type before else okay', () => {
|
|
829
|
+
expect((0, test_translator_1.expr) `
|
|
830
|
+
case
|
|
831
|
+
when ai = 42 then null
|
|
832
|
+
else 'not the answer'
|
|
833
|
+
end
|
|
834
|
+
`).toLog((0, test_translator_1.warning)('sql-case'));
|
|
835
|
+
});
|
|
836
|
+
test('non boolean when', () => {
|
|
837
|
+
expect((0, test_translator_1.expr) `
|
|
838
|
+
case when ai then null end
|
|
839
|
+
`).toLog((0, test_translator_1.warning)('sql-case'), (0, test_translator_1.error)('case-when-must-be-boolean'));
|
|
840
|
+
});
|
|
841
|
+
test('type of null then second', () => {
|
|
842
|
+
expect(`
|
|
843
|
+
case
|
|
844
|
+
when ai = 42 then 'the answer'
|
|
845
|
+
when ai = 54 then null
|
|
846
|
+
end
|
|
847
|
+
`).toReturnType('string');
|
|
848
|
+
});
|
|
849
|
+
test('type of null then first', () => {
|
|
850
|
+
expect(`
|
|
851
|
+
case
|
|
852
|
+
when ai = 54 then null
|
|
853
|
+
when ai = 42 then 'the answer'
|
|
854
|
+
end
|
|
855
|
+
`).toReturnType('string');
|
|
856
|
+
});
|
|
857
|
+
test('type of null else', () => {
|
|
858
|
+
expect(`
|
|
859
|
+
case
|
|
860
|
+
when ai = 42 then 'the answer'
|
|
861
|
+
else null
|
|
862
|
+
end
|
|
863
|
+
`).toReturnType('string');
|
|
864
|
+
});
|
|
865
|
+
test('type of null then type before else', () => {
|
|
866
|
+
expect(`
|
|
867
|
+
case
|
|
868
|
+
when ai = 42 then null
|
|
869
|
+
else 'not the answer'
|
|
870
|
+
end
|
|
871
|
+
`).toReturnType('string');
|
|
872
|
+
});
|
|
873
|
+
test('replacement for full case', () => {
|
|
874
|
+
const e = (0, test_translator_1.expr) `case
|
|
875
|
+
when ai = 42 then 'the answer'
|
|
876
|
+
when ai = 54 then 'the questionable answer'
|
|
877
|
+
else 'random'
|
|
878
|
+
end`;
|
|
879
|
+
e.translator.translate();
|
|
880
|
+
expect(e.translator.logger.getLog()[0].replacement).toBe("pick 'the answer' when ai = 42 pick 'the questionable answer' when ai = 54 else 'random'");
|
|
881
|
+
});
|
|
882
|
+
test('replacement for case with no else', () => {
|
|
883
|
+
const e = (0, test_translator_1.expr) `case
|
|
884
|
+
when ai = 42 then 'the answer'
|
|
885
|
+
when ai = 54 then 'the questionable answer'
|
|
886
|
+
end`;
|
|
887
|
+
e.translator.translate();
|
|
888
|
+
expect(e.translator.logger.getLog()[0].replacement).toBe("pick 'the answer' when ai = 42 pick 'the questionable answer' when ai = 54 else null");
|
|
889
|
+
});
|
|
890
|
+
test('replacement for case with value', () => {
|
|
891
|
+
const e = (0, test_translator_1.expr) `case ai
|
|
892
|
+
when 42 then 'a'
|
|
893
|
+
when 54 then 'b'
|
|
894
|
+
end`;
|
|
895
|
+
e.translator.translate();
|
|
896
|
+
expect(e.translator.logger.getLog()[0].replacement).toBe("ai ? pick 'a' when 42 pick 'b' when 54 else null");
|
|
897
|
+
});
|
|
898
|
+
test('interaction with pick', () => {
|
|
899
|
+
expect((0, test_translator_1.expr) `
|
|
900
|
+
pick case when true then 'hooray' end when true else null
|
|
901
|
+
`).toLog((0, test_translator_1.warning)('sql-case'));
|
|
902
|
+
});
|
|
903
|
+
});
|
|
738
904
|
describe('pick statements', () => {
|
|
739
905
|
test('full', () => {
|
|
740
906
|
expect((0, test_translator_1.expr) `
|
|
@@ -33,11 +33,11 @@ function rangeToStr(loc) {
|
|
|
33
33
|
}
|
|
34
34
|
return 'undefined';
|
|
35
35
|
}
|
|
36
|
-
function ensureNoProblems(trans) {
|
|
36
|
+
function ensureNoProblems(trans, warningsOkay = false) {
|
|
37
37
|
if (trans.logger === undefined) {
|
|
38
38
|
throw new Error('JESTERY BROKEN, CANT FIND ERORR LOG');
|
|
39
39
|
}
|
|
40
|
-
if (!trans.logger.empty()) {
|
|
40
|
+
if (warningsOkay ? trans.logger.hasErrors() : !trans.logger.empty()) {
|
|
41
41
|
return {
|
|
42
42
|
message: () => `Translation problems:\n${trans.prettyErrors()}`,
|
|
43
43
|
pass: false,
|
|
@@ -114,8 +114,8 @@ function xlator(ts) {
|
|
|
114
114
|
}
|
|
115
115
|
return ts.translator || new test_translator_1.TestTranslator(ts.code);
|
|
116
116
|
}
|
|
117
|
-
function xlated(tt) {
|
|
118
|
-
const errorCheck = ensureNoProblems(tt);
|
|
117
|
+
function xlated(tt, warningsOkay = false) {
|
|
118
|
+
const errorCheck = ensureNoProblems(tt, warningsOkay);
|
|
119
119
|
if (!errorCheck.pass) {
|
|
120
120
|
return errorCheck;
|
|
121
121
|
}
|
|
@@ -157,6 +157,19 @@ function eToStr(e, symbols) {
|
|
|
157
157
|
case 'true':
|
|
158
158
|
case 'false':
|
|
159
159
|
return e.node;
|
|
160
|
+
case 'case': {
|
|
161
|
+
const caseStmt = ['case'];
|
|
162
|
+
if (e.kids.caseValue !== undefined) {
|
|
163
|
+
caseStmt.push(`${subExpr(e.kids.caseValue)}`);
|
|
164
|
+
}
|
|
165
|
+
for (let i = 0; i < e.kids.caseWhen.length; i += 1) {
|
|
166
|
+
caseStmt.push(`when ${subExpr(e.kids.caseWhen[i])} then ${subExpr(e.kids.caseThen[i])}`);
|
|
167
|
+
}
|
|
168
|
+
if (e.kids.caseElse !== undefined) {
|
|
169
|
+
caseStmt.push(`else ${subExpr(e.kids.caseElse)}`);
|
|
170
|
+
}
|
|
171
|
+
return `{${caseStmt.join(' ')}}`;
|
|
172
|
+
}
|
|
160
173
|
case 'regexpMatch':
|
|
161
174
|
return `{${subExpr(e.kids.expr)} regex-match ${subExpr(e.kids.regex)}}`;
|
|
162
175
|
}
|
|
@@ -185,13 +198,13 @@ expect.extend({
|
|
|
185
198
|
toReturnType: function (exprText, returnType) {
|
|
186
199
|
const exprModel = new test_translator_1.BetaExpression(exprText);
|
|
187
200
|
exprModel.compile();
|
|
188
|
-
const ok = xlated(exprModel);
|
|
201
|
+
const ok = xlated(exprModel, true);
|
|
189
202
|
if (!ok.pass) {
|
|
190
203
|
return ok;
|
|
191
204
|
}
|
|
192
205
|
const d = exprModel.generated();
|
|
193
206
|
const pass = d.dataType === returnType;
|
|
194
|
-
const msg = `Expression type ${d.dataType} ${pass ? '=' : '!='} $
|
|
207
|
+
const msg = `Expression type ${d.dataType} ${pass ? '=' : '!='} ${returnType}`;
|
|
195
208
|
return { pass, message: () => msg };
|
|
196
209
|
},
|
|
197
210
|
toLog: function (s, ...msgs) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Dialect, DialectFieldList } from '../dialect';
|
|
2
|
-
import { AggregateFunctionType, Annotation, CompiledQuery, Expr, FieldDef, Filtered, FunctionOverloadDef, FunctionParameterDef, JoinRelationship, ModelDef, OrderBy, OutputFieldNode, Parameter, ParameterNode, PipeSegment, Query, QueryFieldDef, QuerySegment, ResultMetadataDef, ResultStructMetadataDef, SearchIndexResult, SegmentFieldDef, StructDef, StructRef, TurtleDef, FunctionOrderBy, Argument, AggregateExpr, FilterCondition, GenericSQLExpr, FieldnameNode, FunctionCallNode, UngroupNode, SourceReferenceNode,
|
|
2
|
+
import { AggregateFunctionType, Annotation, CompiledQuery, Expr, FieldDef, Filtered, FunctionOverloadDef, FunctionParameterDef, JoinRelationship, ModelDef, OrderBy, OutputFieldNode, Parameter, ParameterNode, PipeSegment, Query, QueryFieldDef, QuerySegment, ResultMetadataDef, ResultStructMetadataDef, SearchIndexResult, SegmentFieldDef, StructDef, StructRef, TurtleDef, FunctionOrderBy, Argument, AggregateExpr, FilterCondition, GenericSQLExpr, FieldnameNode, FunctionCallNode, UngroupNode, SourceReferenceNode, SpreadExpr, FilteredExpr, SourceDef, AtomicFieldDef, BooleanFieldDef, QueryToMaterialize, PrepareResultOptions, CaseExpr } from './malloy_types';
|
|
3
3
|
import { Connection } from '../connection/types';
|
|
4
4
|
import { AndChain } from './utils';
|
|
5
5
|
import { QueryInfo } from '../dialect/dialect';
|
|
@@ -92,7 +92,7 @@ declare class QueryField extends QueryNode {
|
|
|
92
92
|
generateSourceReference(resultSet: FieldInstanceResult, context: QueryStruct, expr: SourceReferenceNode): string;
|
|
93
93
|
getAnalyticPartitions(resultStruct: FieldInstanceResult, extraPartitionFields?: string[]): string[];
|
|
94
94
|
generateAnalyticFragment(dialect: string, resultStruct: FieldInstanceResult, context: QueryStruct, expr: Expr, overload: FunctionOverloadDef, state: GenerateState, args: Expr[], partitionByFields?: string[], funcOrdering?: string): string;
|
|
95
|
-
|
|
95
|
+
generateCaseSQL(pf: CaseExpr): string;
|
|
96
96
|
exprToSQL(resultSet: FieldInstanceResult, context: QueryStruct, exprToTranslate: Expr, state?: GenerateState): string;
|
|
97
97
|
isNestedInParent(parentDef: FieldDef): boolean;
|
|
98
98
|
isArrayElement(parentDef: FieldDef): boolean;
|
|
@@ -708,12 +708,18 @@ class QueryField extends QueryNode {
|
|
|
708
708
|
}
|
|
709
709
|
return retExpr;
|
|
710
710
|
}
|
|
711
|
-
|
|
711
|
+
generateCaseSQL(pf) {
|
|
712
712
|
const caseStmt = ['CASE'];
|
|
713
|
-
|
|
714
|
-
caseStmt.push(
|
|
713
|
+
if (pf.kids.caseValue !== undefined) {
|
|
714
|
+
caseStmt.push(`${pf.kids.caseValue.sql}`);
|
|
715
715
|
}
|
|
716
|
-
|
|
716
|
+
for (let i = 0; i < pf.kids.caseWhen.length; i += 1) {
|
|
717
|
+
caseStmt.push(`WHEN ${pf.kids.caseWhen[i].sql} THEN ${pf.kids.caseThen[i].sql}`);
|
|
718
|
+
}
|
|
719
|
+
if (pf.kids.caseElse !== undefined) {
|
|
720
|
+
caseStmt.push(`ELSE ${pf.kids.caseElse.sql}`);
|
|
721
|
+
}
|
|
722
|
+
caseStmt.push('END');
|
|
717
723
|
return caseStmt.join(' ');
|
|
718
724
|
}
|
|
719
725
|
exprToSQL(resultSet, context, exprToTranslate, state = new GenerateState()) {
|
|
@@ -740,6 +746,8 @@ class QueryField extends QueryNode {
|
|
|
740
746
|
expr = { ...exprToTranslate };
|
|
741
747
|
const oldKids = exprToTranslate.kids;
|
|
742
748
|
for (const [name, kidExpr] of Object.entries(oldKids)) {
|
|
749
|
+
if (kidExpr === null)
|
|
750
|
+
continue;
|
|
743
751
|
if (Array.isArray(kidExpr)) {
|
|
744
752
|
expr.kids[name] = kidExpr.map(e => {
|
|
745
753
|
return { ...e, sql: subExpr(this, e) };
|
|
@@ -852,8 +860,8 @@ class QueryField extends QueryNode {
|
|
|
852
860
|
return expr.node;
|
|
853
861
|
case 'null':
|
|
854
862
|
return 'NULL';
|
|
855
|
-
case '
|
|
856
|
-
return this.
|
|
863
|
+
case 'case':
|
|
864
|
+
return this.generateCaseSQL(expr);
|
|
857
865
|
case '':
|
|
858
866
|
return '';
|
|
859
867
|
case 'filterCondition':
|
|
@@ -24,7 +24,7 @@ export type AnyExpr = ExprE | ExprOptionalE | ExprWithKids | ExprLeaf;
|
|
|
24
24
|
export declare function exprHasKids(e: AnyExpr): e is ExprWithKids;
|
|
25
25
|
export declare function exprHasE(e: AnyExpr): e is ExprE;
|
|
26
26
|
export declare function exprIsLeaf(e: AnyExpr): boolean;
|
|
27
|
-
export type Expr = BinaryExpr | UnaryExpr | FunctionCallNode | OutputFieldNode | FilterCondition | FilteredExpr | AggregateExpr | EmptyExpr | UngroupNode | FunctionParameterNode | SpreadExpr | AggregateOrderByNode | AggregateLimitNode | FieldnameNode | SourceReferenceNode | ParameterNode | NowNode | MeasureTimeExpr | TimeDeltaExpr | TimeTruncExpr | TimeExtractExpr | TypecastExpr | RegexMatchExpr | RegexLiteralNode | TimeLiteralNode | StringLiteralNode | NumberLiteralNode | BooleanLiteralNode | RecordLiteralNode | ArrayLiteralNode | FunctionOrderBy | GenericSQLExpr | NullNode |
|
|
27
|
+
export type Expr = BinaryExpr | UnaryExpr | FunctionCallNode | OutputFieldNode | FilterCondition | FilteredExpr | AggregateExpr | EmptyExpr | UngroupNode | FunctionParameterNode | SpreadExpr | AggregateOrderByNode | AggregateLimitNode | FieldnameNode | SourceReferenceNode | ParameterNode | NowNode | MeasureTimeExpr | TimeDeltaExpr | TimeTruncExpr | TimeExtractExpr | TypecastExpr | RegexMatchExpr | RegexLiteralNode | TimeLiteralNode | StringLiteralNode | NumberLiteralNode | BooleanLiteralNode | RecordLiteralNode | ArrayLiteralNode | FunctionOrderBy | GenericSQLExpr | NullNode | CaseExpr | ArrayEachExpr | ErrorNode;
|
|
28
28
|
interface HasDataType {
|
|
29
29
|
dataType: AtomicFieldType;
|
|
30
30
|
}
|
|
@@ -210,12 +210,13 @@ export interface GenericSQLExpr extends ExprWithKids {
|
|
|
210
210
|
export interface NullNode extends ExprLeaf {
|
|
211
211
|
node: 'null';
|
|
212
212
|
}
|
|
213
|
-
export interface
|
|
214
|
-
node: '
|
|
213
|
+
export interface CaseExpr extends ExprWithKids {
|
|
214
|
+
node: 'case';
|
|
215
215
|
kids: {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
caseValue?: Expr;
|
|
217
|
+
caseWhen: Expr[];
|
|
218
|
+
caseThen: Expr[];
|
|
219
|
+
caseElse?: Expr;
|
|
219
220
|
};
|
|
220
221
|
}
|
|
221
222
|
export interface ArrayEachExpr extends ExprLeaf {
|
package/dist/model/utils.js
CHANGED
|
@@ -119,7 +119,7 @@ function* exprKids(eNode) {
|
|
|
119
119
|
if (Array.isArray(kidEnt)) {
|
|
120
120
|
yield* kidEnt;
|
|
121
121
|
}
|
|
122
|
-
else {
|
|
122
|
+
else if (kidEnt !== null) {
|
|
123
123
|
yield kidEnt;
|
|
124
124
|
}
|
|
125
125
|
}
|
|
@@ -150,7 +150,7 @@ function exprMap(eNode, mapFunc) {
|
|
|
150
150
|
if (Array.isArray(kidEnt)) {
|
|
151
151
|
parentNode.kids[name] = kidEnt.map(kidEl => mapFunc(kidEl));
|
|
152
152
|
}
|
|
153
|
-
else {
|
|
153
|
+
else if (kidEnt !== null) {
|
|
154
154
|
parentNode.kids[name] = mapFunc(kidEnt);
|
|
155
155
|
}
|
|
156
156
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MALLOY_VERSION = "0.0.
|
|
1
|
+
export declare const MALLOY_VERSION = "0.0.198";
|
package/dist/version.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MALLOY_VERSION = void 0;
|
|
4
4
|
// generated with 'generate-version-file' script; do not edit manually
|
|
5
|
-
exports.MALLOY_VERSION = '0.0.
|
|
5
|
+
exports.MALLOY_VERSION = '0.0.198';
|
|
6
6
|
//# sourceMappingURL=version.js.map
|