blob: d009699035793533b878507f25465f8f7771c353 [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.
#include "format_v2/table_reader.h"
#include <arrow/api.h>
#include <arrow/io/api.h>
#include <gtest/gtest.h>
#include <parquet/api/reader.h>
#include <parquet/arrow/writer.h>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <memory>
#include <optional>
#include <string>
#include <typeinfo>
#include <vector>
#include "common/consts.h"
#include "core/assert_cast.h"
#include "core/block/block.h"
#include "core/column/column_array.h"
#include "core/column/column_const.h"
#include "core/column/column_map.h"
#include "core/column/column_nullable.h"
#include "core/column/column_string.h"
#include "core/column/column_struct.h"
#include "core/column/column_vector.h"
#include "core/data_type/data_type_array.h"
#include "core/data_type/data_type_map.h"
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_number.h"
#include "core/data_type/data_type_string.h"
#include "core/data_type/data_type_struct.h"
#include "exprs/runtime_filter_expr.h"
#include "exprs/vectorized_fn_call.h"
#include "exprs/vexpr.h"
#include "exprs/vliteral.h"
#include "exprs/vslot_ref.h"
#include "gen_cpp/Exprs_types.h"
#include "gen_cpp/ExternalTableSchema_types.h"
#include "gen_cpp/PlanNodes_types.h"
#include "io/io_common.h"
#include "runtime/runtime_profile.h"
#include "runtime/runtime_state.h"
#include "storage/segment/condition_cache.h"
namespace doris::format {
namespace {
std::vector<int32_t> projection_ids(const std::vector<LocalColumnIndex>& projections) {
std::vector<int32_t> ids;
ids.reserve(projections.size());
for (const auto& projection : projections) {
ids.push_back(projection.index);
}
return ids;
}
TEST(LocalColumnIndexTest, MergeUnionsPartialChildrenAndFullProjectionDominates) {
LocalColumnIndex target {.index = 10, .project_all_children = false};
target.children.push_back({.index = 1});
target.children.push_back({.index = 2, .project_all_children = false});
target.children.back().children.push_back({.index = 20});
LocalColumnIndex source {.index = 10, .project_all_children = false};
source.children.push_back({.index = 2, .project_all_children = false});
source.children.back().children.push_back({.index = 21});
source.children.push_back({.index = 3});
ASSERT_TRUE(merge_local_column_index(&target, source).ok());
ASSERT_FALSE(target.project_all_children);
ASSERT_EQ(std::vector<int32_t>({1, 2, 3}), projection_ids(target.children));
ASSERT_FALSE(target.children[1].project_all_children);
ASSERT_EQ(std::vector<int32_t>({20, 21}), projection_ids(target.children[1].children));
ASSERT_TRUE(target.children[2].project_all_children);
LocalColumnIndex full_source {.index = 10};
ASSERT_TRUE(merge_local_column_index(&target, full_source).ok());
ASSERT_TRUE(target.project_all_children);
ASSERT_TRUE(target.children.empty());
}
TEST(LocalColumnIndexTest, FindsProjectedChildren) {
LocalColumnIndex projection {.index = 10, .project_all_children = false};
projection.children.push_back({.index = 1});
projection.children.push_back({.index = 2});
EXPECT_TRUE(is_full_projection(nullptr));
EXPECT_FALSE(is_full_projection(&projection));
EXPECT_TRUE(is_partial_projection(&projection));
ASSERT_NE(find_child_projection(&projection, 2), nullptr);
EXPECT_EQ(find_child_projection(&projection, 2)->local_id(), 2);
EXPECT_EQ(find_child_projection(&projection, 3), nullptr);
EXPECT_TRUE(is_child_projected(nullptr, 3));
EXPECT_TRUE(is_child_projected(&projection, 1));
EXPECT_FALSE(is_child_projected(&projection, 3));
}
TEST(LocalColumnIndexTest, ProjectColumnDefinitionMatchesChildrenByLocalId) {
auto int_type = std::make_shared<DataTypeInt32>();
auto string_type = std::make_shared<DataTypeString>();
ColumnDefinition field;
field.identifier = Field::create_field<TYPE_INT>(5);
field.name = "root";
field.type =
std::make_shared<DataTypeStruct>(DataTypes {int_type, string_type}, Strings {"a", "b"});
ColumnDefinition a_child;
a_child.identifier = Field::create_field<TYPE_INT>(10);
a_child.local_id = 0;
a_child.name = "a";
a_child.type = int_type;
ColumnDefinition b_child;
b_child.identifier = Field::create_field<TYPE_INT>(20);
b_child.local_id = 1;
b_child.name = "b";
b_child.type = string_type;
field.children = {
a_child,
b_child,
};
LocalColumnIndex projection {.index = 5, .project_all_children = false};
projection.children.push_back({.index = 1});
ColumnDefinition projected_field;
ASSERT_TRUE(project_column_definition(field, projection, &projected_field).ok());
ASSERT_EQ(projected_field.children.size(), 1);
EXPECT_EQ(projected_field.children[0].get_identifier_field_id(), 20);
EXPECT_EQ(projected_field.children[0].name, "b");
const auto* projected_type =
assert_cast<const DataTypeStruct*>(remove_nullable(projected_field.type).get());
ASSERT_EQ(projected_type->get_elements().size(), 1);
EXPECT_EQ(projected_type->get_element_name(0), "b");
EXPECT_TRUE(projected_type->get_element(0)->equals(*string_type));
}
TEST(LocalColumnIndexTest, ProjectColumnDefinitionKeepsFileChildOrder) {
auto int_type = std::make_shared<DataTypeInt32>();
auto string_type = std::make_shared<DataTypeString>();
ColumnDefinition a_child;
a_child.identifier = Field::create_field<TYPE_INT>(10);
a_child.local_id = 0;
a_child.name = "a";
a_child.type = int_type;
ColumnDefinition b_child;
b_child.identifier = Field::create_field<TYPE_INT>(20);
b_child.local_id = 1;
b_child.name = "b";
b_child.type = string_type;
ColumnDefinition field;
field.identifier = Field::create_field<TYPE_INT>(5);
field.name = "root";
field.type =
std::make_shared<DataTypeStruct>(DataTypes {int_type, string_type}, Strings {"a", "b"});
field.children = {a_child, b_child};
LocalColumnIndex projection {.index = 5, .project_all_children = false};
projection.children.push_back({.index = 1});
projection.children.push_back({.index = 0});
ColumnDefinition projected_field;
ASSERT_TRUE(project_column_definition(field, projection, &projected_field).ok());
ASSERT_EQ(projected_field.children.size(), 2);
EXPECT_EQ(projected_field.children[0].name, "a");
EXPECT_EQ(projected_field.children[1].name, "b");
const auto* projected_type =
assert_cast<const DataTypeStruct*>(remove_nullable(projected_field.type).get());
ASSERT_EQ(projected_type->get_elements().size(), 2);
EXPECT_EQ(projected_type->get_element_name(0), "a");
EXPECT_EQ(projected_type->get_element_name(1), "b");
}
VExprSPtr table_int32_slot_ref(int slot_id, int column_id, const std::string& column_name) {
const auto nullable_int_type = make_nullable(std::make_shared<DataTypeInt32>());
return VSlotRef::create_shared(slot_id, column_id, slot_id, nullable_int_type, column_name);
}
VExprSPtr table_int32_literal(int32_t value) {
return VLiteral::create_shared(std::make_shared<DataTypeInt32>(),
Field::create_field<TYPE_INT>(value));
}
TExprNode table_function_node(const std::string& function_name, const DataTypePtr& return_type,
const std::vector<DataTypePtr>& arg_types,
TExprNodeType::type node_type,
TExprOpcode::type opcode = TExprOpcode::INVALID_OPCODE,
bool short_circuit_evaluation = false) {
TFunctionName fn_name;
fn_name.__set_function_name(function_name);
TFunction fn;
fn.__set_name(fn_name);
fn.__set_binary_type(TFunctionBinaryType::BUILTIN);
std::vector<TTypeDesc> thrift_arg_types;
thrift_arg_types.reserve(arg_types.size());
for (const auto& arg_type : arg_types) {
thrift_arg_types.push_back(arg_type->to_thrift());
}
fn.__set_arg_types(thrift_arg_types);
fn.__set_ret_type(return_type->to_thrift());
fn.__set_has_var_args(false);
TExprNode node;
node.__set_node_type(node_type);
node.__set_opcode(opcode);
node.__set_type(return_type->to_thrift());
node.__set_fn(fn);
node.__set_num_children(static_cast<int16_t>(arg_types.size()));
node.__set_is_nullable(return_type->is_nullable());
if (short_circuit_evaluation) {
node.__set_short_circuit_evaluation(true);
}
return node;
}
VExprSPtr create_expr_from_node(const TExprNode& node) {
VExprSPtr expr;
auto status = VExpr::create_expr(node, expr);
DORIS_CHECK(status.ok()) << status.to_string();
return expr;
}
VExprSPtr table_function_expr(const std::string& function_name, const DataTypePtr& return_type,
const std::vector<DataTypePtr>& arg_types,
TExprNodeType::type node_type = TExprNodeType::FUNCTION_CALL,
TExprOpcode::type opcode = TExprOpcode::INVALID_OPCODE) {
const auto node = table_function_node(function_name, return_type, arg_types, node_type, opcode);
return VectorizedFnCall::create_shared(node);
}
VExprSPtr table_int32_greater_than_expr(int slot_id, int column_id, int32_t value) {
const auto int_type = std::make_shared<DataTypeInt32>();
const auto nullable_int_type = make_nullable(int_type);
auto expr = table_function_expr("gt", make_nullable(std::make_shared<DataTypeUInt8>()),
{nullable_int_type, int_type}, TExprNodeType::BINARY_PRED,
TExprOpcode::GT);
expr->add_child(table_int32_slot_ref(slot_id, column_id, "id"));
expr->add_child(table_int32_literal(value));
return expr;
}
VExprSPtr runtime_filter_wrapper_expr(VExprSPtr impl) {
TExprNode node;
node.__set_node_type(TExprNodeType::SLOT_REF);
node.__set_type(std::make_shared<DataTypeUInt8>()->to_thrift());
node.__set_num_children(1);
return RuntimeFilterExpr::create_shared(node, std::move(impl), 0, false, /*filter_id=*/1);
}
class NonDeterministicPartitionPredicate final : public VExpr {
public:
explicit NonDeterministicPartitionPredicate(bool* executed)
: VExpr(std::make_shared<DataTypeUInt8>(), false), _executed(executed) {}
Status execute_column_impl(VExprContext*, const Block*, const Selector*, size_t count,
ColumnPtr& result_column) const override {
DORIS_CHECK(_executed != nullptr);
*_executed = true;
auto result = ColumnUInt8::create();
result->get_data().resize_fill(count, 0);
result_column = std::move(result);
return Status::OK();
}
const std::string& expr_name() const override { return _expr_name; }
bool is_deterministic() const override { return false; }
Status clone_node(VExprSPtr* cloned_expr) const override {
DORIS_CHECK(cloned_expr != nullptr);
*cloned_expr = std::make_shared<NonDeterministicPartitionPredicate>(_executed);
return Status::OK();
}
private:
bool* const _executed;
const std::string _expr_name = "NonDeterministicPartitionPredicate";
};
class NullableArrayBigintDefaultExpr final : public VExpr {
public:
explicit NullableArrayBigintDefaultExpr(DataTypePtr data_type)
: _name("single_element_groups") {
_data_type = std::move(data_type);
}
const std::string& expr_name() const override { return _name; }
bool is_constant() const override { return false; }
Status execute_column_impl(VExprContext*, const Block*, const Selector* selector, size_t count,
ColumnPtr& result_column) const override {
DCHECK(selector == nullptr || selector->size() == count);
auto values = ColumnInt64::create();
auto offsets = ColumnArray::ColumnOffsets::create();
auto null_map = ColumnUInt8::create();
for (size_t i = 0; i < count; ++i) {
values->insert_value(7);
offsets->insert_value(static_cast<Int64>(i + 1));
null_map->insert_value(0);
}
auto array_column = ColumnArray::create(std::move(values), std::move(offsets));
result_column = ColumnNullable::create(std::move(array_column), std::move(null_map));
return Status::OK();
}
private:
std::string _name;
};
class TableReaderMaterializeTestHelper final : public TableReader {
public:
using TableReader::_materialize_map_mapping_column;
};
VExprSPtr table_int32_sum_expr(int left_slot_id, int left_column_id, int right_slot_id,
int right_column_id) {
const auto int_type = std::make_shared<DataTypeInt32>();
const auto nullable_int_type = make_nullable(int_type);
auto expr =
table_function_expr("add", nullable_int_type, {nullable_int_type, nullable_int_type});
expr->add_child(table_int32_slot_ref(left_slot_id, left_column_id, "id"));
expr->add_child(table_int32_slot_ref(right_slot_id, right_column_id, "score"));
return expr;
}
VExprSPtr table_int32_sum_greater_than_expr(int left_slot_id, int left_column_id, int right_slot_id,
int right_column_id, int32_t value) {
const auto int_type = std::make_shared<DataTypeInt32>();
const auto nullable_int_type = make_nullable(int_type);
auto expr = table_function_expr("gt", make_nullable(std::make_shared<DataTypeUInt8>()),
{nullable_int_type, int_type}, TExprNodeType::BINARY_PRED,
TExprOpcode::GT);
expr->add_child(
table_int32_sum_expr(left_slot_id, left_column_id, right_slot_id, right_column_id));
expr->add_child(table_int32_literal(value));
return expr;
}
VExprSPtr table_condition_function_expr(const std::string& function_name, bool short_circuit) {
const auto int_type = std::make_shared<DataTypeInt32>();
std::vector<DataTypePtr> arg_types;
if (function_name == "if") {
arg_types = {std::make_shared<DataTypeUInt8>(), int_type, int_type};
} else {
arg_types = {int_type, int_type};
}
auto expr = create_expr_from_node(
table_function_node(function_name, int_type, arg_types, TExprNodeType::FUNCTION_CALL,
TExprOpcode::INVALID_OPCODE, short_circuit));
if (function_name == "if") {
expr->add_child(table_int32_greater_than_expr(0, 0, 0));
expr->add_child(table_int32_literal(1));
expr->add_child(table_int32_literal(0));
} else {
expr->add_child(table_int32_slot_ref(0, 0, "id"));
expr->add_child(table_int32_literal(0));
}
return expr;
}
VExprSPtr table_case_expr(bool short_circuit) {
const auto int_type = std::make_shared<DataTypeInt32>();
TCaseExpr case_node;
case_node.__set_has_case_expr(false);
case_node.__set_has_else_expr(true);
TExprNode node;
node.__set_node_type(TExprNodeType::CASE_EXPR);
node.__set_type(int_type->to_thrift());
node.__set_is_nullable(false);
node.__set_num_children(3);
node.__set_case_expr(case_node);
if (short_circuit) {
node.__set_short_circuit_evaluation(true);
}
auto expr = create_expr_from_node(node);
expr->add_child(table_int32_greater_than_expr(0, 0, 0));
expr->add_child(table_int32_literal(1));
expr->add_child(table_int32_literal(0));
return expr;
}
TEST(CloneTableExprTreeTest, ClonesConditionalExpressions) {
const std::vector<VExprSPtr> expressions {
table_condition_function_expr("if", false),
table_condition_function_expr("if", true),
table_condition_function_expr("ifnull", false),
table_condition_function_expr("ifnull", true),
table_condition_function_expr("coalesce", false),
table_condition_function_expr("coalesce", true),
table_case_expr(false),
table_case_expr(true),
};
for (const auto& expr : expressions) {
VExprSPtr cloned;
const auto status = clone_table_expr_tree(expr, &cloned);
ASSERT_TRUE(status.ok()) << expr->debug_string() << ": " << status.to_string();
ASSERT_NE(cloned, nullptr);
const auto* original_expr = expr.get();
const auto* cloned_expr = cloned.get();
EXPECT_TRUE(typeid(*original_expr) == typeid(*cloned_expr))
<< expr->expr_name() << " cloned as " << typeid(*cloned_expr).name();
EXPECT_EQ(expr->expr_name(), cloned->expr_name());
EXPECT_EQ(expr->get_num_children(), cloned->get_num_children());
EXPECT_NE(original_expr, cloned_expr);
}
}
// Scenario: cloning a VectorizedFnCall whose return type is complex must not reconstruct the expr
// from TExprNode, because DataTypeFactory rejects nested types through the primitive-type path.
TEST(CloneTableExprTreeTest, ClonesVectorizedFnCallWithComplexReturnType) {
const auto int_type = std::make_shared<DataTypeInt32>();
const auto string_type = std::make_shared<DataTypeString>();
const auto struct_type =
std::make_shared<DataTypeStruct>(DataTypes {int_type, string_type}, Strings {"a", "b"});
const auto array_type = std::make_shared<DataTypeArray>(struct_type);
auto expr = table_function_expr("element_at", struct_type, {array_type, int_type});
expr->add_child(VSlotRef::create_shared(0, 0, -1, array_type, "array_of_struct"));
expr->add_child(table_int32_literal(1));
VExprSPtr cloned;
const auto status = clone_table_expr_tree(expr, &cloned);
ASSERT_TRUE(status.ok()) << status.to_string();
ASSERT_NE(cloned, nullptr);
EXPECT_EQ(cloned->expr_name(), expr->expr_name());
EXPECT_TRUE(cloned->data_type()->equals(*struct_type));
EXPECT_EQ(cloned->get_num_children(), 2);
EXPECT_NE(cloned.get(), expr.get());
}
std::shared_ptr<arrow::Array> finish_array(arrow::ArrayBuilder* builder) {
std::shared_ptr<arrow::Array> array;
EXPECT_TRUE(builder->Finish(&array).ok());
return array;
}
std::shared_ptr<arrow::Array> build_int32_array(const std::vector<int32_t>& values) {
arrow::Int32Builder builder;
for (const auto value : values) {
EXPECT_TRUE(builder.Append(value).ok());
}
return finish_array(&builder);
}
std::shared_ptr<arrow::Array> build_string_array(const std::vector<std::string>& values) {
arrow::StringBuilder builder;
for (const auto& value : values) {
EXPECT_TRUE(builder.Append(value).ok());
}
return finish_array(&builder);
}
void write_parquet_file(const std::string& file_path, int32_t id, const std::string& value) {
auto schema = arrow::schema({
arrow::field("id", arrow::int32(), false),
arrow::field("value", arrow::utf8(), false),
});
auto table = arrow::Table::Make(schema, {build_int32_array({id}), build_string_array({value})});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out, 1,
builder.build()));
}
void write_struct_parquet_file(const std::string& file_path, int32_t id) {
auto struct_type = arrow::struct_({arrow::field("id", arrow::int32(), false)});
arrow::StructBuilder builder(
struct_type, arrow::default_memory_pool(),
{std::make_shared<arrow::Int32Builder>(arrow::default_memory_pool())});
auto* id_builder = assert_cast<arrow::Int32Builder*>(builder.field_builder(0));
EXPECT_TRUE(builder.Append().ok());
EXPECT_TRUE(id_builder->Append(id).ok());
auto schema = arrow::schema({
arrow::field("s", struct_type, false),
});
auto table = arrow::Table::Make(schema, {finish_array(&builder)});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder writer_builder;
writer_builder.version(::parquet::ParquetVersion::PARQUET_2_6);
writer_builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
writer_builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out, 1,
writer_builder.build()));
}
void write_struct_parquet_file(const std::string& file_path, const std::vector<int32_t>& ids,
int64_t row_group_size = -1) {
auto struct_type = arrow::struct_({arrow::field("id", arrow::int32(), false)});
arrow::StructBuilder builder(
struct_type, arrow::default_memory_pool(),
{std::make_shared<arrow::Int32Builder>(arrow::default_memory_pool())});
auto* id_builder = assert_cast<arrow::Int32Builder*>(builder.field_builder(0));
for (const auto id : ids) {
EXPECT_TRUE(builder.Append().ok());
EXPECT_TRUE(id_builder->Append(id).ok());
}
auto schema = arrow::schema({
arrow::field("s", struct_type, false),
});
auto table = arrow::Table::Make(schema, {finish_array(&builder)});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder writer_builder;
writer_builder.version(::parquet::ParquetVersion::PARQUET_2_6);
writer_builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
writer_builder.compression(::parquet::Compression::UNCOMPRESSED);
const auto write_row_group_size =
row_group_size > 0 ? row_group_size : static_cast<int64_t>(ids.size());
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out,
write_row_group_size,
writer_builder.build()));
}
void write_struct_with_nullable_child_parquet_file(const std::string& file_path) {
auto struct_type = arrow::struct_({
arrow::field("id", arrow::int32(), false),
arrow::field("note", arrow::utf8(), true),
});
std::vector<std::shared_ptr<arrow::ArrayBuilder>> field_builders;
auto id_builder = std::make_unique<arrow::Int32Builder>();
field_builders.push_back(std::shared_ptr<arrow::ArrayBuilder>(std::move(id_builder)));
auto note_builder = std::make_unique<arrow::StringBuilder>();
field_builders.push_back(std::shared_ptr<arrow::ArrayBuilder>(std::move(note_builder)));
arrow::StructBuilder builder(struct_type, arrow::default_memory_pool(),
std::move(field_builders));
auto* struct_id_builder = assert_cast<arrow::Int32Builder*>(builder.field_builder(0));
auto* struct_note_builder = assert_cast<arrow::StringBuilder*>(builder.field_builder(1));
EXPECT_TRUE(builder.Append().ok());
EXPECT_TRUE(struct_id_builder->Append(7).ok());
EXPECT_TRUE(struct_note_builder->Append("seven").ok());
EXPECT_TRUE(builder.Append().ok());
EXPECT_TRUE(struct_id_builder->Append(8).ok());
EXPECT_TRUE(struct_note_builder->AppendNull().ok());
auto schema = arrow::schema({
arrow::field("s", struct_type, false),
});
auto table = arrow::Table::Make(schema, {finish_array(&builder)});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder writer_builder;
writer_builder.version(::parquet::ParquetVersion::PARQUET_2_6);
writer_builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
writer_builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out, 2,
writer_builder.build()));
}
void write_list_struct_parquet_file(const std::string& file_path) {
auto struct_type = arrow::struct_(
{arrow::field("a", arrow::int32(), false), arrow::field("b", arrow::int32(), false)});
std::vector<std::shared_ptr<arrow::ArrayBuilder>> field_builders;
auto a_array_builder = std::make_unique<arrow::Int32Builder>();
field_builders.push_back(std::shared_ptr<arrow::ArrayBuilder>(std::move(a_array_builder)));
auto b_array_builder = std::make_unique<arrow::Int32Builder>();
field_builders.push_back(std::shared_ptr<arrow::ArrayBuilder>(std::move(b_array_builder)));
auto struct_builder = std::make_shared<arrow::StructBuilder>(
struct_type, arrow::default_memory_pool(), std::move(field_builders));
auto list_type = arrow::list(arrow::field("element", struct_type, true));
arrow::ListBuilder builder(arrow::default_memory_pool(), struct_builder, list_type);
auto* a_builder = assert_cast<arrow::Int32Builder*>(struct_builder->field_builder(0));
auto* b_builder = assert_cast<arrow::Int32Builder*>(struct_builder->field_builder(1));
EXPECT_TRUE(builder.Append().ok());
EXPECT_TRUE(struct_builder->Append().ok());
EXPECT_TRUE(a_builder->Append(10).ok());
EXPECT_TRUE(b_builder->Append(11).ok());
EXPECT_TRUE(struct_builder->Append().ok());
EXPECT_TRUE(a_builder->Append(20).ok());
EXPECT_TRUE(b_builder->Append(21).ok());
EXPECT_TRUE(builder.Append().ok());
EXPECT_TRUE(struct_builder->Append().ok());
EXPECT_TRUE(a_builder->Append(30).ok());
EXPECT_TRUE(b_builder->Append(31).ok());
EXPECT_TRUE(builder.Append().ok());
EXPECT_TRUE(struct_builder->Append().ok());
EXPECT_TRUE(a_builder->Append(40).ok());
EXPECT_TRUE(b_builder->Append(41).ok());
auto schema = arrow::schema({
arrow::field("xs", list_type, false),
});
auto table = arrow::Table::Make(schema, {finish_array(&builder)});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder writer_builder;
writer_builder.version(::parquet::ParquetVersion::PARQUET_2_6);
writer_builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
writer_builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out, 3,
writer_builder.build()));
}
void write_map_struct_parquet_file(const std::string& file_path) {
auto key_builder = std::make_shared<arrow::Int32Builder>();
auto struct_type = arrow::struct_(
{arrow::field("a", arrow::int32(), false), arrow::field("b", arrow::utf8(), false)});
std::vector<std::shared_ptr<arrow::ArrayBuilder>> field_builders;
auto a_array_builder = std::make_unique<arrow::Int32Builder>();
field_builders.push_back(std::shared_ptr<arrow::ArrayBuilder>(std::move(a_array_builder)));
auto b_array_builder = std::make_unique<arrow::StringBuilder>();
field_builders.push_back(std::shared_ptr<arrow::ArrayBuilder>(std::move(b_array_builder)));
auto value_builder = std::make_shared<arrow::StructBuilder>(
struct_type, arrow::default_memory_pool(), std::move(field_builders));
auto map_type = arrow::map(arrow::int32(), arrow::field("value", struct_type, false));
arrow::MapBuilder builder(arrow::default_memory_pool(), key_builder, value_builder, map_type);
auto* a_builder = assert_cast<arrow::Int32Builder*>(value_builder->field_builder(0));
auto* b_builder = assert_cast<arrow::StringBuilder*>(value_builder->field_builder(1));
EXPECT_TRUE(builder.Append().ok());
EXPECT_TRUE(key_builder->Append(1).ok());
EXPECT_TRUE(value_builder->Append().ok());
EXPECT_TRUE(a_builder->Append(10).ok());
EXPECT_TRUE(b_builder->Append("ma").ok());
EXPECT_TRUE(key_builder->Append(2).ok());
EXPECT_TRUE(value_builder->Append().ok());
EXPECT_TRUE(a_builder->Append(20).ok());
EXPECT_TRUE(b_builder->Append("mb").ok());
EXPECT_TRUE(builder.Append().ok());
EXPECT_TRUE(key_builder->Append(3).ok());
EXPECT_TRUE(value_builder->Append().ok());
EXPECT_TRUE(a_builder->Append(30).ok());
EXPECT_TRUE(b_builder->Append("mc").ok());
EXPECT_TRUE(builder.AppendEmptyValue().ok());
auto schema = arrow::schema({
arrow::field("kv", map_type, false),
});
auto table = arrow::Table::Make(schema, {finish_array(&builder)});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder writer_builder;
writer_builder.version(::parquet::ParquetVersion::PARQUET_2_6);
writer_builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
writer_builder.compression(::parquet::Compression::UNCOMPRESSED);
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out, 3,
writer_builder.build()));
}
void write_int_pair_parquet_file(const std::string& file_path, const std::vector<int32_t>& ids,
const std::vector<int32_t>& scores,
const std::vector<std::string>& values,
int64_t row_group_size = -1) {
const auto id_metadata = arrow::key_value_metadata({"PARQUET:field_id"}, {"0"});
const auto score_metadata = arrow::key_value_metadata({"PARQUET:field_id"}, {"1"});
const auto value_metadata = arrow::key_value_metadata({"PARQUET:field_id"}, {"2"});
auto schema = arrow::schema({
arrow::field("id", arrow::int32(), false)->WithMetadata(id_metadata),
arrow::field("score", arrow::int32(), false)->WithMetadata(score_metadata),
arrow::field("value", arrow::utf8(), false)->WithMetadata(value_metadata),
});
auto table = arrow::Table::Make(schema, {build_int32_array(ids), build_int32_array(scores),
build_string_array(values)});
auto file_result = arrow::io::FileOutputStream::Open(file_path);
ASSERT_TRUE(file_result.ok()) << file_result.status();
std::shared_ptr<arrow::io::FileOutputStream> out = *file_result;
::parquet::WriterProperties::Builder builder;
builder.version(::parquet::ParquetVersion::PARQUET_2_6);
builder.data_page_version(::parquet::ParquetDataPageVersion::V2);
builder.compression(::parquet::Compression::UNCOMPRESSED);
const auto write_row_group_size =
row_group_size > 0 ? row_group_size : static_cast<int64_t>(ids.size());
PARQUET_THROW_NOT_OK(::parquet::arrow::WriteTable(*table, arrow::default_memory_pool(), out,
write_row_group_size, builder.build()));
}
Block build_table_block(const std::vector<ColumnDefinition>& columns) {
Block block;
for (const auto& column : columns) {
block.insert({column.type->create_column(), column.type, column.name});
}
return block;
}
const IColumn& expect_not_null_nullable_nested_column(const IColumn& column) {
if (!column.is_nullable()) {
return column;
}
const auto& nullable_column = assert_cast<const ColumnNullable&>(column);
for (const auto is_null : nullable_column.get_null_map_data()) {
EXPECT_EQ(is_null, 0);
}
return nullable_column.get_nested_column();
}
void expect_nullable_column_all_null(const IColumn& column) {
const auto full_column = column.convert_to_full_column_if_const();
const auto& nullable_column = assert_cast<const ColumnNullable&>(*full_column);
for (const auto is_null : nullable_column.get_null_map_data()) {
EXPECT_EQ(is_null, 1);
}
}
const IColumn& expect_not_null_table_column(const Block& block, size_t position) {
return expect_not_null_nullable_nested_column(*block.get_by_position(position).column);
}
ColumnDefinition make_table_column(int32_t id, const std::string& name, const DataTypePtr& type);
void expect_int32_column_values(const IColumn& column,
const std::vector<int32_t>& expected_values) {
const auto full_column = column.convert_to_full_column_if_const();
const auto& nested_column = expect_not_null_nullable_nested_column(*full_column);
const auto& values = assert_cast<const ColumnInt32&>(nested_column).get_data();
ASSERT_EQ(values.size(), expected_values.size());
for (size_t row = 0; row < expected_values.size(); ++row) {
EXPECT_EQ(values[row], expected_values[row]);
}
}
SplitReadOptions build_split_options(const std::string& file_path) {
SplitReadOptions options;
options.current_range.__set_path(file_path);
options.current_range.__set_file_size(
static_cast<int64_t>(std::filesystem::file_size(file_path)));
return options;
}
void set_table_level_row_count(SplitReadOptions* split_options, int64_t row_count) {
split_options->current_range.__isset.table_format_params = true;
split_options->current_range.table_format_params.__isset.table_level_row_count = true;
split_options->current_range.table_format_params.table_level_row_count = row_count;
}
int64_t parquet_column_start_offset(const ::parquet::ColumnChunkMetaData& column_metadata) {
return column_metadata.has_dictionary_page()
? static_cast<int64_t>(column_metadata.dictionary_page_offset())
: static_cast<int64_t>(column_metadata.data_page_offset());
}
SplitReadOptions build_split_options_for_row_group_mid(const std::string& file_path,
int row_group_idx) {
auto options = build_split_options(file_path);
auto reader = ::parquet::ParquetFileReader::OpenFile(file_path, false);
auto metadata = reader->metadata();
auto row_group_metadata = metadata->RowGroup(row_group_idx);
auto first_column = row_group_metadata->ColumnChunk(0);
auto last_column = row_group_metadata->ColumnChunk(row_group_metadata->num_columns() - 1);
const int64_t row_group_start_offset = parquet_column_start_offset(*first_column);
const int64_t row_group_end_offset =
parquet_column_start_offset(*last_column) + last_column->total_compressed_size();
const int64_t row_group_mid_offset =
row_group_start_offset + (row_group_end_offset - row_group_start_offset) / 2;
options.current_range.__set_start_offset(row_group_mid_offset);
options.current_range.__set_size(1);
return options;
}
DataTypePtr make_table_test_type(const DataTypePtr& type, bool nullable_root = true) {
DORIS_CHECK(type != nullptr);
const auto nested_type = remove_nullable(type);
DataTypePtr result;
if (const auto* struct_type = typeid_cast<const DataTypeStruct*>(nested_type.get())) {
DataTypes child_types;
child_types.reserve(struct_type->get_elements().size());
for (const auto& child_type : struct_type->get_elements()) {
child_types.push_back(make_table_test_type(child_type));
}
result = std::make_shared<DataTypeStruct>(child_types, struct_type->get_element_names());
} else if (const auto* array_type = typeid_cast<const DataTypeArray*>(nested_type.get())) {
result = std::make_shared<DataTypeArray>(
make_table_test_type(array_type->get_nested_type()));
} else if (const auto* map_type = typeid_cast<const DataTypeMap*>(nested_type.get())) {
result = std::make_shared<DataTypeMap>(make_table_test_type(map_type->get_key_type()),
make_table_test_type(map_type->get_value_type()));
} else {
result = nested_type;
}
return nullable_root ? make_nullable(result) : result;
}
ColumnDefinition make_table_column(int32_t id, const std::string& name, const DataTypePtr& type) {
ColumnDefinition column;
if (id >= 0) {
column.identifier = Field::create_field<TYPE_INT>(id);
}
column.name = name;
// TableReader tests model external table scan descriptors. Those table columns are nullable
// even when the Parquet file field itself is required, so keep the test schema aligned with
// the real scan contract at the construction boundary.
column.type = make_table_test_type(type);
return column;
}
ColumnDefinition make_file_column(int32_t id, const std::string& name, const DataTypePtr& type) {
ColumnDefinition field;
field.identifier = Field::create_field<TYPE_INT>(id);
field.local_id = id;
field.name = name;
field.type = make_table_test_type(type);
return field;
}
schema::external::TFieldPtr external_schema_field(std::string name, int32_t id,
std::vector<std::string> aliases = {}) {
auto field = std::make_shared<schema::external::TField>();
field->__set_name(std::move(name));
field->__set_id(id);
if (!aliases.empty()) {
field->__set_name_mapping(std::move(aliases));
}
schema::external::TFieldPtr field_ptr;
field_ptr.field_ptr = std::move(field);
field_ptr.__isset.field_ptr = true;
return field_ptr;
}
schema::external::TFieldPtr external_array_field(std::string name, int32_t id,
schema::external::TFieldPtr item_field,
std::vector<std::string> aliases = {}) {
auto field = external_schema_field(std::move(name), id, std::move(aliases));
schema::external::TArrayField array_field;
array_field.__set_item_field(std::move(item_field));
field.field_ptr->nestedField.__set_array_field(std::move(array_field));
field.field_ptr->__isset.nestedField = true;
return field;
}
schema::external::TFieldPtr external_map_field(std::string name, int32_t id,
schema::external::TFieldPtr key_field,
schema::external::TFieldPtr value_field,
std::vector<std::string> aliases = {}) {
auto field = external_schema_field(std::move(name), id, std::move(aliases));
schema::external::TMapField map_field;
map_field.__set_key_field(std::move(key_field));
map_field.__set_value_field(std::move(value_field));
field.field_ptr->nestedField.__set_map_field(std::move(map_field));
field.field_ptr->__isset.nestedField = true;
return field;
}
schema::external::TFieldPtr external_struct_field(std::string name, int32_t id,
std::vector<schema::external::TFieldPtr> fields,
std::vector<std::string> aliases = {}) {
auto field = external_schema_field(std::move(name), id, std::move(aliases));
schema::external::TStructField struct_field;
struct_field.__set_fields(std::move(fields));
field.field_ptr->nestedField.__set_struct_field(std::move(struct_field));
field.field_ptr->__isset.nestedField = true;
return field;
}
schema::external::TSchema external_schema(int64_t schema_id,
std::vector<schema::external::TFieldPtr> fields) {
schema::external::TStructField root_field;
root_field.__set_fields(std::move(fields));
schema::external::TSchema schema;
schema.__set_schema_id(schema_id);
schema.__set_root_field(std::move(root_field));
return schema;
}
ColumnDefinition make_nullable_column_definition(ColumnDefinition column) {
column.type = make_table_test_type(column.type);
for (auto& child : column.children) {
child = make_nullable_column_definition(std::move(child));
}
return column;
}
MutableColumnPtr make_not_null_nullable_column(MutableColumnPtr nested_column) {
auto null_map = ColumnUInt8::create();
for (size_t i = 0; i < nested_column->size(); ++i) {
null_map->insert_value(0);
}
return ColumnNullable::create(std::move(nested_column), std::move(null_map));
}
class TableReaderCharVarcharTestHelper final : public TableReader {
public:
using TableReader::_should_truncate_char_or_varchar_column;
using TableReader::_truncate_char_or_varchar_column;
};
class TableReaderCastTestHelper final : public TableReader {
public:
using TableReader::_cast_column_to_type;
};
TEST(TableReaderTest, TruncateCharOrVarcharPredicateOnlyAppliesToParquetStringWidthMismatch) {
ColumnMapping mapping;
mapping.table_type = std::make_shared<DataTypeString>(3, TYPE_VARCHAR);
mapping.file_type = std::make_shared<DataTypeString>(10, TYPE_VARCHAR);
EXPECT_TRUE(TableReaderCharVarcharTestHelper::_should_truncate_char_or_varchar_column(mapping));
mapping.file_type = std::make_shared<DataTypeString>(2, TYPE_VARCHAR);
EXPECT_FALSE(
TableReaderCharVarcharTestHelper::_should_truncate_char_or_varchar_column(mapping));
mapping.file_type = std::make_shared<DataTypeString>();
EXPECT_TRUE(TableReaderCharVarcharTestHelper::_should_truncate_char_or_varchar_column(mapping));
mapping.file_type = std::make_shared<DataTypeInt32>();
EXPECT_TRUE(TableReaderCharVarcharTestHelper::_should_truncate_char_or_varchar_column(mapping));
mapping.table_type = std::make_shared<DataTypeString>();
EXPECT_FALSE(
TableReaderCharVarcharTestHelper::_should_truncate_char_or_varchar_column(mapping));
}
TEST(TableReaderTest, TruncateCharOrVarcharColumnKeepsNullMap) {
auto nested = ColumnString::create();
nested->insert_data("abcdef", 6);
nested->insert_data("xyz", 3);
auto null_map = ColumnUInt8::create();
null_map->insert_value(0);
null_map->insert_value(1);
auto type = make_nullable(std::make_shared<DataTypeString>(3, TYPE_VARCHAR));
Block block;
block.insert({ColumnNullable::create(std::move(nested), std::move(null_map)), type, "v"});
TableReaderCharVarcharTestHelper::_truncate_char_or_varchar_column(&block, 0, 3);
ASSERT_EQ(block.columns(), 1);
ASSERT_EQ(block.rows(), 2);
const auto* nullable_column =
assert_cast<const ColumnNullable*>(block.get_by_position(0).column.get());
EXPECT_EQ(nullable_column->get_nested_column().get_data_at(0).to_string(), "abc");
EXPECT_FALSE(nullable_column->is_null_at(0));
EXPECT_TRUE(nullable_column->is_null_at(1));
}
void set_name_identifiers(std::vector<ColumnDefinition>* columns);
void set_name_identifier(ColumnDefinition* column) {
DORIS_CHECK(column != nullptr);
column->identifier = Field::create_field<TYPE_STRING>(column->name);
set_name_identifiers(&column->children);
}
void set_name_identifiers(std::vector<ColumnDefinition>* columns) {
DORIS_CHECK(columns != nullptr);
for (auto& column : *columns) {
set_name_identifier(&column);
}
}
VExprContextSPtr prepared_conjunct(RuntimeState* state, const VExprSPtr& expr) {
auto ctx = VExprContext::create_shared(expr);
auto status = ctx->prepare(state, RowDescriptor());
EXPECT_TRUE(status.ok()) << status;
status = ctx->open(state);
EXPECT_TRUE(status.ok()) << status;
return ctx;
}
struct FakeFileReaderState {
int init_count = 0;
int open_count = 0;
int close_count = 0;
int64_t total_rows = 2;
int64_t aggregate_count = -1;
int64_t condition_cache_base_granule = 0;
size_t condition_cache_num_granules = 0;
bool eof_with_first_batch = true;
bool inject_delete_conjunct = false;
bool stop_during_aggregate = false;
bool stop_during_read = false;
bool not_found_during_init = false;
std::shared_ptr<FileScanRequest> last_request;
std::optional<FileAggregateRequest> last_aggregate_request;
std::shared_ptr<ConditionCacheContext> condition_cache_ctx;
std::shared_ptr<io::IOContext> io_ctx;
};
class FakeFileReader final : public FileReader {
public:
FakeFileReader(std::shared_ptr<io::FileSystemProperties>& system_properties,
std::unique_ptr<io::FileDescription>& file_description,
std::vector<ColumnDefinition> schema, std::shared_ptr<FakeFileReaderState> state)
: FileReader(system_properties, file_description, state->io_ctx, nullptr),
_schema(std::move(schema)),
_state(std::move(state)) {}
Status init(RuntimeState* state) override {
(void)state;
++_state->init_count;
if (_state->not_found_during_init) {
return Status::NotFound("fake table reader input is missing");
}
_eof = false;
return Status::OK();
}
Status get_schema(std::vector<ColumnDefinition>* file_schema) const override {
DORIS_CHECK(file_schema != nullptr);
*file_schema = _schema;
for (auto& column : *file_schema) {
column = make_nullable_column_definition(std::move(column));
}
return Status::OK();
}
Status open(std::shared_ptr<FileScanRequest> request) override {
RETURN_IF_ERROR(FileReader::open(std::move(request)));
_state->last_request = _request;
++_state->open_count;
_returned_batch = false;
return Status::OK();
}
Status get_block(Block* file_block, size_t* rows, bool* eof) override {
DORIS_CHECK(file_block != nullptr);
DORIS_CHECK(rows != nullptr);
DORIS_CHECK(eof != nullptr);
DORIS_CHECK(_request != nullptr);
if (_returned_batch) {
*rows = 0;
*eof = true;
return Status::OK();
}
for (const auto& [file_column_id, block_position] : _request->local_positions) {
if (file_column_id == LocalColumnId(0)) {
auto column = ColumnInt32::create();
column->insert_value(1);
column->insert_value(2);
file_block->replace_by_position(block_position.value(),
make_not_null_nullable_column(std::move(column)));
} else if (file_column_id == LocalColumnId(1)) {
auto column = ColumnString::create();
column->insert_data("one", 3);
column->insert_data("two", 3);
file_block->replace_by_position(block_position.value(),
make_not_null_nullable_column(std::move(column)));
} else if (file_column_id == LocalColumnId(2)) {
auto country_values = ColumnString::create();
country_values->insert_data("USA", 3);
country_values->insert_data("UK", 2);
auto country_column = make_not_null_nullable_column(std::move(country_values));
auto city_column = ColumnString::create();
city_column->insert_data("New York", 8);
city_column->insert_data("London", 6);
MutableColumns struct_children;
struct_children.push_back(std::move(country_column));
struct_children.push_back(make_not_null_nullable_column(std::move(city_column)));
auto struct_column = ColumnStruct::create(std::move(struct_children));
file_block->replace_by_position(
block_position.value(),
make_not_null_nullable_column(std::move(struct_column)));
} else {
return Status::InvalidArgument("Unexpected fake file column id {}",
file_column_id.value());
}
}
if (_state->stop_during_read) {
DORIS_CHECK(_state->io_ctx != nullptr);
_state->io_ctx->should_stop = true;
}
_returned_batch = true;
*rows = 2;
*eof = _state->eof_with_first_batch;
if (_state->condition_cache_ctx != nullptr && !_state->condition_cache_ctx->is_hit &&
_state->condition_cache_ctx->filter_result != nullptr &&
!_state->condition_cache_ctx->filter_result->empty()) {
// The real file reader marks a granule after local row-level predicates keep at least
// one row from that granule. The fake reader does it here so TableReader tests can
// focus on condition-cache lifecycle decisions without depending on Parquet internals.
(*_state->condition_cache_ctx->filter_result)[0] = true;
}
return Status::OK();
}
Status get_aggregate_result(const FileAggregateRequest& request,
FileAggregateResult* result) override {
DORIS_CHECK(result != nullptr);
if (_state->aggregate_count < 0) {
return FileReader::get_aggregate_result(request, result);
}
if (request.agg_type != TPushAggOp::type::COUNT) {
return Status::NotSupported("fake reader only supports COUNT aggregate pushdown");
}
_state->last_aggregate_request = request;
if (_state->stop_during_aggregate) {
DORIS_CHECK(_state->io_ctx != nullptr);
_state->io_ctx->should_stop = true;
return Status::EndOfFile("stop");
}
result->count = _state->aggregate_count;
result->columns.clear();
_record_scan_rows(_state->aggregate_count);
_eof = true;
return Status::OK();
}
void set_condition_cache_context(std::shared_ptr<ConditionCacheContext> ctx) override {
_state->condition_cache_ctx = std::move(ctx);
if (_state->condition_cache_ctx != nullptr && !_state->condition_cache_ctx->is_hit) {
_state->condition_cache_ctx->base_granule = _state->condition_cache_base_granule;
if (_state->condition_cache_num_granules > 0) {
_state->condition_cache_ctx->num_granules = _state->condition_cache_num_granules;
}
}
}
int64_t get_total_rows() const override { return _state->total_rows; }
Status close() override {
++_state->close_count;
_request.reset();
_eof = true;
return Status::OK();
}
private:
std::vector<ColumnDefinition> _schema;
std::shared_ptr<FakeFileReaderState> _state;
bool _returned_batch = false;
};
class FakeTableReader final : public TableReader {
public:
FakeTableReader(std::vector<ColumnDefinition> file_schema,
std::shared_ptr<FakeFileReaderState> state)
: _file_schema(std::move(file_schema)), _state(std::move(state)) {}
protected:
Status create_file_reader(std::unique_ptr<FileReader>* reader) override {
DORIS_CHECK(reader != nullptr);
auto system_properties = std::make_shared<io::FileSystemProperties>();
system_properties->system_type = TFileType::FILE_LOCAL;
auto file_description = std::make_unique<io::FileDescription>();
file_description->path = "fake-table-reader-input";
*reader = std::make_unique<FakeFileReader>(system_properties, file_description,
_file_schema, _state);
return Status::OK();
}
Status customize_file_scan_request(FileScanRequest* file_request) override {
RETURN_IF_ERROR(TableReader::customize_file_scan_request(file_request));
if (_state->inject_delete_conjunct) {
// Table-format delete handling is represented in v2 by TableReader injecting
// delete_conjuncts into the file scan request. The fake reader does not execute it;
// this only tests that condition cache is disabled once such table-level delete state
// is present in the request.
file_request->delete_conjuncts.push_back(
VExprContext::create_shared(table_int32_literal(1)));
}
return Status::OK();
}
private:
std::vector<ColumnDefinition> _file_schema;
std::shared_ptr<FakeFileReaderState> _state;
};
class ScopedConditionCacheForTest {
public:
ScopedConditionCacheForTest()
: _previous(ExecEnv::GetInstance()->get_condition_cache()),
_cache(segment_v2::ConditionCache::create_global_cache(1024 * 1024, 4)) {
ExecEnv::GetInstance()->_condition_cache = _cache.get();
}
~ScopedConditionCacheForTest() { ExecEnv::GetInstance()->_condition_cache = _previous; }
segment_v2::ConditionCache* get() { return _cache.get(); }
private:
segment_v2::ConditionCache* _previous = nullptr;
std::unique_ptr<segment_v2::ConditionCache> _cache;
};
TEST(TableReaderTest, PrepareSplitPrunesPartitionRuntimeFilter) {
std::vector<ColumnDefinition> projected_columns;
auto partition_column = make_table_column(0, "part", std::make_shared<DataTypeInt32>());
partition_column.is_partition_key = true;
projected_columns.push_back(std::move(partition_column));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
RuntimeProfile profile("scanner");
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
SplitReadOptions pruned_split;
pruned_split.current_range.__set_path("unused-pruned-file");
pruned_split.partition_values.emplace("part", Field::create_field<TYPE_INT>(7));
pruned_split.partition_prune_conjuncts.push_back(VExprContext::create_shared(
runtime_filter_wrapper_expr(table_int32_greater_than_expr(0, 0, 10))));
ASSERT_TRUE(reader.prepare_split(pruned_split).ok());
EXPECT_TRUE(reader.current_split_pruned());
ASSERT_NE(profile.get_counter("RuntimeFilterPartitionPrunedRangeNum"), nullptr);
EXPECT_EQ(profile.get_counter("RuntimeFilterPartitionPrunedRangeNum")->value(), 1);
SplitReadOptions retained_split;
retained_split.current_range.__set_path("unused-retained-file");
retained_split.partition_values.emplace("part", Field::create_field<TYPE_INT>(11));
retained_split.partition_prune_conjuncts.push_back(VExprContext::create_shared(
runtime_filter_wrapper_expr(table_int32_greater_than_expr(0, 0, 10))));
ASSERT_TRUE(reader.prepare_split(retained_split).ok());
EXPECT_FALSE(reader.current_split_pruned());
}
TEST(TableReaderTest, PrepareSplitDoesNotEvaluateNonDeterministicPartitionPredicate) {
std::vector<ColumnDefinition> projected_columns;
auto partition_column = make_table_column(0, "part", std::make_shared<DataTypeInt32>());
partition_column.is_partition_key = true;
projected_columns.push_back(std::move(partition_column));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
RuntimeProfile profile("scanner");
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = &profile,
})
.ok());
bool predicate_executed = false;
auto predicate = std::make_shared<NonDeterministicPartitionPredicate>(&predicate_executed);
predicate->add_child(table_int32_slot_ref(0, 0, "part"));
SplitReadOptions split;
split.current_range.__set_path("unused-nondeterministic-file");
split.partition_values.emplace("part", Field::create_field<TYPE_INT>(7));
split.partition_prune_conjuncts.push_back(
VExprContext::create_shared(runtime_filter_wrapper_expr(std::move(predicate))));
split.partition_prune_conjuncts.push_back(VExprContext::create_shared(
runtime_filter_wrapper_expr(table_int32_greater_than_expr(0, 0, 10))));
ASSERT_TRUE(reader.prepare_split(split).ok());
EXPECT_FALSE(predicate_executed);
EXPECT_FALSE(reader.current_split_pruned());
ASSERT_NE(profile.get_counter("RuntimeFilterPartitionPrunedRangeNum"), nullptr);
EXPECT_EQ(profile.get_counter("RuntimeFilterPartitionPrunedRangeNum")->value(), 0);
}
TEST(TableReaderTest, ConstantPruningStopsAtUnsafePredicate) {
std::vector<ColumnDefinition> projected_columns;
auto partition_column = make_table_column(0, "part", std::make_shared<DataTypeInt32>());
partition_column.is_partition_key = true;
projected_columns.push_back(std::move(partition_column));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
bool predicate_executed = false;
auto unsafe_predicate =
std::make_shared<NonDeterministicPartitionPredicate>(&predicate_executed);
unsafe_predicate->add_child(table_int32_slot_ref(0, 0, "part"));
auto fake_state = std::make_shared<FakeFileReaderState>();
FakeTableReader reader({}, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts =
{
prepared_conjunct(&state, unsafe_predicate),
prepared_conjunct(&state,
table_int32_greater_than_expr(
0, 0, 10)),
},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
SplitReadOptions split;
split.current_range.__set_path("fake-table-reader-input");
split.partition_values.emplace("part", Field::create_field<TYPE_INT>(7));
ASSERT_TRUE(reader.prepare_split(split).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_FALSE(predicate_executed);
EXPECT_FALSE(eos);
EXPECT_EQ(fake_state->open_count, 1);
ASSERT_TRUE(reader.close().ok());
}
TEST(TableReaderTest, ConstantPruningStopsAtUnsafeSlotlessPredicate) {
std::vector<ColumnDefinition> projected_columns;
auto partition_column = make_table_column(0, "part", std::make_shared<DataTypeInt32>());
partition_column.is_partition_key = true;
projected_columns.push_back(std::move(partition_column));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
bool predicate_executed = false;
auto unsafe_slotless_predicate =
std::make_shared<NonDeterministicPartitionPredicate>(&predicate_executed);
auto fake_state = std::make_shared<FakeFileReaderState>();
FakeTableReader reader({}, fake_state);
ASSERT_TRUE(
reader
.init({
.projected_columns = projected_columns,
.conjuncts =
{
prepared_conjunct(&state, unsafe_slotless_predicate),
prepared_conjunct(&state, table_int32_greater_than_expr(
0, 0, 10)),
},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
SplitReadOptions split;
split.current_range.__set_path("fake-table-reader-input");
split.partition_values.emplace("part", Field::create_field<TYPE_INT>(7));
ASSERT_TRUE(reader.prepare_split(split).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_TRUE(predicate_executed);
EXPECT_FALSE(eos);
// The later partition predicate is false for part=7. Opening the file proves constant pruning
// stopped at the earlier unsafe expression even though that expression had no slot and thus no
// entry in `_table_filters`.
EXPECT_EQ(fake_state->open_count, 1);
ASSERT_NE(fake_state->last_request, nullptr);
// A slotless unsafe conjunct is an ordering barrier even though it has no TableFilter entry.
// The later predicate must stay on the scanner's row-level path instead of running inside the
// file reader before the unsafe conjunct.
EXPECT_TRUE(fake_state->last_request->conjuncts.empty());
ASSERT_TRUE(reader.close().ok());
}
TEST(TableReaderTest, CanUseInjectedFileReaderForStandaloneUnitTest) {
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
file_schema.push_back(make_file_column(1, "value", std::make_shared<DataTypeString>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(1, "value", std::make_shared<DataTypeString>()));
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_FALSE(eos);
ASSERT_EQ(fake_state->init_count, 1);
ASSERT_EQ(fake_state->open_count, 1);
ASSERT_EQ(fake_state->close_count, 1);
ASSERT_NE(fake_state->last_request, nullptr);
ASSERT_EQ(fake_state->last_request->local_positions.at(LocalColumnId(1)).value(), 0);
ASSERT_EQ(fake_state->last_request->local_positions.at(LocalColumnId(0)).value(), 1);
EXPECT_EQ(projection_ids(fake_state->last_request->non_predicate_columns),
std::vector<int32_t>({1, 0}));
EXPECT_TRUE(fake_state->last_request->predicate_columns.empty());
const auto& value_column =
assert_cast<const ColumnString&>(expect_not_null_table_column(block, 0));
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 1));
ASSERT_EQ(block.rows(), 2);
EXPECT_EQ(value_column.get_data_at(0).to_string(), "one");
EXPECT_EQ(value_column.get_data_at(1).to_string(), "two");
EXPECT_EQ(id_column.get_element(0), 1);
EXPECT_EQ(id_column.get_element(1), 2);
block = build_table_block(projected_columns);
eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_TRUE(eos);
}
TEST(TableReaderTest, PrepareSplitReplacesInitialConjunctSnapshot) {
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {VExprContext::create_shared(
table_int32_greater_than_expr(0, 0, 0))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
split_options.conjuncts = VExprContextSPtrs {VExprContext::create_shared(
runtime_filter_wrapper_expr(table_int32_greater_than_expr(0, 0, 1)))};
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_NE(fake_state->last_request, nullptr);
ASSERT_EQ(fake_state->last_request->conjuncts.size(), 1);
EXPECT_TRUE(fake_state->last_request->conjuncts.front()->root()->is_rf_wrapper());
ASSERT_TRUE(reader.close().ok());
}
TEST(TableReaderTest, RefreshedConjunctDisablesTableLevelCount) {
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns = std::vector<GlobalIndex> {},
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
split_options.conjuncts = VExprContextSPtrs {VExprContext::create_shared(
runtime_filter_wrapper_expr(table_int32_greater_than_expr(0, 0, 1)))};
set_table_level_row_count(&split_options, 5);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
// The metadata count advertises five rows, while the fake reader contains two. Opening the
// reader and returning its rows proves the fresh runtime filter did not take the synthetic
// table-level COUNT path that would bypass all row predicates.
EXPECT_EQ(fake_state->open_count, 1);
EXPECT_EQ(block.rows(), 2);
ASSERT_TRUE(reader.close().ok());
}
TEST(TableReaderTest, PendingRuntimeFilterDisablesTableLevelCount) {
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->aggregate_count = state.batch_size() + 5;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns = std::vector<GlobalIndex> {},
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
// A pending runtime filter makes metadata COUNT ineligible before its first synthetic batch.
// This prevents the filter from arriving between scheduler reads after unfiltered rows have
// already escaped.
split_options.all_runtime_filters_applied = false;
set_table_level_row_count(&split_options, state.batch_size() + 5);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_EQ(fake_state->open_count, 1);
EXPECT_EQ(block.rows(), 2);
ASSERT_NE(fake_state->last_request, nullptr);
// Aggregate pushdown is disabled while a runtime filter is pending, but COUNT(*) semantics do
// not change. The retained output slot remains a value-less placeholder during row fallback.
EXPECT_TRUE(fake_state->last_request->is_count_star_placeholder(LocalColumnId(0)));
ASSERT_TRUE(reader.close().ok());
}
TEST(TableReaderTest, PendingRuntimeFilterDisablesMinMaxPushdown) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_pending_rf_minmax_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {3, 1, 5, 2}, {30, 10, 50, 20},
{"three", "one", "five", "two"}, 2);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
projected_columns.push_back(make_table_column(1, "score", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::MINMAX,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.all_runtime_filters_applied = false;
ASSERT_TRUE(reader.prepare_split(split_options).ok());
bool eos = false;
size_t total_rows = 0;
bool checked_first_batch = false;
while (!eos) {
Block block = build_table_block(projected_columns);
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
total_rows += block.rows();
if (!checked_first_batch && block.rows() > 0) {
const auto& ids =
assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 0));
ASSERT_EQ(ids.size(), 2);
EXPECT_EQ(ids.get_element(0), 3);
EXPECT_EQ(ids.get_element(1), 1);
checked_first_batch = true;
}
}
// MIN/MAX pushdown would return the two synthetic extrema [1, 5]. Reading the original first
// row group [3, 1] and all four rows proves a pending RF kept the physical reader active.
EXPECT_TRUE(checked_first_batch);
EXPECT_EQ(total_rows, 4);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, SlotlessConjunctDisablesAggregatePushdown) {
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
bool predicate_executed = false;
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->aggregate_count = 3;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state,
std::make_shared<NonDeterministicPartitionPredicate>(
&predicate_executed))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns = std::vector<GlobalIndex> {},
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
// The slotless predicate cannot become a TableFilter or a file-reader conjunct, but its
// presence still prevents the fake aggregate count (3) from replacing the two physical rows.
ASSERT_NE(fake_state->last_request, nullptr);
EXPECT_TRUE(fake_state->last_request->conjuncts.empty());
EXPECT_EQ(block.rows(), 2);
EXPECT_TRUE(predicate_executed);
ASSERT_TRUE(reader.close().ok());
}
TEST(TableReaderTest, AbortSplitClearsReaderAfterIgnorableNotFound) {
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->not_found_during_init = true;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("missing-fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
const auto status = reader.get_block(&block, &eos);
ASSERT_TRUE(status.is<ErrorCode::NOT_FOUND>()) << status;
ASSERT_TRUE(reader.abort_split().ok());
EXPECT_EQ(fake_state->init_count, 1);
EXPECT_EQ(fake_state->close_count, 1);
fake_state->not_found_during_init = false;
split_options.current_range.__set_path("existing-fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_EQ(fake_state->init_count, 2);
EXPECT_EQ(fake_state->close_count, 2);
ASSERT_TRUE(reader.close().ok());
}
TEST(TableReaderTest, PushDownCountRecordsReaderRowsBeforeClosingReader) {
const auto nullable_int_type = make_nullable(std::make_shared<DataTypeInt32>());
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", nullable_int_type));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", nullable_int_type));
set_name_identifiers(&projected_columns);
io::FileReaderStats file_reader_stats;
auto io_ctx = std::make_shared<io::IOContext>();
io_ctx->file_reader_stats = &file_reader_stats;
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->aggregate_count = 3;
fake_state->io_ctx = io_ctx;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns =
std::vector<GlobalIndex> {GlobalIndex(0)},
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_FALSE(eos);
EXPECT_EQ(block.rows(), 3);
EXPECT_EQ(file_reader_stats.read_rows, 3);
EXPECT_EQ(fake_state->close_count, 1);
EXPECT_TRUE(reader.current_split_uses_metadata_count());
ASSERT_TRUE(fake_state->last_request != nullptr);
EXPECT_TRUE(fake_state->last_request->count_star_placeholder_columns.empty());
ASSERT_TRUE(fake_state->last_aggregate_request.has_value());
ASSERT_EQ(fake_state->last_aggregate_request->columns.size(), 1);
// A primitive COUNT(col) projection must reach the file reader just like a complex one.
EXPECT_EQ(fake_state->last_aggregate_request->columns[0].projection.local_id(), 0);
}
TEST(TableReaderTest, PushDownCountStarIgnoresProjectedPlaceholderColumn) {
const auto nullable_int_type = make_nullable(std::make_shared<DataTypeInt32>());
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "nullable_id", nullable_int_type));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "nullable_id", nullable_int_type));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->aggregate_count = 3;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(
reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
// COUNT(*) deliberately has no explicit count columns. The
// nullable_id projection is only the planner's scan placeholder.
.push_down_count_columns = std::vector<GlobalIndex> {},
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_FALSE(eos);
EXPECT_EQ(block.rows(), 3);
ASSERT_TRUE(fake_state->last_request != nullptr);
ASSERT_EQ(fake_state->last_request->count_star_placeholder_columns.size(), 1);
EXPECT_TRUE(fake_state->last_request->is_count_star_placeholder(LocalColumnId(0)));
ASSERT_TRUE(fake_state->last_aggregate_request.has_value());
// Passing nullable_id here would implement COUNT(nullable_id) and reproduce the external ORC
// and Parquet failures where footer row counts were reduced by null values.
EXPECT_TRUE(fake_state->last_aggregate_request->columns.empty());
}
TEST(TableReaderTest, PushDownCountFallsBackWhenSemanticArgumentsAreAbsent) {
const auto nullable_int_type = make_nullable(std::make_shared<DataTypeInt32>());
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", nullable_int_type));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", nullable_int_type));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->aggregate_count = 3;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
// Simulate an old FE: it can request COUNT pushdown but cannot
// serialize push_down_count_slot_ids.
.push_down_count_columns = std::nullopt,
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_FALSE(eos);
EXPECT_EQ(block.rows(), 2);
// The explicit aggregate_count=3 would be returned if absence were confused with COUNT(*).
EXPECT_FALSE(fake_state->last_aggregate_request.has_value());
EXPECT_FALSE(reader.current_split_uses_metadata_count());
}
TEST(TableReaderTest, PushDownCountFallsBackForMultipleArguments) {
const auto nullable_int_type = make_nullable(std::make_shared<DataTypeInt32>());
const auto nullable_string_type = make_nullable(std::make_shared<DataTypeString>());
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", nullable_int_type));
file_schema.push_back(make_file_column(1, "name", nullable_string_type));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", nullable_int_type));
projected_columns.push_back(make_table_column(1, "name", nullable_string_type));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->aggregate_count = 3;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(
reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns =
std::vector<GlobalIndex> {GlobalIndex(0), GlobalIndex(1)},
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_FALSE(eos);
EXPECT_EQ(block.rows(), 2);
EXPECT_FALSE(fake_state->last_aggregate_request.has_value());
EXPECT_FALSE(reader.current_split_uses_metadata_count());
}
TEST(TableReaderTest, PushDownCountFallsBackForNullableToRequiredMapping) {
const auto nullable_int_type = make_nullable(std::make_shared<DataTypeInt32>());
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", nullable_int_type));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
// make_table_column models the usual nullable external-table descriptor. Override it here to
// reproduce an evolved table contract that declares the mapped physical column required.
projected_columns[0].type = std::make_shared<DataTypeInt32>();
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->aggregate_count = 3;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns =
std::vector<GlobalIndex> {GlobalIndex(0)},
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
// The normal scan rejects the nullable physical column because it cannot satisfy the required
// table contract. Footer COUNT would bypass that validation and incorrectly return 3.
EXPECT_FALSE(reader.get_block(&block, &eos).ok());
EXPECT_FALSE(fake_state->last_aggregate_request.has_value());
}
TEST(TableReaderTest, PushDownCountFallsBackForCastMapping) {
const auto nullable_int_type = make_nullable(std::make_shared<DataTypeInt32>());
const auto nullable_bigint_type = make_nullable(std::make_shared<DataTypeInt64>());
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", nullable_int_type));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", nullable_bigint_type));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->aggregate_count = 3;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns =
std::vector<GlobalIndex> {GlobalIndex(0)},
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_FALSE(eos);
EXPECT_EQ(block.rows(), 2);
EXPECT_TRUE(block.get_by_position(0).type->equals(*nullable_bigint_type));
// INT->BIGINT is a non-trivial mapping, so COUNT must not skip the cast/materialization path.
EXPECT_FALSE(fake_state->last_aggregate_request.has_value());
}
TEST(TableReaderTest, PushDownCountStopConvertsAggregateEndOfFileToEos) {
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
auto io_ctx = std::make_shared<io::IOContext>();
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->aggregate_count = 3;
fake_state->io_ctx = io_ctx;
fake_state->stop_during_aggregate = true;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns = std::vector<GlobalIndex> {},
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_TRUE(eos);
EXPECT_EQ(block.rows(), 0);
EXPECT_EQ(fake_state->close_count, 0);
}
TEST(TableReaderTest, DebugStringCoversReaderStateAndEnumNames) {
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
file_schema.push_back(make_file_column(1, "value", std::make_shared<DataTypeString>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
projected_columns.push_back(make_table_column(1, "value", std::make_shared<DataTypeString>()));
projected_columns[0].name_mapping = {"legacy_id"};
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->eof_with_first_batch = false;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(0, 0, 0))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = std::make_shared<io::IOContext>(),
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns = std::vector<GlobalIndex> {},
})
.ok());
SplitReadOptions split_options;
split_options.partition_values.emplace("dt", Field::create_field<TYPE_STRING>("2026-06-29"));
split_options.current_range.__set_path("fake-table-reader-input");
split_options.current_range.__set_file_size(64);
split_options.current_range.__set_start_offset(7);
split_options.current_range.__set_size(11);
split_options.current_range.__set_modification_time(13);
split_options.current_range.__set_fs_name("local-fs");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto debug = reader.debug_string();
EXPECT_NE(debug.find("format=PARQUET"), std::string::npos);
EXPECT_NE(debug.find("push_down_agg_type=COUNT"), std::string::npos);
EXPECT_NE(debug.find("current_file=FileDescription{path=fake-table-reader-input"),
std::string::npos);
EXPECT_NE(debug.find("partition_values={dt}"), std::string::npos);
EXPECT_NE(debug.find("table_filters=[TableFilter{conjunct=VExprContext"), std::string::npos);
EXPECT_NE(debug.find("ColumnDefinition{name=id"), std::string::npos);
EXPECT_NE(debug.find("name_mapping=[legacy_id]"), std::string::npos);
EXPECT_NE(debug.find("ColumnMapping{global_index=0"), std::string::npos);
EXPECT_NE(debug.find("FileBlockColumn{file_column_id=0"), std::string::npos);
ASSERT_TRUE(reader.close().ok());
const std::vector<FileFormat> formats {FileFormat::ORC, FileFormat::CSV, FileFormat::JSON,
FileFormat::TEXT, FileFormat::JNI, FileFormat::NATIVE,
FileFormat::ARROW};
const std::vector<std::string> format_names {"ORC", "CSV", "JSON", "TEXT",
"JNI", "NATIVE", "ARROW"};
for (size_t idx = 0; idx < formats.size(); ++idx) {
TableReader enum_reader;
ASSERT_TRUE(enum_reader
.init({
.projected_columns = {},
.conjuncts = {},
.format = formats[idx],
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
EXPECT_NE(enum_reader.debug_string().find("format=" + format_names[idx]),
std::string::npos);
}
const std::vector<TPushAggOp::type> agg_ops {TPushAggOp::type::NONE, TPushAggOp::type::MINMAX,
TPushAggOp::type::MIX,
TPushAggOp::type::COUNT_ON_INDEX};
const std::vector<std::string> agg_names {"NONE", "MINMAX", "MIX", "COUNT_ON_INDEX"};
for (size_t idx = 0; idx < agg_ops.size(); ++idx) {
TableReader enum_reader;
ASSERT_TRUE(enum_reader
.init({
.projected_columns = {},
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = agg_ops[idx],
})
.ok());
EXPECT_NE(enum_reader.debug_string().find("push_down_agg_type=" + agg_names[idx]),
std::string::npos);
}
}
TEST(TableReaderTest, AnnotateProjectedColumnUsesCurrentHistorySchemaForNestedTypes) {
TFileScanRangeParams scan_params;
scan_params.__set_current_schema_id(200);
auto profile_field = external_struct_field(
"profile", 20,
{external_array_field("old_scores", 21, external_schema_field("old_score", 22),
{"scores"}),
external_map_field("old_props", 23, external_schema_field("old_key", 24),
external_schema_field("old_value", 25), {"props"})},
{"user_profile"});
scan_params.__set_history_schema_info(
{external_schema(100, {external_schema_field("ignored_profile", 10)}),
external_schema(200, {profile_field})});
const auto int_type = std::make_shared<DataTypeInt32>();
const auto string_type = std::make_shared<DataTypeString>();
auto scores_type = std::make_shared<DataTypeArray>(int_type);
auto props_type = std::make_shared<DataTypeMap>(string_type, string_type);
auto profile_type = std::make_shared<DataTypeStruct>(DataTypes {scores_type, props_type},
Strings {"scores", "props"});
ColumnDefinition profile_column = make_table_column(-1, "user_profile", profile_type);
ProjectedColumnBuildContext context;
context.scan_params = &scan_params;
TFileScanSlotInfo slot_info;
TableReader reader;
ASSERT_TRUE(reader.annotate_projected_column(slot_info, &context, &profile_column).ok());
EXPECT_EQ(profile_column.get_identifier_field_id(), 20);
EXPECT_EQ(profile_column.name_mapping, std::vector<std::string>({"user_profile"}));
ASSERT_TRUE(context.schema_column.has_value());
ASSERT_EQ(context.schema_column->children.size(), 2);
EXPECT_EQ(context.schema_column->children[0].name, "old_scores");
EXPECT_EQ(context.schema_column->children[0].get_identifier_field_id(), 21);
ASSERT_EQ(context.schema_column->children[0].children.size(), 1);
EXPECT_EQ(context.schema_column->children[0].children[0].name, "element");
EXPECT_EQ(context.schema_column->children[0].children[0].get_identifier_field_id(), 22);
ASSERT_EQ(context.schema_column->children[1].children.size(), 2);
EXPECT_EQ(context.schema_column->children[1].name, "old_props");
EXPECT_EQ(context.schema_column->children[1].children[0].name, "key");
EXPECT_EQ(context.schema_column->children[1].children[0].get_identifier_field_id(), 24);
EXPECT_EQ(context.schema_column->children[1].children[1].name, "value");
EXPECT_EQ(context.schema_column->children[1].children[1].get_identifier_field_id(), 25);
}
TEST(TableReaderTest, ComplexRematerializeCastsScalarChildToTableType) {
const auto string_type = std::make_shared<DataTypeString>();
const auto nullable_string_type = make_nullable(string_type);
const auto file_struct_type = make_nullable(std::make_shared<DataTypeStruct>(
DataTypes {nullable_string_type, string_type}, Strings {"country", "city"}));
auto file_struct_column = make_file_column(2, "struct_column", file_struct_type);
file_struct_column.children = {make_file_column(0, "country", nullable_string_type),
make_file_column(1, "city", string_type)};
std::vector<ColumnDefinition> file_schema = {file_struct_column};
const auto table_struct_type = make_nullable(std::make_shared<DataTypeStruct>(
DataTypes {nullable_string_type, nullable_string_type}, Strings {"country", "city"}));
auto country_child = make_table_column(0, "country", nullable_string_type);
auto city_child = make_table_column(1, "city", nullable_string_type);
auto table_struct_column = make_table_column(2, "struct_column", table_struct_type);
table_struct_column.children = {country_child, city_child};
std::vector<ColumnDefinition> projected_columns = {table_struct_column};
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
const auto status = reader.get_block(&block, &eos);
ASSERT_TRUE(status.ok()) << status.to_string();
ASSERT_FALSE(eos);
ASSERT_TRUE(block.check_type_and_column().ok()) << block.dump_structure();
const auto& result_nullable =
assert_cast<const ColumnNullable&>(*block.get_by_position(0).column);
const auto& struct_result =
assert_cast<const ColumnStruct&>(result_nullable.get_nested_column());
ASSERT_EQ(struct_result.get_columns().size(), 2);
const auto& country_column = assert_cast<const ColumnNullable&>(struct_result.get_column(0));
const auto& city_column = assert_cast<const ColumnNullable&>(struct_result.get_column(1));
const auto& country_values =
assert_cast<const ColumnString&>(country_column.get_nested_column());
const auto& city_values = assert_cast<const ColumnString&>(city_column.get_nested_column());
ASSERT_EQ(city_column.size(), 2);
EXPECT_FALSE(city_column.is_null_at(0));
EXPECT_FALSE(city_column.is_null_at(1));
EXPECT_EQ(country_values.get_data_at(0).to_string(), "USA");
EXPECT_EQ(country_values.get_data_at(1).to_string(), "UK");
EXPECT_EQ(city_values.get_data_at(0).to_string(), "New York");
EXPECT_EQ(city_values.get_data_at(1).to_string(), "London");
}
TEST(TableReaderTest, ComplexRematerializeCastsNonNullableScalarChildWithNullableFileType) {
const auto int_type = std::make_shared<DataTypeInt32>();
const auto bigint_type = std::make_shared<DataTypeInt64>();
const auto nullable_int_type = make_nullable(int_type);
const auto nullable_bigint_type = make_nullable(bigint_type);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
TableReaderCastTestHelper reader;
ASSERT_TRUE(reader.init({
.projected_columns = {},
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
auto column = ColumnInt32::create();
column->insert_value(10);
column->insert_value(20);
ColumnPtr result_column = std::move(column);
const auto status = reader._cast_column_to_type(&result_column, nullable_int_type,
nullable_bigint_type, "struct_column.a");
ASSERT_TRUE(status.ok()) << status.to_string();
const auto& result_nullable = assert_cast<const ColumnNullable&>(*result_column);
const auto& child_values = assert_cast<const ColumnInt64&>(result_nullable.get_nested_column());
ASSERT_EQ(result_nullable.size(), 2);
EXPECT_FALSE(result_nullable.is_null_at(0));
EXPECT_FALSE(result_nullable.is_null_at(1));
EXPECT_EQ(child_values.get_element(0), 10);
EXPECT_EQ(child_values.get_element(1), 20);
}
TEST(TableReaderTest, ReopenSplitAfterClose) {
const auto test_dir = std::filesystem::temp_directory_path() / "doris_table_reader_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const std::vector<std::string> file_paths = {
(test_dir / "split_1.parquet").string(),
(test_dir / "split_2.parquet").string(),
(test_dir / "split_3.parquet").string(),
};
write_parquet_file(file_paths[0], 1, "one");
write_parquet_file(file_paths[1], 2, "two");
write_parquet_file(file_paths[2], 3, "three");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(1, "value", std::make_shared<DataTypeString>()));
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(1, 1, 0))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
// Simulate the scanner lifecycle for three different splits:
// init() once, then repeat prepare_split() -> get_block() -> close().
// This verifies TableReader::close() fully releases the previous low-level reader and task
// state, so a later prepare_split() can open and read a new split on the same TableReader.
// The table-level conjunct is also rebuilt for each split. The projection order puts value
// before id, so the pushed conjunct has to be rewritten to the ParquetReader file-local block
// position every time a new split is opened.
std::vector<int32_t> ids;
std::vector<std::string> values;
for (const auto& file_path : file_paths) {
auto split_options = build_split_options(file_path);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto& value_column =
assert_cast<const ColumnString&>(expect_not_null_table_column(block, 0));
const auto& id_column =
assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 1));
ASSERT_EQ(id_column.size(), 1);
ASSERT_EQ(value_column.size(), 1);
ids.push_back(id_column.get_element(0));
values.push_back(value_column.get_data_at(0).to_string());
ASSERT_TRUE(reader.close().ok());
}
EXPECT_EQ(ids, std::vector<int32_t>({1, 2, 3}));
EXPECT_EQ(values, std::vector<std::string>({"one", "two", "three"}));
std::filesystem::remove_all(test_dir);
}
// Scenario: requests without file-local row conjuncts do not produce a row-level survivor bitmap,
// so TableReader must not enable condition cache.
TEST(TableReaderTest, ConditionCacheSkipsRequestWithoutFileLocalConjuncts) {
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.condition_cache_digest = 7,
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_EQ(fake_state->condition_cache_ctx, nullptr);
EXPECT_EQ(reader.condition_cache_hit_count(), 0);
ASSERT_TRUE(reader.close().ok());
}
// Scenario: a standalone caller has only the initial digest for stable predicate P, while its
// current conjunct snapshot also contains an RF. Without an explicit split digest, TableReader must
// not store P AND RF under P's stale key.
TEST(TableReaderTest, ConditionCacheSkipsRuntimeFilterWithoutSplitDigest) {
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(
reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, runtime_filter_wrapper_expr(
table_int32_greater_than_expr(0, 0, 0)))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.condition_cache_digest = 7,
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_EQ(fake_state->condition_cache_ctx, nullptr);
EXPECT_EQ(reader.condition_cache_hit_count(), 0);
ASSERT_TRUE(reader.close().ok());
}
// Scenario: FileScannerV2 supplies a non-zero digest computed from the exact Ready RF payload in
// this split. The RF wrapper is no longer a reason to disable condition cache; a MISS context is
// created and can be published under that payload-specific key after EOF.
TEST(TableReaderTest, ConditionCacheAllowsRuntimeFilterCoveredBySplitDigest) {
ScopedConditionCacheForTest cache;
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->total_rows = ConditionCacheContext::GRANULE_SIZE;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(
reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, runtime_filter_wrapper_expr(
table_int32_greater_than_expr(0, 0, 0)))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.condition_cache_digest = 7,
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
split_options.condition_cache_digest = 11;
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_NE(fake_state->condition_cache_ctx, nullptr);
EXPECT_FALSE(fake_state->condition_cache_ctx->is_hit);
segment_v2::ConditionCacheHandle handle;
segment_v2::ConditionCache::ExternalCacheKey initial_digest_key(
"fake-table-reader-input", 0, -1, 7, 0, -1,
segment_v2::ConditionCache::ExternalCacheKey::BASE_GRANULE_AWARE_VERSION);
EXPECT_FALSE(cache.get()->lookup(initial_digest_key, &handle));
segment_v2::ConditionCache::ExternalCacheKey split_digest_key(
"fake-table-reader-input", 0, -1, 11, 0, -1,
segment_v2::ConditionCache::ExternalCacheKey::BASE_GRANULE_AWARE_VERSION);
EXPECT_TRUE(cache.get()->lookup(split_digest_key, &handle));
ASSERT_TRUE(reader.close().ok());
}
// Scenario: table-format delete files/deletion vectors are outside the data-file cache key. When
// TableReader injects delete conjuncts into the file scan request, condition cache must be disabled
// for that split.
TEST(TableReaderTest, ConditionCacheSkipsRequestWithDeleteConjuncts) {
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->inject_delete_conjunct = true;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(0, 0, 0))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.condition_cache_digest = 7,
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_EQ(fake_state->condition_cache_ctx, nullptr);
EXPECT_EQ(reader.condition_cache_hit_count(), 0);
ASSERT_TRUE(reader.close().ok());
}
// Scenario: a MISS bitmap is safe to publish only after the physical reader reaches EOF. This test
// returns EOF together with the first batch and verifies TableReader publishes the marked bitmap.
TEST(TableReaderTest, ConditionCacheMissPublishesBitmapAfterReaderEof) {
ScopedConditionCacheForTest cache;
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->total_rows = ConditionCacheContext::GRANULE_SIZE;
fake_state->condition_cache_base_granule = 7;
fake_state->condition_cache_num_granules = 1;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(0, 0, 0))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.condition_cache_digest = 7,
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_NE(fake_state->condition_cache_ctx, nullptr);
EXPECT_FALSE(fake_state->condition_cache_ctx->is_hit);
segment_v2::ConditionCache::ExternalCacheKey legacy_key("fake-table-reader-input", 0, -1, 7, 0,
-1);
segment_v2::ConditionCacheHandle handle;
EXPECT_FALSE(cache.get()->lookup(legacy_key, &handle));
segment_v2::ConditionCache::ExternalCacheKey key(
"fake-table-reader-input", 0, -1, 7, 0, -1,
segment_v2::ConditionCache::ExternalCacheKey::BASE_GRANULE_AWARE_VERSION);
ASSERT_TRUE(cache.get()->lookup(key, &handle));
const auto cached_bitmap = handle.get_filter_result();
ASSERT_NE(cached_bitmap, nullptr);
ASSERT_FALSE(cached_bitmap->empty());
EXPECT_EQ(cached_bitmap->size(), 1);
EXPECT_TRUE((*cached_bitmap)[0]);
EXPECT_EQ(handle.get_base_granule(), 7);
ASSERT_TRUE(reader.close().ok());
}
// Scenario: LIMIT/cancel can close a reader before it reaches EOF. TableReader must drop the MISS
// bitmap because unvisited granules would still be false and unsafe for future cache hits.
TEST(TableReaderTest, ConditionCacheMissIsDroppedWhenReaderClosesBeforeEof) {
ScopedConditionCacheForTest cache;
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->total_rows = ConditionCacheContext::GRANULE_SIZE;
fake_state->eof_with_first_batch = false;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(0, 0, 0))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.condition_cache_digest = 7,
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_NE(fake_state->condition_cache_ctx, nullptr);
EXPECT_FALSE(fake_state->condition_cache_ctx->is_hit);
ASSERT_TRUE(reader.close().ok());
segment_v2::ConditionCache::ExternalCacheKey key(
"fake-table-reader-input", 0, -1, 7, 0, -1,
segment_v2::ConditionCache::ExternalCacheKey::BASE_GRANULE_AWARE_VERSION);
segment_v2::ConditionCacheHandle handle;
EXPECT_FALSE(cache.get()->lookup(key, &handle));
}
// Scenario: a stop request can arrive while a physical read is in progress. Even if the reader
// converts that stop into eof=true, TableReader must not publish the partially visited MISS bitmap.
TEST(TableReaderTest, ConditionCacheMissIsDroppedWhenStopTurnsReadIntoEof) {
ScopedConditionCacheForTest cache;
std::vector<ColumnDefinition> file_schema;
file_schema.push_back(make_file_column(0, "id", std::make_shared<DataTypeInt32>()));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
auto io_ctx = std::make_shared<io::IOContext>();
RuntimeState state {TQueryOptions(), TQueryGlobals()};
auto fake_state = std::make_shared<FakeFileReaderState>();
fake_state->total_rows = ConditionCacheContext::GRANULE_SIZE * 2;
fake_state->condition_cache_num_granules = 2;
fake_state->stop_during_read = true;
fake_state->io_ctx = io_ctx;
FakeTableReader reader(file_schema, fake_state);
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(0, 0, 0))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = io_ctx,
.runtime_state = &state,
.scanner_profile = nullptr,
.condition_cache_digest = 7,
})
.ok());
SplitReadOptions split_options;
split_options.current_range.__set_path("fake-table-reader-input");
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_TRUE(io_ctx->should_stop);
EXPECT_EQ(block.rows(), 2);
ASSERT_NE(fake_state->condition_cache_ctx, nullptr);
EXPECT_FALSE(fake_state->condition_cache_ctx->is_hit);
segment_v2::ConditionCache::ExternalCacheKey key(
"fake-table-reader-input", 0, -1, 7, 0, -1,
segment_v2::ConditionCache::ExternalCacheKey::BASE_GRANULE_AWARE_VERSION);
segment_v2::ConditionCacheHandle handle;
EXPECT_FALSE(cache.get()->lookup(key, &handle));
ASSERT_TRUE(reader.close().ok());
}
TEST(TableReaderTest, PushDownCountFromNewParquetReader) {
const auto test_dir = std::filesystem::temp_directory_path() / "doris_table_reader_count_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3, 4, 5}, {10, 20, 30, 40, 50},
{"one", "two", "three", "four", "five"}, 2);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns = std::vector<GlobalIndex> {},
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 5);
EXPECT_FALSE(is_column_const(*block.get_by_position(0).column));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, TableLevelCountUsesAssignedRowCount) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_table_count_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
TQueryOptions query_options;
query_options.__set_batch_size(2);
RuntimeState state {query_options, TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns = std::vector<GlobalIndex> {},
})
.ok());
auto split_options = build_split_options(file_path);
set_table_level_row_count(&split_options, 5);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
EXPECT_EQ(block.rows(), 2);
block = build_table_block(projected_columns);
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
EXPECT_EQ(block.rows(), 2);
block = build_table_block(projected_columns);
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
EXPECT_EQ(block.rows(), 1);
block = build_table_block(projected_columns);
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_TRUE(eos);
EXPECT_EQ(block.rows(), 0);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, TableLevelCountRequiresExplicitCountStarArguments) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_table_reader_table_count_arguments_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
set_name_identifiers(&projected_columns);
const std::vector<std::optional<std::vector<GlobalIndex>>> unsafe_count_arguments {
std::nullopt, std::vector<GlobalIndex> {GlobalIndex(0)}};
for (const auto& count_arguments : unsafe_count_arguments) {
RuntimeState state {TQueryOptions(), TQueryGlobals()};
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns = count_arguments,
})
.ok());
auto split_options = build_split_options(file_path);
// Five metadata rows deliberately disagree with the three physical rows. nullopt models an
// old FE, while the non-empty vector models COUNT(id); neither may be treated as COUNT(*).
set_table_level_row_count(&split_options, 5);
ASSERT_TRUE(reader.prepare_split(split_options).ok());
size_t total_rows = 0;
bool eos = false;
while (!eos) {
Block block = build_table_block(projected_columns);
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
total_rows += block.rows();
}
EXPECT_EQ(3, total_rows);
ASSERT_TRUE(reader.close().ok());
}
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, PushDownMinMaxFromNewParquetReader) {
const auto test_dir = std::filesystem::temp_directory_path() / "doris_table_reader_minmax_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {3, 1, 5, 2}, {30, 10, 50, 20},
{"three", "one", "five", "two"}, 2);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
projected_columns.push_back(make_table_column(1, "score", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::MINMAX,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 2);
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 0));
const auto& score_column =
assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 1));
EXPECT_EQ(id_column.get_element(0), 1);
EXPECT_EQ(id_column.get_element(1), 5);
EXPECT_EQ(score_column.get_element(0), 10);
EXPECT_EQ(score_column.get_element(1), 50);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, PushDownMinMaxFallsBackForFileToTableCast) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_minmax_cast_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {3, 1, 5, 2}, {30, 10, 50, 20},
{"three", "one", "five", "two"}, 2);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt64>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::MINMAX,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
auto status = reader.get_block(&block, &eos);
ASSERT_TRUE(status.ok()) << status;
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 2);
const auto& id_column = assert_cast<const ColumnInt64&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(id_column.get_element(0), 3);
EXPECT_EQ(id_column.get_element(1), 1);
block = build_table_block(projected_columns);
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 2);
const auto& second_id_column =
assert_cast<const ColumnInt64&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(second_id_column.get_element(0), 5);
EXPECT_EQ(second_id_column.get_element(1), 2);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, PushDownMinMaxFromProjectedStructLeaf) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_minmax_struct_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_struct_parquet_file(file_path, {3, 1, 5, 2}, 2);
const auto int_type = std::make_shared<DataTypeInt32>();
auto id_child = make_table_column(0, "id", int_type);
auto struct_type = std::make_shared<DataTypeStruct>(DataTypes {int_type}, Strings {"id"});
auto struct_column = make_table_column(100, "s", struct_type);
struct_column.children = {id_child};
std::vector<ColumnDefinition> projected_columns = {struct_column};
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::MINMAX,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
auto status = reader.get_block(&block, &eos);
ASSERT_TRUE(status.ok()) << status;
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 2);
const auto& struct_result =
assert_cast<const ColumnStruct&>(expect_not_null_table_column(block, 0));
ASSERT_EQ(struct_result.get_columns().size(), 1);
const auto& ids = assert_cast<const ColumnInt32&>(
expect_not_null_nullable_nested_column(struct_result.get_column(0)));
EXPECT_EQ(ids.get_element(0), 1);
EXPECT_EQ(ids.get_element(1), 5);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, PushDownMinMaxFallsBackForProjectedListStructLeaf) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_minmax_list_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_list_struct_parquet_file(file_path);
const auto int_type = std::make_shared<DataTypeInt32>();
const auto nullable_int_type = make_nullable(int_type);
auto element_type = std::make_shared<DataTypeStruct>(
DataTypes {nullable_int_type, nullable_int_type}, Strings {"a", "b"});
auto nullable_element_type = make_nullable(element_type);
auto list_column =
make_table_column(100, "xs", std::make_shared<DataTypeArray>(nullable_element_type));
std::vector<ColumnDefinition> projected_columns = {list_column};
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::MINMAX,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
auto status = reader.get_block(&block, &eos);
ASSERT_TRUE(status.ok()) << status;
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 3);
const auto& array_result =
assert_cast<const ColumnArray&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(array_result.get_offsets()[0], 2);
EXPECT_EQ(array_result.get_offsets()[1], 3);
EXPECT_EQ(array_result.get_offsets()[2], 4);
const auto& nullable_elements = assert_cast<const ColumnNullable&>(array_result.get_data());
for (const auto is_null : nullable_elements.get_null_map_data()) {
EXPECT_EQ(is_null, 0);
}
const auto& element_struct =
assert_cast<const ColumnStruct&>(nullable_elements.get_nested_column());
ASSERT_EQ(element_struct.get_columns().size(), 2);
const auto& a_values = assert_cast<const ColumnInt32&>(
expect_not_null_nullable_nested_column(element_struct.get_column(0)));
EXPECT_EQ(a_values.get_element(0), 10);
EXPECT_EQ(a_values.get_element(1), 20);
EXPECT_EQ(a_values.get_element(2), 30);
EXPECT_EQ(a_values.get_element(3), 40);
const auto& b_values = assert_cast<const ColumnInt32&>(
expect_not_null_nullable_nested_column(element_struct.get_column(1)));
EXPECT_EQ(b_values.get_element(0), 11);
EXPECT_EQ(b_values.get_element(1), 21);
EXPECT_EQ(b_values.get_element(2), 31);
EXPECT_EQ(b_values.get_element(3), 41);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ProjectedListStructReadsSelectedElementChild) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_list_projection_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_list_struct_parquet_file(file_path);
const auto int_type = std::make_shared<DataTypeInt32>();
auto a_child = make_table_column(0, "a", int_type);
auto element_type = std::make_shared<DataTypeStruct>(DataTypes {int_type}, Strings {"a"});
auto nullable_element_type = make_nullable(element_type);
auto element_child = make_table_column(0, "element", nullable_element_type);
element_child.children = {a_child};
auto list_column =
make_table_column(100, "xs", std::make_shared<DataTypeArray>(nullable_element_type));
list_column.children = {element_child};
std::vector<ColumnDefinition> projected_columns = {list_column};
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 3);
const auto& array_result =
assert_cast<const ColumnArray&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(array_result.get_offsets()[0], 2);
EXPECT_EQ(array_result.get_offsets()[1], 3);
EXPECT_EQ(array_result.get_offsets()[2], 4);
const auto& nullable_elements = assert_cast<const ColumnNullable&>(array_result.get_data());
const auto& element_struct =
assert_cast<const ColumnStruct&>(nullable_elements.get_nested_column());
ASSERT_EQ(element_struct.get_columns().size(), 1);
const auto& a_values = assert_cast<const ColumnInt32&>(
expect_not_null_nullable_nested_column(element_struct.get_column(0)));
EXPECT_EQ(a_values.get_element(0), 10);
EXPECT_EQ(a_values.get_element(1), 20);
EXPECT_EQ(a_values.get_element(2), 30);
EXPECT_EQ(a_values.get_element(3), 40);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ProjectedListStructReordersRenamedAndMissingElementChildren) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_table_reader_list_schema_evolution_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_list_struct_parquet_file(file_path);
const auto int_type = std::make_shared<DataTypeInt32>();
const auto nullable_int_type = make_nullable(int_type);
const auto string_type = std::make_shared<DataTypeString>();
auto b_child = make_table_column(1, "renamed_b", nullable_int_type);
b_child.name_mapping = {"b"};
auto missing_child = make_table_column(99, "missing_child", string_type);
auto a_child = make_table_column(0, "renamed_a", nullable_int_type);
a_child.name_mapping = {"a"};
auto element_type = std::make_shared<DataTypeStruct>(
DataTypes {nullable_int_type, string_type, nullable_int_type},
Strings {"renamed_b", "missing_child", "renamed_a"});
auto nullable_element_type = make_nullable(element_type);
auto element_child = make_table_column(0, "element", nullable_element_type);
element_child.children = {b_child, missing_child, a_child};
auto list_column =
make_table_column(100, "xs", std::make_shared<DataTypeArray>(nullable_element_type));
list_column.children = {element_child};
std::vector<ColumnDefinition> projected_columns = {list_column};
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 3);
const auto& array_result =
assert_cast<const ColumnArray&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(array_result.get_offsets()[0], 2);
EXPECT_EQ(array_result.get_offsets()[1], 3);
EXPECT_EQ(array_result.get_offsets()[2], 4);
const auto& nullable_elements = assert_cast<const ColumnNullable&>(array_result.get_data());
const auto& element_struct =
assert_cast<const ColumnStruct&>(nullable_elements.get_nested_column());
ASSERT_EQ(element_struct.get_columns().size(), 3);
const auto& b_values = assert_cast<const ColumnInt32&>(
expect_not_null_nullable_nested_column(element_struct.get_column(0)));
const auto& missing_values = element_struct.get_column(1);
const auto& a_values = assert_cast<const ColumnInt32&>(
expect_not_null_nullable_nested_column(element_struct.get_column(2)));
EXPECT_EQ(b_values.get_element(0), 11);
EXPECT_EQ(b_values.get_element(1), 21);
EXPECT_EQ(b_values.get_element(2), 31);
EXPECT_EQ(b_values.get_element(3), 41);
expect_nullable_column_all_null(missing_values);
EXPECT_EQ(a_values.get_element(0), 10);
EXPECT_EQ(a_values.get_element(1), 20);
EXPECT_EQ(a_values.get_element(2), 30);
EXPECT_EQ(a_values.get_element(3), 40);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
// Scenario: when every projected array-element struct child is missing/default-only, the reader
// still receives a full element projection and can materialize the default child without crashing.
TEST(TableReaderTest, ProjectedListStructOnlyMissingElementChildFallsBackToFullElement) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_table_reader_list_only_missing_child_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_list_struct_parquet_file(file_path);
const auto string_type = std::make_shared<DataTypeString>();
auto missing_child = make_table_column(99, "missing_child", string_type);
auto element_type =
std::make_shared<DataTypeStruct>(DataTypes {string_type}, Strings {"missing_child"});
auto nullable_element_type = make_nullable(element_type);
auto element_child = make_table_column(0, "element", nullable_element_type);
element_child.children = {missing_child};
auto list_column =
make_table_column(100, "xs", std::make_shared<DataTypeArray>(nullable_element_type));
list_column.children = {element_child};
std::vector<ColumnDefinition> projected_columns = {list_column};
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 3);
const auto& array_result =
assert_cast<const ColumnArray&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(array_result.get_offsets()[0], 2);
EXPECT_EQ(array_result.get_offsets()[1], 3);
EXPECT_EQ(array_result.get_offsets()[2], 4);
const auto& nullable_elements = assert_cast<const ColumnNullable&>(array_result.get_data());
const auto& element_struct =
assert_cast<const ColumnStruct&>(nullable_elements.get_nested_column());
ASSERT_EQ(element_struct.get_columns().size(), 1);
expect_nullable_column_all_null(element_struct.get_column(0));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, PushDownMinMaxFallsBackForProjectedMapValueStructLeaf) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_minmax_map_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_map_struct_parquet_file(file_path);
const auto key_type = std::make_shared<DataTypeInt32>();
const auto string_type = std::make_shared<DataTypeString>();
const auto nullable_string_type = make_nullable(string_type);
auto b_child = make_table_column(1, "b", nullable_string_type);
auto value_type =
std::make_shared<DataTypeStruct>(DataTypes {nullable_string_type}, Strings {"b"});
auto nullable_value_type = make_nullable(value_type);
auto value_child = make_table_column(1, "value", nullable_value_type);
value_child.children = {b_child};
auto map_column = make_table_column(
100, "kv", std::make_shared<DataTypeMap>(key_type, nullable_value_type));
map_column.children = {value_child};
std::vector<ColumnDefinition> projected_columns = {map_column};
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::MINMAX,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 3);
const auto& map_result = assert_cast<const ColumnMap&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(map_result.get_offsets()[0], 2);
EXPECT_EQ(map_result.get_offsets()[1], 3);
EXPECT_EQ(map_result.get_offsets()[2], 3);
const auto& keys = assert_cast<const ColumnInt32&>(
expect_not_null_nullable_nested_column(map_result.get_keys()));
EXPECT_EQ(keys.get_element(0), 1);
EXPECT_EQ(keys.get_element(1), 2);
EXPECT_EQ(keys.get_element(2), 3);
const auto& nullable_values = assert_cast<const ColumnNullable&>(map_result.get_values());
for (const auto is_null : nullable_values.get_null_map_data()) {
EXPECT_EQ(is_null, 0);
}
const auto& value_struct =
assert_cast<const ColumnStruct&>(nullable_values.get_nested_column());
ASSERT_EQ(value_struct.get_columns().size(), 1);
const auto& b_values = assert_cast<const ColumnString&>(
expect_not_null_nullable_nested_column(value_struct.get_column(0)));
EXPECT_EQ(b_values.get_data_at(0).to_string(), "ma");
EXPECT_EQ(b_values.get_data_at(1).to_string(), "mb");
EXPECT_EQ(b_values.get_data_at(2).to_string(), "mc");
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ProjectedMapValueStructReordersRenamedAndMissingChildren) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_map_schema_evolution_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_map_struct_parquet_file(file_path);
const auto key_type = std::make_shared<DataTypeInt32>();
const auto int_type = std::make_shared<DataTypeInt32>();
const auto nullable_int_type = make_nullable(int_type);
const auto string_type = std::make_shared<DataTypeString>();
const auto nullable_string_type = make_nullable(string_type);
auto b_child = make_table_column(1, "renamed_b", nullable_string_type);
b_child.name_mapping = {"b"};
auto missing_child = make_table_column(99, "missing_child", string_type);
auto a_child = make_table_column(0, "renamed_a", nullable_int_type);
a_child.name_mapping = {"a"};
auto value_type = std::make_shared<DataTypeStruct>(
DataTypes {nullable_string_type, string_type, nullable_int_type},
Strings {"renamed_b", "missing_child", "renamed_a"});
auto nullable_value_type = make_nullable(value_type);
auto value_child = make_table_column(1, "value", nullable_value_type);
value_child.children = {b_child, missing_child, a_child};
auto map_column = make_table_column(
100, "kv", std::make_shared<DataTypeMap>(key_type, nullable_value_type));
map_column.children = {value_child};
std::vector<ColumnDefinition> projected_columns = {map_column};
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 3);
const auto& map_result = assert_cast<const ColumnMap&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(map_result.get_offsets()[0], 2);
EXPECT_EQ(map_result.get_offsets()[1], 3);
EXPECT_EQ(map_result.get_offsets()[2], 3);
const auto& keys = assert_cast<const ColumnInt32&>(
expect_not_null_nullable_nested_column(map_result.get_keys()));
EXPECT_EQ(keys.get_element(0), 1);
EXPECT_EQ(keys.get_element(1), 2);
EXPECT_EQ(keys.get_element(2), 3);
const auto& nullable_values = assert_cast<const ColumnNullable&>(map_result.get_values());
const auto& value_struct =
assert_cast<const ColumnStruct&>(nullable_values.get_nested_column());
ASSERT_EQ(value_struct.get_columns().size(), 3);
const auto& b_values = assert_cast<const ColumnString&>(
expect_not_null_nullable_nested_column(value_struct.get_column(0)));
const auto& missing_values = value_struct.get_column(1);
const auto& a_values = assert_cast<const ColumnInt32&>(
expect_not_null_nullable_nested_column(value_struct.get_column(2)));
EXPECT_EQ(b_values.get_data_at(0).to_string(), "ma");
EXPECT_EQ(b_values.get_data_at(1).to_string(), "mb");
EXPECT_EQ(b_values.get_data_at(2).to_string(), "mc");
expect_nullable_column_all_null(missing_values);
EXPECT_EQ(a_values.get_element(0), 10);
EXPECT_EQ(a_values.get_element(1), 20);
EXPECT_EQ(a_values.get_element(2), 30);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, MaterializeMapKeyStructReordersRenamedChildren) {
const auto int_type = std::make_shared<DataTypeInt32>();
const auto string_type = std::make_shared<DataTypeString>();
const auto file_key_type =
std::make_shared<DataTypeStruct>(DataTypes {int_type, string_type}, Strings {"a", "b"});
const auto table_key_type = std::make_shared<DataTypeStruct>(
DataTypes {string_type, int_type}, Strings {"renamed_b", "renamed_a"});
const auto file_map_type = std::make_shared<DataTypeMap>(file_key_type, int_type);
const auto table_map_type = std::make_shared<DataTypeMap>(table_key_type, int_type);
ColumnMapping a_mapping;
a_mapping.table_column_name = "renamed_a";
a_mapping.file_column_name = "a";
a_mapping.file_local_id = 0;
a_mapping.table_type = int_type;
a_mapping.file_type = int_type;
a_mapping.is_trivial = true;
ColumnMapping b_mapping;
b_mapping.table_column_name = "renamed_b";
b_mapping.file_column_name = "b";
b_mapping.file_local_id = 1;
b_mapping.table_type = string_type;
b_mapping.file_type = string_type;
b_mapping.is_trivial = true;
ColumnMapping key_mapping;
key_mapping.table_column_name = "key";
key_mapping.file_column_name = "key";
key_mapping.file_local_id = 0;
key_mapping.table_type = table_key_type;
key_mapping.file_type = file_key_type;
key_mapping.is_trivial = false;
key_mapping.child_mappings = {b_mapping, a_mapping};
ColumnMapping value_mapping;
value_mapping.table_column_name = "value";
value_mapping.file_column_name = "value";
value_mapping.file_local_id = 1;
value_mapping.table_type = int_type;
value_mapping.file_type = int_type;
value_mapping.is_trivial = true;
ColumnMapping map_mapping;
map_mapping.table_column_name = "kv";
map_mapping.file_column_name = "kv";
map_mapping.table_type = table_map_type;
map_mapping.file_type = file_map_type;
map_mapping.is_trivial = false;
map_mapping.child_mappings = {key_mapping, value_mapping};
auto a_keys = ColumnInt32::create();
a_keys->insert_value(10);
a_keys->insert_value(20);
a_keys->insert_value(30);
auto b_keys = ColumnString::create();
b_keys->insert_value("x");
b_keys->insert_value("y");
b_keys->insert_value("z");
MutableColumns key_children;
key_children.push_back(std::move(a_keys));
key_children.push_back(std::move(b_keys));
auto key_column = ColumnStruct::create(std::move(key_children));
auto value_column = ColumnInt32::create();
value_column->insert_value(100);
value_column->insert_value(200);
value_column->insert_value(300);
auto offsets_column = ColumnArray::ColumnOffsets::create();
offsets_column->insert_value(2);
offsets_column->insert_value(3);
ColumnPtr file_column = ColumnMap::create(std::move(key_column), std::move(value_column),
std::move(offsets_column));
TableReaderMaterializeTestHelper reader;
ColumnPtr result_column;
ASSERT_TRUE(reader._materialize_map_mapping_column(map_mapping, file_column, 2, &result_column)
.ok());
const auto& result_map = assert_cast<const ColumnMap&>(*result_column);
EXPECT_EQ(result_map.get_offsets()[0], 2);
EXPECT_EQ(result_map.get_offsets()[1], 3);
const auto& result_key = assert_cast<const ColumnStruct&>(result_map.get_keys());
ASSERT_EQ(result_key.get_columns().size(), 2);
const auto& b_result = assert_cast<const ColumnString&>(result_key.get_column(0));
const auto& a_result = assert_cast<const ColumnInt32&>(result_key.get_column(1));
EXPECT_EQ(b_result.get_data_at(0).to_string(), "x");
EXPECT_EQ(b_result.get_data_at(1).to_string(), "y");
EXPECT_EQ(b_result.get_data_at(2).to_string(), "z");
EXPECT_EQ(a_result.get_element(0), 10);
EXPECT_EQ(a_result.get_element(1), 20);
EXPECT_EQ(a_result.get_element(2), 30);
const auto& result_value = assert_cast<const ColumnInt32&>(result_map.get_values());
EXPECT_EQ(result_value.get_element(0), 100);
EXPECT_EQ(result_value.get_element(1), 200);
EXPECT_EQ(result_value.get_element(2), 300);
}
// Scenario: map value struct materialization follows DataTypeStruct field order even when
// ColumnMapping children arrive in a different order from projected ColumnDefinition children.
TEST(TableReaderTest, MaterializeMapValueStructUsesTableTypeOrder) {
const auto key_type = std::make_shared<DataTypeString>();
const auto string_type = std::make_shared<DataTypeString>();
const auto file_value_type = std::make_shared<DataTypeStruct>(
DataTypes {string_type, string_type}, Strings {"full_name", "gender"});
const auto table_value_type = std::make_shared<DataTypeStruct>(
DataTypes {string_type, string_type}, Strings {"full_name", "gender"});
const auto file_map_type = std::make_shared<DataTypeMap>(key_type, file_value_type);
const auto table_map_type = std::make_shared<DataTypeMap>(key_type, table_value_type);
ColumnMapping full_name_mapping;
full_name_mapping.table_column_name = "full_name";
full_name_mapping.file_column_name = "full_name";
full_name_mapping.file_local_id = 0;
full_name_mapping.table_type = string_type;
full_name_mapping.file_type = string_type;
full_name_mapping.is_trivial = true;
ColumnMapping gender_mapping;
gender_mapping.table_column_name = "gender";
gender_mapping.file_column_name = "gender";
gender_mapping.file_local_id = 1;
gender_mapping.table_type = string_type;
gender_mapping.file_type = string_type;
gender_mapping.is_trivial = true;
ColumnMapping value_mapping;
value_mapping.table_column_name = "value";
value_mapping.file_column_name = "value";
value_mapping.file_local_id = 1;
value_mapping.table_type = table_value_type;
value_mapping.file_type = file_value_type;
value_mapping.is_trivial = false;
value_mapping.child_mappings = {gender_mapping, full_name_mapping};
ColumnMapping key_mapping;
key_mapping.table_column_name = "key";
key_mapping.file_column_name = "key";
key_mapping.file_local_id = 0;
key_mapping.table_type = key_type;
key_mapping.file_type = key_type;
key_mapping.is_trivial = true;
ColumnMapping map_mapping;
map_mapping.table_column_name = "new_map_column";
map_mapping.file_column_name = "new_map_column";
map_mapping.table_type = table_map_type;
map_mapping.file_type = file_map_type;
map_mapping.is_trivial = false;
map_mapping.child_mappings = {key_mapping, value_mapping};
auto key_column = ColumnString::create();
key_column->insert_value("person10");
key_column->insert_value("person20");
auto full_name_column = ColumnString::create();
full_name_column->insert_value("Jack");
full_name_column->insert_value("James Lee");
auto gender_column = ColumnString::create();
gender_column->insert_value("Male");
gender_column->insert_value("Male");
MutableColumns value_children;
value_children.push_back(std::move(full_name_column));
value_children.push_back(std::move(gender_column));
auto value_column = ColumnStruct::create(std::move(value_children));
auto offsets_column = ColumnArray::ColumnOffsets::create();
offsets_column->insert_value(1);
offsets_column->insert_value(2);
ColumnPtr file_column = ColumnMap::create(std::move(key_column), std::move(value_column),
std::move(offsets_column));
TableReaderMaterializeTestHelper reader;
ColumnPtr result_column;
ASSERT_TRUE(reader._materialize_map_mapping_column(map_mapping, file_column, 2, &result_column)
.ok());
const auto& result_map = assert_cast<const ColumnMap&>(*result_column);
const auto& result_value = assert_cast<const ColumnStruct&>(result_map.get_values());
ASSERT_EQ(result_value.get_columns().size(), 2);
const auto& full_name_result = assert_cast<const ColumnString&>(result_value.get_column(0));
const auto& gender_result = assert_cast<const ColumnString&>(result_value.get_column(1));
EXPECT_EQ(full_name_result.get_data_at(0).to_string(), "Jack");
EXPECT_EQ(full_name_result.get_data_at(1).to_string(), "James Lee");
EXPECT_EQ(gender_result.get_data_at(0).to_string(), "Male");
EXPECT_EQ(gender_result.get_data_at(1).to_string(), "Male");
}
TEST(TableReaderTest, PushDownMinMaxOnlyUsesSelectedRowGroupInFileRange) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_minmax_range_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {10, 1, 100}, {100, 10, 1000}, {"ten", "one", "hundred"},
1);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::MINMAX,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options_for_row_group_mid(file_path, 1)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 2);
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(id_column.get_element(0), 1);
EXPECT_EQ(id_column.get_element(1), 1);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, PushDownCountOnlyUsesSelectedRowGroupInFileRange) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_count_range_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"}, 1);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns = std::vector<GlobalIndex> {},
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options_for_row_group_mid(file_path, 2)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 1);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, PushDownCountFallsBackWithTableConjunct) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_count_conjunct_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(0, 0, 2))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns = std::vector<GlobalIndex> {},
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 1);
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(id_column.get_element(0), 3);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, PushDownCountFallsBackWithFilter) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_count_predicate_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30}, {"one", "two", "three"}, 1);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(0, 0, 2))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::COUNT,
.push_down_count_columns = std::vector<GlobalIndex> {},
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 1);
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 0));
EXPECT_EQ(id_column.get_element(0), 3);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, PushDownMinMaxFallsBackWithoutDirectFileMapping) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_table_reader_minmax_missing_mapping_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_parquet_file(file_path, 1, "one");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(
make_table_column(99, "missing_id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
.push_down_agg_type = TPushAggOp::type::MINMAX,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 1);
expect_nullable_column_all_null(*block.get_by_position(0).column);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, OpenReaderBuildsTableFiltersFromConjuncts) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_conjunct_filter_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_parquet_file(file_path, 3, "three");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(1, "value", std::make_shared<DataTypeString>()));
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(1, 1, 2))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
// open_reader() should convert the table-level conjunct on projected column id 1 into
// _table_filters before ColumnMapper creates the FileScanRequest. ColumnMapper then rewrites
// the conjunct's slot ref from table column id 1 to the file-local block position used by
// ParquetReader. The projection order intentionally puts value before id, so the id filter
// column is not at position 0 in the file block.
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 1));
ASSERT_EQ(id_column.size(), 1);
EXPECT_EQ(id_column.get_element(0), 3);
ASSERT_TRUE(reader.close().ok());
TableReader filtered_reader;
ASSERT_TRUE(filtered_reader
.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(1, 1, 4))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(filtered_reader.prepare_split(build_split_options(file_path)).ok());
block = build_table_block(projected_columns);
eos = false;
ASSERT_TRUE(filtered_reader.get_block(&block, &eos).ok());
EXPECT_TRUE(eos);
EXPECT_EQ(block.get_by_position(1).column->size(), 0);
ASSERT_TRUE(filtered_reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, OpenReaderPushesVExprPredicateToParquetReader) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_column_predicate_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
// Keep one row per row group so the VExpr ZoneMap path can prune the first two row groups and
// leave only id = 3.
write_int_pair_parquet_file(file_path, {1, 2, 3}, {1, 5, 8}, {"one", "two", "three"}, 1);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(2, "value", std::make_shared<DataTypeString>()));
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(1, 1, 2))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto& value_column =
assert_cast<const ColumnString&>(expect_not_null_table_column(block, 0));
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 1));
ASSERT_EQ(id_column.size(), 1);
ASSERT_EQ(value_column.size(), 1);
EXPECT_EQ(id_column.get_element(0), 3);
EXPECT_EQ(value_column.get_data_at(0).to_string(), "three");
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, VExprPredicateSurvivesReopenSplit) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_predicate_reopen_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const std::vector<std::string> file_paths = {
(test_dir / "split_1.parquet").string(),
(test_dir / "split_2.parquet").string(),
};
write_int_pair_parquet_file(file_paths[0], {1, 3}, {10, 30}, {"one", "three"}, 1);
write_int_pair_parquet_file(file_paths[1], {2, 4}, {20, 40}, {"two", "four"}, 1);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(0, 0, 2))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
std::vector<int32_t> ids;
for (const auto& file_path : file_paths) {
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto& id_column =
assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 0));
ASSERT_EQ(id_column.size(), 1);
ids.push_back(id_column.get_element(0));
ASSERT_TRUE(reader.close().ok());
}
EXPECT_EQ(ids, std::vector<int32_t>({3, 4}));
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, CreateScanRequestDeduplicatesSharedPredicateColumns) {
const auto int_type = std::make_shared<DataTypeInt32>();
const std::vector<ColumnDefinition> projected_columns = {
make_table_column(0, "a", int_type),
make_table_column(1, "b", int_type),
make_table_column(2, "c", int_type),
make_table_column(3, "value", std::make_shared<DataTypeString>()),
};
const std::vector<ColumnDefinition> file_schema = {
make_file_column(0, "a", int_type),
make_file_column(1, "b", int_type),
make_file_column(2, "c", int_type),
make_file_column(3, "value", std::make_shared<DataTypeString>()),
};
TableColumnMapper mapper;
ASSERT_TRUE(mapper.create_mapping(projected_columns, {}, file_schema).ok());
std::vector<TableFilter> table_filters;
table_filters.push_back({
.conjunct =
VExprContext::create_shared(table_int32_sum_greater_than_expr(0, 0, 1, 1, 1)),
.global_indices = {GlobalIndex(0), GlobalIndex(1)},
});
table_filters.push_back({
.conjunct =
VExprContext::create_shared(table_int32_sum_greater_than_expr(0, 0, 2, 2, 1)),
.global_indices = {GlobalIndex(0), GlobalIndex(2)},
});
FileScanRequest file_request;
ASSERT_TRUE(mapper.create_scan_request(table_filters, projected_columns, &file_request).ok());
// Both filters reference column a. It must still be read once as a predicate column, and a
// predicate column must not be repeated as a non-predicate column.
EXPECT_EQ(projection_ids(file_request.predicate_columns), std::vector<int32_t>({0, 1, 2}));
EXPECT_EQ(projection_ids(file_request.non_predicate_columns), std::vector<int32_t>({3}));
ASSERT_EQ(file_request.local_positions.size(), 4);
EXPECT_EQ(file_request.local_positions.at(LocalColumnId(3)).value(), 0);
EXPECT_EQ(file_request.local_positions.at(LocalColumnId(0)).value(), 1);
EXPECT_EQ(file_request.local_positions.at(LocalColumnId(1)).value(), 2);
EXPECT_EQ(file_request.local_positions.at(LocalColumnId(2)).value(), 3);
const auto predicate_column_ids = projection_ids(file_request.predicate_columns);
const auto non_predicate_column_ids = projection_ids(file_request.non_predicate_columns);
for (const auto predicate_column_id : predicate_column_ids) {
EXPECT_TRUE(std::find(non_predicate_column_ids.begin(), non_predicate_column_ids.end(),
predicate_column_id) == non_predicate_column_ids.end());
}
}
TEST(TableReaderTest, CreateScanRequestPromotesProjectedColumnToPredicateColumn) {
const auto int_type = std::make_shared<DataTypeInt32>();
const std::vector<ColumnDefinition> projected_columns = {
make_table_column(0, "id", int_type),
make_table_column(1, "score", int_type),
};
const std::vector<ColumnDefinition> file_schema = {
make_file_column(0, "id", int_type),
make_file_column(1, "score", int_type),
};
TableColumnMapper mapper;
ASSERT_TRUE(mapper.create_mapping(projected_columns, {}, file_schema).ok());
TableFilter table_filter {
.conjunct = VExprContext::create_shared(table_int32_greater_than_expr(0, 0, 1)),
.global_indices = {GlobalIndex(0)},
};
FileScanRequest file_request;
ASSERT_TRUE(mapper.create_scan_request({table_filter}, projected_columns, &file_request).ok());
EXPECT_EQ(projection_ids(file_request.predicate_columns), std::vector<int32_t>({0}));
EXPECT_EQ(projection_ids(file_request.non_predicate_columns), std::vector<int32_t>({1}));
ASSERT_EQ(file_request.local_positions.size(), 2);
EXPECT_EQ(file_request.local_positions.at(LocalColumnId(0)).value(), 1);
EXPECT_EQ(file_request.local_positions.at(LocalColumnId(1)).value(), 0);
}
TEST(TableReaderTest, CreateScanRequestUsesColumnNameForByNamePredicateMapping) {
const auto int_type = std::make_shared<DataTypeInt32>();
std::vector<ColumnDefinition> projected_columns = {
make_table_column(10, "id", int_type),
make_table_column(11, "score", int_type),
};
const std::vector<ColumnDefinition> file_schema = {
make_file_column(0, "ID", int_type),
make_file_column(1, "score", int_type),
};
TableColumnMapper mapper({.mode = TableColumnMappingMode::BY_NAME});
set_name_identifiers(&projected_columns);
ASSERT_TRUE(mapper.create_mapping(projected_columns, {}, file_schema).ok());
TableFilter table_filter {
.conjunct = VExprContext::create_shared(table_int32_greater_than_expr(0, 0, 1)),
.global_indices = {GlobalIndex(0)},
};
FileScanRequest file_request;
ASSERT_TRUE(mapper.create_scan_request({table_filter}, projected_columns, &file_request).ok());
EXPECT_EQ(projection_ids(file_request.predicate_columns), std::vector<int32_t>({0}));
EXPECT_EQ(projection_ids(file_request.non_predicate_columns), std::vector<int32_t>({1}));
ASSERT_EQ(file_request.conjuncts.size(), 1);
const auto* localized_slot =
assert_cast<const VSlotRef*>(file_request.conjuncts[0]->root()->children()[0].get());
EXPECT_EQ(localized_slot->slot_id(), 0);
EXPECT_EQ(localized_slot->column_id(), 1);
}
TEST(TableReaderTest, OpenReaderPushesMultiColumnConjunctToParquetReader) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_multi_conjunct_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {1, 5, 8}, {"one", "two", "three"});
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(2, "value", std::make_shared<DataTypeString>()));
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
projected_columns.push_back(make_table_column(1, "score", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(
reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_sum_greater_than_expr(1, 1, 2, 2, 8))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
// The conjunct references both id and score, so ColumnMapper must put both file columns into
// predicate_columns and rewrite both slot refs to ParquetReader's file-local block positions.
// ParquetReader then evaluates the expression after all predicate columns have been read.
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto& value_column =
assert_cast<const ColumnString&>(expect_not_null_table_column(block, 0));
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 1));
const auto& score_column =
assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 2));
ASSERT_EQ(id_column.size(), 1);
ASSERT_EQ(score_column.size(), 1);
ASSERT_EQ(value_column.size(), 1);
EXPECT_EQ(id_column.get_element(0), 3);
EXPECT_EQ(score_column.get_element(0), 8);
EXPECT_EQ(value_column.get_data_at(0).to_string(), "three");
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ProjectedColumnsFillDefaultForParquetSchemaMismatch) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_schema_mismatch_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_parquet_file(file_path, 1, "one");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(
make_table_column(99, "missing_value", std::make_shared<DataTypeString>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
// The table projection asks for field id 99, but the ParquetReader exposes only file-local
// fields 0 and 1. Missing columns are allowed by the current mapper options, so TableReader
// should still use the Parquet row count and fill a default column in table schema.
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
EXPECT_EQ(block.get_by_position(0).column->size(), 1);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, DefaultExprResultMatchesNullableTableType) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_nullable_default_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_parquet_file(file_path, 1, "one");
const auto int_type = std::make_shared<DataTypeInt32>();
auto missing_column = make_table_column(99, "c_new", make_nullable(int_type));
missing_column.default_expr = VExprContext::create_shared(
VLiteral::create_shared(int_type, Field::create_field<TYPE_INT>(42)));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(std::move(missing_column));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
auto status = reader.get_block(&block, &eos);
ASSERT_TRUE(status.ok()) << status.to_string();
ASSERT_FALSE(eos);
const auto& result = block.get_by_position(0);
ASSERT_TRUE(result.check_type_and_column_match().ok());
EXPECT_TRUE(result.type->is_nullable());
ASSERT_TRUE(result.column->is_nullable());
const auto& nullable_column = assert_cast<const ColumnNullable&>(*result.column);
ASSERT_EQ(nullable_column.size(), 1);
EXPECT_EQ(nullable_column.get_null_map_data()[0], 0);
const auto& values = assert_cast<const ColumnInt32&>(nullable_column.get_nested_column());
EXPECT_EQ(values.get_element(0), 42);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, DefaultExprAlignsNestedNullableArrayTableType) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_table_reader_nested_nullable_array_default_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_parquet_file(file_path, 1, "one");
const auto bigint_type = std::make_shared<DataTypeInt64>();
const auto array_type = std::make_shared<DataTypeArray>(make_nullable(bigint_type));
const auto table_type = make_nullable(array_type);
auto missing_column = make_table_column(99, "single_element_groups", table_type);
missing_column.default_expr = VExprContext::create_shared(
std::make_shared<NullableArrayBigintDefaultExpr>(table_type));
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(std::move(missing_column));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
auto status = reader.get_block(&block, &eos);
ASSERT_TRUE(status.ok()) << status.to_string();
ASSERT_FALSE(eos);
const auto& result = block.get_by_position(0);
ASSERT_TRUE(result.check_type_and_column_match().ok());
ASSERT_TRUE(result.column->is_nullable());
const auto& nullable_column = assert_cast<const ColumnNullable&>(*result.column);
ASSERT_EQ(nullable_column.size(), 1);
EXPECT_EQ(nullable_column.get_null_map_data()[0], 0);
const auto& array_column = assert_cast<const ColumnArray&>(nullable_column.get_nested_column());
ASSERT_EQ(array_column.size(), 1);
EXPECT_EQ(array_column.get_offsets()[0], 1);
ASSERT_TRUE(array_column.get_data().is_nullable());
const auto& nested_nullable = assert_cast<const ColumnNullable&>(array_column.get_data());
ASSERT_EQ(nested_nullable.size(), 1);
EXPECT_EQ(nested_nullable.get_null_map_data()[0], 0);
const auto& values = assert_cast<const ColumnInt64&>(nested_nullable.get_nested_column());
EXPECT_EQ(values.get_element(0), 7);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ProjectedColumnsFillMissingParquetColumnWithDefault) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_table_reader_schema_mismatch_reject_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_parquet_file(file_path, 1, "one");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(
make_table_column(99, "missing_value", std::make_shared<DataTypeString>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
const auto status = reader.get_block(&block, &eos);
ASSERT_TRUE(status.ok()) << status.to_string();
ASSERT_FALSE(eos);
const auto& result = block.get_by_position(0);
ASSERT_TRUE(result.check_type_and_column_match().ok());
// A missing scalar column without an explicit default is materialized as a default-value
// column. It may stay constant, so verify through the IColumn interface instead of assuming a
// concrete ColumnString instance.
ASSERT_EQ(result.column->size(), 1);
EXPECT_EQ(result.column->get_data_at(0).to_string(), "");
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ProjectedStructFillsMissingChildWithDefault) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_struct_missing_child_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_struct_parquet_file(file_path, 7);
const auto int_type = std::make_shared<DataTypeInt32>();
const auto string_type = std::make_shared<DataTypeString>();
auto id_child = make_table_column(0, "id", int_type);
auto missing_child = make_table_column(99, "missing_child", string_type);
auto struct_type = std::make_shared<DataTypeStruct>(DataTypes {int_type, string_type},
Strings {"id", "missing_child"});
auto struct_column = make_table_column(100, "s", struct_type);
struct_column.children = {id_child, missing_child};
std::vector<ColumnDefinition> projected_columns = {struct_column};
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto& struct_result =
assert_cast<const ColumnStruct&>(expect_not_null_table_column(block, 0));
ASSERT_EQ(struct_result.get_columns().size(), 2);
const auto& ids = assert_cast<const ColumnInt32&>(
expect_not_null_nullable_nested_column(struct_result.get_column(0)));
ASSERT_EQ(struct_result.size(), 1);
EXPECT_EQ(ids.get_element(0), 7);
expect_nullable_column_all_null(struct_result.get_column(1));
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ReusedBlockClearsProjectedStructWithNullableChild) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_table_reader_struct_nullable_child_reuse_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_struct_with_nullable_child_parquet_file(file_path);
const auto int_type = std::make_shared<DataTypeInt32>();
const auto string_type = std::make_shared<DataTypeString>();
const auto nullable_string_type = make_nullable(string_type);
auto id_child = make_table_column(0, "id", int_type);
auto note_child = make_table_column(1, "note", nullable_string_type);
auto missing_child = make_table_column(99, "missing_child", string_type);
auto struct_type = std::make_shared<DataTypeStruct>(
DataTypes {int_type, nullable_string_type, string_type},
Strings {"id", "note", "missing_child"});
auto struct_column = make_table_column(100, "s", struct_type);
struct_column.children = {id_child, note_child, missing_child};
std::vector<ColumnDefinition> projected_columns = {struct_column};
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.rows(), 2);
const auto& struct_result =
assert_cast<const ColumnStruct&>(expect_not_null_table_column(block, 0));
const auto& notes = assert_cast<const ColumnNullable&>(struct_result.get_column(1));
EXPECT_FALSE(notes.is_null_at(0));
EXPECT_TRUE(notes.is_null_at(1));
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_TRUE(eos);
EXPECT_EQ(block.rows(), 0);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ProjectedRenamedStructPreservesParentDeclaredChildNullability) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_table_reader_struct_parent_child_nullability_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_struct_with_nullable_child_parquet_file(file_path);
const auto string_type = std::make_shared<DataTypeString>();
const auto nullable_string_type = make_nullable(string_type);
auto renamed_note = make_table_column(1, "renamed_note", nullable_string_type);
renamed_note.name_mapping = {"note"};
// Iceberg nested schema metadata can omit nullability on this child while the parent DataType
// remains authoritative and declares it nullable.
renamed_note.type = string_type;
auto struct_type = std::make_shared<DataTypeStruct>(DataTypes {nullable_string_type},
Strings {"renamed_note"});
auto struct_column = make_table_column(100, "s", struct_type);
struct_column.children = {renamed_note};
std::vector<ColumnDefinition> projected_columns = {struct_column};
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
const auto status = reader.get_block(&block, &eos);
ASSERT_TRUE(status.ok()) << status.to_string();
ASSERT_FALSE(eos);
const auto& struct_result =
assert_cast<const ColumnStruct&>(expect_not_null_table_column(block, 0));
ASSERT_EQ(struct_result.get_columns().size(), 1);
const auto& notes = assert_cast<const ColumnNullable&>(struct_result.get_column(0));
EXPECT_FALSE(notes.is_null_at(0));
EXPECT_EQ(notes.get_data_at(0).to_string(), "seven");
EXPECT_TRUE(notes.is_null_at(1));
ASSERT_TRUE(block.get_by_position(0).check_type_and_column_match().ok());
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ProjectedPartitionColumnUsesSplitPartitionValue) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_partition_value_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_parquet_file(file_path, 1, "one");
std::vector<ColumnDefinition> projected_columns;
auto partition_column = make_table_column(1, "value", std::make_shared<DataTypeString>());
partition_column.is_partition_key = true;
projected_columns.push_back(std::move(partition_column));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.partition_values.emplace("value", Field::create_field<TYPE_STRING>("p1"));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
// The file has a physical column with the same id/name. The split partition value should still
// take precedence and be materialized by TableReader.
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto partition_value = block.get_by_position(0).column->convert_to_full_column_if_const();
const auto& partition_value_data = assert_cast<const ColumnString&>(
expect_not_null_nullable_nested_column(*partition_value));
ASSERT_EQ(partition_value_data.size(), 1);
EXPECT_EQ(partition_value_data.get_data_at(0).to_string(), "p1");
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ConstantPartitionFilterSkipsSplitWhenFalse) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_table_reader_constant_partition_filter_skip_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_parquet_file(file_path, 1, "one");
std::vector<ColumnDefinition> projected_columns;
auto partition_column = make_table_column(0, "part", std::make_shared<DataTypeInt32>());
partition_column.is_partition_key = true;
projected_columns.push_back(std::move(partition_column));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(0, 0, 10))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.partition_values.emplace("part", Field::create_field<TYPE_INT>(7));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_TRUE(eos);
EXPECT_EQ(block.get_by_position(0).column->size(), 0);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ConstantPartitionFilterKeepsSplitWhenTrue) {
const auto test_dir = std::filesystem::temp_directory_path() /
"doris_table_reader_constant_partition_filter_keep_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_parquet_file(file_path, 1, "one");
std::vector<ColumnDefinition> projected_columns;
auto partition_column = make_table_column(0, "part", std::make_shared<DataTypeInt32>());
partition_column.is_partition_key = true;
projected_columns.push_back(std::move(partition_column));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, table_int32_greater_than_expr(0, 0, 1))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.partition_values.emplace("part", Field::create_field<TYPE_INT>(7));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
expect_int32_column_values(*block.get_by_position(0).column, {7});
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, RuntimeFilterOnConstantPartitionIsNotPreExecuted) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_constant_runtime_filter";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_parquet_file(file_path, 1, "one");
std::vector<ColumnDefinition> projected_columns;
auto partition_column = make_table_column(0, "part", std::make_shared<DataTypeInt32>());
partition_column.is_partition_key = true;
projected_columns.push_back(std::move(partition_column));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(
reader.init({
.projected_columns = projected_columns,
.conjuncts = {prepared_conjunct(
&state, runtime_filter_wrapper_expr(
table_int32_greater_than_expr(0, 0, 1)))},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
auto split_options = build_split_options(file_path);
split_options.partition_values.emplace("part", Field::create_field<TYPE_INT>(7));
ASSERT_TRUE(reader.prepare_split(split_options).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
const auto status = reader.get_block(&block, &eos);
ASSERT_TRUE(status.ok()) << status.to_string();
ASSERT_FALSE(eos);
expect_int32_column_values(*block.get_by_position(0).column, {7});
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ParquetReaderReadsOnlyRowGroupsInFileRange) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_file_range_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_int_pair_parquet_file(file_path, {1, 2, 3}, {10, 20, 30},
{"range_group_one", "range_group_two", "range_group_three"}, 1);
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt32>()));
projected_columns.push_back(make_table_column(2, "value", std::make_shared<DataTypeString>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options_for_row_group_mid(file_path, 1)).ok());
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 0));
const auto& value_column =
assert_cast<const ColumnString&>(expect_not_null_table_column(block, 1));
ASSERT_EQ(block.rows(), 1);
EXPECT_EQ(id_column.get_element(0), 2);
EXPECT_EQ(value_column.get_data_at(0).to_string(), "range_group_two");
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
EXPECT_TRUE(eos);
EXPECT_EQ(block.rows(), 0);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ProjectedColumnsUseMapperExpressionForSameNameDifferentIdParquetSchema) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_same_name_diff_id_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_parquet_file(file_path, 1, "one");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(99, "id", std::make_shared<DataTypeInt32>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
// The table column has the same name as the Parquet field, but a different field id.
// ColumnMapper should still resolve it by name and build a SlotRef projection from the file
// column into the requested table column.
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
const auto& id_column = assert_cast<const ColumnInt32&>(expect_not_null_table_column(block, 0));
ASSERT_EQ(id_column.size(), 1);
EXPECT_EQ(id_column.get_element(0), 1);
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
TEST(TableReaderTest, ProjectedColumnsUseMapperExpressionsForParquetSchemaMismatch) {
const auto test_dir =
std::filesystem::temp_directory_path() / "doris_table_reader_mapper_expr_test";
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
const auto file_path = (test_dir / "split.parquet").string();
write_parquet_file(file_path, 7, "seven");
std::vector<ColumnDefinition> projected_columns;
projected_columns.push_back(make_table_column(0, "id", std::make_shared<DataTypeInt64>()));
projected_columns.push_back(make_table_column(1, "value", std::make_shared<DataTypeString>()));
RuntimeState state {TQueryOptions(), TQueryGlobals()};
set_name_identifiers(&projected_columns);
TableReader reader;
ASSERT_TRUE(reader.init({
.projected_columns = projected_columns,
.conjuncts = {},
.format = FileFormat::PARQUET,
.scan_params = nullptr,
.io_ctx = nullptr,
.runtime_state = &state,
.scanner_profile = nullptr,
})
.ok());
ASSERT_TRUE(reader.prepare_split(build_split_options(file_path)).ok());
// The table projection requests id as BIGINT instead of the file INT, so ColumnMapper should
// build a Cast expression. The second field has the same type and should build a SlotRef
// projection. Both columns should still materialize in table schema order.
Block block = build_table_block(projected_columns);
bool eos = false;
ASSERT_TRUE(reader.get_block(&block, &eos).ok());
ASSERT_FALSE(eos);
ASSERT_EQ(block.get_by_position(0).name, "id");
ASSERT_EQ(block.get_by_position(1).name, "value");
const auto& id_column = assert_cast<const ColumnInt64&>(expect_not_null_table_column(block, 0));
const auto& value_column =
assert_cast<const ColumnString&>(expect_not_null_table_column(block, 1));
ASSERT_EQ(id_column.size(), 1);
ASSERT_EQ(value_column.size(), 1);
EXPECT_EQ(id_column.get_element(0), 7);
EXPECT_EQ(value_column.get_data_at(0).to_string(), "seven");
ASSERT_TRUE(reader.close().ok());
std::filesystem::remove_all(test_dir);
}
} // namespace
} // namespace doris::format