This is an automated email from the ASF dual-hosted git repository.

yihua pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hudi-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 6c14a9ed perf(core): encode record index entries directly as Avro 
binary (#873)
6c14a9ed is described below

commit 6c14a9ed8cc526c869f99ac456bb6bc7d7e4eb24
Author: Y Ethan Guo <[email protected]>
AuthorDate: Thu Oct 29 16:09:56 2026 -0700

    perf(core): encode record index entries directly as Avro binary (#873)
    
    Every written row adds a record index entry, and each was encoded by 
building
    an apache_avro Value tree and validating it against the full metadata record
    schema. The entry's shape is fixed, so it is now written straight into Avro
    binary; a test holds the bytes equal to the generic encoding.
    
    Closes #872
---
 crates/core/src/metadata/table/encode.rs | 110 +++++++++++++++++++++++++++++++
 1 file changed, 110 insertions(+)

diff --git a/crates/core/src/metadata/table/encode.rs 
b/crates/core/src/metadata/table/encode.rs
index 862d1747..7bce01d0 100644
--- a/crates/core/src/metadata/table/encode.rs
+++ b/crates/core/src/metadata/table/encode.rs
@@ -316,12 +316,77 @@ pub struct RecordIndexEntry {
 ///
 /// Deletes must not use this path — emit a delete log block instead. Null
 /// `recordIndexMetadata` NPEs Java's RLI reader.
+///
+/// Written straight into Avro binary rather than through a generic `Value`
+/// tree, since the record's shape is fixed and a write encodes one entry per
+/// row. The tests hold this encoding to the generic `Value` encoding.
 pub fn encode_record_index_entry(entry: &RecordIndexEntry) -> Result<Vec<u8>> {
     if entry.is_deleted {
         return Err(CoreError::MetadataTable(
             "RLI deletes must be written as delete log blocks, not null 
payloads".to_string(),
         ));
     }
+    let (high, low, file_index, file_id_str, encoding) =
+        encode_file_id_for_record_index(&entry.file_id);
+    // Field order and union branches follow HoodieMetadata.avsc: a union is
+    // its branch index then the value, `null` encodes as nothing.
+    let mut out = Vec::with_capacity(56 + entry.partition_path.len() + 
file_id_str.len());
+    for _ in MetaField::field_names() {
+        put_int(&mut out, 0); // _hoodie_* meta fields: null
+    }
+    put_string(&mut out, ""); // key: lives on the HFile key only
+    put_int(&mut out, MetadataRecordType::RecordIndex as i32);
+    put_int(&mut out, 0); // filesystemMetadata: null
+    put_int(&mut out, 0); // BloomFilterMetadata: null
+    put_int(&mut out, 0); // ColumnStatsMetadata: null
+    put_int(&mut out, 1); // recordIndexMetadata: HoodieRecordIndexInfo
+    put_int(&mut out, 1);
+    put_string(&mut out, &entry.partition_path);
+    put_int(&mut out, 1);
+    put_long(&mut out, high);
+    put_int(&mut out, 1);
+    put_long(&mut out, low);
+    put_int(&mut out, 1);
+    put_int(&mut out, file_index);
+    put_int(&mut out, 1);
+    put_string(&mut out, &file_id_str);
+    put_int(&mut out, 1);
+    put_long(&mut out, entry.instant_time_millis);
+    put_int(&mut out, encoding);
+    put_int(&mut out, 0); // position: null
+    put_int(&mut out, 0); // SecondaryIndexMetadata: null
+    Ok(out)
+}
+
+/// Avro zig-zag varint for a `long`.
+fn put_long(out: &mut Vec<u8>, value: i64) {
+    let mut zigzag = ((value << 1) ^ (value >> 63)) as u64;
+    loop {
+        let byte = (zigzag & 0x7f) as u8;
+        zigzag >>= 7;
+        if zigzag == 0 {
+            out.push(byte);
+            return;
+        }
+        out.push(byte | 0x80);
+    }
+}
+
+/// Avro zig-zag varint for an `int`.
+fn put_int(out: &mut Vec<u8>, value: i32) {
+    put_long(out, i64::from(value));
+}
+
+/// Avro `string`: byte length as a varint, then the UTF-8 bytes.
+fn put_string(out: &mut Vec<u8>, value: &str) {
+    put_long(out, value.len() as i64);
+    out.extend_from_slice(value.as_bytes());
+}
+
+/// The same record through the generic Avro encoder; the reference for
+/// [`encode_record_index_entry`].
+#[cfg(test)]
+fn encode_record_index_entry_generic(entry: &RecordIndexEntry) -> 
Result<Vec<u8>> {
     let (high, low, file_index, file_id_str, encoding) =
         encode_file_id_for_record_index(&entry.file_id);
     let rli_payload = Value::Union(
@@ -1032,6 +1097,51 @@ mod tests {
         assert_eq!(avro_key_field(&bytes), "");
     }
 
+    #[test]
+    fn encode_record_index_matches_generic_avro_encoding() {
+        let entries = [
+            RecordIndexEntry {
+                record_key: "k".to_string(),
+                partition_path: String::new(),
+                file_id: "3f2504e0-4f89-11d3-9a0c-0305e82c3301-0".to_string(),
+                instant_time_millis: 1_700_000_000_123,
+                is_deleted: false,
+            },
+            RecordIndexEntry {
+                record_key: "k".to_string(),
+                partition_path: "city=san_francisco".to_string(),
+                file_id: "3f2504e0-4f89-11d3-9a0c-0305e82c3301".to_string(),
+                instant_time_millis: 0,
+                is_deleted: false,
+            },
+            RecordIndexEntry {
+                record_key: "k".to_string(),
+                partition_path: format!("city={}", "x".repeat(200)),
+                file_id: "3f2504e0-4f89-11d3-9a0c-0305e82c3301-7".to_string(),
+                instant_time_millis: i64::MIN,
+                is_deleted: false,
+            },
+            RecordIndexEntry {
+                record_key: "k".to_string(),
+                partition_path: "p=\u{e9}t\u{e9}".to_string(),
+                file_id: "not-a-uuid-file-id".to_string(),
+                instant_time_millis: -1,
+                is_deleted: false,
+            },
+        ];
+        for entry in &entries {
+            let fast = encode_record_index_entry(entry).unwrap();
+            let generic = encode_record_index_entry_generic(entry).unwrap();
+            assert_eq!(fast, generic, "entry {entry:?}");
+            let decoded = decode_record_index_entry("k", &fast, None)
+                .unwrap()
+                .unwrap();
+            assert_eq!(decoded.file_id, entry.file_id);
+            assert_eq!(decoded.partition_path, entry.partition_path);
+            assert_eq!(decoded.instant_time_millis, entry.instant_time_millis);
+        }
+    }
+
     #[test]
     fn encode_record_index_clears_avro_key_for_hfile_interop() {
         let bytes = encode_record_index_entry(&RecordIndexEntry {

Reply via email to