Skip to content

Commit

Permalink
Implement case with warning
Browse files Browse the repository at this point in the history
  • Loading branch information
christopherswenson committed Oct 8, 2024
1 parent 4f17832 commit b79843d
Show file tree
Hide file tree
Showing 12 changed files with 372 additions and 42 deletions.
123 changes: 123 additions & 0 deletions packages/malloy/src/lang/ast/expressions/case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {ExprValue} from '../types/expr-value';
import {ExpressionDef} from '../types/expression-def';
import {FieldSpace} from '../types/field-space';
import {MalloyElement} from '../types/malloy-element';
import {FT} from '../fragtype-utils';
import {
CaseExpr,
EvalSpace,
ExpressionType,
maxExpressionType,
mergeEvalSpaces,
} from '../../../model';

interface Choice {
then: ExprValue;
when: ExprValue;
}

function typeCoalesce(ev1: ExprValue | undefined, ev2: ExprValue): ExprValue {
return ev1 === undefined ||
ev1.dataType === 'null' ||
ev1.dataType === 'error'
? ev2
: ev1;
}

export class Case extends ExpressionDef {
elementType = 'case';
constructor(
readonly choices: CaseWhen[],
readonly elseValue?: ExpressionDef
) {
super({choices});
this.has({elseValue});
}

getExpression(fs: FieldSpace): ExprValue {
const caseValue: CaseExpr = {
node: 'case',
kids: {
caseWhen: [],
caseThen: [],
caseElse: null,
},
};
const choiceValues: Choice[] = [];
for (const c of this.choices) {
const when = c.when.getExpression(fs);
const then = c.then.getExpression(fs);
choiceValues.push({when, then});
}
let returnType: ExprValue | undefined;
let expressionType: ExpressionType = 'scalar';
let evalSpace: EvalSpace = 'constant';
for (const aChoice of choiceValues) {
if (!FT.typeEq(aChoice.when, FT.boolT)) {
return this.loggedErrorExpr('case-when-must-be-boolean', {
whenType: aChoice.when.dataType,
});
}
if (returnType && !FT.typeEq(returnType, aChoice.then, true)) {
return this.loggedErrorExpr('case-then-type-does-not-match', {
thenType: aChoice.then.dataType,
returnType: returnType.dataType,
});
}
returnType = typeCoalesce(returnType, aChoice.then);
expressionType = maxExpressionType(
expressionType,
maxExpressionType(
aChoice.then.expressionType,
aChoice.when.expressionType
)
);
evalSpace = mergeEvalSpaces(
evalSpace,
aChoice.then.evalSpace,
aChoice.when.evalSpace
);
caseValue.kids.caseWhen.push(aChoice.when.value);
caseValue.kids.caseThen.push(aChoice.then.value);
}
if (this.elseValue) {
const elseValue = this.elseValue.getExpression(fs);
if (returnType && !FT.typeEq(returnType, elseValue, true)) {
return this.loggedErrorExpr('case-else-type-does-not-match', {
elseType: elseValue.dataType,
returnType: returnType.dataType,
});
}
returnType = typeCoalesce(returnType, elseValue);
expressionType = maxExpressionType(
expressionType,
elseValue.expressionType
);
evalSpace = mergeEvalSpaces(evalSpace, elseValue.evalSpace);
caseValue.kids.caseElse = elseValue.value;
}
return {
value: caseValue,
dataType: returnType?.dataType ?? 'null',
expressionType,
evalSpace,
};
}
}

export class CaseWhen extends MalloyElement {
elementType = 'caseWhen';
constructor(
readonly when: ExpressionDef,
readonly then: ExpressionDef
) {
super({when, then});
}
}
40 changes: 20 additions & 20 deletions packages/malloy/src/lang/ast/expressions/pick-when.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
*/

import {
CaseExpr,
EvalSpace,
ExpressionType,
maxExpressionType,
mergeEvalSpaces,
PickExpr,
} from '../../../model/malloy_types';

import {FT} from '../fragtype-utils';
Expand Down Expand Up @@ -78,12 +78,12 @@ export class Pick extends ExpressionDef {
}

