| _source_type: IcebergType=PrivateAttr() |
| _width: PositiveInt=PrivateAttr() |
| |
| def__init__(self, width: int, **data: Any): |
| super().__init__(root=f"truncate[{width}]", **data) |
| self._width=width |
| |
| defcan_transform(self, source: IcebergType) ->bool: |
| returnisinstance(source, (IntegerType, LongType, StringType, BinaryType, DecimalType)) |
| |
| defresult_type(self, source: IcebergType) ->IcebergType: |
| returnsource |
| |
| @property |
| defpreserves_order(self) ->bool: |
| returnTrue |
| |
| @property |
| defsource_type(self) ->IcebergType: |
| returnself._source_type |
| |
| defproject(self, name: str, pred: BoundPredicate) ->UnboundPredicate|None: |
| field_type=pred.term.ref().field.field_type |
| |
| ifisinstance(pred.term, BoundTransform): |
| return_project_transform_predicate(self, name, pred) |
| |
| ifisinstance(pred, BoundUnaryPredicate): |
| returnpred.as_unbound(Reference(name)) |
| elifisinstance(pred, BoundIn): |
| return_set_apply_transform(name, pred, self.transform(field_type)) |
| elifisinstance(field_type, (IntegerType, LongType, DecimalType)): # type: ignore |
| ifisinstance(pred, BoundLiteralPredicate): |
| return_truncate_number(name, pred, self.transform(field_type)) |
| elifisinstance(field_type, (BinaryType, StringType)): |
| ifisinstance(pred, BoundLiteralPredicate): |
| ifisinstance(pred, BoundNotStartsWith): |
| literal_width=len(pred.literal.value) |
| ifliteral_width<self.width: |
| returnpred.as_unbound(name, pred.literal.value) |
| elifliteral_width==self.width: |
| returnNotEqualTo(name, pred.literal.value) |
| else: |
| returnNone |
| else: |
| return_truncate_array(name, pred, self.transform(field_type)) |
| |
| defstrict_project(self, name: str, pred: BoundPredicate) ->UnboundPredicate|None: |
| field_type=pred.term.ref().field.field_type |
| |
| ifisinstance(pred.term, BoundTransform): |
| return_project_transform_predicate(self, name, pred) |
| |
| ifisinstance(pred, BoundUnaryPredicate): |
| returnpred.as_unbound(Reference(name)) |
| |
| ifisinstance(field_type, (IntegerType, LongType, DecimalType)): |
| ifisinstance(pred, BoundLiteralPredicate): |
| return_truncate_number_strict(name, pred, self.transform(field_type)) |
| elifisinstance(pred, BoundNotIn): |
| return_set_apply_transform(name, pred, self.transform(field_type)) |
| else: |
| returnNone# type: ignore |
| |
| ifisinstance(pred, BoundLiteralPredicate): |
| ifisinstance(pred, BoundStartsWith): |
| literal_width=len(pred.literal.value) |
| ifliteral_width<self.width: |
| returnpred.as_unbound(name, pred.literal.value) |
| elifliteral_width==self.width: |
| returnEqualTo(name, pred.literal.value) |
| else: |
| returnNone |
| elifisinstance(pred, BoundNotStartsWith): |
| literal_width=len(pred.literal.value) |
| ifliteral_width<self.width: |
| returnpred.as_unbound(name, pred.literal.value) |
| elifliteral_width==self.width: |
| returnNotEqualTo(name, pred.literal.value) |
| else: |
| returnpred.as_unbound(name, self.transform(field_type)(pred.literal.value)) |
| else: |
| # ProjectionUtil.truncateArrayStrict(name, pred, this); |
| return_truncate_array_strict(name, pred, self.transform(field_type)) |
| elifisinstance(pred, BoundNotIn): |
| return_set_apply_transform(name, pred, self.transform(field_type)) |
| else: |
| returnNone# type: ignore |
| |
| @property |
| defwidth(self) ->int: |
| returnself._width |
| |
| deftransform(self, source: IcebergType) ->Callable[[S|None], S|None]: |
| ifisinstance(source, (IntegerType, LongType)): |
| |
| deftruncate_func(v: Any) ->Any: |
| returnv-v%self._width |
| |
| elifisinstance(source, (StringType, BinaryType)): |
| |
| deftruncate_func(v: Any) ->Any: |
| returnv[0 : min(self._width, len(v))] |
| |
| elifisinstance(source, DecimalType): |
| |
| deftruncate_func(v: Any) ->Any: |
| returntruncate_decimal(v, self._width) |
| |
| else: |
| raiseValueError(f"Cannot truncate for type: {source}") |
| |
| returnlambdav: truncate_func(v) ifvisnotNoneelseNone |
| |
| defsatisfies_order_of(self, other: Transform[S, T]) ->bool: |
| ifself==other: |
| returnTrue |
| elif ( |
| isinstance(self.source_type, StringType) |
| andisinstance(other, TruncateTransform) |
| andisinstance(other.source_type, StringType) |
| ): |
| returnself.width>=other.width |
Apache Iceberg version
0.11.0 (latest release)
Please describe the bug 🐞
Description
Calling
TruncateTransform.satisfies_order_ofwith two valid truncate transforms that have different widths raises anAttributeErrorinstead of returning a boolean.Same-width comparisons work because the method returns early when the transforms are equal.
Reproduction
On
mainat commit48e710d20ceeeaa637d5aeae7746b787410859f8, this raises:The failure reproduces consistently. The same code is also present in the 0.11.1 release.
Expected behavior
The method should compare the truncate widths and return a boolean:
This matches the current Apache Iceberg Java implementation:
https://github.com/apache/iceberg/blob/25654ab4b29c8b5b5c20fc427da01cb70d94ed14/api/src/main/java/org/apache/iceberg/transforms/Truncate.java#L130-L141
Cause
TruncateTransform.__init__initializes_widthbut not_source_type. However,satisfies_order_ofstill accesses thesource_typeproperty backed by_source_type:iceberg-python/pyiceberg/transforms.py
Lines 783 to 905 in 48e710d
The existing unit test only compares a transform with itself, so it returns before reaching the failing branch:
iceberg-python/tests/test_transforms.py
Lines 501 to 512 in 48e710d
A focused fix could compare
TruncateTransformwidths directly, consistent with the Java implementation, and add regression cases for different widths.I would be happy to contribute the fix and regression tests.
Willingness to contribute