Summary
A WHERE or HAVING clause ending in a dangling AND / OR parses successfully with the
entire clause silently discarded. The statement that runs is not the statement written.
This is the #213 data-loss family; it survived because it never throws.
Measured (real compiled Parser, ac54a079)
| input |
parses as |
DELETE FROM orders WHERE id = 1 AND |
DELETE FROM orders — empties the index |
UPDATE orders SET a = 1 WHERE id = 1 AND |
UPDATE orders SET a = 1 — updates every document |
SELECT a FROM t WHERE a = 1 AND |
SELECT a FROM t — filter dropped |
SELECT a FROM t WHERE a = 1 OR |
SELECT a FROM t |
SELECT COUNT(a) AS n FROM t GROUP BY b HAVING n > 1 AND |
HAVING dropped — silent wrong answer |
Cause
A trailing operator leaves the stack as List(op, criteria). WhereParser.processTokensHelper's
case Nil arm runs stack.headOption.collect { case c: Criteria => c } on a PredicateOperator
head and returns None — and Where(None) / Having(None) render as no clause at all.
Fix
where and having reject it: processTokens returning Right(None) emits
err("WHERE clause requires criteria") / err("HAVING clause requires criteria") instead of
success(Where(None)). No valid statement can be lost — where runs only after the literal
WHERE has matched and whereCriteria is rep1, so None always means "a WHERE was written and
nothing usable came of it".
case_condition is deliberately not changed: an empty WHEN legitimately means NULL.
⚠️ Customer-visible: statements that parse today stop parsing. Release-noted for 0.23.0.
Found while implementing #250 (story 21.4) and fixed in the same PR.
Summary
A
WHEREorHAVINGclause ending in a danglingAND/ORparses successfully with theentire clause silently discarded. The statement that runs is not the statement written.
This is the #213 data-loss family; it survived because it never throws.
Measured (real compiled
Parser,ac54a079)DELETE FROM orders WHERE id = 1 ANDDELETE FROM orders— empties the indexUPDATE orders SET a = 1 WHERE id = 1 ANDUPDATE orders SET a = 1— updates every documentSELECT a FROM t WHERE a = 1 ANDSELECT a FROM t— filter droppedSELECT a FROM t WHERE a = 1 ORSELECT a FROM tSELECT COUNT(a) AS n FROM t GROUP BY b HAVING n > 1 ANDCause
A trailing operator leaves the stack as
List(op, criteria).WhereParser.processTokensHelper'scase Nilarm runsstack.headOption.collect { case c: Criteria => c }on aPredicateOperatorhead and returns
None— andWhere(None)/Having(None)render as no clause at all.Fix
whereandhavingreject it:processTokensreturningRight(None)emitserr("WHERE clause requires criteria")/err("HAVING clause requires criteria")instead ofsuccess(Where(None)). No valid statement can be lost —whereruns only after the literalWHEREhas matched andwhereCriteriaisrep1, soNonealways means "a WHERE was written andnothing usable came of it".
case_conditionis deliberately not changed: an emptyWHENlegitimately meansNULL.Found while implementing #250 (story 21.4) and fixed in the same PR.