Skip to content

Commit 59ae97b

Browse files
banmoyclaude
andcommitted
[Enhancement] Support native MAP/STRUCT target columns for Avro routine load
Routine Load with format=avro aborted at BE scanner open() when a destination column was MAP or STRUCT, failing with "Not support cast VARCHAR(1048576) to MAP<...>". _construct_avro_types() only had a native ARRAY branch; MAP/STRUCT fell through to VARCHAR(MAX), and the subsequent VARCHAR->MAP/STRUCT cast does not exist. Build native MAP/STRUCT intermediate types and writers (mirroring the avro-cpp readers), and dispatch the column writer on the intermediate (_avro_types) type rather than the destination type so the source-column down_casts match. - avro_scanner: recursive construct_avro_type() for ARRAY/MAP/STRUCT; dispatch _construct_column on _avro_types in both the jsonpath and no-jsonpath paths (via a slot-id -> intermediate-type map). - formats/avro/nullable_column: native add_map_column/add_struct_column, union unwrapping for nested map values / struct fields / array elements, and snapshot/resize rewind on partial nested append. Avro map keys are always strings, so the intermediate map key is forced to VARCHAR/CHAR and the cast stage converts to the declared key type. The existing avro->JSON path (TYPE_JSON columns) is unchanged. Tests: native MAP/STRUCT, nested (ARRAY<MAP>, MAP<ARRAY>, ARRAY<STRUCT>, STRUCT-with-MAP, 4-level deep), nullable-union fields (null + value), both avro_ignore_union_type_tag values; regression test_map_to_json preserved. Docs: update the Avro -> StarRocks complex-type mapping table (maps -> MAP, record -> STRUCT) in loading/RoutineLoad.md (en/zh/ja). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9f95b67 commit 59ae97b

7 files changed

Lines changed: 726 additions & 101 deletions

File tree

be/src/exec/file_scanner/avro_scanner.cpp

Lines changed: 76 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,15 @@ Status AvroScanner::open() {
161161
}
162162
++_counter->num_files_read;
163163

