Skip to content
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

refine: refine interface of ManifestWriter #738

Merged
merged 4 commits into from
Jan 18, 2025

Conversation

ZENOTME
Copy link
Contributor

@ZENOTME ZENOTME commented Nov 28, 2024

This PR refine the write interface of ManifestWriter according to ManifestWriter from pyiceberg. It add 3 interface add, delete, existing which will rewrite some metadata of manifest entry, e.g. snapshot id, sequence number, file sequence number.

These refined interfaces are benefit for MergeAppend.( I' m working on it now.

@ZENOTME
Copy link
Contributor Author

ZENOTME commented Nov 28, 2024

Copy link
Contributor

@liurenjie1024 liurenjie1024 left a comment

Choose a reason for hiding this comment

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

Thanks @ZENOTME for this pr!

/// Write a manifest.
pub async fn write(mut self, manifest: Manifest) -> Result<ManifestFile> {
/// Add a new manifest entry.
pub fn add(&mut self, mut entry: ManifestEntry) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

It's kind of weird of manipulating arguments, how about make the arguments DataFile?

Copy link
Contributor

Choose a reason for hiding this comment

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

Applies to other apis.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the reason here use ManifesEntry is that in some case we will add entry from other Manifest. In this case, there are some info we need from original ManifsetEntry. E.g. when we add the delete manifest entry, we change the snapshot id and keep the original sequence number.

/// Add a delete manifest entry.
    pub fn delete(&mut self, mut entry: ManifestEntry) -> Result<()> {
        entry.status = ManifestStatus::Deleted;
        entry.snapshot_id = Some(self.snapshot_id);
        self.add_entry(entry)?;
        Ok(())
    }

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not convinced. If we ask user to provide ManifestEntry, it would be confusing to user which part will be used and which part not. I think the style in java would be more clear from a user's view. If we to use ManifestEntry approach, we must have clear documentation about the behavior of each part, e.g. which is ignored, which is reserved.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we to use ManifestEntry approach, we must have clear documentation about the behavior of each part, e.g. which is ignored, which is reserved.

I agree with this. Then I think these functions can be pub(crate) to ensure public users will not use it. I think for now there is no demand that user need to use this API.🤔

}

/// Write manifest file and return it.
pub async fn to_manifest_file(mut self, metadata: ManifestMetadata) -> Result<ManifestFile> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I have concerns with this api, since it's error prone. According to iceberg's spec, each manifest file should contains one type of data file: data or deletes. It's quite possible that the user pass different kinds entries in previouse method, then the metadata is different. My suggestion is to follow java/python's approach:

  1. A factory method like
pub fn new_v1_writer(...) {}
pub fn new_v2_writer(...) {}
pub fn new_v2_delete_writer(...) {}
  1. We could use things like trait or enum to abstract out common parts of different writers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could use things like trait or enum to abstract out common parts of different writers.

Difference between v1, v2, delete is:

  1. the metadata of avro file
  2. avro schema
  3. content type
  4. check in add_entry to make sure entry.content_type == writer.content_type
  5. serialize the ManifestEntry

I think both differences except for serializing the ManifestEntry can be implemented by storing different data in the writer when we create the writer using the factory method. So do we really need to abstract out common parts of different writers now?🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm fine without trait/enum, the focus is factory methods to ensure api safety.

Copy link
Contributor

@liurenjie1024 liurenjie1024 left a comment

Choose a reason for hiding this comment

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

Thanks @ZENOTME for this pr, generally LGTM! I still have concerns with the add/eixsting/delete api, and prefer the approach used in java api: org.apache.iceberg.ManifestWriter#add(F), which provides better api safety. For what I mentioned in comments, it's possible to add some check, but it's not a good api for user which throws error at runtime.

/// Create a new builder.
pub fn new(
output: OutputFile,
snapshot_id: i64,
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be optional.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should use some special value like -1 here. The reason it's optional is that when we append a data file here, the snapshot id is unknown. Actual snapshot id is determined when do commit, which may fail and retry.


/// Add an existing manifest entry. This method will update following status of the entry:
/// - Update the entry status to `Existing`
pub fn existing(&mut self, mut entry: ManifestEntry) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This incorrect, an existing entry requires user to provide snapshot id, data sequence number, which are all optional in ManifestEntry..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems iceberg-java also has ian nterface like here: https://github.com/apache/iceberg/blob/d96901b843395fe669f6bd4f618f8e5e46c0eed4/core/src/main/java/org/apache/iceberg/ManifestWriter.java#L157. And looks like it also support the case the existing entry without snapshot id.🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

They are different cases, one is public api, another is package private. They have different callers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh I got you. I think the interface here is package private. I forget to mark them as pub(crate) to avoid confusion.

/// Add a delete manifest entry. This method will update following status of the entry:
/// - Update the entry status to `Deleted`
/// - Set the snapshot id to the current snapshot id
pub fn delete(&mut self, mut entry: ManifestEntry) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is also incorrect. The sequence number must be provided.

@ZENOTME
Copy link
Contributor Author

ZENOTME commented Jan 16, 2025

Thanks @ZENOTME for this pr, generally LGTM! I still have concerns with the add/eixsting/delete api, and prefer the approach used in java api: org.apache.iceberg.ManifestWriter#add(F), which provides better api safety. For what I mentioned in comments, it's possible to add some check, but it's not a good api for user which throws error at runtime.

Hi @liurenjie1024, seems iceberg-java also provide the interface for entry in https://github.com/apache/iceberg/blob/d96901b843395fe669f6bd4f618f8e5e46c0eed4/core/src/main/java/org/apache/iceberg/ManifestWriter.java#L157, and use them in ManifsetMergeManager: https://github.com/apache/iceberg/blob/d96901b843395fe669f6bd4f618f8e5e46c0eed4/core/src/main/java/org/apache/iceberg/ManifestMergeManager.java#L188. And it also don't check some case like existing entry with null data sequence number. Is the case that iceberg-java missed or it's acceptable.

ZENOTME added 3 commits January 17, 2025 14:53
1. adopt factory method to build different type manifest writer
2. provide add, exist, delete method
2. mark add entry method as crate private
Copy link
Contributor

@liurenjie1024 liurenjie1024 left a comment

Choose a reason for hiding this comment

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

Thanks @ZENOTME for this great pr! Just left some minor suggestions, and we are close !

data_file: DataFile,
snapshot_id: i64,
sequence_number: i64,
file_sequence_number: i64,
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be optional, file_sequence_number could be inherited from snapshot.

&mut self,
data_file: DataFile,
sequence_number: i64,
file_sequence_number: i64,
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be optional, file_sequence_number could be inherited from snapshot.

@@ -41,6 +41,9 @@ use crate::io::OutputFile;
use crate::spec::PartitionField;
use crate::{Error, ErrorKind};

/// Placeholder for snapshot ID. The field with this value must be replaced with the actual snapshot ID before it is committed.
pub const UNASSIGNED_SNAPSHOT_ID: i64 = -1;
Copy link
Contributor

Choose a reason for hiding this comment

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

We should move this to snapshot module.

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 @ZENOTME for working on this. The design mostly look good to me. Only have some question about the API naming.

/// # TODO
/// Remove this allow later
#[allow(dead_code)]
pub(crate) fn existing(&mut self, mut entry: ManifestEntry) -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

Hi, the API naming seems a bit unclear. If we are "adding an existing manifest entry," how about naming this API add_existing_entry?

/// # TODO
/// Remove this allow later
#[allow(dead_code)]
pub(crate) fn delete(&mut self, mut entry: ManifestEntry) -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe delete_entry?

/// - Set the snapshot id to the current snapshot id
/// - Set the sequence number to `None` if it is invalid(smaller than 0)
/// - Set the file sequence number to `None`
pub(crate) fn add(&mut self, mut entry: ManifestEntry) -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add_entry?


/// Add an existing manifest entry. The original data and file sequence numbers, snapshot ID,
/// which were assigned at commit, must be preserved when adding an existing entry.
pub fn existing_file(
Copy link
Member

Choose a reason for hiding this comment

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

How about add_existing_file?

}

/// Write manifest file and return it.
pub async fn to_manifest_file(mut self) -> Result<ManifestFile> {
Copy link
Member

Choose a reason for hiding this comment

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

to_manifest_file sounds more like a conversion function, but it actually involves heavy I/O operations. How about using the name suggested in the comments: write_manifest_file?

Copy link
Contributor

@liurenjie1024 liurenjie1024 left a comment

Choose a reason for hiding this comment

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

Thanks @ZENOTME for this pr, LGTM!

@liurenjie1024
Copy link
Contributor

Let's wait for a moment to see if @Xuanwo has other comments.

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 @ZENOTME for working on this, let move!

@Xuanwo Xuanwo merged commit 55cca03 into apache:main Jan 18, 2025
17 checks passed
@ZENOTME ZENOTME deleted the refine_manifest branch January 20, 2025 06:21
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.

3 participants