blob: 1ce3fb93c4d405f80cea6c9e8c4191b1adba94d6 [file]
# 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.
import unittest
import uuid
from datetime import datetime
from unittest.mock import MagicMock, Mock, patch
from pypaimon.manifest.schema.data_file_meta import DataFileMeta
from pypaimon.manifest.schema.manifest_entry import ManifestEntry
from pypaimon.manifest.schema.manifest_file_meta import ManifestFileMeta
from pypaimon.manifest.schema.simple_stats import SimpleStats
from pypaimon.snapshot.snapshot_commit import PartitionStatistics
from pypaimon.table.row.binary_row import BinaryRow
from pypaimon.table.row.generic_row import GenericRow, GenericRowSerializer
from pypaimon.write.commit.row_id_conflict_rewriter import RowIdRewriteResult
from pypaimon.write.commit_message import CommitMessage
from pypaimon.write.file_store_commit import (
FileStoreCommit,
ManifestMergeResult,
RetryResult,
RewriteResult,
_try_replace_manifest_files,
)
@patch('pypaimon.write.file_store_commit.ManifestFileManager')
@patch('pypaimon.write.file_store_commit.ManifestListManager')
class TestFileStoreCommit(unittest.TestCase):
"""Test cases for FileStoreCommit class."""
def setUp(self):
"""Set up test fixtures."""
# Mock table with required attributes
self.mock_table = Mock()
self.mock_table.partition_keys = ['dt', 'region']
self.mock_table.current_branch.return_value = 'main'
self.mock_table.table_path = '/test/table/path'
self.mock_table.file_io = Mock()
self.mock_table.options.manifest_target_size.return_value = 8 * 1024 * 1024
self.mock_table.options.manifest_merge_min_count.return_value = 30
# Mock snapshot commit
self.mock_snapshot_commit = Mock()
def _create_file_store_commit(self):
"""Helper method to create FileStoreCommit instance."""
return FileStoreCommit(
snapshot_commit=self.mock_snapshot_commit,
table=self.mock_table,
commit_user='test_user'
)
@staticmethod
def _manifest_meta(name):
row = GenericRowSerializer.to_bytes(GenericRow([], []))
return ManifestFileMeta(
file_name=name,
file_size=10,
num_added_files=1,
num_deleted_files=0,
partition_stats=SimpleStats(
BinaryRow(row, []), BinaryRow(row, []), []),
schema_id=0,
)
def test_replace_manifest_files_uses_stable_value_equality(
self, mock_manifest_list_manager, mock_manifest_file_manager):
previous = [self._manifest_meta('a'), self._manifest_meta('b')]
current = [
self._manifest_meta('prefix'),
self._manifest_meta('a'),
self._manifest_meta('b'),
self._manifest_meta('suffix'),
]
replacement = [self._manifest_meta('merged')]
result = _try_replace_manifest_files(
current, previous, replacement)
self.assertEqual(
['prefix', 'merged', 'suffix'],
[manifest.file_name for manifest in result],
)
self.assertIsNot(current[1], previous[0])
def test_replace_manifest_files_preserves_order_and_empty_semantics(
self, mock_manifest_list_manager, mock_manifest_file_manager):
a = self._manifest_meta('a')
b = self._manifest_meta('b')
merged = self._manifest_meta('merged')
self.assertIsNone(_try_replace_manifest_files(
[a, self._manifest_meta('x'), b], [a, b], [merged]))
self.assertEqual(
['a', 'merged'],
[manifest.file_name for manifest in _try_replace_manifest_files(
[a, self._manifest_meta('a'), b], [a, b], [merged])],
)
self.assertEqual(
[merged], _try_replace_manifest_files([], [], [merged]))
self.assertIsNone(_try_replace_manifest_files([a], [], [merged]))
def test_manifest_merge_result_copies_and_freezes_lists(
self, mock_manifest_list_manager, mock_manifest_file_manager):
before = [self._manifest_meta('before')]
after = [self._manifest_meta('after')]
result = ManifestMergeResult(before, after)
before.clear()
after.clear()
self.assertIsInstance(result.merge_before_manifests, tuple)
self.assertIsInstance(result.merge_after_manifests, tuple)
self.assertEqual(
['before'],
[manifest.file_name
for manifest in result.merge_before_manifests],
)
self.assertEqual(
['after'],
[manifest.file_name
for manifest in result.merge_after_manifests],
)
def _run_manifest_commit_attempt(self, commit_side_effect=None,
commit_result=None, retry_result=None,
existing_manifests=None,
merged_manifests=None,
latest_watermark=None):
file_store_commit = self._create_file_store_commit()
self.mock_table.identifier = 'default.test_table'
self.mock_table.table_schema.id = 7
self.mock_table.options.row_tracking_enabled.return_value = False
snapshot_commit = MagicMock()
snapshot_commit.__enter__.return_value = snapshot_commit
snapshot_commit.__exit__.return_value = False
snapshot_commit.commit.side_effect = commit_side_effect
snapshot_commit.commit.return_value = commit_result
file_store_commit.snapshot_commit = snapshot_commit
before = self._manifest_meta('before')
after = self._manifest_meta('after')
existing_manifests = (
[before] if existing_manifests is None
else existing_manifests)
merged_manifests = (
[after] if merged_manifests is None
else merged_manifests)
delta = self._manifest_meta('delta')
file_store_commit._write_manifest_files = Mock(
return_value=[delta])
file_store_commit._generate_partition_statistics = Mock(
return_value=[])
file_store_commit.manifest_list_manager.read_all.return_value = (
existing_manifests)
file_store_commit.manifest_file_merger = Mock()
file_store_commit.manifest_file_merger.merge.return_value = (
merged_manifests, merged_manifests)
file_store_commit._clean_up_reuse_tmp_manifests = Mock()
file_store_commit._clean_up_no_reuse_tmp_manifests = Mock()
latest_snapshot = Mock(
id=3,
uuid='base-snapshot-uuid',
total_record_count=10,
index_manifest=None,
watermark=latest_watermark,
)
commit_entry = Mock(kind=0)
commit_entry.file.row_count = 2
result = file_store_commit._try_commit_once(
retry_result=retry_result,
commit_kind='APPEND',
commit_entries=[commit_entry],
changelog_entries=[],
commit_identifier=11,
latest_snapshot=latest_snapshot,
)
return file_store_commit, result
def test_append_commit_inherits_watermark(
self, mock_manifest_list_manager, mock_manifest_file_manager):
file_store_commit, result = self._run_manifest_commit_attempt(
commit_result=True,
latest_watermark=123,
)
self.assertTrue(result.is_success())
committed_snapshot = (
file_store_commit.snapshot_commit.commit.call_args[0][1])
self.assertEqual(123, committed_snapshot.watermark)
def test_false_atomic_commit_retains_manifest_merge_result(
self, mock_manifest_list_manager, mock_manifest_file_manager):
file_store_commit, result = self._run_manifest_commit_attempt(
commit_result=False)
self.assertIsInstance(result, RetryResult)
self.assertIsNone(result.exception)
self.assertEqual(
['before'],
[manifest.file_name for manifest
in result.manifest_merge_result.merge_before_manifests],
)
self.assertEqual(
['after'],
[manifest.file_name for manifest
in result.manifest_merge_result.merge_after_manifests],
)
file_store_commit.manifest_file_merger.merge.assert_called_once()
def test_atomic_commit_exception_does_not_retain_manifest_merge_result(
self, mock_manifest_list_manager, mock_manifest_file_manager):
failure = TimeoutError('lost commit response')
file_store_commit, result = self._run_manifest_commit_attempt(
commit_side_effect=failure)
self.assertIsInstance(result, RetryResult)
self.assertIs(failure, result.exception)
self.assertTrue(result.commit_result_may_be_uncertain)
self.assertIsNone(result.manifest_merge_result)
file_store_commit._clean_up_reuse_tmp_manifests.assert_not_called()
file_store_commit._clean_up_no_reuse_tmp_manifests.assert_not_called()
def test_retry_reuses_manifest_merge_and_preserves_surrounding_files(
self, mock_manifest_list_manager, mock_manifest_file_manager):
previous_before = [
self._manifest_meta('before-a'),
self._manifest_meta('before-b'),
]
previous_after = [self._manifest_meta('merged')]
retry_result = RetryResult(
Mock(id=3),
manifest_merge_result=ManifestMergeResult(
previous_before, previous_after),
)
current = [
self._manifest_meta('prefix'),
self._manifest_meta('before-a'),
self._manifest_meta('before-b'),
self._manifest_meta('suffix'),
]
file_store_commit, result = self._run_manifest_commit_attempt(
commit_result=False,
retry_result=retry_result,
existing_manifests=current,
)
self.assertIsInstance(result, RetryResult)
self.assertEqual(
['prefix', 'before-a', 'before-b', 'suffix'],
[manifest.file_name for manifest
in result.manifest_merge_result.merge_before_manifests],
)
self.assertEqual(
['prefix', 'merged', 'suffix'],
[manifest.file_name for manifest
in result.manifest_merge_result.merge_after_manifests],
)
file_store_commit.manifest_file_merger.merge.assert_not_called()
base_manifests = (
file_store_commit.manifest_list_manager.write
.call_args_list[-1].args[1])
self.assertEqual(
['prefix', 'merged', 'suffix'],
[manifest.file_name for manifest in base_manifests],
)
def test_retry_skips_manifest_merge_when_previous_input_is_not_contiguous(
self, mock_manifest_list_manager, mock_manifest_file_manager):
previous_before = [
self._manifest_meta('before-a'),
self._manifest_meta('before-b'),
]
retry_result = RetryResult(
Mock(id=3),
manifest_merge_result=ManifestMergeResult(
previous_before, [self._manifest_meta('merged')]),
)
current = [
self._manifest_meta('before-a'),
self._manifest_meta('between'),
self._manifest_meta('before-b'),
]
file_store_commit, result = self._run_manifest_commit_attempt(
commit_result=False,
retry_result=retry_result,
existing_manifests=current,
)
self.assertIsInstance(result, RetryResult)
self.assertIsNone(result.manifest_merge_result)
file_store_commit.manifest_file_merger.merge.assert_not_called()
base_manifests = (
file_store_commit.manifest_list_manager.write
.call_args_list[-1].args[1])
self.assertEqual(current, base_manifests)
def test_manifest_merge_runs_once_across_multiple_retries(
self, mock_manifest_list_manager, mock_manifest_file_manager):
first_commit, retry_result = self._run_manifest_commit_attempt(
commit_result=False,
existing_manifests=[self._manifest_meta('before')],
merged_manifests=[self._manifest_meta('merged')],
)
first_commit.manifest_file_merger.merge.assert_called_once()
unchanged_retry, retry_result = self._run_manifest_commit_attempt(
commit_result=False,
retry_result=retry_result,
existing_manifests=[self._manifest_meta('before')],
)
retry_commits = [unchanged_retry]
current_names = ['before']
for suffix in ['concurrent-1', 'concurrent-2']:
current_names.append(suffix)
retry_commit, retry_result = self._run_manifest_commit_attempt(
commit_result=False,
retry_result=retry_result,
existing_manifests=[
self._manifest_meta(name) for name in current_names
],
)
retry_commits.append(retry_commit)
final_names = current_names + ['concurrent-3']
final_commit, result = self._run_manifest_commit_attempt(
commit_result=True,
retry_result=retry_result,
existing_manifests=[
self._manifest_meta(name) for name in final_names
],
)
self.assertTrue(result.is_success())
for retry_commit in retry_commits + [final_commit]:
retry_commit.manifest_file_merger.merge.assert_not_called()
base_manifests = (
final_commit.manifest_list_manager.write
.call_args_list[-1].args[1])
self.assertEqual(
['merged', 'concurrent-1', 'concurrent-2', 'concurrent-3'],
[manifest.file_name for manifest in base_manifests],
)
def test_generate_partition_statistics_single_partition_single_file(
self, mock_manifest_list_manager, mock_manifest_file_manager):
"""Test partition statistics generation with single partition and single file."""
# Create FileStoreCommit instance
file_store_commit = self._create_file_store_commit()
# Create test data
creation_time_dt = datetime(2024, 1, 15, 10, 30, 0)
from pypaimon.data.timestamp import Timestamp
from pypaimon.table.row.generic_row import GenericRow
from pypaimon.manifest.schema.simple_stats import SimpleStats
creation_time = Timestamp.from_local_date_time(creation_time_dt)
file_meta = DataFileMeta.create(
file_name="test_file_1.parquet",
file_size=1024 * 1024, # 1MB
row_count=10000,
min_key=GenericRow([], []),
max_key=GenericRow([], []),
key_stats=SimpleStats.empty_stats(),
value_stats=SimpleStats.empty_stats(),
min_sequence_number=1,
max_sequence_number=100,
schema_id=0,
level=0,
extra_files=[],
creation_time=creation_time,
external_path=None,
first_row_id=None,
write_cols=None
)
commit_message = CommitMessage(
partition=('2024-01-15', 'us-east-1'),
bucket=0,
new_files=[file_meta]
)
# Test method
statistics = file_store_commit._generate_partition_statistics(self._to_entries([commit_message]))
# Verify results
self.assertEqual(len(statistics), 1)
stat = statistics[0]
self.assertIsInstance(stat, PartitionStatistics)
self.assertEqual(stat.spec, {'dt': '2024-01-15', 'region': 'us-east-1'})
self.assertEqual(stat.record_count, 10000)
self.assertEqual(stat.file_count, 1)
self.assertEqual(stat.file_size_in_bytes, 1024 * 1024)
expected_time = file_meta.creation_time_epoch_millis()
self.assertEqual(stat.last_file_creation_time, expected_time)
def test_generate_partition_statistics_multiple_files_same_partition(
self, mock_manifest_list_manager, mock_manifest_file_manager):
"""Test partition statistics generation with multiple files in same partition."""
# Create FileStoreCommit instance
file_store_commit = self._create_file_store_commit()
from pypaimon.data.timestamp import Timestamp
from pypaimon.table.row.generic_row import GenericRow
from pypaimon.manifest.schema.simple_stats import SimpleStats
creation_time_1 = Timestamp.from_local_date_time(datetime(2024, 1, 15, 10, 30, 0))
creation_time_2 = Timestamp.from_local_date_time(datetime(2024, 1, 15, 11, 30, 0)) # Later time
file_meta_1 = DataFileMeta.create(
file_name="test_file_1.parquet",
file_size=1024 * 1024, # 1MB
row_count=10000,
min_key=GenericRow([], []),
max_key=GenericRow([], []),
key_stats=SimpleStats.empty_stats(),
value_stats=SimpleStats.empty_stats(),
min_sequence_number=1,
max_sequence_number=100,
schema_id=0,
level=0,
extra_files=[],
creation_time=creation_time_1
)
file_meta_2 = DataFileMeta.create(
file_name="test_file_2.parquet",
file_size=2 * 1024 * 1024, # 2MB
row_count=15000,
min_key=GenericRow([], []),
max_key=GenericRow([], []),
key_stats=SimpleStats.empty_stats(),
value_stats=SimpleStats.empty_stats(),
min_sequence_number=101,
max_sequence_number=200,
schema_id=0,
level=0,
extra_files=[],
creation_time=creation_time_2
)
commit_message = CommitMessage(
partition=('2024-01-15', 'us-east-1'),
bucket=0,
new_files=[file_meta_1, file_meta_2]
)
# Test method
statistics = file_store_commit._generate_partition_statistics(self._to_entries([commit_message]))
# Verify results
self.assertEqual(len(statistics), 1)
stat = statistics[0]
self.assertEqual(stat.spec, {'dt': '2024-01-15', 'region': 'us-east-1'})
self.assertEqual(stat.record_count, 25000) # 10000 + 15000
self.assertEqual(stat.file_count, 2)
self.assertEqual(stat.file_size_in_bytes, 3 * 1024 * 1024) # 1MB + 2MB
expected_time = file_meta_2.creation_time_epoch_millis()
self.assertEqual(stat.last_file_creation_time, expected_time)
def test_partition_statistics_use_replacement_bucket_count(
self, mock_manifest_list_manager, mock_manifest_file_manager):
file_store_commit = self._create_file_store_commit()
file_meta = Mock(row_count=1, file_size=10, creation_time=None)
for old_buckets, new_buckets in [(-2, 2), (2, 3)]:
with self.subTest(old=old_buckets, new=new_buckets):
partition = GenericRow(['2024-01-15', 'us-east-1'], None)
entries = [
ManifestEntry(
kind=1,
partition=partition,
bucket=0,
total_buckets=old_buckets,
file=file_meta,
),
ManifestEntry(
kind=0,
partition=partition,
bucket=0,
total_buckets=new_buckets,
file=file_meta,
),
]
statistics = (
file_store_commit._generate_partition_statistics(entries))
self.assertEqual(new_buckets, statistics[0].total_buckets)
def test_generate_partition_statistics_multiple_partitions(
self, mock_manifest_list_manager, mock_manifest_file_manager):
"""Test partition statistics generation with multiple different partitions."""
# Create FileStoreCommit instance
file_store_commit = self._create_file_store_commit()
creation_time_dt = datetime(2024, 1, 15, 10, 30, 0)
from pypaimon.data.timestamp import Timestamp
from pypaimon.table.row.generic_row import GenericRow
from pypaimon.manifest.schema.simple_stats import SimpleStats
creation_time = Timestamp.from_local_date_time(creation_time_dt)
# File for partition 1
file_meta_1 = DataFileMeta.create(
file_name="test_file_1.parquet",
file_size=1024 * 1024,
row_count=10000,
min_key=GenericRow([], []),
max_key=GenericRow([], []),
key_stats=SimpleStats.empty_stats(),
value_stats=SimpleStats.empty_stats(),
min_sequence_number=1,
max_sequence_number=100,
schema_id=0,
level=0,
extra_files=[],
creation_time=creation_time,
external_path=None,
first_row_id=None,
write_cols=None
)
# File for partition 2
file_meta_2 = DataFileMeta.create(
file_name="test_file_2.parquet",
file_size=2 * 1024 * 1024,
row_count=20000,
min_key=GenericRow([], []),
max_key=GenericRow([], []),
key_stats=SimpleStats.empty_stats(),
value_stats=SimpleStats.empty_stats(),
min_sequence_number=101,
max_sequence_number=200,
schema_id=0,
level=0,
extra_files=[],
creation_time=creation_time,
external_path=None,
first_row_id=None,
write_cols=None
)
commit_message_1 = CommitMessage(
partition=('2024-01-15', 'us-east-1'),
bucket=0,
new_files=[file_meta_1]
)
commit_message_2 = CommitMessage(
partition=('2024-01-15', 'us-west-2'),
bucket=0,
new_files=[file_meta_2]
)
# Test method
statistics = file_store_commit._generate_partition_statistics(
self._to_entries([commit_message_1, commit_message_2]))
# Verify results
self.assertEqual(len(statistics), 2)
# Sort statistics by partition spec for consistent testing
statistics.sort(key=lambda s: s.spec['region'])
# Check first partition (us-east-1)
stat_1 = statistics[0]
self.assertEqual(stat_1.spec, {'dt': '2024-01-15', 'region': 'us-east-1'})
self.assertEqual(stat_1.record_count, 10000)
self.assertEqual(stat_1.file_count, 1)
self.assertEqual(stat_1.file_size_in_bytes, 1024 * 1024)
# Check second partition (us-west-2)
stat_2 = statistics[1]
self.assertEqual(stat_2.spec, {'dt': '2024-01-15', 'region': 'us-west-2'})
self.assertEqual(stat_2.record_count, 20000)
self.assertEqual(stat_2.file_count, 1)
self.assertEqual(stat_2.file_size_in_bytes, 2 * 1024 * 1024)
def test_generate_partition_statistics_unpartitioned_table(
self, mock_manifest_list_manager, mock_manifest_file_manager):
"""Test partition statistics generation for unpartitioned table."""
# Update mock table to have no partition keys
self.mock_table.partition_keys = []
# Create FileStoreCommit instance
file_store_commit = self._create_file_store_commit()
creation_time_dt = datetime(2024, 1, 15, 10, 30, 0)
from pypaimon.data.timestamp import Timestamp
from pypaimon.table.row.generic_row import GenericRow
from pypaimon.manifest.schema.simple_stats import SimpleStats
creation_time = Timestamp.from_local_date_time(creation_time_dt)
file_meta = DataFileMeta.create(
file_name="test_file_1.parquet",
file_size=1024 * 1024,
row_count=10000,
min_key=GenericRow([], []),
max_key=GenericRow([], []),
key_stats=SimpleStats.empty_stats(),
value_stats=SimpleStats.empty_stats(),
min_sequence_number=1,
max_sequence_number=100,
schema_id=0,
level=0,
extra_files=[],
creation_time=creation_time,
external_path=None,
first_row_id=None,
write_cols=None
)
commit_message = CommitMessage(
partition=(), # Empty partition for unpartitioned table
bucket=0,
new_files=[file_meta]
)
# Test method
statistics = file_store_commit._generate_partition_statistics(self._to_entries([commit_message]))
# Verify results
self.assertEqual(len(statistics), 1)
stat = statistics[0]
self.assertEqual(stat.spec, {}) # Empty spec for unpartitioned table
self.assertEqual(stat.record_count, 10000)
self.assertEqual(stat.file_count, 1)
self.assertEqual(stat.file_size_in_bytes, 1024 * 1024)
def test_generate_partition_statistics_no_creation_time(
self, mock_manifest_list_manager, mock_manifest_file_manager):
"""Test partition statistics generation when file has no creation time."""
# Create FileStoreCommit instance
file_store_commit = self._create_file_store_commit()
file_meta = DataFileMeta(
file_name="test_file_1.parquet",
file_size=1024 * 1024,
row_count=10000,
min_key=None,
max_key=None,
key_stats=None,
value_stats=None,
min_sequence_number=1,
max_sequence_number=100,
schema_id=0,
level=0,
extra_files=None,
)
commit_message = CommitMessage(
partition=('2024-01-15', 'us-east-1'),
bucket=0,
new_files=[file_meta]
)
# Test method
statistics = file_store_commit._generate_partition_statistics(self._to_entries([commit_message]))
# Verify results
self.assertEqual(len(statistics), 1)
stat = statistics[0]
# Should have a valid timestamp (current time)
self.assertGreater(stat.last_file_creation_time, 0)
def test_generate_partition_statistics_mismatched_partition_keys(
self, mock_manifest_list_manager, mock_manifest_file_manager):
"""Test partition statistics generation when partition tuple doesn't match partition keys."""
# Create FileStoreCommit instance
file_store_commit = self._create_file_store_commit()
# Table has 2 partition keys but partition tuple has 3 values
from pypaimon.data.timestamp import Timestamp
from pypaimon.table.row.generic_row import GenericRow
from pypaimon.manifest.schema.simple_stats import SimpleStats
file_meta = DataFileMeta.create(
file_name="test_file_1.parquet",
file_size=1024 * 1024,
row_count=10000,
min_key=GenericRow([], []),
max_key=GenericRow([], []),
key_stats=SimpleStats.empty_stats(),
value_stats=SimpleStats.empty_stats(),
min_sequence_number=1,
max_sequence_number=100,
schema_id=0,
level=0,
extra_files=[],
creation_time=Timestamp.from_local_date_time(datetime(2024, 1, 15, 10, 30, 0))
)
commit_message = CommitMessage(
partition=('2024-01-15', 'us-east-1', 'extra-value'), # 3 values but table has 2 keys
bucket=0,
new_files=[file_meta]
)
# Test method
statistics = file_store_commit._generate_partition_statistics(self._to_entries([commit_message]))
# Verify results - should fallback to index-based naming
self.assertEqual(len(statistics), 1)
stat = statistics[0]
expected_spec = {
'partition_0': '2024-01-15',
'partition_1': 'us-east-1',
'partition_2': 'extra-value'
}
self.assertEqual(stat.spec, expected_spec)
def test_generate_partition_statistics_empty_commit_messages(
self, mock_manifest_list_manager, mock_manifest_file_manager):
"""Test partition statistics generation with empty commit messages list."""
# Create FileStoreCommit instance
file_store_commit = self._create_file_store_commit()
# Test method
statistics = file_store_commit._generate_partition_statistics([])
# Verify results
self.assertEqual(len(statistics), 0)
def test_append_commit_inherits_index_manifest(
self, mock_manifest_list_manager, mock_manifest_file_manager):
file_store_commit = self._create_file_store_commit()
self.mock_table.identifier = 'default.test_table'
self.mock_table.table_schema = Mock()
self.mock_table.table_schema.id = 7
self.mock_table.options.row_tracking_enabled.return_value = False
snapshot_commit = MagicMock()
snapshot_commit.__enter__.return_value = snapshot_commit
snapshot_commit.__exit__.return_value = False
snapshot_commit.commit.return_value = True
file_store_commit.snapshot_commit = snapshot_commit
file_store_commit._write_manifest_files = Mock(return_value=[Mock()])
file_store_commit._generate_partition_statistics = Mock(return_value=[])
file_store_commit.manifest_list_manager.read_all.return_value = []
latest_snapshot = Mock()
latest_snapshot.id = 3
latest_snapshot.uuid = "base-snapshot-uuid"
latest_snapshot.total_record_count = 10
latest_snapshot.index_manifest = "index-manifest-existing"
latest_snapshot.watermark = None
commit_entry = Mock()
commit_entry.kind = 0
commit_entry.file = Mock()
commit_entry.file.row_count = 2
result = file_store_commit._try_commit_once(
retry_result=None,
commit_kind="APPEND",
commit_entries=[commit_entry],
changelog_entries=[],
commit_identifier=11,
latest_snapshot=latest_snapshot
)
self.assertTrue(result.is_success())
self.assertEqual(
"base-snapshot-uuid",
snapshot_commit.commit.call_args[0][0],
)
committed_snapshot = snapshot_commit.commit.call_args[0][1]
self.assertEqual(
"index-manifest-existing",
committed_snapshot.index_manifest
)
self.assertEqual(str(uuid.UUID(committed_snapshot.uuid)), committed_snapshot.uuid)
def test_null_partition_value(
self, mock_manifest_list_manager, mock_manifest_file_manager):
from pypaimon.data.timestamp import Timestamp
from pypaimon.manifest.schema.simple_stats import SimpleStats
from pypaimon.schema.data_types import DataField, AtomicType
file_store_commit = self._create_file_store_commit()
self.mock_table.partition_keys = ['dt']
self.mock_table.partition_keys_fields = [
DataField(0, 'dt', AtomicType('STRING'))
]
self.mock_table.table_schema = Mock()
self.mock_table.table_schema.id = 0
file_store_commit.manifest_file_manager = Mock()
file_store_commit.manifest_file_manager.manifest_path = '/test/manifest'
self.mock_table.file_io.get_file_size.return_value = 1024
creation_time = Timestamp.from_local_date_time(datetime(2024, 1, 15, 10, 30, 0))
def make_file(name):
return DataFileMeta.create(
file_name=name,
file_size=1024,
row_count=100,
min_key=GenericRow([], []),
max_key=GenericRow([], []),
key_stats=SimpleStats.empty_stats(),
value_stats=SimpleStats.empty_stats(),
min_sequence_number=1,
max_sequence_number=10,
schema_id=0,
level=0,
extra_files=[],
creation_time=creation_time,
)
entries = [
ManifestEntry(kind=0, partition=GenericRow([None], None), bucket=0, total_buckets=None,
file=make_file("f1.parquet")),
ManifestEntry(kind=0, partition=GenericRow(['2024-01-15'], None), bucket=0, total_buckets=None,
file=make_file("f2.parquet")),
]
result = file_store_commit._write_manifest_files(entries, "manifest-test")
self.assertIsNotNone(result)
def test_row_id_rewrite_respects_commit_retry_limit(
self, mock_manifest_list_manager, mock_manifest_file_manager):
file_store_commit = self._create_file_store_commit()
file_store_commit.commit_max_retries = 1
file_store_commit.commit_timeout = 10 ** 9
file_store_commit._commit_retry_wait = Mock()
latest_snapshot = Mock()
latest_snapshot.id = 7
file_store_commit.snapshot_manager.get_latest_snapshot.return_value = (
latest_snapshot
)
commit_entry = Mock()
rewrite = RewriteResult(RowIdRewriteResult([commit_entry], 1))
file_store_commit._try_commit_once = Mock(side_effect=[
rewrite,
rewrite,
AssertionError("rewrite retry budget was not enforced"),
])
with self.assertRaises(RuntimeError) as ctx:
file_store_commit._try_commit(
commit_kind="APPEND",
commit_identifier=11,
commit_entries_plan=lambda snapshot: [commit_entry],
)
self.assertIn("with 1 retries", str(ctx.exception))
self.assertEqual(2, file_store_commit._try_commit_once.call_count)
file_store_commit._commit_retry_wait.assert_called_once_with(0)
@staticmethod
def _to_entries(commit_messages):
commit_entries = []
for msg in commit_messages:
partition = GenericRow(list(msg.partition), None)
for file in msg.new_files:
commit_entries.append(ManifestEntry(
kind=0,
partition=partition,
bucket=msg.bucket,
total_buckets=None,
file=file
))
return commit_entries