This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 9c2d8d36e37 [fix](adbc) Materialize Arrow list view arrays (#66819)
9c2d8d36e37 is described below
commit 9c2d8d36e3715b824f9719ee719590c727c444a0
Author: Gabriel <[email protected]>
AuthorDate: Fri Aug 21 10:11:19 2026 +0800
[fix](adbc) Materialize Arrow list view arrays (#66819)
### What problem does this PR solve?
ADBC maps Arrow ListView and LargeListView fields to Doris arrays, but
these encoding variants must be converted to canonical lists before
materialization. Incremental child-builder growth can also cause
avoidable allocation spikes, and nested views can overflow before a
parent builder reports capacity. A source schema can also become
nullable while a cached Doris target remains non-nullable.
### What is changed and how does it work?
- Validate ListView ranges before copying child values.
- Preflight the total logical child length before any builder mutation
and reject offset overflow.
- Reject nested ListView encodings before allocation until recursive
canonicalization is supported.
- Reserve the expanded child capacity once before appending values.
- Route all normalization allocations through a caller-owned
Doris-tracked Arrow memory pool.
- Reject runtime null rows when the cached Doris target is non-nullable,
preventing null arrays from becoming empty arrays.
- Add allocation-peak, no-allocation overflow, nullable-slice,
malformed-range, required-target, and ADBC materialization regressions.
### Check List
- [x] Arrow normalizer and ADBC reader ASAN tests (25 tests)
- [x] clang-format 16
---
be/src/format/arrow/arrow_array_normalizer.cpp | 199 +++++++++-
be/src/format/arrow/arrow_array_normalizer.h | 12 +-
be/src/format_v2/column_mapper.cpp | 31 +-
be/src/format_v2/table/adbc_reader.cpp | 171 ++++++++-
be/src/format_v2/table/adbc_reader.h | 2 +
.../format/arrow/arrow_array_normalizer_test.cpp | 367 ++++++++++++++++++-
be/test/format_v2/table/adbc_reader_test.cpp | 406 +++++++++++++++++++++
7 files changed, 1160 insertions(+), 28 deletions(-)
diff --git a/be/src/format/arrow/arrow_array_normalizer.cpp
b/be/src/format/arrow/arrow_array_normalizer.cpp
index 4b4483cb922..4f8aa4df38c 100644
--- a/be/src/format/arrow/arrow_array_normalizer.cpp
+++ b/be/src/format/arrow/arrow_array_normalizer.cpp
@@ -18,9 +18,13 @@
#include "format/arrow/arrow_array_normalizer.h"
#include <arrow/array/array_base.h>
+#include <arrow/array/array_nested.h>
+#include <arrow/array/builder_base.h>
+#include <arrow/array/builder_nested.h>
#include <arrow/compute/cast.h>
#include <arrow/type.h>
+#include <limits>
#include <memory>
#include "common/check.h"
@@ -30,22 +34,164 @@ namespace doris {
namespace {
-// The accepted counterpart of an encoding-only variant. Null when there is
none.
-std::shared_ptr<arrow::DataType> target_type_for(const arrow::DataType& type) {
- switch (type.id()) {
+// The accepted counterpart of an encoding-only variant. Null when neither
this type nor any child
+// needs normalization.
+std::shared_ptr<arrow::DataType> target_type_for(const
std::shared_ptr<arrow::DataType>& type) {
+ switch (type->id()) {
case arrow::Type::LARGE_STRING:
case arrow::Type::STRING_VIEW:
return arrow::utf8();
case arrow::Type::LARGE_BINARY:
case arrow::Type::BINARY_VIEW:
return arrow::binary();
+ case arrow::Type::DICTIONARY: {
+ const auto& dictionary = static_cast<const
arrow::DictionaryType&>(*type);
+ auto nested_target = target_type_for(dictionary.value_type());
+ return nested_target != nullptr ? nested_target :
dictionary.value_type();
+ }
+ case arrow::Type::RUN_END_ENCODED: {
+ const auto& encoded = static_cast<const
arrow::RunEndEncodedType&>(*type);
+ auto nested_target = target_type_for(encoded.value_type());
+ return nested_target != nullptr ? nested_target : encoded.value_type();
+ }
+ case arrow::Type::LIST:
+ case arrow::Type::LARGE_LIST:
+ case arrow::Type::FIXED_SIZE_LIST: {
+ const auto& list = static_cast<const arrow::BaseListType&>(*type);
+ auto child_target = target_type_for(list.value_type());
+ if (child_target == nullptr) {
+ return nullptr;
+ }
+ auto child = list.value_field()->WithType(std::move(child_target));
+ if (type->id() == arrow::Type::LIST) {
+ return arrow::list(std::move(child));
+ }
+ if (type->id() == arrow::Type::LARGE_LIST) {
+ return arrow::large_list(std::move(child));
+ }
+ const auto& fixed = static_cast<const
arrow::FixedSizeListType&>(*type);
+ return arrow::fixed_size_list(std::move(child), fixed.list_size());
+ }
+ case arrow::Type::STRUCT: {
+ arrow::FieldVector fields;
+ fields.reserve(type->num_fields());
+ bool changed = false;
+ for (const auto& field : type->fields()) {
+ auto child_target = target_type_for(field->type());
+ changed |= child_target != nullptr;
+ fields.push_back(child_target != nullptr ?
field->WithType(std::move(child_target))
+ : field);
+ }
+ return changed ? arrow::struct_(fields) : nullptr;
+ }
+ case arrow::Type::MAP: {
+ const auto& map = static_cast<const arrow::MapType&>(*type);
+ auto key_target = target_type_for(map.key_type());
+ auto item_target = target_type_for(map.item_type());
+ if (key_target == nullptr && item_target == nullptr) {
+ return nullptr;
+ }
+ auto key = key_target != nullptr ?
map.key_field()->WithType(std::move(key_target))
+ : map.key_field();
+ auto item = item_target != nullptr ?
map.item_field()->WithType(std::move(item_target))
+ : map.item_field();
+ return std::make_shared<arrow::MapType>(std::move(key),
std::move(item), map.keys_sorted());
+ }
+ default:
+ return nullptr;
+ }
+}
+
+bool contains_list_view(const arrow::DataType& type) {
+ switch (type.id()) {
+ case arrow::Type::LIST_VIEW:
+ case arrow::Type::LARGE_LIST_VIEW:
+ return true;
case arrow::Type::DICTIONARY:
- return static_cast<const arrow::DictionaryType&>(type).value_type();
+ return contains_list_view(*static_cast<const
arrow::DictionaryType&>(type).value_type());
case arrow::Type::RUN_END_ENCODED:
- return static_cast<const arrow::RunEndEncodedType&>(type).value_type();
+ return contains_list_view(*static_cast<const
arrow::RunEndEncodedType&>(type).value_type());
+ case arrow::Type::EXTENSION:
+ return contains_list_view(*static_cast<const
arrow::ExtensionType&>(type).storage_type());
default:
- return nullptr;
+ for (const auto& field : type.fields()) {
+ if (contains_list_view(*field->type())) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+template <typename OffsetType, typename ViewArray, typename ListBuilder,
typename ListArray>
+arrow::Result<std::shared_ptr<arrow::Array>> canonicalize_list_view(const
ViewArray& source,
+
arrow::MemoryPool* pool) {
+ // Imported C stream arrays are not guaranteed to have validated ranges;
copying an invalid
+ // range before this check could read beyond the child array.
+ auto validation = source.ValidateFull();
+ if (!validation.ok()) {
+ return validation;
+ }
+
+ // Preflight the full expansion before any builder mutation. Nested view
builders and Arrow's
+ // NullBuilder can otherwise overflow signed lengths before their parent
reports capacity.
+ if (contains_list_view(*source.value_type())) {
+ return arrow::Status::Invalid("nested list view canonicalization is
not supported");
+ }
+ int64_t logical_value_count = 0;
+ constexpr int64_t max_value_count = std::numeric_limits<OffsetType>::max();
+ for (int64_t i = 0; i < source.length(); ++i) {
+ if (source.IsNull(i)) {
+ continue;
+ }
+ const int64_t value_length = source.value_length(i);
+ if (value_length > max_value_count - logical_value_count) {
+ return arrow::Status::CapacityError("list view logical values
exceed output capacity");
+ }
+ logical_value_count += value_length;
+ }
+
+ auto child_builder_result = arrow::MakeBuilder(source.value_type(), pool);
+ if (!child_builder_result.ok()) {
+ return child_builder_result.status();
+ }
+ std::shared_ptr<arrow::ArrayBuilder>
child_builder(child_builder_result.MoveValueUnsafe());
+ ListBuilder builder(pool, child_builder);
+ auto reserve_status = builder.Reserve(source.length());
+ if (!reserve_status.ok()) {
+ return reserve_status;
+ }
+ reserve_status = child_builder->Reserve(logical_value_count);
+ if (!reserve_status.ok()) {
+ return reserve_status;
}
+
+ arrow::ArraySpan values(*source.values()->data());
+ for (int64_t i = 0; i < source.length(); ++i) {
+ if (source.IsNull(i)) {
+ auto append_status = builder.AppendNull();
+ if (!append_status.ok()) {
+ return append_status;
+ }
+ continue;
+ }
+ auto append_status = builder.Append();
+ if (!append_status.ok()) {
+ return append_status;
+ }
+ append_status = child_builder->AppendArraySlice(values,
source.value_offset(i),
+
source.value_length(i));
+ if (!append_status.ok()) {
+ return append_status;
+ }
+ }
+
+ std::shared_ptr<ListArray> out;
+ auto finish_status = builder.Finish(&out);
+ if (!finish_status.ok()) {
+ return finish_status;
+ }
+ return std::static_pointer_cast<arrow::Array>(out);
}
} // namespace
@@ -59,6 +205,8 @@ bool is_serde_acceptable_arrow_type(const arrow::DataType&
type) {
case arrow::Type::BINARY_VIEW:
case arrow::Type::DICTIONARY:
case arrow::Type::RUN_END_ENCODED:
+ case arrow::Type::LIST_VIEW:
+ case arrow::Type::LARGE_LIST_VIEW:
return false;
// No Doris column can hold these, so they must not reach a serde either.
case arrow::Type::INTERVAL_MONTHS:
@@ -69,13 +217,19 @@ bool is_serde_acceptable_arrow_type(const arrow::DataType&
type) {
case arrow::Type::DENSE_UNION:
return false;
default:
+ for (const auto& field : type.fields()) {
+ if (!is_serde_acceptable_arrow_type(*field->type())) {
+ return false;
+ }
+ }
return true;
}
}
-Status normalize_arrow_array(const std::shared_ptr<arrow::Array>& arr,
+Status normalize_arrow_array(const std::shared_ptr<arrow::Array>& arr,
arrow::MemoryPool* pool,
std::shared_ptr<arrow::Array>* out) {
DORIS_CHECK(arr != nullptr);
+ DORIS_CHECK(pool != nullptr);
DORIS_CHECK(out != nullptr);
std::shared_ptr<arrow::Array> current = arr;
@@ -89,14 +243,41 @@ Status normalize_arrow_array(const
std::shared_ptr<arrow::Array>& arr,
return Status::OK();
}
- auto target = target_type_for(type);
+ // List views may share or reorder value ranges, so rebuild canonical
offsets instead of
+ // exposing their buffers to a serde that requires contiguous list
values.
+ if (type.id() == arrow::Type::LIST_VIEW) {
+ auto converted = canonicalize_list_view<int32_t,
arrow::ListViewArray,
+ arrow::ListBuilder,
arrow::ListArray>(
+ static_cast<const arrow::ListViewArray&>(*current), pool);
+ if (!converted.ok()) {
+ return Status::InternalError("ADBC: failed to normalize arrow
type '{}': {}",
+ type.ToString(),
converted.status().ToString());
+ }
+ current = converted.MoveValueUnsafe();
+ continue;
+ }
+ if (type.id() == arrow::Type::LARGE_LIST_VIEW) {
+ auto converted = canonicalize_list_view<int64_t,
arrow::LargeListViewArray,
+ arrow::LargeListBuilder,
arrow::LargeListArray>(
+ static_cast<const arrow::LargeListViewArray&>(*current),
pool);
+ if (!converted.ok()) {
+ return Status::InternalError("ADBC: failed to normalize arrow
type '{}': {}",
+ type.ToString(),
converted.status().ToString());
+ }
+ current = converted.MoveValueUnsafe();
+ continue;
+ }
+
+ auto target = target_type_for(current->type());
if (target == nullptr) {
return Status::NotSupported(
"ADBC: arrow type '{}' cannot be materialized into a Doris
column",
type.ToString());
}
- auto casted = arrow::compute::Cast(*current, target);
+ arrow::compute::ExecContext exec_context(pool);
+ auto casted = arrow::compute::Cast(*current, target,
arrow::compute::CastOptions::Safe(),
+ &exec_context);
if (!casted.ok()) {
return Status::InternalError("ADBC: failed to normalize arrow type
'{}' to '{}': {}",
type.ToString(), target->ToString(),
diff --git a/be/src/format/arrow/arrow_array_normalizer.h
b/be/src/format/arrow/arrow_array_normalizer.h
index eed9ab02293..b2a6a49fa51 100644
--- a/be/src/format/arrow/arrow_array_normalizer.h
+++ b/be/src/format/arrow/arrow_array_normalizer.h
@@ -30,15 +30,17 @@ namespace doris {
/// emits string_view, Go-based drivers may emit large_* and dictionary. This
normalizes them into
/// a shape the serdes accept.
///
-/// Only top-level types are normalized. A nested type whose child is an
unaccepted variant (say
-/// list<large_utf8>) passes through and fails inside the serde, loudly rather
than silently.
+/// Encoding-only descendants are normalized recursively so a complex serde
never mistakes encoded
+/// child buffers for logical values. Nested list views remain unsupported
because their shared
+/// ranges cannot be canonicalized safely by an outer Arrow cast.
/// Whether the serdes take this Arrow type as-is.
bool is_serde_acceptable_arrow_type(const arrow::DataType& type);
-/// Normalizes `arr` into a serde-acceptable shape. Returns it unchanged (no
copy) when it already
-/// is one, and fails with the offending type named when no accepted shape
exists.
-Status normalize_arrow_array(const std::shared_ptr<arrow::Array>& arr,
+/// Normalizes `arr` into a serde-acceptable shape using `pool` for every
conversion. Returns it
+/// unchanged (no copy) when it already is one, and fails with the offending
type named when no
+/// accepted shape exists.
+Status normalize_arrow_array(const std::shared_ptr<arrow::Array>& arr,
arrow::MemoryPool* pool,
std::shared_ptr<arrow::Array>* out);
} // namespace doris
diff --git a/be/src/format_v2/column_mapper.cpp
b/be/src/format_v2/column_mapper.cpp
index 341ecd45395..e80f7aba0f4 100644
--- a/be/src/format_v2/column_mapper.cpp
+++ b/be/src/format_v2/column_mapper.cpp
@@ -1916,6 +1916,32 @@ static void attach_timestamp_semantics(const
ColumnMapping& mapping, LocalColumn
attach_timestamp_semantics(child_mapping, &*child_it);
}
}
+static Status apply_projected_file_definition_to_mapping(const
ColumnDefinition& projected_field,
+ ColumnMapping*
mapping) {
+ DORIS_CHECK(mapping != nullptr);
+ mapping->file_type = projected_field.type;
+ mapping->projected_file_children = projected_field.children;
+ for (auto& child_mapping : mapping->child_mappings) {
+ if (!child_mapping.file_local_id.has_value()) {
+ continue;
+ }
+ const auto child_it =
+ std::ranges::find_if(projected_field.children, [&](const
ColumnDefinition& child) {
+ return child.file_local_id() ==
*child_mapping.file_local_id;
+ });
+ if (child_it == projected_field.children.end()) {
+ return Status::InternalError(
+ "Projected file type for '{}' is missing mapped child id
{}",
+ mapping->file_column_name, *child_mapping.file_local_id);
+ }
+ // A full root projection changes every descendant's runtime shape
too. Keep the recursive
+ // mapping in sync so a formerly pruned child cannot be mistaken for a
trivial direct column.
+ RETURN_IF_ERROR(apply_projected_file_definition_to_mapping(*child_it,
&child_mapping));
+ }
+ mapping->is_trivial = mapping_can_use_file_column_directly(*mapping);
+ return Status::OK();
+}
+
// Update the mapping's file type according to the projection, and determine
whether the projection
// is trivial (i.e. the projected file type is the same as the table type, so
no need to
// rematerialize the complex value back to table layout after reading from
file).
@@ -1934,10 +1960,7 @@ static Status
apply_projection_to_mapping_file_type(const LocalColumnIndex& proj
field.children = mapping->original_file_children;
ColumnDefinition projected_field;
RETURN_IF_ERROR(project_column_definition(field, projection,
&projected_field));
- mapping->file_type = std::move(projected_field.type);
- mapping->projected_file_children = std::move(projected_field.children);
- mapping->is_trivial = mapping_can_use_file_column_directly(*mapping);
- return Status::OK();
+ return apply_projected_file_definition_to_mapping(projected_field,
mapping);
}
static const ColumnDefinition* find_file_child_by_name(
diff --git a/be/src/format_v2/table/adbc_reader.cpp
b/be/src/format_v2/table/adbc_reader.cpp
index 98847c40603..0ab86359bae 100644
--- a/be/src/format_v2/table/adbc_reader.cpp
+++ b/be/src/format_v2/table/adbc_reader.cpp
@@ -40,13 +40,18 @@
#include "core/data_type/data_type_map.h"
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_struct.h"
+#include "core/data_type/primitive_type.h"
#include "core/data_type_serde/data_type_serde.h"
#include "format/arrow/arrow_array_normalizer.h"
+#include "format/parquet/arrow_memory_pool.h"
+#include "format_v2/column_mapper.h"
#include "format_v2/materialized_reader_util.h"
#include "runtime/descriptors.h"
+#include "runtime/exec_env.h"
#include "runtime/file_scan_profile.h"
#include "runtime/runtime_state.h"
#include "util/adbc_driver_registry.h"
+#include "util/string_util.h"
#include "util/timezone_utils.h"
#include "util/url_coding.h"
@@ -105,6 +110,136 @@ Status validate_adbc_range(const TFileRangeDesc& range) {
return Status::OK();
}
+Status arrow_type_mismatch(const std::string& path, const arrow::DataType&
arrow_type,
+ const DataTypePtr& doris_type) {
+ return Status::InvalidArgument(
+ "ADBC Arrow type mismatch at '{}': runtime type '{}' does not
match cached Doris "
+ "type '{}'",
+ path, arrow_type.ToString(), doris_type->get_name());
+}
+
+int timestamp_scale(arrow::TimeUnit::type unit) {
+ switch (unit) {
+ case arrow::TimeUnit::SECOND:
+ return 0;
+ case arrow::TimeUnit::MILLI:
+ return 3;
+ case arrow::TimeUnit::MICRO:
+ case arrow::TimeUnit::NANO:
+ return 6;
+ }
+ return -1;
+}
+
+Status validate_arrow_type(const arrow::DataType& arrow_type, const
DataTypePtr& doris_type,
+ const std::string& path) {
+ DORIS_CHECK(doris_type != nullptr);
+ const auto nested_doris_type = remove_nullable(doris_type);
+ const auto primitive_type = nested_doris_type->get_primitive_type();
+ const auto mismatch = [&]() { return arrow_type_mismatch(path, arrow_type,
doris_type); };
+
+ switch (arrow_type.id()) {
+ case arrow::Type::BOOL:
+ return primitive_type == TYPE_BOOLEAN ? Status::OK() : mismatch();
+ case arrow::Type::INT8:
+ return primitive_type == TYPE_TINYINT ? Status::OK() : mismatch();
+ case arrow::Type::UINT8:
+ return primitive_type == TYPE_SMALLINT ? Status::OK() : mismatch();
+ case arrow::Type::INT16:
+ return primitive_type == TYPE_SMALLINT ? Status::OK() : mismatch();
+ case arrow::Type::UINT16:
+ return primitive_type == TYPE_INT ? Status::OK() : mismatch();
+ case arrow::Type::INT32:
+ return primitive_type == TYPE_INT ? Status::OK() : mismatch();
+ case arrow::Type::UINT32:
+ return primitive_type == TYPE_BIGINT ? Status::OK() : mismatch();
+ case arrow::Type::INT64:
+ return primitive_type == TYPE_BIGINT ? Status::OK() : mismatch();
+ case arrow::Type::UINT64:
+ return primitive_type == TYPE_LARGEINT ? Status::OK() : mismatch();
+ case arrow::Type::HALF_FLOAT:
+ case arrow::Type::FLOAT:
+ return primitive_type == TYPE_FLOAT ? Status::OK() : mismatch();
+ case arrow::Type::DOUBLE:
+ return primitive_type == TYPE_DOUBLE ? Status::OK() : mismatch();
+ case arrow::Type::STRING:
+ case arrow::Type::BINARY:
+ case arrow::Type::FIXED_SIZE_BINARY:
+ return primitive_type == TYPE_STRING ? Status::OK() : mismatch();
+ case arrow::Type::DATE32:
+ return primitive_type == TYPE_DATEV2 ? Status::OK() : mismatch();
+ case arrow::Type::DATE64:
+ return primitive_type == TYPE_DATETIMEV2 &&
nested_doris_type->get_scale() == 3
+ ? Status::OK()
+ : mismatch();
+ case arrow::Type::TIMESTAMP: {
+ const auto& timestamp = static_cast<const
arrow::TimestampType&>(arrow_type);
+ const bool zoned = !timestamp.timezone().empty();
+ const auto expected = zoned ? TYPE_TIMESTAMPTZ : TYPE_DATETIMEV2;
+ return primitive_type == expected &&
+ nested_doris_type->get_scale() ==
timestamp_scale(timestamp.unit())
+ ? Status::OK()
+ : mismatch();
+ }
+ case arrow::Type::DECIMAL128:
+ case arrow::Type::DECIMAL256: {
+ const auto& decimal = static_cast<const
arrow::DecimalType&>(arrow_type);
+ return is_decimal(primitive_type) &&
+ nested_doris_type->get_precision() ==
decimal.precision() &&
+ nested_doris_type->get_scale() ==
decimal.scale()
+ ? Status::OK()
+ : mismatch();
+ }
+ case arrow::Type::LIST:
+ case arrow::Type::LARGE_LIST:
+ case arrow::Type::FIXED_SIZE_LIST: {
+ if (primitive_type != TYPE_ARRAY) {
+ return mismatch();
+ }
+ const auto& list = static_cast<const arrow::BaseListType&>(arrow_type);
+ const auto& array = assert_cast<const
DataTypeArray&>(*nested_doris_type);
+ return validate_arrow_type(*list.value_type(),
array.get_nested_type(), path + ".element");
+ }
+ case arrow::Type::STRUCT: {
+ if (primitive_type != TYPE_STRUCT) {
+ return mismatch();
+ }
+ const auto& arrow_struct = static_cast<const
arrow::StructType&>(arrow_type);
+ const auto& doris_struct = assert_cast<const
DataTypeStruct&>(*nested_doris_type);
+ if (cast_set<size_t>(arrow_struct.num_fields()) !=
doris_struct.get_elements().size()) {
+ return mismatch();
+ }
+ for (int field_idx = 0; field_idx < arrow_struct.num_fields();
++field_idx) {
+ const auto& arrow_field = arrow_struct.field(field_idx);
+ const auto& doris_name = doris_struct.get_element_name(field_idx);
+ if (to_lower(arrow_field->name()) != to_lower(doris_name)) {
+ return Status::InvalidArgument(
+ "ADBC Arrow field mismatch at '{}': runtime field '{}'
does not match "
+ "cached field '{}' at ordinal {}",
+ path, arrow_field->name(), doris_name, field_idx);
+ }
+ RETURN_IF_ERROR(validate_arrow_type(*arrow_field->type(),
+
doris_struct.get_element(field_idx),
+ path + "." + doris_name));
+ }
+ return Status::OK();
+ }
+ case arrow::Type::MAP: {
+ if (primitive_type != TYPE_MAP) {
+ return mismatch();
+ }
+ const auto& arrow_map = static_cast<const arrow::MapType&>(arrow_type);
+ const auto& doris_map = assert_cast<const
DataTypeMap&>(*nested_doris_type);
+ RETURN_IF_ERROR(validate_arrow_type(*arrow_map.key_type(),
doris_map.get_key_type(),
+ path + ".key"));
+ return validate_arrow_type(*arrow_map.item_type(),
doris_map.get_value_type(),
+ path + ".value");
+ }
+ default:
+ return mismatch();
+ }
+}
+
// Drivers allocate the strings inside AdbcError, so every populated error has
to be released.
class AdbcErrorGuard {
public:
@@ -498,6 +633,13 @@ Status
AdbcFileReader::get_schema(std::vector<ColumnDefinition>* file_schema) co
return Status::OK();
}
+std::unique_ptr<TableColumnMapper> AdbcFileReader::create_column_mapper(
+ TableColumnMapperOptions options) const {
+ // ADBC streams return complete Arrow roots, so TableReader must own every
nested projection and
+ // reorder after the full source shape has been materialized.
+ return std::make_unique<MaterializedColumnMapper>(std::move(options));
+}
+
Status AdbcFileReader::open(std::shared_ptr<FileScanRequest> request) {
SCOPED_TIMER(_total_time);
SCOPED_TIMER(_open_stream_time);
@@ -594,6 +736,13 @@ Status AdbcFileReader::_materialize_record_batch(const
arrow::RecordBatch& batch
return Status::OK();
}
+ ArrowMemoryPool<> local_arrow_pool;
+ arrow::MemoryPool* arrow_pool =
ExecEnv::GetInstance()->arrow_memory_pool();
+ if (arrow_pool == nullptr) {
+ // Embedded and unit-test runtimes may omit ExecEnv memory
initialization; keep conversions
+ // on Doris' tracked allocator instead of falling back to Arrow's
untracked default pool.
+ arrow_pool = &local_arrow_pool;
+ }
std::vector<bool> materialized_columns(file_block->columns(), false);
for (int arrow_idx = 0; arrow_idx < batch.num_columns(); ++arrow_idx) {
const std::string& column_name =
batch.schema()->field(arrow_idx)->name();
@@ -608,7 +757,7 @@ Status AdbcFileReader::_materialize_record_batch(const
arrow::RecordBatch& batch
std::shared_ptr<arrow::Array> array;
{
SCOPED_TIMER(_normalize_time);
- RETURN_IF_ERROR(normalize_arrow_array(batch.column(arrow_idx),
&array));
+ RETURN_IF_ERROR(normalize_arrow_array(batch.column(arrow_idx),
arrow_pool, &array));
}
RETURN_IF_ERROR(_materialize_arrow_column(column_name, array,
batch.num_rows(),
file_id_it->second,
block_position_it->second,
@@ -643,9 +792,15 @@ Status AdbcFileReader::_materialize_arrow_column(const
std::string& column_name,
return Status::InternalError("ADBC block position {} out of range,
block columns {}",
block_position.value(),
file_block->columns());
}
- auto columns_guard = file_block->mutate_columns_scoped();
- auto& columns = columns_guard.mutable_columns();
- const auto& target_type =
columns_guard.get_datatype_by_position(block_position.value());
+ const auto& target_type =
file_block->get_by_position(block_position.value()).type;
+
+ // The cached FE target remains authoritative when a source schema changes
mid-cache-window;
+ // otherwise SerDes without a null map can silently turn source nulls into
default values.
+ if (array->null_count() > 0 && !target_type->is_nullable()) {
+ return Status::InternalError(
+ "ADBC Arrow column '{}' contains {} null rows for non-nullable
Doris type {}",
+ column_name, array->null_count(), target_type->get_name());
+ }
// An all-null column arrives with a type that says nothing about the
column.
//
@@ -665,10 +820,18 @@ Status AdbcFileReader::_materialize_arrow_column(const
std::string& column_name,
// Only for a nullable target: substituting defaults into a NOT NULL
column would turn a source
// that wrongly sent nulls into silently wrong data, so that keeps failing
in the serde.
if (array->null_count() == array->length() && target_type->is_nullable()) {
+ auto columns_guard = file_block->mutate_columns_scoped();
+ auto& columns = columns_guard.mutable_columns();
columns[block_position.value()]->insert_many_defaults(cast_set<size_t>(num_rows));
return Status::OK();
}
+ // Validate the whole logical shape before acquiring mutable block
columns. Struct SerDes map
+ // children by ordinal, so a stale cache entry can otherwise produce
plausible but wrong values.
+ RETURN_IF_ERROR(validate_arrow_type(*array->type(), target_type,
column_name));
+
+ auto columns_guard = file_block->mutate_columns_scoped();
+ auto& columns = columns_guard.mutable_columns();
try {
RETURN_IF_ERROR(target_type->get_serde()->read_column_from_arrow(
*columns[block_position.value()], array.get(), 0, num_rows,
_ctz));
diff --git a/be/src/format_v2/table/adbc_reader.h
b/be/src/format_v2/table/adbc_reader.h
index 8e184364e99..39addf8eec2 100644
--- a/be/src/format_v2/table/adbc_reader.h
+++ b/be/src/format_v2/table/adbc_reader.h
@@ -79,6 +79,8 @@ public:
Status init(RuntimeState* state) override;
Status get_schema(std::vector<ColumnDefinition>* file_schema) const
override;
+ std::unique_ptr<TableColumnMapper> create_column_mapper(
+ TableColumnMapperOptions options) const override;
Status open(std::shared_ptr<FileScanRequest> request) override;
Status get_block(Block* file_block, size_t* rows, bool* eof) override;
Status close() override;
diff --git a/be/test/format/arrow/arrow_array_normalizer_test.cpp
b/be/test/format/arrow/arrow_array_normalizer_test.cpp
index d83385981fb..75b73b99dbe 100644
--- a/be/test/format/arrow/arrow_array_normalizer_test.cpp
+++ b/be/test/format/arrow/arrow_array_normalizer_test.cpp
@@ -19,9 +19,13 @@
#include <arrow/array.h>
#include <arrow/builder.h>
+#include <arrow/memory_pool.h>
#include <arrow/type.h>
#include <gtest/gtest.h>
+#include <algorithm>
+#include <cstring>
+#include <limits>
#include <memory>
#include <string>
#include <vector>
@@ -40,6 +44,59 @@ std::shared_ptr<arrow::Array> build_large_string(const
std::vector<std::string>&
return out;
}
+class AllocateBeforeFreeMemoryPool : public arrow::MemoryPool {
+public:
+ explicit AllocateBeforeFreeMemoryPool(arrow::MemoryPool* delegate) :
_delegate(delegate) {}
+
+ arrow::Status Allocate(int64_t size, int64_t alignment, uint8_t** out)
override {
+ auto status = _delegate->Allocate(size, alignment, out);
+ if (status.ok()) {
+ _record_allocation(size);
+ }
+ return status;
+ }
+
+ arrow::Status Reallocate(int64_t old_size, int64_t new_size, int64_t
alignment,
+ uint8_t** ptr) override {
+ uint8_t* replacement = nullptr;
+ auto status = _delegate->Allocate(new_size, alignment, &replacement);
+ if (!status.ok()) {
+ return status;
+ }
+ std::memcpy(replacement, *ptr, static_cast<size_t>(std::min(old_size,
new_size)));
+ _record_allocation(new_size);
+ _delegate->Free(*ptr, old_size, alignment);
+ _bytes_allocated -= old_size;
+ *ptr = replacement;
+ return arrow::Status::OK();
+ }
+
+ void Free(uint8_t* buffer, int64_t size, int64_t alignment) override {
+ _delegate->Free(buffer, size, alignment);
+ _bytes_allocated -= size;
+ }
+
+ int64_t bytes_allocated() const override { return _bytes_allocated; }
+ int64_t max_memory() const override { return _max_memory; }
+ int64_t total_bytes_allocated() const override { return
_total_bytes_allocated; }
+ int64_t num_allocations() const override { return _num_allocations; }
+ std::string backend_name() const override { return
"allocate-before-free-test"; }
+
+private:
+ void _record_allocation(int64_t size) {
+ _bytes_allocated += size;
+ _max_memory = std::max(_max_memory, _bytes_allocated);
+ _total_bytes_allocated += size;
+ ++_num_allocations;
+ }
+
+ arrow::MemoryPool* _delegate;
+ int64_t _bytes_allocated = 0;
+ int64_t _max_memory = 0;
+ int64_t _total_bytes_allocated = 0;
+ int64_t _num_allocations = 0;
+};
+
} // namespace
// Doris' own Arrow is already in the accepted shape; normalizing must not
copy it.
@@ -52,7 +109,7 @@ TEST(ArrowArrayNormalizerTest,
PlainStringIsAcceptedUnchanged) {
EXPECT_TRUE(is_serde_acceptable_arrow_type(*in->type()));
std::shared_ptr<arrow::Array> out;
- ASSERT_TRUE(normalize_arrow_array(in, &out).ok());
+ ASSERT_TRUE(normalize_arrow_array(in, arrow::default_memory_pool(),
&out).ok());
EXPECT_EQ(in.get(), out.get());
}
@@ -62,7 +119,7 @@ TEST(ArrowArrayNormalizerTest,
LargeStringIsConvertedToString) {
EXPECT_FALSE(is_serde_acceptable_arrow_type(*in->type()));
std::shared_ptr<arrow::Array> out;
- ASSERT_TRUE(normalize_arrow_array(in, &out).ok());
+ ASSERT_TRUE(normalize_arrow_array(in, arrow::default_memory_pool(),
&out).ok());
ASSERT_EQ(out->type_id(), arrow::Type::STRING);
ASSERT_EQ(out->length(), 3);
auto sa = std::static_pointer_cast<arrow::StringArray>(out);
@@ -87,7 +144,7 @@ TEST(ArrowArrayNormalizerTest,
DictionaryIsDecodedToValueType) {
EXPECT_FALSE(is_serde_acceptable_arrow_type(*in->type()));
std::shared_ptr<arrow::Array> out;
- ASSERT_TRUE(normalize_arrow_array(in, &out).ok());
+ ASSERT_TRUE(normalize_arrow_array(in, arrow::default_memory_pool(),
&out).ok());
ASSERT_EQ(out->type_id(), arrow::Type::STRING);
auto sa = std::static_pointer_cast<arrow::StringArray>(out);
ASSERT_EQ(sa->length(), 3);
@@ -105,7 +162,7 @@ TEST(ArrowArrayNormalizerTest,
NullsArePreservedAcrossConversion) {
ASSERT_TRUE(b.Finish(&in).ok());
std::shared_ptr<arrow::Array> out;
- ASSERT_TRUE(normalize_arrow_array(in, &out).ok());
+ ASSERT_TRUE(normalize_arrow_array(in, arrow::default_memory_pool(),
&out).ok());
ASSERT_EQ(out->length(), 2);
EXPECT_FALSE(out->IsNull(0));
EXPECT_TRUE(out->IsNull(1));
@@ -124,7 +181,7 @@ TEST(ArrowArrayNormalizerTest,
DictionaryOfLargeStringIsFullyNormalized) {
auto in = std::make_shared<arrow::DictionaryArray>(dict_type, idx, dict);
std::shared_ptr<arrow::Array> out;
- ASSERT_TRUE(normalize_arrow_array(in, &out).ok());
+ ASSERT_TRUE(normalize_arrow_array(in, arrow::default_memory_pool(),
&out).ok());
ASSERT_EQ(out->type_id(), arrow::Type::STRING);
auto sa = std::static_pointer_cast<arrow::StringArray>(out);
ASSERT_EQ(sa->length(), 2);
@@ -132,13 +189,311 @@ TEST(ArrowArrayNormalizerTest,
DictionaryOfLargeStringIsFullyNormalized) {
EXPECT_EQ(sa->GetString(1), "x");
}
+TEST(ArrowArrayNormalizerTest, ListViewIsConvertedToListInLogicalOrder) {
+ arrow::Int32Builder offsets_builder;
+ ASSERT_TRUE(offsets_builder.AppendValues({2, 0, 1}).ok());
+ std::shared_ptr<arrow::Array> offsets;
+ ASSERT_TRUE(offsets_builder.Finish(&offsets).ok());
+
+ arrow::Int32Builder sizes_builder;
+ ASSERT_TRUE(sizes_builder.AppendValues({2, 1, 2}).ok());
+ std::shared_ptr<arrow::Array> sizes;
+ ASSERT_TRUE(sizes_builder.Finish(&sizes).ok());
+
+ arrow::Int32Builder values_builder;
+ ASSERT_TRUE(values_builder.AppendValues({1, 2, 3, 4}).ok());
+ std::shared_ptr<arrow::Array> values;
+ ASSERT_TRUE(values_builder.Finish(&values).ok());
+
+ auto in = arrow::ListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ EXPECT_FALSE(is_serde_acceptable_arrow_type(*in->type()));
+
+ std::shared_ptr<arrow::Array> out;
+ ASSERT_TRUE(normalize_arrow_array(in, arrow::default_memory_pool(),
&out).ok());
+ ASSERT_EQ(out->type_id(), arrow::Type::LIST);
+ auto list = std::static_pointer_cast<arrow::ListArray>(out);
+ ASSERT_EQ(list->length(), 3);
+ EXPECT_EQ(list->value_slice(0)->ToString(), "[\n 3,\n 4\n]");
+ EXPECT_EQ(list->value_slice(1)->ToString(), "[\n 1\n]");
+ EXPECT_EQ(list->value_slice(2)->ToString(), "[\n 2,\n 3\n]");
+}
+
+TEST(ArrowArrayNormalizerTest, LargeListViewIsConvertedToLargeList) {
+ arrow::Int64Builder offsets_builder;
+ ASSERT_TRUE(offsets_builder.AppendValues({1, 0}).ok());
+ std::shared_ptr<arrow::Array> offsets;
+ ASSERT_TRUE(offsets_builder.Finish(&offsets).ok());
+
+ arrow::Int64Builder sizes_builder;
+ ASSERT_TRUE(sizes_builder.AppendValues({2, 1}).ok());
+ std::shared_ptr<arrow::Array> sizes;
+ ASSERT_TRUE(sizes_builder.Finish(&sizes).ok());
+
+ arrow::Int32Builder values_builder;
+ ASSERT_TRUE(values_builder.AppendValues({10, 20, 30}).ok());
+ std::shared_ptr<arrow::Array> values;
+ ASSERT_TRUE(values_builder.Finish(&values).ok());
+
+ auto in = arrow::LargeListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ EXPECT_FALSE(is_serde_acceptable_arrow_type(*in->type()));
+
+ std::shared_ptr<arrow::Array> out;
+ ASSERT_TRUE(normalize_arrow_array(in, arrow::default_memory_pool(),
&out).ok());
+ ASSERT_EQ(out->type_id(), arrow::Type::LARGE_LIST);
+ auto list = std::static_pointer_cast<arrow::LargeListArray>(out);
+ ASSERT_EQ(list->length(), 2);
+ EXPECT_EQ(list->value_slice(0)->ToString(), "[\n 20,\n 30\n]");
+ EXPECT_EQ(list->value_slice(1)->ToString(), "[\n 10\n]");
+}
+
+TEST(ArrowArrayNormalizerTest, ListViewCanonicalizationUsesCallerMemoryPool) {
+ arrow::Int32Builder offsets_builder;
+ ASSERT_TRUE(offsets_builder.AppendValues({0, 0, 0}).ok());
+ std::shared_ptr<arrow::Array> offsets;
+ ASSERT_TRUE(offsets_builder.Finish(&offsets).ok());
+
+ arrow::Int32Builder sizes_builder;
+ ASSERT_TRUE(sizes_builder.AppendValues({3, 3, 3}).ok());
+ std::shared_ptr<arrow::Array> sizes;
+ ASSERT_TRUE(sizes_builder.Finish(&sizes).ok());
+
+ arrow::Int32Builder values_builder;
+ ASSERT_TRUE(values_builder.AppendValues({10, 20, 30}).ok());
+ std::shared_ptr<arrow::Array> values;
+ ASSERT_TRUE(values_builder.Finish(&values).ok());
+
+ auto in = arrow::ListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ arrow::ProxyMemoryPool pool(arrow::default_memory_pool());
+ std::shared_ptr<arrow::Array> out;
+ ASSERT_TRUE(normalize_arrow_array(in, &pool, &out).ok());
+ EXPECT_GT(pool.bytes_allocated(), 0);
+ out.reset();
+ EXPECT_EQ(pool.bytes_allocated(), 0);
+}
+
+TEST(ArrowArrayNormalizerTest, ListViewPreReservesExpandedChildAllocation) {
+ constexpr int32_t kRows = 1025;
+ constexpr int32_t kValuesPerRow = 1000;
+ arrow::Int32Builder offsets_builder;
+ ASSERT_TRUE(offsets_builder.AppendValues(std::vector<int32_t>(kRows,
0)).ok());
+ std::shared_ptr<arrow::Array> offsets;
+ ASSERT_TRUE(offsets_builder.Finish(&offsets).ok());
+
+ arrow::Int32Builder sizes_builder;
+ ASSERT_TRUE(sizes_builder.AppendValues(std::vector<int32_t>(kRows,
kValuesPerRow)).ok());
+ std::shared_ptr<arrow::Array> sizes;
+ ASSERT_TRUE(sizes_builder.Finish(&sizes).ok());
+
+ arrow::Int32Builder values_builder;
+
ASSERT_TRUE(values_builder.AppendValues(std::vector<int32_t>(kValuesPerRow,
7)).ok());
+ std::shared_ptr<arrow::Array> values;
+ ASSERT_TRUE(values_builder.Finish(&values).ok());
+
+ auto in = arrow::ListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ AllocateBeforeFreeMemoryPool pool(arrow::default_memory_pool());
+ std::shared_ptr<arrow::Array> out;
+ ASSERT_TRUE(normalize_arrow_array(in, &pool, &out).ok());
+ EXPECT_LE(pool.max_memory(), pool.bytes_allocated() + 256 * 1024);
+}
+
+TEST(ArrowArrayNormalizerTest,
NestedListViewOverflowIsRejectedBeforeAllocation) {
+ const int64_t range_length = std::numeric_limits<int64_t>::max() - 1;
+ arrow::Int64Builder inner_offsets_builder;
+ ASSERT_TRUE(inner_offsets_builder.AppendValues({0, 0}).ok());
+ std::shared_ptr<arrow::Array> inner_offsets;
+ ASSERT_TRUE(inner_offsets_builder.Finish(&inner_offsets).ok());
+ arrow::Int64Builder inner_sizes_builder;
+ ASSERT_TRUE(inner_sizes_builder.AppendValues({range_length,
range_length}).ok());
+ std::shared_ptr<arrow::Array> inner_sizes;
+ ASSERT_TRUE(inner_sizes_builder.Finish(&inner_sizes).ok());
+ auto null_values =
std::make_shared<arrow::NullArray>(std::numeric_limits<int64_t>::max());
+ auto inner = arrow::LargeListViewArray::FromArrays(*inner_offsets,
*inner_sizes, *null_values)
+ .ValueOrDie();
+
+ arrow::Int64Builder outer_offsets_builder;
+ ASSERT_TRUE(outer_offsets_builder.Append(0).ok());
+ std::shared_ptr<arrow::Array> outer_offsets;
+ ASSERT_TRUE(outer_offsets_builder.Finish(&outer_offsets).ok());
+ arrow::Int64Builder outer_sizes_builder;
+ ASSERT_TRUE(outer_sizes_builder.Append(2).ok());
+ std::shared_ptr<arrow::Array> outer_sizes;
+ ASSERT_TRUE(outer_sizes_builder.Finish(&outer_sizes).ok());
+ auto outer = arrow::LargeListViewArray::FromArrays(*outer_offsets,
*outer_sizes, *inner)
+ .ValueOrDie();
+
+ arrow::ProxyMemoryPool pool(arrow::default_memory_pool());
+ std::shared_ptr<arrow::Array> out;
+ EXPECT_FALSE(normalize_arrow_array(outer, &pool, &out).ok());
+ EXPECT_EQ(pool.max_memory(), 0);
+}
+
+TEST(ArrowArrayNormalizerTest, NullableListViewSlicePreservesValidity) {
+ arrow::Int32Builder offsets_builder;
+ ASSERT_TRUE(offsets_builder.Append(0).ok());
+ ASSERT_TRUE(offsets_builder.AppendNull().ok());
+ ASSERT_TRUE(offsets_builder.Append(1).ok());
+ std::shared_ptr<arrow::Array> offsets;
+ ASSERT_TRUE(offsets_builder.Finish(&offsets).ok());
+
+ arrow::Int32Builder sizes_builder;
+ ASSERT_TRUE(sizes_builder.AppendValues({1, 0, 1}).ok());
+ std::shared_ptr<arrow::Array> sizes;
+ ASSERT_TRUE(sizes_builder.Finish(&sizes).ok());
+
+ arrow::Int32Builder values_builder;
+ ASSERT_TRUE(values_builder.AppendValues({10, 20}).ok());
+ std::shared_ptr<arrow::Array> values;
+ ASSERT_TRUE(values_builder.Finish(&values).ok());
+
+ auto full = arrow::ListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ auto in = full->Slice(1, 2);
+ ASSERT_EQ(in->offset(), 1);
+
+ std::shared_ptr<arrow::Array> out;
+ ASSERT_TRUE(normalize_arrow_array(in, arrow::default_memory_pool(),
&out).ok());
+ ASSERT_EQ(out->type_id(), arrow::Type::LIST);
+ EXPECT_TRUE(out->IsNull(0));
+ EXPECT_TRUE(out->IsValid(1));
+ auto list = std::static_pointer_cast<arrow::ListArray>(out);
+ EXPECT_EQ(list->value_slice(1)->ToString(), "[\n 20\n]");
+}
+
+TEST(ArrowArrayNormalizerTest, NullableLargeListViewSlicePreservesValidity) {
+ arrow::Int64Builder offsets_builder;
+ ASSERT_TRUE(offsets_builder.Append(0).ok());
+ ASSERT_TRUE(offsets_builder.AppendNull().ok());
+ ASSERT_TRUE(offsets_builder.Append(1).ok());
+ std::shared_ptr<arrow::Array> offsets;
+ ASSERT_TRUE(offsets_builder.Finish(&offsets).ok());
+
+ arrow::Int64Builder sizes_builder;
+ ASSERT_TRUE(sizes_builder.AppendValues({1, 0, 1}).ok());
+ std::shared_ptr<arrow::Array> sizes;
+ ASSERT_TRUE(sizes_builder.Finish(&sizes).ok());
+
+ arrow::Int32Builder values_builder;
+ ASSERT_TRUE(values_builder.AppendValues({10, 20}).ok());
+ std::shared_ptr<arrow::Array> values;
+ ASSERT_TRUE(values_builder.Finish(&values).ok());
+
+ auto full = arrow::LargeListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ auto in = full->Slice(1, 2);
+ ASSERT_EQ(in->offset(), 1);
+
+ std::shared_ptr<arrow::Array> out;
+ ASSERT_TRUE(normalize_arrow_array(in, arrow::default_memory_pool(),
&out).ok());
+ ASSERT_EQ(out->type_id(), arrow::Type::LARGE_LIST);
+ EXPECT_TRUE(out->IsNull(0));
+ EXPECT_TRUE(out->IsValid(1));
+ auto list = std::static_pointer_cast<arrow::LargeListArray>(out);
+ EXPECT_EQ(list->value_slice(1)->ToString(), "[\n 20\n]");
+}
+
+TEST(ArrowArrayNormalizerTest, InvalidListViewRangeIsRejectedBeforeCopy) {
+ arrow::Int32Builder offsets_builder;
+ ASSERT_TRUE(offsets_builder.AppendValues({0, 2}).ok());
+ std::shared_ptr<arrow::Array> offsets;
+ ASSERT_TRUE(offsets_builder.Finish(&offsets).ok());
+
+ arrow::Int32Builder sizes_builder;
+ ASSERT_TRUE(sizes_builder.AppendValues({1, 1}).ok());
+ std::shared_ptr<arrow::Array> sizes;
+ ASSERT_TRUE(sizes_builder.Finish(&sizes).ok());
+
+ arrow::Int32Builder values_builder;
+ ASSERT_TRUE(values_builder.AppendValues({10, 20}).ok());
+ std::shared_ptr<arrow::Array> values;
+ ASSERT_TRUE(values_builder.Finish(&values).ok());
+
+ auto in = arrow::ListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ std::shared_ptr<arrow::Array> out;
+ EXPECT_FALSE(normalize_arrow_array(in, arrow::default_memory_pool(),
&out).ok());
+}
+
+TEST(ArrowArrayNormalizerTest, InvalidLargeListViewRangeIsRejectedBeforeCopy) {
+ arrow::Int64Builder offsets_builder;
+ ASSERT_TRUE(offsets_builder.AppendValues({0, 2}).ok());
+ std::shared_ptr<arrow::Array> offsets;
+ ASSERT_TRUE(offsets_builder.Finish(&offsets).ok());
+
+ arrow::Int64Builder sizes_builder;
+ ASSERT_TRUE(sizes_builder.AppendValues({1, 1}).ok());
+ std::shared_ptr<arrow::Array> sizes;
+ ASSERT_TRUE(sizes_builder.Finish(&sizes).ok());
+
+ arrow::Int32Builder values_builder;
+ ASSERT_TRUE(values_builder.AppendValues({10, 20}).ok());
+ std::shared_ptr<arrow::Array> values;
+ ASSERT_TRUE(values_builder.Finish(&values).ok());
+
+ auto in = arrow::LargeListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ std::shared_ptr<arrow::Array> out;
+ EXPECT_FALSE(normalize_arrow_array(in, arrow::default_memory_pool(),
&out).ok());
+}
+
+void expect_dictionary_child_list_view_is_decoded(bool large_offsets) {
+ arrow::Int8Builder dictionary_builder;
+ ASSERT_TRUE(dictionary_builder.AppendValues({42, 43}).ok());
+ std::shared_ptr<arrow::Array> dictionary;
+ ASSERT_TRUE(dictionary_builder.Finish(&dictionary).ok());
+
+ arrow::Int8Builder indices_builder;
+ ASSERT_TRUE(indices_builder.AppendValues({0, 1}).ok());
+ std::shared_ptr<arrow::Array> indices;
+ ASSERT_TRUE(indices_builder.Finish(&indices).ok());
+ auto dictionary_type = arrow::dictionary(arrow::int8(), arrow::int8());
+ auto values = std::make_shared<arrow::DictionaryArray>(dictionary_type,
indices, dictionary);
+
+ std::shared_ptr<arrow::Array> input;
+ if (large_offsets) {
+ arrow::Int64Builder offsets_builder;
+ ASSERT_TRUE(offsets_builder.Append(0).ok());
+ std::shared_ptr<arrow::Array> offsets;
+ ASSERT_TRUE(offsets_builder.Finish(&offsets).ok());
+ arrow::Int64Builder sizes_builder;
+ ASSERT_TRUE(sizes_builder.Append(2).ok());
+ std::shared_ptr<arrow::Array> sizes;
+ ASSERT_TRUE(sizes_builder.Finish(&sizes).ok());
+ input = arrow::LargeListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ } else {
+ arrow::Int32Builder offsets_builder;
+ ASSERT_TRUE(offsets_builder.Append(0).ok());
+ std::shared_ptr<arrow::Array> offsets;
+ ASSERT_TRUE(offsets_builder.Finish(&offsets).ok());
+ arrow::Int32Builder sizes_builder;
+ ASSERT_TRUE(sizes_builder.Append(2).ok());
+ std::shared_ptr<arrow::Array> sizes;
+ ASSERT_TRUE(sizes_builder.Finish(&sizes).ok());
+ input = arrow::ListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ }
+
+ std::shared_ptr<arrow::Array> output;
+ ASSERT_TRUE(normalize_arrow_array(input, arrow::default_memory_pool(),
&output).ok());
+ ASSERT_TRUE(is_serde_acceptable_arrow_type(*output->type()));
+ const auto decoded_values =
+ large_offsets ?
std::static_pointer_cast<arrow::LargeListArray>(output)->values()
+ :
std::static_pointer_cast<arrow::ListArray>(output)->values();
+ ASSERT_EQ(decoded_values->type_id(), arrow::Type::INT8);
+ const auto decoded =
std::static_pointer_cast<arrow::Int8Array>(decoded_values);
+ EXPECT_EQ(decoded->Value(0), 42);
+ EXPECT_EQ(decoded->Value(1), 43);
+}
+
+TEST(ArrowArrayNormalizerTest, ListViewDictionaryChildIsDecoded) {
+ expect_dictionary_child_list_view_is_decoded(false);
+}
+
+TEST(ArrowArrayNormalizerTest, LargeListViewDictionaryChildIsDecoded) {
+ expect_dictionary_child_list_view_is_decoded(true);
+}
+
// An unsupported type must name itself, otherwise the offending column cannot
be found in prod.
TEST(ArrowArrayNormalizerTest, UnsupportedTypeFailsLoudWithTypeName) {
auto in = arrow::MakeArrayOfNull(arrow::month_interval(), 1).ValueOrDie();
EXPECT_FALSE(is_serde_acceptable_arrow_type(*in->type()));
std::shared_ptr<arrow::Array> out;
- Status st = normalize_arrow_array(in, &out);
+ Status st = normalize_arrow_array(in, arrow::default_memory_pool(), &out);
EXPECT_FALSE(st.ok());
EXPECT_NE(st.to_string().find("interval"), std::string::npos);
}
diff --git a/be/test/format_v2/table/adbc_reader_test.cpp
b/be/test/format_v2/table/adbc_reader_test.cpp
index 0d463641cba..00e7f68554b 100644
--- a/be/test/format_v2/table/adbc_reader_test.cpp
+++ b/be/test/format_v2/table/adbc_reader_test.cpp
@@ -35,11 +35,17 @@
#include "common/object_pool.h"
#include "core/assert_cast.h"
#include "core/block/block.h"
+#include "core/column/column_array.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_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 "format_v2/column_mapper.h"
#include "format_v2/file_reader.h"
#include "gen_cpp/PlanNodes_types.h"
#include "io/file_factory.h"
@@ -118,6 +124,33 @@ std::vector<SlotDescriptor*> string_slot(ObjectPool* pool,
DescriptorTbl** desc_
return (*desc_tbl)->get_tuple_descriptor(0)->slots();
}
+std::vector<SlotDescriptor*> int_array_slot(ObjectPool* pool, DescriptorTbl**
desc_tbl) {
+ DescriptorTblBuilder builder(pool);
+ builder.declare_tuple() << std::make_tuple(
+ std::make_shared<DataTypeArray>(std::make_shared<DataTypeInt32>()),
+ std::string("c_array"));
+ *desc_tbl = builder.build();
+ return (*desc_tbl)->get_tuple_descriptor(0)->slots();
+}
+
+DataTypePtr nullable_int32_type() {
+ return make_nullable(std::make_shared<DataTypeInt32>());
+}
+
+DataTypePtr array_struct_type(const Strings& names) {
+ DataTypes fields(names.size(), nullable_int32_type());
+ return make_nullable(std::make_shared<DataTypeArray>(
+ make_nullable(std::make_shared<DataTypeStruct>(std::move(fields),
names))));
+}
+
+std::vector<SlotDescriptor*> array_struct_slot(ObjectPool* pool,
DescriptorTbl** desc_tbl) {
+ DescriptorTblBuilder builder(pool);
+ builder.declare_tuple() << std::make_tuple(array_struct_type({"a", "b"}),
+ std::string("c_array"));
+ *desc_tbl = builder.build();
+ return (*desc_tbl)->get_tuple_descriptor(0)->slots();
+}
+
std::vector<SlotDescriptor*> sqlite_slots(ObjectPool* pool, DescriptorTbl**
desc_tbl) {
DescriptorTblBuilder builder(pool);
// SQLite stores integers as 64-bit and reals as doubles; the ADBC driver
reports them as such.
@@ -146,6 +179,143 @@ std::shared_ptr<arrow::RecordBatch>
make_large_string_batch() {
return make_named_large_string_batch("c_str");
}
+std::shared_ptr<arrow::RecordBatch> make_list_view_batch() {
+ arrow::Int32Builder offsets_builder;
+ EXPECT_TRUE(offsets_builder.AppendValues({2, 0}).ok());
+ std::shared_ptr<arrow::Array> offsets;
+ EXPECT_TRUE(offsets_builder.Finish(&offsets).ok());
+
+ arrow::Int32Builder sizes_builder;
+ EXPECT_TRUE(sizes_builder.AppendValues({1, 2}).ok());
+ std::shared_ptr<arrow::Array> sizes;
+ EXPECT_TRUE(sizes_builder.Finish(&sizes).ok());
+
+ arrow::Int32Builder values_builder;
+ EXPECT_TRUE(values_builder.AppendValues({10, 20, 30}).ok());
+ std::shared_ptr<arrow::Array> values;
+ EXPECT_TRUE(values_builder.Finish(&values).ok());
+
+ auto array = arrow::ListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ auto schema = arrow::schema({arrow::field("c_array", array->type())});
+ return arrow::RecordBatch::Make(schema, 2, {array});
+}
+
+std::shared_ptr<arrow::RecordBatch> make_nullable_list_view_batch(bool
large_offsets) {
+ std::shared_ptr<arrow::Array> offsets;
+ if (large_offsets) {
+ arrow::Int64Builder offsets_builder;
+ EXPECT_TRUE(offsets_builder.Append(0).ok());
+ EXPECT_TRUE(offsets_builder.AppendNull().ok());
+ EXPECT_TRUE(offsets_builder.Finish(&offsets).ok());
+ } else {
+ arrow::Int32Builder offsets_builder;
+ EXPECT_TRUE(offsets_builder.Append(0).ok());
+ EXPECT_TRUE(offsets_builder.AppendNull().ok());
+ EXPECT_TRUE(offsets_builder.Finish(&offsets).ok());
+ }
+
+ std::shared_ptr<arrow::Array> sizes;
+ if (large_offsets) {
+ arrow::Int64Builder sizes_builder;
+ EXPECT_TRUE(sizes_builder.AppendValues({1, 0}).ok());
+ EXPECT_TRUE(sizes_builder.Finish(&sizes).ok());
+ } else {
+ arrow::Int32Builder sizes_builder;
+ EXPECT_TRUE(sizes_builder.AppendValues({1, 0}).ok());
+ EXPECT_TRUE(sizes_builder.Finish(&sizes).ok());
+ }
+
+ arrow::Int32Builder values_builder;
+ EXPECT_TRUE(values_builder.Append(10).ok());
+ std::shared_ptr<arrow::Array> values;
+ EXPECT_TRUE(values_builder.Finish(&values).ok());
+
+ std::shared_ptr<arrow::Array> array;
+ if (large_offsets) {
+ array = arrow::LargeListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ } else {
+ array = arrow::ListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ }
+ auto schema = arrow::schema({arrow::field("c_array", array->type())});
+ return arrow::RecordBatch::Make(schema, 2, {array});
+}
+
+std::shared_ptr<arrow::RecordBatch> make_float_list_view_batch(bool
large_offsets) {
+ std::shared_ptr<arrow::Array> offsets;
+ std::shared_ptr<arrow::Array> sizes;
+ if (large_offsets) {
+ arrow::Int64Builder offsets_builder;
+ EXPECT_TRUE(offsets_builder.Append(0).ok());
+ EXPECT_TRUE(offsets_builder.Finish(&offsets).ok());
+ arrow::Int64Builder sizes_builder;
+ EXPECT_TRUE(sizes_builder.Append(1).ok());
+ EXPECT_TRUE(sizes_builder.Finish(&sizes).ok());
+ } else {
+ arrow::Int32Builder offsets_builder;
+ EXPECT_TRUE(offsets_builder.Append(0).ok());
+ EXPECT_TRUE(offsets_builder.Finish(&offsets).ok());
+ arrow::Int32Builder sizes_builder;
+ EXPECT_TRUE(sizes_builder.Append(1).ok());
+ EXPECT_TRUE(sizes_builder.Finish(&sizes).ok());
+ }
+
+ arrow::FloatBuilder values_builder;
+ EXPECT_TRUE(values_builder.Append(1.0F).ok());
+ std::shared_ptr<arrow::Array> values;
+ EXPECT_TRUE(values_builder.Finish(&values).ok());
+
+ std::shared_ptr<arrow::Array> array;
+ if (large_offsets) {
+ array = arrow::LargeListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ } else {
+ array = arrow::ListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ }
+ return arrow::RecordBatch::Make(arrow::schema({arrow::field("c_array",
array->type())}), 1,
+ {array});
+}
+
+std::shared_ptr<arrow::RecordBatch> make_struct_list_view_batch(const Strings&
names = {"a", "b"}) {
+ arrow::Int32Builder offsets_builder;
+ EXPECT_TRUE(offsets_builder.Append(0).ok());
+ std::shared_ptr<arrow::Array> offsets;
+ EXPECT_TRUE(offsets_builder.Finish(&offsets).ok());
+ arrow::Int32Builder sizes_builder;
+ EXPECT_TRUE(sizes_builder.Append(2).ok());
+ std::shared_ptr<arrow::Array> sizes;
+ EXPECT_TRUE(sizes_builder.Finish(&sizes).ok());
+
+ arrow::Int32Builder a_builder;
+ EXPECT_TRUE(a_builder.AppendValues({11, 12}).ok());
+ std::shared_ptr<arrow::Array> a;
+ EXPECT_TRUE(a_builder.Finish(&a).ok());
+ arrow::Int32Builder b_builder;
+ EXPECT_TRUE(b_builder.AppendValues({21, 22}).ok());
+ std::shared_ptr<arrow::Array> b;
+ EXPECT_TRUE(b_builder.Finish(&b).ok());
+ auto values = arrow::StructArray::Make({a, b}, {arrow::field(names[0],
arrow::int32()),
+ arrow::field(names[1],
arrow::int32())})
+ .ValueOrDie();
+ auto array = arrow::ListViewArray::FromArrays(*offsets, *sizes,
*values).ValueOrDie();
+ return arrow::RecordBatch::Make(arrow::schema({arrow::field("c_array",
array->type())}), 1,
+ {array});
+}
+
+ColumnDefinition projected_array_struct_b() {
+ ColumnDefinition b {.identifier = Field::create_field<TYPE_STRING>("b"),
+ .name = "b",
+ .type = nullable_int32_type()};
+ auto struct_type =
+ make_nullable(std::make_shared<DataTypeStruct>(DataTypes {b.type},
Strings {"b"}));
+ ColumnDefinition element {.identifier =
Field::create_field<TYPE_STRING>("element"),
+ .name = "element",
+ .type = struct_type,
+ .children = {b}};
+ return {.identifier = Field::create_field<TYPE_STRING>("c_array"),
+ .name = "c_array",
+ .type =
make_nullable(std::make_shared<DataTypeArray>(struct_type)),
+ .children = {element}};
+}
+
std::unique_ptr<AdbcFileReader> create_reader(RuntimeProfile* profile, const
TFileRangeDesc& range,
const
std::vector<SlotDescriptor*>& slots,
AdbcStreamFactory factory) {
@@ -301,6 +471,242 @@ TEST(AdbcReaderTest,
NormalizesLargeStringBeforeMaterializing) {
EXPECT_EQ(*close_count, 1);
}
+TEST(AdbcReaderTest, NormalizesListViewBeforeMaterializing) {
+ ObjectPool pool;
+ DescriptorTbl* desc_tbl = nullptr;
+ const auto slots = int_array_slot(&pool, &desc_tbl);
+ RuntimeState state;
+ RuntimeProfile profile("adbc_reader_list_view_test");
+ auto close_count = std::make_shared<int>(0);
+
+ auto reader = create_reader(
+ &profile, fake_adbc_range(), slots,
+ [close_count](const TFileRangeDesc&, std::unique_ptr<AdbcStream>*
out) {
+ *out = std::make_unique<BatchAdbcStream>(
+ std::vector<std::shared_ptr<arrow::RecordBatch>>
{make_list_view_batch()},
+ close_count);
+ return Status::OK();
+ });
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<FileScanRequest>();
+ FileScanRequestBuilder builder(request.get());
+ ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
+ ASSERT_TRUE(reader->open(request).ok());
+
+ auto block = make_request_block(schema, {0});
+ size_t rows = 0;
+ bool eof = false;
+ const auto status = reader->get_block(&block, &rows, &eof);
+ ASSERT_TRUE(status.ok()) << status.to_string();
+ ASSERT_EQ(rows, 2);
+ const auto& nullable = assert_cast<const
ColumnNullable&>(*block.get_by_position(0).column);
+ const auto& array = assert_cast<const
ColumnArray&>(nullable.get_nested_column());
+ ASSERT_EQ(array.get_offsets(), ColumnArray::Offsets64({1, 3}));
+ const auto& elements = assert_cast<const
ColumnNullable&>(array.get_data());
+ const auto& data = assert_cast<const
ColumnInt32&>(elements.get_nested_column()).get_data();
+ EXPECT_EQ(data, ColumnInt32::Container({30, 10, 20}));
+
+ ASSERT_TRUE(reader->close().ok());
+ EXPECT_EQ(*close_count, 1);
+}
+
+void assert_required_array_rejects_null_list_view(bool large_offsets) {
+ ObjectPool pool;
+ DescriptorTbl* desc_tbl = nullptr;
+ const auto slots = int_array_slot(&pool, &desc_tbl);
+ RuntimeState state;
+ RuntimeProfile profile(large_offsets ?
"adbc_reader_required_large_list_view_test"
+ :
"adbc_reader_required_list_view_test");
+ auto close_count = std::make_shared<int>(0);
+
+ auto reader = create_reader(
+ &profile, fake_adbc_range(), slots,
+ [close_count, large_offsets](const TFileRangeDesc&,
std::unique_ptr<AdbcStream>* out) {
+ *out = std::make_unique<BatchAdbcStream>(
+ std::vector<std::shared_ptr<arrow::RecordBatch>> {
+ make_nullable_list_view_batch(large_offsets)},
+ close_count);
+ return Status::OK();
+ });
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<FileScanRequest>();
+ FileScanRequestBuilder builder(request.get());
+ ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
+ ASSERT_TRUE(reader->open(request).ok());
+
+ const auto required_type = remove_nullable(schema[0].type);
+ ASSERT_FALSE(required_type->is_nullable());
+ Block block;
+ block.insert({required_type->create_column(), required_type,
schema[0].name});
+ size_t rows = 0;
+ bool eof = false;
+ const auto status = reader->get_block(&block, &rows, &eof);
+ EXPECT_FALSE(status.ok());
+ EXPECT_NE(status.to_string().find("c_array"), std::string::npos) <<
status.to_string();
+ EXPECT_NE(status.to_string().find("non-nullable"), std::string::npos) <<
status.to_string();
+
+ ASSERT_TRUE(reader->close().ok());
+ EXPECT_EQ(*close_count, 1);
+}
+
+TEST(AdbcReaderTest, RejectsNullListViewForRequiredArray) {
+ assert_required_array_rejects_null_list_view(false);
+}
+
+TEST(AdbcReaderTest, RejectsNullLargeListViewForRequiredArray) {
+ assert_required_array_rejects_null_list_view(true);
+}
+
+void assert_int_array_rejects_float_list_view(bool large_offsets) {
+ ObjectPool pool;
+ DescriptorTbl* desc_tbl = nullptr;
+ const auto slots = int_array_slot(&pool, &desc_tbl);
+ RuntimeState state;
+ RuntimeProfile profile(large_offsets ?
"adbc_reader_large_list_view_schema_drift_test"
+ :
"adbc_reader_list_view_schema_drift_test");
+ auto close_count = std::make_shared<int>(0);
+
+ auto reader = create_reader(
+ &profile, fake_adbc_range(), slots,
+ [close_count, large_offsets](const TFileRangeDesc&,
std::unique_ptr<AdbcStream>* out) {
+ *out = std::make_unique<BatchAdbcStream>(
+ std::vector<std::shared_ptr<arrow::RecordBatch>> {
+ make_float_list_view_batch(large_offsets)},
+ close_count);
+ return Status::OK();
+ });
+ ASSERT_TRUE(reader->init(&state).ok());
+
+ std::vector<ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<FileScanRequest>();
+ FileScanRequestBuilder builder(request.get());
+ ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
+ ASSERT_TRUE(reader->open(request).ok());
+
+ auto block = make_request_block(schema, {0});
+ size_t rows = 0;
+ bool eof = false;
+ const auto status = reader->get_block(&block, &rows, &eof);
+ EXPECT_FALSE(status.ok());
+ EXPECT_NE(status.to_string().find("c_array"), std::string::npos) <<
status.to_string();
+ EXPECT_NE(status.to_string().find("float"), std::string::npos) <<
status.to_string();
+ EXPECT_NE(status.to_string().find("INT"), std::string::npos) <<
status.to_string();
+
+ ASSERT_TRUE(reader->close().ok());
+ EXPECT_EQ(*close_count, 1);
+}
+
+TEST(AdbcReaderTest, RejectsFloatListViewForCachedIntArray) {
+ assert_int_array_rejects_float_list_view(false);
+}
+
+TEST(AdbcReaderTest, RejectsFloatLargeListViewForCachedIntArray) {
+ assert_int_array_rejects_float_list_view(true);
+}
+
+TEST(AdbcReaderTest, CreatesMaterializedColumnMapper) {
+ ObjectPool pool;
+ DescriptorTbl* desc_tbl = nullptr;
+ const auto slots = int_array_slot(&pool, &desc_tbl);
+ RuntimeProfile profile("adbc_reader_materialized_mapper_test");
+ auto reader = create_reader(&profile, fake_adbc_range(), slots, {});
+
+ auto mapper = reader->create_column_mapper({.mode =
TableColumnMappingMode::BY_NAME});
+
+ ASSERT_NE(dynamic_cast<MaterializedColumnMapper*>(mapper.get()), nullptr);
+}
+
+TEST(AdbcReaderTest, RejectsReorderedSameTypedStructFields) {
+ ObjectPool pool;
+ DescriptorTbl* desc_tbl = nullptr;
+ const auto slots = array_struct_slot(&pool, &desc_tbl);
+ RuntimeState state;
+ RuntimeProfile profile("adbc_reader_reordered_struct_test");
+ auto close_count = std::make_shared<int>(0);
+ auto reader =
+ create_reader(&profile, fake_adbc_range(), slots,
+ [close_count](const TFileRangeDesc&,
std::unique_ptr<AdbcStream>* out) {
+ *out = std::make_unique<BatchAdbcStream>(
+
std::vector<std::shared_ptr<arrow::RecordBatch>> {
+
make_struct_list_view_batch({"b", "a"})},
+ close_count);
+ return Status::OK();
+ });
+ ASSERT_TRUE(reader->init(&state).ok());
+ std::vector<ColumnDefinition> schema;
+ ASSERT_TRUE(reader->get_schema(&schema).ok());
+ auto request = std::make_shared<FileScanRequest>();
+ FileScanRequestBuilder builder(request.get());
+ ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
+ ASSERT_TRUE(reader->open(request).ok());
+
+ auto block = make_request_block(schema, {0});
+ size_t rows = 0;
+ bool eof = false;
+ const auto status = reader->get_block(&block, &rows, &eof);
+ EXPECT_FALSE(status.ok());
+ EXPECT_NE(status.to_string().find("runtime field 'b'"), std::string::npos)
+ << status.to_string();
+ EXPECT_NE(status.to_string().find("cached field 'a'"), std::string::npos)
<< status.to_string();
+
+ ASSERT_TRUE(reader->close().ok());
+ EXPECT_EQ(*close_count, 1);
+}
+
+TEST(AdbcReaderTest, TableReaderProjectsAListViewStructChildByName) {
+ ObjectPool pool;
+ DescriptorTbl* desc_tbl = nullptr;
+ const auto slots = array_struct_slot(&pool, &desc_tbl);
+ RuntimeState state;
+ RuntimeProfile profile("adbc_reader_nested_projection_test");
+ auto close_count = std::make_shared<int>(0);
+ AdbcReader reader([close_count](const TFileRangeDesc&,
std::unique_ptr<AdbcStream>* out) {
+ *out = std::make_unique<BatchAdbcStream>(
+ std::vector<std::shared_ptr<arrow::RecordBatch>>
{make_struct_list_view_batch()},
+ close_count);
+ return Status::OK();
+ });
+ const auto projected = projected_array_struct_b();
+ TFileScanRangeParams scan_params;
+ ASSERT_TRUE(reader.init({.projected_columns = {projected},
+ .conjuncts = {},
+ .format = FileFormat::ARROW,
+ .scan_params = &scan_params,
+ .io_ctx = std::make_shared<io::IOContext>(),
+ .runtime_state = &state,
+ .scanner_profile = &profile,
+ .file_slot_descs = &slots})
+ .ok());
+ SplitReadOptions split;
+ split.current_range = fake_adbc_range();
+ split.current_split_format = FileFormat::ARROW;
+ ASSERT_TRUE(reader.prepare_split(split).ok());
+
+ Block block;
+ block.insert({projected.type->create_column(), projected.type,
projected.name});
+ bool eos = false;
+ const auto status = reader.get_block(&block, &eos);
+ ASSERT_TRUE(status.ok()) << status.to_string();
+ ASSERT_EQ(block.rows(), 1);
+ const auto& root = assert_cast<const
ColumnNullable&>(*block.get_by_position(0).column);
+ const auto& array = assert_cast<const
ColumnArray&>(root.get_nested_column());
+ const auto& element = assert_cast<const ColumnNullable&>(array.get_data());
+ const auto& row = assert_cast<const
ColumnStruct&>(element.get_nested_column());
+ const auto& b = assert_cast<const ColumnNullable&>(row.get_column(0));
+ const auto& values = assert_cast<const
ColumnInt32&>(b.get_nested_column()).get_data();
+ EXPECT_EQ(values, ColumnInt32::Container({21, 22}));
+
+ ASSERT_TRUE(reader.close().ok());
+ EXPECT_EQ(*close_count, 1);
+}
+
// An all-null int64 array standing in for a TEXT column, which is what a
source that infers Arrow
// types from the values it returns sends when a filter leaves only nulls.
std::shared_ptr<arrow::RecordBatch> make_all_null_int64_batch(const
std::string& column_name) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]