164-
for (const auto& desc : _src_slot_descriptors) {
164+
for (size_t i = 0; i < _src_slot_descriptors.size(); ++i) {
165+
const auto& desc = _src_slot_descriptors[i];
165166
if (desc == nullptr) {
166167
continue;
167168
}
168169
_slot_desc_dict.emplace(desc->col_name(), desc);
170+
// Remember the intermediate avro type per slot so the no-jsonpath path can dispatch
171+
// _construct_column on the source column's type rather than the destination type.
172+
_slot_id_to_avro_type.emplace(desc->id(), _avro_types[i]);
169173
}
170174
_init_data_idx_to_slot_once = false;
171175
return Status::OK();
@@ -217,8 +221,13 @@ Status AvroScanner::_construct_row(const avro_value_t& avro_value, Chunk* chunk)
217221
avro_value_t output_value;
218222
auto st = _extract_field(avro_value, _json_paths[i], &output_value);
219223
if (LIKELY(st.ok())) {
220-
RETURN_IF_ERROR(_construct_column(output_value, column, _src_slot_descriptors[i]->type(),
221-
_src_slot_descriptors[i]->col_name()));
224+
// Dispatch on the intermediate avro type (which matches the source column created in
225+
// _create_src_chunk), not the destination type. Otherwise a complex column whose
226+
// intermediate type differs from the destination (e.g. MAP<VARCHAR,VARCHAR> vs
227+
// MAP<VARCHAR,DATE>) would be written by a writer chosen for the destination and crash
228+
// on down_cast. The cast stage converts the intermediate column to the destination type.
229+
RETURN_IF_ERROR(
230+
_construct_column(output_value, column, _avro_types[i], _src_slot_descriptors[i]->col_name()));
222231
} else if (st.is_not_found()) {
223232
column->append_nulls(1);
224233
} else {
@@ -355,7 +364,9 @@ Status AvroScanner::_construct_row_without_jsonpath(const avro_value_t& avro_val
355364
}
356365
auto slot_desc = itr->second;
357366
slot_info.id = slot_desc->id();
358-
slot_info.type = slot_desc->type();
367+
// Store the intermediate avro type (not the destination type) so _construct_column
368+
// dispatches on the type the source column was built with. See _construct_row.
369+
slot_info.type = _slot_id_to_avro_type[slot_desc->id()];
359370
slot_info.key = key;
360371
int column_index = chunk->get_index_by_slot_id(slot_info.id);
361372
_found_columns[column_index] = true;
@@ -460,87 +471,75 @@ Status AvroScanner::_construct_cast_exprs() {
460471
return Status::OK();
461472
}
462473

463-
Status AvroScanner::_construct_avro_types() {
464-
size_t slot_size = _src_slot_descriptors.size();
465-
_avro_types.resize(slot_size);
466-
for (int column_pos = 0; column_pos < slot_size; ++column_pos) {
467-
auto slot_desc = _src_slot_descriptors[column_pos];
468-
if (slot_desc == nullptr) {
469-
continue;
474+
// Build the intermediate avro load type for a destination slot type. Complex types (ARRAY / MAP /
475+
// STRUCT) recurse so they are loaded natively; directly-representable scalars are kept as-is and
476+
// everything else is loaded as VARCHAR(MAX), with the cast stage converting to the destination type
477+
// (the cast factory supports recursive ARRAY/MAP/STRUCT casts). This mirrors
478+
// JsonUtils::construct_json_type. Avro map keys are always strings, so a map's intermediate key
479+
// type is forced to a string type regardless of the destination key type.
480+
static TypeDescriptor construct_avro_type(const TypeDescriptor& slot_type) {
481+
switch (slot_type.type) {
482+
case TYPE_ARRAY: {
483+
TypeDescriptor avro_type(TYPE_ARRAY);
484+
avro_type.children.emplace_back(construct_avro_type(slot_type.children[0]));
485+
return avro_type;
486+
}
487+
case TYPE_MAP: {
488+
TypeDescriptor avro_type(TYPE_MAP);
489+
const auto& key_type = slot_type.children[0];
490+
if (key_type.type == TYPE_CHAR) {
491+
avro_type.children.emplace_back(TypeDescriptor::create_char_type(key_type.len));
492+
} else if (key_type.type == TYPE_VARCHAR) {
493+
avro_type.children.emplace_back(TypeDescriptor::create_varchar_type(key_type.len));
494+
} else {
495+
avro_type.children.emplace_back(TypeDescriptor::create_varchar_type(TypeDescriptor::MAX_VARCHAR_LENGTH));
470496
}
471-
472-
switch (slot_desc->type().type) {
473-
case TYPE_ARRAY: {
474-
TypeDescriptor json_type(TYPE_ARRAY);
475-
TypeDescriptor* child_type = &json_type;
476-
477-
const TypeDescriptor* slot_type = &(slot_desc->type().children[0]);
478-
while (slot_type->type == TYPE_ARRAY) {
479-
slot_type = &(slot_type->children[0]);
480-
481-
child_type->children.emplace_back(TYPE_ARRAY);
482-
child_type = &(child_type->children[0]);
483-
}
484-
485-
// the json lib don't support get_int128_t(), so we load with BinaryColumn and then convert to LargeIntColumn
486-
if (slot_type->type == TYPE_FLOAT || slot_type->type == TYPE_DOUBLE || slot_type->type == TYPE_BIGINT ||
487-
slot_type->type == TYPE_INT || slot_type->type == TYPE_SMALLINT || slot_type->type == TYPE_TINYINT) {
488-
// Treat these types as what they are.
489-
child_type->children.emplace_back(slot_type->type);
490-
} else if (slot_type->type == TYPE_VARCHAR) {
491-
auto varchar_type = TypeDescriptor::create_varchar_type(slot_type->len);
492-
child_type->children.emplace_back(varchar_type);
493-
} else if (slot_type->type == TYPE_CHAR) {
494-
auto char_type = TypeDescriptor::create_char_type(slot_type->len);
495-
child_type->children.emplace_back(char_type);
496-
} else if (slot_type->type == TYPE_JSON) {
497-
child_type->children.emplace_back(TypeDescriptor::create_json_type());
498-
} else {
499-
// Treat other types as VARCHAR.
500-
auto varchar_type = TypeDescriptor::create_varchar_type(TypeDescriptor::MAX_VARCHAR_LENGTH);
501-
child_type->children.emplace_back(varchar_type);
502-
}
503-
504-
_avro_types[column_pos] = std::move(json_type);
505-
break;
497+
avro_type.children.emplace_back(construct_avro_type(slot_type.children[1]));
498+
return avro_type;
499+
}
500+
case TYPE_STRUCT: {
501+
TypeDescriptor avro_type(TYPE_STRUCT);
502+
avro_type.field_names = slot_type.field_names;
503+
for (const auto& child : slot_type.children) {
504+
avro_type.children.emplace_back(construct_avro_type(child));
506505
}
506+
return avro_type;
507+
}
507508

508-
// Treat these types as what they are.
509-
case TYPE_FLOAT:
510-
case TYPE_DOUBLE:
511-
case TYPE_BIGINT:
512-
case TYPE_INT:
513-
case TYPE_BOOLEAN:
514-
case TYPE_SMALLINT:
515-
case TYPE_TINYINT: {
516-
_avro_types[column_pos] = TypeDescriptor{slot_desc->type().type};
517-
break;
518-
}
509+
// Treat these types as what they are.
510+
case TYPE_FLOAT:
511+
case TYPE_DOUBLE:
512+
case TYPE_BIGINT:
513+
case TYPE_INT:
514+
case TYPE_BOOLEAN:
515+
case TYPE_SMALLINT:
516+
case TYPE_TINYINT:
517+
return TypeDescriptor{slot_type.type};
519518

520-
case TYPE_CHAR: {
521-
auto char_type = TypeDescriptor::create_char_type(slot_desc->type().len);
522-
_avro_types[column_pos] = std::move(char_type);
523-
break;
524-
}
519+
case TYPE_CHAR:
520+
return TypeDescriptor::create_char_type(slot_type.len);
525521

526-
case TYPE_VARCHAR: {
527-
auto varchar_type = TypeDescriptor::create_varchar_type(slot_desc->type().len);
528-
_avro_types[column_pos] = std::move(varchar_type);
529-
break;
530-
}
522+
case TYPE_VARCHAR:
523+
return TypeDescriptor::create_varchar_type(slot_type.len);
531524

532-
case TYPE_JSON: {
533-
_avro_types[column_pos] = TypeDescriptor::create_json_type();
534-
break;
535-
}
525+
case TYPE_JSON:
526+
return TypeDescriptor::create_json_type();
536527

537-
// Treat other types as VARCHAR.
538-
default: {
539-
auto varchar_type = TypeDescriptor::create_varchar_type(TypeDescriptor::MAX_VARCHAR_LENGTH);
540-
_avro_types[column_pos] = std::move(varchar_type);
541-
break;
542-
}
528+
// Treat other types as VARCHAR.
529+
default:
530+
return TypeDescriptor::create_varchar_type(TypeDescriptor::MAX_VARCHAR_LENGTH);
531+
}
532+
}
533+
534+
Status AvroScanner::_construct_avro_types() {
535+
size_t slot_size = _src_slot_descriptors.size();
536+
_avro_types.resize(slot_size);
537+
for (int column_pos = 0; column_pos < slot_size; ++column_pos) {
538+
auto slot_desc = _src_slot_descriptors[column_pos];
539+
if (slot_desc == nullptr) {
540+
continue;
543541
}
542+
_avro_types[column_pos] = construct_avro_type(slot_desc->type());
544543
}
545544
return Status::OK();
546545
}

be/src/exec/file_scanner/avro_scanner.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ class AvroScanner final : public FileScanner {
9797
ObjectPool _pool;
9898
std::shared_ptr<SequentialFile> _file;
9999
std::unordered_map<std::string_view, SlotDescriptor*> _slot_desc_dict;
100+
// Maps each source slot id to its intermediate avro load type (see AvroScanner::_construct_avro_types).
101+
std::unordered_map<SlotId, TypeDescriptor> _slot_id_to_avro_type;
100102
std::vector<bool> _found_columns;
101103
std::vector<SlotInfo> _data_idx_to_slot;
102104
std::vector<std::string> _data_idx_to_fieldname;

0 commit comments

Comments
 (0)