Uh oh!
There was an error while loading. Please reload this page.
Data, Parquet: Fix UUID ClassCastException when reading Parquet files with UUIDs - #14027
Conversation
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
ndrluis
commented
Sep 9, 2025
Thank you @huaxingao for the review, I made the requested changes. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| } | ||
| static Function<Object, Object> converterFromParquet(PrimitiveType type) { | ||
| if (type.getLogicalTypeAnnotation() instanceof UUIDLogicalTypeAnnotation) { |
There was a problem hiding this comment.
This fix seems OK to me, but the part I don't quite understand yet (I haven't dug into it yet) is this issue PyIceberg specific? What's different about for instance the dictionaries with UUID produced by Spark and why doesn't that fail?
There was a problem hiding this comment.
cc @Fokko may have some insights too here since I know he was working on some UUID related fixes in the past.
There was a problem hiding this comment.
This change does not solve all the problems. I'm doing some experiments here, playing around with pyiceberg and Spark, so I discovered some other things that I'm double-checking. I intend to add a more detailed analysis here over the weekend.
ndrluis
commented
Sep 14, 2025
Quick update on this issue - I'm going to focus on solving this problem on the Java side first. Once Iceberg Java has the correct behavior, I'll come back to PyIceberg and make the necessary adjustments. So here's the minimal test that I'm running using PySpark (since I have more familiarity with it than the Java environment). Tested with the following Iceberg Runtimes: Test Case @pytest.mark.integrationdeftest_uuid_write_read_with_pyspark(session_catalog: Catalog, spark: SparkSession) ->None:
identifier="default.test_uuid_write_and_read_with_pyspark"catalog=load_catalog("default", type="in-memory")
catalog.create_namespace("ns")
schema=Schema(NestedField(field_id=1, name="uuid_col", field_type=UUIDType(), required=False))
try:
session_catalog.drop_table(identifier=identifier)
exceptNoSuchTableError:
passtable=_create_table(session_catalog, identifier, {"format-version": "2"}, schema=schema)
spark.sql(
f""" INSERT INTO {identifier} VALUES ("22222222-2222-2222-2222-222222222222") """
)
df=spark.table(identifier)
assertdf.count() ==1result=df.where("uuid_col = '22222222-2222-2222-2222-222222222222'")
assertresult.count() ==1Error |
ndrluis
commented
Sep 15, 2025
@huaxingao@amogh-jahagirdar@Fokko with my latest commit, I was able to fix both cases. Since PyArrow (the version used by PyIceberg) does not add information about the logical type annotation, and since we are reverting back to using binary(16) in the visitor to represent the type on the PyIceberg side, we will only have this information when PyArrow has full support for UUID. Therefore, it's safer for us to verify the Iceberg type instead of the Parquet logical type annotation. I have already tested the scenario of writing with PyIceberg using binary(16) and reading with this branch. |
Uh oh!
There was an error while loading. Please reload this page.
| } else if (icebergType.typeId() == Type.TypeID.UUID) { | ||
| return binary -> UUIDUtil.convert(((Binary) binary).toByteBuffer()); |
There was a problem hiding this comment.
This seems like an odd place to apply this conversion since the rows above are more about schema evolution. However, looking at it a bit closer, I think it makes sense. Other logical types, such as TimestampLiteral store the primitive type internally (long), while the UUIDLiteral keeps a UUID rather than bytes.
This will just compare the bytes using an unsigned lexicographical binary comparator.
shangxinli
commented
Sep 24, 2025
The schema defines the column as optional. Can you null UUID value tests? |
249d25d to
9c70716Comparejava.util.UUID cannot be cast to class java.nio.ByteBuffer
ndrluis
commented
Sep 24, 2025
@Fokko@shangxinli I made the suggested changes |
| record.setField("_struct_not_null", structNotNull); // struct with int | ||
| record.setField( | ||
| "_uuid_col", (i % 3 == 0) ? UUID_WITH_ZEROS : (i % 3 == 1) ? UUID_WITH_ONES : null); |
There was a problem hiding this comment.
minor: what's the reason for doing the modulo here? why not just write UUID_WITH_ZEROS?
There was a problem hiding this comment.
The idea was to use different values to make sure the filtering works, but now I think just with_zeros and null are enough. WDYT?
There was a problem hiding this comment.
I think it comes down to also testing other expressions. See my other comment on this
| public void testUUIDEq() { | ||
| assumeThat(format).as("Only valid for Parquet").isEqualTo(FileFormat.PARQUET); | ||
| boolean shouldRead = shouldRead(equal("uuid_col", UUID_WITH_ZEROS)); |
There was a problem hiding this comment.
what about testing other expressions?
shangxinli
commented
Sep 26, 2025
LGTM |
ndrluis
commented
Oct 6, 2025
@nastra I made the suggested changes |
| private static final UUID UUID_WITH_ZEROS = | ||
| UUID.fromString("00000000-0000-0000-0000-000000000000"); | ||
| private static final UUID UUID_WITH_ONES = |
| UUID nonExistentUuid = UUID.fromString("99999999-9999-9999-9999-999999999999"); | ||
| boolean shouldRead = shouldRead(notEqual("uuid_col", UUID_WITH_ZEROS)); |
There was a problem hiding this comment.
the test is still missing equal/greaterThan/lessThan. please also update the other test
Uh oh!
There was an error while loading. Please reload this page.
| structNotNull.setField("_int_field", INT_MIN_VALUE + i); | ||
| record.setField("_struct_not_null", structNotNull); // struct with int | ||
| record.setField("_uuid_col", (i % 2 == 0) ? UUID_WITH_ZEROS : null); |
There was a problem hiding this comment.
nit: newline right above this line can be removed
nastra
commented
Oct 15, 2025
I'll leave this open for a bit in case @huaxingao wants to review this as well |
Uh oh!
There was an error while loading. Please reload this page.
… with UUIDs (apache#14027) (cherry picked from commit ef40079)
I was working on this PyIceberg issue (apache/iceberg-python#2372) and I wrote a new test where PyIceberg writes a parquet file and PySpark writes another one. I wanted to ensure that we are able to read from both parquet files, but then I started receiving this exception: java.util.UUID cannot be cast to class java.nio.ByteBuffer. So this PR focuses on solving this problem to maintain compatibility between both implementations.