github-actions[bot] commented on code in PR #65868:
URL: https://github.com/apache/doris/pull/65868#discussion_r3668682153
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonUtil.java:
##########
@@ -150,71 +145,125 @@ public static List<InternalRow> read(
return rows;
}
- public static PaimonPartitionInfo generatePartitionInfo(List<Column>
partitionColumns,
- List<Partition> paimonPartitions, boolean legacyPartitionName) {
+ public static PaimonPartitionInfo generatePartitionInfo(Table table,
List<Column> partitionColumns,
+ List<PartitionEntry> partitionEntries) {
- if (CollectionUtils.isEmpty(partitionColumns) ||
paimonPartitions.isEmpty()) {
+ if (CollectionUtils.isEmpty(partitionColumns) ||
partitionEntries.isEmpty()) {
return PaimonPartitionInfo.EMPTY;
}
- Map<String, PartitionItem> nameToPartitionItem = Maps.newHashMap();
- Map<String, Partition> nameToPartition = Maps.newHashMap();
- PaimonPartitionInfo partitionInfo = new
PaimonPartitionInfo(nameToPartitionItem, nameToPartition);
+ CoreOptions options = new CoreOptions(table.options());
+ RowType partitionType = table.rowType().project(table.partitionKeys());
+ if (partitionType.getFields().stream().anyMatch(field ->
+ field.type().getTypeRoot() ==
DataTypeRoot.TIMESTAMP_WITH_LOCAL_TIME_ZONE)) {
+ // This metadata is cached by table snapshot, but LTZ values are
represented as
+ // session-local civil times in Doris. Caching those bounds would
let one session
+ // reuse pruning metadata produced in another time zone. Keep scan
correctness by
+ // delegating pruning to Paimon until this cache can carry a
time-zone-independent
+ // typed representation.
+ return PaimonPartitionInfo.UNPRUNABLE;
Review Comment:
[P1] Preserve dictionary freshness for unprunable LTZ partitions
This singleton also clears `nameToPartition`, but Paimon dictionary loading
records the positive snapshot ID from `getTableSnapshot()`. On the next
freshness check, `getNewestUpdateVersionOrTime()` reads this empty map and
returns 0, so `Dictionary.hasNewerSourceVersion()` throws because 0 is smaller
than the saved version. Please keep non-pruning freshness metadata here, or
make both dictionary paths use the snapshot ID consistently; otherwise every
dictionary backed by an LTZ-partitioned Paimon table breaks after its first
successful load.
##########
fe/be-java-extensions/paimon-connector/src/main/java/org/apache/doris/paimon/GlobalIndexAssigner.java:
##########
@@ -0,0 +1,167 @@
+// 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.
+
+package org.apache.doris.paimon;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.crosspartition.BucketAssigner;
+import org.apache.paimon.crosspartition.ExistingProcessor;
+import org.apache.paimon.crosspartition.IndexBootstrap;
+import org.apache.paimon.crosspartition.KeyPartPartitionKeyExtractor;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.sink.PartitionKeyExtractor;
+import org.apache.paimon.table.sink.RowPartitionAllPrimaryKeyExtractor;
+import org.apache.paimon.utils.IDMapping;
+import org.apache.paimon.utils.PositiveIntInt;
+import org.apache.paimon.utils.ProjectToRowFunction;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+/**
+ * Assigns buckets for a key-dynamic table with a process-local global-key
index.
+ *
+ * <p>Doris gathers key-dynamic writes into one writer, so a single in-memory
index can preserve
+ * Paimon's cross-partition merge semantics without a native state backend.
+ */
+final class GlobalIndexAssigner implements AutoCloseable {
+ private final FileStoreTable table;
+ // TODO: After resolving rocksdbjni allocator compatibility with the Doris
BE jemalloc hook,
+ // use a RocksDB-backed on-disk index to bound Java heap usage for large
tables.
+ private final Map<BinaryRow, PositiveIntInt> keyIndex = new HashMap<>();
+
+ private int bucketIndex;
+ private int targetBucketRowNumber;
+ private int assignId;
+ private int numAssigners;
+ private boolean bootstrapping;
+ private BiConsumer<InternalRow, Integer> collector;
+ private PartitionKeyExtractor<InternalRow> extractor;
+ private PartitionKeyExtractor<InternalRow> bootstrapExtractor;
+ private IDMapping<BinaryRow> partitionMapping;
+ private BucketAssigner bucketAssigner;
+ private ExistingProcessor existingProcessor;
+
+ GlobalIndexAssigner(FileStoreTable table) {
+ this.table = table;
+ }
+
+ void open(
+ int numAssigners,
+ int assignId,
+ BiConsumer<InternalRow, Integer> collector) {
+ this.numAssigners = numAssigners;
+ this.assignId = assignId;
+ this.collector = collector;
+
+ CoreOptions coreOptions = table.coreOptions();
+ this.bucketIndex =
IndexBootstrap.bootstrapType(table.schema()).getFieldCount() - 1;
+ this.targetBucketRowNumber = (int)
coreOptions.dynamicBucketTargetRowNum();
Review Comment:
[P2] Validate the long dynamic-bucket target before narrowing it
Paimon exposes `dynamic-bucket.target-row-num` as a `long` with no
`Integer.MAX_VALUE` ceiling, so a valid value such as 2147483648 wraps to a
negative number here. `BucketAssigner` only reuses a bucket when `count <
maxCount`; with the wrapped value that is never true, and every new key creates
another bucket. Please preserve the long threshold or reject/saturate
out-of-range values before opening this writer, and cover the int boundary.
##########
fe/be-java-extensions/paimon-connector/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java:
##########
@@ -0,0 +1,656 @@
+// 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.
+
+package org.apache.doris.paimon;
+
+import org.apache.doris.common.classloader.ThreadClassLoaderContext;
+import
org.apache.doris.common.security.authentication.PreExecutionAuthenticator;
+import
org.apache.doris.common.security.authentication.PreExecutionAuthenticatorCache;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.ipc.ArrowStreamReader;
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.crosspartition.IndexBootstrap;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.disk.IOManagerImpl;
+import org.apache.paimon.index.BucketAssigner;
+import org.apache.paimon.index.HashBucketAssigner;
+import org.apache.paimon.index.SimpleHashBucketAssigner;
+import org.apache.paimon.memory.MemoryPoolFactory;
+import org.apache.paimon.table.BucketMode;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.sink.CommitMessage;
+import org.apache.paimon.table.sink.InnerTableCommit;
+import org.apache.paimon.table.sink.PartitionKeyExtractor;
+import org.apache.paimon.table.sink.RowPartitionKeyExtractor;
+import org.apache.paimon.table.sink.SinkRecord;
+import org.apache.paimon.table.sink.TableWriteImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * JNI entry point for Paimon write operations.
+ *
+ * <p>Called from C++ ({@code JniPaimonWriter}) via JNI. One instance per BE
pipeline
+ * fragment (one per {@code PaimonTableWriter}). Data path:
+ *
+ * <pre>
+ * C++ Block → Arrow IPC Stream → JNI direct ByteBuffer
+ * → PaimonJniWriter.write(directBuffer)
+ * → ArrowStreamReader → VectorSchemaRoot
+ * → PaimonArrowConverter (row-at-a-time typed extraction)
+ * → PaimonWriteSchema.tableRow() (canonical table-schema order)
+ * → Paimon SDK bucket assignment and table write
+ * </pre>
+ *
+ * <p>Commit path:
+ *
+ * <pre>
+ * PaimonTableWriter::close() → JNI → PaimonJniWriter.prepareCommit()
+ * → TableWriteImpl.prepareCommit()
+ * → PaimonCommitCodec.encode() → DPCM-framed byte[][]
+ * → C++ collects TPaimonCommitMessage[] → RPC to FE → PaimonTransaction
+ * </pre>
+ */
+public class PaimonJniWriter {
+ private static final Logger LOG =
LoggerFactory.getLogger(PaimonJniWriter.class);
+
+ private final ClassLoader classLoader;
+ private final PaimonCommitCodec commitCodec = new PaimonCommitCodec();
+
+ private BufferAllocator allocator;
+ private PreExecutionAuthenticator preExecutionAuthenticator;
+ private PaimonArrowConverter arrowConverter;
+
+ private PaimonWriteSchema writeSchema;
+ private FileStoreTable table;
+ private TableWriteImpl<?> writer;
+ private IOManager ioManager;
+ private long commitIdentifier;
+ private String commitUser;
+ private BucketMode bucketMode;
+ private BucketAssigner hashBucketAssigner;
+ private PartitionKeyExtractor<InternalRow> dynamicBucketExtractor;
+ private GlobalIndexAssigner globalIndexAssigner;
+ private boolean fullCompactionChangelog;
+ private final Set<PartitionBucket> fullCompactionBuckets = new HashSet<>();
+ private List<CommitMessage> preparedCommitMessages =
Collections.emptyList();
+ private boolean sdkCloseFailed;
+
+ public PaimonJniWriter() {
+ this.allocator = new RootAllocator(Long.MAX_VALUE);
Review Comment:
[P1] Include Arrow decoding in the writer memory budget
`ArrowStreamReader` allocates its loaded vectors from this allocator, so
`Long.MAX_VALUE` gives every sink writer an effectively unbounded off-heap
decoder outside the Doris query tracker. The new native limit only covers
`DorisMemorySegmentPool`, and the C++ projected block plus IPC buffer are still
live during this decode. A wide block or several parallel writers can therefore
exhaust BE direct memory without tripping the advertised writer/query cap.
Please use a bounded allocator charged to the same per-writer budget, and fail
the query before process-level exhaustion.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalPaimonTableSink.java:
##########
@@ -0,0 +1,173 @@
+// 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.
+
+package org.apache.doris.nereids.trees.plans.physical;
+
+import org.apache.doris.catalog.Column;
+import org.apache.doris.datasource.paimon.PaimonExternalDatabase;
+import org.apache.doris.datasource.paimon.PaimonExternalTable;
+import org.apache.doris.nereids.memo.GroupExpression;
+import org.apache.doris.nereids.properties.DistributionSpecHash.ShuffleType;
+import org.apache.doris.nereids.properties.LogicalProperties;
+import org.apache.doris.nereids.properties.PhysicalProperties;
+import org.apache.doris.nereids.trees.expressions.ExprId;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.statistics.Statistics;
+
+import com.google.common.base.Preconditions;
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.table.BucketMode;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.Table;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.TreeMap;
+
+/**
+ * Physical Paimon table sink.
+ */
+public class PhysicalPaimonTableSink<CHILD_TYPE extends Plan>
+ extends PhysicalBaseExternalTableSink<CHILD_TYPE> {
+
+ public PhysicalPaimonTableSink(PaimonExternalDatabase database,
+ PaimonExternalTable targetTable,
+ List<Column> cols,
+ List<NamedExpression> outputExprs,
+ Optional<GroupExpression> groupExpression,
+ LogicalProperties logicalProperties,
+ CHILD_TYPE child) {
+ this(database, targetTable, cols, outputExprs, groupExpression,
logicalProperties,
+ PhysicalProperties.SINK_RANDOM_PARTITIONED, null, child);
+ }
+
+ public PhysicalPaimonTableSink(PaimonExternalDatabase database,
+ PaimonExternalTable targetTable,
+ List<Column> cols,
+ List<NamedExpression> outputExprs,
+ Optional<GroupExpression> groupExpression,
+ LogicalProperties logicalProperties,
+ PhysicalProperties physicalProperties,
+ Statistics statistics,
+ CHILD_TYPE child) {
+ super(PlanType.PHYSICAL_PAIMON_TABLE_SINK, database, targetTable,
cols, outputExprs,
+ groupExpression, logicalProperties, physicalProperties,
statistics, child);
+ }
+
+ @Override
+ public Plan withChildren(List<Plan> children) {
+ return new PhysicalPaimonTableSink<>(
+ (PaimonExternalDatabase) database, (PaimonExternalTable)
targetTable,
+ cols, outputExprs, groupExpression,
+ getLogicalProperties(), physicalProperties, statistics,
children.get(0));
+ }
+
+ @Override
+ public Plan withGroupExpression(Optional<GroupExpression> groupExpression)
{
+ return new PhysicalPaimonTableSink<>(
+ (PaimonExternalDatabase) database, (PaimonExternalTable)
targetTable,
+ cols, outputExprs, groupExpression,
+ getLogicalProperties(), physicalProperties, statistics,
child());
+ }
+
+ @Override
+ public Plan withGroupExprLogicalPropChildren(Optional<GroupExpression>
groupExpression,
+ Optional<LogicalProperties> logicalProperties, List<Plan>
children) {
+ return new PhysicalPaimonTableSink<>(
+ (PaimonExternalDatabase) database, (PaimonExternalTable)
targetTable,
+ cols, outputExprs, groupExpression,
+ logicalProperties.get(), physicalProperties, statistics,
children.get(0));
+ }
+
+ @Override
+ public PhysicalPaimonTableSink<Plan> withPhysicalPropertiesAndStats(
+ PhysicalProperties physicalProperties, Statistics stats) {
+ return new PhysicalPaimonTableSink<>(
+ (PaimonExternalDatabase) database, (PaimonExternalTable)
targetTable,
+ cols, outputExprs, groupExpression,
+ getLogicalProperties(), physicalProperties, stats, child());
+ }
+
+ @Override
+ public PhysicalProperties getRequirePhysicalProperties() {
+ if (requiresSingleWriter()) {
+ return PhysicalProperties.GATHER;
+ }
+
+ PaimonExternalTable paimonExternalTable = (PaimonExternalTable)
targetTable;
+ Table paimonTable =
paimonExternalTable.getPaimonTable(Optional.empty());
+ List<String> primaryKeys = paimonTable.primaryKeys();
+ if (primaryKeys.isEmpty()) {
+ return PhysicalProperties.SINK_RANDOM_PARTITIONED;
+ }
+
+ List<Slot> outputSlots = child().getOutput();
+ Preconditions.checkState(cols.size() == outputSlots.size(),
+ "Paimon sink columns must match child output");
+ Map<String, ExprId> columnExprIds = new
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ for (int i = 0; i < cols.size(); i++) {
+ columnExprIds.put(cols.get(i).getName(),
outputSlots.get(i).getExprId());
+ }
+
+ List<ExprId> primaryKeyExprIds = new ArrayList<>(primaryKeys.size());
+ for (String primaryKey : primaryKeys) {
+ primaryKeyExprIds.add(Preconditions.checkNotNull(
+ columnExprIds.get(primaryKey),
+ "Paimon primary-key column is missing from sink output"));
+ }
+ return PhysicalProperties.createHash(primaryKeyExprIds,
ShuffleType.REQUIRE);
+ }
+
+ /**
+ * Whether this sink must use one writer to preserve Paimon write
semantics.
+ */
+ public boolean requiresSingleWriter() {
+ Table paimonTable = ((PaimonExternalTable) targetTable)
+ .getPaimonTable(Optional.empty());
+ Preconditions.checkState(paimonTable instanceof FileStoreTable,
+ "Paimon write requires a file store table");
+ FileStoreTable fileStoreTable = (FileStoreTable) paimonTable;
+ BucketMode bucketMode = fileStoreTable.bucketMode();
+ if (bucketMode == BucketMode.HASH_DYNAMIC
+ || bucketMode == BucketMode.KEY_DYNAMIC
+ // Until Doris has a Paimon bucket-aware exchange, a
fixed-bucket
+ // primary-key table must not let independent writers own the
same
+ // partition/bucket. Hashing the complete primary key is
insufficient
+ // when bucket-key is a subset and can also split compaction
ownership.
+ || (bucketMode == BucketMode.HASH_FIXED
+ && !paimonTable.primaryKeys().isEmpty())) {
+ return true;
+ }
+
+ CoreOptions coreOptions = CoreOptions.fromMap(paimonTable.options());
Review Comment:
[P1] Keep one writer per fixed append bucket during compaction
An ordinary `HASH_FIXED` append table still reaches random sink distribution
here, even though each local Paimon writer restores the existing files for
every bucket it touches and may compact them. Once a bucket has enough old
files, two local writers can select the same compaction inputs; FE then
combines both replacement sets, and Paimon's manifest conflict check rejects
the duplicate deletes, failing an otherwise valid INSERT. Please gather these
writes while auto-compaction is enabled, or exchange by the exact Paimon
partition and bucket so each bucket has one owner.
##########
fe/be-java-extensions/paimon-connector/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java:
##########
@@ -0,0 +1,656 @@
+// 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.
+
+package org.apache.doris.paimon;
+
+import org.apache.doris.common.classloader.ThreadClassLoaderContext;
+import
org.apache.doris.common.security.authentication.PreExecutionAuthenticator;
+import
org.apache.doris.common.security.authentication.PreExecutionAuthenticatorCache;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.ipc.ArrowStreamReader;
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.crosspartition.IndexBootstrap;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.disk.IOManagerImpl;
+import org.apache.paimon.index.BucketAssigner;
+import org.apache.paimon.index.HashBucketAssigner;
+import org.apache.paimon.index.SimpleHashBucketAssigner;
+import org.apache.paimon.memory.MemoryPoolFactory;
+import org.apache.paimon.table.BucketMode;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.sink.CommitMessage;
+import org.apache.paimon.table.sink.InnerTableCommit;
+import org.apache.paimon.table.sink.PartitionKeyExtractor;
+import org.apache.paimon.table.sink.RowPartitionKeyExtractor;
+import org.apache.paimon.table.sink.SinkRecord;
+import org.apache.paimon.table.sink.TableWriteImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * JNI entry point for Paimon write operations.
+ *
+ * <p>Called from C++ ({@code JniPaimonWriter}) via JNI. One instance per BE
pipeline
+ * fragment (one per {@code PaimonTableWriter}). Data path:
+ *
+ * <pre>
+ * C++ Block → Arrow IPC Stream → JNI direct ByteBuffer
+ * → PaimonJniWriter.write(directBuffer)
+ * → ArrowStreamReader → VectorSchemaRoot
+ * → PaimonArrowConverter (row-at-a-time typed extraction)
+ * → PaimonWriteSchema.tableRow() (canonical table-schema order)
+ * → Paimon SDK bucket assignment and table write
+ * </pre>
+ *
+ * <p>Commit path:
+ *
+ * <pre>
+ * PaimonTableWriter::close() → JNI → PaimonJniWriter.prepareCommit()
+ * → TableWriteImpl.prepareCommit()
+ * → PaimonCommitCodec.encode() → DPCM-framed byte[][]
+ * → C++ collects TPaimonCommitMessage[] → RPC to FE → PaimonTransaction
+ * </pre>
+ */
+public class PaimonJniWriter {
+ private static final Logger LOG =
LoggerFactory.getLogger(PaimonJniWriter.class);
+
+ private final ClassLoader classLoader;
+ private final PaimonCommitCodec commitCodec = new PaimonCommitCodec();
+
+ private BufferAllocator allocator;
+ private PreExecutionAuthenticator preExecutionAuthenticator;
+ private PaimonArrowConverter arrowConverter;
+
+ private PaimonWriteSchema writeSchema;
+ private FileStoreTable table;
+ private TableWriteImpl<?> writer;
+ private IOManager ioManager;
+ private long commitIdentifier;
+ private String commitUser;
+ private BucketMode bucketMode;
+ private BucketAssigner hashBucketAssigner;
+ private PartitionKeyExtractor<InternalRow> dynamicBucketExtractor;
+ private GlobalIndexAssigner globalIndexAssigner;
+ private boolean fullCompactionChangelog;
+ private final Set<PartitionBucket> fullCompactionBuckets = new HashSet<>();
+ private List<CommitMessage> preparedCommitMessages =
Collections.emptyList();
+ private boolean sdkCloseFailed;
+
+ public PaimonJniWriter() {
+ this.allocator = new RootAllocator(Long.MAX_VALUE);
+ this.classLoader = this.getClass().getClassLoader();
+ }
+
+ // ────────────────────────────────────────────────────────────
+ // JNI entry points (called from C++)
+ // ────────────────────────────────────────────────────────────
+
+ /**
+ * Initialize the writer. Called once per BE pipeline fragment via JNI.
+ *
+ * <p>This method:
+ * <ol>
+ * <li>Deserializes the target Paimon {@link FileStoreTable} selected by
FE.</li>
+ * <li>Creates a {@link PaimonWriteSchema} which normalizes Doris input
+ * columns to the table-schema row layout.</li>
+ * <li>Opens one Paimon SDK writer session.</li>
+ * </ol>
+ *
+ * @param serializedTable serialized Paimon table selected by FE
+ * @param hadoopConfig filesystem and authentication configuration
+ * @param columnNames output column names in the order produced by BE
+ * @param transactionId Doris external transaction identifier
+ * @param commitUser Paimon commit user shared with the FE committer
+ * @param overwrite whether this is an overwrite write
+ * @param timeZone normalized Doris session timezone used for Paimon
LTZ values
+ * @param spillDirectories Doris storage-root scoped directories for
Paimon write-buffer spill
+ * @param memoryPoolLimitBytes maximum Doris-managed Paimon write-buffer
memory
+ * @param nativeMemoryManager opaque BE manager used to allocate tracked
native pages
+ */
+ public void open(String serializedTable, Map<String, String> hadoopConfig,
+ String[] columnNames, long transactionId, String
commitUser,
+ boolean overwrite, String timeZone, String
spillDirectories,
+ long memoryPoolLimitBytes, long nativeMemoryManager)
throws Exception {
+ try (ThreadClassLoaderContext ignored = new
ThreadClassLoaderContext(classLoader)) {
+ if (memoryPoolLimitBytes <= 0) {
+ throw new IllegalArgumentException(
+ "PaimonJniWriter requires a positive memory pool
limit");
+ }
+ if (nativeMemoryManager == 0) {
+ throw new IllegalArgumentException(
+ "PaimonJniWriter requires a native memory manager");
+ }
+ this.preExecutionAuthenticator =
PreExecutionAuthenticatorCache.getAuthenticator(hadoopConfig);
+ this.arrowConverter = new
PaimonArrowConverter(ZoneId.of(timeZone));
+ preExecutionAuthenticator.execute(() -> {
+ try {
+ FileStoreTable table =
PaimonUtils.deserialize(serializedTable);
+ LOG.info("PaimonJniWriter opening: table={}, columns={}",
+ table.fullName(), columnNames != null ?
columnNames.length : 0);
+ this.commitIdentifier = transactionId;
+ this.table = table;
+ this.commitUser = commitUser;
+ this.bucketMode = table.bucketMode();
+
+ CoreOptions coreOptions =
CoreOptions.fromMap(table.options());
+ this.writeSchema =
PaimonWriteSchema.create(table.rowType(), columnNames);
+ validateWriteColumnsForMergeEngine(columnNames.length,
coreOptions);
+ this.fullCompactionChangelog =
+ !coreOptions.writeOnly()
+ && coreOptions.changelogProducer()
+ ==
CoreOptions.ChangelogProducer.FULL_COMPACTION;
+ openFileStoreWriter(
+ table,
+ commitUser,
+ overwrite,
+ spillDirectories,
+ coreOptions,
+ memoryPoolLimitBytes,
+ nativeMemoryManager);
+ return null;
+ } catch (Throwable t) {
+ try {
+ closeResources();
+ } catch (Throwable closeFailure) {
+ t.addSuppressed(closeFailure);
+ }
+ throw new RuntimeException("PaimonJniWriter open failed",
t);
+ }
+ });
+ }
+ }
+
+ /**
+ * Write a batch of rows from an Arrow IPC Stream buffer.
+ *
+ * <p>Called from C++ {@code JniPaimonWriter::_write_projected_block()}
+ * once per Block. The buffer is a zero-copy direct view of the native
+ * Arrow IPC Stream bytes. Rows are deserialized, normalized to
table-schema
+ * order, and handed to Paimon's writer and bucket assigner APIs. The SDK
+ * owns partition/bucket semantics, buffering, spill, and file rolling.
+ *
+ * @param directBuffer direct view of the native Arrow IPC Stream bytes
(no copy)
+ */
+ public void write(ByteBuffer directBuffer) throws Exception {
+ try (ThreadClassLoaderContext ignored = new
ThreadClassLoaderContext(classLoader)) {
+ preExecutionAuthenticator.execute(() -> {
+ try {
+ try (ArrowStreamReader reader = new ArrowStreamReader(
+ new DirectBufInputStream(directBuffer),
allocator)) {
+ VectorSchemaRoot root = reader.getVectorSchemaRoot();
+ while (reader.loadNextBatch()) {
+ writeBatch(root);
+ }
+ }
+ return null;
+ } catch (Throwable t) {
+ throw new RuntimeException(
+ "PaimonJniWriter write failed: bytes=" +
directBuffer.capacity(), t);
+ }
+ });
+ }
+ }
+
+ /**
+ * Prepare commit: flush all in-memory data, close files, and serialize
commit
+ * messages for the FE coordinator.
+ *
+ * <p>Flushes and collects Paimon {@link CommitMessage}s, then encodes
them via
+ * {@link PaimonCommitCodec} into DPCM-framed byte chunks that are
forwarded to
+ * FE through the BE.
+ *
+ * @return byte[][] each element is a DPCM-framed serialized
CommitMessage chunk
+ */
+ public byte[][] prepareCommit() throws Exception {
+ try (ThreadClassLoaderContext ignored = new
ThreadClassLoaderContext(classLoader)) {
+ return preExecutionAuthenticator.execute(() -> {
+ try {
+ List<CommitMessage> messages = prepareCommitMessages();
+ if (messages.isEmpty()) {
+ LOG.info("PaimonJniWriter prepareCommit: empty");
+ return new byte[0][];
+ }
+ LOG.info("PaimonJniWriter prepareCommit: {} messages",
messages.size());
+ return commitCodec.encode(messages);
+ } catch (Throwable t) {
+ throw new RuntimeException("PaimonJniWriter prepareCommit
failed", t);
+ }
+ });
+ }
+ }
+
+ /**
+ * Abort: discard all written data files and close the SDK writer.
+ * Called from C++ when write or prepareCommit fails.
+ */
+ public void abort() throws Exception {
+ try (ThreadClassLoaderContext ignored = new
ThreadClassLoaderContext(classLoader)) {
+ try {
+ if (preExecutionAuthenticator != null) {
+ preExecutionAuthenticator.execute(() -> {
+ abortWriter();
+ return null;
+ });
+ } else {
+ abortWriter();
+ }
+ } catch (Exception e) {
+ LOG.error("PaimonJniWriter abort failed", e);
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * Close: release all resources.
+ */
+ public void close() throws Exception {
+ try (ThreadClassLoaderContext ignored = new
ThreadClassLoaderContext(classLoader)) {
+ try {
+ if (preExecutionAuthenticator != null) {
+ preExecutionAuthenticator.execute(() -> {
+ closeResources();
+ return null;
+ });
+ } else {
+ closeResources();
+ }
+ } catch (Exception e) {
+ LOG.warn("PaimonJniWriter close error", e);
+ throw e;
+ }
+ }
+ }
+
+ // ────────────────────────────────────────────────────────────
+ // Initialization helpers
+ // ────────────────────────────────────────────────────────────
+
+ private void openFileStoreWriter(FileStoreTable table, String commitUser,
boolean overwrite,
+ String spillDirectories, CoreOptions coreOptions, long
memoryPoolLimitBytes,
+ long nativeMemoryManager) throws Exception {
+ writer = table.newWrite(commitUser);
+ if (overwrite) {
+ writer.withIgnorePreviousFiles(true);
+ }
+ openMemoryResources(
+ coreOptions, spillDirectories, memoryPoolLimitBytes,
nativeMemoryManager);
+ openDynamicBucketAssigner(table, commitUser, overwrite, coreOptions);
+ }
+
+ private void validateWriteColumnsForMergeEngine(int writeColumnCount,
CoreOptions coreOptions) {
+ if (writeColumnCount == table.rowType().getFieldCount() ||
table.primaryKeys().isEmpty()) {
+ return;
+ }
+
+ CoreOptions.MergeEngine mergeEngine = coreOptions.mergeEngine();
+ if (mergeEngine != CoreOptions.MergeEngine.PARTIAL_UPDATE) {
+ throw new UnsupportedOperationException(
+ "Paimon primary-key partial-column write requires "
+ + "merge-engine=partial-update, but table uses
merge-engine="
+ + mergeEngine);
+ }
+ }
+
+ private void openMemoryResources(
+ CoreOptions coreOptions,
+ String spillDirectories,
+ long memoryPoolLimitBytes,
+ long nativeMemoryManager) throws Exception {
+ int pageSize = coreOptions.pageSize();
+ long effectivePoolLimit = Math.min(coreOptions.writeBufferSize(),
memoryPoolLimitBytes);
+ DorisMemorySegmentPool memorySegmentPool =
+ new DorisMemorySegmentPool(effectivePoolLimit, pageSize,
nativeMemoryManager);
+ MemoryPoolFactory memoryPoolFactory = new
MemoryPoolFactory(memorySegmentPool);
+ writer.withMemoryPoolFactory(memoryPoolFactory);
+ LOG.info("Paimon writer uses Doris-managed memory pool: limit={}
bytes, pageSize={}",
+ memoryPoolFactory.totalBufferSize(), pageSize);
+
+ if (!coreOptions.writeBufferSpillable()) {
+ return;
+ }
+
+ String[] splitDirectories = IOManagerImpl.splitPaths(spillDirectories);
+ for (String directory : splitDirectories) {
+ Files.createDirectories(Paths.get(directory));
+ }
+ ioManager = IOManager.create(splitDirectories);
+ writer.withIOManager(ioManager);
+ LOG.info("Paimon writer spill enabled: dirs={}", spillDirectories);
+ }
+
+ private void openDynamicBucketAssigner(FileStoreTable table, String
commitUser,
Review Comment:
[P1] Prevent overlapping dynamic-bucket write transactions
Gather only reduces one statement to one local writer. Two concurrent Doris
INSERTs still open independent hash assigners or bootstrap independent global
indexes for the same table/partition, with no table-scoped guard through
commit. Paimon explicitly supports only one dynamic-bucket write job per
partition because overlapping jobs can create duplicate data. Please serialize
or reject overlapping HASH_DYNAMIC/KEY_DYNAMIC transactions through
commit/rollback, and add a two-session concurrent INSERT test.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonWriteBinding.java:
##########
@@ -0,0 +1,243 @@
+// 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.
+
+package org.apache.doris.datasource.paimon;
+
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.UserException;
+import org.apache.doris.common.util.TimeUtils;
+import org.apache.doris.datasource.mvcc.MvccUtil;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import
org.apache.doris.nereids.trees.plans.commands.insert.PaimonInsertCommandContext;
+import org.apache.doris.nereids.types.DataType;
+
+import com.google.common.base.Preconditions;
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.options.CatalogOptions;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataTypeRoot;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Transaction-scoped binding between one Doris insert and one concrete Paimon
table handle.
+ *
+ * <p>The table and writer configuration are captured exactly once while the
sink is finalized.
+ * BE preparation, FE commit, abort and outcome reconciliation therefore all
refer to the same
+ * Paimon table handle.
+ */
+public class PaimonWriteBinding {
+ private final PaimonExternalTable dorisTable;
+ private final FileStoreTable table;
+ private final String serializedTable;
+ private final Map<String, String> hadoopConfig;
+ private final boolean overwrite;
+ private final Map<String, String> staticPartition;
+
+ private PaimonWriteBinding(PaimonExternalTable dorisTable, FileStoreTable
table,
+ Map<String, String> hadoopConfig, boolean overwrite,
+ Map<String, String> staticPartition) {
+ this.dorisTable = dorisTable;
+ this.table = table;
+ this.serializedTable = PaimonUtil.encodeObjectToString(table);
+ this.hadoopConfig = Collections.unmodifiableMap(new
HashMap<>(hadoopConfig));
+ this.overwrite = overwrite;
+ this.staticPartition = Collections.unmodifiableMap(new
LinkedHashMap<>(staticPartition));
+ }
+
+ public static PaimonWriteBinding create(PaimonExternalTable dorisTable,
+ PaimonInsertCommandContext context) throws UserException {
+ PaimonExternalCatalog catalog = (PaimonExternalCatalog)
dorisTable.getCatalog();
+ FileStoreTable table;
+ try {
+ table = catalog.getExecutionAuthenticator().execute(() ->
requireFileStoreTable(
+
dorisTable.getPaimonTable(MvccUtil.getSnapshotFromContext(dorisTable))));
Review Comment:
[P1] Do not bind the write target to a source time-travel snapshot
`StatementContext` stores MVCC snapshots by table identity, so in a
self-insert such as `INSERT INTO t SELECT ... FROM t FOR VERSION AS OF ...`,
this lookup returns the source's historical Paimon table and serializes it as
the target writer. Target columns and physical planning were derived from the
latest table; after schema evolution the BE therefore opens an old row type and
can reject valid target columns (or apply stale write-mode assumptions). Please
pin a separate latest write-target handle instead of reusing the source read
snapshot, and add a schema-evolved time-travel self-insert test.
##########
fe/be-java-extensions/paimon-connector/src/main/java/org/apache/doris/paimon/PaimonArrowConverter.java:
##########
@@ -0,0 +1,348 @@
+// 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.
+
+package org.apache.doris.paimon;
+
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.BitVector;
+import org.apache.arrow.vector.DateDayVector;
+import org.apache.arrow.vector.DecimalVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.Float4Vector;
+import org.apache.arrow.vector.Float8Vector;
+import org.apache.arrow.vector.IntVector;
+import org.apache.arrow.vector.SmallIntVector;
+import org.apache.arrow.vector.TimeStampVector;
+import org.apache.arrow.vector.TinyIntVector;
+import org.apache.arrow.vector.VarBinaryVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.complex.ListVector;
+import org.apache.arrow.vector.complex.MapVector;
+import org.apache.arrow.vector.complex.StructVector;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.Decimal;
+import org.apache.paimon.data.GenericArray;
+import org.apache.paimon.data.GenericMap;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.types.ArrayType;
+import org.apache.paimon.types.BinaryType;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.LocalZonedTimestampType;
+import org.apache.paimon.types.MapType;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.types.TimestampType;
+import org.apache.paimon.types.VarBinaryType;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Converts Arrow columns into Paimon internal values without owning writer
state. */
+final class PaimonArrowConverter {
+
+ private final ZoneId sessionTimeZone;
+
+ PaimonArrowConverter(ZoneId sessionTimeZone) {
+ this.sessionTimeZone = sessionTimeZone;
+ }
+
+ RowReader rows(VectorSchemaRoot root, DataType[] targetTypes) {
+ List<Field> fields = root.getSchema().getFields();
+ List<FieldVector> vectors = root.getFieldVectors();
+ if (fields.size() != targetTypes.length) {
+ throw new IllegalArgumentException("Arrow column count does not
match Paimon write type");
+ }
+ return new RowReader(fields, vectors, targetTypes);
+ }
+
+ /** Bound view of one Arrow batch which converts only the requested row. */
+ final class RowReader {
+ private final List<Field> fields;
+ private final List<FieldVector> vectors;
+ private final DataType[] targetTypes;
+
+ private RowReader(
+ List<Field> fields, List<FieldVector> vectors, DataType[]
targetTypes) {
+ this.fields = fields;
+ this.vectors = vectors;
+ this.targetTypes = targetTypes;
+ }
+
+ Object[] values(int rowIndex) {
+ Object[] values = new Object[vectors.size()];
+ for (int column = 0; column < vectors.size(); column++) {
+ values[column] = convertVectorValue(
+ vectors.get(column), rowIndex, fields.get(column),
targetTypes[column]);
+ }
+ return values;
+ }
+ }
+
+ private Object convertVectorValue(
+ FieldVector vector, int index, Field arrowField, DataType
targetType) {
+ if (vector.isNull(index)) {
+ return null;
+ }
+ if (vector instanceof StructVector && targetType instanceof RowType) {
+ return convertStructVector((StructVector) vector, index, (RowType)
targetType);
+ }
+ if (vector instanceof MapVector && targetType instanceof MapType) {
+ return convertMapVector((MapVector) vector, index, (MapType)
targetType);
+ }
+ if (vector instanceof ListVector && targetType instanceof ArrayType) {
+ return convertArrayVector((ListVector) vector, index, (ArrayType)
targetType);
+ }
+ if (vector instanceof IntVector) {
+ return ((IntVector) vector).get(index);
+ }
+ if (vector instanceof BigIntVector) {
+ return ((BigIntVector) vector).get(index);
+ }
+ if (vector instanceof SmallIntVector) {
+ return ((SmallIntVector) vector).get(index);
+ }
+ if (vector instanceof TinyIntVector) {
+ return ((TinyIntVector) vector).get(index);
+ }
+ if (vector instanceof Float4Vector) {
+ return ((Float4Vector) vector).get(index);
+ }
+ if (vector instanceof Float8Vector) {
+ return ((Float8Vector) vector).get(index);
+ }
+ if (vector instanceof BitVector) {
+ return ((BitVector) vector).get(index) == 1;
+ }
+ if (vector instanceof DateDayVector) {
+ return ((DateDayVector) vector).get(index);
+ }
+ if (vector instanceof VarCharVector) {
+ byte[] value = ((VarCharVector) vector).get(index);
+ return targetType instanceof BinaryType || targetType instanceof
VarBinaryType
+ ? value : BinaryString.fromBytes(value);
+ }
+ if (vector instanceof VarBinaryVector) {
+ return ((VarBinaryVector) vector).get(index);
+ }
+ if (vector instanceof TimeStampVector) {
+ ArrowType.Timestamp timestampType = (ArrowType.Timestamp)
arrowField.getType();
+ return toPaimonTimestamp(
+ arrowTimestampToMicros(((TimeStampVector)
vector).get(index), timestampType),
+ timestampType, targetType);
+ }
+ if (vector instanceof DecimalVector) {
+ DecimalVector decimalVector = (DecimalVector) vector;
+ int precision = decimalVector.getPrecision();
+ int scale = decimalVector.getScale();
+ BigDecimal decimal = getBigDecimalFromArrowBuf(
+ decimalVector.getDataBuffer(), index, scale,
DecimalVector.TYPE_WIDTH);
+ return Decimal.fromBigDecimal(decimal, precision, scale);
+ }
+ return convertToPaimonType(vector.getObject(index), arrowField,
targetType);
+ }
+
+ private Object convertToPaimonType(Object value, Field arrowField,
DataType targetType) {
+ if (value == null) {
+ return null;
+ }
+ if (targetType instanceof BinaryType || targetType instanceof
VarBinaryType) {
+ if (value instanceof byte[]) {
+ return value;
+ }
+ if (value instanceof BinaryString) {
+ return ((BinaryString) value).toBytes();
+ }
+ if (value instanceof org.apache.arrow.vector.util.Text) {
+ return ((org.apache.arrow.vector.util.Text) value).copyBytes();
+ }
+ if (value instanceof String) {
+ return ((String) value).getBytes(StandardCharsets.UTF_8);
+ }
+ return value.toString().getBytes(StandardCharsets.UTF_8);
+ }
+ if (value instanceof BinaryString) {
+ return value;
+ }
+ if (value instanceof byte[]) {
+ return BinaryString.fromBytes((byte[]) value);
+ }
+ if (value instanceof org.apache.arrow.vector.util.Text) {
+ return BinaryString.fromBytes(((org.apache.arrow.vector.util.Text)
value).copyBytes());
+ }
+ if (value instanceof org.apache.hadoop.io.Text) {
+ org.apache.hadoop.io.Text text = (org.apache.hadoop.io.Text) value;
+ return BinaryString.fromBytes(text.getBytes(), 0,
text.getLength());
+ }
+ if (value instanceof CharSequence) {
+ return BinaryString.fromString(value.toString());
+ }
+
+ ArrowType.ArrowTypeID typeId = arrowField == null
+ ? null : arrowField.getType().getTypeID();
+ if (value instanceof LocalDateTime) {
+ return toPaimonTimestamp((LocalDateTime) value, targetType);
+ }
+ if (value instanceof Long && typeId ==
ArrowType.ArrowTypeID.Timestamp) {
+ ArrowType.Timestamp timestampType = (ArrowType.Timestamp)
arrowField.getType();
+ return toPaimonTimestamp(
+ arrowTimestampToMicros((Long) value, timestampType),
timestampType, targetType);
+ }
+ if (value instanceof Integer && typeId == ArrowType.ArrowTypeID.Date) {
+ return value;
+ }
+ if (value instanceof java.time.LocalDate) {
+ return (int) ((java.time.LocalDate) value).toEpochDay();
+ }
+ if (value instanceof BigDecimal) {
+ BigDecimal decimal = (BigDecimal) value;
+ return Decimal.fromBigDecimal(decimal, decimal.precision(),
decimal.scale());
+ }
+ return value;
+ }
+
+ private GenericRow convertStructVector(
+ StructVector vector, int index, RowType rowType) {
+ List<DataField> childFields = rowType.getFields();
+ GenericRow row = new GenericRow(childFields.size());
+ for (int i = 0; i < childFields.size(); i++) {
+ DataField childField = childFields.get(i);
+ FieldVector childVector = findChildVector(vector,
childField.name());
Review Comment:
[P1] Preserve nested Paimon field names across the Arrow bridge
Doris `StructField` lowercases every nested name, so a legal Paimon child
such as `MixedCase` reaches this Arrow struct as `mixedcase`. This lookup then
uses the original Paimon name with exact equality and throws `Arrow struct does
not contain Paimon field MixedCase` for every write. Please carry the canonical
Paimon child names into the write schema, or use ambiguity-checked
case-insensitive matching, and cover mixed-case ROW children (including nested
array/map cases).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]