Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e7d100e
[fix](be) Preserve row groups when null count is missing
Gabriel39 Jul 12, 2026
f3e6323
[fix](be) Prevent DST timestamp statistics mispruning
Gabriel39 Jul 12, 2026
47c1be2
[fix](be) guard timestamp pruning across DST rollback
Gabriel39 Jul 12, 2026
3249db1
[fix](be) Scope Parquet bloom cache to row groups
Gabriel39 Jul 12, 2026
d26baa2
[fix](be) Ignore NaN Parquet min max statistics
Gabriel39 Jul 12, 2026
b5a75ac
[fix](be) Keep page index pruning after NaN statistics
Gabriel39 Jul 12, 2026
6f4be22
[fix](be) Preserve null pruning with invalid Parquet bounds
Gabriel39 Jul 12, 2026
1488f8f
[fix](be) Skip unsafe predicates in partition pruning
Gabriel39 Jul 10, 2026
2a9a7ba
[fix](be) Preserve predicate order in split pruning
Gabriel39 Jul 11, 2026
947744f
[fix](be) Reject lossy file predicate literal conversion
Gabriel39 Jul 12, 2026
407c385
[fix](be) Restrict file literal localization to lossless casts
Gabriel39 Jul 12, 2026
4a6bfaa
[fix](be) Disable Parquet binary min max pushdown
Gabriel39 Jul 12, 2026
bbddc01
[fix](be) Preserve conservative split pruning
Gabriel39 Jul 12, 2026
3bdcb98
[fix](be) Reject unsafe file statistics and pushdown
Gabriel39 Jul 12, 2026
f5da27f
update
Gabriel39 Jul 12, 2026
108e69b
[fix](be) Keep VARBINARY filters above file readers
Gabriel39 Jul 13, 2026
92e77db
[fix](be) Refresh V2 file predicates for each split
Gabriel39 Jul 12, 2026
10bd67e
[fix](be) Bound nested Parquet skip memory
Gabriel39 Jul 12, 2026
bf482f1
[fix](be) Guard metadata count against pending filters
Gabriel39 Jul 12, 2026
679a397
[improvement](be) Avoid materializing skipped nested values
Gabriel39 Jul 12, 2026
888dc35
[fix](be) Reset binary builders after nested level reads
Gabriel39 Jul 13, 2026
0c0fb39
[fix](be) Preserve direct MINMAX pushdown after projection
Gabriel39 Jul 13, 2026
db14c3e
[fix](be) Address external reader review gaps
Gabriel39 Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions be/src/exec/scan/file_scanner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,11 @@ void FileScanner::_init_runtime_filter_partition_prune_ctxs() {
auto impl = conjunct->root()->get_impl();
// If impl is not null, which means this a conjuncts from runtime filter.
auto expr = impl ? impl : conjunct->root();
// Preserve a safe prefix of the row-level conjunct order. Considering later predicates
// after an unsafe one could prune the split before the unsafe predicate is evaluated.
if (!expr->is_safe_to_execute_on_selected_rows()) {
break;
}
if (_check_partition_prune_expr(expr)) {
_runtime_filter_partition_prune_ctxs.emplace_back(conjunct);
}
Expand Down
13 changes: 13 additions & 0 deletions be/src/exec/scan/file_scanner.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ class FileScanner : public Scanner {
static const std::string FileReadBytesProfile;
static const std::string FileReadTimeProfile;

#ifdef BE_TEST
void TEST_init_runtime_filter_partition_prune_ctxs(
const VExprContextSPtrs& conjuncts,
const std::unordered_map<SlotId, int>& partition_slot_index_map) {
_conjuncts = conjuncts;
_partition_slot_index_map = partition_slot_index_map;
_init_runtime_filter_partition_prune_ctxs();
}
const VExprContextSPtrs& TEST_runtime_filter_partition_prune_ctxs() const {
return _runtime_filter_partition_prune_ctxs;
}
#endif

FileScanner(RuntimeState* state, FileScanLocalState* parent, int64_t limit,
std::shared_ptr<SplitSourceConnector> split_source, RuntimeProfile* profile,
ShardedKVCache* kv_cache,
Expand Down
6 changes: 6 additions & 0 deletions be/src/exec/scan/file_scanner_v2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -492,13 +492,19 @@ Status FileScannerV2::_prepare_table_reader_split(const TFileRangeDesc& range,
std::map<std::string, Field> partition_values) {
format::FileFormat current_split_format;
RETURN_IF_ERROR(_to_file_format(get_range_format_type(*_params, range), &current_split_format));
VExprContextSPtrs conjuncts;
RETURN_IF_ERROR(_build_table_conjuncts(&conjuncts));
VExprContextSPtrs partition_prune_conjuncts;
if (_state->query_options().enable_runtime_filter_partition_prune) {
RETURN_IF_ERROR(_build_table_conjuncts(&partition_prune_conjuncts));
}
RETURN_IF_ERROR(_table_reader->prepare_split({
.partition_values = std::move(partition_values),
.conjuncts = std::move(conjuncts),
.partition_prune_conjuncts = std::move(partition_prune_conjuncts),
// A metadata COUNT split may span scheduler turns. Do not enter that irreversible
// synthetic-row path while a runtime filter can still arrive between batches.
.all_runtime_filters_applied = _applied_rf_num == _total_rf_num,
.cache = _kv_cache,
.current_range = range,
.current_split_format = current_split_format,
Expand Down
151 changes: 145 additions & 6 deletions be/src/format_v2/column_mapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <algorithm>
#include <cstddef>
#include <memory>
#include <optional>
#include <sstream>
#include <string_view>
#include <utility>
Expand Down Expand Up @@ -530,6 +531,71 @@ static void collect_top_level_slot_columns(const VExprSPtr& expr,
}
}

static std::optional<uint8_t> signed_integer_width(PrimitiveType type) {
switch (type) {
case TYPE_TINYINT:
return 8;
case TYPE_SMALLINT:
return 16;
case TYPE_INT:
return 32;
case TYPE_BIGINT:
return 64;
case TYPE_LARGEINT:
return 128;
default:
return std::nullopt;
}
}

static std::optional<uint8_t> floating_width(PrimitiveType type) {
switch (type) {
case TYPE_FLOAT:
return 32;
case TYPE_DOUBLE:
return 64;
default:
return std::nullopt;
}
}

static std::optional<uint8_t> floating_exact_integer_width(PrimitiveType type) {
switch (type) {
case TYPE_FLOAT:
return 24;
case TYPE_DOUBLE:
return 53;
default:
return std::nullopt;
}
}

static bool is_lossless_file_to_table_numeric_cast(const DataTypePtr& file_type,
const DataTypePtr& table_type) {
const auto file_nested_type = remove_nullable(file_type);
const auto table_nested_type = remove_nullable(table_type);
if (file_nested_type->equals(*table_nested_type)) {
return true;
}

const auto file_primitive_type = file_nested_type->get_primitive_type();
const auto table_primitive_type = table_nested_type->get_primitive_type();
if (const auto file_width = signed_integer_width(file_primitive_type)) {
if (const auto table_width = signed_integer_width(table_primitive_type)) {
return *table_width >= *file_width;
}
if (const auto table_width = floating_exact_integer_width(table_primitive_type)) {
return *table_width >= *file_width;
}
return false;
}
if (const auto file_width = floating_width(file_primitive_type)) {
const auto table_width = floating_width(table_primitive_type);
return table_width.has_value() && *table_width >= *file_width;
}
return false;
}

static VExprSPtr rewrite_literal_to_file_type(const VExprSPtr& literal_expr,
const FileSlotRewriteInfo& rewrite_info,
RewriteContext* rewrite_context) {
Expand All @@ -540,6 +606,16 @@ static VExprSPtr rewrite_literal_to_file_type(const VExprSPtr& literal_expr,
if (rewrite_info.file_type->equals(*original_literal->data_type())) {
return original_literal;
}
// A literal round trip alone cannot prove that file-local evaluation is safe: the file slot
// itself may lose information when materialized as the table type. For example, DOUBLE 1.5
// becomes BIGINT 1, so table predicate `value = 1` is true while file predicate
// `value = 1.0` is false. Complex Field equality also does not compare nested contents.
// Restrict localization to scalar numeric casts that preserve every file value; unsupported
// and complex casts keep the table predicate and evaluate after materialization.
if (!is_lossless_file_to_table_numeric_cast(rewrite_info.file_type,
Comment thread
Gabriel39 marked this conversation as resolved.
original_literal->data_type())) {
return nullptr;
}
Field file_field;
try {
convert_field_to_type(original_field, *rewrite_info.file_type, &file_field,
Expand All @@ -553,6 +629,18 @@ static VExprSPtr rewrite_literal_to_file_type(const VExprSPtr& literal_expr,
if (file_field.get_type() != remove_nullable(rewrite_info.file_type)->get_primitive_type()) {
return nullptr;
}
Field round_trip_field;
try {
convert_field_to_type(file_field, *original_literal->data_type(), &round_trip_field,
rewrite_info.file_type.get());
} catch (const Exception&) {
return nullptr;
}
// The file-to-table type check protects every possible file value. This round trip separately
// proves that the specific predicate boundary is exactly representable in the file type.
if (round_trip_field != original_field) {
return nullptr;
}
auto literal = std::make_shared<SplitLocalFileLiteral>(
rewrite_info.file_type, file_field, original_literal->data_type(), original_field);
rewrite_context->add_created_expr(literal);
Expand Down Expand Up @@ -990,6 +1078,61 @@ static bool mapping_can_use_file_column_directly(const ColumnMapping& mapping) {
return !needs_complex_rematerialize(mapping);
}

static bool type_contains_varbinary(const DataTypePtr& type) {
DORIS_CHECK(type != nullptr);
const auto nested_type = remove_nullable(type);
switch (nested_type->get_primitive_type()) {
case TYPE_VARBINARY:
return true;
case TYPE_ARRAY:
return type_contains_varbinary(
assert_cast<const DataTypeArray&>(*nested_type).get_nested_type());
case TYPE_MAP: {
const auto& map_type = assert_cast<const DataTypeMap&>(*nested_type);
return type_contains_varbinary(map_type.get_key_type()) ||
type_contains_varbinary(map_type.get_value_type());
}
case TYPE_STRUCT:
return std::ranges::any_of(
assert_cast<const DataTypeStruct&>(*nested_type).get_elements(),
[](const DataTypePtr& child_type) { return type_contains_varbinary(child_type); });
default:
return false;
}
}

static FilterConversionType direct_filter_conversion(const ColumnMapping& mapping) {
DORIS_CHECK(mapping.table_type != nullptr);
DORIS_CHECK(mapping.file_type != nullptr);
// FileScanOperator deliberately keeps VARBINARY predicates above external readers. Their
// physical binary representations are not uniformly supported by reader-side expression and
// metadata filtering, so localizing a late runtime filter here can incorrectly reject rows.
// Apply the same rule to a complex root because generic array/map/struct expressions rewrite
// the root slot and can otherwise expose a nested VARBINARY child to the reader.
if (type_contains_varbinary(mapping.table_type)) {
return FilterConversionType::FINALIZE_ONLY;
}
const auto table_type = remove_nullable(mapping.table_type);
const auto file_type = remove_nullable(mapping.file_type);
// TIMESTAMPTZ scale mismatch is intentionally materialized as pass-through: a SQL cast rounds
// fractional seconds. A file-local cast would therefore filter different instants from the
// scanner-level predicate evaluated on the pass-through value.
if (table_type->get_primitive_type() == TYPE_TIMESTAMPTZ &&
file_type->get_primitive_type() == TYPE_TIMESTAMPTZ &&
!mapping.table_type->equals(*mapping.file_type)) {
return FilterConversionType::FINALIZE_ONLY;
}
return mapping.is_trivial ? FilterConversionType::COPY_DIRECTLY
: FilterConversionType::CAST_FILTER;
}

static FilterConversionType projected_filter_conversion(const ColumnMapping& mapping) {
const auto conversion = direct_filter_conversion(mapping);
return !mapping.is_trivial && conversion != FilterConversionType::FINALIZE_ONLY
? FilterConversionType::READER_EXPRESSION
: conversion;
}

static const ColumnDefinition* find_file_child_for_mapping(const ColumnDefinition& table_child,
const ColumnDefinition& file_parent,
TableColumnMappingMode mode,
Expand Down Expand Up @@ -1870,8 +2013,7 @@ Status TableColumnMapper::_create_direct_mapping(const ColumnDefinition& table_c
mapping->projected_file_children = file_field.children;
mapping->file_type = file_field.type;
mapping->is_trivial = mapping_can_use_file_column_directly(*mapping);
mapping->filter_conversion = mapping->is_trivial ? FilterConversionType::COPY_DIRECTLY
: FilterConversionType::CAST_FILTER;
mapping->filter_conversion = direct_filter_conversion(*mapping);
mapping->child_mappings.clear();

auto table_children = table_column.children;
Expand Down Expand Up @@ -1955,10 +2097,7 @@ Status TableColumnMapper::_create_direct_mapping(const ColumnDefinition& table_c
&mapping->projected_file_children, &mapping->file_type));
DCHECK(mapping->table_type != nullptr);
mapping->is_trivial = mapping_can_use_file_column_directly(*mapping);
// TODO: ? READER_EXPRESSION
mapping->filter_conversion = mapping->is_trivial
? FilterConversionType::COPY_DIRECTLY
: FilterConversionType::READER_EXPRESSION;
mapping->filter_conversion = projected_filter_conversion(*mapping);
}
}
return Status::OK();
Expand Down
15 changes: 7 additions & 8 deletions be/src/format_v2/jni/jni_table_reader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,6 @@ namespace doris::format {
Status JniTableReader::init(TableReadOptions&& options) {
RETURN_IF_ERROR(TableReader::init(std::move(options)));
_init_profile();

// JNI readers do not go through TableReader::open_reader(), where file-local filters are
// prepared for file readers. They execute table-level conjuncts directly on the JNI block.
RowDescriptor row_desc;
for (const auto& conjunct : _conjuncts) {
RETURN_IF_ERROR(conjunct->prepare(_runtime_state, row_desc));
RETURN_IF_ERROR(conjunct->open(_runtime_state));
}
return Status::OK();
}

Expand All @@ -55,6 +47,13 @@ Status JniTableReader::prepare_split(const SplitReadOptions& options) {
if (_is_table_level_count_active()) {
return Status::OK();
}
// JNI readers do not go through TableReader::open_reader(), where native readers prepare
// file-local filters. Prepare the fresh per-split snapshot before it filters JNI blocks.
RowDescriptor row_desc;
for (const auto& conjunct : _conjuncts) {
Comment thread
Gabriel39 marked this conversation as resolved.
RETURN_IF_ERROR(conjunct->prepare(_runtime_state, row_desc));
RETURN_IF_ERROR(conjunct->open(_runtime_state));
}
// Subclasses populate split-specific scanner params before calling this method, so the Java
// scanner can be opened here instead of being lazily opened by the first get_block() call.
return _open_jni_scanner();
Expand Down
Loading
Loading