Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
270c73a
feat(paimon): upgrade connector runtime to 2.0.0
Aug 14, 2026
897958a
feat(paimon): add transactional table writes
Aug 14, 2026
c1323f7
fix(paimon): package file format factories
Aug 14, 2026
ff1f9ef
fix(paimon): allow varbinary in external table DDL
Aug 14, 2026
3ee3fda
[fix](paimon) support Paimon 2.0 VARIANT and Vortex reads
Aug 15, 2026
4570ca9
fix(paimon): validate Vortex schemas before create
Aug 15, 2026
27660eb
fix(paimon): satisfy variant static checks
Aug 15, 2026
1e25500
[fix](build) write the hms-hive-shade artifact via explicit outputFile
Aug 18, 2026
02e7d9c
[fix](filesystem) accept the dotted s3.path.style.access alias
Aug 18, 2026
f409caf
[feature](paimon) column schema evolution: ALTER TABLE ADD/DROP/RENAM…
Aug 18, 2026
b578cde
[feature](paimon) row-level DELETE, UPDATE and MERGE INTO
Aug 18, 2026
9cbd4f0
[test](paimon) regression baselines for schema evolution and row-leve…
Aug 18, 2026
f1a1c48
[fix](paimon) keep the historical locator-name fallback and test the …
Aug 19, 2026
b5c73e2
[fix](connector) route static-partition writes through the full-schem…
Aug 19, 2026
66c5888
[fix](paimon) size static-partition column_names off the bound schema…
Aug 20, 2026
77dde25
[fix](paimon) adapt upstream's #66818 scan-limit test to the Paimon 2…
Aug 20, 2026
74e8eeb
[fix](paimon) evict the CachingCatalog's frozen Table on invalidateTa…
Aug 20, 2026
3c446b1
[feature](paimon) support ALTER TABLE ... DROP PARTITION on external …
Aug 20, 2026
464674c
[feature](paimon) support UPDATE and MERGE INTO on append-only tables…
Aug 20, 2026
1efb6c1
[feature](paimon) auto-heal FE table caches after an out-of-band exte…
Aug 20, 2026
006cb11
[fix](connector) canonicalize DECIMALV3 type name on the fe-core writ…
Aug 21, 2026
8c1bcd6
[fix](test) build the DECIMALV2 regression column via the PrimitiveTy…
Aug 21, 2026
a9530f0
[feature](paimon) support RENAME TABLE and TRUNCATE TABLE on external…
Aug 21, 2026
9daeaa6
[fix](paimon) adapt HMS catalog construction to Paimon 2.0 CatalogCon…
Aug 27, 2026
d3eb044
[test](paimon) fix PaimonHmsCatalogTest for CatalogContext API change
Aug 27, 2026
1731885
[fix](be) remove duplicate default argument on clear_blocks forward d…
Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions be/src/common/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1721,6 +1721,11 @@ DEFINE_mInt64(hive_sink_max_file_size, "1073741824"); // 1GB
/** Iceberg sink configurations **/
DEFINE_mInt64(iceberg_sink_max_file_size, "1073741824"); // 1GB

/** Paimon sink configurations **/
DEFINE_mInt64(paimon_jni_writer_memory_pool_limit_bytes, "536870912"); // 512MB
DEFINE_Validator(paimon_jni_writer_memory_pool_limit_bytes,
[](int64_t bytes) -> bool { return bytes > 0; });

// URI scheme to Doris file type mappings used by paimon-cpp DorisFileSystem.
// Each entry uses the format "<scheme>=<file_type>", and file_type must be one of:
// local, hdfs, s3, http, broker.
Expand Down
4 changes: 4 additions & 0 deletions be/src/common/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -1787,6 +1787,10 @@ DECLARE_mInt64(hive_sink_max_file_size);
/** Iceberg sink configurations **/
DECLARE_mInt64(iceberg_sink_max_file_size);

/** Paimon sink configurations **/
// Hard upper bound for Doris-managed Paimon write-buffer memory per JNI writer.
DECLARE_mInt64(paimon_jni_writer_memory_pool_limit_bytes);

/** Paimon file system configurations **/
DECLARE_Strings(paimon_file_system_scheme_mappings);

