use crate::io::FileIO;
use crate::spec::{batch_to_serialized_bytes, DataField, IndexFileMeta, MergeEngine};
use crate::table::bucket_assigner::{BatchAssignOutput, BucketAssigner, PartitionBucketKey};
use crate::table::Table;
use crate::Result;
use arrow_array::RecordBatch;
use futures::TryStreamExt;
use std::collections::{HashMap, HashSet};
enum AssignResult {
SamePartition { bucket: i32 },
CrossPartition {
old_partition: Vec<u8>,
old_bucket: i32,
new_bucket: i32,
},
Skip,
}
struct GlobalPartitionIndex {
key_to_location: HashMap<Vec<u8>, (Vec<u8>, i32)>,
partition_non_full_buckets: HashMap<Vec<u8>, HashMap<i32, i64>>,
partition_all_buckets: HashMap<Vec<u8>, HashSet<i32>>,
partition_next_bucket_id: HashMap<Vec<u8>, i32>,
bucket_row_counts: HashMap<(Vec<u8>, i32), i64>,
target_bucket_row_number: i64,
merge_engine: MergeEngine,
}
impl GlobalPartitionIndex {
async fn load_from_data_scan(
table: &Table,
primary_key_indices: &[usize],
target_bucket_row_number: i64,
merge_engine: MergeEngine,
) -> Result<Self> {
let mut key_to_location: HashMap<Vec<u8>, (Vec<u8>, i32)> = HashMap::new();
let mut bucket_row_counts: HashMap<(Vec<u8>, i32), i64> = HashMap::new();
let fields = table.schema().fields();
let pk_field_names: Vec<&str> = primary_key_indices
.iter()
.map(|&idx| fields[idx].name())
.collect();
let pk_fields: Vec<DataField> = primary_key_indices
.iter()
.map(|&idx| fields[idx].clone())
.collect();
let projected_pk_indices: Vec<usize> = (0..pk_fields.len()).collect();
let mut rb = table.new_read_builder();
rb.with_projection(&pk_field_names);
let scan = rb.new_scan().with_scan_all_files();
let plan = scan.plan().await?;
let read = rb.new_read()?;
for split in plan.splits() {
let partition_bytes = split.partition().to_serialized_bytes();
let bucket = split.bucket();
let batches: Vec<RecordBatch> = read
.to_arrow(std::slice::from_ref(split))?
.try_collect()
.await?;
let pb_key = (partition_bytes.clone(), bucket);
for batch in &batches {
let pk_bytes_vec =
batch_to_serialized_bytes(batch, &projected_pk_indices, &pk_fields)?;
let count = pk_bytes_vec.len() as i64;
for pk_bytes in pk_bytes_vec {
key_to_location.insert(pk_bytes, (partition_bytes.clone(), bucket));
}
*bucket_row_counts.entry(pb_key.clone()).or_insert(0) += count;
}
}
let mut partition_all_buckets: HashMap<Vec<u8>, HashSet<i32>> = HashMap::new();
let mut partition_non_full_buckets: HashMap<Vec<u8>, HashMap<i32, i64>> = HashMap::new();
for ((partition, bucket), count) in &bucket_row_counts {
partition_all_buckets
.entry(partition.clone())
.or_default()
.insert(*bucket);
if *count < target_bucket_row_number {
partition_non_full_buckets
.entry(partition.clone())
.or_default()
.insert(*bucket, *count);
}
}
let partition_next_bucket_id: HashMap<Vec<u8>, i32> = partition_all_buckets
.iter()
.map(|(p, buckets)| {
let next = buckets.iter().copied().max().map_or(0, |m| m + 1);
(p.clone(), next)
})
.collect();
Ok(Self {
key_to_location,
partition_non_full_buckets,
partition_all_buckets,
partition_next_bucket_id,
bucket_row_counts,
target_bucket_row_number,
merge_engine,
})
}
fn assign(&mut self, pk_bytes: &[u8], new_partition: &[u8]) -> Result<AssignResult> {
if let Some((existing_partition, existing_bucket)) = self.key_to_location.get(pk_bytes) {
if existing_partition == new_partition {
return Ok(AssignResult::SamePartition {
bucket: *existing_bucket,
});
}
match self.merge_engine {
MergeEngine::FirstRow => {
return Ok(AssignResult::Skip);
}
MergeEngine::Deduplicate => {
let old_partition = existing_partition.clone();
let old_bucket = *existing_bucket;
let old_key = (old_partition.clone(), old_bucket);
let old_count = self.bucket_row_counts.get(&old_key).copied().unwrap_or(0);
let new_count = (old_count - 1).max(0);
self.bucket_row_counts.insert(old_key, new_count);
if new_count < self.target_bucket_row_number {
self.partition_non_full_buckets
.entry(old_partition.clone())
.or_default()
.insert(old_bucket, new_count);
}
let new_bucket = self.assign_bucket_in_partition(new_partition);
self.key_to_location
.insert(pk_bytes.to_vec(), (new_partition.to_vec(), new_bucket));
return Ok(AssignResult::CrossPartition {
old_partition,
old_bucket,
new_bucket,
});
}
MergeEngine::PartialUpdate => {
return Err(crate::Error::Unsupported {
message: "CrossPartitionAssigner does not support merge-engine=partial-update yet".to_string(),
});
}
}
}
let bucket = self.assign_bucket_in_partition(new_partition);
self.key_to_location
.insert(pk_bytes.to_vec(), (new_partition.to_vec(), bucket));
Ok(AssignResult::SamePartition { bucket })
}
fn assign_bucket_in_partition(&mut self, partition: &[u8]) -> i32 {
let non_full = self
.partition_non_full_buckets
.entry(partition.to_vec())
.or_default();
let mut full_buckets = Vec::new();
let mut assigned_bucket = None;
for (&bucket, count) in non_full.iter_mut() {
if *count < self.target_bucket_row_number {
*count += 1;
assigned_bucket = Some(bucket);
break;
} else {
full_buckets.push(bucket);
}
}
for b in full_buckets {
non_full.remove(&b);
}
let bucket = if let Some(b) = assigned_bucket {
b
} else {
let all = self
.partition_all_buckets
.entry(partition.to_vec())
.or_default();
let next_id = self
.partition_next_bucket_id
.entry(partition.to_vec())
.or_insert(0);
let new_bucket = *next_id;
*next_id += 1;
all.insert(new_bucket);
self.partition_non_full_buckets
.entry(partition.to_vec())
.or_default()
.insert(new_bucket, 1);
new_bucket
};
*self
.bucket_row_counts
.entry((partition.to_vec(), bucket))
.or_insert(0) += 1;
bucket
}
}
pub(crate) struct CrossPartitionAssigner {
table: Table,
partition_field_indices: Vec<usize>,
primary_key_indices: Vec<usize>,
global_partition_index: Option<GlobalPartitionIndex>,
target_bucket_row_number: i64,
merge_engine: MergeEngine,
}
impl CrossPartitionAssigner {
pub fn new(
table: Table,
partition_field_indices: Vec<usize>,
primary_key_indices: Vec<usize>,
target_bucket_row_number: i64,
merge_engine: MergeEngine,
) -> Self {
Self {
table,
partition_field_indices,
primary_key_indices,
global_partition_index: None,
target_bucket_row_number,
merge_engine,
}
}
}
impl BucketAssigner for CrossPartitionAssigner {
async fn assign_batch(
&mut self,
batch: &RecordBatch,
fields: &[DataField],
) -> Result<BatchAssignOutput> {
if self.global_partition_index.is_none() {
let index = GlobalPartitionIndex::load_from_data_scan(
&self.table,
&self.primary_key_indices,
self.target_bucket_row_number,
self.merge_engine,
)
.await?;
self.global_partition_index = Some(index);
}
let partition_bytes_vec =
batch_to_serialized_bytes(batch, &self.partition_field_indices, fields)?;
let pk_bytes_vec = batch_to_serialized_bytes(batch, &self.primary_key_indices, fields)?;
let global_index = self.global_partition_index.as_mut().unwrap();
let num_rows = batch.num_rows();
let mut buckets = Vec::with_capacity(num_rows);
let mut deletes = Vec::new();
let mut skips = Vec::new();
for row_idx in 0..num_rows {
match global_index.assign(&pk_bytes_vec[row_idx], &partition_bytes_vec[row_idx])? {
AssignResult::SamePartition { bucket } => {
buckets.push(bucket);
}
AssignResult::CrossPartition {
old_partition,
old_bucket,
new_bucket,
} => {
buckets.push(new_bucket);
deletes.push((row_idx, old_partition, old_bucket));
}
AssignResult::Skip => {
buckets.push(-1); skips.push(row_idx);
}
}
}
Ok(BatchAssignOutput {
partition_bytes: partition_bytes_vec,
buckets,
deletes,
skips,
})
}
async fn prepare_commit_index(
&mut self,
_file_io: &FileIO,
_index_dir: &str,
) -> Result<HashMap<PartitionBucketKey, Vec<IndexFileMeta>>> {
Ok(HashMap::new())
}
}