You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@paimon.apache.org by ai...@apache.org on 2024/09/02 14:21:59 UTC
(paimon-rust) branch main updated: feat(file_index): add file index format read and write (#63)
This is an automated email from the ASF dual-hosted git repository.
aitozi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 65eb13a feat(file_index): add file index format read and write (#63)
65eb13a is described below
commit 65eb13a9fb1ce569e6d5bf26f39e6ce8296379f5
Author: Huanbing <de...@users.noreply.github.com>
AuthorDate: Mon Sep 2 22:21:55 2024 +0800
feat(file_index): add file index format read and write (#63)
---
crates/paimon/Cargo.toml | 7 +-
crates/paimon/src/error.rs | 5 +
crates/paimon/src/file_index/file_index_format.rs | 519 ++++++++++++++++++++++
crates/paimon/src/{lib.rs => file_index/mod.rs} | 8 +-
crates/paimon/src/io/file_io.rs | 49 +-
crates/paimon/src/lib.rs | 1 +
6 files changed, 559 insertions(+), 30 deletions(-)
diff --git a/crates/paimon/Cargo.toml b/crates/paimon/Cargo.toml
index d3dd4c0..959475f 100644
--- a/crates/paimon/Cargo.toml
+++ b/crates/paimon/Cargo.toml
@@ -46,6 +46,9 @@ serde_json = "1.0.120"
serde_with = "3.9.0"
snafu = "0.8.3"
typed-builder = "^0.19"
-opendal = { version = "0.49",features = ["services-fs"] }
+opendal = { version = "0.49", features = ["services-fs"] }
pretty_assertions = "1"
-apache-avro = { version = "0.17", features = ["snappy"] }
\ No newline at end of file
+apache-avro = { version = "0.17", features = ["snappy"] }
+
+[dev-dependencies]
+rand = "0.8.5"
diff --git a/crates/paimon/src/error.rs b/crates/paimon/src/error.rs
index 9294c64..d7cfd18 100644
--- a/crates/paimon/src/error.rs
+++ b/crates/paimon/src/error.rs
@@ -60,6 +60,11 @@ pub enum Error {
message: String,
source: apache_avro::Error,
},
+ #[snafu(
+ visibility(pub(crate)),
+ display("Paimon hitting invalid file index format: {}", message)
+ )]
+ FileIndexFormatInvalid { message: String },
}
impl From<opendal::Error> for Error {
diff --git a/crates/paimon/src/file_index/file_index_format.rs b/crates/paimon/src/file_index/file_index_format.rs
new file mode 100644
index 0000000..696fea8
--- /dev/null
+++ b/crates/paimon/src/file_index/file_index_format.rs
@@ -0,0 +1,519 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use bytes::{Buf, BufMut, Bytes, BytesMut};
+
+use crate::{
+ io::{FileIO, FileRead, FileStatus, InputFile, OutputFile},
+ Error,
+};
+
+/// Default 1MB read block size
+const READ_BLOCK_SIZE: u64 = 1024 * 1024;
+
+/// Quoted from the Java implement of the structure,
+/// `MAGIC`` is used to mark the beginning of a FileFormat structure.
+pub const MAGIC: u64 = 1493475289347502;
+
+/// Used to mark an empty INDEX.
+pub const EMPTY_INDEX_FLAG: i64 = -1;
+
+#[derive(Debug)]
+struct IndexInfo {
+ start_pos: i64,
+ length: i64,
+}
+
+#[repr(i32)]
+#[derive(Debug, PartialEq, Eq)]
+enum Version {
+ V1,
+}
+
+/// File index file format. All columns and offsets are stored in the header.
+///
+/// ```text
+/// _____________________________________ _____________________
+/// | magic |version|head length |
+/// |-------------------------------------|
+/// | column number |
+/// |-------------------------------------|
+/// | column 1 | index number |
+/// |-------------------------------------|
+/// | index name 1 |start pos |length |
+/// |-------------------------------------|
+/// | index name 2 |start pos |length |
+/// |-------------------------------------|
+/// | index name 3 |start pos |length |
+/// |-------------------------------------| HEADER
+/// | column 2 | index number |
+/// |-------------------------------------|
+/// | index name 1 |start pos |length |
+/// |-------------------------------------|
+/// | index name 2 |start pos |length |
+/// |-------------------------------------|
+/// | index name 3 |start pos |length |
+/// |-------------------------------------|
+/// | ... |
+/// |-------------------------------------|
+/// | ... |
+/// |-------------------------------------|
+/// | redundant length |redundant bytes |
+/// |-------------------------------------| ---------------------
+/// | BODY |
+/// | BODY |
+/// | BODY | BODY
+/// | BODY |
+/// |_____________________________________| _____________________
+///
+/// - `magic`: 8 bytes long
+/// - `version`: 4-byte integer
+/// - `head length`: 4-byte integer
+/// - `column number`: 4-byte integer
+/// - `column x`: variable-length UTF-8 string (length + bytes)
+/// - `index number`: 4-byte integer (number of index items below)
+/// - `index name x`: variable-length UTF-8 string
+/// - `start pos`: 4-byte integer
+/// - `length`: 4-byte integer
+/// - `redundant length`: 4-byte integer (for compatibility with future versions; content is zero in this version)
+/// - `redundant bytes`: variable-length bytes (for compatibility with future versions; empty in this version)
+/// - `BODY`: sequence of index data (concatenated index data for each column)
+/// ```
+///
+/// Impl Reference: <https://github.com/apache/paimon/blob/release-0.8.2/paimon-common/src/main/java/org/apache/paimon/fileindex/FileIndexFormat.java>
+pub async fn write_column_indexes(
+ path: &str,
+ indexes: HashMap<String, HashMap<String, Bytes>>,
+) -> crate::Result<OutputFile> {
+ let file_io = FileIO::from_url(path)?.build()?;
+ let output = file_io.new_output(path)?;
+ let mut writer = output.writer().await?;
+
+ let mut body_info: HashMap<String, HashMap<String, IndexInfo>> = HashMap::new();
+ let mut total_data_size = 0;
+
+ // Calculate the total data size
+ for bytes_map in indexes.values() {
+ for data in bytes_map.values() {
+ if !data.is_empty() {
+ total_data_size += data.len();
+ }
+ }
+ }
+
+ let mut body = BytesMut::with_capacity(total_data_size);
+
+ for (column_name, bytes_map) in indexes.into_iter() {
+ let inner_map = body_info.entry(column_name.clone()).or_default();
+ for (index_name, data) in bytes_map {
+ let start_position = body.len() as i64;
+ if data.is_empty() {
+ inner_map.insert(
+ index_name,
+ IndexInfo {
+ start_pos: EMPTY_INDEX_FLAG,
+ length: 0,
+ },
+ );
+ } else {
+ body.extend_from_slice(&data);
+ inner_map.insert(
+ index_name,
+ IndexInfo {
+ start_pos: start_position,
+ length: body.len() as i64 - start_position,
+ },
+ );
+ }
+ }
+ }
+
+ // write_head(writer, &body_info).await?;
+ let head_length = calculate_head_length(&body_info)?;
+ let mut head_buffer = BytesMut::with_capacity(head_length);
+
+ // Magic
+ head_buffer.put_u64_le(MAGIC);
+ // Version
+ head_buffer.put_i32_le(Version::V1 as i32);
+ // HeadLength
+ head_buffer.put_i32_le(head_length as i32);
+ // ColumnSize
+ head_buffer.put_i32_le(body_info.len() as i32);
+
+ for (column_name, index_info) in body_info {
+ // ColumnName
+ head_buffer.put_u16_le(column_name.len() as u16);
+ head_buffer.put_slice(column_name.as_bytes());
+ // IndexTypeSize
+ head_buffer.put_i32_le(index_info.len() as i32);
+ // ColumnInfo,offset = headLength
+ for (index_name, IndexInfo { start_pos, length }) in index_info {
+ head_buffer.put_u16_le(index_name.len() as u16);
+ head_buffer.put_slice(index_name.as_bytes());
+ let adjusted_start = if start_pos == EMPTY_INDEX_FLAG {
+ EMPTY_INDEX_FLAG
+ } else {
+ start_pos + head_length as i64
+ };
+ head_buffer.put_i64_le(adjusted_start);
+ head_buffer.put_i64_le(length);
+ }
+ }
+
+ // Redundant length for future compatibility
+ head_buffer.put_i32_le(0);
+
+ // Write into
+ writer.write(head_buffer.freeze()).await?;
+ writer.write(body.freeze()).await?;
+ writer.close().await?;
+ Ok(output)
+}
+
+fn calculate_head_length(
+ body_info: &HashMap<String, HashMap<String, IndexInfo>>,
+) -> crate::Result<usize> {
+ // Magic + Version + HeadLength + ColumnNumber + RedundantLength
+ let base_length = 8 + 4 + 4 + 4 + 4;
+ let mut total_length = base_length;
+
+ for (column_name, index_info) in body_info {
+ // Column name length + actual column name length
+ total_length += 2 + column_name.len();
+ // IndexTypeSize (index number)
+ total_length += 4;
+
+ for index_name in index_info.keys() {
+ // Index name length + actual index name length
+ total_length += 2 + index_name.len();
+ // start_pos (8 bytes) + length (8 bytes)
+ total_length += 16;
+ }
+ }
+
+ Ok(total_length)
+}
+
+pub struct FileIndex {
+ reader: Box<dyn FileRead>,
+ header: HashMap<String, HashMap<String, IndexInfo>>,
+}
+
+impl FileIndex {
+ pub async fn get_column_index(
+ &self,
+ column_name: &str,
+ ) -> crate::Result<HashMap<String, Bytes>> {
+ if let Some(index_info) = self.header.get(column_name) {
+ let mut result = HashMap::new();
+ for (index_name, info) in index_info {
+ let bytes = self.get_bytes_with_start_and_length(info).await?;
+ result.insert(index_name.clone(), bytes);
+ }
+ Ok(result)
+ } else {
+ Err(Error::FileIndexFormatInvalid {
+ message: format!("Column '{}' not found in header", column_name),
+ })
+ }
+ }
+
+ pub async fn get_index(&self) -> crate::Result<HashMap<String, HashMap<String, Bytes>>> {
+ let mut result = HashMap::new();
+ for (column_name, index_info) in self.header.iter() {
+ let mut column_index = HashMap::new();
+ for (index_name, info) in index_info {
+ let bytes = self.get_bytes_with_start_and_length(info).await?;
+ column_index.insert(index_name.clone(), bytes);
+ }
+ result.insert(column_name.clone(), column_index);
+ }
+ Ok(result)
+ }
+
+ async fn get_bytes_with_start_and_length(
+ &self,
+ index_info: &IndexInfo,
+ ) -> crate::Result<Bytes> {
+ let data_bytes = self
+ .reader
+ .read(index_info.start_pos as u64..(index_info.start_pos + index_info.length) as u64)
+ .await?;
+
+ Ok(data_bytes)
+ }
+}
+
+pub struct FileIndexFormatReader {
+ reader: Box<dyn FileRead>,
+ stat: FileStatus,
+}
+
+impl FileIndexFormatReader {
+ pub async fn get_file_index(input_file: InputFile) -> crate::Result<FileIndex> {
+ let reader = input_file.reader().await?;
+ let mut file_reader = Self {
+ reader: Box::new(reader),
+ stat: input_file.metadata().await?,
+ };
+ let header = file_reader.read_header().await?;
+ Ok(FileIndex {
+ header,
+ reader: file_reader.reader,
+ })
+ }
+
+ async fn read_header(&mut self) -> crate::Result<HashMap<String, HashMap<String, IndexInfo>>> {
+ let read_size = if self.stat.size < READ_BLOCK_SIZE {
+ self.stat.size
+ } else {
+ READ_BLOCK_SIZE
+ };
+ let mut buffer = self.reader.read(0..read_size).await?;
+
+ // Magic (8 bytes)
+ let magic = buffer.get_u64_le();
+ if magic != MAGIC {
+ return Err(Error::FileIndexFormatInvalid {
+ message: format!("Expected MAGIC: {}, but found: {}", MAGIC, magic),
+ });
+ }
+
+ // Version (4 bytes)
+ let version = buffer.get_i32_le();
+ if version != Version::V1 as i32 {
+ return Err(Error::FileIndexFormatInvalid {
+ message: format!(
+ "Unsupported file index version: expected {}, but found: {}",
+ Version::V1 as i32,
+ version
+ ),
+ });
+ }
+
+ // Head Length (4 bytes)
+ let head_length = buffer.get_i32_le() as usize;
+
+ // Ensure the header is fully contained in the buffer
+ if buffer.len() < head_length {
+ let remaining = head_length - buffer.len();
+ let mut remaining_head_buffer = BytesMut::with_capacity(remaining);
+ let additional_data = self
+ .reader
+ .read(buffer.len() as u64..buffer.len() as u64 + remaining as u64)
+ .await?;
+ remaining_head_buffer.extend_from_slice(&additional_data);
+ buffer = Bytes::from(
+ [buffer.slice(0..), remaining_head_buffer.freeze().slice(0..)].concat(),
+ );
+ }
+
+ // Column Number (4 bytes)
+ let column_number = buffer.get_i32_le();
+
+ let mut current_offset = 20;
+ let mut header = HashMap::new();
+
+ for _ in 0..column_number {
+ // Column Name Length (2 bytes)
+ let column_name_len = buffer.get_u16_le();
+ current_offset += 2;
+
+ // Column Name (variable-length UTF-8 string)
+ let column_name = String::from_utf8(buffer.split_to(column_name_len as usize).to_vec())
+ .map_err(|e| Error::FileIndexFormatInvalid {
+ message: format!("Invalid UTF-8 sequence in column name: {}", e),
+ })?;
+ current_offset += column_name_len as u64;
+
+ // Index Number (4 bytes)
+ let index_number = buffer.get_i32_le();
+ current_offset += 4;
+
+ let mut index_info_map = HashMap::new();
+ for _ in 0..index_number {
+ // Index Name Length (2 bytes)
+ let index_name_len = buffer.get_u16_le();
+ current_offset += 2;
+
+ // Index Name (variable-length UTF-8 string)
+ let index_name =
+ String::from_utf8(buffer.split_to(index_name_len as usize).to_vec()).unwrap();
+ current_offset += index_name_len as u64;
+
+ // Start Pos (8 bytes)
+ let start_pos = buffer.get_i64_le();
+ current_offset += 4;
+
+ // Length (8 bytes)
+ let length = buffer.get_i64_le();
+ current_offset += 4;
+
+ index_info_map.insert(index_name, IndexInfo { start_pos, length });
+ }
+
+ header.insert(column_name, index_info_map);
+ }
+
+ let redundant_length = buffer.get_i32_le() as u64;
+ current_offset += 4;
+
+ if redundant_length > 0 {
+ let redundant_bytes = buffer.split_to(redundant_length as usize);
+
+ if redundant_bytes.len() as u64 != redundant_length {
+ return Err(Error::FileIndexFormatInvalid {
+ message: format!(
+ "Expected to read {} redundant bytes, but found only {}, on offset {}",
+ redundant_length,
+ redundant_bytes.len(),
+ current_offset
+ ),
+ });
+ }
+ }
+
+ Ok(header)
+ }
+}
+
+#[cfg(test)]
+mod file_index_format_tests {
+
+ use super::*;
+ use bytes::Bytes;
+ use std::collections::HashMap;
+
+ #[tokio::test]
+ async fn test_single_column_single_index() -> crate::Result<()> {
+ let path = "memory:/tmp/test_single_column_single_index";
+
+ let mut indexes = HashMap::new();
+ let mut index_map = HashMap::new();
+ index_map.insert("index1".to_string(), Bytes::from("sample_data"));
+ indexes.insert("column111".to_string(), index_map);
+
+ let output = write_column_indexes(path, indexes.clone()).await?;
+
+ let input = output.to_input_file();
+
+ let reader = FileIndexFormatReader::get_file_index(input).await?;
+ let column_data = reader.get_column_index("column111").await?;
+ assert_eq!(
+ column_data.get("index1").unwrap(),
+ &Bytes::from("sample_data")
+ );
+
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_multiple_columns_multiple_indexes() -> crate::Result<()> {
+ let path = "memory:/tmp/test_multiple_columns_multiple_indexes";
+
+ let mut indexes = HashMap::new();
+ for col_num in 1..5 {
+ let column_name = format!("column{}", col_num);
+ let mut index_map = HashMap::new();
+ for idx_num in 1..5 {
+ index_map.insert(
+ format!("index{}", idx_num),
+ random_bytes(100 + col_num * idx_num),
+ );
+ }
+ indexes.insert(column_name, index_map);
+ }
+
+ let output = write_column_indexes(path, indexes.clone()).await?;
+
+ let input = output.to_input_file();
+
+ let reader = FileIndexFormatReader::get_file_index(input).await?;
+ for (column, index_map) in indexes {
+ let column_data = reader.get_column_index(&column).await?;
+ for (index_name, expected_data) in index_map {
+ assert_eq!(column_data.get(&index_name).unwrap(), &expected_data);
+ }
+ }
+
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_empty_file_index() -> crate::Result<()> {
+ let path = "memory:/tmp/test_empty_file_index";
+
+ let mut indexes = HashMap::new();
+ let mut a_index = HashMap::new();
+ a_index.insert("b".to_string(), Bytes::new());
+ a_index.insert("c".to_string(), Bytes::new());
+ indexes.insert("a".to_string(), a_index);
+
+ let output = write_column_indexes(path, indexes.clone()).await?;
+
+ let input = output.to_input_file();
+
+ let reader = FileIndexFormatReader::get_file_index(input).await?;
+
+ let column_indexes = reader.get_column_index("a").await?;
+ assert_eq!(column_indexes.len(), 2);
+ assert_eq!(column_indexes.get("b").unwrap(), &Bytes::new());
+ assert_eq!(column_indexes.get("c").unwrap(), &Bytes::new());
+
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_large_data_set() -> crate::Result<()> {
+ let path = "memory:/tmp/test_large_data_set";
+
+ let mut indexes = HashMap::new();
+ let mut large_data = HashMap::new();
+ large_data.insert("large_index".to_string(), random_bytes(100_000_000)); // 100MB data
+ indexes.insert("large_column".to_string(), large_data);
+
+ write_column_indexes(path, indexes.clone()).await?;
+
+ let output = write_column_indexes(path, indexes.clone()).await?;
+
+ let input = output.to_input_file();
+
+ let reader = FileIndexFormatReader::get_file_index(input).await?;
+ let column_data = reader.get_column_index("large_column").await?;
+ assert_eq!(
+ column_data.get("large_index").unwrap(),
+ &indexes
+ .get("large_column")
+ .unwrap()
+ .get("large_index")
+ .unwrap()
+ );
+
+ Ok(())
+ }
+
+ fn random_bytes(len: usize) -> Bytes {
+ use rand::RngCore;
+ let mut rng = rand::thread_rng();
+ let mut bytes = vec![0u8; len];
+ rng.fill_bytes(&mut bytes);
+ Bytes::from(bytes)
+ }
+}
diff --git a/crates/paimon/src/lib.rs b/crates/paimon/src/file_index/mod.rs
similarity index 90%
copy from crates/paimon/src/lib.rs
copy to crates/paimon/src/file_index/mod.rs
index 6e15e0b..ca9ee54 100644
--- a/crates/paimon/src/lib.rs
+++ b/crates/paimon/src/file_index/mod.rs
@@ -15,9 +15,5 @@
// specific language governing permissions and limitations
// under the License.
-mod error;
-pub use error::Error;
-pub use error::Result;
-
-pub mod io;
-pub mod spec;
+mod file_index_format;
+pub use file_index_format::*;
diff --git a/crates/paimon/src/io/file_io.rs b/crates/paimon/src/io/file_io.rs
index d9ebc87..8b1d926 100644
--- a/crates/paimon/src/io/file_io.rs
+++ b/crates/paimon/src/io/file_io.rs
@@ -205,13 +205,6 @@ impl FileIOBuilder {
}
}
- pub fn new_fs_io_builder() -> Self {
- Self {
- scheme_str: None,
- props: HashMap::default(),
- }
- }
-
pub(crate) fn into_parts(self) -> (String, HashMap<String, String>) {
(self.scheme_str.unwrap_or_default(), self.props)
}
@@ -246,11 +239,7 @@ pub trait FileRead: Send + Unpin + 'static {
#[async_trait::async_trait]
impl FileRead for opendal::Reader {
async fn read(&self, range: Range<u64>) -> crate::Result<Bytes> {
- // TODO: build a error type
- Ok(opendal::Reader::read(self, range)
- .await
- .expect("read error")
- .to_bytes())
+ Ok(opendal::Reader::read(self, range).await?.to_bytes())
}
}
@@ -323,7 +312,7 @@ impl InputFile {
}
}
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub struct OutputFile {
op: Operator,
path: String,
@@ -386,7 +375,9 @@ mod file_action_test {
async fn common_test_get_status(file_io: &FileIO, path: &str) {
let output = file_io.new_output(path).unwrap();
- output.write(Bytes::from("hello world")).await.unwrap();
+ let mut writer = output.writer().await.unwrap();
+ writer.write(Bytes::from("hello world")).await.unwrap();
+ writer.close().await.unwrap();
let status = file_io.get_status(path).await.unwrap();
assert_eq!(status.size, 11);
@@ -396,7 +387,9 @@ mod file_action_test {
async fn common_test_exists(file_io: &FileIO, path: &str) {
let output = file_io.new_output(path).unwrap();
- output.write(Bytes::from("hello world")).await.unwrap();
+ let mut writer = output.writer().await.unwrap();
+ writer.write(Bytes::from("hello world")).await.unwrap();
+ writer.close().await.unwrap();
let exists = file_io.exists(path).await.unwrap();
assert!(exists);
@@ -406,7 +399,9 @@ mod file_action_test {
async fn common_test_delete_file(file_io: &FileIO, path: &str) {
let output = file_io.new_output(path).unwrap();
- output.write(Bytes::from("hello world")).await.unwrap();
+ let mut writer = output.writer().await.unwrap();
+ writer.write(Bytes::from("hello world")).await.unwrap();
+ writer.close().await.unwrap();
file_io.delete_file(path).await.unwrap();
@@ -425,7 +420,9 @@ mod file_action_test {
async fn common_test_rename(file_io: &FileIO, src: &str, dst: &str) {
let output = file_io.new_output(src).unwrap();
- output.write(Bytes::from("hello world")).await.unwrap();
+ let mut writer = output.writer().await.unwrap();
+ writer.write(Bytes::from("hello world")).await.unwrap();
+ writer.close().await.unwrap();
file_io.rename(src, dst).await.unwrap();
@@ -500,7 +497,9 @@ mod input_output_test {
async fn common_test_output_file_write_and_read(file_io: &FileIO, path: &str) {
let output = file_io.new_output(path).unwrap();
- output.write(Bytes::from("hello world")).await.unwrap();
+ let mut writer = output.writer().await.unwrap();
+ writer.write(Bytes::from("hello world")).await.unwrap();
+ writer.close().await.unwrap();
let input = output.to_input_file();
let content = input.read().await.unwrap();
@@ -512,7 +511,9 @@ mod input_output_test {
async fn common_test_output_file_exists(file_io: &FileIO, path: &str) {
let output = file_io.new_output(path).unwrap();
- output.write(Bytes::from("hello world")).await.unwrap();
+ let mut writer = output.writer().await.unwrap();
+ writer.write(Bytes::from("hello world")).await.unwrap();
+ writer.close().await.unwrap();
let exists = output.exists().await.unwrap();
assert!(exists);
@@ -522,7 +523,9 @@ mod input_output_test {
async fn common_test_input_file_metadata(file_io: &FileIO, path: &str) {
let output = file_io.new_output(path).unwrap();
- output.write(Bytes::from("hello world")).await.unwrap();
+ let mut writer = output.writer().await.unwrap();
+ writer.write(Bytes::from("hello world")).await.unwrap();
+ writer.close().await.unwrap();
let input = output.to_input_file();
let metadata = input.metadata().await.unwrap();
@@ -534,11 +537,13 @@ mod input_output_test {
async fn common_test_input_file_partial_read(file_io: &FileIO, path: &str) {
let output = file_io.new_output(path).unwrap();
- output.write(Bytes::from("hello world")).await.unwrap();
+ let mut writer = output.writer().await.unwrap();
+ writer.write(Bytes::from("hello world")).await.unwrap();
+ writer.close().await.unwrap();
let input = output.to_input_file();
let reader = input.reader().await.unwrap();
- let partial_content = reader.read(0..5).await.unwrap(); // 读取 "hello"
+ let partial_content = reader.read(0..5).await.unwrap(); // read "hello"
assert_eq!(&partial_content[..], b"hello");
diff --git a/crates/paimon/src/lib.rs b/crates/paimon/src/lib.rs
index 6e15e0b..5296a02 100644
--- a/crates/paimon/src/lib.rs
+++ b/crates/paimon/src/lib.rs
@@ -19,5 +19,6 @@ mod error;
pub use error::Error;
pub use error::Result;
+pub mod file_index;
pub mod io;
pub mod spec;