This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new a8074ed60c [parquet] Support column compression configuration (#8279)
a8074ed60c is described below

commit a8074ed60c760972fb2a6ac9fb8218f6cd7d96a3
Author: Jingsong Lee <[email protected]>
AuthorDate: Thu Jun 18 17:09:04 2026 +0800

    [parquet] Support column compression configuration (#8279)
    
    Support configuring Parquet compression codecs per column. This lets
    users keep a table-level codec such as ZSTD while overriding selected
    columns to another codec, including `none` / `UNCOMPRESSED`.
---
 .../parquet/writer/RowDataParquetBuilder.java      |  15 +-
 .../parquet/hadoop/ColumnCompressionCodecs.java    |  52 ++
 .../hadoop/ColumnCompressionPageWriteStore.java    | 660 +++++++++++++++++++++
 .../hadoop/ColumnCompressionRecordWriter.java      | 242 ++++++++
 .../org/apache/parquet/hadoop/ParquetWriter.java   |  26 +-
 .../format/parquet/ParquetFormatReadWriteTest.java |  43 ++
 6 files changed, 1031 insertions(+), 7 deletions(-)

diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/RowDataParquetBuilder.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/RowDataParquetBuilder.java
index 7d72c8a1a9..2e84df0932 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/RowDataParquetBuilder.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/RowDataParquetBuilder.java
@@ -19,6 +19,7 @@
 package org.apache.paimon.format.parquet.writer;
 
 import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.format.HadoopCompressionType;
 import org.apache.paimon.format.parquet.ColumnConfigParser;
 import org.apache.paimon.format.parquet.VariantUtils;
 import org.apache.paimon.options.Options;
@@ -60,8 +61,7 @@ public class RowDataParquetBuilder implements 
ParquetBuilder<InternalRow> {
         ParquetRowDataBuilder builder =
                 new ParquetRowDataBuilder(out, rowType, shreddingSchemas)
                         .withConf(conf)
-                        .withCompressionCodec(
-                                
CompressionCodecName.fromConf(getCompression(compression)))
+                        
.withCompressionCodec(getCompressionCodec(getCompression(compression)))
                         .withRowGroupSize(
                                 conf.getLong(
                                         ParquetOutputFormat.BLOCK_SIZE,
@@ -114,6 +114,10 @@ public class RowDataParquetBuilder implements 
ParquetBuilder<InternalRow> {
                                         
ParquetOutputFormat.COLUMN_INDEX_TRUNCATE_LENGTH,
                                         
ParquetProperties.DEFAULT_COLUMN_INDEX_TRUNCATE_LENGTH));
         new ColumnConfigParser()
+                .withColumnConfig(
+                        ParquetOutputFormat.COMPRESSION,
+                        key -> getCompressionCodec(conf.get(key)),
+                        builder::withCompressionCodec)
                 .withColumnConfig(
                         ParquetOutputFormat.ENABLE_DICTIONARY,
                         key -> conf.getBoolean(key, false),
@@ -137,4 +141,11 @@ public class RowDataParquetBuilder implements 
ParquetBuilder<InternalRow> {
     public String getCompression(String compression) {
         return conf.get("parquet.compression", compression);
     }
+
+    private CompressionCodecName getCompressionCodec(String compression) {
+        if (HadoopCompressionType.NONE.value().equalsIgnoreCase(compression)) {
+            return CompressionCodecName.UNCOMPRESSED;
+        }
+        return CompressionCodecName.fromConf(compression);
+    }
 }
diff --git 
a/paimon-format/src/main/java/org/apache/parquet/hadoop/ColumnCompressionCodecs.java
 
b/paimon-format/src/main/java/org/apache/parquet/hadoop/ColumnCompressionCodecs.java
new file mode 100644
index 0000000000..4f9c6842d8
--- /dev/null
+++ 
b/paimon-format/src/main/java/org/apache/parquet/hadoop/ColumnCompressionCodecs.java
@@ -0,0 +1,52 @@
+/*
+ * 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.parquet.hadoop;
+
+import org.apache.parquet.column.ColumnDescriptor;
+import org.apache.parquet.hadoop.metadata.ColumnPath;
+import org.apache.parquet.hadoop.metadata.CompressionCodecName;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Compression codecs configured per parquet column path. */
+class ColumnCompressionCodecs {
+
+    private final CompressionCodecName defaultCodec;
+    private final Map<ColumnPath, CompressionCodecName> columnCodecs;
+
+    ColumnCompressionCodecs(CompressionCodecName defaultCodec) {
+        this(defaultCodec, Collections.<ColumnPath, 
CompressionCodecName>emptyMap());
+    }
+
+    ColumnCompressionCodecs(
+            CompressionCodecName defaultCodec, Map<ColumnPath, 
CompressionCodecName> columnCodecs) {
+        this.defaultCodec = defaultCodec;
+        this.columnCodecs =
+                columnCodecs.isEmpty()
+                        ? Collections.<ColumnPath, 
CompressionCodecName>emptyMap()
+                        : new HashMap<>(columnCodecs);
+    }
+
+    CompressionCodecName getCodec(ColumnDescriptor descriptor) {
+        CompressionCodecName codec = 
columnCodecs.get(ColumnPath.get(descriptor.getPath()));
+        return codec == null ? defaultCodec : codec;
+    }
+}
diff --git 
a/paimon-format/src/main/java/org/apache/parquet/hadoop/ColumnCompressionPageWriteStore.java
 
b/paimon-format/src/main/java/org/apache/parquet/hadoop/ColumnCompressionPageWriteStore.java
new file mode 100644
index 0000000000..12250aa3f8
--- /dev/null
+++ 
b/paimon-format/src/main/java/org/apache/parquet/hadoop/ColumnCompressionPageWriteStore.java
@@ -0,0 +1,660 @@
+/*
+ * 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.parquet.hadoop;
+
+import org.apache.parquet.bytes.ByteBufferAllocator;
+import org.apache.parquet.bytes.ByteBufferReleaser;
+import org.apache.parquet.bytes.BytesInput;
+import org.apache.parquet.bytes.ConcatenatingByteBufferCollector;
+import org.apache.parquet.column.ColumnDescriptor;
+import org.apache.parquet.column.Encoding;
+import org.apache.parquet.column.page.DictionaryPage;
+import org.apache.parquet.column.page.PageWriteStore;
+import org.apache.parquet.column.page.PageWriter;
+import org.apache.parquet.column.statistics.SizeStatistics;
+import org.apache.parquet.column.statistics.Statistics;
+import org.apache.parquet.column.statistics.geospatial.GeospatialStatistics;
+import org.apache.parquet.column.values.bloomfilter.BloomFilter;
+import org.apache.parquet.column.values.bloomfilter.BloomFilterWriteStore;
+import org.apache.parquet.column.values.bloomfilter.BloomFilterWriter;
+import org.apache.parquet.compression.CompressionCodecFactory;
+import org.apache.parquet.crypto.AesCipher;
+import org.apache.parquet.crypto.InternalColumnEncryptionSetup;
+import org.apache.parquet.crypto.InternalFileEncryptor;
+import org.apache.parquet.crypto.ModuleCipherFactory.ModuleType;
+import org.apache.parquet.format.BlockCipher;
+import org.apache.parquet.format.converter.ParquetMetadataConverter;
+import org.apache.parquet.hadoop.metadata.ColumnPath;
+import org.apache.parquet.internal.column.columnindex.ColumnIndexBuilder;
+import org.apache.parquet.internal.column.columnindex.OffsetIndexBuilder;
+import org.apache.parquet.io.ParquetEncodingException;
+import org.apache.parquet.schema.MessageType;
+import org.apache.parquet.util.AutoCloseables;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.zip.CRC32;
+
+/**
+ * Page write store with per-column compression.
+ *
+ * <p>NOTE: The file is adapted from Apache parquet's {@link 
ColumnChunkPageWriteStore}.
+ */
+public class ColumnCompressionPageWriteStore implements PageWriteStore, 
BloomFilterWriteStore {
+
+    private static final Logger LOG =
+            LoggerFactory.getLogger(ColumnCompressionPageWriteStore.class);
+
+    private static final ParquetMetadataConverter PARQUET_METADATA_CONVERTER =
+            new ParquetMetadataConverter();
+
+    private static final class ColumnChunkPageWriter implements PageWriter, 
BloomFilterWriter {
+
+        private final ColumnDescriptor path;
+        private final CompressionCodecFactory.BytesInputCompressor compressor;
+
+        private final ByteArrayOutputStream tempOutputStream = new 
ByteArrayOutputStream();
+        private final ConcatenatingByteBufferCollector buf;
+        private DictionaryPage dictionaryPage;
+
+        private long uncompressedLength;
+        private long compressedLength;
+        private long totalValueCount;
+        private int pageCount;
+
+        private Set<Encoding> rlEncodings = new HashSet<>();
+        private Set<Encoding> dlEncodings = new HashSet<>();
+        private List<Encoding> dataEncodings = new ArrayList<>();
+
+        private BloomFilter bloomFilter;
+        private ColumnIndexBuilder columnIndexBuilder;
+        private OffsetIndexBuilder offsetIndexBuilder;
+        private Statistics totalStatistics;
+        private final SizeStatistics totalSizeStatistics;
+        private final GeospatialStatistics totalGeospatialStatistics;
+        private final ByteBufferReleaser releaser;
+
+        private final CRC32 crc;
+        private final boolean pageWriteChecksumEnabled;
+
+        private final BlockCipher.Encryptor headerBlockEncryptor;
+        private final BlockCipher.Encryptor pageBlockEncryptor;
+        private final int rowGroupOrdinal;
+        private final int columnOrdinal;
+        private int pageOrdinal;
+        private final byte[] dataPageAAD;
+        private final byte[] dataPageHeaderAAD;
+        private final byte[] fileAAD;
+
+        private ColumnChunkPageWriter(
+                ColumnDescriptor path,
+                CompressionCodecFactory.BytesInputCompressor compressor,
+                ByteBufferAllocator allocator,
+                int columnIndexTruncateLength,
+                boolean pageWriteChecksumEnabled,
+                BlockCipher.Encryptor headerBlockEncryptor,
+                BlockCipher.Encryptor pageBlockEncryptor,
+                byte[] fileAAD,
+                int rowGroupOrdinal,
+                int columnOrdinal) {
+            this.path = path;
+            this.compressor = compressor;
+            this.releaser = new ByteBufferReleaser(allocator);
+            this.buf = new ConcatenatingByteBufferCollector(allocator);
+            this.columnIndexBuilder =
+                    ColumnIndexBuilder.getBuilder(
+                            path.getPrimitiveType(), 
columnIndexTruncateLength);
+            this.offsetIndexBuilder = OffsetIndexBuilder.getBuilder();
+            this.totalSizeStatistics =
+                    SizeStatistics.newBuilder(
+                                    path.getPrimitiveType(),
+                                    path.getMaxRepetitionLevel(),
+                                    path.getMaxDefinitionLevel())
+                            .build();
+            this.totalGeospatialStatistics =
+                    
GeospatialStatistics.newBuilder(path.getPrimitiveType()).build();
+            this.pageWriteChecksumEnabled = pageWriteChecksumEnabled;
+            this.crc = pageWriteChecksumEnabled ? new CRC32() : null;
+            this.headerBlockEncryptor = headerBlockEncryptor;
+            this.pageBlockEncryptor = pageBlockEncryptor;
+            this.fileAAD = fileAAD;
+            this.rowGroupOrdinal = rowGroupOrdinal;
+            this.columnOrdinal = columnOrdinal;
+            this.pageOrdinal = -1;
+            if (headerBlockEncryptor != null) {
+                dataPageHeaderAAD =
+                        AesCipher.createModuleAAD(
+                                fileAAD,
+                                ModuleType.DataPageHeader,
+                                rowGroupOrdinal,
+                                columnOrdinal,
+                                0);
+            } else {
+                dataPageHeaderAAD = null;
+            }
+            if (pageBlockEncryptor != null) {
+                dataPageAAD =
+                        AesCipher.createModuleAAD(
+                                fileAAD, ModuleType.DataPage, rowGroupOrdinal, 
columnOrdinal, 0);
+            } else {
+                dataPageAAD = null;
+            }
+        }
+
+        @Override
+        @Deprecated
+        public void writePage(
+                BytesInput bytesInput,
+                int valueCount,
+                Statistics<?> statistics,
+                Encoding rlEncoding,
+                Encoding dlEncoding,
+                Encoding valuesEncoding)
+                throws IOException {
+            columnIndexBuilder = ColumnIndexBuilder.getNoOpBuilder();
+            offsetIndexBuilder = OffsetIndexBuilder.getNoOpBuilder();
+
+            writePage(
+                    bytesInput, valueCount, -1, statistics, rlEncoding, 
dlEncoding, valuesEncoding);
+        }
+
+        @Override
+        public void writePage(
+                BytesInput bytes,
+                int valueCount,
+                int rowCount,
+                Statistics<?> statistics,
+                Encoding rlEncoding,
+                Encoding dlEncoding,
+                Encoding valuesEncoding)
+                throws IOException {
+            writePage(
+                    bytes,
+                    valueCount,
+                    rowCount,
+                    statistics,
+                    null,
+                    null,
+                    rlEncoding,
+                    dlEncoding,
+                    valuesEncoding);
+        }
+
+        @Override
+        public void writePage(
+                BytesInput bytes,
+                int valueCount,
+                int rowCount,
+                Statistics<?> statistics,
+                SizeStatistics sizeStatistics,
+                GeospatialStatistics geospatialStatistics,
+                Encoding rlEncoding,
+                Encoding dlEncoding,
+                Encoding valuesEncoding)
+                throws IOException {
+            pageOrdinal++;
+            long uncompressedSize = bytes.size();
+            if (uncompressedSize > Integer.MAX_VALUE || uncompressedSize < 0) {
+                throw new ParquetEncodingException(
+                        "Cannot write page larger than Integer.MAX_VALUE or 
negative bytes: "
+                                + uncompressedSize);
+            }
+            BytesInput compressedBytes = compressor.compress(bytes);
+            if (pageBlockEncryptor != null) {
+                AesCipher.quickUpdatePageAAD(dataPageAAD, pageOrdinal);
+                compressedBytes =
+                        BytesInput.from(
+                                pageBlockEncryptor.encrypt(
+                                        compressedBytes.toByteArray(), 
dataPageAAD));
+            }
+            long compressedSize = compressedBytes.size();
+            if (compressedSize > Integer.MAX_VALUE) {
+                throw new ParquetEncodingException(
+                        "Cannot write compressed page larger than 
Integer.MAX_VALUE bytes: "
+                                + compressedSize);
+            }
+            tempOutputStream.reset();
+            if (headerBlockEncryptor != null) {
+                AesCipher.quickUpdatePageAAD(dataPageHeaderAAD, pageOrdinal);
+            }
+            if (pageWriteChecksumEnabled) {
+                crc.reset();
+                crc.update(compressedBytes.toByteArray());
+                PARQUET_METADATA_CONVERTER.writeDataPageV1Header(
+                        (int) uncompressedSize,
+                        (int) compressedSize,
+                        valueCount,
+                        rlEncoding,
+                        dlEncoding,
+                        valuesEncoding,
+                        (int) crc.getValue(),
+                        tempOutputStream,
+                        headerBlockEncryptor,
+                        dataPageHeaderAAD);
+            } else {
+                PARQUET_METADATA_CONVERTER.writeDataPageV1Header(
+                        (int) uncompressedSize,
+                        (int) compressedSize,
+                        valueCount,
+                        rlEncoding,
+                        dlEncoding,
+                        valuesEncoding,
+                        tempOutputStream,
+                        headerBlockEncryptor,
+                        dataPageHeaderAAD);
+            }
+            this.uncompressedLength += uncompressedSize;
+            this.compressedLength += compressedSize;
+            this.totalValueCount += valueCount;
+            this.pageCount += 1;
+
+            mergeColumnStatistics(statistics, sizeStatistics, 
geospatialStatistics);
+            offsetIndexBuilder.add(
+                    toIntWithCheck(tempOutputStream.size() + compressedSize),
+                    rowCount,
+                    sizeStatistics != null
+                            ? sizeStatistics.getUnencodedByteArrayDataBytes()
+                            : Optional.empty());
+
+            buf.collect(BytesInput.concat(BytesInput.from(tempOutputStream), 
compressedBytes));
+            rlEncodings.add(rlEncoding);
+            dlEncodings.add(dlEncoding);
+            dataEncodings.add(valuesEncoding);
+        }
+
+        @Override
+        public void writePageV2(
+                int rowCount,
+                int nullCount,
+                int valueCount,
+                BytesInput repetitionLevels,
+                BytesInput definitionLevels,
+                Encoding dataEncoding,
+                BytesInput data,
+                Statistics<?> statistics)
+                throws IOException {
+            writePageV2(
+                    rowCount,
+                    nullCount,
+                    valueCount,
+                    repetitionLevels,
+                    definitionLevels,
+                    dataEncoding,
+                    data,
+                    statistics,
+                    null,
+                    null);
+        }
+
+        @Override
+        public void writePageV2(
+                int rowCount,
+                int nullCount,
+                int valueCount,
+                BytesInput repetitionLevels,
+                BytesInput definitionLevels,
+                Encoding dataEncoding,
+                BytesInput data,
+                Statistics<?> statistics,
+                SizeStatistics sizeStatistics,
+                GeospatialStatistics geospatialStatistics)
+                throws IOException {
+            pageOrdinal++;
+
+            int rlByteLength = toIntWithCheck(repetitionLevels.size());
+            int dlByteLength = toIntWithCheck(definitionLevels.size());
+            int uncompressedSize =
+                    toIntWithCheck(data.size() + repetitionLevels.size() + 
definitionLevels.size());
+            boolean compressed = false;
+            BytesInput compressedData = BytesInput.empty();
+            if (data.size() > 0) {
+                compressedData = compressor.compress(data);
+                compressed = true;
+            }
+            if (pageBlockEncryptor != null) {
+                AesCipher.quickUpdatePageAAD(dataPageAAD, pageOrdinal);
+                compressedData =
+                        BytesInput.from(
+                                pageBlockEncryptor.encrypt(
+                                        compressedData.toByteArray(), 
dataPageAAD));
+            }
+            int compressedSize =
+                    toIntWithCheck(
+                            compressedData.size()
+                                    + repetitionLevels.size()
+                                    + definitionLevels.size());
+            tempOutputStream.reset();
+            if (headerBlockEncryptor != null) {
+                AesCipher.quickUpdatePageAAD(dataPageHeaderAAD, pageOrdinal);
+            }
+            if (pageWriteChecksumEnabled) {
+                crc.reset();
+                if (repetitionLevels.size() > 0) {
+                    crc.update(repetitionLevels.toByteArray());
+                }
+                if (definitionLevels.size() > 0) {
+                    crc.update(definitionLevels.toByteArray());
+                }
+                if (compressedData.size() > 0) {
+                    crc.update(compressedData.toByteArray());
+                }
+                PARQUET_METADATA_CONVERTER.writeDataPageV2Header(
+                        uncompressedSize,
+                        compressedSize,
+                        valueCount,
+                        nullCount,
+                        rowCount,
+                        dataEncoding,
+                        rlByteLength,
+                        dlByteLength,
+                        compressed,
+                        (int) crc.getValue(),
+                        tempOutputStream,
+                        headerBlockEncryptor,
+                        dataPageHeaderAAD);
+            } else {
+                PARQUET_METADATA_CONVERTER.writeDataPageV2Header(
+                        uncompressedSize,
+                        compressedSize,
+                        valueCount,
+                        nullCount,
+                        rowCount,
+                        dataEncoding,
+                        rlByteLength,
+                        dlByteLength,
+                        compressed,
+                        tempOutputStream,
+                        headerBlockEncryptor,
+                        dataPageHeaderAAD);
+            }
+            this.uncompressedLength += uncompressedSize;
+            this.compressedLength += compressedSize;
+            this.totalValueCount += valueCount;
+            this.pageCount += 1;
+
+            mergeColumnStatistics(statistics, sizeStatistics, 
geospatialStatistics);
+            offsetIndexBuilder.add(
+                    toIntWithCheck((long) tempOutputStream.size() + 
compressedSize),
+                    rowCount,
+                    sizeStatistics != null
+                            ? sizeStatistics.getUnencodedByteArrayDataBytes()
+                            : Optional.empty());
+
+            buf.collect(
+                    BytesInput.concat(
+                            BytesInput.from(tempOutputStream),
+                            repetitionLevels,
+                            definitionLevels,
+                            compressedData));
+            dataEncodings.add(dataEncoding);
+        }
+
+        private int toIntWithCheck(long size) {
+            if (size > Integer.MAX_VALUE) {
+                throw new ParquetEncodingException(
+                        "Cannot write page larger than " + Integer.MAX_VALUE + 
" bytes: " + size);
+            }
+            return (int) size;
+        }
+
+        private void mergeColumnStatistics(
+                Statistics<?> statistics,
+                SizeStatistics sizeStatistics,
+                GeospatialStatistics geospatialStatistics) {
+            totalSizeStatistics.mergeStatistics(sizeStatistics);
+            if (!totalSizeStatistics.isValid()) {
+                sizeStatistics = null;
+            }
+
+            totalGeospatialStatistics.merge(geospatialStatistics);
+
+            if (totalStatistics != null && totalStatistics.isEmpty()) {
+                return;
+            }
+
+            if (statistics == null || statistics.isEmpty()) {
+                totalStatistics = 
Statistics.getBuilderForReading(path.getPrimitiveType()).build();
+                columnIndexBuilder = ColumnIndexBuilder.getNoOpBuilder();
+            } else if (totalStatistics == null) {
+                totalStatistics = statistics.copy();
+                columnIndexBuilder.add(statistics, sizeStatistics);
+            } else {
+                totalStatistics.mergeStatistics(statistics);
+                columnIndexBuilder.add(statistics, sizeStatistics);
+            }
+        }
+
+        @Override
+        public long getMemSize() {
+            return buf.size();
+        }
+
+        public void writeToFileWriter(ParquetFileWriter writer) throws 
IOException {
+            if (headerBlockEncryptor == null) {
+                writer.writeColumnChunk(
+                        path,
+                        totalValueCount,
+                        compressor.getCodecName(),
+                        dictionaryPage,
+                        buf,
+                        uncompressedLength,
+                        compressedLength,
+                        totalStatistics,
+                        totalSizeStatistics,
+                        totalGeospatialStatistics,
+                        columnIndexBuilder,
+                        offsetIndexBuilder,
+                        bloomFilter,
+                        rlEncodings,
+                        dlEncodings,
+                        dataEncodings);
+            } else {
+                writer.writeColumnChunk(
+                        path,
+                        totalValueCount,
+                        compressor.getCodecName(),
+                        dictionaryPage,
+                        buf,
+                        uncompressedLength,
+                        compressedLength,
+                        totalStatistics,
+                        totalSizeStatistics,
+                        totalGeospatialStatistics,
+                        columnIndexBuilder,
+                        offsetIndexBuilder,
+                        bloomFilter,
+                        rlEncodings,
+                        dlEncodings,
+                        dataEncodings,
+                        headerBlockEncryptor,
+                        rowGroupOrdinal,
+                        columnOrdinal,
+                        fileAAD);
+            }
+            if (LOG.isDebugEnabled()) {
+                LOG.debug(
+                        String.format(
+                                        "written %,dB for %s: %,d values, %,dB 
raw, %,dB comp, %d pages, encodings: %s",
+                                        buf.size(),
+                                        path,
+                                        totalValueCount,
+                                        uncompressedLength,
+                                        compressedLength,
+                                        pageCount,
+                                        new HashSet<>(dataEncodings))
+                                + (dictionaryPage != null
+                                        ? String.format(
+                                                ", dic { %,d entries, %,dB 
raw, %,dB comp}",
+                                                
dictionaryPage.getDictionarySize(),
+                                                
dictionaryPage.getUncompressedSize(),
+                                                
dictionaryPage.getDictionarySize())
+                                        : ""));
+            }
+            rlEncodings.clear();
+            dlEncodings.clear();
+            dataEncodings.clear();
+            pageCount = 0;
+            pageOrdinal = -1;
+        }
+
+        @Override
+        public long allocatedSize() {
+            return buf.size();
+        }
+
+        @Override
+        public void writeDictionaryPage(DictionaryPage dictionaryPage) throws 
IOException {
+            if (this.dictionaryPage != null) {
+                throw new ParquetEncodingException("Only one dictionary page 
is allowed");
+            }
+            BytesInput dictionaryBytes = dictionaryPage.getBytes();
+            int uncompressedSize = (int) dictionaryBytes.size();
+            BytesInput compressedBytes = compressor.compress(dictionaryBytes);
+            if (pageBlockEncryptor != null) {
+                byte[] dictionaryPageAAD =
+                        AesCipher.createModuleAAD(
+                                fileAAD,
+                                ModuleType.DictionaryPage,
+                                rowGroupOrdinal,
+                                columnOrdinal,
+                                -1);
+                compressedBytes =
+                        BytesInput.from(
+                                pageBlockEncryptor.encrypt(
+                                        compressedBytes.toByteArray(), 
dictionaryPageAAD));
+            }
+            this.dictionaryPage =
+                    new DictionaryPage(
+                            compressedBytes.copy(releaser),
+                            uncompressedSize,
+                            dictionaryPage.getDictionarySize(),
+                            dictionaryPage.getEncoding());
+        }
+
+        @Override
+        public String memUsageString(String prefix) {
+            return buf.memUsageString(prefix + " ColumnChunkPageWriter");
+        }
+
+        @Override
+        public void close() {
+            AutoCloseables.uncheckedClose(buf, releaser);
+        }
+
+        @Override
+        public void writeBloomFilter(BloomFilter bloomFilter) {
+            this.bloomFilter = bloomFilter;
+        }
+    }
+
+    private final Map<ColumnDescriptor, ColumnChunkPageWriter> writers = new 
HashMap<>();
+    private final MessageType schema;
+
+    public ColumnCompressionPageWriteStore(
+            CompressionCodecFactory codecFactory,
+            ColumnCompressionCodecs compressionCodecs,
+            MessageType schema,
+            ByteBufferAllocator allocator,
+            int columnIndexTruncateLength,
+            boolean pageWriteChecksumEnabled,
+            InternalFileEncryptor fileEncryptor,
+            int rowGroupOrdinal) {
+        this.schema = schema;
+        if (fileEncryptor == null) {
+            for (ColumnDescriptor path : schema.getColumns()) {
+                writers.put(
+                        path,
+                        new ColumnChunkPageWriter(
+                                path,
+                                
codecFactory.getCompressor(compressionCodecs.getCodec(path)),
+                                allocator,
+                                columnIndexTruncateLength,
+                                pageWriteChecksumEnabled,
+                                null,
+                                null,
+                                null,
+                                -1,
+                                -1));
+            }
+            return;
+        }
+
+        int columnOrdinal = -1;
+        byte[] fileAAD = fileEncryptor.getFileAAD();
+        for (ColumnDescriptor path : schema.getColumns()) {
+            columnOrdinal++;
+            BlockCipher.Encryptor headerBlockEncryptor = null;
+            BlockCipher.Encryptor pageBlockEncryptor = null;
+            ColumnPath columnPath = ColumnPath.get(path.getPath());
+
+            InternalColumnEncryptionSetup columnSetup =
+                    fileEncryptor.getColumnSetup(columnPath, true, 
columnOrdinal);
+            if (columnSetup.isEncrypted()) {
+                headerBlockEncryptor = columnSetup.getMetaDataEncryptor();
+                pageBlockEncryptor = columnSetup.getDataEncryptor();
+            }
+
+            writers.put(
+                    path,
+                    new ColumnChunkPageWriter(
+                            path,
+                            
codecFactory.getCompressor(compressionCodecs.getCodec(path)),
+                            allocator,
+                            columnIndexTruncateLength,
+                            pageWriteChecksumEnabled,
+                            headerBlockEncryptor,
+                            pageBlockEncryptor,
+                            fileAAD,
+                            rowGroupOrdinal,
+                            columnOrdinal));
+        }
+    }
+
+    @Override
+    public PageWriter getPageWriter(ColumnDescriptor path) {
+        return writers.get(path);
+    }
+
+    @Override
+    public void close() {
+        AutoCloseables.uncheckedClose(writers.values());
+        writers.clear();
+    }
+
+    @Override
+    public BloomFilterWriter getBloomFilterWriter(ColumnDescriptor path) {
+        return writers.get(path);
+    }
+
+    public void flushToFileWriter(ParquetFileWriter writer) throws IOException 
{
+        for (ColumnDescriptor path : schema.getColumns()) {
+            ColumnChunkPageWriter pageWriter = writers.get(path);
+            pageWriter.writeToFileWriter(writer);
+        }
+    }
+}
diff --git 
a/paimon-format/src/main/java/org/apache/parquet/hadoop/ColumnCompressionRecordWriter.java
 
b/paimon-format/src/main/java/org/apache/parquet/hadoop/ColumnCompressionRecordWriter.java
new file mode 100644
index 0000000000..d8afcf4a43
--- /dev/null
+++ 
b/paimon-format/src/main/java/org/apache/parquet/hadoop/ColumnCompressionRecordWriter.java
@@ -0,0 +1,242 @@
+/*
+ * 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.parquet.hadoop;
+
+import org.apache.parquet.column.ColumnWriteStore;
+import org.apache.parquet.column.ParquetProperties;
+import org.apache.parquet.column.values.bloomfilter.BloomFilterWriteStore;
+import org.apache.parquet.compression.CompressionCodecFactory;
+import org.apache.parquet.crypto.InternalFileEncryptor;
+import org.apache.parquet.hadoop.api.WriteSupport;
+import org.apache.parquet.hadoop.api.WriteSupport.FinalizedWriteContext;
+import org.apache.parquet.hadoop.metadata.ParquetMetadata;
+import org.apache.parquet.io.ColumnIOFactory;
+import org.apache.parquet.io.MessageColumnIO;
+import org.apache.parquet.io.api.RecordConsumer;
+import org.apache.parquet.schema.MessageType;
+import org.apache.parquet.util.AutoCloseables;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+/**
+ * Record writer with per-column compression.
+ *
+ * <p>NOTE: The file is adapted from Apache parquet's 
InternalParquetRecordWriter.
+ */
+class ColumnCompressionRecordWriter<T> {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ColumnCompressionRecordWriter.class);
+
+    private final ParquetFileWriter parquetFileWriter;
+    private final WriteSupport<T> writeSupport;
+    private final MessageType schema;
+    private final Map<String, String> extraMetaData;
+    private long rowGroupSizeThreshold;
+    private final int rowGroupRecordCountThreshold;
+    private long nextRowGroupSize;
+    private final CompressionCodecFactory codecFactory;
+    private final ColumnCompressionCodecs compressionCodecs;
+    private final boolean validating;
+    private final ParquetProperties props;
+
+    private boolean closed;
+
+    private long recordCount = 0;
+    private long recordCountForNextMemCheck;
+    private long lastRowGroupEndPos = 0;
+
+    private ColumnWriteStore columnStore;
+    private ColumnCompressionPageWriteStore pageStore;
+    private BloomFilterWriteStore bloomFilterWriteStore;
+    private RecordConsumer recordConsumer;
+
+    private InternalFileEncryptor fileEncryptor;
+    private int rowGroupOrdinal;
+    private boolean aborted;
+
+    ColumnCompressionRecordWriter(
+            ParquetFileWriter parquetFileWriter,
+            WriteSupport<T> writeSupport,
+            MessageType schema,
+            Map<String, String> extraMetaData,
+            long rowGroupSize,
+            CompressionCodecFactory codecFactory,
+            ColumnCompressionCodecs compressionCodecs,
+            boolean validating,
+            ParquetProperties props) {
+        this.parquetFileWriter = parquetFileWriter;
+        this.writeSupport = Objects.requireNonNull(writeSupport, "writeSupport 
cannot be null");
+        this.schema = schema;
+        this.extraMetaData = extraMetaData;
+        this.rowGroupSizeThreshold = rowGroupSize;
+        this.rowGroupRecordCountThreshold = props.getRowGroupRowCountLimit();
+        this.nextRowGroupSize = rowGroupSizeThreshold;
+        this.codecFactory = codecFactory;
+        this.compressionCodecs = compressionCodecs;
+        this.validating = validating;
+        this.props = props;
+        this.fileEncryptor = parquetFileWriter.getEncryptor();
+        this.rowGroupOrdinal = 0;
+        initStore();
+        recordCountForNextMemCheck = props.getMinRowCountForPageSizeCheck();
+    }
+
+    public ParquetMetadata getFooter() {
+        return parquetFileWriter.getFooter();
+    }
+
+    private void initStore() {
+        ColumnCompressionPageWriteStore columnCompressionPageWriteStore =
+                new ColumnCompressionPageWriteStore(
+                        codecFactory,
+                        compressionCodecs,
+                        schema,
+                        props.getAllocator(),
+                        props.getColumnIndexTruncateLength(),
+                        props.getPageWriteChecksumEnabled(),
+                        fileEncryptor,
+                        rowGroupOrdinal);
+        pageStore = columnCompressionPageWriteStore;
+        bloomFilterWriteStore = columnCompressionPageWriteStore;
+
+        columnStore = props.newColumnWriteStore(schema, pageStore, 
bloomFilterWriteStore);
+        MessageColumnIO columnIO = new 
ColumnIOFactory(validating).getColumnIO(schema);
+        this.recordConsumer = columnIO.getRecordWriter(columnStore);
+        writeSupport.prepareForWrite(recordConsumer);
+    }
+
+    public void close() throws IOException, InterruptedException {
+        if (!closed) {
+            try {
+                if (aborted) {
+                    return;
+                }
+                flushRowGroupToStore();
+                FinalizedWriteContext finalWriteContext = 
writeSupport.finalizeWrite();
+                Map<String, String> finalMetadata = new 
HashMap<>(extraMetaData);
+                String modelName = writeSupport.getName();
+                if (modelName != null) {
+                    finalMetadata.put(ParquetWriter.OBJECT_MODEL_NAME_PROP, 
modelName);
+                }
+                finalMetadata.putAll(finalWriteContext.getExtraMetaData());
+                parquetFileWriter.end(finalMetadata);
+            } finally {
+                AutoCloseables.uncheckedClose(
+                        columnStore, pageStore, bloomFilterWriteStore, 
parquetFileWriter);
+                closed = true;
+            }
+        }
+    }
+
+    public void write(T value) throws IOException, InterruptedException {
+        try {
+            writeSupport.write(value);
+            ++recordCount;
+            checkBlockSizeReached();
+        } catch (Throwable t) {
+            aborted = true;
+            throw t;
+        }
+    }
+
+    public long getDataSize() {
+        return lastRowGroupEndPos + columnStore.getBufferedSize();
+    }
+
+    private void checkBlockSizeReached() throws IOException {
+        if (recordCount >= rowGroupRecordCountThreshold) {
+            LOG.debug("record count reaches threshold: flushing {} records to 
disk.", recordCount);
+            flushRowGroupToStore();
+            initStore();
+            recordCountForNextMemCheck =
+                    min(
+                            max(props.getMinRowCountForPageSizeCheck(), 
recordCount / 2),
+                            props.getMaxRowCountForPageSizeCheck());
+            this.lastRowGroupEndPos = parquetFileWriter.getPos();
+        } else if (recordCount >= recordCountForNextMemCheck) {
+            long memSize = columnStore.getBufferedSize();
+            long recordSize = memSize / recordCount;
+            if (memSize > (nextRowGroupSize - 2 * recordSize)) {
+                LOG.debug(
+                        "mem size {} > {}: flushing {} records to disk.",
+                        memSize,
+                        nextRowGroupSize,
+                        recordCount);
+                flushRowGroupToStore();
+                initStore();
+                recordCountForNextMemCheck =
+                        min(
+                                max(props.getMinRowCountForPageSizeCheck(), 
recordCount / 2),
+                                props.getMaxRowCountForPageSizeCheck());
+                this.lastRowGroupEndPos = parquetFileWriter.getPos();
+            } else {
+                recordCountForNextMemCheck =
+                        min(
+                                max(
+                                        props.getMinRowCountForPageSizeCheck(),
+                                        (recordCount
+                                                        + (long)
+                                                                
(nextRowGroupSize
+                                                                        / 
((float) recordSize)))
+                                                / 2),
+                                recordCount + 
props.getMaxRowCountForPageSizeCheck());
+                LOG.debug(
+                        "Checked mem at {} will check again at: {}",
+                        recordCount,
+                        recordCountForNextMemCheck);
+            }
+        }
+    }
+
+    private void flushRowGroupToStore() throws IOException {
+        try {
+            recordConsumer.flush();
+            LOG.debug(
+                    "Flushing mem columnStore to file. allocated memory: {}",
+                    columnStore.getAllocatedSize());
+            if (columnStore.getAllocatedSize() > (3 * rowGroupSizeThreshold)) {
+                LOG.warn("Too much memory used: {}", 
columnStore.memUsageString());
+            }
+
+            if (recordCount > 0) {
+                rowGroupOrdinal++;
+                parquetFileWriter.startBlock(recordCount);
+                columnStore.flush();
+                pageStore.flushToFileWriter(parquetFileWriter);
+                recordCount = 0;
+                parquetFileWriter.endBlock();
+                this.nextRowGroupSize =
+                        Math.min(parquetFileWriter.getNextRowGroupSize(), 
rowGroupSizeThreshold);
+            }
+        } finally {
+            AutoCloseables.uncheckedClose(columnStore, pageStore, 
bloomFilterWriteStore);
+            columnStore = null;
+            pageStore = null;
+            bloomFilterWriteStore = null;
+        }
+    }
+}
diff --git 
a/paimon-format/src/main/java/org/apache/parquet/hadoop/ParquetWriter.java 
b/paimon-format/src/main/java/org/apache/parquet/hadoop/ParquetWriter.java
index 3358e84fdc..ee092bbfb9 100644
--- a/paimon-format/src/main/java/org/apache/parquet/hadoop/ParquetWriter.java
+++ b/paimon-format/src/main/java/org/apache/parquet/hadoop/ParquetWriter.java
@@ -26,6 +26,7 @@ import 
org.apache.parquet.column.ParquetProperties.WriterVersion;
 import org.apache.parquet.compression.CompressionCodecFactory;
 import org.apache.parquet.crypto.FileEncryptionProperties;
 import org.apache.parquet.hadoop.api.WriteSupport;
+import org.apache.parquet.hadoop.metadata.ColumnPath;
 import org.apache.parquet.hadoop.metadata.CompressionCodecName;
 import org.apache.parquet.hadoop.metadata.ParquetMetadata;
 import org.apache.parquet.io.OutputFile;
@@ -58,7 +59,7 @@ public class ParquetWriter<T> implements Closeable {
     // max size (bytes) to write as padding and the min size of a row group
     public static final int MAX_PADDING_SIZE_DEFAULT = 8 * 1024 * 1024; // 8MB
 
-    private final InternalParquetRecordWriter<T> writer;
+    private final ColumnCompressionRecordWriter<T> writer;
     private final CompressionCodecFactory codecFactory;
 
     ParquetWriter(
@@ -67,6 +68,7 @@ public class ParquetWriter<T> implements Closeable {
             WriteSupport<T> writeSupport,
             CompressionCodecName compressionCodecName,
             CompressionCodecFactory codecFactory,
+            Map<ColumnPath, CompressionCodecName> columnCompressionCodecNames,
             long rowGroupSize,
             boolean validating,
             Configuration conf,
@@ -99,8 +101,6 @@ public class ParquetWriter<T> implements Closeable {
         fileWriter.start();
 
         this.codecFactory = codecFactory;
-        CompressionCodecFactory.BytesInputCompressor compressor =
-                codecFactory.getCompressor(compressionCodecName);
 
         final Map<String, String> extraMetadata;
         if (encodingProps.getExtraMetaData() == null
@@ -130,13 +130,15 @@ public class ParquetWriter<T> implements Closeable {
         }
 
         this.writer =
-                new InternalParquetRecordWriter<T>(
+                new ColumnCompressionRecordWriter<T>(
                         fileWriter,
                         writeSupport,
                         schema,
                         extraMetadata,
                         rowGroupSize,
-                        compressor,
+                        codecFactory,
+                        new ColumnCompressionCodecs(
+                                compressionCodecName, 
columnCompressionCodecNames),
                         validating,
                         encodingProps);
     }
@@ -188,6 +190,7 @@ public class ParquetWriter<T> implements Closeable {
         private ParquetFileWriter.Mode mode;
         private CompressionCodecFactory codecFactory = null;
         private CompressionCodecName codecName = 
DEFAULT_COMPRESSION_CODEC_NAME;
+        private final Map<ColumnPath, CompressionCodecName> columnCodecNames = 
new HashMap<>();
         private long rowGroupSize = DEFAULT_BLOCK_SIZE;
         private int maxPaddingSize = MAX_PADDING_SIZE_DEFAULT;
         private boolean enableValidation = DEFAULT_IS_VALIDATING_ENABLED;
@@ -240,6 +243,18 @@ public class ParquetWriter<T> implements Closeable {
             return self();
         }
 
+        /**
+         * Set the compression codec used by the specified column for the 
constructed writer.
+         *
+         * @param columnPath the path of the column (dot-string)
+         * @param codecName a {@code CompressionCodecName}
+         * @return this builder for method chaining.
+         */
+        public SELF withCompressionCodec(String columnPath, 
CompressionCodecName codecName) {
+            this.columnCodecNames.put(ColumnPath.fromDotString(columnPath), 
codecName);
+            return self();
+        }
+
         /**
          * Set the {@link CompressionCodecFactory codec factory} used by the 
constructed writer.
          *
@@ -658,6 +673,7 @@ public class ParquetWriter<T> implements Closeable {
                     getWriteSupport(conf),
                     codecName,
                     codecFactory,
+                    columnCodecNames,
                     rowGroupSize,
                     enableValidation,
                     conf,
diff --git 
a/paimon-format/src/test/java/org/apache/paimon/format/parquet/ParquetFormatReadWriteTest.java
 
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/ParquetFormatReadWriteTest.java
index 0f71fbbcd7..137da29aa6 100644
--- 
a/paimon-format/src/test/java/org/apache/paimon/format/parquet/ParquetFormatReadWriteTest.java
+++ 
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/ParquetFormatReadWriteTest.java
@@ -18,6 +18,7 @@
 
 package org.apache.paimon.format.parquet;
 
+import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.format.FileFormat;
 import org.apache.paimon.format.FileFormatFactory;
@@ -32,12 +33,16 @@ import 
org.apache.parquet.column.values.bloomfilter.BloomFilter;
 import org.apache.parquet.hadoop.ParquetFileReader;
 import org.apache.parquet.hadoop.metadata.BlockMetaData;
 import org.apache.parquet.hadoop.metadata.ColumnChunkMetaData;
+import org.apache.parquet.hadoop.metadata.CompressionCodecName;
 import org.apache.parquet.hadoop.metadata.ParquetMetadata;
 import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ThreadLocalRandom;
 
 /** A parquet {@link FormatReadWriteTest}. */
@@ -89,4 +94,42 @@ public class ParquetFormatReadWriteTest extends 
FormatReadWriteTest {
             }
         }
     }
+
+    @Test
+    public void testColumnCompressionCodec() throws Exception {
+        Options options = new Options();
+        options.set("parquet.compression#name", "none");
+        ParquetFileFormat format =
+                new ParquetFileFormat(new 
FileFormatFactory.FormatContext(options, 1024, 1024));
+
+        RowType rowType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "id", DataTypes.INT()),
+                        DataTypes.FIELD(1, "name", DataTypes.STRING()));
+
+        PositionOutputStream out = fileIO.newOutputStream(file, false);
+        FormatWriter writer = format.createWriterFactory(rowType).create(out, 
"zstd");
+        writer.addElement(GenericRow.of(1, BinaryString.fromString("one")));
+        writer.addElement(GenericRow.of(2, BinaryString.fromString("two")));
+        writer.addElement(GenericRow.of(3, BinaryString.fromString("three")));
+        writer.close();
+        out.close();
+
+        try (ParquetFileReader reader =
+                ParquetUtil.getParquetReader(
+                        fileIO, file, fileIO.getFileSize(file), new 
Options())) {
+            Map<String, CompressionCodecName> codecs = new HashMap<>();
+            for (BlockMetaData blockMetaData : reader.getFooter().getBlocks()) 
{
+                for (ColumnChunkMetaData columnChunkMetaData : 
blockMetaData.getColumns()) {
+                    codecs.put(
+                            columnChunkMetaData.getPath().toDotString(),
+                            columnChunkMetaData.getCodec());
+                }
+            }
+
+            Assertions.assertThat(codecs)
+                    .containsEntry("id", CompressionCodecName.ZSTD)
+                    .containsEntry("name", CompressionCodecName.UNCOMPRESSED);
+        }
+    }
 }

Reply via email to