Repository navigation
perf: parse manifest list entries without buffering Avro values #3224
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
base: main
Are you sure you want to change the base?
Changes from all commits
2d95153
b124830
e448f0f
3db218e
7015b31
2002867
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 |
|---|---|---|
|
|
@@ -23,15 +23,14 @@ mod manifest_file; | |
| mod reader; | ||
| mod writer; | ||
|
|
||
| use apache_avro::types::Value; | ||
| use apache_avro::{Reader, from_value}; | ||
| use apache_avro::Reader; | ||
| pub use manifest_file::*; | ||
| pub use reader::*; | ||
| pub use serde_bytes::ByteBuf; | ||
| pub use writer::*; | ||
|
|
||
| use self::_const_schema::MANIFEST_LIST_AVRO_SCHEMA_V1; | ||
| use super::FormatVersion; | ||
| use crate::avro::Resolved; | ||
| use crate::error::Result; | ||
|
|
||
| /// Placeholder for sequence number. The field with this value must be replaced with the actual sequence number before it write. | ||
|
|
@@ -59,25 +58,22 @@ pub struct ManifestList { | |
| impl ManifestList { | ||
| /// Parse manifest list from bytes. | ||
| pub fn parse_with_version(bs: &[u8], version: FormatVersion) -> Result<ManifestList> { | ||
| match version { | ||
| FormatVersion::V1 => { | ||
| let reader = Reader::builder(bs) | ||
| .reader_schema(&MANIFEST_LIST_AVRO_SCHEMA_V1) | ||
| .build()?; | ||
| let values = Value::Array(reader.collect::<std::result::Result<Vec<Value>, _>>()?); | ||
| from_value::<_serde::ManifestListV1>(&values)?.try_into() | ||
| } | ||
| FormatVersion::V2 => { | ||
| let reader = Reader::new(bs)?; | ||
| let values = Value::Array(reader.collect::<std::result::Result<Vec<Value>, _>>()?); | ||
| from_value::<_serde::ManifestListV2>(&values)?.try_into() | ||
| } | ||
| FormatVersion::V3 => { | ||
| let reader = Reader::new(bs)?; | ||
| let values = Value::Array(reader.collect::<std::result::Result<Vec<Value>, _>>()?); | ||
| from_value::<_serde::ManifestListV3>(&values)?.try_into() | ||
| } | ||
| } | ||
| let reader = Reader::new(bs)?; | ||
| let entries = match version { | ||
| FormatVersion::V1 => reader | ||
| .into_deser_iter::<Resolved<_serde::ManifestFileV1>>() | ||
| .map(|entry| entry?.0.try_into()) | ||
| .collect::<Result<Vec<_>>>()?, | ||
| FormatVersion::V2 => reader | ||
| .into_deser_iter::<Resolved<_serde::ManifestFileV2>>() | ||
| .map(|entry| entry?.0.try_into()) | ||
| .collect::<Result<Vec<_>>>()?, | ||
| FormatVersion::V3 => reader | ||
| .into_deser_iter::<Resolved<_serde::ManifestFileV3>>() | ||
| .map(|entry| entry?.0.try_into()) | ||
| .collect::<Result<Vec<_>>>()?, | ||
| }; | ||
| Ok(ManifestList { entries }) | ||
| } | ||
|
Comment on lines
59
to
77
Collaborator
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. #3354 merged today. It upgraded to What do you think about rebasing and reading manifest lists the same way as the manifest entry reader? Something like: /// Parse manifest list from bytes.
pub fn parse_with_version(bs: &[u8], version: FormatVersion) -> Result<ManifestList> {
let reader = Reader::new(bs)?;
let entries = match version {
FormatVersion::V1 => reader
.into_deser_iter::<Resolved<_serde::ManifestFileV1>>()
.map(|entry| entry?.0.try_into())
.collect::<Result<Vec<_>>>()?,
FormatVersion::V2 => reader
.into_deser_iter::<Resolved<_serde::ManifestFileV2>>()
.map(|entry| entry?.0.try_into())
.collect::<Result<Vec<_>>>()?,
FormatVersion::V3 => reader
.into_deser_iter::<Resolved<_serde::ManifestFileV3>>()
.map(|entry| entry?.0.try_into())
.collect::<Result<Vec<_>>>()?,
};
Ok(ManifestList { entries })
}This removes the per-record This is also the "Read manifest lists without
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. Done. I merged
Collaborator
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.
Awesome, and thank you for updating the PR description with the new numbers! |
||
|
|
||
| /// Get the entries in the manifest list. | ||
|
|
@@ -123,7 +119,7 @@ mod test { | |
| existing_rows_count: Some(0), | ||
| deleted_rows_count: Some(0), | ||
| partitions: Some(vec![]), | ||
| key_metadata: None, | ||
| key_metadata: Some(vec![1, 2, 3]), | ||
| first_row_id: None, | ||
| } | ||
| ] | ||
|
|
@@ -159,6 +155,71 @@ mod test { | |
| assert_eq!(manifest_list, parsed_manifest_list); | ||
| } | ||
|
|
||
| const VERSIONS: [FormatVersion; 3] = [FormatVersion::V1, FormatVersion::V2, FormatVersion::V3]; | ||
|
|
||
| fn entry(version: FormatVersion) -> ManifestFile { | ||
| ManifestFile { | ||
| manifest_path: "s3://test/metadata/manifest.avro".to_string(), | ||
| manifest_length: 1234, | ||
| partition_spec_id: 0, | ||
| content: ManifestContentType::Data, | ||
| sequence_number: if version == FormatVersion::V1 { 0 } else { 7 }, | ||
| min_sequence_number: if version == FormatVersion::V1 { 0 } else { 3 }, | ||
| added_snapshot_id: 42, | ||
| added_files_count: Some(1), | ||
| existing_files_count: Some(0), | ||
| deleted_files_count: Some(0), | ||
| added_rows_count: Some(9), | ||
| existing_rows_count: Some(0), | ||
| deleted_rows_count: Some(0), | ||
| partitions: Some(vec![FieldSummary { | ||
| contains_null: true, | ||
| contains_nan: None, | ||
| lower_bound: Some(vec![1, 0, 0, 0].into()), | ||
| upper_bound: None, | ||
| }]), | ||
| key_metadata: Some(vec![1, 2, 3]), | ||
| first_row_id: (version == FormatVersion::V3).then_some(100), | ||
| } | ||
| } | ||
|
|
||
| async fn encode(version: FormatVersion, entries: Vec<ManifestFile>) -> Vec<u8> { | ||
| let io = FileIO::new_with_memory(); | ||
| let output = io.new_output("memory:///manifest-list.avro").unwrap(); | ||
| let writer = output.writer().await.unwrap(); | ||
| let mut writer = match version { | ||
| FormatVersion::V1 => ManifestListWriter::v1(writer, 42, None), | ||
| FormatVersion::V2 => ManifestListWriter::v2(writer, 42, None, 7), | ||
| FormatVersion::V3 => ManifestListWriter::v3(writer, 42, None, 7, Some(100)), | ||
| }; | ||
| writer.add_manifests(entries.into_iter()).unwrap(); | ||
| writer.close().await.unwrap(); | ||
| io.new_input("memory:///manifest-list.avro") | ||
| .unwrap() | ||
| .read() | ||
| .await | ||
| .unwrap() | ||
| .to_vec() | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn test_parse_empty_manifest_list() { | ||
| for version in VERSIONS { | ||
| let bs = encode(version, vec![]).await; | ||
| let parsed = ManifestList::parse_with_version(&bs, version).unwrap(); | ||
| assert!(parsed.entries().is_empty(), "{version:?}"); | ||
| } | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn test_parse_truncated_manifest_list() { | ||
| for version in VERSIONS { | ||
| let bs = encode(version, vec![entry(version)]).await; | ||
| let result = ManifestList::parse_with_version(&bs[..bs.len() - 1], version); | ||
| assert!(result.is_err(), "{version:?}"); | ||
| } | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn test_parse_manifest_list_v2() { | ||
| let manifest_list = ManifestList { | ||
|
|
@@ -198,9 +259,9 @@ mod test { | |
| existing_rows_count: Some(0), | ||
| deleted_rows_count: Some(0), | ||
| partitions: Some( | ||
| vec![FieldSummary { contains_null: false, contains_nan: Some(false), lower_bound: Some(Datum::float(1.1_f32).to_bytes().unwrap()), upper_bound: Some(Datum::float(2.1_f32).to_bytes().unwrap())}] | ||
| vec![FieldSummary { contains_null: false, contains_nan: None, lower_bound: Some(Datum::float(1.1_f32).to_bytes().unwrap()), upper_bound: None}] | ||
| ), | ||
| key_metadata: None, | ||
| key_metadata: Some(vec![1, 2, 3]), | ||
| first_row_id: None, | ||
| } | ||
| ] | ||
|
|
@@ -316,9 +377,9 @@ mod test { | |
| existing_rows_count: Some(0), | ||
| deleted_rows_count: Some(0), | ||
| partitions: Some( | ||
| vec![FieldSummary { contains_null: false, contains_nan: Some(false), lower_bound: Some(Datum::float(1.1_f32).to_bytes().unwrap()), upper_bound: Some(Datum::float(2.1_f32).to_bytes().unwrap())}] | ||
| vec![FieldSummary { contains_null: false, contains_nan: None, lower_bound: Some(Datum::float(1.1_f32).to_bytes().unwrap()), upper_bound: None}] | ||
| ), | ||
| key_metadata: None, | ||
| key_metadata: Some(vec![1, 2, 3]), | ||
| first_row_id: Some(13), | ||
| } | ||
| ] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Java writes V1 manifest lists with the field names
added_files_count,existing_files_countanddeleted_files_count(seeV1Metadata.MANIFEST_LIST_SCHEMAandManifestFile), which are the names in the spec's manifest list fields.ManifestFileV1only knows the olderadded_data_files_countnames and has no aliases. So for a V1 list written by current Java, those three counts parse asNone.I checked this by writing a one-entry list with Java's V1 Avro schema and parsing it as V1.
added_files_countandexisting_files_countcome backNone, whileadded_rows_count(same name in both) comes backSome(9).mainatb154603areturns the same thing, so this isn't from this PR. Before, resolution againstMANIFEST_LIST_AVRO_SCHEMA_V1filledadded_data_files_countwith its null default, so serde never saw Java's names. Now that V1 reads against the writer schema, the#[serde(alias = ...)]approachManifestFileV2already uses works for V1 too. Adding the three aliases toManifestFileV1returnsSome(3)andSome(0)for that list.The spec treats a null count as non-zero, so planning stays correct, but the counts are lost. As I read
existing_manifest, a fast append on such a table copies these entries into the new manifest list, so the nulls get written back. Since this is a separate bug, could you open an issue for it? The fix plus a test with Java's V1 names would make a small follow-up PR.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great find, opened #3445