fix: reject DecimalType with precision outside [1, 38]#3584
fix: reject DecimalType with precision outside [1, 38]#3584gitcommit90 wants to merge 2 commits into
Conversation
The Iceberg spec states precision must be 38 or less [1]. The Python implementation silently accepted any precision, including values like decimal(39, 0) that cannot be represented in decimal fixed-byte storage. This aligns the Python library with the Java reference implementation which raises IllegalArgumentException for precision > 38. Changes: - Add a validation check in DecimalType.__init__ that raises ValidationError when precision < 1 or precision > 38 - Add four tests covering: precision=39 (direct), precision=0 (direct), precision=38 (boundary, must pass), and precision=39 via deserialization Fixes apache#3583 [1] https://iceberg.apache.org/spec/#primitive-types
|
|
||
| def test_decimal_deserialization_precision_above_38_raises() -> None: | ||
| """Deserialization of decimal(39, 0) must also raise a ValidationError.""" | ||
| with pytest.raises(Exception, match="Precision must be between 1 and 38"): |
There was a problem hiding this comment.
| with pytest.raises(Exception, match="Precision must be between 1 and 38"): | |
| with pytest.raises(ValidationError, match="Precision must be between 1 and 38"): |
|
|
||
| def __init__(self, precision: int, scale: int) -> None: | ||
| if not (1 <= precision <= 38): | ||
| raise ValidationError(f"Precision must be between 1 and 38 (inclusive), got: {precision}") |
There was a problem hiding this comment.
model_copy still misses this guard. The following code doesn't raise the exception:
DecimalType(10, 2).model_copy(update={"root": (39, 0)})Could you consider using @model_validator(mode="after")?
There was a problem hiding this comment.
+1. This is how we do validations elsewhere in the code as well.
| """Precision 0 is not valid; minimum precision is 1.""" | ||
| with pytest.raises(ValidationError, match="Precision must be between 1 and 38"): | ||
| DecimalType(0, 0) |
There was a problem hiding this comment.
nit: We could remove the code comment. It's obvious when reading the code.
rambleraptor
left a comment
There was a problem hiding this comment.
This is basically there. Thanks for doing that
| options=self.options, | ||
| ).plan_files( | ||
| manifests=manifests, | ||
| manifest_entry_filter=lambda manifest_entry: manifest_entry.snapshot_id in append_snapshot_ids |
There was a problem hiding this comment.
This looks stray. Can you remove this?
|
|
||
| def __init__(self, precision: int, scale: int) -> None: | ||
| if not (1 <= precision <= 38): | ||
| raise ValidationError(f"Precision must be between 1 and 38 (inclusive), got: {precision}") |
There was a problem hiding this comment.
+1. This is how we do validations elsewhere in the code as well.
|
@gitcommit90 are you able to make these quick changes? I'd love to get this in before 0.12 |
The Iceberg spec states precision must be 38 or less (minimum 1). The Python library silently accepted any precision value;
DecimalType(39, 0)succeeded with no error, which can corrupt downstream fixed-byte decimal encoding.The Java reference implementation rejects it:
DecimalType.of(39, 0)raisesIllegalArgumentException.Fix: Add a 2-line guard in
DecimalType.__init__that raisespyiceberg.exceptions.ValidationErrorwhenprecision < 1orprecision > 38. Uses the same exception class and pattern already used throughoutpyiceberg/types.py.Tests added (4 new):
test_decimal_precision_above_38_raises— precision=39 raisestest_decimal_precision_zero_raises— precision=0 raisestest_decimal_precision_boundary_38_accepted— precision=38 acceptedtest_decimal_deserialization_precision_above_38_raises— deserialization path also raisesAll 292 existing tests pass; ruff clean.
closes #3583