This is an automated email from the ASF dual-hosted git repository. suxiaogang223 pushed a commit to branch feature/paimon-jni-write-v1 in repository https://gitbox.apache.org/repos/asf/doris.git
commit 78f11466fecdc584b386e26d4d0e8447736934e6 Author: Socrates <[email protected]> AuthorDate: Wed Jul 8 15:31:52 2026 +0800 [feature](paimon) Step 5: Java JNI writer - PaimonJniWriter Implement the Java-side JNI entry point for Paimon write: - open(): load table, init writer with bucket mode and spill support - write(): Arrow IPC Stream -> VectorSchemaRoot -> Paimon InternalRow - prepareCommit(): flush, serialize CommitMessage via DPCM framing - abort()/close(): resource cleanup Supported types: all Paimon primitives, DECIMAL, TIMESTAMP, ARRAY, MAP, STRUCT. Supports fixed-bucket mode via RowKeyExtractor. v1 explicitly rejects HASH_DYNAMIC, KEY_DYNAMIC, and POSTPONE_MODE. Added arrow-vector dependency for Arrow IPC deserialization. Co-Authored-By: Claude <[email protected]> --- fe/be-java-extensions/paimon-scanner/pom.xml | 5 + .../org/apache/doris/paimon/PaimonJniWriter.java | 850 +++++++++++++++++++++ 2 files changed, 855 insertions(+) diff --git a/fe/be-java-extensions/paimon-scanner/pom.xml b/fe/be-java-extensions/paimon-scanner/pom.xml index 60ff39c2681..b1c74ef1f64 100644 --- a/fe/be-java-extensions/paimon-scanner/pom.xml +++ b/fe/be-java-extensions/paimon-scanner/pom.xml @@ -61,6 +61,11 @@ under the License. <artifactId>paimon-format</artifactId> </dependency> + <dependency> + <groupId>org.apache.arrow</groupId> + <artifactId>arrow-vector</artifactId> + </dependency> + </dependencies> <build> diff --git a/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java new file mode 100644 index 00000000000..e74db6993d1 --- /dev/null +++ b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java @@ -0,0 +1,850 @@ +// 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.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.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.TimeStampMicroVector; +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.holders.NullableTimeStampMicroHolder; +import org.apache.arrow.vector.ipc.ArrowStreamReader; +import org.apache.arrow.vector.types.pojo.ArrowType; +import org.apache.arrow.vector.types.pojo.Field; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.paimon.CoreOptions; +import org.apache.paimon.catalog.Catalog; +import org.apache.paimon.catalog.CatalogContext; +import org.apache.paimon.catalog.CatalogFactory; +import org.apache.paimon.catalog.Identifier; +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.disk.IOManager; +import org.apache.paimon.disk.IOManagerImpl; +import org.apache.paimon.io.DataOutputSerializer; +import org.apache.paimon.memory.HeapMemorySegmentPool; +import org.apache.paimon.options.Options; +import org.apache.paimon.table.BucketMode; +import org.apache.paimon.table.FileStoreTable; +import org.apache.paimon.table.Table; +import org.apache.paimon.table.sink.BatchTableWrite; +import org.apache.paimon.table.sink.CommitMessage; +import org.apache.paimon.table.sink.CommitMessageSerializer; +import org.apache.paimon.table.sink.RowKeyExtractor; +import org.apache.paimon.table.sink.TableWriteImpl; +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.MapType; +import org.apache.paimon.types.RowType; +import org.apache.paimon.types.VarBinaryType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * JNI entry point for Paimon write operations. + * + * Called from C++ (VPaimonTableWriter) via JNI. Data path: + * + * C++ Block → Arrow IPC Stream → direct memory address + * → PaimonJniWriter.write(address, length) + * → ArrowStreamReader → VectorSchemaRoot + * → convert row-by-row → Paimon GenericRow + * → BatchTableWrite.write(row) / write(row, bucket) + * + * Commit path: + * + * VPaimonTableWriter::close() → JNI → PaimonJniWriter.prepareCommit() + * → CommitMessageSerializer → byte[][] + * → C++ collects TPaimonCommitMessage → FE PaimonTransaction + */ +public class PaimonJniWriter { + private static final Logger LOG = LoggerFactory.getLogger(PaimonJniWriter.class); + + private static final String OPTION_PREFIX_PAIMON = "paimon."; + private static final String OPTION_PREFIX_HADOOP = "hadoop."; + private static final String KEY_SERIALIZED_TABLE = "serialized_table"; + private static final String KEY_COMMIT_IDENTIFIER = "doris.commit_identifier"; + private static final String KEY_COMMIT_USER = "doris.commit_user"; + private static final String KEY_SPILL_DIR = "paimon_jni_spill_dir"; + + static final int COMMIT_PAYLOAD_HEADER_BYTES = 12; + static final int MAX_COMMIT_PAYLOAD_BYTES = 8 * 1024 * 1024; + static final int DEFAULT_COMMIT_CHUNK_SIZE = 512; + + private BatchTableWrite writer; + private TableWriteImpl<?> tableWrite; + private RowKeyExtractor rowKeyExtractor; + + private final BufferAllocator allocator; + private final CommitMessageSerializer serializer = new CommitMessageSerializer(); + private final ClassLoader classLoader; + + private String tableLocation; + private PreExecutionAuthenticator preExecutionAuthenticator; + + private Map<String, DataField> paimonFieldMap; + private DataType[] targetTypes; + private BucketMode bucketMode; + private boolean useExplicitBucketWrite; + private long commitIdentifier; + private int totalBuckets; + + private IOManager ioManager; + private HeapMemorySegmentPool memorySegmentPool; + + private static volatile Constructor<?> directByteBufferConstructor; + + public PaimonJniWriter() { + this.allocator = new RootAllocator(Long.MAX_VALUE); + this.classLoader = this.getClass().getClassLoader(); + configureLogLevels(); + } + + // ──────────────────────────────────────────────────────────── + // JNI entry points (called from C++) + // ──────────────────────────────────────────────────────────── + + /** + * Initialize the writer. Called once per BE fragment. + * + * @param tableLocation Paimon table root path + * @param options merged options map (Paimon keys prefixed "paimon.", + * Hadoop keys prefixed "hadoop.") + + * @param columnNames output column names + */ + public void open(String tableLocation, Map<String, String> options, + String[] columnNames) throws Exception { + this.tableLocation = tableLocation; + Thread.currentThread().setContextClassLoader(classLoader); + + Map<String, String> paimonOpts = extractPrefixed(options, OPTION_PREFIX_PAIMON); + Map<String, String> hadoopOpts = extractPrefixed(options, OPTION_PREFIX_HADOOP); + + if (!paimonOpts.containsKey("warehouse") && tableLocation != null) { + paimonOpts.put("warehouse", tableLocation); + } + + this.preExecutionAuthenticator = PreExecutionAuthenticatorCache.getAuthenticator(options); + preExecutionAuthenticator.execute(() -> { + try { + LOG.info("PaimonJniWriter opening: location={}, columns={}", + tableLocation, columnNames != null ? columnNames.length : 0); + + Table table = loadTable(options, paimonOpts, hadoopOpts); + this.commitIdentifier = Long.parseLong( + options.getOrDefault(KEY_COMMIT_IDENTIFIER, "-1")); + + initFieldMap(table.rowType().getFields()); + RowType writeType = buildWriteType(columnNames); + initWriter(table, options); + + if (!isFullRowWrite(table.rowType(), writeType)) { + if (!table.primaryKeys().isEmpty()) { + throw new UnsupportedOperationException( + "Paimon primary-key write requires full table row type"); + } + this.writer.withWriteType(writeType); + } + return null; + } catch (Throwable t) { + throw new RuntimeException("PaimonJniWriter open failed: location=" + + tableLocation, t); + } + }); + } + + /** + * Write a batch of rows. Called once per Arrow IPC buffer from C++. + * + * @param address native memory address of the Arrow IPC Stream bytes + * @param length number of bytes + */ + public void write(long address, int length) throws Exception { + preExecutionAuthenticator.execute(() -> { + try { + ByteBuffer directBuffer = getDirectBuffer(address, length); + 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: addr=" + + address + ", len=" + length, t); + } + }); + } + + /** + * Prepare commit: flush all in-memory data, close files, serialize commit messages. + * + * @return byte[][] each element is a DPCM-framed serialized CommitMessage chunk + */ + public byte[][] prepareCommit() throws Exception { + if (writer == null) { + return new byte[0][]; + } + return preExecutionAuthenticator.execute(() -> { + try { + List<CommitMessage> messages = doPrepareCommit(); + if (messages == null || messages.isEmpty()) { + LOG.info("PaimonJniWriter prepareCommit: empty, location={}", tableLocation); + return new byte[0][]; + } + LOG.info("PaimonJniWriter prepareCommit: {} messages", messages.size()); + return serializeMessages(messages); + } catch (Throwable t) { + throw new RuntimeException("PaimonJniWriter prepareCommit failed: location=" + + tableLocation, t); + } + }); + } + + /** + * Abort: discard written data files. Called on error. + */ + public void abort() throws Exception { + try { + if (preExecutionAuthenticator != null) { + preExecutionAuthenticator.execute(() -> { + closeWriterResources(false); + return null; + }); + } + } catch (Exception e) { + LOG.error("PaimonJniWriter abort failed", e); + throw e; + } + } + + /** + * Close: release all resources. + */ + public void close() throws Exception { + try { + if (preExecutionAuthenticator != null) { + preExecutionAuthenticator.execute(() -> { + closeWriterResources(true); + return null; + }); + } + } catch (Exception e) { + LOG.warn("PaimonJniWriter close error", e); + throw e; + } + } + + // ──────────────────────────────────────────────────────────── + // Initialization helpers + // ──────────────────────────────────────────────────────────── + + private static Map<String, String> extractPrefixed(Map<String, String> options, + String prefix) { + return options.entrySet().stream() + .filter(kv -> kv.getKey().startsWith(prefix)) + .collect(Collectors.toMap( + kv -> kv.getKey().substring(prefix.length()), + Map.Entry::getValue)); + } + + private Table loadTable(Map<String, String> options, + Map<String, String> paimonOpts, + Map<String, String> hadoopOpts) throws Exception { + if (options.containsKey(KEY_SERIALIZED_TABLE)) { + return PaimonUtils.deserialize(options.get(KEY_SERIALIZED_TABLE)); + } + Catalog catalog = createCatalog(paimonOpts, hadoopOpts); + String dbName = options.getOrDefault("db_name", "default"); + String tblName = options.getOrDefault("table_name", "paimon_table"); + return catalog.getTable(Identifier.create(dbName, tblName)); + } + + private void initFieldMap(List<DataField> fields) { + this.paimonFieldMap = new HashMap<>(); + for (DataField field : fields) { + this.paimonFieldMap.put(field.name(), field); + } + } + + private void initWriter(Table table, Map<String, String> options) throws Exception { + if (!(table instanceof FileStoreTable)) { + this.writer = table.newBatchWriteBuilder().newWrite(); + return; + } + + FileStoreTable fileStoreTable = (FileStoreTable) table; + CoreOptions coreOptions = CoreOptions.fromMap(fileStoreTable.options()); + this.totalBuckets = coreOptions.bucket(); + + String commitUser = options.get(KEY_COMMIT_USER); + if (commitUser == null || commitUser.isEmpty()) { + commitUser = coreOptions.createCommitUser(); + } + + this.tableWrite = fileStoreTable.newWrite(commitUser); + this.writer = this.tableWrite; + initBucketMode(fileStoreTable); + initSpillIfNeeded(coreOptions, options); + } + + private void initBucketMode(FileStoreTable fileStoreTable) { + this.bucketMode = fileStoreTable.bucketMode(); + if (bucketMode == BucketMode.HASH_DYNAMIC + || bucketMode == BucketMode.KEY_DYNAMIC + || bucketMode == BucketMode.POSTPONE_MODE) { + throw new UnsupportedOperationException( + "Unsupported Paimon bucket mode for write: " + bucketMode + + ". Only HASH_FIXED and BUCKET_UNAWARE are supported in v1."); + } + this.useExplicitBucketWrite = bucketMode == BucketMode.HASH_FIXED; + if (useExplicitBucketWrite) { + this.rowKeyExtractor = fileStoreTable.createRowKeyExtractor(); + } + } + + private void initSpillIfNeeded(CoreOptions coreOptions, Map<String, String> options) + throws Exception { + if (!coreOptions.writeBufferSpillable()) { + return; + } + String spillDir = options.get(KEY_SPILL_DIR); + if (spillDir == null || spillDir.isEmpty()) { + spillDir = System.getProperty("java.io.tmpdir"); + } else { + Files.createDirectories(Paths.get(spillDir)); + } + this.ioManager = new IOManagerImpl(spillDir); + this.memorySegmentPool = new HeapMemorySegmentPool( + coreOptions.writeBufferSize(), coreOptions.pageSize()); + this.writer.withIOManager(ioManager).withMemoryPool(memorySegmentPool); + LOG.info("PaimonJniWriter spill enabled: dir={}", spillDir); + } + + // ──────────────────────────────────────────────────────────── + // Data writing + // ──────────────────────────────────────────────────────────── + + private void writeBatch(VectorSchemaRoot root) throws Exception { + int rowCount = root.getRowCount(); + if (rowCount == 0) { + return; + } + List<Field> fields = root.getSchema().getFields(); + List<FieldVector> vectors = root.getFieldVectors(); + int colCount = fields.size(); + DataType[] currentTypes = resolveTargetTypes(fields); + GenericRow reusedRow = new GenericRow(colCount); + + for (int i = 0; i < rowCount; i++) { + for (int col = 0; col < colCount; col++) { + try { + reusedRow.setField(col, + readArrowValue(vectors.get(col), i, fields.get(col), currentTypes[col])); + } catch (Throwable t) { + throw new RuntimeException("PaimonJniWriter row conversion failed: row=" + i + + ", col=" + col + ", field=" + fields.get(col).getName() + + ", targetType=" + (currentTypes[col] != null + ? currentTypes[col].asSQLString() : "null"), t); + } + } + try { + if (useExplicitBucketWrite) { + rowKeyExtractor.setRecord(reusedRow); + writer.write(reusedRow, rowKeyExtractor.bucket()); + } else { + writer.write(reusedRow); + } + } catch (Throwable t) { + throw new RuntimeException("PaimonJniWriter write failed: row=" + i + + ", totalRows=" + rowCount, t); + } + } + } + + // ──────────────────────────────────────────────────────────── + // Arrow → Paimon type conversion + // ──────────────────────────────────────────────────────────── + + private Object readArrowValue(FieldVector vector, int row, Field arrowField, + DataType targetType) { + if (vector == null || vector.isNull(row)) { + return null; + } + if (targetType instanceof BinaryType || targetType instanceof VarBinaryType) { + if (vector instanceof VarBinaryVector) { + return ((VarBinaryVector) vector).get(row); + } + if (vector instanceof VarCharVector) { + return ((VarCharVector) vector).get(row); + } + } else { + if (vector instanceof VarCharVector) { + return BinaryString.fromBytes(((VarCharVector) vector).get(row)); + } + } + if (vector instanceof BitVector) { + return ((BitVector) vector).get(row) == 1; + } + if (vector instanceof TinyIntVector) { + return ((TinyIntVector) vector).get(row); + } + if (vector instanceof SmallIntVector) { + return ((SmallIntVector) vector).get(row); + } + if (vector instanceof IntVector) { + return ((IntVector) vector).get(row); + } + if (vector instanceof BigIntVector) { + return ((BigIntVector) vector).get(row); + } + if (vector instanceof Float4Vector) { + return ((Float4Vector) vector).get(row); + } + if (vector instanceof Float8Vector) { + return ((Float8Vector) vector).get(row); + } + if (vector instanceof DateDayVector) { + return ((DateDayVector) vector).get(row); + } + if (vector instanceof TimeStampMicroVector) { + NullableTimeStampMicroHolder holder = new NullableTimeStampMicroHolder(); + ((TimeStampMicroVector) vector).get(row, holder); + return Timestamp.fromMicros(holder.value); + } + if (vector instanceof DecimalVector) { + return readDecimal((DecimalVector) vector, row); + } + Object val = vector.getObject(row); + return convertToPaimonType(val, arrowField, targetType); + } + + private Decimal readDecimal(DecimalVector vector, int row) { + BigDecimal bd = getBigDecimalFromArrowBuf(vector.getDataBuffer(), row, + vector.getScale(), DecimalVector.TYPE_WIDTH); + return Decimal.fromBigDecimal(bd, vector.getPrecision(), vector.getScale()); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private Object convertToPaimonType(Object val, Field arrowField, DataType targetType) { + if (val == null) { + return null; + } + if (targetType instanceof BinaryType || targetType instanceof VarBinaryType) { + if (val instanceof byte[]) { + return val; + } + if (val instanceof BinaryString) { + return ((BinaryString) val).toBytes(); + } + if (val instanceof org.apache.arrow.vector.util.Text) { + return ((org.apache.arrow.vector.util.Text) val).copyBytes(); + } + if (val instanceof String) { + return ((String) val).getBytes(StandardCharsets.UTF_8); + } + return val.toString().getBytes(StandardCharsets.UTF_8); + } + if (val instanceof BinaryString) { + return val; + } + if (val instanceof byte[]) { + return BinaryString.fromBytes((byte[]) val); + } + if (val instanceof org.apache.arrow.vector.util.Text) { + return BinaryString.fromBytes(((org.apache.arrow.vector.util.Text) val).copyBytes()); + } + if (val instanceof org.apache.hadoop.io.Text) { + org.apache.hadoop.io.Text t = (org.apache.hadoop.io.Text) val; + return BinaryString.fromBytes(t.getBytes(), 0, t.getLength()); + } + if (val instanceof CharSequence) { + return BinaryString.fromString(val.toString()); + } + + ArrowType.ArrowTypeID typeID = arrowField != null + ? arrowField.getType().getTypeID() : null; + + if (val instanceof LocalDateTime) { + return Timestamp.fromLocalDateTime((LocalDateTime) val); + } + if (val instanceof Long && typeID == ArrowType.ArrowTypeID.Timestamp) { + return Timestamp.fromMicros((Long) val); + } + if (val instanceof Integer && typeID == ArrowType.ArrowTypeID.Date) { + return val; + } + if (val instanceof java.time.LocalDate) { + return (int) ((java.time.LocalDate) val).toEpochDay(); + } + if (val instanceof BigDecimal) { + BigDecimal bd = (BigDecimal) val; + return Decimal.fromBigDecimal(bd, bd.precision(), bd.scale()); + } + if (targetType instanceof RowType && val instanceof Map) { + return convertStruct((Map<?, ?>) val, (RowType) targetType, arrowField); + } + if (targetType instanceof MapType && val instanceof List) { + return convertMap((List<?>) val, (MapType) targetType, arrowField); + } + if (targetType instanceof ArrayType && val instanceof List) { + return convertArray((List<?>) val, (ArrayType) targetType, arrowField); + } + if (val instanceof byte[]) { + return val; + } + return val; + } + + private GenericRow convertStruct(Map<?, ?> mapVal, RowType rowType, Field arrowField) { + List<DataField> childFields = rowType.getFields(); + GenericRow structRow = new GenericRow(childFields.size()); + for (int i = 0; i < childFields.size(); i++) { + DataField childField = childFields.get(i); + Object childVal = mapVal.get(childField.name()); + Field childArrowField = findChildField(arrowField, childField.name()); + structRow.setField(i, + convertToPaimonType(childVal, childArrowField, childField.type())); + } + return structRow; + } + + private GenericMap convertMap(List<?> list, MapType mapType, Field arrowField) { + Field keyArrowField = null; + Field valueArrowField = null; + if (arrowField != null && !arrowField.getChildren().isEmpty()) { + Field entries = arrowField.getChildren().get(0); + if (entries.getChildren().size() >= 2) { + keyArrowField = entries.getChildren().get(0); + valueArrowField = entries.getChildren().get(1); + } + } + String keyName = keyArrowField != null ? keyArrowField.getName() : "key"; + String valueName = valueArrowField != null ? valueArrowField.getName() : "value"; + + Map<Object, Object> converted = new HashMap<>(); + for (Object element : list) { + if (element instanceof Map) { + Map<?, ?> kv = (Map<?, ?>) element; + Object k = convertToPaimonType(kv.get(keyName), keyArrowField, mapType.getKeyType()); + Object v = convertToPaimonType(kv.get(valueName), valueArrowField, + mapType.getValueType()); + converted.put(k, v); + } + } + return new GenericMap(converted); + } + + private GenericArray convertArray(List<?> list, ArrayType arrayType, Field arrowField) { + Object[] converted = new Object[list.size()]; + Field childArrowField = null; + if (arrowField != null && !arrowField.getChildren().isEmpty()) { + childArrowField = arrowField.getChildren().get(0); + } + for (int i = 0; i < list.size(); i++) { + converted[i] = convertToPaimonType(list.get(i), childArrowField, + arrayType.getElementType()); + } + return new GenericArray(converted); + } + + private static Field findChildField(Field parent, String name) { + if (parent == null || parent.getChildren() == null) { + return null; + } + for (Field f : parent.getChildren()) { + if (f.getName().equals(name)) { + return f; + } + } + return null; + } + + // ──────────────────────────────────────────────────────────── + // Commit & serialization + // ──────────────────────────────────────────────────────────── + + private List<CommitMessage> doPrepareCommit() throws Exception { + return tableWrite != null && commitIdentifier > 0 + ? tableWrite.prepareCommit(true, commitIdentifier) + : writer.prepareCommit(); + } + + private byte[][] serializeMessages(List<CommitMessage> messages) throws Exception { + int chunkSize = DEFAULT_COMMIT_CHUNK_SIZE; + List<byte[]> payloads = new ArrayList<>(); + int i = 0; + while (i < messages.size()) { + int end = Math.min(i + chunkSize, messages.size()); + byte[] payload = serializeChunk(messages.subList(i, end)); + if (payload.length > MAX_COMMIT_PAYLOAD_BYTES && chunkSize > 1) { + chunkSize = Math.max(1, chunkSize / 2); + continue; + } + payloads.add(payload); + i = end; + } + return payloads.toArray(new byte[0][]); + } + + private byte[] serializeChunk(List<CommitMessage> messages) throws Exception { + DataOutputSerializer out = new DataOutputSerializer(1024); + serializer.serializeList(messages, out); + byte[] data = out.getCopyOfBuffer(); + int len = data.length; + int version = serializer.getVersion(); + + byte[] payload = new byte[COMMIT_PAYLOAD_HEADER_BYTES + len]; + // Magic bytes "DPCM" + payload[0] = 'D'; + payload[1] = 'P'; + payload[2] = 'C'; + payload[3] = 'M'; + // Version (big-endian) + payload[4] = (byte) ((version >>> 24) & 0xFF); + payload[5] = (byte) ((version >>> 16) & 0xFF); + payload[6] = (byte) ((version >>> 8) & 0xFF); + payload[7] = (byte) (version & 0xFF); + // Length (big-endian) + payload[8] = (byte) ((len >>> 24) & 0xFF); + payload[9] = (byte) ((len >>> 16) & 0xFF); + payload[10] = (byte) ((len >>> 8) & 0xFF); + payload[11] = (byte) (len & 0xFF); + System.arraycopy(data, 0, payload, COMMIT_PAYLOAD_HEADER_BYTES, len); + return payload; + } + + // ──────────────────────────────────────────────────────────── + // Schema & type resolution + // ──────────────────────────────────────────────────────────── + + private RowType buildWriteType(String[] columnNames) { + if (columnNames == null || columnNames.length == 0) { + throw new IllegalArgumentException( + "PaimonJniWriter requires explicit column names"); + } + List<DataField> writeFields = new ArrayList<>(columnNames.length); + this.targetTypes = new DataType[columnNames.length]; + for (int i = 0; i < columnNames.length; i++) { + DataField field = paimonFieldMap.get(columnNames[i]); + if (field == null) { + throw new IllegalArgumentException( + "Paimon column '" + columnNames[i] + "' not found in table schema"); + } + writeFields.add(field); + this.targetTypes[i] = field.type(); + } + return new RowType(writeFields); + } + + private DataType[] resolveTargetTypes(List<Field> fields) { + if (targetTypes != null && targetTypes.length == fields.size()) { + return targetTypes; + } + DataType[] result = new DataType[fields.size()]; + for (int i = 0; i < fields.size(); i++) { + DataField df = paimonFieldMap.get(fields.get(i).getName()); + if (df == null) { + throw new IllegalArgumentException( + "Paimon column '" + fields.get(i).getName() + "' not found"); + } + result[i] = df.type(); + } + return result; + } + + static boolean isFullRowWrite(RowType tableRowType, RowType writeType) { + return tableRowType.getFieldNames().equals(writeType.getFieldNames()); + } + + // ──────────────────────────────────────────────────────────── + // Resource management + // ──────────────────────────────────────────────────────────── + + private void closeWriterResources(boolean closeAllocator) throws Exception { + try { + if (writer != null) { + writer.close(); + writer = null; + } + tableWrite = null; + rowKeyExtractor = null; + useExplicitBucketWrite = false; + if (ioManager != null) { + ioManager.close(); + ioManager = null; + } + memorySegmentPool = null; + if (closeAllocator && allocator != null) { + allocator.close(); + } + } catch (Throwable t) { + throw new RuntimeException("PaimonJniWriter close failed", t); + } + } + + // ──────────────────────────────────────────────────────────── + // Utilities + // ──────────────────────────────────────────────────────────── + + private static BigDecimal getBigDecimalFromArrowBuf( + org.apache.arrow.memory.ArrowBuf byteBuf, + int index, int scale, int byteWidth) { + byte[] value = new byte[byteWidth]; + long startIndex = (long) index * byteWidth; + byteBuf.getBytes(startIndex, value, 0, byteWidth); + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + int stop = byteWidth / 2; + for (int i = 0; i < stop; i++) { + byte temp = value[i]; + int j = (byteWidth - 1) - i; + value[i] = value[j]; + value[j] = temp; + } + } + return new BigDecimal(new BigInteger(value), scale); + } + + private static ByteBuffer getDirectBuffer(long address, int length) throws Exception { + Constructor<?> ctor = directByteBufferConstructor; + if (ctor == null) { + Class<?> cls = Class.forName("java.nio.DirectByteBuffer"); + ctor = cls.getDeclaredConstructor(long.class, int.class); + ctor.setAccessible(true); + directByteBufferConstructor = ctor; + } + return (ByteBuffer) ctor.newInstance(address, length); + } + + private static Catalog createCatalog(Map<String, String> paimonOpts, + Map<String, String> hadoopOpts) { + Options options = new Options(); + paimonOpts.forEach(options::set); + + Configuration conf = new Configuration(); + hadoopOpts.forEach(conf::set); + + String hadoopConfDir = options.getString("hadoop-conf-dir", (String) null); + if (hadoopConfDir != null) { + conf.addResource(new Path(hadoopConfDir + "core-site.xml")); + conf.addResource(new Path(hadoopConfDir + "hdfs-site.xml")); + } + String hiveConfDir = options.getString("hive-conf-dir", (String) null); + if (hiveConfDir != null) { + conf.addResource(new Path(hiveConfDir + "hive-site.xml")); + } + paimonOpts.forEach(conf::set); + + CatalogContext ctx = CatalogContext.create(options, conf); + return CatalogFactory.createCatalog(ctx); + } + + private static void configureLogLevels() { + try { + for (String name : new String[] { + "org.apache.paimon.shade.org.apache.parquet", + "org.apache.paimon" + }) { + org.slf4j.Logger targetLogger = org.slf4j.LoggerFactory.getLogger(name); + Class<?> logbackCls = Class.forName("ch.qos.logback.classic.Logger"); + Class<?> levelCls = Class.forName("ch.qos.logback.classic.Level"); + if (logbackCls.isInstance(targetLogger)) { + Object warnLevel = levelCls.getField("WARN").get(null); + logbackCls.getMethod("setLevel", levelCls).invoke(targetLogger, warnLevel); + } + } + } catch (Throwable t) { + LOG.debug("PaimonJniWriter: log level configuration skipped", t); + } + } + + @SuppressWarnings("unused") + private String currentState() { + return "location=" + tableLocation + + ", writerNull=" + (writer == null) + + ", bucketMode=" + bucketMode + + ", explicitBucket=" + useExplicitBucketWrite; + } + + /** InputStream over a direct ByteBuffer (no copy). */ + private static class DirectBufInputStream extends InputStream { + private final ByteBuffer buf; + + DirectBufInputStream(ByteBuffer buf) { + this.buf = buf; + } + + @Override + public int read() { + if (buf.hasRemaining()) { + return buf.get() & 0xFF; + } + return -1; + } + + @Override + public int read(byte[] b, int off, int len) { + if (!buf.hasRemaining()) { + return -1; + } + int n = Math.min(len, buf.remaining()); + buf.get(b, off, n); + return n; + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
