blob: 7350c513613d858b447eafb68ff95f39a31f5b3e [file]
// 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 crate::spec::stats::BinaryTableStats;
use chrono::serde::ts_milliseconds_option::deserialize as from_millis_opt;
use chrono::serde::ts_milliseconds_option::serialize as to_millis_opt;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
/// Suffix for sidecar file-index files, matching Java `DataFilePathFactory.INDEX_PATH_SUFFIX`.
pub const DATA_FILE_INDEX_SUFFIX: &str = ".index";
/// Metadata of a data file.
///
/// Impl References: <https://github.com/apache/paimon/blob/release-0.8.2/paimon-core/src/main/java/org/apache/paimon/io/DataFileMeta.java>
#[derive(Debug, Eq, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DataFileMeta {
#[serde(rename = "_FILE_NAME")]
pub file_name: String,
#[serde(rename = "_FILE_SIZE")]
pub file_size: i64,
// row_count tells the total number of rows (including add & delete) in this file.
#[serde(rename = "_ROW_COUNT")]
pub row_count: i64,
#[serde(rename = "_MIN_KEY", with = "serde_bytes")]
pub min_key: Vec<u8>,
#[serde(rename = "_MAX_KEY", with = "serde_bytes")]
pub max_key: Vec<u8>,
#[serde(rename = "_KEY_STATS")]
pub key_stats: BinaryTableStats,
#[serde(rename = "_VALUE_STATS")]
pub value_stats: BinaryTableStats,
#[serde(rename = "_MIN_SEQUENCE_NUMBER")]
pub min_sequence_number: i64,
#[serde(rename = "_MAX_SEQUENCE_NUMBER")]
pub max_sequence_number: i64,
#[serde(rename = "_SCHEMA_ID")]
pub schema_id: i64,
#[serde(rename = "_LEVEL")]
pub level: i32,
#[serde(rename = "_EXTRA_FILES")]
pub extra_files: Vec<String>,
#[serde(
rename = "_CREATION_TIME",
serialize_with = "to_millis_opt",
deserialize_with = "from_millis_opt",
default
)]
pub creation_time: Option<DateTime<Utc>>,
#[serde(rename = "_DELETE_ROW_COUNT")]
// rowCount = add_row_count + delete_row_count.
pub delete_row_count: Option<i64>,
// file index filter bytes, if it is small, store in data file meta
#[serde(rename = "_EMBEDDED_FILE_INDEX", with = "serde_bytes")]
pub embedded_index: Option<Vec<u8>>,
/// File source identifier (e.g. APPEND, COMPACT).
#[serde(
rename = "_FILE_SOURCE",
default,
skip_serializing_if = "Option::is_none"
)]
pub file_source: Option<i32>,
/// Column names covered by `_VALUE_STATS` when stats are written in dense mode.
#[serde(
rename = "_VALUE_STATS_COLS",
default,
skip_serializing_if = "Option::is_none"
)]
pub value_stats_cols: Option<Vec<String>>,
/// External path for the data file (e.g. when data is stored outside the table directory).
#[serde(
rename = "_EXTERNAL_PATH",
default,
skip_serializing_if = "Option::is_none"
)]
pub external_path: Option<String>,
/// The starting row ID for this file's data (used in data evolution mode).
#[serde(
rename = "_FIRST_ROW_ID",
default,
skip_serializing_if = "Option::is_none"
)]
pub first_row_id: Option<i64>,
/// Which table columns this file contains (used in data evolution mode).
#[serde(
rename = "_WRITE_COLS",
default,
skip_serializing_if = "Option::is_none"
)]
pub write_cols: Option<Vec<String>>,
}
impl Display for DataFileMeta {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"DataFileMeta{{fileName={}, fileSize={}, rowCount={}, embeddedIndex={:?}, \
minKey={:?}, maxKey={:?}, keyStats={:?}, valueStats={:?}, \
minSequenceNumber={}, maxSequenceNumber={}, schemaId={}, level={}, \
extraFiles={:?}, creationTime={:?}, deleteRowCount={:?}, fileSource={:?}, \
valueStatsCols={:?}, externalPath={:?}, firstRowId={:?}, writeCols={:?}}}",
self.file_name,
self.file_size,
self.row_count,
self.embedded_index,
self.min_key,
self.max_key,
self.key_stats,
self.value_stats,
self.min_sequence_number,
self.max_sequence_number,
self.schema_id,
self.level,
self.extra_files,
self.creation_time,
self.delete_row_count,
self.file_source,
self.value_stats_cols,
self.external_path,
self.first_row_id,
self.write_cols,
)
}
}
impl DataFileMeta {
/// Returns the row ID range `[first_row_id, first_row_id + row_count - 1]` if `first_row_id` is set.
pub fn row_id_range(&self) -> Option<(i64, i64)> {
self.first_row_id.map(|fid| (fid, fid + self.row_count - 1))
}
/// Full path for this data file.
///
/// Mirrors Java `DataFilePathFactory#toPath(DataFileMeta)`: use
/// `_EXTERNAL_PATH` when present, otherwise align the file name under the
/// split/bucket path.
pub fn data_file_path(&self, bucket_path: &str) -> String {
self.external_path
.clone()
.unwrap_or_else(|| join_path(bucket_path, &self.file_name))
}
/// Parent directory of `_EXTERNAL_PATH`, if present.
///
/// Mirrors Java `DataFileMeta#externalPathDir`.
pub fn external_path_dir(&self) -> Option<&str> {
self.external_path.as_deref().and_then(parent_path)
}
/// Full path for an extra file whose location should be aligned with this data file.
///
/// Mirrors Java `DataFilePathFactory#toAlignedPath`: sidecars live beside
/// the external data file when `_EXTERNAL_PATH` is present, otherwise under
/// the data file's bucket path.
pub fn aligned_file_path(&self, bucket_path: &str, file_name: &str) -> String {
let parent = self.external_path_dir().unwrap_or(bucket_path);
join_path(parent, file_name)
}
/// File name for the default independent file-index sidecar of this data file.
///
/// Mirrors Java `DataFilePathFactory#dataFileToFileIndexPath`, which appends
/// `.index` to the full data-file name.
pub fn data_file_index_file_name(&self) -> String {
data_file_to_file_index_file_name(&self.file_name)
}
/// Full path for the default independent file-index sidecar of this data file.
pub fn data_file_index_path(&self, bucket_path: &str) -> String {
self.aligned_file_path(bucket_path, &self.data_file_index_file_name())
}
/// Data file plus all extra files as physical paths.
///
/// Mirrors Java `DataFileMeta#collectFiles`.
pub fn collect_files(&self, bucket_path: &str) -> Vec<String> {
let mut files = Vec::with_capacity(1 + self.extra_files.len());
files.push(self.data_file_path(bucket_path));
files.extend(
self.extra_files
.iter()
.map(|extra| self.aligned_file_path(bucket_path, extra)),
);
files
}
}
pub fn data_file_to_file_index_file_name(file_name: &str) -> String {
format!("{file_name}{DATA_FILE_INDEX_SUFFIX}")
}
fn join_path(parent: &str, file_name: &str) -> String {
let parent = parent.trim_end_matches('/');
if parent.is_empty() {
file_name.to_string()
} else if parent == "/" {
format!("/{file_name}")
} else {
format!("{parent}/{file_name}")
}
}
fn parent_path(path: &str) -> Option<&str> {
let trimmed = path.trim_end_matches('/');
let idx = trimmed.rfind('/')?;
if idx == 0 {
Some("/")
} else {
Some(&trimmed[..idx])
}
}
#[cfg(test)]
mod tests {
use super::*;
fn data_file(file_name: &str) -> DataFileMeta {
let stats = BinaryTableStats::empty();
DataFileMeta {
file_name: file_name.to_string(),
file_size: 42,
row_count: 7,
min_key: vec![1],
max_key: vec![9],
key_stats: stats.clone(),
value_stats: stats,
min_sequence_number: 3,
max_sequence_number: 5,
schema_id: 11,
level: 2,
extra_files: Vec::new(),
creation_time: DateTime::from_timestamp_millis(1_234),
delete_row_count: Some(1),
embedded_index: Some(vec![4, 5]),
file_source: Some(6),
value_stats_cols: Some(vec!["v".to_string()]),
external_path: None,
first_row_id: Some(100),
write_cols: Some(vec!["k".to_string(), "v".to_string()]),
}
}
#[test]
fn display_includes_all_data_file_meta_fields() {
let file = DataFileMeta {
extra_files: vec!["extra-1".to_string()],
external_path: Some("s3://bucket/data-1.parquet".to_string()),
..data_file("data-1.parquet")
};
let display = file.to_string();
for expected in [
"DataFileMeta{fileName=data-1.parquet",
"fileSize=42",
"rowCount=7",
"embeddedIndex=Some([4, 5])",
"minKey=[1]",
"maxKey=[9]",
"keyStats=BinaryTableStats",
"valueStats=BinaryTableStats",
"minSequenceNumber=3",
"maxSequenceNumber=5",
"schemaId=11",
"level=2",
"extraFiles=[\"extra-1\"]",
"creationTime=Some(1970-01-01T00:00:01.234Z)",
"deleteRowCount=Some(1)",
"fileSource=Some(6)",
"valueStatsCols=Some([\"v\"])",
"externalPath=Some(\"s3://bucket/data-1.parquet\")",
"firstRowId=Some(100)",
"writeCols=Some([\"k\", \"v\"])",
] {
assert!(
display.contains(expected),
"Display output missing `{expected}`: {display}"
);
}
}
#[test]
fn data_file_paths_align_to_bucket_path() {
let mut file = data_file("data-0.row");
file.extra_files = vec!["data-0.row.index".to_string(), "data-0.row.dv".to_string()];
assert_eq!(
file.data_file_path("s3://warehouse/table/bucket-0"),
"s3://warehouse/table/bucket-0/data-0.row"
);
assert_eq!(
file.aligned_file_path("s3://warehouse/table/bucket-0/", "data-0.row.index"),
"s3://warehouse/table/bucket-0/data-0.row.index"
);
assert_eq!(file.data_file_index_file_name(), "data-0.row.index");
assert_eq!(
file.data_file_index_path("s3://warehouse/table/bucket-0"),
"s3://warehouse/table/bucket-0/data-0.row.index"
);
assert_eq!(
file.collect_files("s3://warehouse/table/bucket-0"),
vec![
"s3://warehouse/table/bucket-0/data-0.row".to_string(),
"s3://warehouse/table/bucket-0/data-0.row.index".to_string(),
"s3://warehouse/table/bucket-0/data-0.row.dv".to_string(),
]
);
}
#[test]
fn data_file_paths_align_extra_files_to_external_path_parent() {
let mut file = data_file("data-0.row");
file.external_path = Some("s3://external/table/data-0.row".to_string());
file.extra_files = vec!["data-0.row.index".to_string()];
assert_eq!(
file.data_file_path("s3://warehouse/table/bucket-0"),
"s3://external/table/data-0.row"
);
assert_eq!(file.external_path_dir(), Some("s3://external/table"));
assert_eq!(
file.data_file_index_path("s3://warehouse/table/bucket-0"),
"s3://external/table/data-0.row.index"
);
assert_eq!(
file.collect_files("s3://warehouse/table/bucket-0"),
vec![
"s3://external/table/data-0.row".to_string(),
"s3://external/table/data-0.row.index".to_string(),
]
);
}
#[test]
fn data_file_to_file_index_file_name_appends_java_suffix() {
assert_eq!(
data_file_to_file_index_file_name("part-0.row"),
"part-0.row.index"
);
}
}