apply(fs: FieldSpace, op: string, expr: ExpressionDef): ExprValue {
const caseValue: PickExpr = {
node: 'pick',
const caseValue: CaseExpr = {
node: 'case',
kids: {
pickWhen: [],
pickThen: [],
pickElse: {node: 'error', message: 'pick statement not complete'},
caseWhen: [],
caseThen: [],
caseElse: null,
},
};
let returnType: ExprValue | undefined;
Expand All @@ -110,8 +110,8 @@ export class Pick extends ExpressionDef {
});
}
returnType = typeCoalesce(returnType, thenExpr);
caseValue.kids.pickWhen.push(whenExpr.value);
caseValue.kids.pickThen.push(thenExpr.value);
caseValue.kids.caseWhen.push(whenExpr.value);
caseValue.kids.caseThen.push(thenExpr.value);
}
const elsePart = this.elsePick || expr;
const elseVal = elsePart.getExpression(fs);
Expand All @@ -129,7 +129,7 @@ export class Pick extends ExpressionDef {
});
}
}
caseValue.kids.pickElse = elseVal.value;
caseValue.kids.caseElse = elseVal.value;
return {
dataType: returnType.dataType,
expressionType: maxExpressionType(
Expand All @@ -142,12 +142,12 @@ export class Pick extends ExpressionDef {
}

getExpression(fs: FieldSpace): ExprValue {
const pick: PickExpr = {
node: 'pick',
const pick: CaseExpr = {
node: 'case',
kids: {
pickWhen: [],
pickThen: [],
pickElse: {node: 'error', message: 'pick statement not complete'},
caseWhen: [],
caseThen: [],
caseElse: null,
},
};
if (this.elsePick === undefined) {
Expand All @@ -165,8 +165,8 @@ export class Pick extends ExpressionDef {
'pick with no value can only be used with apply'
);
}
const pickWhen = c.when.requestExpression(fs);
if (pickWhen === undefined) {
const caseWhen = c.when.requestExpression(fs);
if (caseWhen === undefined) {
this.loggedErrorExpr(
'pick-illegal-partial',
'pick with partial when can only be used with apply'
Expand Down Expand Up @@ -205,8 +205,8 @@ export class Pick extends ExpressionDef {
aChoice.pick.evalSpace,
aChoice.when.evalSpace
);
pick.kids.pickWhen.push(aChoice.when.value);
pick.kids.pickThen.push(aChoice.pick.value);
pick.kids.caseWhen.push(aChoice.when.value);
pick.kids.caseThen.push(aChoice.pick.value);
}
const defVal = this.elsePick.getExpression(fs);
anyExpressionType = maxExpressionType(
Expand All @@ -221,7 +221,7 @@ export class Pick extends ExpressionDef {
returnType: returnType.dataType,
});
}
pick.kids.pickElse = defVal.value;
pick.kids.caseElse = defVal.value;
return {
dataType: returnType.dataType,
expressionType: anyExpressionType,
Expand All @@ -232,7 +232,7 @@ export class Pick extends ExpressionDef {
}

export class PickWhen extends MalloyElement {
elementType = 'pickWhen';
elementType = 'caseWhen';
constructor(
readonly pick: ExpressionDef | undefined,
readonly when: ExpressionDef
Expand Down
1 change: 1 addition & 0 deletions packages/malloy/src/lang/ast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export * from './expressions/time-literal';
export * from './expressions/partial-compare';
export * from './expressions/partition_by';
export * from './expressions/pick-when';
export * from './expressions/case';
export * from './expressions/expr-record-literal';
export * from './expressions/range';
export * from './expressions/time-frame';
Expand Down
11 changes: 10 additions & 1 deletion packages/malloy/src/lang/grammar/MalloyParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ fieldExpr
| ((id (EXCLAM malloyType?)?) | timeframe)
OPAREN ( argumentList? ) CPAREN # exprFunc
| pickStatement # exprPick
| caseStatement # exprCase
| ungroup OPAREN fieldExpr (COMMA fieldName)* CPAREN # exprUngroup
;

Expand All @@ -585,6 +586,14 @@ pick
: PICK (pickValue=fieldExpr)? WHEN pickWhen=partialAllowedFieldExpr
;

caseStatement
: CASE (caseWhen)+ (ELSE caseElse=fieldExpr)? END
;

caseWhen
: WHEN condition=fieldExpr THEN result=fieldExpr
;

recordKey: id;
recordElement
: fieldPath # recordRef
Expand Down Expand Up @@ -644,4 +653,4 @@ connectionName: string;

experimentalStatementForTesting // this only exists to enable tests for the experimental compiler flag
: SEMI SEMI OBRACK string CBRACK
;
;
22 changes: 22 additions & 0 deletions packages/malloy/src/lang/malloy-to-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,28 @@ export class MalloyToAST
);
}

visitCaseStatement(pcx: parse.CaseStatementContext): ast.Case {
const whenCxs = pcx.caseWhen();
const whens = whenCxs.map(whenCx => {
return new ast.CaseWhen(
this.getFieldExpr(whenCx._condition),
this.getFieldExpr(whenCx._result)
);
});
const elseCx = pcx._caseElse;
const theElse = elseCx ? this.getFieldExpr(elseCx) : undefined;
this.warnWithReplacement(
'sql-case',
'Use a `pick` statement instead of `case`',
this.parseInfo.rangeFromContext(pcx),
`${[
...whenCxs.map(whenCx => `pick ${whenCx._result.text} when ${whenCx._condition.text}`),

Check failure on line 1637 in packages/malloy/src/lang/malloy-to-ast.ts

View workflow job for this annotation

GitHub Actions / test-all (18.x)

Replace `whenCx·=>·`pick·${whenCx._result.text}·when·${whenCx._condition.text}`` with `⏎··········whenCx·=>·`pick·${whenCx._result.text}·when·${whenCx._condition.text}`⏎········`
elseCx ? `else ${elseCx.text}` : 'else null'

Check failure on line 1638 in packages/malloy/src/lang/malloy-to-ast.ts

View workflow job for this annotation

GitHub Actions / test-all (18.x)

Insert `,`
].join(' ')}`
);
return new ast.Case(whens, theElse);
}

visitPickStatement(pcx: parse.PickStatementContext): ast.Pick {
const picks = pcx.pick().map(pwCx => {
let pickExpr: ast.ExpressionDef | undefined;
Expand Down
18 changes: 17 additions & 1 deletion packages/malloy/src/lang/parse-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ type MessageParameterTypes = {
'pick-missing-else': {};
'pick-missing-value': {};
'pick-illegal-partial': {};
'pick-when-must-be-boolean': {whenType: string};
'pick-when-must-be-boolean': {whenType: FieldValueType};
'experiment-not-enabled': {experimentId: string};
'experimental-dialect-not-enabled': {dialect: string};
'sql-native-not-allowed-in-expression': {
Expand Down Expand Up @@ -350,6 +350,16 @@ type MessageParameterTypes = {
'sql-is-not-null': string;
'sql-is-null': string;
'illegal-record-property-type': string;
'sql-case': string;
'case-then-type-does-not-match': {
thenType: FieldValueType;
returnType: FieldValueType;
};
'case-else-type-does-not-match': {
elseType: FieldValueType;
returnType: FieldValueType;
};
'case-when-must-be-boolean': {whenType: FieldValueType};
};

export const MESSAGE_FORMATTERS: PartialErrorCodeMessageMap = {
Expand Down Expand Up @@ -390,6 +400,12 @@ export const MESSAGE_FORMATTERS: PartialErrorCodeMessageMap = {
'syntax-error': e => e.message,
'internal-translator-error': e => `Internal Translator Error: ${e.message}`,
'invalid-timezone': e => `Invalid timezone: ${e.timezone}`,
'case-then-type-does-not-match': e =>
`Case then type ${e.thenType} does not match return type ${e.returnType}`,
'case-else-type-does-not-match': e =>
`Case else type ${e.elseType} does not match return type ${e.returnType}`,
'case-when-must-be-boolean': e =>
`Case when expression must be boolean, not ${e.whenType}`,
};

export type MessageCode = keyof MessageParameterTypes;
Expand Down
Loading

0 comments on commit b79843d

Please sign in to comment.