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/11/14 22:01:06 UTC

[iceberg] branch master updated: Python: Make invalid Literal conversions explicit (#6141)

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 2d1e100be5 Python: Make invalid Literal conversions explicit (#6141)
2d1e100be5 is described below

commit 2d1e100be5199d9aabdf886bc7c324d353afd2d9
Author: Fokko Driesprong <fo...@apache.org>
AuthorDate: Mon Nov 14 23:00:58 2022 +0100

    Python: Make invalid Literal conversions explicit (#6141)
    
    Currently we silently turn Literals into None if we can't convert
    them, instead I prefer to raise an exception. This can cause silent
    bugs like: EqualTo(Reference("id"), StringLiteral("123a")) will turn
    into a IsNull predicate since 123a cannot be casted to an Long
    (assuming that the `id` column is a Long).
---
 python/pyiceberg/expressions/literals.py  | 264 ++++++++++++++++++------------
 python/tests/expressions/test_literals.py | 134 ++++++++++-----
 2 files changed, 248 insertions(+), 150 deletions(-)

diff --git a/python/pyiceberg/expressions/literals.py b/python/pyiceberg/expressions/literals.py
index d38cd5f741..81d92a4448 100644
--- a/python/pyiceberg/expressions/literals.py
+++ b/python/pyiceberg/expressions/literals.py
@@ -27,8 +27,9 @@ from datetime import date
 from decimal import ROUND_HALF_UP, Decimal
 from functools import singledispatch, singledispatchmethod
 from typing import (
+    Any,
     Generic,
-    Optional,
+    Type,
     TypeVar,
     Union,
 )
@@ -42,6 +43,7 @@ from pyiceberg.types import (
     DoubleType,
     FixedType,
     FloatType,
+    IcebergType,
     IntegerType,
     LongType,
     StringType,
@@ -66,49 +68,50 @@ 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):
+    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
+        return self._value
 
+    @singledispatchmethod
     @abstractmethod
-    def to(self, type_var) -> Literal:
+    def to(self, type_var: IcebergType) -> Literal:
         ...  # pragma: no cover
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return f"{type(self).__name__}({self.value})"
 
-    def __str__(self):
+    def __str__(self) -> str:
         return str(self.value)
 
-    def __hash__(self):
+    def __hash__(self) -> int:
         return hash(self.value)
 
-    def __eq__(self, other):
+    def __eq__(self, other) -> bool:
         return self.value == other.value
 
-    def __ne__(self, other):
+    def __ne__(self, other) -> bool:
         return not self.__eq__(other)
 
-    def __lt__(self, other):
+    def __lt__(self, other) -> bool:
         return self.value < other.value
 
-    def __gt__(self, other):
+    def __gt__(self, other) -> bool:
         return self.value > other.value
 
-    def __le__(self, other):
+    def __le__(self, other) -> bool:
         return self.value <= other.value
 
-    def __ge__(self, other):
+    def __ge__(self, other) -> bool:
         return self.value >= other.value
 
 
 @singledispatch
-def literal(value) -> Literal:
+def literal(value: Any) -> Literal:
     """
     A generic Literal factory to construct an iceberg Literal based on python primitive data type
     using dynamic overloading
@@ -171,36 +174,60 @@ def _(value: date) -> Literal[int]:
     return DateLiteral(date_to_days(value))
 
 
-class AboveMax(Singleton):
-    @property
-    def value(self):
-        raise ValueError("AboveMax has no value")
+class FloatAboveMax(Literal[float], Singleton):
+    def __init__(self):
+        super().__init__(FloatType.max, float)
+
+    def to(self, type_var: IcebergType) -> Literal:  # type: ignore
+        raise TypeError("Cannot change the type of FloatAboveMax")
+
+    def __repr__(self) -> str:
+        return "FloatAboveMax()"
 
-    def to(self, type_var):
-        raise TypeError("Cannot change the type of AboveMax")
+    def __str__(self) -> str:
+        return "FloatAboveMax"
 
-    def __repr__(self):
-        return "AboveMax()"
 
-    def __str__(self):
-        return "AboveMax"
+class FloatBelowMin(Literal[float], Singleton):
+    def __init__(self):
+        super().__init__(FloatType.min, float)
+
+    def to(self, type_var: IcebergType) -> Literal:  # type: ignore
+        raise TypeError("Cannot change the type of FloatBelowMin")
+
+    def __repr__(self) -> str:
+        return "FloatBelowMin()"
 
+    def __str__(self) -> str:
+        return "FloatBelowMin"
 
-class BelowMin(Singleton):
+
+class IntAboveMax(Literal[int]):
     def __init__(self):
-        pass
+        super().__init__(IntegerType.max, int)
 
-    def value(self):
-        raise ValueError("BelowMin has no value")
+    def to(self, type_var: IcebergType) -> Literal:  # type: ignore
+        raise TypeError("Cannot change the type of IntAboveMax")
 
-    def to(self, type_var):
-        raise TypeError("Cannot change the type of BelowMin")
+    def __repr__(self) -> str:
+        return "IntAboveMax()"
 
-    def __repr__(self):
-        return "BelowMin()"
+    def __str__(self) -> str:
+        return "IntAboveMax"
 
-    def __str__(self):
-        return "BelowMin"
+
+class IntBelowMin(Literal[int]):
+    def __init__(self):
+        super().__init__(IntegerType.min, int)
+
+    def to(self, type_var: IcebergType) -> Literal:  # type: ignore
+        raise TypeError("Cannot change the type of IntBelowMin")
+
+    def __repr__(self) -> str:
+        return "IntBelowMin()"
+
+    def __str__(self) -> str:
+        return "IntBelowMin"
 
 
 class BooleanLiteral(Literal[bool]):
@@ -208,11 +235,11 @@ class BooleanLiteral(Literal[bool]):
         super().__init__(value, bool)
 
     @singledispatchmethod
-    def to(self, type_var):
-        return None
+    def to(self, type_var: IcebergType) -> Literal:
+        raise TypeError(f"Cannot convert BooleanLiteral into {type_var}")
 
     @to.register(BooleanType)
-    def _(self, type_var):
+    def _(self, _: BooleanType) -> Literal[bool]:
         return self
 
 
@@ -221,39 +248,39 @@ class LongLiteral(Literal[int]):
         super().__init__(value, int)
 
     @singledispatchmethod
-    def to(self, type_var):
-        return None
+    def to(self, type_var: IcebergType) -> Literal:
+        raise TypeError(f"Cannot convert LongLiteral into {type_var}")
 
     @to.register(LongType)
-    def _(self, type_var: LongType) -> Literal[int]:
+    def _(self, _: LongType) -> Literal[int]:
         return self
 
     @to.register(IntegerType)
-    def _(self, _: IntegerType) -> Union[AboveMax, BelowMin, Literal[int]]:
+    def _(self, _: IntegerType) -> Literal[int]:
         if IntegerType.max < self.value:
-            return AboveMax()
+            return IntAboveMax()
         elif IntegerType.min > self.value:
-            return BelowMin()
+            return IntBelowMin()
         return self
 
     @to.register(FloatType)
-    def _(self, type_var: FloatType) -> Literal[float]:
+    def _(self, _: FloatType) -> Literal[float]:
         return FloatLiteral(float(self.value))
 
     @to.register(DoubleType)
-    def _(self, type_var: DoubleType) -> Literal[float]:
+    def _(self, _: DoubleType) -> Literal[float]:
         return DoubleLiteral(float(self.value))
 
     @to.register(DateType)
-    def _(self, type_var: DateType) -> Literal[int]:
+    def _(self, _: DateType) -> Literal[int]:
         return DateLiteral(self.value)
 
     @to.register(TimeType)
-    def _(self, type_var: TimeType) -> Literal[int]:
+    def _(self, _: TimeType) -> Literal[int]:
         return TimeLiteral(self.value)
 
     @to.register(TimestampType)
-    def _(self, type_var: TimestampType) -> Literal[int]:
+    def _(self, _: TimestampType) -> Literal[int]:
         return TimestampLiteral(self.value)
 
     @to.register(DecimalType)
@@ -272,31 +299,31 @@ class FloatLiteral(Literal[float]):
         super().__init__(value, float)
         self._value32 = struct.unpack("<f", struct.pack("<f", value))[0]
 
-    def __eq__(self, other):
+    def __eq__(self, other) -> bool:
         return self._value32 == other
 
-    def __lt__(self, other):
+    def __lt__(self, other) -> bool:
         return self._value32 < other
 
-    def __gt__(self, other):
+    def __gt__(self, other) -> bool:
         return self._value32 > other
 
-    def __le__(self, other):
+    def __le__(self, other) -> bool:
         return self._value32 <= other
 
-    def __ge__(self, other):
+    def __ge__(self, other) -> bool:
         return self._value32 >= other
 
     @singledispatchmethod
-    def to(self, type_var):
-        return None
+    def to(self, type_var: IcebergType) -> Literal:
+        raise TypeError(f"Cannot convert FloatLiteral into {type_var}")
 
     @to.register(FloatType)
-    def _(self, type_var: FloatType) -> Literal[float]:
+    def _(self, _: FloatType) -> Literal[float]:
         return self
 
     @to.register(DoubleType)
-    def _(self, type_var: DoubleType) -> Literal[float]:
+    def _(self, _: DoubleType) -> Literal[float]:
         return DoubleLiteral(self.value)
 
     @to.register(DecimalType)
@@ -309,19 +336,19 @@ class DoubleLiteral(Literal[float]):
         super().__init__(value, float)
 
     @singledispatchmethod
-    def to(self, type_var):
-        return None
+    def to(self, type_var: IcebergType) -> Literal:
+        raise TypeError(f"Cannot convert DoubleLiteral into {type_var}")
 
     @to.register(DoubleType)
-    def _(self, type_var: DoubleType) -> Literal[float]:
+    def _(self, _: DoubleType) -> Literal[float]:
         return self
 
     @to.register(FloatType)
-    def _(self, _: FloatType) -> Union[AboveMax, BelowMin, Literal[float]]:
+    def _(self, _: FloatType) -> Union[FloatAboveMax, FloatBelowMin, FloatLiteral]:
         if FloatType.max < self.value:
-            return AboveMax()
+            return FloatAboveMax()
         elif FloatType.min > self.value:
-            return BelowMin()
+            return FloatBelowMin()
         return FloatLiteral(self.value)
 
     @to.register(DecimalType)
@@ -334,11 +361,11 @@ class DateLiteral(Literal[int]):
         super().__init__(value, int)
 
     @singledispatchmethod
-    def to(self, type_var):
-        return None
+    def to(self, type_var: IcebergType) -> Literal:
+        raise TypeError(f"Cannot convert DateLiteral into {type_var}")
 
     @to.register(DateType)
-    def _(self, type_var: DateType) -> Literal[int]:
+    def _(self, _: DateType) -> Literal[int]:
         return self
 
 
@@ -347,11 +374,11 @@ class TimeLiteral(Literal[int]):
         super().__init__(value, int)
 
     @singledispatchmethod
-    def to(self, type_var):
-        return None
+    def to(self, type_var: IcebergType) -> Literal:
+        raise TypeError(f"Cannot convert TimeLiteral into {type_var}")
 
     @to.register(TimeType)
-    def _(self, type_var: TimeType) -> Literal[int]:
+    def _(self, _: TimeType) -> Literal[int]:
         return self
 
 
@@ -360,15 +387,15 @@ class TimestampLiteral(Literal[int]):
         super().__init__(value, int)
 
     @singledispatchmethod
-    def to(self, type_var):
-        return None
+    def to(self, type_var: IcebergType) -> Literal:
+        raise TypeError(f"Cannot convert TimestampLiteral into {type_var}")
 
     @to.register(TimestampType)
-    def _(self, type_var: TimestampType) -> Literal[int]:
+    def _(self, _: TimestampType) -> Literal[int]:
         return self
 
     @to.register(DateType)
-    def _(self, type_var: DateType) -> Literal[int]:
+    def _(self, _: DateType) -> Literal[int]:
         return DateLiteral(micros_to_days(self.value))
 
 
@@ -377,14 +404,14 @@ class DecimalLiteral(Literal[Decimal]):
         super().__init__(value, Decimal)
 
     @singledispatchmethod
-    def to(self, type_var):
-        return None
+    def to(self, type_var: IcebergType) -> Literal:
+        raise TypeError(f"Cannot convert DecimalLiteral into {type_var}")
 
     @to.register(DecimalType)
-    def _(self, type_var: DecimalType) -> Optional[Literal[Decimal]]:
+    def _(self, type_var: DecimalType) -> Literal[Decimal]:
         if type_var.scale == abs(self.value.as_tuple().exponent):
             return self
-        return None
+        raise ValueError(f"Could not convert {self.value} into a {type_var}")
 
 
 class StringLiteral(Literal[str]):
@@ -392,52 +419,69 @@ class StringLiteral(Literal[str]):
         super().__init__(value, str)
 
     @singledispatchmethod
-    def to(self, type_var):
-        return None
+    def to(self, type_var: IcebergType) -> Literal:
+        raise TypeError(f"Cannot convert StringLiteral into {type_var}")
 
     @to.register(StringType)
-    def _(self, type_var: StringType) -> Literal[str]:
+    def _(self, _: StringType) -> Literal[str]:
         return self
 
+    @to.register(IntegerType)
+    def _(self, type_var: IntegerType) -> Union[IntAboveMax, IntBelowMin, LongLiteral]:
+        try:
+            number = int(float(self.value))
+
+            if IntegerType.max < number:
+                return IntAboveMax()
+            elif IntegerType.min > number:
+                return IntBelowMin()
+            return LongLiteral(number)
+        except ValueError as e:
+            raise ValueError(f"Could not convert {self.value} into a {type_var}") from e
+
+    @to.register(LongType)
+    def _(self, type_var: LongType) -> Literal[int]:
+        try:
+            return LongLiteral(int(float(self.value)))
+        except (TypeError, ValueError) as e:
+            raise ValueError(f"Could not convert {self.value} into a {type_var}") from e
+
     @to.register(DateType)
-    def _(self, type_var: DateType) -> Optional[Literal[int]]:
+    def _(self, type_var: DateType) -> Literal[int]:
         try:
             return DateLiteral(date_str_to_days(self.value))
-        except (TypeError, ValueError):
-            return None
+        except (TypeError, ValueError) as e:
+            raise ValueError(f"Could not convert {self.value} into a {type_var}") from e
 
     @to.register(TimeType)
-    def _(self, type_var: TimeType) -> Optional[Literal[int]]:
+    def _(self, type_var: TimeType) -> Literal[int]:
         try:
             return TimeLiteral(time_to_micros(self.value))
-        except (TypeError, ValueError):
-            return None
+        except (TypeError, ValueError) as e:
+            raise ValueError(f"Could not convert {self.value} into a {type_var}") from e
 
     @to.register(TimestampType)
-    def _(self, type_var: TimestampType) -> Optional[Literal[int]]:
+    def _(self, type_var: TimestampType) -> Literal[int]:
         try:
             return TimestampLiteral(timestamp_to_micros(self.value))
-        except (TypeError, ValueError):
-            return None
+        except (TypeError, ValueError) as e:
+            raise ValueError(f"Could not convert {self.value} into a {type_var}") from e
 
     @to.register(TimestamptzType)
-    def _(self, type_var: TimestamptzType) -> Optional[Literal[int]]:
-        try:
-            return TimestampLiteral(timestamptz_to_micros(self.value))
-        except (TypeError, ValueError):
-            return None
+    def _(self, _: TimestamptzType) -> Literal[int]:
+        return TimestampLiteral(timestamptz_to_micros(self.value))
 
     @to.register(UUIDType)
-    def _(self, type_var: UUIDType) -> Literal[UUID]:
+    def _(self, _: 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]:
         dec = Decimal(self.value)
         if type_var.scale == abs(dec.as_tuple().exponent):
             return DecimalLiteral(dec)
         else:
-            return None
+            raise ValueError(f"Could not convert {self.value} into a {type_var}")
 
 
 class UUIDLiteral(Literal[UUID]):
@@ -445,11 +489,11 @@ class UUIDLiteral(Literal[UUID]):
         super().__init__(value, UUID)
 
     @singledispatchmethod
-    def to(self, type_var):
-        return None
+    def to(self, type_var: IcebergType) -> Literal:
+        raise TypeError(f"Cannot convert UUIDLiteral into {type_var}")
 
     @to.register(UUIDType)
-    def _(self, type_var: UUIDType) -> Literal[UUID]:
+    def _(self, _: UUIDType) -> Literal[UUID]:
         return self
 
 
@@ -458,18 +502,18 @@ class FixedLiteral(Literal[bytes]):
         super().__init__(value, bytes)
 
     @singledispatchmethod
-    def to(self, type_var):
-        return None
+    def to(self, type_var: IcebergType) -> Literal:
+        raise TypeError(f"Cannot convert FixedLiteral into {type_var}")
 
     @to.register(FixedType)
-    def _(self, type_var: FixedType) -> Optional[Literal[bytes]]:
+    def _(self, type_var: FixedType) -> Literal[bytes]:
         if len(self.value) == len(type_var):
             return self
         else:
-            return None
+            raise ValueError(f"Could not convert {self.value!r} into a {type_var}")
 
     @to.register(BinaryType)
-    def _(self, type_var: BinaryType) -> Literal[bytes]:
+    def _(self, _: BinaryType) -> Literal[bytes]:
         return BinaryLiteral(self.value)
 
 
@@ -478,16 +522,18 @@ class BinaryLiteral(Literal[bytes]):
         super().__init__(value, bytes)
 
     @singledispatchmethod
-    def to(self, type_var):
-        return None
+    def to(self, type_var: IcebergType) -> Literal:
+        raise TypeError(f"Cannot convert BinaryLiteral into {type_var}")
 
     @to.register(BinaryType)
     def _(self, _: BinaryType) -> Literal[bytes]:
         return self
 
     @to.register(FixedType)
-    def _(self, type_var: FixedType) -> Optional[Literal[bytes]]:
+    def _(self, type_var: FixedType) -> Literal[bytes]:
         if len(type_var) == len(self.value):
             return FixedLiteral(self.value)
         else:
-            return None
+            raise TypeError(
+                f"Cannot convert BinaryLiteral into {type_var}, different length: {len(type_var)} <> {len(self.value)}"
+            )
diff --git a/python/tests/expressions/test_literals.py b/python/tests/expressions/test_literals.py
index f34c8ae683..dee19359e6 100644
--- a/python/tests/expressions/test_literals.py
+++ b/python/tests/expressions/test_literals.py
@@ -14,6 +14,8 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+# pylint:disable=eval-used
+
 import datetime
 import uuid
 from decimal import Decimal
@@ -21,15 +23,17 @@ from decimal import Decimal
 import pytest
 
 from pyiceberg.expressions.literals import (
-    AboveMax,
-    BelowMin,
     BinaryLiteral,
     BooleanLiteral,
     DateLiteral,
     DecimalLiteral,
     DoubleLiteral,
     FixedLiteral,
+    FloatAboveMax,
+    FloatBelowMin,
     FloatLiteral,
+    IntAboveMax,
+    IntBelowMin,
     LongLiteral,
     StringLiteral,
     TimeLiteral,
@@ -53,8 +57,6 @@ from pyiceberg.types import (
     UUIDType,
 )
 
-# Base
-
 
 def test_literal_from_none_error():
     with pytest.raises(TypeError) as e:
@@ -147,11 +149,11 @@ def test_long_to_integer_within_bound():
 def test_long_to_integer_outside_bound():
     big_lit = literal(IntegerType.max + 1).to(LongType())
     above_max_lit = big_lit.to(IntegerType())
-    assert above_max_lit == AboveMax()
+    assert above_max_lit == IntAboveMax()
 
     small_lit = literal(IntegerType.min - 1).to(LongType())
     below_min_lit = small_lit.to(IntegerType())
-    assert below_min_lit == BelowMin()
+    assert below_min_lit == IntBelowMin()
 
 
 def test_long_to_float_conversion():
@@ -218,11 +220,11 @@ def test_double_to_float_within_bound():
 def test_double_to_float_outside_bound():
     big_lit = literal(FloatType.max + 1.0e37).to(DoubleType())
     above_max_lit = big_lit.to(FloatType())
-    assert above_max_lit == AboveMax()
+    assert above_max_lit == FloatAboveMax()
 
     small_lit = literal(FloatType.min - 1.0e37).to(DoubleType())
     below_min_lit = small_lit.to(FloatType())
-    assert below_min_lit == BelowMin()
+    assert below_min_lit == FloatBelowMin()
 
 
 @pytest.mark.parametrize(
@@ -239,9 +241,15 @@ def test_decimal_to_decimal_conversion():
 
     assert lit.value.as_tuple() == lit.to(DecimalType(9, 2)).value.as_tuple()
     assert lit.value.as_tuple() == lit.to(DecimalType(11, 2)).value.as_tuple()
-    assert lit.to(DecimalType(9, 0)) is None
-    assert lit.to(DecimalType(9, 1)) is None
-    assert lit.to(DecimalType(9, 3)) is None
+    with pytest.raises(ValueError) as e:
+        _ = lit.to(DecimalType(9, 0))
+    assert "Could not convert 34.11 into a decimal(9, 0)" in str(e.value)
+    with pytest.raises(ValueError) as e:
+        _ = lit.to(DecimalType(9, 1))
+    assert "Could not convert 34.11 into a decimal(9, 1)" in str(e.value)
+    with pytest.raises(ValueError) as e:
+        _ = lit.to(DecimalType(9, 3))
+    assert "Could not convert 34.11 into a decimal(9, 3)" in str(e.value)
 
 
 def test_timestamp_to_date():
@@ -251,15 +259,15 @@ def test_timestamp_to_date():
     assert date_lit.value == 0
 
 
-# STRING
-
-
 def test_string_literal():
     sqrt2 = literal("1.414").to(StringType())
     pi = literal("3.141").to(StringType())
     pi_string_lit = StringLiteral("3.141")
     pi_double_lit = literal(3.141).to(DoubleType())
 
+    assert literal("3.141").to(IntegerType()) == literal(3)
+    assert literal("3.141").to(LongType()) == literal(3)
+
     assert sqrt2 != pi
     assert pi != pi_double_lit
     assert pi == pi_string_lit
@@ -312,12 +320,16 @@ def test_string_to_timestamp_literal():
 
 def test_timestamp_with_zone_without_zone_in_literal():
     timestamp_str = literal("2017-08-18T14:21:01.919234")
-    assert timestamp_str.to(TimestamptzType()) is None
+    with pytest.raises(ValueError) as e:
+        _ = timestamp_str.to(timestamp_str.to(TimestamptzType()))
+    assert "Invalid timestamp with zone: 2017-08-18T14:21:01.919234 (must be ISO-8601)" in str(e.value)
 
 
 def test_timestamp_without_zone_with_zone_in_literal():
     timestamp_str = literal("2017-08-18T14:21:01.919234+07:00")
-    assert timestamp_str.to(TimestampType()) is None
+    with pytest.raises(ValueError) as e:
+        _ = timestamp_str.to(TimestampType())
+    assert "Could not convert 2017-08-18T14:21:01.919234+07:00 into a timestamp" in str(e.value)
 
 
 def test_string_to_uuid_literal():
@@ -431,12 +443,18 @@ def test_binary_to_fixed():
     fixed_lit = lit.to(FixedType(3))
     assert fixed_lit is not None
     assert lit.value == fixed_lit.value
-    assert lit.to(FixedType(4)) is None
+
+    with pytest.raises(TypeError) as e:
+        _ = lit.to(FixedType(4))
+    assert "Cannot convert BinaryLiteral into fixed[4], different length: 4 <> 3" in str(e.value)
 
 
 def test_binary_to_smaller_fixed_none():
     lit = literal(bytearray([0x00, 0x01, 0x02]))
-    assert lit.to(FixedType(2)) is None
+
+    with pytest.raises(TypeError) as e:
+        _ = lit.to(FixedType(2))
+    assert "Cannot convert BinaryLiteral into fixed[2], different length: 2 <> 3" in str(e.value)
 
 
 def test_fixed_to_binary():
@@ -448,35 +466,61 @@ def test_fixed_to_binary():
 
 def test_fixed_to_smaller_fixed_none():
     lit = literal(bytearray([0x00, 0x01, 0x02])).to(FixedType(3))
-    assert lit.to(FixedType(2)) is None
+    with pytest.raises(ValueError) as e:
+        lit.to(lit.to(FixedType(2)))
+    assert "Could not convert b'\\x00\\x01\\x02' into a fixed[2]" in str(e.value)
 
 
-def test_above_max():
-    a = AboveMax()
+def test_above_max_float():
+    a = FloatAboveMax()
     # singleton
-    assert a == AboveMax()
-    assert str(a) == "AboveMax"
-    assert repr(a) == "AboveMax()"
-    with pytest.raises(ValueError) as e:
-        a.value()
-    assert "AboveMax has no value" in str(e.value)
+    assert a == FloatAboveMax()
+    assert str(a) == "FloatAboveMax"
+    assert repr(a) == "FloatAboveMax()"
+    assert a.value == FloatType.max
+    assert a == eval(repr(a))
     with pytest.raises(TypeError) as e:
         a.to(IntegerType())
-    assert "Cannot change the type of AboveMax" in str(e.value)
+    assert "Cannot change the type of FloatAboveMax" in str(e.value)
 
 
-def test_below_min():
-    b = BelowMin()
+def test_below_min_float():
+    b = FloatBelowMin()
     # singleton
-    assert b == BelowMin()
-    assert str(b) == "BelowMin"
-    assert repr(b) == "BelowMin()"
-    with pytest.raises(ValueError) as e:
-        b.value()
-    assert "BelowMin has no value" in str(e.value)
+    assert b == FloatBelowMin()
+    assert str(b) == "FloatBelowMin"
+    assert repr(b) == "FloatBelowMin()"
+    assert b == eval(repr(b))
+    assert b.value == FloatType.min
     with pytest.raises(TypeError) as e:
         b.to(IntegerType())
-    assert "Cannot change the type of BelowMin" in str(e.value)
+    assert "Cannot change the type of FloatBelowMin" in str(e.value)
+
+
+def test_above_max_int():
+    a = IntAboveMax()
+    # singleton
+    assert a == IntAboveMax()
+    assert str(a) == "IntAboveMax"
+    assert repr(a) == "IntAboveMax()"
+    assert a.value == IntegerType.max
+    assert a == eval(repr(a))
+    with pytest.raises(TypeError) as e:
+        a.to(IntegerType())
+    assert "Cannot change the type of IntAboveMax" in str(e.value)
+
+
+def test_below_min_int():
+    b = IntBelowMin()
+    # singleton
+    assert b == IntBelowMin()
+    assert str(b) == "IntBelowMin"
+    assert repr(b) == "IntBelowMin()"
+    assert b == eval(repr(b))
+    assert b.value == IntegerType.min
+    with pytest.raises(TypeError) as e:
+        b.to(IntegerType())
+    assert "Cannot change the type of IntBelowMin" in str(e.value)
 
 
 def test_invalid_boolean_conversions():
@@ -531,7 +575,8 @@ def test_invalid_long_conversions():
     ],
 )
 def test_invalid_float_conversions(lit, test_type):
-    assert lit.to(test_type) is None
+    with pytest.raises(TypeError):
+        _ = lit.to(test_type)
 
 
 @pytest.mark.parametrize("lit", [literal("2017-08-18").to(DateType())])
@@ -597,6 +642,13 @@ def test_invalid_timestamp_conversions():
     )
 
 
+def test_invalid_decimal_conversion_scale():
+    lit = literal(Decimal("34.11"))
+    with pytest.raises(ValueError) as e:
+        lit.to(DecimalType(9, 4))
+    assert "Could not convert 34.11 into a decimal(9, 4)" in str(e.value)
+
+
 def test_invalid_decimal_conversions():
     assert_invalid_conversions(
         literal(Decimal("34.11")),
@@ -610,7 +662,6 @@ def test_invalid_decimal_conversions():
             TimeType(),
             TimestampType(),
             TimestamptzType(),
-            DecimalType(9, 4),
             StringType(),
             UUIDType(),
             FixedType(1),
@@ -622,7 +673,7 @@ def test_invalid_decimal_conversions():
 def test_invalid_string_conversions():
     assert_invalid_conversions(
         literal("abc"),
-        [BooleanType(), IntegerType(), LongType(), FloatType(), DoubleType(), FixedType(1), BinaryType()],
+        [BooleanType(), FloatType(), DoubleType(), FixedType(1), BinaryType()],
     )
 
 
@@ -689,4 +740,5 @@ def test_invalid_binary_conversions():
 
 def assert_invalid_conversions(lit, types=None):
     for type_var in types:
-        assert lit.to(type_var) is None
+        with pytest.raises(TypeError):
+            _ = lit.to(type_var)