Expand Down
3 changes: 3 additions & 0 deletions be/src/common/consts.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const std::string DYNAMIC_COLUMN_NAME = "__DORIS_DYNAMIC_COL__";
const std::string PARTIAL_UPDATE_AUTO_INC_COL = "__PARTIAL_UPDATE_AUTO_INC_COLUMN__";
const std::string VIRTUAL_COLUMN_PREFIX = "__DORIS_VIRTUAL_COL__";
const std::string ICEBERG_ROWID_COL = "__DORIS_ICEBERG_ROWID_COL__";
// Paimon row locator for append-only row-level DML: STRUCT<file_path STRING, row_position BIGINT>.
// Must equal FE's PaimonRowLevelDmlColumns.ROWID_COL / PaimonWritePlanProvider.DORIS_PAIMON_ROWID_COL.
const std::string PAIMON_ROWID_COL = "__DORIS_PAIMON_ROWID_COL__";

/// The maximum precision representable by a 4-byte decimal (Decimal4Value)
constexpr int MAX_DECIMAL32_PRECISION = 9;
Expand Down
49 changes: 26 additions & 23 deletions be/src/core/column/column_variant.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ DataTypeSerDeSPtr ColumnVariant::Subcolumn::generate_data_serdes(DataTypePtr typ

ColumnVariant::Subcolumn::Subcolumn(MutableColumnPtr&& data_, DataTypePtr type, bool is_nullable_,
bool is_root_)
: least_common_type(type),
: least_common_type(type, is_root_),
is_nullable(is_nullable_),
is_root(is_root_),
num_rows(data_->size()) {
Expand Down Expand Up @@ -463,6 +463,11 @@ void ColumnVariant::Subcolumn::insert_range_from(const Subcolumn& src, size_t st
size_t part_end = end - processed_rows;
insert_from_part(src.data[pos], src.data_types[pos], 0, part_end);
}

const size_t trailing_defaults_start = std::max(start, src.num_rows);
if (end > trailing_defaults_start) {
data.back()->insert_many_defaults(end - trailing_defaults_start);
}
}

bool ColumnVariant::Subcolumn::is_finalized() const {
Expand All @@ -488,20 +493,29 @@ MutableColumnPtr ColumnVariant::apply_for_columns(Func&& func) const {
auto& finalized_object = assert_cast<ColumnVariant&>(*finalized);
return finalized_object.apply_for_columns(std::forward<Func>(func));
}
auto new_root = std::move(*func(get_root())).mutate();
auto res = ColumnVariant::create(_max_subcolumns_count, _enable_doc_mode, get_root_type(),
std::move(new_root));
Subcolumns transformed_subcolumns;
for (const auto& subcolumn : subcolumns) {
if (subcolumn->data.is_root) {
auto transformed = std::move(*func(subcolumn->data.get_finalized_column_ptr())).mutate();
Subcolumn transformed_subcolumn(std::move(transformed),
subcolumn->data.get_least_common_type(), is_nullable,
subcolumn->data.is_root);
if (subcolumn->data.is_root || subcolumn->path.empty()) {
transformed_subcolumns.create_root(std::move(transformed_subcolumn));
continue;
}
auto new_subcolumn = func(subcolumn->data.get_finalized_column_ptr());
if (!res->add_sub_column(subcolumn->path, std::move(*new_subcolumn).mutate(),
subcolumn->data.get_least_common_type())) {
if (!transformed_subcolumns.add(subcolumn->path, std::move(transformed_subcolumn))) {
throw doris::Exception(ErrorCode::INTERNAL_ERROR, "add path {} is error",
subcolumn->path.get_path());
}
}
if (transformed_subcolumns.get_root() == nullptr) {
throw doris::Exception(ErrorCode::INTERNAL_ERROR,
"root is nullptr after transforming variant columns");
}
auto res = ColumnVariant::create(_max_subcolumns_count, _enable_doc_mode,
std::move(transformed_subcolumns));
res->typed_path_count = typed_path_count;
res->nested_path_count = nested_path_count;
auto sparse_column = func(serialized_sparse_column);
res->serialized_sparse_column = IColumn::mutate(std::move(sparse_column));
auto doc_value_column = func(serialized_doc_value_column);
Expand Down Expand Up @@ -869,21 +883,10 @@ void ColumnVariant::insert_from(const IColumn& src, size_t n) {
const auto* src_v = assert_cast<const ColumnVariant*>(&src);
ENABLE_CHECK_CONSISTENCY(src_v);
ENABLE_CHECK_CONSISTENCY(this);
// Preserve the original root-only copy path for ordinary variant columns.
// Reconstructing through try_insert() loses sparse/doc_value structure for
// mixed-shape rows and nested-group data.
if (src_v->get_subcolumns().size() == 1 && get_subcolumns().size() == 1) {
DCHECK(_enable_doc_mode == src_v->_enable_doc_mode)
<< "root-only variant copy requires matching doc mode";
FieldWithDataType field;
src_v->subcolumns.get_root()->data.get(n, field);
subcolumns.get_mutable_root()->data.insert(field);
serialized_sparse_column->insert_from(*src_v->get_sparse_column(), n);
serialized_doc_value_column->insert_from(*src_v->get_doc_value_column(), n);
num_rows++;
} else {
try_insert((*src_v)[n]);
}
// Keep complex and materialized object paths in their native subcolumns. Reconstructing a
// single row as a Field can turn nested objects into TYPE_STRUCT, which is not a scalar type
// that Subcolumn::insert() can create through DataTypeFactory.
insert_range_from(*src_v, n, 1);
ENABLE_CHECK_CONSISTENCY(this);
}

Expand Down
80 changes: 79 additions & 1 deletion be/src/core/data_type_serde/data_type_variant_serde.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "core/data_type_serde/data_type_variant_serde.h"

#include <arrow/array/builder_binary.h>
#include <arrow/array/builder_nested.h>

#include <cstdint>
#include <string>
Expand All @@ -35,6 +36,7 @@
#include "core/types.h"
#include "core/value/jsonb_value.h"
#include "exec/common/variant_util.h"
#include "exprs/function/parse/variant_jsonb_parse.h"
#include "util/json/json_parser.h"
#include "util/jsonb_writer.h"

Expand Down Expand Up @@ -63,6 +65,77 @@ Status write_variant_column_to_arrow_impl(const IColumn& column, const ColumnVar
return Status::OK();
}

Status write_variant_column_to_arrow_struct(const IColumn& column, const ColumnVariant& var,
const NullMap* null_map, arrow::StructBuilder& builder,
int64_t start, int64_t end,
const cctz::time_zone& ctz) {
const auto struct_type = std::dynamic_pointer_cast<arrow::StructType>(builder.type());
if (struct_type == nullptr || builder.num_fields() != 2 ||
struct_type->field(0)->name() != "value" || struct_type->field(1)->name() != "metadata") {
return Status::InvalidArgument(
"Variant Arrow output requires "
"struct<value: binary, metadata: binary>");
}
auto* value_builder = dynamic_cast<arrow::BinaryBuilder*>(builder.field_builder(0));
auto* metadata_builder = dynamic_cast<arrow::BinaryBuilder*>(builder.field_builder(1));
if (value_builder == nullptr || metadata_builder == nullptr) {
return Status::InvalidArgument(
"Variant Arrow output requires binary value and metadata children");
}

const auto* root = var.get_subcolumn(PathInData());
const bool string_root =
root != nullptr && is_string_type(root->get_least_common_base_type_id());
JsonbToVariantEncoder encoder(
VariantBatchBuilder::ReserveHint {.rows = cast_set<size_t>(end - start)});
DataTypeSerDe::FormatOptions options;
options.timezone = &ctz;
for (int64_t row = start; row < end; ++row) {
if (null_map != nullptr && (*null_map)[cast_set<size_t>(row)]) {
encoder.add_null();
continue;
}

std::string serialized_value;
var.serialize_one_row_to_string(row, &serialized_value, options);
if (string_root && !root->is_null_at(cast_set<size_t>(row))) {
JsonbWriter writer;
if (!writer.writeStartString() ||
(!serialized_value.empty() &&
!writer.writeString(serialized_value.data(), serialized_value.size())) ||
!writer.writeEndString()) {
return Status::InternalError("Failed to encode legacy Variant string as JSONB");
}
encoder.add_jsonb({writer.getOutput()->getBuffer(),
static_cast<size_t>(writer.getOutput()->getSize())});
continue;
}

JsonBinaryValue jsonb;
RETURN_IF_ERROR(jsonb.from_json_string(serialized_value));
encoder.add_jsonb({jsonb.value(), jsonb.size()});
}

VariantBatchBuilder batch = encoder.finish_batch();
for (size_t row = 0; row < batch.num_rows(); ++row) {
const size_t source_row = cast_set<size_t>(start) + row;
if (null_map != nullptr && (*null_map)[source_row]) {
RETURN_IF_ERROR(checkArrowStatus(builder.AppendNull(), column, builder));
continue;
}
const VariantRef value = batch.value_at(row);
RETURN_IF_ERROR(checkArrowStatus(builder.Append(), column, builder));
RETURN_IF_ERROR(checkArrowStatus(
value_builder->Append(value.value.data, cast_set<int32_t>(value.value.size)),
column, *value_builder));
RETURN_IF_ERROR(
checkArrowStatus(metadata_builder->Append(value.metadata.data,
cast_set<int32_t>(value.metadata.size)),
column, *metadata_builder));
}
return Status::OK();
}

} // namespace

Status DataTypeVariantSerDe::write_column_to_mysql_binary(const IColumn& column,
Expand Down Expand Up @@ -161,6 +234,10 @@ Status DataTypeVariantSerDe::write_column_to_arrow(const IColumn& column, const
} else if (array_builder->type()->id() == arrow::Type::STRING) {
auto& builder = assert_cast<arrow::StringBuilder&>(*array_builder);
return write_variant_column_to_arrow_impl(column, *var, null_map, builder, start, end, ctz);
} else if (array_builder->type()->id() == arrow::Type::STRUCT) {
auto& builder = assert_cast<arrow::StructBuilder&>(*array_builder);
RETURN_IF_CATCH_EXCEPTION(return write_variant_column_to_arrow_struct(
column, *var, null_map, builder, start, end, ctz));
} else {
return Status::InvalidArgument("Unsupported arrow type for variant column: {}",
array_builder->type()->name());
Expand Down Expand Up @@ -208,7 +285,8 @@ Status DataTypeVariantSerDe::write_column_to_orc(const std::string& timezone, co
size_t len = serialized_value.length();
if (offset + len > total_size) {
return Status::InternalError(
"Buffer overflow when writing column data to ORC file. offset {} with len {} "
"Buffer overflow when writing column data "
"to ORC file. offset {} with len {} "
"exceed total_size {} . ",
offset, len, total_size);
}
Expand Down
50 changes: 50 additions & 0 deletions be/src/core/data_type_serde/data_type_variant_v2_serde.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "core/data_type_serde/data_type_variant_v2_serde.h"

#include <arrow/array/builder_binary.h>
#include <arrow/array/builder_nested.h>

#include <algorithm>
#include <cstring>
Expand Down Expand Up @@ -491,6 +492,50 @@ Status write_arrow(const IColumn& column, const NullMap* null_map, Builder& buil
return status;
}

Status write_arrow_variant(const IColumn& column, const NullMap* null_map,
arrow::StructBuilder& builder, size_t start, size_t end) {
const auto struct_type = std::dynamic_pointer_cast<arrow::StructType>(builder.type());
if (struct_type == nullptr || builder.num_fields() != 2 ||
struct_type->field(0)->name() != "value" || struct_type->field(1)->name() != "metadata") {
return Status::InvalidArgument(
"Variant Arrow output requires struct<value: binary, metadata: binary>");
}
auto* value_builder = dynamic_cast<arrow::BinaryBuilder*>(builder.field_builder(0));
auto* metadata_builder = dynamic_cast<arrow::BinaryBuilder*>(builder.field_builder(1));
if (value_builder == nullptr || metadata_builder == nullptr) {
return Status::InvalidArgument(
"Variant Arrow output requires binary value and metadata children");
}

Status status = Status::OK();
visit_variant_v2_values(
column, start, end, forced_nulls(null_map),
[&](size_t) {
if (status.ok()) {
status = checkArrowStatus(builder.AppendNull(), column, builder);
}
},
[&](size_t, VariantRef value) {
if (!status.ok()) {
return;
}
status = checkArrowStatus(builder.Append(), column, builder);
if (status.ok()) {
status = checkArrowStatus(
value_builder->Append(value.value.data,
cast_set<int32_t>(value.value.size)),
column, *value_builder);
}
if (status.ok()) {
status = checkArrowStatus(
metadata_builder->Append(value.metadata.data,
cast_set<int32_t>(value.metadata.size)),
column, *metadata_builder);
}
});
return status;
}

} // namespace

void DataTypeVariantV2SerDe::to_string(const IColumn& column, size_t row_num, BufferWritable& bw,
Expand Down Expand Up @@ -553,6 +598,11 @@ Status DataTypeVariantV2SerDe::write_column_to_arrow(const IColumn& column, cons
assert_cast<arrow::LargeStringBuilder&>(*array_builder), first, last,
options);
}
if (array_builder->type()->id() == arrow::Type::STRUCT) {
return write_arrow_variant(column, null_map,
assert_cast<arrow::StructBuilder&>(*array_builder), first,
last);
}
return Status::InvalidArgument("Unsupported arrow type for variant column: {}",
array_builder->type()->name());
});
Expand Down
3 changes: 3 additions & 0 deletions be/src/exec/operator/operator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
#include "exec/operator/olap_scan_operator.h"
#include "exec/operator/olap_table_sink_operator.h"
#include "exec/operator/olap_table_sink_v2_operator.h"
#include "exec/operator/paimon_table_sink_operator.h"
#include "exec/operator/partition_sort_sink_operator.h"
#include "exec/operator/partition_sort_source_operator.h"
#include "exec/operator/partitioned_aggregation_sink_operator.h"
Expand Down Expand Up @@ -846,6 +847,7 @@ DECLARE_OPERATOR(OlapTableSinkV2LocalState)
DECLARE_OPERATOR(HiveTableSinkLocalState)
DECLARE_OPERATOR(TVFTableSinkLocalState)
DECLARE_OPERATOR(IcebergTableSinkLocalState)
DECLARE_OPERATOR(PaimonTableSinkLocalState)
DECLARE_OPERATOR(SpillIcebergTableSinkLocalState)
DECLARE_OPERATOR(IcebergDeleteSinkLocalState)
DECLARE_OPERATOR(IcebergMergeSinkLocalState)
Expand Down Expand Up @@ -971,6 +973,7 @@ template class AsyncWriterSink<doris::VTabletWriter, OlapTableSinkOperatorX>;
template class AsyncWriterSink<doris::VTabletWriterV2, OlapTableSinkV2OperatorX>;
template class AsyncWriterSink<doris::VHiveTableWriter, HiveTableSinkOperatorX>;
template class AsyncWriterSink<doris::VIcebergTableWriter, IcebergTableSinkOperatorX>;
template class AsyncWriterSink<doris::PaimonTableWriter, PaimonTableSinkOperatorX>;
template class AsyncWriterSink<doris::VIcebergTableWriter, SpillIcebergTableSinkOperatorX>;
template class AsyncWriterSink<doris::VIcebergDeleteSink, IcebergDeleteSinkOperatorX>;
template class AsyncWriterSink<doris::VIcebergMergeSink, IcebergMergeSinkOperatorX>;
Expand Down
39 changes: 39 additions & 0 deletions be/src/exec/operator/paimon_table_sink_operator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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 "exec/operator/paimon_table_sink_operator.h"

#include "common/logging.h"

namespace doris {

Status PaimonTableSinkLocalState::init(RuntimeState* state, LocalSinkStateInfo& info) {
return Base::init(state, info);
}

Status PaimonTableSinkOperatorX::sink_impl(RuntimeState* state, Block* in_block, bool eos) {
auto& local_state = get_local_state(state);
SCOPED_TIMER(local_state.exec_time_counter());
COUNTER_UPDATE(local_state.rows_input_counter(), static_cast<int64_t>(in_block->rows()));

// Delegate to AsyncWriterSink → PaimonTableWriter for this pipeline instance.
// Each pipeline instance has its own writer session; partition and bucket
// routing is handled internally by the Paimon SDK inside IPaimonWriter::write().
return local_state.sink(state, in_block, eos);
}

} // namespace doris
Loading