mirror of
https://github.com/apache/doris.git
synced 2026-11-04 07:31:17 +00:00
[fix](iceberg)Support LZ4 compression for iceberg/hive Parquet/ORC writers. (#64723)
This commit is contained in:
1 parent
456aa02b7a
commit
85ea37d4a2
11 files changed
+275
-3
No files matched your search
@@ -93,6 +93,10 @@ Status VIcebergDeleteFileWriter::open(RuntimeState* state, RuntimeProfile* profi
|
||||
case TFileCompressType::ZSTD:
|
||||
parquet_compression_type = TParquetCompressionType::ZSTD;
|
||||
break;
|
||||
case TFileCompressType::LZ4BLOCK:
|
||||
// Hadoop-framed Parquet LZ4 (not LZ4_RAW) for cross-engine compatibility.
|
||||
parquet_compression_type = TParquetCompressionType::LZ4_HADOOP;
|
||||
break;
|
||||
default:
|
||||
return Status::InternalError("Unsupported compress type {} with parquet",
|
||||
to_string(_compress_type));
|
||||
|
||||
@@ -80,6 +80,12 @@ Status VIcebergPartitionWriter::open(RuntimeState* state, RuntimeProfile* profil
|
||||
parquet_compression_type = TParquetCompressionType::ZSTD;
|
||||
break;
|
||||
}
|
||||
case TFileCompressType::LZ4BLOCK: {
|
||||
// Map Doris LZ4 to the Hadoop-framed Parquet LZ4 codec (not LZ4_RAW) so the file
|
||||
// stays readable across Spark/Iceberg/Trino. See ParquetBuildHelper.
|
||||
parquet_compression_type = TParquetCompressionType::LZ4_HADOOP;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return Status::InternalError("Unsupported compress type {} with parquet",
|
||||
to_string(_compress_type));
|
||||
@@ -179,6 +185,10 @@ std::string VIcebergPartitionWriter::_get_file_extension(
|
||||
compress_name = ".zstd";
|
||||
break;
|
||||
}
|
||||
case TFileCompressType::LZ4BLOCK: {
|
||||
compress_name = ".lz4";
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
compress_name = "";
|
||||
break;
|
||||
|
||||
@@ -83,6 +83,11 @@ Status VHivePartitionWriter::open(RuntimeState* state, RuntimeProfile* operator_
|
||||
parquet_compression_type = TParquetCompressionType::ZSTD;
|
||||
break;
|
||||
}
|
||||
case TFileCompressType::LZ4BLOCK: {
|
||||
// Hadoop-framed Parquet LZ4 (not LZ4_RAW) for cross-engine compatibility.
|
||||
parquet_compression_type = TParquetCompressionType::LZ4_HADOOP;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return Status::InternalError("Unsupported hive compress type {} with parquet",
|
||||
to_string(_hive_compress_type));
|
||||
|
||||
@@ -183,6 +183,13 @@ void VOrcTransformer::set_compression_type(const TFileCompressType::type& compre
|
||||
_write_options->setCompression(orc::CompressionKind::CompressionKind_ZSTD);
|
||||
break;
|
||||
}
|
||||
case TFileCompressType::LZ4BLOCK: {
|
||||
// ORC has a single, unambiguous LZ4 codec (raw LZ4 inside ORC's own compression
|
||||
// framing), interoperable with Spark/Trino. Honor the requested codec instead of
|
||||
// silently falling back to ZLIB below.
|
||||
_write_options->setCompression(orc::CompressionKind::CompressionKind_LZ4);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
_write_options->setCompression(orc::CompressionKind::CompressionKind_ZLIB);
|
||||
}
|
||||
|
||||
@@ -123,6 +123,17 @@ void ParquetBuildHelper::build_compression_type(
|
||||
builder.compression(arrow::Compression::LZ4);
|
||||
break;
|
||||
}
|
||||
case TParquetCompressionType::LZ4_HADOOP: {
|
||||
constexpr int64_t HADOOP_LZ4_DEFAULT_BUFFER_SIZE = 256 * 1024;
|
||||
// Hadoop-framed LZ4 -> Parquet thrift codec "LZ4" (deprecated). This matches what
|
||||
// Spark/Iceberg writes for `write.parquet.compression-codec=lz4`. Arrow 17 emits one
|
||||
// Hadoop LZ4 block per Parquet page/dictionary page, while Hadoop JVM readers default to
|
||||
// a 256 KiB LZ4 codec buffer, so keep page targets below that buffer size.
|
||||
builder.compression(arrow::Compression::LZ4_HADOOP);
|
||||
builder.data_pagesize(HADOOP_LZ4_DEFAULT_BUFFER_SIZE / 2);
|
||||
builder.dictionary_pagesize_limit(HADOOP_LZ4_DEFAULT_BUFFER_SIZE / 2);
|
||||
break;
|
||||
}
|
||||
// arrow do not support lzo and bz2 compression type.
|
||||
// case TParquetCompressionType::LZO: {
|
||||
// builder.compression(arrow::Compression::LZO);
|
||||
|
||||
@@ -64,8 +64,10 @@ import java.util.stream.Collectors;
|
||||
public final class HiveUtil {
|
||||
|
||||
public static final String COMPRESSION_KEY = "compression";
|
||||
public static final Set<String> SUPPORTED_ORC_COMPRESSIONS = ImmutableSet.of("plain", "zlib", "snappy", "zstd");
|
||||
public static final Set<String> SUPPORTED_PARQUET_COMPRESSIONS = ImmutableSet.of("plain", "snappy", "zstd");
|
||||
public static final Set<String> SUPPORTED_ORC_COMPRESSIONS =
|
||||
ImmutableSet.of("plain", "zlib", "snappy", "zstd", "lz4");
|
||||
public static final Set<String> SUPPORTED_PARQUET_COMPRESSIONS =
|
||||
ImmutableSet.of("plain", "snappy", "zstd", "lz4");
|
||||
public static final Set<String> SUPPORTED_TEXT_COMPRESSIONS =
|
||||
ImmutableSet.of("plain", "gzip", "zstd", "bzip2", "lz4", "snappy");
|
||||
|
||||
|
||||
@@ -63,6 +63,11 @@ enum TParquetCompressionType {
|
||||
LZO = 5,
|
||||
BZ2 = 6,
|
||||
UNCOMPRESSED = 7,
|
||||
// Hadoop-framed (deprecated Parquet "LZ4") codec. Distinct from LZ4 above, which maps to
|
||||
// Arrow LZ4_RAW. Used by the Iceberg/Hive Parquet writers so the output stays readable by
|
||||
// engines that support only the Hadoop-framed LZ4 codec (e.g. Trino), matching what
|
||||
// Spark/Iceberg writes for `write.parquet.compression-codec=lz4`.
|
||||
LZ4_HADOOP = 8,
|
||||
}
|
||||
|
||||
enum TParquetVersion {
|
||||
|
||||
@@ -29,6 +29,12 @@ true 1 1000 2.3
|
||||
true 1 1000 2.3
|
||||
true 1 1000 2.3
|
||||
|
||||
-- !hive_parquet_lz4_write --
|
||||
doris_lz4
|
||||
|
||||
-- !hive_parquet_lz4_footer_codec --
|
||||
LZ4
|
||||
|
||||
-- !insert01 --
|
||||
true 123 9876543210 abcdefghij 3.14 6.28 123.4567 varcharval stringval
|
||||
|
||||
@@ -59,3 +65,6 @@ true 1 1000 2.3
|
||||
true 1 1000 2.3
|
||||
true 1 1000 2.3
|
||||
|
||||
-- !hive_orc_lz4_write --
|
||||
doris_lz4
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
-- This file is automatically generated. You should know what you did if you want to edit this
|
||||
-- !iceberg_parquet_lz4_data --
|
||||
doris0 0
|
||||
doris1 1
|
||||
doris2 2
|
||||
|
||||
-- !iceberg_parquet_snappy_data --
|
||||
doris0 0
|
||||
doris1 1
|
||||
doris2 2
|
||||
|
||||
-- !iceberg_parquet_zstd_data --
|
||||
doris0 0
|
||||
doris1 1
|
||||
doris2 2
|
||||
|
||||
-- !iceberg_parquet_uncompressed_data --
|
||||
doris0 0
|
||||
doris1 1
|
||||
doris2 2
|
||||
|
||||
-- !iceberg_parquet_lz4_footer_codec --
|
||||
LZ4
|
||||
|
||||
-- !iceberg_orc_lz4_data --
|
||||
doris0 0
|
||||
doris1 1
|
||||
doris2 2
|
||||
|
||||
-- !iceberg_orc_lz4_files --
|
||||
orc 3
|
||||
|
||||
-- !iceberg_parquet_lz4_delete --
|
||||
1
|
||||
|
||||
-- !iceberg_parquet_lz4_delete_data --
|
||||
1 a
|
||||
3 c
|
||||
|
||||
-- !iceberg_parquet_lz4_delete_files --
|
||||
parquet 1
|
||||
|
||||
-- !iceberg_parquet_lz4_delete_footer_codec --
|
||||
LZ4
|
||||
@@ -399,6 +399,26 @@ suite("test_hive_ddl", "p0,external") {
|
||||
throw new Exception("Invalid compression type: ${compression} for tbl_${file_format}_${compression}")
|
||||
}
|
||||
|
||||
if (compression.equals("lz4")) {
|
||||
sql """ INSERT INTO tbl_${file_format}_${compression} VALUES ('doris_lz4') """
|
||||
def q_lz4 = "order_qt_hive_${file_format}_${compression}_write"
|
||||
"${q_lz4}" """ SELECT * FROM tbl_${file_format}_${compression} ORDER BY col """
|
||||
|
||||
if (file_format.equals("parquet")) {
|
||||
String hdfsPort = context.config.otherConfigs.get("hive2HdfsPort")
|
||||
String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
|
||||
order_qt_hive_parquet_lz4_footer_codec """
|
||||
SELECT DISTINCT compression
|
||||
FROM parquet_meta(
|
||||
"uri" = "hdfs://${externalEnvIp}:${hdfsPort}/user/hive/warehouse/test_hive_compress.db/tbl_parquet_lz4/*",
|
||||
"hadoop.username" = "doris",
|
||||
"mode" = "parquet_metadata"
|
||||
)
|
||||
ORDER BY compression
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
sql """DROP TABLE `tbl_${file_format}_${compression}`"""
|
||||
sql """ drop database if exists `test_hive_compress` """;
|
||||
}
|
||||
@@ -734,7 +754,7 @@ suite("test_hive_ddl", "p0,external") {
|
||||
sql """set enable_fallback_to_original_planner=false;"""
|
||||
test_db(catalog_name)
|
||||
test_loc_db(externalEnvIp, hdfs_port, catalog_name)
|
||||
def compressions = ["snappy", "zlib", "zstd"]
|
||||
def compressions = ["snappy", "zlib", "zstd", "lz4"]
|
||||
for (String file_format in file_formats) {
|
||||
logger.info("Process file format " + file_format)
|
||||
test_loc_tbl(file_format, externalEnvIp, hdfs_port, catalog_name)
|
||||
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
// 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.
|
||||
|
||||
// Verifies that Doris writes Iceberg Parquet LZ4 as Hadoop-framed Parquet "LZ4"
|
||||
// (not LZ4_RAW) and that Iceberg ORC/delete-file LZ4 paths are reachable.
|
||||
suite("test_iceberg_write_parquet_compression", "p0,external") {
|
||||
String enabled = context.config.otherConfigs.get("enableIcebergTest")
|
||||
if (enabled == null || !enabled.equalsIgnoreCase("true")) {
|
||||
logger.info("disable iceberg test.")
|
||||
return
|
||||
}
|
||||
|
||||
String rest_port = context.config.otherConfigs.get("iceberg_rest_uri_port")
|
||||
String minio_port = context.config.otherConfigs.get("iceberg_minio_port")
|
||||
String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
|
||||
String catalog_name = "test_iceberg_write_parquet_compression"
|
||||
String db = catalog_name + "_db"
|
||||
|
||||
sql """drop catalog if exists ${catalog_name}"""
|
||||
sql """
|
||||
CREATE CATALOG ${catalog_name} PROPERTIES (
|
||||
'type'='iceberg',
|
||||
'iceberg.catalog.type'='rest',
|
||||
'uri' = 'http://${externalEnvIp}:${rest_port}',
|
||||
"s3.access_key" = "admin",
|
||||
"s3.secret_key" = "password",
|
||||
"s3.endpoint" = "http://${externalEnvIp}:${minio_port}",
|
||||
"s3.region" = "us-east-1"
|
||||
);"""
|
||||
|
||||
sql """ switch ${catalog_name} """
|
||||
sql """ drop database if exists ${db} force """
|
||||
sql """ create database ${db} """
|
||||
|
||||
// Codecs Doris is expected to support for Parquet writes.
|
||||
for (String codec : ["lz4", "snappy", "zstd", "uncompressed"]) {
|
||||
String tbl = "${db}.tbl_${codec}"
|
||||
sql """ drop table if exists ${tbl} """
|
||||
sql """
|
||||
CREATE TABLE ${tbl} (a STRING, b BIGINT) PROPERTIES (
|
||||
'write-format' = 'parquet',
|
||||
'write.format.default' = 'parquet',
|
||||
'format-version' = '2',
|
||||
'write.parquet.compression-codec' = '${codec}'
|
||||
)"""
|
||||
|
||||
sql """ INSERT INTO ${tbl} VALUES ('doris0', 0), ('doris1', 1), ('doris2', 2) """
|
||||
|
||||
def q_data = "order_qt_iceberg_parquet_${codec}_data"
|
||||
"${q_data}" """ SELECT a, b FROM ${tbl} ORDER BY b """
|
||||
}
|
||||
|
||||
def lz4DataFiles = sql """ SELECT file_path FROM ${db}.tbl_lz4\$files ORDER BY file_path """
|
||||
String lz4DataFile = lz4DataFiles[0][0].toString()
|
||||
order_qt_iceberg_parquet_lz4_footer_codec """
|
||||
SELECT DISTINCT compression
|
||||
FROM parquet_meta(
|
||||
"uri" = "${lz4DataFile}",
|
||||
"s3.access_key" = "admin",
|
||||
"s3.secret_key" = "password",
|
||||
"s3.endpoint" = "http://${externalEnvIp}:${minio_port}",
|
||||
"s3.region" = "us-east-1",
|
||||
"mode" = "parquet_metadata"
|
||||
)
|
||||
ORDER BY compression
|
||||
"""
|
||||
|
||||
sql """ DROP TABLE IF EXISTS ${db}.tbl_orc_lz4 """
|
||||
sql """
|
||||
CREATE TABLE ${db}.tbl_orc_lz4 (a STRING, b BIGINT) PROPERTIES (
|
||||
'write-format' = 'orc',
|
||||
'write.format.default' = 'orc',
|
||||
'format-version' = '2',
|
||||
'write.orc.compression-codec' = 'lz4'
|
||||
)"""
|
||||
sql """ INSERT INTO ${db}.tbl_orc_lz4 VALUES ('doris0', 0), ('doris1', 1), ('doris2', 2) """
|
||||
order_qt_iceberg_orc_lz4_data """ SELECT a, b FROM ${db}.tbl_orc_lz4 ORDER BY b """
|
||||
order_qt_iceberg_orc_lz4_files """
|
||||
SELECT lower(file_format), sum(record_count)
|
||||
FROM ${db}.tbl_orc_lz4\$files
|
||||
GROUP BY lower(file_format)
|
||||
ORDER BY 1
|
||||
"""
|
||||
|
||||
sql """ DROP TABLE IF EXISTS ${db}.tbl_lz4_delete """
|
||||
sql """
|
||||
CREATE TABLE ${db}.tbl_lz4_delete (id INT, name STRING) PROPERTIES (
|
||||
'write-format' = 'parquet',
|
||||
'write.format.default' = 'parquet',
|
||||
'format-version' = '2',
|
||||
'write.parquet.compression-codec' = 'lz4',
|
||||
'write.delete.mode' = 'merge-on-read',
|
||||
'write.update.mode' = 'merge-on-read',
|
||||
'write.merge.mode' = 'merge-on-read'
|
||||
)"""
|
||||
sql """ INSERT INTO ${db}.tbl_lz4_delete VALUES (1, 'a'), (2, 'b'), (3, 'c') """
|
||||
qt_iceberg_parquet_lz4_delete """ DELETE FROM ${db}.tbl_lz4_delete WHERE id = 2 """
|
||||
order_qt_iceberg_parquet_lz4_delete_data """
|
||||
SELECT id, name FROM ${db}.tbl_lz4_delete ORDER BY id
|
||||
"""
|
||||
order_qt_iceberg_parquet_lz4_delete_files """
|
||||
SELECT lower(file_format), sum(record_count)
|
||||
FROM ${db}.tbl_lz4_delete\$delete_files
|
||||
GROUP BY lower(file_format)
|
||||
ORDER BY 1
|
||||
"""
|
||||
def lz4DeleteFiles = sql """
|
||||
SELECT file_path
|
||||
FROM ${db}.tbl_lz4_delete\$delete_files
|
||||
ORDER BY file_path
|
||||
"""
|
||||
String lz4DeleteFile = lz4DeleteFiles[0][0].toString()
|
||||
order_qt_iceberg_parquet_lz4_delete_footer_codec """
|
||||
SELECT DISTINCT compression
|
||||
FROM parquet_meta(
|
||||
"uri" = "${lz4DeleteFile}",
|
||||
"s3.access_key" = "admin",
|
||||
"s3.secret_key" = "password",
|
||||
"s3.endpoint" = "http://${externalEnvIp}:${minio_port}",
|
||||
"s3.region" = "us-east-1",
|
||||
"mode" = "parquet_metadata"
|
||||
)
|
||||
ORDER BY compression
|
||||
"""
|
||||
|
||||
// GZIP is intentionally not yet supported by the Doris Parquet writer; the INSERT must
|
||||
// fail explicitly rather than silently fall back. Remove this block when GZIP support is
|
||||
// added (and update the cross-engine product test expectation in tandem).
|
||||
sql """ drop table if exists ${db}.tbl_gzip """
|
||||
sql """
|
||||
CREATE TABLE ${db}.tbl_gzip (a STRING, b BIGINT) PROPERTIES (
|
||||
'write-format' = 'parquet',
|
||||
'write.format.default' = 'parquet',
|
||||
'format-version' = '2',
|
||||
'write.parquet.compression-codec' = 'gzip'
|
||||
)"""
|
||||
test {
|
||||
sql """ INSERT INTO ${db}.tbl_gzip VALUES ('doris0', 0) """
|
||||
exception "Unsupported compress type GZ with parquet"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user