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 be34a5a8b0 [python] Fix missing compression for manifest files (#8376)
be34a5a8b0 is described below
commit be34a5a8b05dc75ffb88b78e49ed8ff084cbd711
Author: XiaoHongbo <[email protected]>
AuthorDate: Mon Jun 29 16:42:44 2026 +0800
[python] Fix missing compression for manifest files (#8376)
---
.../pypaimon/common/options/core_options.py | 10 +++++
paimon-python/pypaimon/manifest/__init__.py | 22 ++++++++++
.../pypaimon/manifest/index_manifest_file.py | 6 ++-
.../pypaimon/manifest/manifest_file_manager.py | 13 ++++--
.../pypaimon/manifest/manifest_list_manager.py | 6 ++-
.../tests/manifest/manifest_manager_test.py | 47 +++++++++++++++++++++-
.../tests/manifest/manifest_schema_test.py | 1 +
7 files changed, 98 insertions(+), 7 deletions(-)
diff --git a/paimon-python/pypaimon/common/options/core_options.py
b/paimon-python/pypaimon/common/options/core_options.py
index c601555464..56e323c997 100644
--- a/paimon-python/pypaimon/common/options/core_options.py
+++ b/paimon-python/pypaimon/common/options/core_options.py
@@ -186,6 +186,13 @@ class CoreOptions:
.with_description("The parallelism for scanning manifest files.")
)
+ MANIFEST_COMPRESSION: ConfigOption[str] = (
+ ConfigOptions.key("manifest.compression")
+ .string_type()
+ .default_value("zstd")
+ .with_description("Default file compression for manifest.")
+ )
+
MANIFEST_TARGET_FILE_SIZE: ConfigOption[MemorySize] = (
ConfigOptions.key("manifest.target-file-size")
.memory_type()
@@ -838,6 +845,9 @@ class CoreOptions:
def scan_manifest_parallelism(self, default=None):
return self.options.get(CoreOptions.SCAN_MANIFEST_PARALLELISM, default)
+ def manifest_compression(self, default=None):
+ return self.options.get(CoreOptions.MANIFEST_COMPRESSION, default)
+
def manifest_target_size(self, default=None):
if default is not None and not isinstance(default, MemorySize):
default = MemorySize.of_bytes(default) if isinstance(default, int)
else MemorySize.parse(default)
diff --git a/paimon-python/pypaimon/manifest/__init__.py
b/paimon-python/pypaimon/manifest/__init__.py
index 8cb828ad1d..88064e3a67 100644
--- a/paimon-python/pypaimon/manifest/__init__.py
+++ b/paimon-python/pypaimon/manifest/__init__.py
@@ -23,3 +23,25 @@ if sys.version_info[:2] == (3, 6):
from pypaimon.manifest import fastavro_py36_compat # noqa: F401
except ImportError:
pass
+
+
+_SUPPORTED_MANIFEST_COMPRESSIONS = {
+ 'null', 'deflate', 'snappy', 'zstd', 'bzip2', 'xz',
+}
+
+
+_DEFAULT_MANIFEST_CODEC = 'zstandard'
+
+
+def avro_codec(compression):
+ """Map Paimon manifest.compression config value to fastavro codec name."""
+ if not isinstance(compression, str):
+ return _DEFAULT_MANIFEST_CODEC
+ lower = compression.lower()
+ if lower not in _SUPPORTED_MANIFEST_COMPRESSIONS:
+ raise ValueError(
+ f"Unsupported manifest compression '{compression}'. "
+ f"Supported: {sorted(_SUPPORTED_MANIFEST_COMPRESSIONS)}")
+ if lower == 'zstd':
+ return 'zstandard'
+ return lower
diff --git a/paimon-python/pypaimon/manifest/index_manifest_file.py
b/paimon-python/pypaimon/manifest/index_manifest_file.py
index a7d82d4b12..62ca9ea16d 100644
--- a/paimon-python/pypaimon/manifest/index_manifest_file.py
+++ b/paimon-python/pypaimon/manifest/index_manifest_file.py
@@ -85,12 +85,14 @@ class IndexManifestFile:
def __init__(self, table):
from pypaimon.table.file_store_table import FileStoreTable
+ from pypaimon.manifest import avro_codec
self.table: FileStoreTable = table
manifest_path = table.table_path.rstrip('/')
self.manifest_path = f"{manifest_path}/manifest"
self.file_io = table.file_io
self.partition_keys_fields = self.table.partition_keys_fields
+ self._codec = avro_codec(table.options.manifest_compression())
def read(self, index_manifest_name: str) -> List[IndexManifestEntry]:
index_manifest_path = f"{self.manifest_path}/{index_manifest_name}"
@@ -247,7 +249,9 @@ class IndexManifestFile:
records = [self._to_avro_record(e) for e in entries]
try:
buffer = BytesIO()
- fastavro.writer(buffer, INDEX_MANIFEST_ENTRY_SCHEMA, records)
+ fastavro.writer(
+ buffer, INDEX_MANIFEST_ENTRY_SCHEMA, records,
+ codec=self._codec)
with self.file_io.new_output_stream(path) as output_stream:
output_stream.write(buffer.getvalue())
except Exception as e:
diff --git a/paimon-python/pypaimon/manifest/manifest_file_manager.py
b/paimon-python/pypaimon/manifest/manifest_file_manager.py
index 196b7bedf5..3bd6dbb226 100644
--- a/paimon-python/pypaimon/manifest/manifest_file_manager.py
+++ b/paimon-python/pypaimon/manifest/manifest_file_manager.py
@@ -49,6 +49,8 @@ class ManifestFileManager:
self.partition_keys_fields = self.table.partition_keys_fields
self.primary_keys_fields = self.table.primary_keys_fields
self.trimmed_primary_keys_fields =
self.table.trimmed_primary_keys_fields
+ from pypaimon.manifest import avro_codec
+ self._codec = avro_codec(table.options.manifest_compression())
def read_entries_parallel(self, manifest_files: List[ManifestFileMeta],
manifest_entry_filter=None,
drop_stats=True, max_workers=8,
@@ -211,7 +213,9 @@ class ManifestFileManager:
def write(self, file_name, entries: List[ManifestEntry]):
buf = BytesIO()
- fastavro.writer(buf, MANIFEST_ENTRY_SCHEMA,
self._to_avro_records(entries))
+ fastavro.writer(
+ buf, MANIFEST_ENTRY_SCHEMA, self._to_avro_records(entries),
+ codec=self._codec)
self._flush(file_name, buf.getvalue())
def rolling_write(self, entries: List[ManifestEntry],
@@ -227,7 +231,8 @@ class ManifestFileManager:
written_files = []
chunk_start = 0
buf = BytesIO()
- writer = Writer(buf, MANIFEST_ENTRY_SCHEMA,
sync_interval=sync_interval)
+ writer = Writer(buf, MANIFEST_ENTRY_SCHEMA,
+ sync_interval=sync_interval, codec=self._codec)
try:
for i, entry in enumerate(entries):
writer.write(self._to_avro_record(entry))
@@ -241,7 +246,9 @@ class ManifestFileManager:
file_name, entries[chunk_start:i + 1],
len(avro_bytes)))
chunk_start = i + 1
buf = BytesIO()
- writer = Writer(buf, MANIFEST_ENTRY_SCHEMA,
sync_interval=sync_interval)
+ writer = Writer(
+ buf, MANIFEST_ENTRY_SCHEMA,
+ sync_interval=sync_interval, codec=self._codec)
if chunk_start < len(entries):
writer.flush()
diff --git a/paimon-python/pypaimon/manifest/manifest_list_manager.py
b/paimon-python/pypaimon/manifest/manifest_list_manager.py
index 273dfbe363..3a0e606ef5 100644
--- a/paimon-python/pypaimon/manifest/manifest_list_manager.py
+++ b/paimon-python/pypaimon/manifest/manifest_list_manager.py
@@ -32,11 +32,13 @@ class ManifestListManager:
def __init__(self, table):
from pypaimon.table.file_store_table import FileStoreTable
+ from pypaimon.manifest import avro_codec
self.table: FileStoreTable = table
manifest_path = table.table_path.rstrip('/')
self.manifest_path = f"{manifest_path}/manifest"
self.file_io = self.table.file_io
+ self._codec = avro_codec(table.options.manifest_compression())
def read_all(self, snapshot: Optional[Snapshot]) -> List[ManifestFileMeta]:
"""Read base + delta manifest lists for full file state."""
@@ -124,7 +126,9 @@ class ManifestListManager:
list_path = f"{self.manifest_path}/{file_name}"
try:
buffer = BytesIO()
- fastavro.writer(buffer, MANIFEST_FILE_META_SCHEMA, avro_records)
+ fastavro.writer(
+ buffer, MANIFEST_FILE_META_SCHEMA, avro_records,
+ codec=self._codec)
avro_bytes = buffer.getvalue()
with self.file_io.new_output_stream(list_path) as output_stream:
output_stream.write(avro_bytes)
diff --git a/paimon-python/pypaimon/tests/manifest/manifest_manager_test.py
b/paimon-python/pypaimon/tests/manifest/manifest_manager_test.py
index adde81c34e..ad4754e057 100644
--- a/paimon-python/pypaimon/tests/manifest/manifest_manager_test.py
+++ b/paimon-python/pypaimon/tests/manifest/manifest_manager_test.py
@@ -346,7 +346,7 @@ class ManifestFileManagerTest(_ManifestManagerSetup):
wb = table.new_batch_write_builder()
w = wb.new_write()
- for pt in range(200):
+ for pt in range(800):
rows = [{'pt': f'p{pt}', 'pk': i, 'val': f'v{i}'} for i in
range(5)]
w.write_arrow(pa.Table.from_pylist(rows, schema=pa_schema))
wb.new_commit().commit(w.prepare_commit())
@@ -372,7 +372,7 @@ class ManifestFileManagerTest(_ManifestManagerSetup):
entries = mfm.read(meta.file_name)
self.assertEqual(len(entries), meta.num_added_files +
meta.num_deleted_files)
all_entries.extend(entries)
- self.assertEqual(len(all_entries), 200)
+ self.assertEqual(len(all_entries), 800)
def test_rolling_write_with_skewed_entries(self):
target_size = 16 * 1024
@@ -405,6 +405,49 @@ class ManifestFileManagerTest(_ManifestManagerSetup):
total_entries = sum(m.num_added_files + m.num_deleted_files for m in
metas)
self.assertEqual(total_entries, 300)
+ def test_manifest_compression(self):
+ pa_schema = pa.schema([('pk', pa.int32()), ('val', pa.string())])
+ schema_compressed = Schema.from_pyarrow_schema(
+ pa_schema, primary_keys=['pk'], options={'bucket': '1'})
+ schema_uncompressed = Schema.from_pyarrow_schema(
+ pa_schema, primary_keys=['pk'],
+ options={'bucket': '1', 'manifest.compression': 'null'})
+
+ self.catalog.create_table('default.compressed', schema_compressed,
False)
+ self.catalog.create_table('default.uncompressed', schema_uncompressed,
False)
+
+ rows = [{'pk': i, 'val': f'value-{i}' * 20} for i in range(100)]
+ arrow_data = pa.Table.from_pylist(rows, schema=pa_schema)
+
+ for name in ('default.compressed', 'default.uncompressed'):
+ table = self.catalog.get_table(name)
+ wb = table.new_batch_write_builder()
+ w = wb.new_write()
+ w.write_arrow(arrow_data)
+ wb.new_commit().commit(w.prepare_commit())
+ w.close()
+
+ t_comp = self.catalog.get_table('default.compressed')
+ t_uncomp = self.catalog.get_table('default.uncompressed')
+
+ metas_comp = ManifestListManager(t_comp).read_all(
+ t_comp.snapshot_manager().get_latest_snapshot())
+ metas_uncomp = ManifestListManager(t_uncomp).read_all(
+ t_uncomp.snapshot_manager().get_latest_snapshot())
+
+ size_comp = sum(m.file_size for m in metas_comp)
+ size_uncomp = sum(m.file_size for m in metas_uncomp)
+ self.assertLess(
+ size_comp, size_uncomp,
+ f"Compressed manifest ({size_comp}B) should be smaller "
+ f"than uncompressed ({size_uncomp}B)")
+
+ mfm = ManifestFileManager(t_comp)
+ entries = []
+ for m in metas_comp:
+ entries.extend(mfm.read(m.file_name))
+ self.assertGreater(len(entries), 0)
+
class ManifestListManagerTest(_ManifestManagerSetup):
"""Tests for ManifestListManager."""
diff --git a/paimon-python/pypaimon/tests/manifest/manifest_schema_test.py
b/paimon-python/pypaimon/tests/manifest/manifest_schema_test.py
index b806ad3fa9..f483d84ccc 100644
--- a/paimon-python/pypaimon/tests/manifest/manifest_schema_test.py
+++ b/paimon-python/pypaimon/tests/manifest/manifest_schema_test.py
@@ -221,6 +221,7 @@ class ManifestSchemaTest(unittest.TestCase):
table.table_path = table_path
table.file_io = file_io
table.partition_keys_fields = []
+ table.options.manifest_compression.return_value = 'zstd'
manager = ManifestListManager(table)
metas = manager.read(manifest_list_name)