diff --git a/python/pyiceberg/expressions/base.py b/python/pyiceberg/expressions/base.py index 4b4a487a4d53..f91093b17ad4 100644 --- a/python/pyiceberg/expressions/base.py +++ b/python/pyiceberg/expressions/base.py @@ -19,58 +19,18 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from functools import reduce, singledispatch -from typing import Generic, TypeVar +from typing import ClassVar, Generic, TypeVar +from pyiceberg.expressions.literals import Literal from pyiceberg.files import StructProtocol from pyiceberg.schema import Accessor, Schema -from pyiceberg.types import NestedField +from pyiceberg.types import DoubleType, FloatType, NestedField from pyiceberg.utils.singleton import Singleton T = TypeVar("T") B = TypeVar("B") -class Literal(Generic[T], ABC): - """Literal which has a value and can be converted between types""" - - def __init__(self, value: T, value_type: type): - if value is None or not isinstance(value, value_type): - raise TypeError(f"Invalid literal value: {value} (not a {value_type})") - self._value = value - - @property - def value(self) -> T: - return self._value # type: ignore - - @abstractmethod - def to(self, type_var) -> Literal: - ... # pragma: no cover - - def __repr__(self): - return f"{type(self).__name__}({self.value})" - - def __str__(self): - return str(self.value) - - def __eq__(self, other): - return self.value == other.value - - def __ne__(self, other): - return not self.__eq__(other) - - def __lt__(self, other): - return self.value < other.value - - def __gt__(self, other): - return self.value > other.value - - def __le__(self, other): - return self.value <= other.value - - def __ge__(self, other): - return self.value >= other.value - - class BooleanExpression(ABC): """Represents a boolean expression tree.""" @@ -105,6 +65,10 @@ class BaseReference(Generic[T], Term, ABC): class BoundTerm(Bound[T], Term): """Represents a bound term.""" + @abstractmethod + def ref(self) -> BoundReference[T]: + ... + class UnboundTerm(Unbound[T, BoundTerm[T]], Term): """Represents an unbound term.""" @@ -131,6 +95,9 @@ def eval(self, struct: StructProtocol) -> T: """ return self.accessor.get(struct) + def ref(self) -> BoundReference[T]: + return self + @dataclass(frozen=True) class Reference(UnboundTerm[T], BaseReference[T]): @@ -171,72 +138,6 @@ def bind(self, schema: Schema, case_sensitive: bool) -> BoundReference[T]: return BoundReference(field=field, accessor=accessor) -class BoundPredicate(Bound[T], BooleanExpression): - _term: BoundTerm[T] - _literals: tuple[Literal[T], ...] - - def __init__(self, term: BoundTerm[T], *literals: Literal[T]): - self._term = term - self._literals = literals - self._validate_literals() - - def _validate_literals(self): - if len(self.literals) != 1: - raise AttributeError(f"{self.__class__.__name__} must have exactly 1 literal.") - - @property - def term(self) -> BoundTerm[T]: - return self._term - - @property - def literals(self) -> tuple[Literal[T], ...]: - return self._literals - - def __str__(self) -> str: - return f"{self.__class__.__name__}({str(self.term)}{self.literals and ', '+str(self.literals)})" - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({repr(self.term)}{self.literals and ', '+repr(self.literals)})" - - def __eq__(self, other) -> bool: - return id(self) == id(other) or ( - type(self) == type(other) and self.term == other.term and self.literals == other.literals - ) - - -class UnboundPredicate(Unbound[T, BooleanExpression], BooleanExpression, ABC): - _term: UnboundTerm[T] - _literals: tuple[Literal[T], ...] - - def __init__(self, term: UnboundTerm[T], *literals: Literal[T]): - self._term = term - self._literals = literals - self._validate_literals() - - def _validate_literals(self): - if len(self.literals) != 1: - raise AttributeError(f"{self.__class__.__name__} must have exactly 1 literal.") - - @property - def term(self) -> UnboundTerm[T]: - return self._term - - @property - def literals(self) -> tuple[Literal[T], ...]: - return self._literals - - def __str__(self) -> str: - return f"{self.__class__.__name__}({str(self.term)}{self.literals and ', '+str(self.literals)})" - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({repr(self.term)}{self.literals and ', '+repr(self.literals)})" - - def __eq__(self, other) -> bool: - return id(self) == id(other) or ( - type(self) == type(other) and self.term == other.term and self.literals == other.literals - ) - - class And(BooleanExpression): """AND operation expression - logical conjunction""" @@ -342,7 +243,7 @@ def __str__(self) -> str: @dataclass(frozen=True) -class AlwaysTrue(BooleanExpression, ABC, Singleton): +class AlwaysTrue(BooleanExpression, Singleton): """TRUE expression""" def __invert__(self) -> AlwaysFalse: @@ -350,227 +251,313 @@ def __invert__(self) -> AlwaysFalse: @dataclass(frozen=True) -class AlwaysFalse(BooleanExpression, ABC, Singleton): +class AlwaysFalse(BooleanExpression, Singleton): """FALSE expression""" def __invert__(self) -> AlwaysTrue: return AlwaysTrue() -class IsNull(UnboundPredicate[T]): - def __invert__(self) -> NotNull: - return NotNull(self.term) +@dataclass(frozen=True) +class BoundPredicate(Bound[T], BooleanExpression): + term: BoundTerm[T] - def _validate_literals(self): # pylint: disable=W0238 - if self.literals is not None: - raise AttributeError("Null is a unary predicate and takes no Literals.") + def __invert__(self) -> BoundPredicate[T]: + raise NotImplementedError - def bind(self, schema: Schema, case_sensitive: bool) -> BoundIsNull[T]: - bound_ref = self.term.bind(schema, case_sensitive) - return BoundIsNull(bound_ref) +@dataclass(frozen=True) +class UnboundPredicate(Unbound[T, BooleanExpression], BooleanExpression): + as_bound: ClassVar[type] + term: UnboundTerm[T] -class BoundIsNull(BoundPredicate[T]): - def __invert__(self) -> BoundNotNull: - return BoundNotNull(self.term) + def __invert__(self) -> UnboundPredicate[T]: + raise NotImplementedError - def _validate_literals(self): # pylint: disable=W0238 - if self.literals: - raise AttributeError("Null is a unary predicate and takes no Literals.") + def bind(self, schema: Schema, case_sensitive: bool = True) -> BooleanExpression: + raise NotImplementedError -class NotNull(UnboundPredicate[T]): - def __invert__(self) -> IsNull: - return IsNull(self.term) +@dataclass(frozen=True) +class UnaryPredicate(UnboundPredicate[T]): + def bind(self, schema: Schema, case_sensitive: bool = True) -> BooleanExpression: + bound_term = self.term.bind(schema, case_sensitive) + return self.as_bound(bound_term) + + def __invert__(self) -> UnaryPredicate[T]: + raise NotImplementedError + - def _validate_literals(self): # pylint: disable=W0238 - if self.literals: - raise AttributeError("NotNull is a unary predicate and takes no Literals.") +@dataclass(frozen=True) +class BoundUnaryPredicate(BoundPredicate[T]): + def __invert__(self) -> BoundUnaryPredicate[T]: + raise NotImplementedError + + +@dataclass(frozen=True) +class BoundIsNull(BoundUnaryPredicate[T]): + def __new__(cls, term: BoundTerm[T]): # pylint: disable=W0221 + if term.ref().field.required: + return AlwaysFalse() + return super().__new__(cls) + + def __invert__(self) -> BoundNotNull[T]: + return BoundNotNull(self.term) - def bind(self, schema: Schema, case_sensitive: bool) -> BoundNotNull[T]: - bound_ref = self.term.bind(schema, case_sensitive) - return BoundNotNull(bound_ref) +@dataclass(frozen=True) +class BoundNotNull(BoundUnaryPredicate[T]): + def __new__(cls, term: BoundTerm[T]): # pylint: disable=W0221 + if term.ref().field.required: + return AlwaysTrue() + return super().__new__(cls) -class BoundNotNull(BoundPredicate[T]): def __invert__(self) -> BoundIsNull: return BoundIsNull(self.term) - def _validate_literals(self): # pylint: disable=W0238 - if self.literals: - raise AttributeError("NotNull is a unary predicate and takes no Literals.") +@dataclass(frozen=True) +class IsNull(UnaryPredicate[T]): + as_bound = BoundIsNull + + def __invert__(self) -> NotNull[T]: + return NotNull(self.term) -class IsNaN(UnboundPredicate[T]): - def __invert__(self) -> NotNaN: - return NotNaN(self.term) - def _validate_literals(self): # pylint: disable=W0238 - if self.literals: - raise AttributeError("IsNaN is a unary predicate and takes no Literals.") +@dataclass(frozen=True) +class NotNull(UnaryPredicate[T]): + as_bound = BoundNotNull - def bind(self, schema: Schema, case_sensitive: bool) -> BoundIsNaN[T]: - bound_ref = self.term.bind(schema, case_sensitive) - return BoundIsNaN(bound_ref) + def __invert__(self) -> IsNull[T]: + return IsNull(self.term) -class BoundIsNaN(BoundPredicate[T]): - def __invert__(self) -> BoundNotNaN: +@dataclass(frozen=True) +class BoundIsNaN(BoundUnaryPredicate[T]): + def __new__(cls, term: BoundTerm[T]): # pylint: disable=W0221 + bound_type = term.ref().field.field_type + if type(bound_type) in {FloatType, DoubleType}: + return super().__new__(cls) + return AlwaysFalse() + + def __invert__(self) -> BoundNotNaN[T]: return BoundNotNaN(self.term) - def _validate_literals(self): # pylint: disable=W0238 - if self.literals: - raise AttributeError("IsNaN is a unary predicate and takes no Literals.") + +@dataclass(frozen=True) +class BoundNotNaN(BoundUnaryPredicate[T]): + def __new__(cls, term: BoundTerm[T]): # pylint: disable=W0221 + bound_type = term.ref().field.field_type + if type(bound_type) in {FloatType, DoubleType}: + return super().__new__(cls) + return AlwaysTrue() + + def __invert__(self) -> BoundIsNaN[T]: + return BoundIsNaN(self.term) -class NotNaN(UnboundPredicate[T]): - def __invert__(self) -> IsNaN: +@dataclass(frozen=True) +class IsNaN(UnaryPredicate[T]): + as_bound = BoundIsNaN + + def __invert__(self) -> NotNaN[T]: + return NotNaN(self.term) + + +@dataclass(frozen=True) +class NotNaN(UnaryPredicate[T]): + as_bound = BoundNotNaN + + def __invert__(self) -> IsNaN[T]: return IsNaN(self.term) - def _validate_literals(self): # pylint: disable=W0238 - if self.literals: - raise AttributeError("NotNaN is a unary predicate and takes no Literals.") - def bind(self, schema: Schema, case_sensitive: bool) -> BoundNotNaN[T]: - bound_ref = self.term.bind(schema, case_sensitive) - return BoundNotNaN(bound_ref) +@dataclass(frozen=True) +class SetPredicate(UnboundPredicate[T]): + literals: tuple[Literal[T], ...] + + def __invert__(self) -> SetPredicate[T]: + raise NotImplementedError + def bind(self, schema: Schema, case_sensitive: bool = True) -> BooleanExpression: + bound_term = self.term.bind(schema, case_sensitive) + return self.as_bound(bound_term, {lit.to(bound_term.ref().field.field_type) for lit in self.literals}) -class BoundNotNaN(BoundPredicate[T]): - def __invert__(self) -> BoundIsNaN: - return BoundIsNaN(self.term) - def _validate_literals(self): # pylint: disable=W0238 - if self.literals: - raise AttributeError("NotNaN is a unary predicate and takes no Literals.") +@dataclass(frozen=True) +class BoundSetPredicate(BoundPredicate[T]): + literals: set[Literal[T]] + + def __invert__(self) -> BoundSetPredicate[T]: + raise NotImplementedError -class BoundIn(BoundPredicate[T]): - def _validate_literals(self): # pylint: disable=W0238 - if not self.literals: - raise AttributeError("BoundIn must contain at least 1 literal.") +@dataclass(frozen=True) +class BoundIn(BoundSetPredicate[T]): + def __new__(cls, term: BoundTerm[T], literals: set[Literal[T]]): # 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[T]: - return BoundNotIn(self.term, *self.literals) + return BoundNotIn(self.term, self.literals) + + +@dataclass(frozen=True) +class BoundNotIn(BoundSetPredicate[T]): + def __new__(cls, term: BoundTerm[T], literals: set[Literal[T]]): # pylint: disable=W0221 + 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[T]: + return BoundIn(self.term, self.literals) -class In(UnboundPredicate[T]): - def _validate_literals(self): # pylint: disable=W0238 - if not self.literals: - raise AttributeError("In must contain at least 1 literal.") + +@dataclass(frozen=True) +class In(SetPredicate[T]): + as_bound = BoundIn + + def __new__(cls, term: UnboundTerm[T], literals: tuple[Literal[T], ...]): # pylint: disable=W0221 + count = len(literals) + if count == 0: + return AlwaysFalse() + elif count == 1: + return EqualTo(term, literals[0]) + else: + return super().__new__(cls) def __invert__(self) -> NotIn[T]: - return NotIn(self.term, *self.literals) + return NotIn(self.term, self.literals) - def bind(self, schema: Schema, case_sensitive: bool) -> BoundIn[T]: - bound_ref = self.term.bind(schema, case_sensitive) - return BoundIn(bound_ref, *tuple(lit.to(bound_ref.field.field_type) for lit in self.literals)) # type: ignore +@dataclass(frozen=True) +class NotIn(SetPredicate[T]): + as_bound = BoundNotIn -class BoundNotIn(BoundPredicate[T]): - def _validate_literals(self): # pylint: disable=W0238 - if not self.literals: - raise AttributeError("BoundNotIn must contain at least 1 literal.") + def __new__(cls, term: UnboundTerm[T], literals: tuple[Literal[T], ...]): # pylint: disable=W0221 + count = len(literals) + if count == 0: + return AlwaysTrue() + elif count == 1: + return NotEqualTo(term, literals[0]) + else: + return super().__new__(cls) - def __invert__(self) -> BoundIn[T]: - return BoundIn(self.term, *self.literals) + def __invert__(self) -> In[T]: + return In(self.term, self.literals) -class NotIn(UnboundPredicate[T]): - def _validate_literals(self): # pylint: disable=W0238 - if not self.literals: - raise AttributeError("NotIn must contain at least 1 literal.") +@dataclass(frozen=True) +class LiteralPredicate(UnboundPredicate[T]): + literal: Literal[T] - def __invert__(self) -> In[T]: - return In(self.term, *self.literals) + def bind(self, schema: Schema, case_sensitive: bool = True) -> BooleanExpression: + bound_term = self.term.bind(schema, case_sensitive) + return self.as_bound(bound_term, self.literal.to(bound_term.ref().field.field_type)) - def bind(self, schema: Schema, case_sensitive: bool) -> BoundNotIn[T]: - bound_ref = self.term.bind(schema, case_sensitive) - return BoundNotIn(bound_ref, *tuple(lit.to(bound_ref.field.field_type) for lit in self.literals)) # type: ignore + def __invert__(self) -> LiteralPredicate[T]: + raise NotImplementedError -class BoundEq(BoundPredicate[T]): - def __invert__(self) -> BoundNotEq[T]: - return BoundNotEq(self.term, *self.literals) +@dataclass(frozen=True) +class BoundLiteralPredicate(BoundPredicate[T]): + literal: Literal[T] + def __invert__(self) -> BoundLiteralPredicate[T]: + raise NotImplementedError -class Eq(UnboundPredicate[T]): - def __invert__(self) -> NotEq[T]: - return NotEq(self.term, *self.literals) - def bind(self, schema: Schema, case_sensitive: bool) -> BoundEq[T]: - bound_ref = self.term.bind(schema, case_sensitive) - return BoundEq(bound_ref, self.literals[0].to(bound_ref.field.field_type)) # type: ignore +@dataclass(frozen=True) +class BoundEqualTo(BoundLiteralPredicate[T]): + def __invert__(self) -> BoundNotEqualTo[T]: + return BoundNotEqualTo(self.term, self.literal) -class BoundNotEq(BoundPredicate[T]): - def __invert__(self) -> BoundEq[T]: - return BoundEq(self.term, *self.literals) +@dataclass(frozen=True) +class BoundNotEqualTo(BoundLiteralPredicate[T]): + def __invert__(self) -> BoundEqualTo[T]: + return BoundEqualTo(self.term, self.literal) -class NotEq(UnboundPredicate[T]): - def __invert__(self) -> Eq[T]: - return Eq(self.term, *self.literals) +@dataclass(frozen=True) +class BoundGreaterThanOrEqual(BoundLiteralPredicate[T]): + def __invert__(self) -> BoundLessThan[T]: + return BoundLessThan(self.term, self.literal) - def bind(self, schema: Schema, case_sensitive: bool) -> BoundNotEq[T]: - bound_ref = self.term.bind(schema, case_sensitive) - return BoundNotEq(bound_ref, self.literals[0].to(bound_ref.field.field_type)) # type: ignore +@dataclass(frozen=True) +class BoundGreaterThan(BoundLiteralPredicate[T]): + def __invert__(self) -> BoundLessThanOrEqual[T]: + return BoundLessThanOrEqual(self.term, self.literal) + + +@dataclass(frozen=True) +class BoundLessThan(BoundLiteralPredicate[T]): + def __invert__(self) -> BoundGreaterThanOrEqual[T]: + return BoundGreaterThanOrEqual(self.term, self.literal) -class BoundLt(BoundPredicate[T]): - def __invert__(self) -> BoundGtEq[T]: - return BoundGtEq(self.term, *self.literals) + +@dataclass(frozen=True) +class BoundLessThanOrEqual(BoundLiteralPredicate[T]): + def __invert__(self) -> BoundGreaterThan[T]: + return BoundGreaterThan(self.term, self.literal) -class Lt(UnboundPredicate[T]): - def __invert__(self) -> GtEq[T]: - return GtEq(self.term, *self.literals) +@dataclass(frozen=True) +class EqualTo(LiteralPredicate[T]): + as_bound = BoundEqualTo - def bind(self, schema: Schema, case_sensitive: bool) -> BoundEq[T]: - bound_ref = self.term.bind(schema, case_sensitive) - return BoundLt(bound_ref, self.literals[0].to(bound_ref.field.field_type)) # type: ignore + def __invert__(self) -> NotEqualTo[T]: + return NotEqualTo(self.term, self.literal) -class BoundGtEq(BoundPredicate[T]): - def __invert__(self) -> BoundLt[T]: - return BoundLt(self.term, *self.literals) +@dataclass(frozen=True) +class NotEqualTo(LiteralPredicate[T]): + as_bound = BoundNotEqualTo + def __invert__(self) -> EqualTo[T]: + return EqualTo(self.term, self.literal) -class GtEq(UnboundPredicate[T]): - def __invert__(self) -> Lt[T]: - return Lt(self.term, *self.literals) - def bind(self, schema: Schema, case_sensitive: bool) -> BoundEq[T]: - bound_ref = self.term.bind(schema, case_sensitive) - return BoundGtEq(bound_ref, self.literals[0].to(bound_ref.field.field_type)) # type: ignore +@dataclass(frozen=True) +class LessThan(LiteralPredicate[T]): + as_bound = BoundLessThan + def __invert__(self) -> GreaterThanOrEqual[T]: + return GreaterThanOrEqual(self.term, self.literal) -class BoundGt(BoundPredicate[T]): - def __invert__(self) -> BoundLtEq[T]: - return BoundLtEq(self.term, *self.literals) +@dataclass(frozen=True) +class GreaterThanOrEqual(LiteralPredicate[T]): + as_bound = BoundGreaterThanOrEqual -class Gt(UnboundPredicate[T]): - def __invert__(self) -> LtEq[T]: - return LtEq(self.term, *self.literals) + def __invert__(self) -> LessThan[T]: + return LessThan(self.term, self.literal) - def bind(self, schema: Schema, case_sensitive: bool) -> BoundEq[T]: - bound_ref = self.term.bind(schema, case_sensitive) - return BoundGt(bound_ref, self.literals[0].to(bound_ref.field.field_type)) # type: ignore +@dataclass(frozen=True) +class GreaterThan(LiteralPredicate[T]): + as_bound = BoundGreaterThan -class BoundLtEq(BoundPredicate[T]): - def __invert__(self) -> BoundGt[T]: - return BoundGt(self.term, *self.literals) + def __invert__(self) -> LessThanOrEqual[T]: + return LessThanOrEqual(self.term, self.literal) -class LtEq(UnboundPredicate[T]): - def __invert__(self) -> Gt[T]: - return Gt(self.term, *self.literals) +@dataclass(frozen=True) +class LessThanOrEqual(LiteralPredicate[T]): + as_bound = BoundLessThanOrEqual - def bind(self, schema: Schema, case_sensitive: bool) -> BoundEq[T]: - bound_ref = self.term.bind(schema, case_sensitive) - return BoundLtEq(bound_ref, self.literals[0].to(bound_ref.field.field_type)) # type: ignore + def __invert__(self) -> GreaterThan[T]: + return GreaterThan(self.term, self.literal) class BooleanExpressionVisitor(Generic[T], ABC): @@ -680,6 +667,12 @@ def _(obj: In, visitor: BooleanExpressionVisitor[T]) -> T: return visitor.visit_unbound_predicate(predicate=obj) +@visit.register(UnboundPredicate) +def _(obj: UnboundPredicate, visitor: BooleanExpressionVisitor[T]) -> T: + """Visit an In boolean expression with a concrete BooleanExpressionVisitor""" + return visitor.visit_unbound_predicate(predicate=obj) + + @visit.register(Or) def _(obj: Or, visitor: BooleanExpressionVisitor[T]) -> T: """Visit an Or boolean expression with a concrete BooleanExpressionVisitor""" diff --git a/python/pyiceberg/expressions/literals.py b/python/pyiceberg/expressions/literals.py index 53491854bee5..f49ee61805e3 100644 --- a/python/pyiceberg/expressions/literals.py +++ b/python/pyiceberg/expressions/literals.py @@ -19,14 +19,15 @@ # specific language governing permissions and limitations # under the License. # pylint: disable=W0613 +from __future__ import annotations import struct +from abc import ABC, abstractmethod from decimal import ROUND_HALF_UP, Decimal from functools import singledispatch, singledispatchmethod -from typing import Optional, Union +from typing import Generic, TypeVar from uuid import UUID -from pyiceberg.expressions.base import Literal from pyiceberg.types import ( BinaryType, BooleanType, @@ -52,6 +53,52 @@ ) from pyiceberg.utils.singleton import Singleton +T = TypeVar("T") + + +class Literal(Generic[T], ABC): + """Literal which has a value and can be converted between types""" + + def __init__(self, value: T, value_type: type): + if value is None or not isinstance(value, value_type): + raise TypeError(f"Invalid literal value: {value} (not a {value_type})") + self._value = value + + @property + def value(self) -> T: + return self._value # type: ignore + + @abstractmethod + def to(self, type_var) -> Literal: + ... # pragma: no cover + + def __repr__(self): + return f"{type(self).__name__}({self.value})" + + def __str__(self): + return str(self.value) + + def __hash__(self): + return hash(self.value) + + def __eq__(self, other): + return self.value == other.value + + def __ne__(self, other): + return not self.__eq__(other) + + def __lt__(self, other): + return self.value < other.value + + def __gt__(self, other): + return self.value > other.value + + def __le__(self, other): + return self.value <= other.value + + def __ge__(self, other): + return self.value >= other.value + @singledispatch def literal(value) -> Literal: @@ -170,7 +217,7 @@ def _(self, type_var: LongType) -> Literal[int]: return self @to.register(IntegerType) - def _(self, type_var: IntegerType) -> Union[AboveMax, BelowMin, Literal[int]]: + def _(self, type_var: IntegerType) -> AboveMax | BelowMin | Literal[int]: if IntegerType.max < self.value: return AboveMax() elif IntegerType.min > self.value: @@ -258,7 +305,7 @@ def _(self, type_var: DoubleType) -> Literal[float]: return self @to.register(FloatType) - def _(self, type_var: FloatType) -> Union[AboveMax, BelowMin, Literal[float]]: + def _(self, type_var: FloatType) -> AboveMax | BelowMin | Literal[float]: if FloatType.max < self.value: return AboveMax() elif FloatType.min > self.value: @@ -322,7 +369,7 @@ def to(self, type_var): return None @to.register(DecimalType) - def _(self, type_var: DecimalType) -> Optional[Literal[Decimal]]: + def _(self, type_var: DecimalType) -> Literal[Decimal] | None: if type_var.scale == abs(self.value.as_tuple().exponent): return self return None @@ -341,28 +388,28 @@ def _(self, type_var: StringType) -> Literal[str]: return self @to.register(DateType) - def _(self, type_var: DateType) -> Optional[Literal[int]]: + def _(self, type_var: DateType) -> Literal[int] | None: try: return DateLiteral(date_to_days(self.value)) except (TypeError, ValueError): return None @to.register(TimeType) - def _(self, type_var: TimeType) -> Optional[Literal[int]]: + def _(self, type_var: TimeType) -> Literal[int] | None: try: return TimeLiteral(time_to_micros(self.value)) except (TypeError, ValueError): return None @to.register(TimestampType) - def _(self, type_var: TimestampType) -> Optional[Literal[int]]: + def _(self, type_var: TimestampType) -> Literal[int] | None: try: return TimestampLiteral(timestamp_to_micros(self.value)) except (TypeError, ValueError): return None @to.register(TimestamptzType) - def _(self, type_var: TimestamptzType) -> Optional[Literal[int]]: + def _(self, type_var: TimestamptzType) -> Literal[int] | None: try: return TimestampLiteral(timestamptz_to_micros(self.value)) except (TypeError, ValueError): @@ -373,7 +420,7 @@ def _(self, type_var: UUIDType) -> Literal[UUID]: return UUIDLiteral(UUID(self.value)) @to.register(DecimalType) - def _(self, type_var: DecimalType) -> Optional[Literal[Decimal]]: + def _(self, type_var: DecimalType) -> Literal[Decimal] | None: dec = Decimal(self.value) if type_var.scale == abs(dec.as_tuple().exponent): return DecimalLiteral(dec) @@ -403,7 +450,7 @@ def to(self, type_var): return None @to.register(FixedType) - def _(self, type_var: FixedType) -> Optional[Literal[bytes]]: + def _(self, type_var: FixedType) -> Literal[bytes] | None: if len(self.value) == type_var.length: return self else: @@ -427,7 +474,7 @@ def _(self, type_var: BinaryType) -> Literal[bytes]: return self @to.register(FixedType) - def _(self, type_var: FixedType) -> Optional[Literal[bytes]]: + def _(self, type_var: FixedType) -> Literal[bytes] | None: if type_var.length == len(self.value): return FixedLiteral(self.value) else: diff --git a/python/tests/expressions/test_expressions_base.py b/python/tests/expressions/test_expressions_base.py index 2fca17d6cc1c..ba2850133bb0 100644 --- a/python/tests/expressions/test_expressions_base.py +++ b/python/tests/expressions/test_expressions_base.py @@ -23,8 +23,14 @@ from pyiceberg.expressions import base from pyiceberg.expressions.literals import LongLiteral, StringLiteral, literal -from pyiceberg.schema import Accessor -from pyiceberg.types import IntegerType, NestedField, StringType +from pyiceberg.schema import Accessor, Schema +from pyiceberg.types import ( + DoubleType, + FloatType, + IntegerType, + NestedField, + StringType, +) from pyiceberg.utils.singleton import Singleton @@ -127,6 +133,78 @@ def test_reprs(op, rep): assert repr(op) == rep +def test_isnull_inverse(): + assert ~base.IsNull(base.Reference("a")) == base.NotNull(base.Reference("a")) + + +def test_isnull_bind(): + schema = Schema(NestedField(2, "a", IntegerType()), schema_id=1) + bound = base.BoundIsNull(base.BoundReference(schema.find_field(2), schema.accessor_for_field(2))) + assert base.IsNull(base.Reference("a")).bind(schema) == bound + + +def test_isnull_bind_required(): + schema = Schema(NestedField(2, "a", IntegerType(), required=True), schema_id=1) + assert base.IsNull(base.Reference("a")).bind(schema) == base.AlwaysFalse() + + +def test_notnull_inverse(): + assert ~base.NotNull(base.Reference("a")) == base.IsNull(base.Reference("a")) + + +def test_notnull_bind(): + schema = Schema(NestedField(2, "a", IntegerType()), schema_id=1) + bound = base.BoundNotNull(base.BoundReference(schema.find_field(2), schema.accessor_for_field(2))) + assert base.NotNull(base.Reference("a")).bind(schema) == bound + + +def test_notnull_bind_required(): + schema = Schema(NestedField(2, "a", IntegerType(), required=True), schema_id=1) + assert base.NotNull(base.Reference("a")).bind(schema) == base.AlwaysTrue() + + +def test_isnan_inverse(): + assert ~base.IsNaN(base.Reference("f")) == base.NotNaN(base.Reference("f")) + + +def test_isnan_bind_float(): + schema = Schema(NestedField(2, "f", FloatType()), schema_id=1) + bound = base.BoundIsNaN(base.BoundReference(schema.find_field(2), schema.accessor_for_field(2))) + assert base.IsNaN(base.Reference("f")).bind(schema) == bound + + +def test_isnan_bind_double(): + schema = Schema(NestedField(2, "d", DoubleType()), schema_id=1) + bound = base.BoundIsNaN(base.BoundReference(schema.find_field(2), schema.accessor_for_field(2))) + assert base.IsNaN(base.Reference("d")).bind(schema) == bound + + +def test_isnan_bind_nonfloat(): + schema = Schema(NestedField(2, "i", IntegerType()), schema_id=1) + assert base.IsNaN(base.Reference("i")).bind(schema) == base.AlwaysFalse() + + +def test_notnan_inverse(): + assert ~base.NotNaN(base.Reference("f")) == base.IsNaN(base.Reference("f")) + + +def test_notnan_bind_float(): + schema = Schema(NestedField(2, "f", FloatType()), schema_id=1) + bound = base.BoundNotNaN(base.BoundReference(schema.find_field(2), schema.accessor_for_field(2))) + assert base.NotNaN(base.Reference("f")).bind(schema) == bound + + +def test_notnan_bind_double(): + schema = Schema(NestedField(2, "d", DoubleType()), schema_id=1) + bound = base.BoundNotNaN(base.BoundReference(schema.find_field(2), schema.accessor_for_field(2))) + assert base.NotNaN(base.Reference("d")).bind(schema) == bound + + +def test_notnan_bind_nonfloat(): + schema = Schema(NestedField(2, "i", IntegerType()), schema_id=1) + assert base.NotNaN(base.Reference("i")).bind(schema) == base.AlwaysTrue() + + @pytest.mark.parametrize( "op, string", [ @@ -139,210 +217,138 @@ def test_strs(op, string): assert str(op) == string +def test_ref_binding_case_sensitive(request): + schema = request.getfixturevalue("table_schema_simple") + ref = base.Reference("foo") + bound = base.BoundReference(schema.find_field(1), schema.accessor_for_field(1)) + assert ref.bind(schema, case_sensitive=True) == bound + + +def test_ref_binding_case_sensitive_failure(request): + schema = request.getfixturevalue("table_schema_simple") + ref = base.Reference("Foo") + with pytest.raises(ValueError): + ref.bind(schema, case_sensitive=True) + + +def test_ref_binding_case_insensitive(request): + schema = request.getfixturevalue("table_schema_simple") + ref = base.Reference("Foo") + bound = base.BoundReference(schema.find_field(1), schema.accessor_for_field(1)) + assert ref.bind(schema, case_sensitive=False) == bound + + +def test_ref_binding_case_insensitive_failure(request): + schema = request.getfixturevalue("table_schema_simple") + ref = base.Reference("Foot") + with pytest.raises(ValueError): + ref.bind(schema, case_sensitive=False) + + +def test_in_to_eq(): + assert base.In(base.Reference("x"), (literal(34.56),)) == base.EqualTo(base.Reference("x"), literal(34.56)) + + +def test_bind_in(request): + schema = request.getfixturevalue("table_schema_simple") + bound = base.BoundIn( + base.BoundReference(schema.find_field(1), schema.accessor_for_field(1)), {literal("hello"), literal("world")} + ) + assert base.In(base.Reference("foo"), (literal("hello"), literal("world"))).bind(schema) == bound + + +def test_bind_dedup(request): + schema = request.getfixturevalue("table_schema_simple") + bound = base.BoundIn( + base.BoundReference(schema.find_field(1), schema.accessor_for_field(1)), {literal("hello"), literal("world")} + ) + assert base.In(base.Reference("foo"), (literal("hello"), literal("world"), literal("world"))).bind(schema) == bound + + +def test_bind_dedup_to_eq(request): + schema = request.getfixturevalue("table_schema_simple") + bound = base.BoundEqualTo(base.BoundReference(schema.find_field(1), schema.accessor_for_field(1)), literal("hello")) + assert base.In(base.Reference("foo"), (literal("hello"), literal("hello"))).bind(schema) == bound + + @pytest.mark.parametrize( - "a, schema, case_sensitive, success", + "a, schema", [ ( - base.In(base.Reference("foo"), literal("hello"), literal("world")), - "table_schema_simple", - True, - True, - ), - ( - base.In(base.Reference("not_foo"), literal("hello"), literal("world")), - "table_schema_simple", - False, - False, - ), - ( - base.In(base.Reference("Bar"), literal(5), literal(2)), - "table_schema_simple", - False, - True, - ), - ( - base.In(base.Reference("Bar"), literal(5), literal(2)), - "table_schema_simple", - True, - False, - ), - ( - base.NotIn(base.Reference("foo"), literal("hello"), literal("world")), - "table_schema_simple", - True, - True, - ), - ( - base.NotIn(base.Reference("not_foo"), literal("hello"), literal("world")), - "table_schema_simple", - False, - False, - ), - ( - base.NotIn(base.Reference("Bar"), literal(5), literal(2)), - "table_schema_simple", - False, - True, - ), - ( - base.NotIn(base.Reference("Bar"), literal(5), literal(2)), - "table_schema_simple", - True, - False, - ), - ( - base.NotEq(base.Reference("foo"), literal("hello")), - "table_schema_simple", - True, - True, - ), - ( - base.NotEq(base.Reference("not_foo"), literal("hello")), - "table_schema_simple", - False, - False, - ), - ( - base.NotEq(base.Reference("Bar"), literal(5)), - "table_schema_simple", - False, - True, - ), - ( - base.NotEq(base.Reference("Bar"), literal(5)), - "table_schema_simple", - True, - False, - ), - ( - base.Eq(base.Reference("foo"), literal("hello")), + base.NotIn(base.Reference("foo"), (literal("hello"), literal("world"))), "table_schema_simple", - True, - True, ), ( - base.Eq(base.Reference("not_foo"), literal("hello")), + base.NotEqualTo(base.Reference("foo"), literal("hello")), "table_schema_simple", - False, - False, ), ( - base.Eq(base.Reference("Bar"), literal(5)), + base.EqualTo(base.Reference("foo"), literal("hello")), "table_schema_simple", - False, - True, ), ( - base.Eq(base.Reference("Bar"), literal(5)), + base.GreaterThan(base.Reference("foo"), literal("hello")), "table_schema_simple", - True, - False, ), ( - base.Gt(base.Reference("foo"), literal("hello")), + base.LessThan(base.Reference("foo"), literal("hello")), "table_schema_simple", - True, - True, ), ( - base.Gt(base.Reference("not_foo"), literal("hello")), + base.GreaterThanOrEqual(base.Reference("foo"), literal("hello")), "table_schema_simple", - False, - False, ), ( - base.Gt(base.Reference("Bar"), literal(5)), + base.LessThanOrEqual(base.Reference("foo"), literal("hello")), "table_schema_simple", - False, - True, - ), - ( - base.Gt(base.Reference("Bar"), literal(5)), - "table_schema_simple", - True, - False, - ), - ( - base.Lt(base.Reference("foo"), literal("hello")), - "table_schema_simple", - True, - True, - ), - ( - base.Lt(base.Reference("not_foo"), literal("hello")), - "table_schema_simple", - False, - False, - ), - ( - base.Lt(base.Reference("Bar"), literal(5)), - "table_schema_simple", - False, - True, - ), - ( - base.Lt(base.Reference("Bar"), literal(5)), - "table_schema_simple", - True, - False, ), + ], +) +def test_bind(a, schema, request): + schema = request.getfixturevalue(schema) + assert a.bind(schema, case_sensitive=True).term.field == schema.find_field(a.term.name, case_sensitive=True) + + +@pytest.mark.parametrize( + "a, schema", + [ ( - base.GtEq(base.Reference("foo"), literal("hello")), + base.In(base.Reference("Bar"), (literal(5), literal(2))), "table_schema_simple", - True, - True, ), ( - base.GtEq(base.Reference("not_foo"), literal("hello")), + base.NotIn(base.Reference("Bar"), (literal(5), literal(2))), "table_schema_simple", - False, - False, ), ( - base.GtEq(base.Reference("Bar"), literal(5)), + base.NotEqualTo(base.Reference("Bar"), literal(5)), "table_schema_simple", - False, - True, ), ( - base.GtEq(base.Reference("Bar"), literal(5)), + base.EqualTo(base.Reference("Bar"), literal(5)), "table_schema_simple", - True, - False, ), ( - base.LtEq(base.Reference("foo"), literal("hello")), + base.GreaterThan(base.Reference("Bar"), literal(5)), "table_schema_simple", - True, - True, ), ( - base.LtEq(base.Reference("not_foo"), literal("hello")), + base.LessThan(base.Reference("Bar"), literal(5)), "table_schema_simple", - False, - False, ), ( - base.LtEq(base.Reference("Bar"), literal(5)), + base.GreaterThanOrEqual(base.Reference("Bar"), literal(5)), "table_schema_simple", - False, - True, ), ( - base.LtEq(base.Reference("Bar"), literal(5)), + base.LessThanOrEqual(base.Reference("Bar"), literal(5)), "table_schema_simple", - True, - False, ), ], ) -def test_bind(a, schema, case_sensitive, success, request): +def test_bind_case_insensitive(a, schema, request): schema = request.getfixturevalue(schema) - if success: - assert a.bind(schema, case_sensitive).term.field == schema.find_field(a.term.name, case_sensitive) - else: - with pytest.raises(ValueError): - a.bind(schema, case_sensitive) + assert a.bind(schema, case_sensitive=False).term.field == schema.find_field(a.term.name, case_sensitive=False) @pytest.mark.parametrize( @@ -362,14 +368,14 @@ def test_bind(a, schema, case_sensitive, success, request): (ExpressionA(), ExpressionA(), ExpressionB()), (ExpressionB(), ExpressionB(), ExpressionA()), ( - base.In(base.Reference("foo"), literal("hello"), literal("world")), - base.In(base.Reference("foo"), literal("hello"), literal("world")), - base.In(base.Reference("not_foo"), literal("hello"), literal("world")), + base.In(base.Reference("foo"), (literal("hello"), literal("world"))), + base.In(base.Reference("foo"), (literal("hello"), literal("world"))), + base.In(base.Reference("not_foo"), (literal("hello"), literal("world"))), ), ( - base.In(base.Reference("foo"), literal("hello"), literal("world")), - base.In(base.Reference("foo"), literal("hello"), literal("world")), - base.In(base.Reference("foo"), literal("goodbye"), literal("world")), + base.In(base.Reference("foo"), (literal("hello"), literal("world"))), + base.In(base.Reference("foo"), (literal("hello"), literal("world"))), + base.In(base.Reference("foo"), (literal("goodbye"), literal("world"))), ), ], ) @@ -393,16 +399,16 @@ def test_eq(exp, testexpra, testexprb): ExpressionA(), ), ( - base.In(base.Reference("foo"), literal("hello"), literal("world")), - base.NotIn(base.Reference("foo"), literal("hello"), literal("world")), + base.In(base.Reference("foo"), (literal("hello"), literal("world"))), + base.NotIn(base.Reference("foo"), (literal("hello"), literal("world"))), ), ( - base.NotIn(base.Reference("foo"), literal("hello"), literal("world")), - base.In(base.Reference("foo"), literal("hello"), literal("world")), + base.NotIn(base.Reference("foo"), (literal("hello"), literal("world"))), + base.In(base.Reference("foo"), (literal("hello"), literal("world"))), ), - (base.Gt(base.Reference("foo"), literal(5)), base.LtEq(base.Reference("foo"), literal(5))), - (base.Lt(base.Reference("foo"), literal(5)), base.GtEq(base.Reference("foo"), literal(5))), - (base.Eq(base.Reference("foo"), literal(5)), base.NotEq(base.Reference("foo"), literal(5))), + (base.GreaterThan(base.Reference("foo"), literal(5)), base.LessThanOrEqual(base.Reference("foo"), literal(5))), + (base.LessThan(base.Reference("foo"), literal(5)), base.GreaterThanOrEqual(base.Reference("foo"), literal(5))), + (base.EqualTo(base.Reference("foo"), literal(5)), base.NotEqualTo(base.Reference("foo"), literal(5))), ( ExpressionA(), ExpressionB(), @@ -586,8 +592,8 @@ def test_always_false_or_always_true_expression_binding(table_schema_simple): [ ( base.And( - base.In(base.Reference("foo"), literal("foo"), literal("bar")), - base.In(base.Reference("bar"), literal(1), literal(2), literal(3)), + base.In(base.Reference("foo"), (literal("foo"), literal("bar"))), + base.In(base.Reference("bar"), (literal(1), literal(2), literal(3))), ), base.And( base.BoundIn[str]( @@ -595,30 +601,27 @@ def test_always_false_or_always_true_expression_binding(table_schema_simple): field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False), accessor=Accessor(position=0, inner=None), ), - StringLiteral("foo"), - StringLiteral("bar"), + {StringLiteral("foo"), StringLiteral("bar")}, ), base.BoundIn[int]( base.BoundReference( field=NestedField(field_id=2, name="bar", field_type=IntegerType(), required=True), accessor=Accessor(position=1, inner=None), ), - LongLiteral(1), - LongLiteral(2), - LongLiteral(3), + {LongLiteral(1), LongLiteral(2), LongLiteral(3)}, ), ), ), ( base.And( - base.In(base.Reference("foo"), literal("bar"), literal("baz")), + base.In(base.Reference("foo"), (literal("bar"), literal("baz"))), base.In( base.Reference("bar"), - literal(1), + (literal(1),), ), base.In( base.Reference("foo"), - literal("baz"), + (literal("baz"),), ), ), base.And( @@ -628,10 +631,9 @@ def test_always_false_or_always_true_expression_binding(table_schema_simple): field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False), accessor=Accessor(position=0, inner=None), ), - StringLiteral("bar"), - StringLiteral("baz"), + {StringLiteral("bar"), StringLiteral("baz")}, ), - base.BoundIn[int]( + base.BoundEqualTo[int]( base.BoundReference( field=NestedField(field_id=2, name="bar", field_type=IntegerType(), required=True), accessor=Accessor(position=1, inner=None), @@ -639,7 +641,7 @@ def test_always_false_or_always_true_expression_binding(table_schema_simple): LongLiteral(1), ), ), - base.BoundIn[str]( + base.BoundEqualTo[str]( base.BoundReference( field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False), accessor=Accessor(position=0, inner=None), @@ -661,8 +663,8 @@ def test_and_expression_binding(unbound_and_expression, expected_bound_expressio [ ( base.Or( - base.In(base.Reference("foo"), literal("foo"), literal("bar")), - base.In(base.Reference("bar"), literal(1), literal(2), literal(3)), + base.In(base.Reference("foo"), (literal("foo"), literal("bar"))), + base.In(base.Reference("bar"), (literal(1), literal(2), literal(3))), ), base.Or( base.BoundIn[str]( @@ -670,30 +672,27 @@ def test_and_expression_binding(unbound_and_expression, expected_bound_expressio field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False), accessor=Accessor(position=0, inner=None), ), - StringLiteral("foo"), - StringLiteral("bar"), + {StringLiteral("foo"), StringLiteral("bar")}, ), base.BoundIn[int]( base.BoundReference( field=NestedField(field_id=2, name="bar", field_type=IntegerType(), required=True), accessor=Accessor(position=1, inner=None), ), - LongLiteral(1), - LongLiteral(2), - LongLiteral(3), + {LongLiteral(1), LongLiteral(2), LongLiteral(3)}, ), ), ), ( base.Or( - base.In(base.Reference("foo"), literal("bar"), literal("baz")), + base.In(base.Reference("foo"), (literal("bar"), literal("baz"))), base.In( base.Reference("foo"), - literal("bar"), + (literal("bar"),), ), base.In( base.Reference("foo"), - literal("baz"), + (literal("baz"),), ), ), base.Or( @@ -703,15 +702,14 @@ def test_and_expression_binding(unbound_and_expression, expected_bound_expressio field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False), accessor=Accessor(position=0, inner=None), ), - StringLiteral("bar"), - StringLiteral("baz"), + {StringLiteral("bar"), StringLiteral("baz")}, ), base.BoundIn[str]( base.BoundReference( field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False), accessor=Accessor(position=0, inner=None), ), - StringLiteral("bar"), + {StringLiteral("bar")}, ), ), base.BoundIn[str]( @@ -719,7 +717,7 @@ def test_and_expression_binding(unbound_and_expression, expected_bound_expressio field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False), accessor=Accessor(position=0, inner=None), ), - StringLiteral("baz"), + {StringLiteral("baz")}, ), ), ), @@ -756,33 +754,31 @@ def test_or_expression_binding(unbound_or_expression, expected_bound_expression, "unbound_in_expression,expected_bound_expression", [ ( - base.In(base.Reference("foo"), literal("foo"), literal("bar")), + base.In(base.Reference("foo"), (literal("foo"), literal("bar"))), base.BoundIn[str]( base.BoundReference[str]( field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False), accessor=Accessor(position=0, inner=None), ), - StringLiteral("foo"), - StringLiteral("bar"), + {StringLiteral("foo"), StringLiteral("bar")}, ), ), ( - base.In(base.Reference("foo"), literal("bar"), literal("baz")), + base.In(base.Reference("foo"), (literal("bar"), literal("baz"))), base.BoundIn[str]( base.BoundReference[str]( field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False), accessor=Accessor(position=0, inner=None), ), - StringLiteral("bar"), - StringLiteral("baz"), + {StringLiteral("bar"), StringLiteral("baz")}, ), ), ( base.In( base.Reference("foo"), - literal("bar"), + (literal("bar"),), ), - base.BoundIn[str]( + base.BoundEqualTo( base.BoundReference[str]( field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False), accessor=Accessor(position=0, inner=None), @@ -802,23 +798,22 @@ def test_in_expression_binding(unbound_in_expression, expected_bound_expression, "unbound_not_expression,expected_bound_expression", [ ( - base.Not(base.In(base.Reference("foo"), literal("foo"), literal("bar"))), + base.Not(base.In(base.Reference("foo"), (literal("foo"), literal("bar")))), base.Not( base.BoundIn[str]( base.BoundReference[str]( field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False), accessor=Accessor(position=0, inner=None), ), - StringLiteral("foo"), - StringLiteral("bar"), + {StringLiteral("foo"), StringLiteral("bar")}, ) ), ), ( base.Not( base.Or( - base.In(base.Reference("foo"), literal("foo"), literal("bar")), - base.In(base.Reference("foo"), literal("foo"), literal("bar"), literal("baz")), + base.In(base.Reference("foo"), (literal("foo"), literal("bar"))), + base.In(base.Reference("foo"), (literal("foo"), literal("bar"), literal("baz"))), ) ), base.Not( @@ -828,17 +823,14 @@ def test_in_expression_binding(unbound_in_expression, expected_bound_expression, field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False), accessor=Accessor(position=0, inner=None), ), - StringLiteral("foo"), - StringLiteral("bar"), + {StringLiteral("foo"), StringLiteral("bar")}, ), base.BoundIn[str]( base.BoundReference( field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False), accessor=Accessor(position=0, inner=None), ), - StringLiteral("foo"), - StringLiteral("bar"), - StringLiteral("baz"), + {StringLiteral("foo"), StringLiteral("bar"), StringLiteral("baz")}, ), ), ),