Skip to content

Conversation

@Fokko
Copy link
Contributor

@Fokko Fokko commented May 14, 2025

Which issue does this PR close?

I've been looking into exposing the Avro readers to PyIceberg. This will give a huge benefit to PyIceberg because we can drop the Cython Avro reader.

What changes are included in this PR?

Exposing methods and structures to read the manifest lists, and manifests itself.

Are these changes tested?

By using them in PyIceberg :)

// I don't fully comprehend the deserializer here,
// it works for a Type, but not for a StructType
// So I had to do some awkward stuff to make it work
let res: Result<Type, _> = serde_json::from_str(json);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have an example of the JSON input that fails deserialization into a StructType? If so I'll see what I can do

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @sdd for jumping in here 👍

I would expect the following to work:

Suggested change
let res: Result<Type, _> = serde_json::from_str(json);
let res = serde_json::from_str<StructType>(json);

I was also able to reproduce this in a unit test:

#[test]
fn empty_struct_type() {
    let json = r#"{"type": "struct", "fields": []}"#;

    let expected = StructType {
        fields: vec![],
        id_lookup: OnceLock::default(),
        name_lookup: OnceLock::default(),
    };

    let res = serde_json::from_str::<StructType>(json).unwrap();

    assert_eq!(res, expected);
}

But it looks like we need to wrap it in the Type enum.

@Xuanwo
Copy link
Member

Xuanwo commented May 15, 2025

Hi @Fokko, I experimented a bit with this PR. One possible approach is to allow Python to access our structs in _serde, which map directly to the on-disk representation without any type transformation or parsing.

We could have something like this:

#[pyfunction]
pub fn read_manifest_list_v2(bs: &[u8]) -> PyManifestList {
    let reader = apache_avro::Reader::new(bs).unwrap();
    let values = apache_avro::types::Value::Array(
        reader
            .collect::<std::result::Result<Vec<apache_avro::types::Value>, _>>()
            .unwrap(),
    );
    let manifest_list = apache_avro::from_value::<_serde::ManifestListV2>(&values).unwrap();

    PyManifestList {
        inner: manifest_list,
    }
}

Or much better if we can expose such API directly:

#[pyfunction]
pub fn read_manifest_list_v2(bs: &[u8]) -> PyManifestList {
    PyManifestList {
        inner: ManifestList::parse_as_is(bs),
    }
}

Our current design focuses solely on Rust users, but some users may simply want to parse the file themselves and don’t want iceberg-rust to handle any transformation (such as parsing into Datum).

We could reconsider this, perhaps we can expose these as a public API, but hide them behind a feature gate.

cc @liurenjie1024 and @sdd for ideas.

@Fokko
Copy link
Contributor Author

Fokko commented May 15, 2025

Our current design focuses solely on Rust users, but some users may simply want to parse the file themselves and don’t want iceberg-rust to handle any transformation (such as parsing into Datum).

Yes, that makes sense to me. I think we still want to have Iceberg-Rust some things like setting the default values for V2 (eg, setting 134: content to data, when reading V1 metadata):

image

Apart from that, I think your approach is great. Curious to learn what others think.

Comment on lines 26 to 34
pub struct PyLiteral {
inner: Literal,
}


#[pyclass]
pub struct PyPrimitiveLiteral {
inner: PrimitiveLiteral,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider having a values.rs module like what we did in core crate?

@liurenjie1024
Copy link
Contributor

Our current design focuses solely on Rust users, but some users may simply want to parse the file themselves and don’t want iceberg-rust to handle any transformation (such as parsing into Datum).

I'm leaning toward to this approach, also this makes the api more aligned with python/java implementation.

@Fokko
Copy link
Contributor Author

Fokko commented May 20, 2025

Thanks everyone for chiming in here. Let me summarize the discussion. I think there is consensus that the callback is not ideal.

