| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| from __future__ import annotations |
| |
| import builtins |
| from abc import ABC, abstractmethod |
| from collections.abc import Callable, Iterable, Sequence |
| from functools import cached_property |
| from typing import Any |
| from typing import Literal as TypingLiteral |
| |
| from pydantic import ConfigDict, Field |
| |
| from pyiceberg.expressions.literals import AboveMax, BelowMin, Literal, literal |
| from pyiceberg.schema import Accessor, Schema |
| from pyiceberg.typedef import IcebergBaseModel, IcebergRootModel, L, LiteralValue, StructProtocol |
| from pyiceberg.types import DoubleType, FloatType, NestedField |
| from pyiceberg.utils.singleton import Singleton |
| |
| |
| def _to_unbound_term(term: str | UnboundTerm) -> UnboundTerm: |
| return Reference(term) if isinstance(term, str) else term |
| |
| |
| def _to_literal_set(values: Iterable[L] | Iterable[Literal[L]]) -> set[Literal[L]]: |
| return {_to_literal(v) for v in values} |
| |
| |
| def _to_literal(value: L | Literal[L]) -> Literal[L]: |
| if isinstance(value, Literal): |
| return value |
| else: |
| return literal(value) |
| |
| |
| class BooleanExpression(ABC): |
| """An expression that evaluates to a boolean.""" |
| |
| @abstractmethod |
| def __invert__(self) -> BooleanExpression: |
| """Transform the Expression into its negated version.""" |
| |
| def __and__(self, other: BooleanExpression) -> BooleanExpression: |
| """Perform and operation on another expression.""" |
| if not isinstance(other, BooleanExpression): |
| raise ValueError(f"Expected BooleanExpression, got: {other}") |
| |
| return And(self, other) |
| |
| def __or__(self, other: BooleanExpression) -> BooleanExpression: |
| """Perform or operation on another expression.""" |
| if not isinstance(other, BooleanExpression): |
| raise ValueError(f"Expected BooleanExpression, got: {other}") |
| |
| return Or(self, other) |
| |
| |
| def _build_balanced_tree( |
| operator_: Callable[[BooleanExpression, BooleanExpression], BooleanExpression], items: Sequence[BooleanExpression] |
| ) -> BooleanExpression: |
| """ |
| Recursively constructs a balanced binary tree of BooleanExpressions using the provided binary operator. |
| |
| This function is a safer and more scalable alternative to: |
| reduce(operator_, items) |
| |
| Using `reduce` creates a deeply nested, unbalanced tree (e.g., operator_(a, operator_(b, operator_(c, ...)))), |
| which grows linearly with the number of items. This can lead to RecursionError exceptions in Python |
| when the number of expressions is large (e.g., >1000). |
| |
| In contrast, this function builds a balanced binary tree with logarithmic depth (O(log n)), |
| helping avoid recursion issues and ensuring that expression trees remain stable, predictable, |
| and safe to traverse — especially in tools like PyIceberg that operate on large logical trees. |
| |
| Parameters: |
| operator_ (Callable): A binary operator function (e.g., pyiceberg.expressions.Or, And) that takes two |
| BooleanExpressions and returns a combined BooleanExpression. |
| items (Sequence[BooleanExpression]): A sequence of BooleanExpression objects to combine. |
| |
| Returns: |
| BooleanExpression: The balanced combination of all input BooleanExpressions. |
| |
| Raises: |
| ValueError: If the input sequence is empty. |
| """ |
| if not items: |
| raise ValueError("No expressions to combine") |
| if len(items) == 1: |
| return items[0] |
| mid = len(items) // 2 |
| |
| left = _build_balanced_tree(operator_, items[:mid]) |
| right = _build_balanced_tree(operator_, items[mid:]) |
| return operator_(left, right) |
| |
| |
| class Term: |
| """A simple expression that evaluates to a value.""" |
| |
| |
| class Bound: |
| """Represents a bound value expression.""" |
| |
| |
| class Unbound(ABC): |
| """Represents an unbound value expression.""" |
| |
| @abstractmethod |
| def bind(self, schema: Schema, case_sensitive: bool = True) -> Bound | BooleanExpression: ... |
| |
| @property |
| @abstractmethod |
| def as_bound(self) -> type[Bound]: ... |
| |
| |
| class BoundTerm(Term, Bound, ABC): |
| """Represents a bound term.""" |
| |
| @abstractmethod |
| def ref(self) -> BoundReference: |
| """Return the bound reference.""" |
| |
| @abstractmethod |
| def eval(self, struct: StructProtocol) -> Any: # pylint: disable=W0613 |
| """Return the value at the referenced field's position in an object that abides by the StructProtocol.""" |
| |
| |
| class BoundReference(BoundTerm): |
| """A reference bound to a field in a schema. |
| |
| Args: |
| field (NestedField): A referenced field in an Iceberg schema. |
| accessor (Accessor): An Accessor object to access the value at the field's position. |
| """ |
| |
| field: NestedField |
| accessor: Accessor |
| |
| def __init__(self, field: NestedField, accessor: Accessor): |
| self.field = field |
| self.accessor = accessor |
| |
| def eval(self, struct: StructProtocol) -> Any: |
| """Return the value at the referenced field's position in an object that abides by the StructProtocol. |
| |
| Args: |
| struct (StructProtocol): A row object that abides by the StructProtocol and returns values given a position. |
| Returns: |
| Any: The value at the referenced field's position in `struct`. |
| """ |
| return self.accessor.get(struct) |
| |
| def __eq__(self, other: Any) -> bool: |
| """Return the equality of two instances of the BoundReference class.""" |
| return self.field == other.field if isinstance(other, BoundReference) else False |
| |
| def __repr__(self) -> str: |
| """Return the string representation of the BoundReference class.""" |
| return f"BoundReference(field={repr(self.field)}, accessor={repr(self.accessor)})" |
| |
| def ref(self) -> BoundReference: |
| return self |
| |
| def __hash__(self) -> int: |
| """Return hash value of the BoundReference class.""" |
| return hash(str(self)) |
| |
| |
| class UnboundTerm(Term, Unbound, ABC): |
| """Represents an unbound term.""" |
| |
| @abstractmethod |
| def bind(self, schema: Schema, case_sensitive: bool = True) -> BoundTerm: ... |
| |
| |
| class Reference(UnboundTerm, IcebergRootModel[str]): |
| """A reference not yet bound to a field in a schema. |
| |
| Args: |
| name (str): The name of the field. |
| |
| Note: |
| An unbound reference is sometimes referred to as a "named" reference. |
| """ |
| |
| root: str = Field() |
| |
| def __init__(self, name: str) -> None: |
| super().__init__(name) |
| |
| def __repr__(self) -> str: |
| """Return the string representation of the Reference class.""" |
| return f"Reference(name={repr(self.root)})" |
| |
| def __str__(self) -> str: |
| """Return the string representation of the Reference class.""" |
| return f"Reference(name={repr(self.root)})" |
| |
| def bind(self, schema: Schema, case_sensitive: bool = True) -> BoundReference: |
| """Bind the reference to an Iceberg schema. |
| |
| Args: |
| schema (Schema): An Iceberg schema. |
| case_sensitive (bool): Whether to consider case when binding the reference to the field. |
| |
| Raises: |
| ValueError: If an empty name is provided. |
| |
| Returns: |
| BoundReference: A reference bound to the specific field in the Iceberg schema. |
| """ |
| field = schema.find_field(name_or_id=self.name, case_sensitive=case_sensitive) |
| accessor = schema.accessor_for_field(field.field_id) |
| return self.as_bound(field=field, accessor=accessor) |
| |
| @property |
| def name(self) -> str: |
| return self.root |
| |
| @property |
| def as_bound(self) -> type[BoundReference]: |
| return BoundReference |
| |
| |
| class And(BooleanExpression): |
| """AND operation expression - logical conjunction.""" |
| |
| left: BooleanExpression |
| right: BooleanExpression |
| |
| def __new__(cls, left: BooleanExpression, right: BooleanExpression, *rest: BooleanExpression) -> BooleanExpression: # type: ignore |
| if rest: |
| return _build_balanced_tree(And, (left, right, *rest)) |
| if left is AlwaysFalse() or right is AlwaysFalse(): |
| return AlwaysFalse() |
| elif left is AlwaysTrue(): |
| return right |
| elif right is AlwaysTrue(): |
| return left |
| else: |
| obj = super().__new__(cls) |
| obj.left = left |
| obj.right = right |
| return obj |
| |
| def __eq__(self, other: Any) -> bool: |
| """Return the equality of two instances of the And class.""" |
| return self.left == other.left and self.right == other.right if isinstance(other, And) else False |
| |
| def __str__(self) -> str: |
| """Return the string representation of the And class.""" |
| return f"And(left={str(self.left)}, right={str(self.right)})" |
| |
| def __repr__(self) -> str: |
| """Return the string representation of the And class.""" |
| return f"And(left={repr(self.left)}, right={repr(self.right)})" |
| |
| def __invert__(self) -> BooleanExpression: |
| """Transform the Expression into its negated version.""" |
| # De Morgan's law: not (A and B) = (not A) or (not B) |
| return Or(~self.left, ~self.right) |
| |
| def __getnewargs__(self) -> tuple[BooleanExpression, BooleanExpression]: |
| """Pickle the And class.""" |
| return (self.left, self.right) |
| |
| |
| class Or(IcebergBaseModel, BooleanExpression): |
| """OR operation expression - logical disjunction.""" |
| |
| model_config = ConfigDict(arbitrary_types_allowed=True) |
| |
| type: TypingLiteral["or"] = Field(default="or", alias="type") |
| left: BooleanExpression |
| right: BooleanExpression |
| |
| def __init__(self, left: BooleanExpression, right: BooleanExpression, *rest: BooleanExpression) -> None: |
| if isinstance(self, Or) and not hasattr(self, "left") and not hasattr(self, "right"): |
| super().__init__(left=left, right=right) |
| |
| def __new__(cls, left: BooleanExpression, right: BooleanExpression, *rest: BooleanExpression) -> BooleanExpression: # type: ignore |
| if rest: |
| return _build_balanced_tree(Or, (left, right, *rest)) |
| if left is AlwaysTrue() or right is AlwaysTrue(): |
| return AlwaysTrue() |
| elif left is AlwaysFalse(): |
| return right |
| elif right is AlwaysFalse(): |
| return left |
| else: |
| obj = super().__new__(cls) |
| return obj |
| |
| def __str__(self) -> str: |
| """Return the string representation of the Or class.""" |
| return f"{str(self.__class__.__name__)}(left={repr(self.left)}, right={repr(self.right)})" |
| |
| def __eq__(self, other: Any) -> bool: |
| """Return the equality of two instances of the Or class.""" |
| return self.left == other.left and self.right == other.right if isinstance(other, Or) else False |
| |
| def __repr__(self) -> str: |
| """Return the string representation of the Or class.""" |
| return f"Or(left={repr(self.left)}, right={repr(self.right)})" |
| |
| def __invert__(self) -> BooleanExpression: |
| """Transform the Expression into its negated version.""" |
| # De Morgan's law: not (A or B) = (not A) and (not B) |
| return And(~self.left, ~self.right) |
| |
| def __getnewargs__(self) -> tuple[BooleanExpression, BooleanExpression]: |
| """Pickle the Or class.""" |
| return (self.left, self.right) |
| |
| |
| class Not(IcebergBaseModel, BooleanExpression): |
| """NOT operation expression - logical negation.""" |
| |
| model_config = ConfigDict(arbitrary_types_allowed=True) |
| |
| type: TypingLiteral["not"] = Field(default="not") |
| child: BooleanExpression = Field() |
| |
| def __init__(self, child: BooleanExpression, **_: Any) -> None: |
| super().__init__(child=child) |
| |
| def __new__(cls, child: BooleanExpression, **_: Any) -> BooleanExpression: # type: ignore |
| if child is AlwaysTrue(): |
| return AlwaysFalse() |
| elif child is AlwaysFalse(): |
| return AlwaysTrue() |
| elif isinstance(child, Not): |
| return child.child |
| obj = super().__new__(cls) |
| return obj |
| |
| def __str__(self) -> str: |
| """Return the string representation of the Not class.""" |
| return f"Not(child={self.child})" |
| |
| def __repr__(self) -> str: |
| """Return the string representation of the Not class.""" |
| return f"Not(child={repr(self.child)})" |
| |
| def __eq__(self, other: Any) -> bool: |
| """Return the equality of two instances of the Not class.""" |
| return self.child == other.child if isinstance(other, Not) else False |
| |
| def __invert__(self) -> BooleanExpression: |
| """Transform the Expression into its negated version.""" |
| return self.child |
| |
| def __getnewargs__(self) -> tuple[BooleanExpression]: |
| """Pickle the Not class.""" |
| return (self.child,) |
| |
| |
| class AlwaysTrue(BooleanExpression, Singleton, IcebergRootModel[bool]): |
| """TRUE expression.""" |
| |
| root: bool = True |
| |
| def __invert__(self) -> AlwaysFalse: |
| """Transform the Expression into its negated version.""" |
| return AlwaysFalse() |
| |
| def __str__(self) -> str: |
| """Return the string representation of the AlwaysTrue class.""" |
| return "AlwaysTrue()" |
| |
| def __repr__(self) -> str: |
| """Return the string representation of the AlwaysTrue class.""" |
| return "AlwaysTrue()" |
| |
| |
| class AlwaysFalse(BooleanExpression, Singleton, IcebergRootModel[bool]): |
| """FALSE expression.""" |
| |
| root: bool = False |
| |
| def __invert__(self) -> AlwaysTrue: |
| """Transform the Expression into its negated version.""" |
| return AlwaysTrue() |
| |
| def __str__(self) -> str: |
| """Return the string representation of the AlwaysFalse class.""" |
| return "AlwaysFalse()" |
| |
| def __repr__(self) -> str: |
| """Return the string representation of the AlwaysFalse class.""" |
| return "AlwaysFalse()" |
| |
| |
| class BoundPredicate(Bound, BooleanExpression, ABC): |
| term: BoundTerm |
| |
| def __init__(self, term: BoundTerm): |
| self.term = term |
| |
| def __eq__(self, other: Any) -> bool: |
| """Return the equality of two instances of the BoundPredicate class.""" |
| if isinstance(other, self.__class__): |
| return self.term == other.term |
| return False |
| |
| @property |
| @abstractmethod |
| def as_unbound(self) -> type[UnboundPredicate]: ... |
| |
| |
| class UnboundPredicate(Unbound, BooleanExpression, ABC): |
| term: UnboundTerm |
| |
| def __init__(self, term: str | UnboundTerm): |
| self.term = _to_unbound_term(term) |
| |
| def __eq__(self, other: Any) -> bool: |
| """Return the equality of two instances of the UnboundPredicate class.""" |
| return self.term == other.term if isinstance(other, self.__class__) else False |
| |
| @abstractmethod |
| def bind(self, schema: Schema, case_sensitive: bool = True) -> BooleanExpression: ... |
| |
| @property |
| @abstractmethod |
| def as_bound(self) -> type[BoundPredicate]: ... |
| |
| |
| class UnaryPredicate(IcebergBaseModel, UnboundPredicate, ABC): |
| type: str |
| |
| model_config = {"arbitrary_types_allowed": True} |
| |
| def __init__(self, term: str | UnboundTerm): |
| unbound = _to_unbound_term(term) |
| super().__init__(term=unbound) |
| |
| def __str__(self) -> str: |
| """Return the string representation of the UnaryPredicate class.""" |
| # Sort to make it deterministic |
| return f"{str(self.__class__.__name__)}(term={str(self.term)})" |
| |
| def bind(self, schema: Schema, case_sensitive: bool = True) -> BoundUnaryPredicate: |
| bound_term = self.term.bind(schema, case_sensitive) |
| return self.as_bound(bound_term) # type: ignore |
| |
| def __repr__(self) -> str: |
| """Return the string representation of the UnaryPredicate class.""" |
| return f"{str(self.__class__.__name__)}(term={repr(self.term)})" |
| |
| @property |
| @abstractmethod |
| def as_bound(self) -> type[BoundUnaryPredicate]: ... # type: ignore |
| |
| |
| class BoundUnaryPredicate(BoundPredicate, ABC): |
| def __repr__(self) -> str: |
| """Return the string representation of the BoundUnaryPredicate class.""" |
| return f"{str(self.__class__.__name__)}(term={repr(self.term)})" |
| |
| @property |
| @abstractmethod |
| def as_unbound(self) -> type[UnaryPredicate]: ... |
| |
| def __getnewargs__(self) -> tuple[BoundTerm]: |
| """Pickle the BoundUnaryPredicate class.""" |
| return (self.term,) |
| |
| |
| class BoundIsNull(BoundUnaryPredicate): |
| def __new__(cls, term: BoundTerm) -> BooleanExpression: # type: ignore[misc] # pylint: disable=W0221 |
| if term.ref().field.required: |
| return AlwaysFalse() |
| return super().__new__(cls) |
| |
| def __invert__(self) -> BoundNotNull: |
| """Transform the Expression into its negated version.""" |
| return BoundNotNull(self.term) |
| |
| @property |
| def as_unbound(self) -> type[IsNull]: |
| return IsNull |
| |
| |
| class BoundNotNull(BoundUnaryPredicate): |
| def __new__(cls, term: BoundTerm) -> BooleanExpression: # type: ignore[misc] # pylint: disable=W0221 |
| if term.ref().field.required: |
| return AlwaysTrue() |
| return super().__new__(cls) |
| |
| def __invert__(self) -> BoundIsNull: |
| """Transform the Expression into its negated version.""" |
| return BoundIsNull(self.term) |
| |
| @property |
| def as_unbound(self) -> type[NotNull]: |
| return NotNull |
| |
| |
| class IsNull(UnaryPredicate): |
| type: str = "is-null" |
| |
| def __invert__(self) -> NotNull: |
| """Transform the Expression into its negated version.""" |
| return NotNull(self.term) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundIsNull]: |
| return BoundIsNull |
| |
| |
| class NotNull(UnaryPredicate): |
| type: str = "not-null" |
| |
| def __invert__(self) -> IsNull: |
| """Transform the Expression into its negated version.""" |
| return IsNull(self.term) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundNotNull]: |
| return BoundNotNull |
| |
| |
| class BoundIsNaN(BoundUnaryPredicate): |
| def __new__(cls, term: BoundTerm) -> BooleanExpression: # type: ignore[misc] # pylint: disable=W0221 |
| bound_type = term.ref().field.field_type |
| if isinstance(bound_type, (FloatType, DoubleType)): |
| return super().__new__(cls) |
| return AlwaysFalse() |
| |
| def __invert__(self) -> BoundNotNaN: |
| """Transform the Expression into its negated version.""" |
| return BoundNotNaN(self.term) |
| |
| @property |
| def as_unbound(self) -> type[IsNaN]: |
| return IsNaN |
| |
| |
| class BoundNotNaN(BoundUnaryPredicate): |
| def __new__(cls, term: BoundTerm) -> BooleanExpression: # type: ignore[misc] # pylint: disable=W0221 |
| bound_type = term.ref().field.field_type |
| if isinstance(bound_type, (FloatType, DoubleType)): |
| return super().__new__(cls) |
| return AlwaysTrue() |
| |
| def __invert__(self) -> BoundIsNaN: |
| """Transform the Expression into its negated version.""" |
| return BoundIsNaN(self.term) |
| |
| @property |
| def as_unbound(self) -> type[NotNaN]: |
| return NotNaN |
| |
| |
| class IsNaN(UnaryPredicate): |
| type: str = "is-nan" |
| |
| def __invert__(self) -> NotNaN: |
| """Transform the Expression into its negated version.""" |
| return NotNaN(self.term) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundIsNaN]: |
| return BoundIsNaN |
| |
| |
| class NotNaN(UnaryPredicate): |
| type: str = "not-nan" |
| |
| def __invert__(self) -> IsNaN: |
| """Transform the Expression into its negated version.""" |
| return IsNaN(self.term) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundNotNaN]: |
| return BoundNotNaN |
| |
| |
| class SetPredicate(IcebergBaseModel, UnboundPredicate, ABC): |
| model_config = ConfigDict(arbitrary_types_allowed=True) |
| |
| type: TypingLiteral["in", "not-in"] = Field(default="in") |
| literals: set[LiteralValue] = Field(alias="values") |
| |
| def __init__(self, term: str | UnboundTerm, literals: Iterable[Any] | Iterable[LiteralValue]): |
| literal_set = _to_literal_set(literals) |
| super().__init__(term=_to_unbound_term(term), values=literal_set) # type: ignore |
| object.__setattr__(self, "literals", literal_set) |
| |
| def bind(self, schema: Schema, case_sensitive: bool = True) -> BoundSetPredicate: |
| bound_term = self.term.bind(schema, case_sensitive) |
| literal_set = self.literals |
| return self.as_bound(bound_term, {lit.to(bound_term.ref().field.field_type) for lit in literal_set}) |
| |
| def __str__(self) -> str: |
| """Return the string representation of the SetPredicate class.""" |
| # Sort to make it deterministic |
| return f"{str(self.__class__.__name__)}({str(self.term)}, {{{', '.join(sorted([str(literal) for literal in self.literals]))}}})" |
| |
| def __repr__(self) -> str: |
| """Return the string representation of the SetPredicate class.""" |
| # Sort to make it deterministic |
| return f"{str(self.__class__.__name__)}({repr(self.term)}, {{{', '.join(sorted([repr(literal) for literal in self.literals]))}}})" |
| |
| def __eq__(self, other: Any) -> bool: |
| """Return the equality of two instances of the SetPredicate class.""" |
| return self.term == other.term and self.literals == other.literals if isinstance(other, self.__class__) else False |
| |
| def __getnewargs__(self) -> tuple[UnboundTerm, set[Any]]: |
| """Pickle the SetPredicate class.""" |
| return (self.term, self.literals) |
| |
| @property |
| @abstractmethod |
| def as_bound(self) -> builtins.type[BoundSetPredicate]: |
| return BoundSetPredicate |
| |
| |
| class BoundSetPredicate(BoundPredicate, ABC): |
| literals: set[LiteralValue] |
| |
| def __init__(self, term: BoundTerm, literals: set[LiteralValue]): |
| super().__init__(term) |
| self.literals = _to_literal_set(literals) # pylint: disable=W0621 |
| |
| @cached_property |
| def value_set(self) -> set[Any]: |
| return {lit.value for lit in self.literals} |
| |
| def __str__(self) -> str: |
| """Return the string representation of the BoundSetPredicate class.""" |
| # Sort to make it deterministic |
| return f"{str(self.__class__.__name__)}({str(self.term)}, {{{', '.join(sorted([str(literal) for literal in self.literals]))}}})" |
| |
| def __repr__(self) -> str: |
| """Return the string representation of the BoundSetPredicate class.""" |
| # Sort to make it deterministic |
| return f"{str(self.__class__.__name__)}({repr(self.term)}, {{{', '.join(sorted([repr(literal) for literal in self.literals]))}}})" |
| |
| def __eq__(self, other: Any) -> bool: |
| """Return the equality of two instances of the BoundSetPredicate class.""" |
| return self.term == other.term and self.literals == other.literals if isinstance(other, self.__class__) else False |
| |
| def __getnewargs__(self) -> tuple[BoundTerm, set[LiteralValue]]: |
| """Pickle the BoundSetPredicate class.""" |
| return (self.term, self.literals) |
| |
| @property |
| @abstractmethod |
| def as_unbound(self) -> type[SetPredicate]: ... |
| |
| |
| class BoundIn(BoundSetPredicate): |
| def __new__(cls, term: BoundTerm, literals: set[LiteralValue]) -> BooleanExpression: # type: ignore[misc] # pylint: disable=W0221 |
| count = len(literals) |
| if count == 0: |
| return AlwaysFalse() |
| elif count == 1: |
| return BoundEqualTo(term, next(iter(literals))) |
| else: |
| return super().__new__(cls) |
| |
| def __invert__(self) -> BoundNotIn: |
| """Transform the Expression into its negated version.""" |
| return BoundNotIn(self.term, self.literals) |
| |
| def __eq__(self, other: Any) -> bool: |
| """Return the equality of two instances of the BoundIn class.""" |
| return self.term == other.term and self.literals == other.literals if isinstance(other, self.__class__) else False |
| |
| @property |
| def as_unbound(self) -> type[In]: |
| return In |
| |
| |
| class BoundNotIn(BoundSetPredicate): |
| def __new__( # type: ignore[misc] # pylint: disable=W0221 |
| cls, |
| term: BoundTerm, |
| literals: set[LiteralValue], |
| ) -> BooleanExpression: |
| count = len(literals) |
| if count == 0: |
| return AlwaysTrue() |
| elif count == 1: |
| return BoundNotEqualTo(term, next(iter(literals))) |
| else: |
| return super().__new__(cls) |
| |
| def __invert__(self) -> BoundIn: |
| """Transform the Expression into its negated version.""" |
| return BoundIn(self.term, self.literals) |
| |
| @property |
| def as_unbound(self) -> type[NotIn]: |
| return NotIn |
| |
| |
| class In(SetPredicate): |
| type: TypingLiteral["in"] = Field(default="in", alias="type") |
| |
| def __new__( # type: ignore[misc] # pylint: disable=W0221 |
| cls, term: str | UnboundTerm, literals: Iterable[Any] | Iterable[LiteralValue] |
| ) -> BooleanExpression: |
| literals_set: set[LiteralValue] = _to_literal_set(literals) |
| count = len(literals_set) |
| if count == 0: |
| return AlwaysFalse() |
| elif count == 1: |
| return EqualTo(term, next(iter(literals))) |
| else: |
| return super().__new__(cls) |
| |
| def __invert__(self) -> NotIn: |
| """Transform the Expression into its negated version.""" |
| return NotIn(self.term, self.literals) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundIn]: |
| return BoundIn |
| |
| |
| class NotIn(SetPredicate, ABC): |
| type: TypingLiteral["not-in"] = Field(default="not-in", alias="type") |
| |
| def __new__( # type: ignore[misc] # pylint: disable=W0221 |
| cls, term: str | UnboundTerm, literals: Iterable[Any] | Iterable[LiteralValue] |
| ) -> BooleanExpression: |
| literals_set: set[LiteralValue] = _to_literal_set(literals) |
| count = len(literals_set) |
| if count == 0: |
| return AlwaysTrue() |
| elif count == 1: |
| return NotEqualTo(term, next(iter(literals_set))) |
| else: |
| return super().__new__(cls) |
| |
| def __invert__(self) -> In: |
| """Transform the Expression into its negated version.""" |
| return In(self.term, self.literals) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundNotIn]: |
| return BoundNotIn |
| |
| |
| class LiteralPredicate(IcebergBaseModel, UnboundPredicate, ABC): |
| type: TypingLiteral["lt", "lt-eq", "gt", "gt-eq", "eq", "not-eq", "starts-with", "not-starts-with"] = Field(alias="type") |
| term: UnboundTerm |
| value: LiteralValue = Field() |
| model_config = ConfigDict(populate_by_name=True, frozen=True, arbitrary_types_allowed=True) |
| |
| def __init__(self, term: str | UnboundTerm, literal: Any): |
| super().__init__(term=_to_unbound_term(term), value=_to_literal(literal)) # type: ignore[call-arg] |
| |
| @property |
| def literal(self) -> LiteralValue: |
| return self.value |
| |
| def bind(self, schema: Schema, case_sensitive: bool = True) -> BoundLiteralPredicate: |
| bound_term = self.term.bind(schema, case_sensitive) |
| lit = self.literal.to(bound_term.ref().field.field_type) |
| |
| if isinstance(lit, AboveMax): |
| if isinstance(self, (LessThan, LessThanOrEqual, NotEqualTo)): |
| return AlwaysTrue() |
| elif isinstance(self, (GreaterThan, GreaterThanOrEqual, EqualTo)): |
| return AlwaysFalse() |
| elif isinstance(lit, BelowMin): |
| if isinstance(self, (GreaterThan, GreaterThanOrEqual, NotEqualTo)): |
| return AlwaysTrue() |
| elif isinstance(self, (LessThan, LessThanOrEqual, EqualTo)): |
| return AlwaysFalse() |
| |
| return self.as_bound(bound_term, lit) |
| |
| def __eq__(self, other: Any) -> bool: |
| """Return the equality of two instances of the LiteralPredicate class.""" |
| if isinstance(other, self.__class__): |
| return self.term == other.term and self.literal == other.literal |
| return False |
| |
| def __str__(self) -> str: |
| """Return the string representation of the LiteralPredicate class.""" |
| return f"{str(self.__class__.__name__)}(term={repr(self.term)}, literal={repr(self.literal)})" |
| |
| def __repr__(self) -> str: |
| """Return the string representation of the LiteralPredicate class.""" |
| return f"{str(self.__class__.__name__)}(term={repr(self.term)}, literal={repr(self.literal)})" |
| |
| @property |
| @abstractmethod |
| def as_bound(self) -> builtins.type[BoundLiteralPredicate]: ... |
| |
| |
| class BoundLiteralPredicate(BoundPredicate, ABC): |
| literal: LiteralValue |
| |
| def __init__(self, term: BoundTerm, literal: LiteralValue): # pylint: disable=W0621 |
| super().__init__(term) |
| self.literal = literal # pylint: disable=W0621 |
| |
| def __eq__(self, other: Any) -> bool: |
| """Return the equality of two instances of the BoundLiteralPredicate class.""" |
| if isinstance(other, self.__class__): |
| return self.term == other.term and self.literal == other.literal |
| return False |
| |
| def __repr__(self) -> str: |
| """Return the string representation of the BoundLiteralPredicate class.""" |
| return f"{str(self.__class__.__name__)}(term={repr(self.term)}, literal={repr(self.literal)})" |
| |
| @property |
| @abstractmethod |
| def as_unbound(self) -> type[LiteralPredicate]: ... |
| |
| |
| class BoundEqualTo(BoundLiteralPredicate): |
| def __invert__(self) -> BoundNotEqualTo: |
| """Transform the Expression into its negated version.""" |
| return BoundNotEqualTo(self.term, self.literal) |
| |
| @property |
| def as_unbound(self) -> type[EqualTo]: |
| return EqualTo |
| |
| |
| class BoundNotEqualTo(BoundLiteralPredicate): |
| def __invert__(self) -> BoundEqualTo: |
| """Transform the Expression into its negated version.""" |
| return BoundEqualTo(self.term, self.literal) |
| |
| @property |
| def as_unbound(self) -> type[NotEqualTo]: |
| return NotEqualTo |
| |
| |
| class BoundGreaterThanOrEqual(BoundLiteralPredicate): |
| def __invert__(self) -> BoundLessThan: |
| """Transform the Expression into its negated version.""" |
| return BoundLessThan(self.term, self.literal) |
| |
| @property |
| def as_unbound(self) -> type[GreaterThanOrEqual]: |
| return GreaterThanOrEqual |
| |
| |
| class BoundGreaterThan(BoundLiteralPredicate): |
| def __invert__(self) -> BoundLessThanOrEqual: |
| """Transform the Expression into its negated version.""" |
| return BoundLessThanOrEqual(self.term, self.literal) |
| |
| @property |
| def as_unbound(self) -> type[GreaterThan]: |
| return GreaterThan |
| |
| |
| class BoundLessThan(BoundLiteralPredicate): |
| def __invert__(self) -> BoundGreaterThanOrEqual: |
| """Transform the Expression into its negated version.""" |
| return BoundGreaterThanOrEqual(self.term, self.literal) |
| |
| @property |
| def as_unbound(self) -> type[LessThan]: |
| return LessThan |
| |
| |
| class BoundLessThanOrEqual(BoundLiteralPredicate): |
| def __invert__(self) -> BoundGreaterThan: |
| """Transform the Expression into its negated version.""" |
| return BoundGreaterThan(self.term, self.literal) |
| |
| @property |
| def as_unbound(self) -> type[LessThanOrEqual]: |
| return LessThanOrEqual |
| |
| |
| class BoundStartsWith(BoundLiteralPredicate): |
| def __invert__(self) -> BoundNotStartsWith: |
| """Transform the Expression into its negated version.""" |
| return BoundNotStartsWith(self.term, self.literal) |
| |
| @property |
| def as_unbound(self) -> type[StartsWith]: |
| return StartsWith |
| |
| |
| class BoundNotStartsWith(BoundLiteralPredicate): |
| def __invert__(self) -> BoundStartsWith: |
| """Transform the Expression into its negated version.""" |
| return BoundStartsWith(self.term, self.literal) |
| |
| @property |
| def as_unbound(self) -> type[NotStartsWith]: |
| return NotStartsWith |
| |
| |
| class EqualTo(LiteralPredicate): |
| type: TypingLiteral["eq"] = Field(default="eq", alias="type") |
| |
| def __invert__(self) -> NotEqualTo: |
| """Transform the Expression into its negated version.""" |
| return NotEqualTo(self.term, self.literal) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundEqualTo]: |
| return BoundEqualTo |
| |
| |
| class NotEqualTo(LiteralPredicate): |
| type: TypingLiteral["not-eq"] = Field(default="not-eq", alias="type") |
| |
| def __invert__(self) -> EqualTo: |
| """Transform the Expression into its negated version.""" |
| return EqualTo(self.term, self.literal) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundNotEqualTo]: |
| return BoundNotEqualTo |
| |
| |
| class LessThan(LiteralPredicate): |
| type: TypingLiteral["lt"] = Field(default="lt", alias="type") |
| |
| def __invert__(self) -> GreaterThanOrEqual: |
| """Transform the Expression into its negated version.""" |
| return GreaterThanOrEqual(self.term, self.literal) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundLessThan]: |
| return BoundLessThan |
| |
| |
| class GreaterThanOrEqual(LiteralPredicate): |
| type: TypingLiteral["gt-eq"] = Field(default="gt-eq", alias="type") |
| |
| def __invert__(self) -> LessThan: |
| """Transform the Expression into its negated version.""" |
| return LessThan(self.term, self.literal) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundGreaterThanOrEqual]: |
| return BoundGreaterThanOrEqual |
| |
| |
| class GreaterThan(LiteralPredicate): |
| type: TypingLiteral["gt"] = Field(default="gt", alias="type") |
| |
| def __invert__(self) -> LessThanOrEqual: |
| """Transform the Expression into its negated version.""" |
| return LessThanOrEqual(self.term, self.literal) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundGreaterThan]: |
| return BoundGreaterThan |
| |
| |
| class LessThanOrEqual(LiteralPredicate): |
| type: TypingLiteral["lt-eq"] = Field(default="lt-eq", alias="type") |
| |
| def __invert__(self) -> GreaterThan: |
| """Transform the Expression into its negated version.""" |
| return GreaterThan(self.term, self.literal) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundLessThanOrEqual]: |
| return BoundLessThanOrEqual |
| |
| |
| class StartsWith(LiteralPredicate): |
| type: TypingLiteral["starts-with"] = Field(default="starts-with", alias="type") |
| |
| def __invert__(self) -> NotStartsWith: |
| """Transform the Expression into its negated version.""" |
| return NotStartsWith(self.term, self.literal) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundStartsWith]: |
| return BoundStartsWith |
| |
| |
| class NotStartsWith(LiteralPredicate): |
| type: TypingLiteral["not-starts-with"] = Field(default="not-starts-with", alias="type") |
| |
| def __invert__(self) -> StartsWith: |
| """Transform the Expression into its negated version.""" |
| return StartsWith(self.term, self.literal) |
| |
| @property |
| def as_bound(self) -> builtins.type[BoundNotStartsWith]: |
| return BoundNotStartsWith |