Describe the bug
A simple CASE <operand> WHEN <value> THEN ... END runs as a searched CASE WHEN <condition> THEN ... END when its operand ends in ) or END. That covers a bracketed expression, a function call, cast(... AS ...) and another CASE. The operand is dropped, and each WHEN value is evaluated as a condition.
- With a BOOLEAN operand, the query returns wrong rows and reports no error. In
WHERE, it selects the wrong rows.
- With an operand of any other type, valid SQL fails with
BOOLEAN expected, found <type> at the first WHEN value.
Impact: silent wrong results for valid SQL.
Reproduced at 77c6cf2. Master 6003169 has no change under core/src/main/java/io/questdb/griffin/ since then.
To reproduce
CREATE TABLE t AS (
SELECT x::INT i, x > 1 b, ('s' || x)::SYMBOL s, CASE WHEN x = 2 THEN NULL ELSE x END::INT n
FROM long_sequence(3)
);
-- i b s n
-- 1 false s1 1
-- 2 true s2 null
-- 3 true s3 3
Wrong results, no error:
| Statement |
Expected |
Actual |
SELECT i, b, CASE (b) WHEN true THEN 'x' ELSE 'y' END c FROM t |
y, x, x |
x, x, x |
SELECT i, b, CASE CASE WHEN b THEN true ELSE false END WHEN true THEN 'x' ELSE 'y' END c FROM t |
y, x, x |
x, x, x |
SELECT i, CASE (i > 1) WHEN false THEN 'big' ELSE 'small' END c FROM t |
big, small, small |
small, small, small |
SELECT i FROM t WHERE CASE (b) WHEN false THEN true ELSE false END |
1 |
no rows |
EXPLAIN SELECT CASE (b) WHEN false THEN 'x' ELSE 'y' END c FROM t shows the expression folded to the constant 'y' (functions: ['y']); the plan no longer references b.
Valid SQL that fails:
| Statement |
Expected |
Actual |
SELECT i, CASE abs(i) WHEN 1 THEN 'one' ELSE 'other' END c FROM t |
one, other, other |
[27] BOOLEAN expected, found INT |
SELECT i, CASE coalesce(n, 0) WHEN 0 THEN 'null' ELSE 'set' END c FROM t |
set, null, set |
[35] BOOLEAN expected, found INT |
SELECT i, CASE cast(i AS LONG) WHEN 1 THEN 'one' ELSE 'other' END c FROM t |
one, other, other |
[36] BOOLEAN expected, found INT |
SELECT i, CASE (i) WHEN 1 THEN 'one' ELSE 'other' END c FROM t |
one, other, other |
[24] BOOLEAN expected, found INT |
SELECT i, CASE (s) WHEN 's1' THEN 'one' ELSE 'other' END c FROM t |
one, other, other |
[24] BOOLEAN expected, found STRING |
Spellings that return the expected rows: CASE b WHEN ..., CASE i > 1 WHEN ..., CASE i + 0 WHEN ... and CASE i::INT WHEN .... Brackets do not change an expression's value, so CASE (b) WHEN true ... should return the same rows as CASE b WHEN true ....
Cause
ExpressionParser.java:1989 on master:
int argCount = 0;
while ((node = opStack.pop()) != null && !SqlKeywords.isCaseKeyword(node.token)) {
argStackDepth = onNode(listener, node, argStackDepth, prevBranch);
argCount++;
}
if (paramCount == 0) {
if (argCount == 0) {
// this is 'case when', we will indicate that this is regular 'case'
// to the rewrite logic
onNode(listener, expressionNodePool.next().of(ExpressionNode.LITERAL,
null, Integer.MIN_VALUE, -1), argStackDepth, prevBranch);
}
paramCount++;
}
At the first WHEN, the parser pops the operators above case and counts them in argCount. A count of 0 makes it treat the statement as a searched CASE and emit the searched-case marker for the rewrite. An operand that ends in ) or END has already left the operator stack by the time WHEN arrives, so the count is 0 although an operand is present. The spellings that work still have a node on the operator stack when WHEN arrives: the parser parks a bare column there in case a ( follows, and i > 1 or i::INT leaves its operator there.
The check dates from 7d7bf5a (2019), which added the simple CASE.
Suggested fix
Test argStackDepth == 0 in place of argCount == 0. The CASE keyword resets argStackDepth to 0 when it opens its scope, so at the first WHEN the depth is non-zero exactly when an operand is present, whichever way it reached the output.
With that one-line change on 77c6cf2, every statement above returns the expected rows, and the EXPLAIN plan shows case([b,'y','x']). CaseFunctionFactoryTest, SwitchFunctionFactoryTest, CaseCommonTest, NullCaseFunctionTest, ExpressionParserTest and SqlParserTest pass, 1,553 of 1,554 tests. The one failure is ExpressionParserTest.testCaseWithCast, which pins the current RPN for case (cast(1 as int)) when 1 ..., searched-case marker included (1 int cast 1 'th1' ..., with an empty token after cast). Its expectation needs updating to 1 int cast 1 'th1' 2 'th2' 0 case 5 * 1 +.
Related
Where it was found
Review of PR #7602. The PR does not change this code path.
Describe the bug
A simple
CASE <operand> WHEN <value> THEN ... ENDruns as a searchedCASE WHEN <condition> THEN ... ENDwhen its operand ends in)orEND. That covers a bracketed expression, a function call,cast(... AS ...)and anotherCASE. The operand is dropped, and eachWHENvalue is evaluated as a condition.WHERE, it selects the wrong rows.BOOLEAN expected, found <type>at the firstWHENvalue.Impact: silent wrong results for valid SQL.
Reproduced at 77c6cf2. Master 6003169 has no change under
core/src/main/java/io/questdb/griffin/since then.To reproduce
Wrong results, no error:
SELECT i, b, CASE (b) WHEN true THEN 'x' ELSE 'y' END c FROM ty,x,xx,x,xSELECT i, b, CASE CASE WHEN b THEN true ELSE false END WHEN true THEN 'x' ELSE 'y' END c FROM ty,x,xx,x,xSELECT i, CASE (i > 1) WHEN false THEN 'big' ELSE 'small' END c FROM tbig,small,smallsmall,small,smallSELECT i FROM t WHERE CASE (b) WHEN false THEN true ELSE false END1EXPLAIN SELECT CASE (b) WHEN false THEN 'x' ELSE 'y' END c FROM tshows the expression folded to the constant'y'(functions: ['y']); the plan no longer referencesb.Valid SQL that fails:
SELECT i, CASE abs(i) WHEN 1 THEN 'one' ELSE 'other' END c FROM tone,other,other[27] BOOLEAN expected, found INTSELECT i, CASE coalesce(n, 0) WHEN 0 THEN 'null' ELSE 'set' END c FROM tset,null,set[35] BOOLEAN expected, found INTSELECT i, CASE cast(i AS LONG) WHEN 1 THEN 'one' ELSE 'other' END c FROM tone,other,other[36] BOOLEAN expected, found INTSELECT i, CASE (i) WHEN 1 THEN 'one' ELSE 'other' END c FROM tone,other,other[24] BOOLEAN expected, found INTSELECT i, CASE (s) WHEN 's1' THEN 'one' ELSE 'other' END c FROM tone,other,other[24] BOOLEAN expected, found STRINGSpellings that return the expected rows:
CASE b WHEN ...,CASE i > 1 WHEN ...,CASE i + 0 WHEN ...andCASE i::INT WHEN .... Brackets do not change an expression's value, soCASE (b) WHEN true ...should return the same rows asCASE b WHEN true ....Cause
ExpressionParser.java:1989on master:At the first
WHEN, the parser pops the operators abovecaseand counts them inargCount. A count of 0 makes it treat the statement as a searchedCASEand emit the searched-case marker for the rewrite. An operand that ends in)orENDhas already left the operator stack by the timeWHENarrives, so the count is 0 although an operand is present. The spellings that work still have a node on the operator stack whenWHENarrives: the parser parks a bare column there in case a(follows, andi > 1ori::INTleaves its operator there.The check dates from 7d7bf5a (2019), which added the simple
CASE.Suggested fix
Test
argStackDepth == 0in place ofargCount == 0. TheCASEkeyword resetsargStackDepthto 0 when it opens its scope, so at the firstWHENthe depth is non-zero exactly when an operand is present, whichever way it reached the output.With that one-line change on 77c6cf2, every statement above returns the expected rows, and the
EXPLAINplan showscase([b,'y','x']).CaseFunctionFactoryTest,SwitchFunctionFactoryTest,CaseCommonTest,NullCaseFunctionTest,ExpressionParserTestandSqlParserTestpass, 1,553 of 1,554 tests. The one failure isExpressionParserTest.testCaseWithCast, which pins the current RPN forcase (cast(1 as int)) when 1 ..., searched-case marker included (1 int cast 1 'th1' ..., with an empty token aftercast). Its expectation needs updating to1 int cast 1 'th1' 2 'th2' 0 case 5 * 1 +.Related
(true, false)in aCASEbranch or operator operand drops operands. It is in the same parser code but has a different cause: there the slot holds two values, here the operand is counted as none.Where it was found
Review of PR #7602. The PR does not change this code path.