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 2fc0e7e137 [python] Reduce file store scan for overwriting on commit
retries (#8359)
2fc0e7e137 is described below
commit 2fc0e7e13753429d32570009016c229c05245873
Author: XiaoHongbo <[email protected]>
AuthorDate: Sat Jun 27 22:41:47 2026 +0800
[python] Reduce file store scan for overwriting on commit retries (#8359)
---
.../pypaimon/tests/overwrite_changes_cache_test.py | 318 +++++++++++++++++++++
.../pypaimon/tests/partition_predicate_test.py | 2 +-
.../write/commit/overwrite_changes_provider.py | 136 +++++++++
paimon-python/pypaimon/write/file_store_commit.py | 45 ++-
4 files changed, 473 insertions(+), 28 deletions(-)
diff --git a/paimon-python/pypaimon/tests/overwrite_changes_cache_test.py
b/paimon-python/pypaimon/tests/overwrite_changes_cache_test.py
new file mode 100644
index 0000000000..c7fb16e628
--- /dev/null
+++ b/paimon-python/pypaimon/tests/overwrite_changes_cache_test.py
@@ -0,0 +1,318 @@
+# 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.
+
+"""Regression test for caching of OVERWRITE changes across commit retries.
+
+On every OVERWRITE retry, pypaimon re-scanned the full target partitions to
+recompute the files to delete. Under concurrent writers this made each retry as
+expensive as the first attempt. OverwriteChangesProvider caches the existing
+files of the target partitions and, on retry, reuses them when the snapshots in
+between are all APPEND and have not touched the target partitions (verified by
a
+cheap DELTA probe), instead of a full re-scan.
+
+The test deterministically forces ``K`` conflicts, each advancing the latest
+snapshot with an append to an unrelated partition, and asserts the full scan
+runs once (not ``K + 1``) and the cache is advanced by a delta probe per retry.
+"""
+
+import os
+import shutil
+import tempfile
+import unittest
+
+import pandas as pd
+import pyarrow as pa
+
+from pypaimon import CatalogFactory, Schema
+from pypaimon.write.commit.overwrite_changes_provider import
OverwriteChangesProvider
+
+
+class OverwriteChangesCacheTest(unittest.TestCase):
+
+ def setUp(self):
+ self.temp_dir = tempfile.mkdtemp(prefix="ow_cache_")
+ self.warehouse = os.path.join(self.temp_dir, 'wh')
+ self.catalog = CatalogFactory.create({"warehouse": self.warehouse})
+ self.catalog.create_database("test_db", True)
+
+ pa_schema = pa.schema([('f0', pa.int32()), ('f1', pa.string())])
+ schema = Schema.from_pyarrow_schema(
+ pa_schema,
+ partition_keys=['f0'],
+ options={'dynamic-partition-overwrite': 'false'},
+ )
+ self.catalog.create_table('test_db.t', schema, False)
+ self.table = self.catalog.get_table('test_db.t')
+
+ # Seed: f0=1 is the overwrite target, f0=2 untouched.
+ self._append(pd.DataFrame({'f0': [1, 1, 2], 'f1': ['a', 'b', 'c']}))
+
+ def tearDown(self):
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
+
+ def _append(self, df):
+ wb = self.table.new_batch_write_builder()
+ w = wb.new_write()
+ c = wb.new_commit()
+ w.write_pandas(df)
+ c.commit(w.prepare_commit())
+ w.close()
+ c.close()
+
+ def test_overwrite_scan_runs_once_not_once_per_retry(self):
+ K = 3 # force 3 conflicts; the 4th attempt wins
+
+ wb = self.table.new_batch_write_builder().overwrite({'f0': 1})
+ w = wb.new_write()
+ c = wb.new_commit()
+ w.write_pandas(pd.DataFrame({'f0': [1], 'f1': ['new']}))
+ messages = w.prepare_commit()
+
+ fsc = c.file_store_commit
+
+ # --- count provider full scans and delta probes (class-level spies)
---
+ counts = {'full_scan': 0, 'probe': 0}
+ orig_full = OverwriteChangesProvider._full_scan
+ orig_probe = OverwriteChangesProvider._delta_touches_target
+
+ def spy_full(self, *a, **k):
+ counts['full_scan'] += 1
+ return orig_full(self, *a, **k)
+
+ def spy_probe(self, *a, **k):
+ counts['probe'] += 1
+ return orig_probe(self, *a, **k)
+
+ # --- inject K CAS conflicts; each advances latest via an APPEND to an
+ # unrelated partition (f0=99) so the delta probe finds the target
+ # (f0=1) untouched and the cache is reused.
+ orig_cas = fsc.snapshot_commit.commit
+ cas = {'fails': 0}
+
+ def patched_cas(snapshot, statistics):
+ if snapshot.commit_kind == "OVERWRITE" and cas['fails'] < K:
+ cas['fails'] += 1
+ self._append(pd.DataFrame({'f0': [99], 'f1':
[f'x{cas["fails"]}']}))
+ return False
+ return orig_cas(snapshot, statistics)
+
+ fsc.snapshot_commit.commit = patched_cas
+ OverwriteChangesProvider._full_scan = spy_full
+ OverwriteChangesProvider._delta_touches_target = spy_probe
+ try:
+ c.commit(messages)
+ c.close()
+ finally:
+ OverwriteChangesProvider._full_scan = orig_full
+ OverwriteChangesProvider._delta_touches_target = orig_probe
+
+ # Harness sanity: we really did force K conflicts and then converged.
+ self.assertEqual(cas['fails'], K, "expected exactly K forced
conflicts")
+
+ print(f"\n[overwrite-cache] K={K} conflicts -> "
+ f"full_scan={counts['full_scan']}, probe={counts['probe']} "
+ f"(target: full_scan=1, probe=K)")
+
+ # #7894: the full overwrite scan runs once; each retry reuses the cache
+ # after a cheap delta probe of the (unrelated) intervening append.
+ self.assertEqual(
+ counts['full_scan'], 1,
+ f"full overwrite scan ran {counts['full_scan']}x; should run once
and "
+ f"reuse the cache on retries")
+ self.assertEqual(
+ counts['probe'], K,
+ f"delta probe ran {counts['probe']}x; should probe once per retry "
+ f"(= K = {K})")
+
+ # Sanity: f0=1 overwritten, f0=2 preserved, f0=99 appended K times.
+ read_builder = self.table.new_read_builder()
+ actual = read_builder.new_read().to_pandas(
+ read_builder.new_scan().plan().splits())
+ self.assertEqual(sorted(actual[actual['f0'] == 1]['f1'].tolist()),
['new'])
+ self.assertEqual(sorted(actual[actual['f0'] == 2]['f1'].tolist()),
['c'])
+ self.assertEqual(len(actual[actual['f0'] == 99]), K)
+
+ def test_cache_rebuilt_when_concurrent_append_hits_target_partition(self):
+ # Concurrent appends hit the overwrite target; probe sees it touched
-> rebuild.
+ K = 3
+
+ wb = self.table.new_batch_write_builder().overwrite({'f0': 1})
+ w = wb.new_write()
+ c = wb.new_commit()
+ w.write_pandas(pd.DataFrame({'f0': [1], 'f1': ['new']}))
+ messages = w.prepare_commit()
+
+ fsc = c.file_store_commit
+
+ counts = {'full_scan': 0, 'probe': 0}
+ orig_full = OverwriteChangesProvider._full_scan
+ orig_probe = OverwriteChangesProvider._delta_touches_target
+
+ def spy_full(self, *a, **k):
+ counts['full_scan'] += 1
+ return orig_full(self, *a, **k)
+
+ def spy_probe(self, *a, **k):
+ counts['probe'] += 1
+ return orig_probe(self, *a, **k)
+
+ orig_cas = fsc.snapshot_commit.commit
+ cas = {'fails': 0}
+
+ def patched_cas(snapshot, statistics):
+ if snapshot.commit_kind == "OVERWRITE" and cas['fails'] < K:
+ cas['fails'] += 1
+ self._append(pd.DataFrame({'f0': [1], 'f1':
[f'y{cas["fails"]}']}))
+ return False
+ return orig_cas(snapshot, statistics)
+
+ fsc.snapshot_commit.commit = patched_cas
+ OverwriteChangesProvider._full_scan = spy_full
+ OverwriteChangesProvider._delta_touches_target = spy_probe
+ try:
+ c.commit(messages)
+ c.close()
+ finally:
+ OverwriteChangesProvider._full_scan = orig_full
+ OverwriteChangesProvider._delta_touches_target = orig_probe
+
+ self.assertEqual(cas['fails'], K, "expected exactly K forced
conflicts")
+
+ # Target touched each retry => cache rebuilds; full scan runs every
attempt.
+ self.assertEqual(counts['full_scan'], K + 1,
+ f"full scan ran {counts['full_scan']}x; cache must
rebuild "
+ f"when the target partition is touched")
+ self.assertEqual(counts['probe'], K,
+ f"delta probe ran {counts['probe']}x; once per retry
(= K)")
+
+ # Overwrite wins: f0=1 is just 'new', f0=2 untouched.
+ read_builder = self.table.new_read_builder()
+ actual = read_builder.new_read().to_pandas(
+ read_builder.new_scan().plan().splits())
+ self.assertEqual(sorted(actual[actual['f0'] == 1]['f1'].tolist()),
['new'])
+ self.assertEqual(sorted(actual[actual['f0'] == 2]['f1'].tolist()),
['c'])
+
+ def _overwrite_partition(self, part_val, f1_val):
+ wb = self.table.new_batch_write_builder().overwrite({'f0': part_val})
+ w = wb.new_write()
+ c = wb.new_commit()
+ w.write_pandas(pd.DataFrame({'f0': [part_val], 'f1': [f1_val]}))
+ c.commit(w.prepare_commit())
+ w.close()
+ c.close()
+
+ def _run_with_conflicts(self, c, messages, K, concurrent_fn):
+ # Run an overwrite commit, forcing K CAS conflicts (each calls
+ # concurrent_fn(i) to advance the latest snapshot). Returns this
commit's
+ # own OverwriteChangesProvider so the caller can read its counters
+ # (only this provider, not any concurrent writer's).
+ fsc = c.file_store_commit
+ captured = {}
+ orig_factory = fsc._overwrite_changes_provider
+
+ def capturing_factory(*a, **k):
+ captured['provider'] = orig_factory(*a, **k)
+ return captured['provider']
+
+ fsc._overwrite_changes_provider = capturing_factory
+
+ orig_cas = fsc.snapshot_commit.commit
+ cas = {'fails': 0}
+
+ def patched_cas(snapshot, statistics):
+ if snapshot.commit_kind == "OVERWRITE" and cas['fails'] < K:
+ cas['fails'] += 1
+ concurrent_fn(cas['fails'])
+ return False
+ return orig_cas(snapshot, statistics)
+
+ fsc.snapshot_commit.commit = patched_cas
+
+ c.commit(messages)
+ c.close()
+
+ self.assertEqual(cas['fails'], K, "expected exactly K forced
conflicts")
+ return captured['provider']
+
+ def test_cache_rebuilt_on_non_append_snapshot(self):
+ # A non-APPEND (OVERWRITE) snapshot between retries forces a rebuild
even
+ # though it only touches an unrelated partition.
+ K = 2
+ wb = self.table.new_batch_write_builder().overwrite({'f0': 1})
+ w = wb.new_write()
+ c = wb.new_commit()
+ w.write_pandas(pd.DataFrame({'f0': [1], 'f1': ['new']}))
+ provider = self._run_with_conflicts(
+ c, w.prepare_commit(), K,
+ lambda i: self._overwrite_partition(99, f'z{i}'))
+
+ self.assertEqual(provider.full_scan_count, K + 1) # rebuilt every
retry
+ self.assertEqual(provider.delta_probe_count, K) # probed, bailed
at kind check
+
+ def test_whole_table_overwrite_always_full_scans(self):
+ # Whole-table overwrite (no partition filter) can never reuse the
cache.
+ K = 2
+ wb = self.table.new_batch_write_builder().overwrite()
+ w = wb.new_write()
+ c = wb.new_commit()
+ w.write_pandas(pd.DataFrame({'f0': [1], 'f1': ['new']}))
+ provider = self._run_with_conflicts(
+ c, w.prepare_commit(), K,
+ lambda i: self._append(pd.DataFrame({'f0': [99], 'f1':
[f'x{i}']})))
+
+ self.assertEqual(provider.full_scan_count, K + 1) # null filter ->
always full scan
+ self.assertEqual(provider.delta_probe_count, 0) # never enters the
probe loop
+
+ read_builder = self.table.new_read_builder()
+ actual = read_builder.new_read().to_pandas(
+ read_builder.new_scan().plan().splits())
+ self.assertEqual(sorted(actual['f1'].tolist()), ['new'])
+
+ def test_dynamic_partition_overwrite_reuses_cache(self):
+ # Dynamic-partition overwrite is scoped to the data's partitions, so an
+ # unrelated concurrent append lets the cache be reused.
+ pa_schema = pa.schema([('f0', pa.int32()), ('f1', pa.string())])
+ schema = Schema.from_pyarrow_schema(pa_schema, partition_keys=['f0'])
+ self.catalog.create_table('test_db.t_dyn', schema, False)
+ table = self.catalog.get_table('test_db.t_dyn')
+
+ def append(df):
+ wb = table.new_batch_write_builder()
+ w = wb.new_write()
+ c = wb.new_commit()
+ w.write_pandas(df)
+ c.commit(w.prepare_commit())
+ w.close()
+ c.close()
+
+ append(pd.DataFrame({'f0': [1, 1, 2], 'f1': ['a', 'b', 'c']}))
+
+ K = 3
+ wb = table.new_batch_write_builder().overwrite() # dynamic: filter
from data (f0=1)
+ w = wb.new_write()
+ c = wb.new_commit()
+ w.write_pandas(pd.DataFrame({'f0': [1], 'f1': ['new']}))
+ provider = self._run_with_conflicts(
+ c, w.prepare_commit(), K,
+ lambda i: append(pd.DataFrame({'f0': [99], 'f1': [f'x{i}']})))
+
+ self.assertEqual(provider.full_scan_count, 1) # target f0=1
untouched -> reuse
+ self.assertEqual(provider.delta_probe_count, K)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/paimon-python/pypaimon/tests/partition_predicate_test.py
b/paimon-python/pypaimon/tests/partition_predicate_test.py
index fdea8b17e6..9e47757e82 100644
--- a/paimon-python/pypaimon/tests/partition_predicate_test.py
+++ b/paimon-python/pypaimon/tests/partition_predicate_test.py
@@ -174,7 +174,7 @@ class TestOverwritePartitionPredicate(unittest.TestCase):
def _extract_partition_predicate(self, commit):
entries_plan = commit._try_commit.call_args[1]['commit_entries_plan']
- with patch('pypaimon.write.file_store_commit.FileScanner') as mock_cls:
+ with
patch('pypaimon.write.commit.overwrite_changes_provider.FileScanner') as
mock_cls:
mock_cls.return_value.read_manifest_entries.return_value = []
commit.manifest_list_manager.read_all.return_value = []
entries_plan(Mock(id=1))
diff --git a/paimon-python/pypaimon/write/commit/overwrite_changes_provider.py
b/paimon-python/pypaimon/write/commit/overwrite_changes_provider.py
new file mode 100644
index 0000000000..a76d5936f1
--- /dev/null
+++ b/paimon-python/pypaimon/write/commit/overwrite_changes_provider.py
@@ -0,0 +1,136 @@
+# 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.
+
+from typing import List, Optional
+
+from pypaimon.manifest.schema.manifest_entry import ManifestEntry
+from pypaimon.read.scanner.file_scanner import FileScanner
+from pypaimon.snapshot.snapshot import Snapshot
+from pypaimon.table.row.generic_row import GenericRow
+
+
+class OverwriteChangesProvider:
+ """Builds the commit entries (DELETE existing + ADD new) for an OVERWRITE,
+ caching the existing files of the target partitions across commit retries
+ to avoid repeated full scans.
+
+ On retry, if the latest snapshot advanced, the cache is reused only when
the
+ snapshots in between are all APPEND and have not touched the target
+ partitions; otherwise it is rebuilt by a full scan. A whole-table overwrite
+ (``partition_filter is None``) always rebuilds. Mirrors Java
+ ``OverwriteChangesProvider`` (#7894).
+ """
+
+ def __init__(self, table, manifest_list_manager, snapshot_manager,
+ partition_filter, commit_messages):
+ self.table = table
+ self.manifest_list_manager = manifest_list_manager
+ self.snapshot_manager = snapshot_manager
+ self.partition_filter = partition_filter
+ self.commit_messages = commit_messages
+
+ self._cached_snapshot: Optional[Snapshot] = None
+ self._cached_entries: List[ManifestEntry] = []
+
+ # Counters for tests / observability (mirrors Java @VisibleForTesting).
+ self.full_scan_count = 0
+ self.delta_probe_count = 0
+
+ def provide(self, latest_snapshot: Optional[Snapshot]) ->
List[ManifestEntry]:
+ if latest_snapshot is None:
+ # Empty table: nothing existing to delete, just add the new files.
+ return self._build_result([])
+
+ if self._cached_snapshot is None:
+ self._cached_entries = self._full_scan(latest_snapshot)
+ self._cached_snapshot = latest_snapshot
+ elif self._cached_snapshot.id > latest_snapshot.id:
+ raise RuntimeError(
+ f"Cached snapshot id {self._cached_snapshot.id} is greater
than "
+ f"latest snapshot id {latest_snapshot.id}")
+ elif self._cached_snapshot.id < latest_snapshot.id:
+ if not self._can_use_cache(latest_snapshot):
+ self._cached_entries = self._full_scan(latest_snapshot)
+ self._cached_snapshot = latest_snapshot
+ # cached_snapshot.id == latest_snapshot.id -> reuse cache as-is
+
+ return self._build_result(self._cached_entries)
+
+ def _full_scan(self, latest_snapshot: Snapshot) -> List[ManifestEntry]:
+ self.full_scan_count += 1
+ return (FileScanner(self.table, lambda: ([], None),
+ partition_predicate=self.partition_filter)
+
.read_manifest_entries(self.manifest_list_manager.read_all(latest_snapshot)))
+
+ def _can_use_cache(self, latest_snapshot: Snapshot) -> bool:
+ if self.partition_filter is None:
+ # Whole-table overwrite: any concurrent commit touches the target,
+ # so skip the delta probe and force a full scan.
+ return False
+ for snapshot_id in range(self._cached_snapshot.id + 1,
latest_snapshot.id + 1):
+ self.delta_probe_count += 1
+ try:
+ snapshot =
self.snapshot_manager.get_snapshot_by_id(snapshot_id)
+ if snapshot is None:
+ return False
+ if snapshot.commit_kind != "APPEND":
+ # Only APPEND snapshots produce a reliable DELTA manifest
for
+ # probing; other kinds may rewrite/reorganize manifests.
+ return False
+ if self._delta_touches_target(snapshot):
+ return False
+ except Exception:
+ # e.g. the snapshot is being expired; a full scan is always
safe.
+ return False
+ return True
+
+ def _delta_touches_target(self, snapshot: Snapshot) -> bool:
+ delta_manifests = self.manifest_list_manager.read_delta(snapshot)
+ if not delta_manifests:
+ return False
+ # Only APPEND snapshots are probed (see _can_use_cache), so the delta
has
+ # no standalone DELETEs; FileScanner's partition predicate prunes at
the
+ # manifest-file level before reading entries.
+ entries = (FileScanner(self.table, lambda: ([], None),
+ partition_predicate=self.partition_filter)
+ .read_manifest_entries(delta_manifests))
+ return len(entries) > 0
+
+ def _build_result(self, existing_entries: List[ManifestEntry]) ->
List[ManifestEntry]:
+ entries = []
+ # Existing files of the target partitions become DELETE entries. Build
+ # fresh entries so the cached (kind=0) entries are never mutated.
+ for entry in existing_entries:
+ entries.append(ManifestEntry(
+ kind=1,
+ partition=entry.partition,
+ bucket=entry.bucket,
+ total_buckets=entry.total_buckets,
+ file=entry.file,
+ ))
+ # New files being written by this overwrite.
+ for msg in self.commit_messages:
+ partition = GenericRow(list(msg.partition),
self.table.partition_keys_fields)
+ for file in msg.new_files:
+ entries.append(ManifestEntry(
+ kind=0,
+ partition=partition,
+ bucket=msg.bucket,
+ total_buckets=self.table.total_buckets,
+ file=file,
+ ))
+ return entries
diff --git a/paimon-python/pypaimon/write/file_store_commit.py
b/paimon-python/pypaimon/write/file_store_commit.py
index a2e41a1217..3149a3678d 100644
--- a/paimon-python/pypaimon/write/file_store_commit.py
+++ b/paimon-python/pypaimon/write/file_store_commit.py
@@ -30,7 +30,6 @@ 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.read.scanner.file_scanner import FileScanner
from pypaimon.snapshot.snapshot import Snapshot
from pypaimon.snapshot.snapshot_commit import (PartitionStatistics,
SnapshotCommit)
@@ -39,6 +38,7 @@ from pypaimon.table.row.offset_row import OffsetRow
from pypaimon.write.commit.commit_rollback import CommitRollback
from pypaimon.write.commit.commit_scanner import CommitScanner
from pypaimon.write.commit.conflict_detection import ConflictDetection
+from pypaimon.write.commit.overwrite_changes_provider import
OverwriteChangesProvider
from pypaimon.table.special_fields import SpecialFields
from pypaimon.write.commit_callback import CommitCallback,
CommitCallbackContext
from pypaimon.write.commit_message import CommitMessage
@@ -217,11 +217,11 @@ class FileStoreCommit:
changelog_entries = self._collect_changelog_entries(commit_messages)
if not skip_overwrite:
+ provider = self._overwrite_changes_provider(partition_filter,
commit_messages)
self._try_commit(
commit_kind="OVERWRITE",
commit_identifier=commit_identifier,
- commit_entries_plan=lambda snapshot:
self._generate_overwrite_entries(
- snapshot, partition_filter, commit_messages),
+ commit_entries_plan=provider.provide,
changelog_entries=changelog_entries,
detect_conflicts=True,
allow_rollback=False,
@@ -260,22 +260,22 @@ class FileStoreCommit:
partition_filter =
predicate_builder.or_predicates(partition_predicates)
+ provider = self._overwrite_changes_provider(partition_filter, [])
self._try_commit(
commit_kind="OVERWRITE",
commit_identifier=commit_identifier,
- commit_entries_plan=lambda snapshot:
self._generate_overwrite_entries(
- snapshot, partition_filter, []),
+ commit_entries_plan=provider.provide,
detect_conflicts=True,
allow_rollback=False,
)
def truncate_table(self, commit_identifier: int) -> None:
"""Truncate the entire table, deleting all data."""
+ provider = self._overwrite_changes_provider(None, [])
self._try_commit(
commit_kind="OVERWRITE",
commit_identifier=commit_identifier,
- commit_entries_plan=lambda snapshot:
self._generate_overwrite_entries(
- snapshot, None, []),
+ commit_entries_plan=provider.provide,
detect_conflicts=True,
allow_rollback=False,
)
@@ -579,26 +579,17 @@ class FileStoreCommit:
f"in {msg.partition} does not belong to
this partition")
return partition_filter
- def _generate_overwrite_entries(self, latest_snapshot, partition_filter,
commit_messages):
- """Generate commit entries for OVERWRITE mode based on latest
snapshot."""
- entries = []
- current_entries = [] if latest_snapshot is None \
- else (FileScanner(self.table, lambda: ([], None),
partition_predicate=partition_filter).
-
read_manifest_entries(self.manifest_list_manager.read_all(latest_snapshot)))
- for entry in current_entries:
- entry.kind = 1 # DELETE
- entries.append(entry)
- for msg in commit_messages:
- partition = GenericRow(list(msg.partition),
self.table.partition_keys_fields)
- for file in msg.new_files:
- entries.append(ManifestEntry(
- kind=0, # ADD
- partition=partition,
- bucket=msg.bucket,
- total_buckets=self.table.total_buckets,
- file=file
- ))
- return entries
+ def _overwrite_changes_provider(self, partition_filter, commit_messages):
+ """Build a stateful provider of OVERWRITE commit entries that caches
the
+ existing files of the target partitions across retries (see
+ OverwriteChangesProvider). One instance per overwrite operation."""
+ return OverwriteChangesProvider(
+ self.table,
+ self.manifest_list_manager,
+ self.snapshot_manager,
+ partition_filter,
+ commit_messages,
+ )
def _commit_retry_wait(self, retry_count: int):