  1. Supply required information to construct the summaries
    1. Instead of having the Fn(i32) -> Result<Option<StructType>> provider, we could pass in a HashMap<i32, StructType>. We would bind all the PartitionSpec's in PyIceberg. This is relative straightforward, but comes at a cost when there are many PartitionSpecs (which should be okay for the majority of tables).
    2. What @kevinjqliu suggested Expose Avro reader to PyIceberg #1328 (comment) suggested. Pass in the current Schema and PartitionSpec's to Iceberg-Rust where we can do the lazy binding on the Iceberg-Rust side.
    3. Go all the way, and convert the TableMetadata to Iceberg-Rust, this is probably where we end up at some point at some day, but require a lot of scaffolding.
  2. Deserialize in Vec<u8> instead of a Datum, and convert them later into the actual type. This removes the dependency on the Schema and the PartitionSpec's.

I'm leaning towards 2 since that aligns the best with PyIceberg, where we can deserialize the manifest-list without having to know about the schema. I would make sure that we have consensus before moving into a certain direction, and happy to follow up on that.

@liurenjie1024
Copy link
Contributor

Thanks everyone for chiming in here. Let me summarize the discussion. I think there is consensus that the callback is not ideal.

  1. Supply required information to construct the summaries

    1. Instead of having the Fn(i32) -> Result<Option<StructType>> provider, we could pass in a HashMap<i32, StructType>. We would bind all the PartitionSpec's in PyIceberg. This is relative straightforward, but comes at a cost when there are many PartitionSpecs (which should be okay for the majority of tables).
    2. What @kevinjqliu suggested Expose Avro reader to PyIceberg #1328 (comment) suggested. Pass in the current Schema and PartitionSpec's to Iceberg-Rust where we can do the lazy binding on the Iceberg-Rust side.
    3. Go all the way, and convert the TableMetadata to Iceberg-Rust, this is probably where we end up at some point at some day, but require a lot of scaffolding.
  2. Deserialize in Vec<u8> instead of a Datum, and convert them later into the actual type. This removes the dependency on the Schema and the PartitionSpec's.

I'm leaning towards 2 since that aligns the best with PyIceberg, where we can deserialize the manifest-list without having to know about the schema. I would make sure that we have consensus before moving into a certain direction, and happy to follow up on that.

+1

@kevinjqliu
Copy link
Contributor

I like #2 as well. The refactor should be less effort than scaffolding between python class and rust struct

@Fokko
Copy link
Contributor Author

Fokko commented May 22, 2025

Thanks for chiming in here, I've created PR #1369 that implements #2. PTAL

liurenjie1024 pushed a commit that referenced this pull request May 28, 2025
## Which issue does this PR close?

I would like to invite everyone to roast my Rust-skills in order for me
to improve myself :)

Unblocks #1328

This aligns closely with PyIceberg and Java and greatly simplifies the
use of the Avro readers in PyIceberg. Otherwise, we would need to update
public APIs.

## What changes are included in this PR?



## Are these changes tested?



---------

Co-authored-by: Kevin Liu <[email protected]>
@Xuanwo
Copy link
Member

Xuanwo commented May 30, 2025

#1369 has been merged, maybe we can remove the callback now.

@Fokko Fokko force-pushed the fd-avro-pyiceberg branch 5 times, most recently from 0fae964 to 51b3f97 Compare June 1, 2025 21:37
@Fokko
Copy link
Contributor Author

Fokko commented Sep 1, 2025

@Xuanwo @liurenjie1024 @kevinjqliu This is ready for another round of reviews :)

@kevinjqliu kevinjqliu self-requested a review September 16, 2025 01:57
@kevinjqliu
Copy link
Contributor

kevinjqliu commented Sep 17, 2025

I pushed a fix for CI running on Windows OS

The proper fix requires fixing how path is handled by pyiceberg's PyArrowFileIO class. This is an issue on the pyiceberg side too when running tests/utils/test_manifest.py::test_read_manifest_entry on Windows. I'll open an issue on the PyIceberg side with more details

@Fokko
Copy link
Contributor Author

Fokko commented Sep 17, 2025

@kevinjqliu Thanks for pushing the fix! I would expect that Windows will still work against an object store, but it cannot handle the c:/ prefix.

@Fokko
Copy link
Contributor Author

Fokko commented Sep 17, 2025

@Xuanwo @liurenjie1024 CI is now green 💚

@Fokko Fokko added this to the 0.7.0 release milestone Sep 17, 2025
Copy link
Member

@Xuanwo Xuanwo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this, let's move!

@Xuanwo Xuanwo merged commit bad8e4e into apache:main Sep 17, 2025
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants