-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Python: Add more tests for the Avro writer #8067
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
8cc9578
2c56348
8a8727c
6cfaa43
19aa56d
5b43ea9
6ba0761
1052661
3fba6bd
a9b5dea
da66c46
0a04169
4847294
2829d4b
6c645ff
f20c70f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,7 +28,7 @@ | |
| from abc import abstractmethod | ||
| from dataclasses import dataclass | ||
| from dataclasses import field as dataclassfield | ||
| from datetime import datetime, time | ||
| from datetime import date, datetime, time | ||
| from decimal import Decimal | ||
| from typing import ( | ||
| Any, | ||
|
|
@@ -43,6 +43,7 @@ | |
| from pyiceberg.avro.decoder import BinaryDecoder | ||
| from pyiceberg.typedef import StructProtocol | ||
| from pyiceberg.types import StructType | ||
| from pyiceberg.utils.datetime import days_to_date | ||
| from pyiceberg.utils.singleton import Singleton | ||
|
|
||
|
|
||
|
|
@@ -153,8 +154,8 @@ def skip(self, decoder: BinaryDecoder) -> None: | |
|
|
||
|
|
||
| class DateReader(Reader): | ||
| def read(self, decoder: BinaryDecoder) -> int: | ||
| return decoder.read_int() | ||
| def read(self, decoder: BinaryDecoder) -> date: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this should produce a
We do not want to use native date/time representations internally because they require a lot of extra logic to compare and work with in other ways.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I reverted this, and also fixed this for the |
||
| return days_to_date(decoder.read_int()) | ||
|
|
||
| def skip(self, decoder: BinaryDecoder) -> None: | ||
| decoder.skip_int() | ||
|
|
@@ -194,7 +195,7 @@ def skip(self, decoder: BinaryDecoder) -> None: | |
|
|
||
| class UUIDReader(Reader): | ||
| def read(self, decoder: BinaryDecoder) -> UUID: | ||
| return decoder.read_uuid_from_fixed() | ||
| return UUID(bytes=decoder.read(16)) | ||
|
|
||
| def skip(self, decoder: BinaryDecoder) -> None: | ||
| decoder.skip(16) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ | |
|
|
||
| from pyiceberg.avro.encoder import BinaryEncoder | ||
| from pyiceberg.types import StructType | ||
| from pyiceberg.utils.datetime import date_to_days, datetime_to_micros | ||
| from pyiceberg.utils.singleton import Singleton | ||
|
|
||
|
|
||
|
|
@@ -78,22 +79,25 @@ def write(self, encoder: BinaryEncoder, val: float) -> None: | |
|
|
||
| class DateWriter(Writer): | ||
| def write(self, encoder: BinaryEncoder, val: Any) -> None: | ||
| encoder.write_date_int(val) | ||
| encoder.write_int(date_to_days(val)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here as well, the encoders and decoders should not use datetime objects.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is correct, right? The encoders only work with physical types. The
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's fine to have a writer that work with date, but right now we need one that works with |
||
|
|
||
|
|
||
| class TimeWriter(Writer): | ||
| def write(self, encoder: BinaryEncoder, val: time) -> None: | ||
| encoder.write_time_micros_long(val) | ||
| encoder.write_time_micros(val) | ||
|
|
||
|
|
||
| class TimestampWriter(Writer): | ||
| def write(self, encoder: BinaryEncoder, val: datetime) -> None: | ||
| encoder.write_timestamp_micros_long(val) | ||
| if val.tzinfo is not None: | ||
| raise ValueError(f"Timestamp should not have a timezone, but has: {val.tzinfo}") | ||
|
|
||
| encoder.write_int(datetime_to_micros(val)) | ||
|
|
||
|
|
||
| class TimestamptzWriter(Writer): | ||
| def write(self, encoder: BinaryEncoder, val: datetime) -> None: | ||
| encoder.write_timestamp_micros_long(val) | ||
| encoder.write_int(datetime_to_micros(val)) | ||
|
|
||
|
|
||
| class StringWriter(Writer): | ||
|
|
@@ -103,19 +107,18 @@ def write(self, encoder: BinaryEncoder, val: Any) -> None: | |
|
|
||
| class UUIDWriter(Writer): | ||
| def write(self, encoder: BinaryEncoder, val: UUID) -> None: | ||
| uuid_bytes = val.bytes | ||
|
|
||
| if len(uuid_bytes) != 16: | ||
| raise ValueError(f"Expected UUID to be 16 bytes, got: {len(uuid_bytes)}") | ||
|
|
||
| encoder.write_bytes_fixed(uuid_bytes) | ||
| if len(val.bytes) != 16: | ||
| raise ValueError(f"Expected UUID to be 16 bytes, got: {len(val.bytes)}") | ||
|
Fokko marked this conversation as resolved.
Outdated
|
||
| encoder.write(val.bytes) | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class FixedWriter(Writer): | ||
| _len: int = dataclassfield() | ||
|
|
||
| def write(self, encoder: BinaryEncoder, val: bytes) -> None: | ||
| if len(val) != self._len: | ||
| raise ValueError(f"Expected {self._len} bytes, got {len(val)}") | ||
| encoder.write(val) | ||
|
|
||
| def __len__(self) -> int: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,11 +64,11 @@ | |
|
|
||
| LOGICAL_FIELD_TYPE_MAPPING: Dict[Tuple[str, str], PrimitiveType] = { | ||
| ("date", "int"): DateType(), | ||
| ("time-millis", "int"): TimeType(), | ||
| ("time-micros", "long"): TimeType(), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also want to be able to read
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't part of the Iceberg spec, I'd rather leave it out unless you have strong concerns here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine either way. Eventually, we'll need to read files that were written for Hive and imported into an Iceberg table, so we are generally more permissive with reads. Since this is mostly about Iceberg metadata right now, we don't need to worry about it. |
||
| ("timestamp-millis", "long"): TimestampType(), | ||
| ("time-micros", "int"): TimeType(), | ||
| ("timestamp-micros", "long"): TimestampType(), | ||
| ("uuid", "fixed"): UUIDType(), | ||
| ("uuid", "string"): UUIDType(), | ||
| } | ||
|
|
||
| AvroType = Union[str, Any] | ||
|
|
@@ -369,6 +369,11 @@ def _convert_logical_type(self, avro_logical_type: Dict[str, Any]) -> IcebergTyp | |
| return self._convert_logical_decimal_type(avro_logical_type) | ||
| elif logical_type == "map": | ||
| return self._convert_logical_map_type(avro_logical_type) | ||
| elif logical_type == "timestamp-micros": | ||
| if avro_logical_type.get("adjust-to-utc", False) is True: | ||
| return TimestamptzType() | ||
| else: | ||
| return TimestampType() | ||
| elif (logical_type, physical_type) in LOGICAL_FIELD_TYPE_MAPPING: | ||
| return LOGICAL_FIELD_TYPE_MAPPING[(logical_type, physical_type)] | ||
| else: | ||
|
|
@@ -542,6 +547,8 @@ def map(self, map_type: MapType, key_result: AvroType, value_result: AvroType) - | |
| return { | ||
| "type": "map", | ||
| "values": value_result, | ||
| "key-id": self.last_map_key_field_id, | ||
| "value-id": self.last_map_value_field_id, | ||
| } | ||
| else: | ||
| # Creates a logical map that's a list of schema's | ||
|
|
@@ -592,13 +599,13 @@ def visit_timestamp(self, timestamp_type: TimestampType) -> AvroType: | |
|
|
||
| def visit_timestamptz(self, timestamptz_type: TimestamptzType) -> AvroType: | ||
| # Iceberg only supports micro's | ||
| return {"type": "long", "logicalType": "timestamp-micros"} | ||
| return {"type": "long", "logicalType": "timestamp-micros", "adjust-to-utc": True} | ||
|
Fokko marked this conversation as resolved.
|
||
|
|
||
| def visit_string(self, string_type: StringType) -> AvroType: | ||
| return "string" | ||
|
|
||
| def visit_uuid(self, uuid_type: UUIDType) -> AvroType: | ||
| return {"type": "string", "logicalType": "uuid"} | ||
| return {"type": "fixed", "size": "16", "logicalType": "uuid"} | ||
|
|
||
| def visit_binary(self, binary_type: BinaryType) -> AvroType: | ||
| return "bytes" | ||
Uh oh!
There was an error while loading. Please reload this page.