From ccdfcbcc4b2ecbbee996502317f5eecb89b533e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 00:14:20 +0000 Subject: [PATCH] parser: reject IN without a parenthesised list/subquery (was silently dropped) `IN` must be followed by a parenthesised value list or subquery. When it was not - e.g. `WHERE a IN 5` - the parser left `in_operand` null, hit the "neither list nor subquery" fallback, and `continue`d without building an InExpr: the IN was swallowed and the predicate degraded to its bare left operand `a`. That is a silent wrong result - the filter effectively vanished and the query would return all rows. Error out when IN is not followed by `(`, instead of dropping it. The valid parenthesised list and subquery forms (and NOT IN) are unchanged. Tests (test_parser_expr_hardening): `a IN 5` / `a NOT IN 5` are rejected; `a IN (1,2,3)`, `a IN (SELECT ...)`, and `a NOT IN (1,2)` still parse to InExpr. Full suite 37/37 green. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01FyKDAbMWGwiZq4iJx3imsg --- src/parser/parser_expressions.cpp | 11 ++++++++- tests/test_parser_expr_hardening.cpp | 36 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/parser/parser_expressions.cpp b/src/parser/parser_expressions.cpp index 0150e6e..1ef154c 100644 --- a/src/parser/parser_expressions.cpp +++ b/src/parser/parser_expressions.cpp @@ -694,7 +694,16 @@ ast::ASTNode* Parser::parse_expression(int min_precedence) { if (op_keyword_id == db25::Keyword::IN) { // Check if next is a subquery or a list ast::ASTNode* in_operand = nullptr; - + + // IN must be followed by a parenthesised list or subquery. Anything + // else (e.g. `a IN 5`) is malformed; error rather than silently + // dropping the IN and degrading the predicate to its bare left + // operand (a silent wrong result - the filter would vanish). + if (!current_token_ || current_token_->value != "(") { + error("expected '(' with a value list or subquery after IN"); + return nullptr; + } + // Look for opening paren if (current_token_ && current_token_->value == "(") { // Peek ahead to see if it's a SELECT diff --git a/tests/test_parser_expr_hardening.cpp b/tests/test_parser_expr_hardening.cpp index dcd1356..0301ae5 100644 --- a/tests/test_parser_expr_hardening.cpp +++ b/tests/test_parser_expr_hardening.cpp @@ -673,3 +673,39 @@ TEST_F(ExprHardeningTest, SingleParenIsGroupingNotRow) { EXPECT_EQ(lhs->primary_text, "+"); EXPECT_EQ(find(ast, NodeType::RowConstructor), nullptr) << "no comma -> no row"; } + +// ---- IN requires a parenthesised list or subquery ------------------------- + +TEST_F(ExprHardeningTest, InWithoutParensIsRejected) { + // `a IN 5` (no parens) is malformed; it must error rather than silently + // dropping the IN and leaving the predicate as the bare column `a`. + EXPECT_EQ(parse("SELECT a FROM t WHERE a IN 5"), nullptr); + EXPECT_EQ(parse("SELECT a FROM t WHERE a NOT IN 5"), nullptr); +} + +TEST_F(ExprHardeningTest, InListAndSubqueryStillParse) { + // Regression guard: the valid parenthesised forms are unaffected. + { + auto* ast = parse("SELECT a FROM t WHERE a IN (1, 2, 3)"); + ASSERT_NE(ast, nullptr); + auto* pred = where_predicate(ast); + ASSERT_NE(pred, nullptr); + EXPECT_EQ(pred->node_type, NodeType::InExpr); + EXPECT_EQ(pred->primary_text, "IN"); + } + { + auto* ast = parse("SELECT a FROM t WHERE a IN (SELECT id FROM u)"); + ASSERT_NE(ast, nullptr); + auto* pred = where_predicate(ast); + ASSERT_NE(pred, nullptr); + EXPECT_EQ(pred->node_type, NodeType::InExpr); + } + { + auto* ast = parse("SELECT a FROM t WHERE a NOT IN (1, 2)"); + ASSERT_NE(ast, nullptr); + auto* pred = where_predicate(ast); + ASSERT_NE(pred, nullptr); + EXPECT_EQ(pred->node_type, NodeType::InExpr); + EXPECT_EQ(pred->primary_text, "NOT IN"); + } +}