blob: b4dc178a4cce7304cbe2704c2deb0c74540aea11 [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.
//! Bin packing utilities for splitting data files into reasonably-sized groups.
//!
//! Reference: [BinPacking](https://github.com/apache/paimon/blob/release-1.3/paimon-common/src/main/java/org/apache/paimon/utils/BinPacking.java)
use crate::spec::DataFileMeta;
use std::cmp;
/// Greedy order-preserving bin packing.
///
/// Groups items into bins where each bin's total weight does not exceed `target_weight`.
/// Items are never reordered across bins. A single item larger than `target_weight`
/// will become its own bin.
///
/// Reference: [BinPacking.packForOrdered](https://github.com/apache/paimon/blob/release-1.3/paimon-common/src/main/java/org/apache/paimon/utils/BinPacking.java)
pub fn pack_for_ordered<T>(
items: Vec<T>,
weight_func: impl Fn(&T) -> i64,
target_weight: i64,
) -> Vec<Vec<T>> {
let mut packed: Vec<Vec<T>> = Vec::new();
let mut bin_items: Vec<T> = Vec::new();
let mut bin_weight: i64 = 0;
for item in items {
let weight = weight_func(&item);
if bin_weight + weight > target_weight && !bin_items.is_empty() {
packed.push(bin_items);
bin_items = Vec::new();
bin_weight = 0;
}
bin_weight += weight;
bin_items.push(item);
}
if !bin_items.is_empty() {
packed.push(bin_items);
}
packed
}
/// Split data files for batch reading using bin packing.
///
/// Files are sorted by `min_sequence_number`, then packed into groups where each group's
/// total weight (using `max(file_size, open_file_cost)` as per-file weight) does not
/// exceed `target_split_size`.
///
/// Reference: [AppendOnlySplitGenerator.splitForBatch](https://github.com/apache/paimon/blob/master/paimon-core/src/main/java/org/apache/paimon/table/source/AppendOnlySplitGenerator.java)
pub fn split_for_batch(
mut files: Vec<DataFileMeta>,
target_split_size: i64,
open_file_cost: i64,
) -> Vec<Vec<DataFileMeta>> {
files.sort_by_key(|f| f.min_sequence_number);
let weight_func = move |f: &DataFileMeta| cmp::max(f.file_size, open_file_cost);
pack_for_ordered(files, weight_func, target_split_size)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec::stats::BinaryTableStats;
use chrono::{DateTime, Utc};
fn make_file(file_name: &str, file_size: i64, min_seq: i64, max_seq: i64) -> DataFileMeta {
DataFileMeta {
file_name: file_name.to_string(),
file_size,
row_count: 100,
min_key: Vec::new(),
max_key: Vec::new(),
key_stats: BinaryTableStats::new(Vec::new(), Vec::new(), Vec::new()),
value_stats: BinaryTableStats::new(Vec::new(), Vec::new(), Vec::new()),
min_sequence_number: min_seq,
max_sequence_number: max_seq,
schema_id: 0,
level: 0,
extra_files: Vec::new(),
creation_time: DateTime::<Utc>::from_timestamp(0, 0),
delete_row_count: None,
embedded_index: None,
first_row_id: None,
write_cols: None,
external_path: None,
file_source: None,
value_stats_cols: None,
}
}
#[test]
fn test_pack_for_ordered_empty() {
let result: Vec<Vec<i32>> = pack_for_ordered(vec![], |_| 1, 10);
assert!(result.is_empty());
}
#[test]
fn test_pack_for_ordered_single_oversized() {
let result = pack_for_ordered(vec![100], |x| *x, 10);
assert_eq!(result, vec![vec![100]]);
}
#[test]
fn test_pack_for_ordered_all_fit_in_one_bin() {
let result = pack_for_ordered(vec![1, 2, 3], |x| *x, 100);
assert_eq!(result, vec![vec![1, 2, 3]]);
}
/// Matches Java SplitGeneratorTest with targetSplitSize=40, openFileCost=2.
#[test]
fn test_split_for_batch_low_open_cost() {
let files = vec![
make_file("1", 11, 0, 20),
make_file("2", 13, 21, 30),
make_file("3", 46, 31, 40),
make_file("4", 23, 41, 50),
make_file("5", 4, 51, 60),
make_file("6", 101, 61, 100),
];
let groups = split_for_batch(files, 40, 2);
let names: Vec<Vec<&str>> = groups
.iter()
.map(|g| g.iter().map(|f| f.file_name.as_str()).collect())
.collect();
assert_eq!(
names,
vec![vec!["1", "2"], vec!["3"], vec!["4", "5"], vec!["6"]]
);
}
/// Matches Java SplitGeneratorTest with targetSplitSize=40, openFileCost=20.
#[test]
fn test_split_for_batch_high_open_cost() {
let files = vec![
make_file("1", 11, 0, 20),
make_file("2", 13, 21, 30),
make_file("3", 46, 31, 40),
make_file("4", 23, 41, 50),
make_file("5", 4, 51, 60),
make_file("6", 101, 61, 100),
];
let groups = split_for_batch(files, 40, 20);
let names: Vec<Vec<&str>> = groups
.iter()
.map(|g| g.iter().map(|f| f.file_name.as_str()).collect())
.collect();
assert_eq!(
names,
vec![vec!["1", "2"], vec!["3"], vec!["4"], vec!["5"], vec!["6"]]
);
}
/// Verify files are sorted by min_sequence_number before packing.
#[test]
fn test_split_for_batch_sorts_by_sequence() {
let files = vec![
make_file("c", 10, 30, 40),
make_file("a", 10, 10, 20),
make_file("b", 10, 20, 30),
];
let groups = split_for_batch(files, 1000, 0);
assert_eq!(groups.len(), 1);
let names: Vec<&str> = groups[0].iter().map(|f| f.file_name.as_str()).collect();
assert_eq!(names, vec!["a", "b", "c"]);
}
#[test]
fn test_split_for_batch_empty() {
let groups = split_for_batch(vec![], 128, 4);
assert!(groups.is_empty());
}
}