Skip to content

Commit 2953aab

Browse files
jackylee-chclaude
andcommitted
fix(expressions): accept a bare true / false as an AND / OR / NOT operand
The BooleanLiteral to AlwaysTrue/AlwaysFalse conversion was attached only to the whole expression, so a bare boolean reached And/Or/Not as a BooleanLiteral and failed pydantic validation: parse("(false) or foo = 1") -> EqualTo(foo, 1) parse("false or foo = 1") -> ValidationError: 1 validation error for Or Seeding a filter with `true` and appending clauses is a common way to build a row_filter, so `row_filter="true and status = 'x'"` raised instead of scanning. Give `predicate` its own copy of the boolean element that folds to AlwaysTrue/AlwaysFalse; `literal` and `literal_set` keep the raw BooleanLiteral, so `foo = true` and `foo in (true, false)` are unchanged. Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 7a7a99b commit 2953aab

2 files changed

Lines changed: 35 additions & 1 deletion

File tree

‎pyiceberg/expressions/parser.py‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,16 @@ def _(result: ParseResults) -> Literal[bool]:
121121
return BooleanLiteral(False)
122122

123123

124+
# As an operand a bare boolean has to fold to AlwaysTrue/AlwaysFalse. This needs its own
125+
# copy because `literal` and `literal_set` keep the raw BooleanLiteral for `foo = true`.
126+
always_boolean = boolean.copy()
127+
128+
129+
@always_boolean.add_parse_action
130+
def _(result: ParseResults) -> BooleanExpression:
131+
return AlwaysTrue() if result[0].value else AlwaysFalse()
132+
133+
124134
@string.set_parse_action
125135
def _(result: ParseResults) -> Literal[str]:
126136
return StringLiteral(result.raw_quoted_string[1:-1].replace("''", "'"))
@@ -265,7 +275,9 @@ def _evaluate_like_statement(result: ParseResults) -> BooleanExpression:
265275
return EqualTo(result.column, StringLiteral(literal_like.value.replace("\\%", "%")))
266276

267277

268-
predicate = (between | comparison | in_check | null_check | nan_check | starts_check | boolean).set_results_name("predicate")
278+
predicate = (between | comparison | in_check | null_check | nan_check | starts_check | always_boolean).set_results_name(
279+
"predicate"
280+
)
269281

270282

271283
def handle_not(result: ParseResults) -> Not:

‎tests/expressions/test_parser.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
AlwaysFalse,
2525
AlwaysTrue,
2626
And,
27+
BooleanExpression,
2728
EqualTo,
2829
GreaterThan,
2930
GreaterThanOrEqual,
@@ -272,3 +273,24 @@ def test_valid_between_with_numerics() -> None:
272273
) == parser.parse("foo between '2025-01-01T00:00:00.000000' and '2025-01-10T12:00:00.000000'")
273274

274275
assert parser.parse("foo between 1 and 3") == parser.parse("1 <= foo and foo <= 3")
276+
277+
278+
@pytest.mark.parametrize(
279+
"expression, expected",
280+
[
281+
("true and foo = 1", EqualTo(Reference("foo"), literal(1))),
282+
("foo = 1 and true", EqualTo(Reference("foo"), literal(1))),
283+
("foo = 1 or false", EqualTo(Reference("foo"), literal(1))),
284+
("foo = 1 or true", AlwaysTrue()),
285+
("foo = 1 and false", AlwaysFalse()),
286+
("not true", AlwaysFalse()),
287+
("not false", AlwaysTrue()),
288+
],
289+
)
290+
def test_boolean_as_operand(expression: str, expected: BooleanExpression) -> None:
291+
assert parser.parse(expression) == expected
292+
293+
294+
def test_boolean_as_literal_is_unchanged() -> None:
295+
assert parser.parse("foo = true") == EqualTo(Reference("foo"), literal(True))
296+
assert parser.parse("foo in (true, false)") == In(Reference("foo"), {literal(True), literal(False)})

0 commit comments

Comments
 (0)