You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by bl...@apache.org on 2022/10/05 20:45:55 UTC

[iceberg] branch master updated: Python: Add more expression tests (#5923)

This is an automated email from the ASF dual-hosted git repository.

blue pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/master by this push:
     new d46734e518 Python: Add more expression tests (#5923)
d46734e518 is described below

commit d46734e51834e209e461bd26af5a77cb1a467ff7
Author: Fokko Driesprong <fo...@apache.org>
AuthorDate: Wed Oct 5 22:45:49 2022 +0200

    Python: Add more expression tests (#5923)
---
 python/pyiceberg/expressions/base.py              |  25 +-
 python/pyiceberg/schema.py                        |  25 +-
 python/tests/expressions/test_expressions_base.py | 477 +++++++++++++++++-----
 3 files changed, 412 insertions(+), 115 deletions(-)

diff --git a/python/pyiceberg/expressions/base.py b/python/pyiceberg/expressions/base.py
index ea0873ede0..5272145758 100644
--- a/python/pyiceberg/expressions/base.py
+++ b/python/pyiceberg/expressions/base.py
@@ -65,11 +65,11 @@ class BoundTerm(Term[T], Bound, ABC):
 
     @abstractmethod
     def ref(self) -> BoundReference[T]:
-        ...
+        """Returns the bound reference"""
 
     @abstractmethod
-    def eval(self, struct: StructProtocol):  # pylint: disable=W0613
-        ...  # pragma: no cover
+    def eval(self, struct: StructProtocol) -> T:  # pylint: disable=W0613
+        """Returns the value at the referenced field's position in an object that abides by the StructProtocol"""
 
 
 @dataclass(frozen=True)
@@ -129,13 +129,7 @@ class Reference(UnboundTerm[T]):
             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)
-        if not field:
-            raise ValueError(f"Cannot find field '{self.name}' in schema: {schema}")
-
         accessor = schema.accessor_for_field(field.field_id)
-        if not accessor:
-            raise ValueError(f"Cannot find accessor for field '{self.name}' in schema: {schema}")
-
         return BoundReference(field=field, accessor=accessor)
 
 
@@ -233,6 +227,7 @@ class BoundPredicate(Generic[T], Bound, BooleanExpression):
     term: BoundTerm[T]
 
     def __invert__(self) -> BoundPredicate[T]:
+        """Inverts the predicate"""
         raise NotImplementedError
 
 
@@ -242,9 +237,11 @@ class UnboundPredicate(Generic[T], Unbound[BooleanExpression], BooleanExpression
     term: UnboundTerm[T]
 
     def __invert__(self) -> UnboundPredicate[T]:
+        """Inverts the predicate"""
         raise NotImplementedError
 
     def bind(self, schema: Schema, case_sensitive: bool = True) -> BooleanExpression:
+        """Binds the predicate to a schema"""
         raise NotImplementedError
 
 
@@ -255,12 +252,14 @@ class UnaryPredicate(UnboundPredicate[T]):
         return self.as_bound(bound_term)
 
     def __invert__(self) -> UnaryPredicate[T]:
+        """Inverts the unary predicate"""
         raise NotImplementedError
 
 
 @dataclass(frozen=True)
 class BoundUnaryPredicate(BoundPredicate[T]):
     def __invert__(self) -> BoundUnaryPredicate[T]:
+        """Inverts the unary predicate"""
         raise NotImplementedError
 
 
@@ -347,6 +346,7 @@ class SetPredicate(UnboundPredicate[T]):
     literals: tuple[Literal[T], ...]
 
     def __invert__(self) -> SetPredicate[T]:
+        """Inverted expression of the SetPredicate"""
         raise NotImplementedError
 
     def bind(self, schema: Schema, case_sensitive: bool = True) -> BooleanExpression:
@@ -359,6 +359,7 @@ class BoundSetPredicate(BoundPredicate[T]):
     literals: set[Literal[T]]
 
     def __invert__(self) -> BoundSetPredicate[T]:
+        """Inverted expression of the SetPredicate"""
         raise NotImplementedError
 
 
@@ -435,6 +436,7 @@ class LiteralPredicate(UnboundPredicate[T]):
         return self.as_bound(bound_term, self.literal.to(bound_term.ref().field.field_type))
 
     def __invert__(self) -> LiteralPredicate[T]:
+        """Inverts the literal predicate"""
         raise NotImplementedError
 
 
@@ -443,6 +445,7 @@ class BoundLiteralPredicate(BoundPredicate[T]):
     literal: Literal[T]
 
     def __invert__(self) -> BoundLiteralPredicate[T]:
+        """Inverts the bound literal predicate"""
         raise NotImplementedError
 
 
@@ -678,10 +681,10 @@ class BindVisitor(BooleanExpressionVisitor[BooleanExpression]):
     def visit_or(self, left_result: BooleanExpression, right_result: BooleanExpression) -> BooleanExpression:
         return Or(left=left_result, right=right_result)
 
-    def visit_unbound_predicate(self, predicate) -> BooleanExpression:
+    def visit_unbound_predicate(self, predicate: UnboundPredicate) -> BooleanExpression:
         return predicate.bind(self._schema, case_sensitive=self._case_sensitive)
 
-    def visit_bound_predicate(self, predicate) -> BooleanExpression:
+    def visit_bound_predicate(self, predicate: BoundPredicate) -> BooleanExpression:
         raise TypeError(f"Found already bound predicate: {predicate}")
 
 
diff --git a/python/pyiceberg/schema.py b/python/pyiceberg/schema.py
index b0fc41b0e1..7f2c3166ff 100644
--- a/python/pyiceberg/schema.py
+++ b/python/pyiceberg/schema.py
@@ -133,27 +133,33 @@ class Schema(IcebergBaseModel):
         """Returns the schema as a struct"""
         return StructType(*self.fields)
 
-    def find_field(self, name_or_id: Union[str, int], case_sensitive: bool = True) -> Optional[NestedField]:
+    def find_field(self, name_or_id: Union[str, int], case_sensitive: bool = True) -> NestedField:
         """Find a field using a field name or field ID
 
         Args:
             name_or_id (str | int): Either a field name or a field ID
             case_sensitive (bool, optional): Whether to perform a case-sensitive lookup using a field name. Defaults to True.
 
+        Raises:
+            ValueError: When the value cannot be found
+
         Returns:
             NestedField: The matched NestedField
         """
         if isinstance(name_or_id, int):
-            return self._lazy_id_to_field.get(name_or_id)
+            if name_or_id not in self._lazy_id_to_field:
+                raise ValueError(f"Could not find field with id: {name_or_id}")
+            return self._lazy_id_to_field[name_or_id]
+
         if case_sensitive:
             field_id = self._name_to_id.get(name_or_id)
         else:
             field_id = self._lazy_name_to_id_lower.get(name_or_id.lower())
 
         if field_id is None:
-            raise ValueError(f"Could not find field with name or id {name_or_id}, case_sensitive={case_sensitive}")
+            raise ValueError(f"Could not find field with name {name_or_id}, case_sensitive={case_sensitive}")
 
-        return self._lazy_id_to_field.get(field_id)
+        return self._lazy_id_to_field[field_id]
 
     def find_type(self, name_or_id: Union[str, int], case_sensitive: bool = True) -> IcebergType:
         """Find a field type using a field name or field ID
@@ -185,16 +191,23 @@ class Schema(IcebergBaseModel):
         """
         return self._lazy_id_to_name.get(column_id)
 
-    def accessor_for_field(self, field_id: int) -> Optional["Accessor"]:
+    def accessor_for_field(self, field_id: int) -> "Accessor":
         """Find a schema position accessor given a field ID
 
         Args:
             field_id (int): The ID of the field
 
+        Raises:
+            ValueError: When the value cannot be found
+
         Returns:
             Accessor: An accessor for the given field ID
         """
-        return self._lazy_id_to_accessor.get(field_id)
+
+        if field_id not in self._lazy_id_to_accessor:
+            raise ValueError(f"Could not find accessor for field with id: {field_id}")
+
+        return self._lazy_id_to_accessor[field_id]
 
     def select(self, names: List[str], case_sensitive: bool = True) -> "Schema":
         """Return a new schema instance pruned to a subset of columns
diff --git a/python/tests/expressions/test_expressions_base.py b/python/tests/expressions/test_expressions_base.py
index 02997eaa18..30b6688c75 100644
--- a/python/tests/expressions/test_expressions_base.py
+++ b/python/tests/expressions/test_expressions_base.py
@@ -22,7 +22,7 @@ from typing import List, Set
 import pytest
 
 from pyiceberg.expressions import base
-from pyiceberg.expressions.base import rewrite_not
+from pyiceberg.expressions.base import rewrite_not, visit_bound_predicate
 from pyiceberg.expressions.literals import (
     Literal,
     LongLiteral,
@@ -213,7 +213,7 @@ class FooBoundBooleanExpressionVisitor(base.BoundBooleanExpressionVisitor[List])
         (base.Not(ExpressionA()), "Not(child=ExpressionA())"),
     ],
 )
-def test_reprs(op, rep):
+def test_reprs(op: base.BooleanExpression, rep: str):
     assert repr(op) == rep
 
 
@@ -227,6 +227,42 @@ def test_isnull_bind():
     assert base.IsNull(base.Reference("a")).bind(schema) == bound
 
 
+def test_invert_is_null_bind():
+    schema = Schema(NestedField(2, "a", IntegerType(), required=False), schema_id=1)
+    assert ~base.IsNull(base.Reference("a")).bind(schema) == base.NotNull(base.Reference("a")).bind(schema)
+
+
+def test_invert_not_null_bind():
+    schema = Schema(NestedField(2, "a", IntegerType(), required=False), schema_id=1)
+    assert ~base.NotNull(base.Reference("a")).bind(schema) == base.IsNull(base.Reference("a")).bind(schema)
+
+
+def test_invert_is_nan_bind():
+    schema = Schema(NestedField(2, "a", DoubleType(), required=False), schema_id=1)
+    assert ~base.IsNaN(base.Reference("a")).bind(schema) == base.NotNaN(base.Reference("a")).bind(schema)
+
+
+def test_invert_not_nan_bind():
+    schema = Schema(NestedField(2, "a", DoubleType(), required=False), schema_id=1)
+    assert ~base.NotNaN(base.Reference("a")).bind(schema) == base.IsNaN(base.Reference("a")).bind(schema)
+
+
+def test_bind_expr_does_not_exists():
+    schema = Schema(NestedField(2, "a", IntegerType()), schema_id=1)
+    with pytest.raises(ValueError) as exc_info:
+        base.IsNull(base.Reference("b")).bind(schema)
+
+    assert str(exc_info.value) == "Could not find field with name b, case_sensitive=True"
+
+
+def test_bind_does_not_exists():
+    schema = Schema(NestedField(2, "a", IntegerType()), schema_id=1)
+    with pytest.raises(ValueError) as exc_info:
+        base.Reference("b").bind(schema)
+
+    assert str(exc_info.value) == "Could not find field with name b, case_sensitive=True"
+
+
 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()
@@ -301,138 +337,289 @@ 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(table_schema_simple: Schema):
+    ref = base.Reference[str]("foo")
+    bound = base.BoundReference[str](table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1))
+    assert ref.bind(table_schema_simple, case_sensitive=True) == bound
 
 
-def test_ref_binding_case_sensitive_failure(request):
-    schema = request.getfixturevalue("table_schema_simple")
-    ref = base.Reference("Foo")
+def test_ref_binding_case_sensitive_failure(table_schema_simple: Schema):
+    ref = base.Reference[str]("Foo")
     with pytest.raises(ValueError):
-        ref.bind(schema, case_sensitive=True)
+        ref.bind(table_schema_simple, 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(table_schema_simple: Schema):
+    ref = base.Reference[str]("Foo")
+    bound = base.BoundReference[str](table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1))
+    assert ref.bind(table_schema_simple, case_sensitive=False) == bound
 
 
-def test_ref_binding_case_insensitive_failure(request):
-    schema = request.getfixturevalue("table_schema_simple")
-    ref = base.Reference("Foot")
+def test_ref_binding_case_insensitive_failure(table_schema_simple: Schema):
+    ref = base.Reference[str]("Foot")
     with pytest.raises(ValueError):
-        ref.bind(schema, case_sensitive=False)
+        ref.bind(table_schema_simple, 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")
+def test_empty_bind_in(table_schema_simple: Schema):
+    bound = base.BoundIn[str](
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)), set()
+    )
+    assert bound == base.AlwaysFalse()
+
+
+def test_empty_bind_not_in(table_schema_simple: Schema):
+    bound = base.BoundNotIn(
+        base.BoundReference[str](table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)), set()
+    )
+    assert bound == base.AlwaysTrue()
+
+
+def test_bind_not_in_equal_term(table_schema_simple: Schema):
+    bound = base.BoundNotIn(
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)), {literal("hello")}
+    )
+    assert (
+        base.BoundNotEqualTo[str](
+            term=base.BoundReference(
+                field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+                accessor=Accessor(position=0, inner=None),
+            ),
+            literal=StringLiteral("hello"),
+        )
+        == bound
+    )
+
+
+def test_in_empty():
+    assert base.In(base.Reference("foo"), ()) == base.AlwaysFalse()
+
+
+def test_not_in_empty():
+    assert base.NotIn(base.Reference("foo"), ()) == base.AlwaysTrue()
+
+
+def test_not_in_equal():
+    assert base.NotIn(base.Reference("foo"), (literal("hello"),)) == base.NotEqualTo(
+        term=base.Reference(name="foo"), literal=StringLiteral("hello")
+    )
+
+
+def test_bind_in(table_schema_simple: Schema):
     bound = base.BoundIn(
-        base.BoundReference(schema.find_field(1), schema.accessor_for_field(1)), {literal("hello"), literal("world")}
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)),
+        {literal("hello"), literal("world")},
     )
-    assert base.In(base.Reference("foo"), (literal("hello"), literal("world"))).bind(schema) == bound
+    assert base.In(base.Reference("foo"), (literal("hello"), literal("world"))).bind(table_schema_simple) == bound
 
 
-def test_bind_dedup(request):
-    schema = request.getfixturevalue("table_schema_simple")
+def test_bind_in_invert(table_schema_simple: Schema):
     bound = base.BoundIn(
-        base.BoundReference(schema.find_field(1), schema.accessor_for_field(1)), {literal("hello"), literal("world")}
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)),
+        {literal("hello"), literal("world")},
+    )
+    assert ~bound == base.BoundNotIn(
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.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
+def test_bind_not_in_invert(table_schema_simple: Schema):
+    bound = base.BoundNotIn(
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)),
+        {literal("hello"), literal("world")},
+    )
+    assert ~bound == base.BoundIn(
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)),
+        {literal("hello"), literal("world")},
+    )
 
 
-@pytest.mark.parametrize(
-    "a,  schema",
-    [
-        (
-            base.NotIn(base.Reference("foo"), (literal("hello"), literal("world"))),
-            "table_schema_simple",
-        ),
-        (
-            base.NotEqualTo(base.Reference("foo"), literal("hello")),
-            "table_schema_simple",
-        ),
-        (
-            base.EqualTo(base.Reference("foo"), literal("hello")),
-            "table_schema_simple",
-        ),
-        (
-            base.GreaterThan(base.Reference("foo"), literal("hello")),
-            "table_schema_simple",
+def test_bind_dedup(table_schema_simple: Schema):
+    bound = base.BoundIn(
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)),
+        {literal("hello"), literal("world")},
+    )
+    assert (
+        base.In(base.Reference("foo"), (literal("hello"), literal("world"), literal("world"))).bind(table_schema_simple) == bound
+    )
+
+
+def test_bind_dedup_to_eq(table_schema_simple: Schema):
+    bound = base.BoundEqualTo(
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)), literal("hello")
+    )
+    assert base.In(base.Reference("foo"), (literal("hello"), literal("hello"))).bind(table_schema_simple) == bound
+
+
+def test_bound_equal_to_invert(table_schema_simple: Schema):
+    bound = base.BoundEqualTo(
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)), literal("hello")
+    )
+    assert ~bound == base.BoundNotEqualTo(
+        term=base.BoundReference[str](
+            field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+            accessor=Accessor(position=0, inner=None),
         ),
-        (
-            base.LessThan(base.Reference("foo"), literal("hello")),
-            "table_schema_simple",
+        literal=StringLiteral("hello"),
+    )
+
+
+def test_bound_not_equal_to_invert(table_schema_simple: Schema):
+    bound = base.BoundNotEqualTo(
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)), literal("hello")
+    )
+    assert ~bound == base.BoundEqualTo(
+        term=base.BoundReference[str](
+            field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+            accessor=Accessor(position=0, inner=None),
         ),
-        (
-            base.GreaterThanOrEqual(base.Reference("foo"), literal("hello")),
-            "table_schema_simple",
+        literal=StringLiteral("hello"),
+    )
+
+
+def test_bound_greater_than_or_equal_invert(table_schema_simple: Schema):
+    bound = base.BoundGreaterThanOrEqual(
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)), literal("hello")
+    )
+    assert ~bound == base.BoundLessThan(
+        term=base.BoundReference[str](
+            field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+            accessor=Accessor(position=0, inner=None),
         ),
-        (
-            base.LessThanOrEqual(base.Reference("foo"), literal("hello")),
-            "table_schema_simple",
+        literal=StringLiteral("hello"),
+    )
+
+
+def test_bound_greater_than_invert(table_schema_simple: Schema):
+    bound = base.BoundGreaterThan(
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)), literal("hello")
+    )
+    assert ~bound == base.BoundLessThanOrEqual(
+        term=base.BoundReference[str](
+            field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+            accessor=Accessor(position=0, inner=None),
         ),
-    ],
-)
-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)
+        literal=StringLiteral("hello"),
+    )
 
 
-@pytest.mark.parametrize(
-    "a,  schema",
-    [
-        (
-            base.In(base.Reference("Bar"), (literal(5), literal(2))),
-            "table_schema_simple",
+def test_bound_less_than_invert(table_schema_simple: Schema):
+    bound = base.BoundLessThan(
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)), literal("hello")
+    )
+    assert ~bound == base.BoundGreaterThanOrEqual(
+        term=base.BoundReference[str](
+            field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+            accessor=Accessor(position=0, inner=None),
         ),
-        (
-            base.NotIn(base.Reference("Bar"), (literal(5), literal(2))),
-            "table_schema_simple",
+        literal=StringLiteral("hello"),
+    )
+
+
+def test_bound_less_than_or_equal_invert(table_schema_simple: Schema):
+    bound = base.BoundLessThanOrEqual(
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)), literal("hello")
+    )
+    assert ~bound == base.BoundGreaterThan(
+        term=base.BoundReference[str](
+            field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+            accessor=Accessor(position=0, inner=None),
         ),
-        (
-            base.NotEqualTo(base.Reference("Bar"), literal(5)),
-            "table_schema_simple",
+        literal=StringLiteral("hello"),
+    )
+
+
+def test_not_equal_to_invert():
+    bound = base.NotEqualTo(
+        term=base.BoundReference(
+            field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+            accessor=Accessor(position=0, inner=None),
         ),
-        (
-            base.EqualTo(base.Reference("Bar"), literal(5)),
-            "table_schema_simple",
+        literal=StringLiteral("hello"),
+    )
+    assert ~bound == base.EqualTo(
+        term=base.BoundReference(
+            field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+            accessor=Accessor(position=0, inner=None),
         ),
-        (
-            base.GreaterThan(base.Reference("Bar"), literal(5)),
-            "table_schema_simple",
+        literal=StringLiteral("hello"),
+    )
+
+
+def test_greater_than_or_equal_invert():
+    bound = base.GreaterThanOrEqual(
+        term=base.BoundReference(
+            field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+            accessor=Accessor(position=0, inner=None),
         ),
-        (
-            base.LessThan(base.Reference("Bar"), literal(5)),
-            "table_schema_simple",
+        literal=StringLiteral("hello"),
+    )
+    assert ~bound == base.LessThan(
+        term=base.BoundReference(
+            field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+            accessor=Accessor(position=0, inner=None),
         ),
-        (
-            base.GreaterThanOrEqual(base.Reference("Bar"), literal(5)),
-            "table_schema_simple",
+        literal=StringLiteral("hello"),
+    )
+
+
+def test_less_than_or_equal_invert():
+    bound = base.LessThanOrEqual(
+        term=base.BoundReference(
+            field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+            accessor=Accessor(position=0, inner=None),
         ),
-        (
-            base.LessThanOrEqual(base.Reference("Bar"), literal(5)),
-            "table_schema_simple",
+        literal=StringLiteral("hello"),
+    )
+    assert ~bound == base.GreaterThan(
+        term=base.BoundReference(
+            field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+            accessor=Accessor(position=0, inner=None),
         ),
+        literal=StringLiteral("hello"),
+    )
+
+
+@pytest.mark.parametrize(
+    "pred",
+    [
+        base.NotIn(base.Reference("foo"), (literal("hello"), literal("world"))),
+        base.NotEqualTo(base.Reference("foo"), literal("hello")),
+        base.EqualTo(base.Reference("foo"), literal("hello")),
+        base.GreaterThan(base.Reference("foo"), literal("hello")),
+        base.LessThan(base.Reference("foo"), literal("hello")),
+        base.GreaterThanOrEqual(base.Reference("foo"), literal("hello")),
+        base.LessThanOrEqual(base.Reference("foo"), literal("hello")),
     ],
 )
-def test_bind_case_insensitive(a, schema, request):
-    schema = request.getfixturevalue(schema)
-    assert a.bind(schema, case_sensitive=False).term.field == schema.find_field(a.term.name, case_sensitive=False)
+def test_bind(pred, table_schema_simple: Schema):
+    assert pred.bind(table_schema_simple, case_sensitive=True).term.field == table_schema_simple.find_field(
+        pred.term.name, case_sensitive=True
+    )
+
+
+@pytest.mark.parametrize(
+    "pred",
+    [
+        base.In(base.Reference("Bar"), (literal(5), literal(2))),
+        base.NotIn(base.Reference("Bar"), (literal(5), literal(2))),
+        base.NotEqualTo(base.Reference("Bar"), literal(5)),
+        base.EqualTo(base.Reference("Bar"), literal(5)),
+        base.GreaterThan(base.Reference("Bar"), literal(5)),
+        base.LessThan(base.Reference("Bar"), literal(5)),
+        base.GreaterThanOrEqual(base.Reference("Bar"), literal(5)),
+        base.LessThanOrEqual(base.Reference("Bar"), literal(5)),
+    ],
+)
+def test_bind_case_insensitive(pred, table_schema_simple: Schema):
+    assert pred.bind(table_schema_simple, case_sensitive=False).term.field == table_schema_simple.find_field(
+        pred.term.name, case_sensitive=False
+    )
 
 
 @pytest.mark.parametrize(
@@ -526,15 +713,24 @@ def test_reduce(lhs, rhs):
     [
         (base.And(base.AlwaysTrue(), ExpressionB()), ExpressionB()),
         (base.And(base.AlwaysFalse(), ExpressionB()), base.AlwaysFalse()),
+        (base.And(ExpressionB(), base.AlwaysTrue()), ExpressionB()),
         (base.Or(base.AlwaysTrue(), ExpressionB()), base.AlwaysTrue()),
         (base.Or(base.AlwaysFalse(), ExpressionB()), ExpressionB()),
+        (base.Or(ExpressionA(), base.AlwaysFalse()), ExpressionA()),
         (base.Not(base.Not(ExpressionA())), ExpressionA()),
+        (base.Not(base.AlwaysTrue()), base.AlwaysFalse()),
+        (base.Not(base.AlwaysFalse()), base.AlwaysTrue()),
     ],
 )
 def test_base_AlwaysTrue_base_AlwaysFalse(lhs, rhs):
     assert lhs == rhs
 
 
+def test_invert_always():
+    assert ~base.AlwaysFalse() == base.AlwaysTrue()
+    assert ~base.AlwaysTrue() == base.AlwaysFalse()
+
+
 def test_accessor_base_class(foo_struct):
     """Test retrieving a value at a position of a container using an accessor"""
 
@@ -643,28 +839,46 @@ def test_boolean_expression_visit_raise_not_implemented_error():
     assert str(exc_info.value) == "Cannot visit unsupported expression: foo"
 
 
-def test_always_true_expression_binding(table_schema_simple):
+def test_bind_visitor_already_bound(table_schema_simple: Schema):
+    bound = base.BoundEqualTo(
+        base.BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)), literal("hello")
+    )
+    with pytest.raises(TypeError) as exc_info:
+        base.BindVisitor(base.visit(bound, visitor=base.BindVisitor(schema=table_schema_simple)))  # type: ignore
+    assert (
+        "Found already bound predicate: BoundEqualTo(term=BoundReference(field=NestedField(field_id=1, name='foo', field_type=StringType(), required=False), accessor=Accessor(position=0,inner=None)), literal=StringLiteral(hello))"
+        == str(exc_info.value)
+    )
+
+
+def test_visit_bound_visitor_unknown_predicate():
+    with pytest.raises(TypeError) as exc_info:
+        visit_bound_predicate({"something"}, FooBoundBooleanExpressionVisitor())
+    assert "Unknown predicate: {'something'}" == str(exc_info.value)
+
+
+def test_always_true_expression_binding(table_schema_simple: Schema):
     """Test that visiting an always-true expression returns always-true"""
     unbound_expression = base.AlwaysTrue()
     bound_expression = base.visit(unbound_expression, visitor=base.BindVisitor(schema=table_schema_simple))
     assert bound_expression == base.AlwaysTrue()
 
 
-def test_always_false_expression_binding(table_schema_simple):
+def test_always_false_expression_binding(table_schema_simple: Schema):
     """Test that visiting an always-false expression returns always-false"""
     unbound_expression = base.AlwaysFalse()
     bound_expression = base.visit(unbound_expression, visitor=base.BindVisitor(schema=table_schema_simple))
     assert bound_expression == base.AlwaysFalse()
 
 
-def test_always_false_and_always_true_expression_binding(table_schema_simple):
+def test_always_false_and_always_true_expression_binding(table_schema_simple: Schema):
     """Test that visiting both an always-true AND always-false expression returns always-false"""
     unbound_expression = base.And(base.AlwaysTrue(), base.AlwaysFalse())
     bound_expression = base.visit(unbound_expression, visitor=base.BindVisitor(schema=table_schema_simple))
     assert bound_expression == base.AlwaysFalse()
 
 
-def test_always_false_or_always_true_expression_binding(table_schema_simple):
+def test_always_false_or_always_true_expression_binding(table_schema_simple: Schema):
     """Test that visiting always-true OR always-false expression returns always-true"""
     unbound_expression = base.Or(base.AlwaysTrue(), base.AlwaysFalse())
     bound_expression = base.visit(unbound_expression, visitor=base.BindVisitor(schema=table_schema_simple))
@@ -1154,6 +1368,73 @@ def test_bound_boolean_expression_visitor_raise_on_unbound_predicate():
     assert "Not a bound predicate" in str(exc_info.value)
 
 
+def test_bound_predicate_invert():
+    with pytest.raises(NotImplementedError):
+        _ = ~base.BoundPredicate(
+            term=base.BoundReference(
+                field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+                accessor=Accessor(position=0, inner=None),
+            )
+        )
+
+
+def test_bound_unary_predicate_invert():
+    with pytest.raises(NotImplementedError):
+        _ = ~base.BoundUnaryPredicate(
+            term=base.BoundReference(
+                field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+                accessor=Accessor(position=0, inner=None),
+            )
+        )
+
+
+def test_bound_set_predicate_invert():
+    with pytest.raises(NotImplementedError):
+        _ = ~base.BoundSetPredicate(
+            term=base.BoundReference(
+                field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+                accessor=Accessor(position=0, inner=None),
+            ),
+            literals={literal("hello"), literal("world")},
+        )
+
+
+def test_bound_literal_predicate_invert():
+    with pytest.raises(NotImplementedError):
+        _ = ~base.BoundLiteralPredicate(
+            term=base.BoundReference(
+                field=NestedField(field_id=1, name="foo", field_type=StringType(), required=False),
+                accessor=Accessor(position=0, inner=None),
+            ),
+            literal=literal("world"),
+        )
+
+
+def test_unbound_predicate_invert():
+    with pytest.raises(NotImplementedError):
+        _ = ~base.UnboundPredicate(term=base.Reference("a"))
+
+
+def test_unbound_predicate_bind(table_schema_simple: Schema):
+    with pytest.raises(NotImplementedError):
+        _ = base.UnboundPredicate(term=base.Reference("a")).bind(table_schema_simple)
+
+
+def test_unbound_unary_predicate_invert():
+    with pytest.raises(NotImplementedError):
+        _ = ~base.UnaryPredicate(term=base.Reference("a"))
+
+
+def test_unbound_set_predicate_invert():
+    with pytest.raises(NotImplementedError):
+        _ = ~base.SetPredicate(term=base.Reference("a"), literals=(literal("hello"), literal("world")))
+
+
+def test_unbound_literal_predicate_invert():
+    with pytest.raises(NotImplementedError):
+        _ = ~base.LiteralPredicate(term=base.Reference("a"), literal=literal("hello"))
+
+
 def test_rewrite_not_equal_to():
     assert rewrite_not(base.Not(base.EqualTo(base.Reference("x"), literal(34.56)))) == base.NotEqualTo(
         base.Reference("x"), literal(34.56)