blob: 6d3e4e5ad6b78f6614b6bdc0d7ff02eb6db1a159 [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.
//! Key-value file reader for primary-key tables using sort-merge with LoserTree.
//!
//! Each data file in a split is read as a separate sorted stream. The streams
//! are merged by primary key using a LoserTree, and rows with the same key are
//! deduplicated by keeping the one with the highest `_SEQUENCE_NUMBER`.
//!
//! Reference: Java Paimon `SortMergeReaderWithMinHeap`.
use super::data_file_reader::DataFileReader;
use super::sort_merge::{
AggregateMergeFunction, DeduplicateMergeFunction, PartialUpdateMergeFunction,
SortMergeReaderBuilder,
};
use crate::arrow::build_target_arrow_schema;
use crate::io::FileIO;
use crate::spec::{
BigIntType, DataField, DataType as PaimonDataType, MergeEngine, Predicate, TinyIntType,
SEQUENCE_NUMBER_FIELD_ID, SEQUENCE_NUMBER_FIELD_NAME, VALUE_KIND_FIELD_ID,
VALUE_KIND_FIELD_NAME,
};
use crate::table::schema_manager::SchemaManager;
use crate::table::ArrowRecordBatchStream;
use crate::{DataSplit, Error};
use arrow_array::{RecordBatch, RecordBatchOptions};
use async_stream::try_stream;
use futures::StreamExt;
use std::collections::HashMap;
/// Reads primary-key table data files using sort-merge deduplication.
pub(crate) struct KeyValueFileReader {
file_io: FileIO,
config: KeyValueReadConfig,
}
/// Configuration for [`KeyValueFileReader`], grouping table schema and
/// key/predicate parameters.
pub(crate) struct KeyValueReadConfig {
pub table_name: String,
pub table_options: HashMap<String, String>,
pub schema_manager: SchemaManager,
pub table_schema_id: i64,
pub table_fields: Vec<DataField>,
pub read_type: Vec<DataField>,
pub predicates: Vec<Predicate>,
pub primary_keys: Vec<String>,
pub merge_engine: MergeEngine,
pub sequence_fields: Vec<String>,
}
impl KeyValueFileReader {
pub(crate) fn new(file_io: FileIO, config: KeyValueReadConfig) -> Self {
// Only keep predicates that reference primary key columns.
// Non-PK predicates applied before merge can cause incorrect results.
// Use project_field_index_inclusive: AND keeps PK children, OR requires all PK.
let pk_set: std::collections::HashSet<&str> =
config.primary_keys.iter().map(|s| s.as_str()).collect();
let mapping: Vec<Option<usize>> = config
.table_fields
.iter()
.enumerate()
.map(|(i, f)| {
if pk_set.contains(f.name()) {
Some(i)
} else {
None
}
})
.collect();
let pk_predicates = config
.predicates
.into_iter()
.filter_map(|p| p.project_field_index_inclusive(&mapping))
.collect();
Self {
file_io,
config: KeyValueReadConfig {
predicates: pk_predicates,
..config
},
}
}
fn new_merge_function(
merge_engine: MergeEngine,
table_options: &HashMap<String, String>,
table_name: &str,
merge_output_fields: &[DataField],
primary_keys: &[String],
sequence_fields: &[String],
) -> crate::Result<Box<dyn super::sort_merge::MergeFunction>> {
match merge_engine {
MergeEngine::Deduplicate => Ok(Box::new(DeduplicateMergeFunction)),
MergeEngine::PartialUpdate => Ok(Box::new(PartialUpdateMergeFunction::new(
table_options,
table_name,
)?)),
MergeEngine::FirstRow => Err(Error::Unsupported {
message: "KeyValueFileReader does not support merge-engine=first-row; first-row reads should use the non-KV path".to_string(),
}),
MergeEngine::Aggregation => Ok(Box::new(AggregateMergeFunction::new(
table_options,
table_name,
merge_output_fields,
primary_keys,
sequence_fields,
)?)),
}
}
pub fn read(self, data_splits: &[DataSplit]) -> crate::Result<ArrowRecordBatchStream> {
// Build the internal read type for thin-mode files.
// Physical file schema: [_SEQUENCE_NUMBER, _VALUE_KIND, all_user_cols...]
// We need: _SEQ + _VK + union(read_type, primary_keys)
let seq_field = DataField::new(
SEQUENCE_NUMBER_FIELD_ID,
SEQUENCE_NUMBER_FIELD_NAME.to_string(),
PaimonDataType::BigInt(BigIntType::new()),
);
let value_kind_field = DataField::new(
VALUE_KIND_FIELD_ID,
VALUE_KIND_FIELD_NAME.to_string(),
PaimonDataType::TinyInt(TinyIntType::new()),
);
let key_names: std::collections::HashSet<&str> = self
.config
.primary_keys
.iter()
.map(|s| s.as_str())
.collect();
// Collect key fields from table schema.
let key_fields: Vec<DataField> = self
.config
.primary_keys
.iter()
.map(|pk| {
self.config
.table_fields
.iter()
.find(|f| f.name() == pk)
.cloned()
.ok_or_else(|| Error::UnexpectedError {
message: format!("Primary key column '{pk}' not found in table schema"),
source: None,
})
})
.collect::<crate::Result<Vec<_>>>()?;
// User columns = read_type fields + any key fields not already in read_type
// + any sequence fields not already included.
let read_type_names: std::collections::HashSet<&str> =
self.config.read_type.iter().map(|f| f.name()).collect();
let mut user_fields: Vec<DataField> = self.config.read_type.clone();
for kf in &key_fields {
if !read_type_names.contains(kf.name()) {
user_fields.push(kf.clone());
}
}
// Add sequence fields if not already present.
for sf_name in &self.config.sequence_fields {
if user_fields.iter().all(|f| f.name() != sf_name.as_str()) {
let sf = self
.config
.table_fields
.iter()
.find(|f| f.name() == sf_name.as_str())
.cloned()
.ok_or_else(|| Error::UnexpectedError {
message: format!("Sequence field '{sf_name}' not found in table schema"),
source: None,
})?;
user_fields.push(sf);
}
}
// Internal read type: [_SEQ, _VK, user_fields...]
let mut internal_read_type: Vec<DataField> = Vec::new();
internal_read_type.push(seq_field);
internal_read_type.push(value_kind_field);
internal_read_type.extend(user_fields.clone());
let internal_schema = build_target_arrow_schema(&internal_read_type)?;
// Output schema: user's read_type order
let output_schema = build_target_arrow_schema(&self.config.read_type)?;
// Indices within internal_schema (offset 2 for _SEQ and _VK).
let seq_index = 0;
let value_kind_index = 1;
let key_indices: Vec<usize> = self
.config
.primary_keys
.iter()
.map(|pk| {
user_fields
.iter()
.position(|f| f.name() == pk)
.map(|p| p + 2)
.unwrap()
})
.collect();
let value_fields: Vec<DataField> = user_fields
.iter()
.filter(|f| !key_names.contains(f.name()))
.cloned()
.collect();
let value_indices: Vec<usize> = user_fields
.iter()
.enumerate()
.filter(|(_, f)| !key_names.contains(f.name()))
.map(|(i, _)| i + 2)
.collect();
// If sequence.field is configured, find each field's index in the internal schema.
let user_sequence_indices: Vec<usize> = self
.config
.sequence_fields
.iter()
.filter_map(|sf| {
user_fields
.iter()
.position(|f| f.name() == sf.as_str())
.map(|p| p + 2)
})
.collect();
// Build the reorder mapping: merge output is [keys..., values...],
// but user wants them in read_type order.
let num_keys = key_fields.len();
let mut reorder_map: Vec<usize> = vec![0; self.config.read_type.len()];
for (out_idx, field) in self.config.read_type.iter().enumerate() {
if key_names.contains(field.name()) {
// Find position in key_fields
let key_pos = key_fields
.iter()
.position(|kf| kf.name() == field.name())
.unwrap();
reorder_map[out_idx] = key_pos;
} else {
// Find position in value_fields
let val_pos = value_fields
.iter()
.position(|vf| vf.name() == field.name())
.unwrap();
reorder_map[out_idx] = num_keys + val_pos;
}
}
let splits: Vec<DataSplit> = data_splits.to_vec();
let file_io = self.file_io;
let merge_engine = self.config.merge_engine;
let schema_manager = self.config.schema_manager;
let table_schema_id = self.config.table_schema_id;
let table_fields = self.config.table_fields;
let table_name = self.config.table_name;
let table_options = self.config.table_options;
let predicates = self.config.predicates;
let primary_keys = self.config.primary_keys;
let sequence_fields = self.config.sequence_fields;
// Build the merge output schema (keys + values, no system columns).
let mut merge_output_fields: Vec<DataField> = Vec::new();
merge_output_fields.extend(key_fields);
merge_output_fields.extend(value_fields);
let merge_output_schema = build_target_arrow_schema(&merge_output_fields)?;
Ok(try_stream! {
for split in &splits {
// DV mode should not reach KeyValueFileReader.
if split
.data_deletion_files()
.is_some_and(|files| files.iter().any(Option::is_some))
{
Err(Error::Unsupported {
message: "KeyValueFileReader does not support deletion vectors".to_string(),
})?;
}
// Create one stream per data file.
let mut file_streams: Vec<ArrowRecordBatchStream> = Vec::new();
for file_meta in split.data_files().to_vec() {
let data_fields: Option<Vec<DataField>> = if file_meta.schema_id != table_schema_id {
let data_schema = schema_manager.schema(file_meta.schema_id).await?;
Some(data_schema.fields().to_vec())
} else {
None
};
let reader = DataFileReader::new(
file_io.clone(),
schema_manager.clone(),
table_schema_id,
table_fields.clone(),
internal_read_type.clone(),
predicates.clone(),
);
let stream = reader.read_single_file_stream(
split,
file_meta,
data_fields,
None,
None,
)?;
file_streams.push(stream);
}
if file_streams.is_empty() {
continue;
}
// Always go through sort-merge even for a single file: files
// written before the writer merged key groups at flush may
// still contain duplicate keys.
let mut merge_stream = SortMergeReaderBuilder::new(
file_streams,
internal_schema.clone(),
key_indices.clone(),
seq_index,
value_kind_index,
user_sequence_indices.clone(),
value_indices.clone(),
merge_output_schema.clone(),
Self::new_merge_function(
merge_engine,
&table_options,
&table_name,
&merge_output_fields,
&primary_keys,
&sequence_fields,
)?,
)
.build()?;
while let Some(batch) = merge_stream.next().await {
let batch = batch?;
// Reorder columns from [keys..., values...] to read_type order.
let columns: Vec<_> = reorder_map
.iter()
.map(|&src| batch.column(src).clone())
.collect();
// An explicit row count keeps empty projections working
// (e.g. COUNT(*) reads no columns).
let options =
RecordBatchOptions::new().with_row_count(Some(batch.num_rows()));
let reordered =
RecordBatch::try_new_with_options(output_schema.clone(), columns, &options)
.map_err(|e| Error::UnexpectedError {
message: format!("Failed to reorder merged RecordBatch: {e}"),
source: Some(Box::new(e)),
})?;
yield reordered;
}
}
}
.boxed())
}
}