blob: 6f71455c4640c98adc45bf3f4a7b9885469939fc [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 super::data_evolution_reader::DataEvolutionReader;
use super::data_file_reader::DataFileReader;
use super::kv_file_reader::{KeyValueFileReader, KeyValueReadConfig};
use super::read_builder::split_scan_predicates;
use super::{ArrowRecordBatchStream, Table};
use crate::arrow::filtering::reader_pruning_predicates;
use crate::spec::{CoreOptions, DataField, MergeEngine, Predicate};
use crate::DataSplit;
/// Table read: reads data from splits (e.g. produced by [TableScan::plan]).
///
/// Reference: [pypaimon.read.table_read.TableRead](https://github.com/apache/paimon/blob/master/paimon-python/pypaimon/read/table_read.py)
#[derive(Debug, Clone)]
pub struct TableRead<'a> {
table: &'a Table,
read_type: Vec<DataField>,
data_predicates: Vec<Predicate>,
}
impl<'a> TableRead<'a> {
/// Create a new TableRead with a specific read type (projected fields).
pub fn new(
table: &'a Table,
read_type: Vec<DataField>,
data_predicates: Vec<Predicate>,
) -> Self {
Self {
table,
read_type,
data_predicates,
}
}
/// Schema (fields) that this read will produce.
pub fn read_type(&self) -> &[DataField] {
&self.read_type
}
/// Data predicates for read-side pruning.
pub fn data_predicates(&self) -> &[Predicate] {
&self.data_predicates
}
/// Table for this read.
pub fn table(&self) -> &Table {
self.table
}
/// Set a filter predicate for conservative read-side pruning.
pub fn with_filter(mut self, filter: Predicate) -> Self {
let (_, data_predicates) = split_scan_predicates(self.table, filter);
self.data_predicates = reader_pruning_predicates(data_predicates);
self
}
/// Returns an [`ArrowRecordBatchStream`].
pub fn to_arrow(&self, data_splits: &[DataSplit]) -> crate::Result<ArrowRecordBatchStream> {
let has_primary_keys = !self.table.schema.primary_keys().is_empty();
let core_options = CoreOptions::new(self.table.schema.options());
let merge_engine = core_options.merge_engine()?;
// PK table with Deduplicate engine: splits that may hold multiple
// versions of a key need KeyValueFileReader for sort-merge dedup;
// splits marked raw convertible by scan planning — and all compacted
// files of deletion-vector tables, where DVs mask stale versions —
// use the faster DataFileReader.
// PartialUpdate / Aggregation always go through KeyValueFileReader so
// that per-key materialization can run on the read side.
if has_primary_keys
&& matches!(
merge_engine,
MergeEngine::Deduplicate | MergeEngine::PartialUpdate | MergeEngine::Aggregation
)
{
return self.read_pk(data_splits, &core_options);
}
if core_options.data_evolution_enabled() {
self.read_with_evolution(data_splits, &core_options)
} else {
self.read_raw(data_splits)
}
}
/// Read PK table. For `Deduplicate`, splits marked raw convertible by scan
/// planning (mirrors Java `DataSplit#convertToRawFiles`) use the faster
/// DataFileReader; the rest go through KeyValueFileReader for sort-merge
/// dedup. Deletion-vector tables are exempt: their stale versions are
/// masked by DVs, and KeyValueFileReader does not support DVs, so they keep
/// the plain level-0 dispatch. `PartialUpdate` and `Aggregation` always go
/// through KeyValueFileReader because their merge semantics require per-key
/// materialization even for compacted runs.
fn read_pk(
&self,
data_splits: &[DataSplit],
core_options: &CoreOptions,
) -> crate::Result<ArrowRecordBatchStream> {
if matches!(
core_options.merge_engine()?,
MergeEngine::PartialUpdate | MergeEngine::Aggregation
) {
return self.read_kv(data_splits, core_options);
}
// Deletion-vector tables read raw by design: stale versions of a key
// are masked by DVs, not merged, and KeyValueFileReader does not
// support DVs. Keep the plain level-0 dispatch for them.
let dv_enabled = core_options.deletion_vectors_enabled();
let mut kv_splits = Vec::new();
let mut raw_splits = Vec::new();
for split in data_splits {
if pk_split_needs_merge(split, dv_enabled) {
kv_splits.push(split.clone());
} else {
raw_splits.push(split.clone());
}
}
if raw_splits.is_empty() {
return self.read_kv(&kv_splits, core_options);
}
if kv_splits.is_empty() {
return self.read_raw(&raw_splits);
}
let kv_stream = self.read_kv(&kv_splits, core_options)?;
let raw_stream = self.read_raw(&raw_splits)?;
Ok(Box::pin(futures::stream::select_all([
kv_stream, raw_stream,
])))
}
/// Read splits via KeyValueFileReader (sort-merge dedup).
fn read_kv(
&self,
splits: &[DataSplit],
core_options: &CoreOptions,
) -> crate::Result<ArrowRecordBatchStream> {
let reader = KeyValueFileReader::new(
self.table.file_io.clone(),
KeyValueReadConfig {
table_name: self.table.identifier().full_name(),
table_options: self.table.schema().options().clone(),
schema_manager: self.table.schema_manager().clone(),
table_schema_id: self.table.schema().id(),
table_fields: self.table.schema.fields().to_vec(),
read_type: self.read_type().to_vec(),
predicates: self.data_predicates.clone(),
primary_keys: self.table.schema.trimmed_primary_keys(),
merge_engine: core_options.merge_engine()?,
sequence_fields: core_options
.sequence_fields()
.iter()
.map(|s| s.to_string())
.collect(),
},
);
reader.read(splits)
}
/// Read with data-evolution support.
fn read_with_evolution(
&self,
data_splits: &[DataSplit],
core_options: &CoreOptions,
) -> crate::Result<ArrowRecordBatchStream> {
let reader = DataEvolutionReader::new(
self.table.file_io.clone(),
self.table.schema_manager().clone(),
self.table.schema().id(),
self.table.schema.fields().to_vec(),
self.read_type().to_vec(),
core_options.blob_as_descriptor(),
core_options.blob_descriptor_fields(),
)?;
reader.read(data_splits)
}
/// Read raw data files without dedup or evolution.
fn read_raw(&self, data_splits: &[DataSplit]) -> crate::Result<ArrowRecordBatchStream> {
self.new_data_file_reader().read(data_splits)
}
fn new_data_file_reader(&self) -> DataFileReader {
DataFileReader::new(
self.table.file_io.clone(),
self.table.schema_manager().clone(),
self.table.schema().id(),
self.table.schema.fields().to_vec(),
self.read_type().to_vec(),
self.data_predicates.clone(),
)
}
}
/// Whether a primary-key split must go through the sort-merge reader.
///
/// Mirrors Java `PrimaryKeyTableRawFileSplitReadProvider#match`: a raw read
/// needs the split marked raw convertible AND a known `delete_row_count` on
/// every file. Legacy files without the stat may hide delete rows — scan
/// planning treats the missing stat as "no deletes" for compatibility, so the
/// read side must fall back to the merge reader, which drops them.
///
/// Deletion-vector tables keep the plain level-0 dispatch: stale versions are
/// masked by DVs and KeyValueFileReader does not support DVs.
fn pk_split_needs_merge(split: &DataSplit, dv_enabled: bool) -> bool {
if dv_enabled {
return split.data_files().iter().any(|f| f.level == 0);
}
!split.raw_convertible()
|| split
.data_files()
.iter()
.any(|f| f.delete_row_count.is_none())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec::stats::BinaryTableStats;
use crate::spec::{BinaryRow, DataFileMeta};
use crate::table::source::DataSplitBuilder;
fn file(name: &str, level: i32, delete_row_count: Option<i64>) -> DataFileMeta {
DataFileMeta {
file_name: name.to_string(),
file_size: 128,
row_count: 10,
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: 0,
max_sequence_number: 0,
schema_id: 0,
level,
extra_files: Vec::new(),
creation_time: None,
delete_row_count,
embedded_index: None,
first_row_id: None,
write_cols: None,
external_path: None,
file_source: None,
value_stats_cols: None,
}
}
fn split(files: Vec<DataFileMeta>, raw_convertible: bool) -> DataSplit {
DataSplitBuilder::new()
.with_snapshot(1)
.with_partition(BinaryRow::new(0))
.with_bucket(0)
.with_bucket_path("file:/tmp/bucket-0".to_string())
.with_total_buckets(1)
.with_data_files(files)
.with_raw_convertible(raw_convertible)
.build()
.unwrap()
}
#[test]
fn test_pk_split_needs_merge_routing() {
// Raw convertible with known delete counts: raw read.
let raw = split(vec![file("a", 5, Some(0))], true);
assert!(!pk_split_needs_merge(&raw, false));
// Not raw convertible: merge read.
let merge = split(vec![file("a", 5, Some(0))], false);
assert!(pk_split_needs_merge(&merge, false));
// Raw convertible but a legacy file lacks delete_row_count: the file
// may hide delete rows, so it must go through the merge reader.
let legacy = split(vec![file("a", 5, None)], true);
assert!(pk_split_needs_merge(&legacy, false));
// Deletion-vector tables dispatch on level 0 only.
let dv_l0 = split(vec![file("a", 0, None)], false);
assert!(pk_split_needs_merge(&dv_l0, true));
let dv_compacted = split(vec![file("a", 5, None)], false);
assert!(!pk_split_needs_merge(&dv_compacted, true));
}
}