Skip to main content

Batch Writes

Write Arrow or pandas data with an explicit write-and-commit lifecycle. These examples assume you have a table from Catalogs and Tables.

Batch Write​

A batch write has two steps: write data files, then publish them in a snapshot. You can write multiple Arrow or pandas batches with one writer. Call prepare_commit() once, pass its messages to the commit object, and close both objects. Create a new writer for another commit.

The following example shows alternative input formats. Replace the placeholder values with data matching your table schema; the quick start contains a complete executable example.

table = catalog.get_table('database_name.table_name')

# 1. Create table write and commit
write_builder = table.new_batch_write_builder()
table_write = write_builder.new_write()
table_commit = write_builder.new_commit()

# 2. Choose the method for your input format:
# 2.1 Write pandas.DataFrame
dataframe = ...
table_write.write_pandas(dataframe)

# 2.2 Write pyarrow.Table
pa_table = ...
table_write.write_arrow(pa_table)

# 2.3 Write pyarrow.RecordBatch
record_batch = ...
table_write.write_arrow_batch(record_batch)

# 3. Publish the written batches in one snapshot
commit_messages = table_write.prepare_commit()
table_commit.commit(commit_messages)

# 4. Close resources
table_write.close()
table_commit.close()

new_batch_write_builder() keeps postpone-bucket writes in bucket-postpone. Use new_postpone_fixed_bucket_write_builder() to write real buckets. New partitions are buffered until prepare_commit(); existing ones are incremental. This builder currently supports bucket-function.type=default only.

Commit Identity​

Write builders generate a commit user by default. Use with_commit_user() before creating writers, updates or committers to share an application identity across distributed tasks or restore a stream application:

write_builder = table.new_stream_write_builder().with_commit_user('application-1')
table_write = write_builder.new_write()
table_commit = write_builder.new_commit()

Writers and their committer must use the same identity. Use different identities for independent applications. Batch and postpone fixed-bucket builders expose the same method. Changing a builder affects only operations created afterward.

Native Write and Commit​

With the pypaimon-rust extension installed, set write.native.enabled=true to use Rust for Parquet/BLOB writes and commit.native.enabled=true to use Rust for REST snapshot publication. Batch and stream writers retain the same prepare-and-commit lifecycle. Native commits also support overwrite, truncate_table(), truncate_partitions() and commit callbacks. Overwrite and truncate record an OVERWRITE snapshot even when no files match, and remove index metadata for the replaced partitions. Physical files remain available to historical snapshots. Changelog-only commits also publish a snapshot. For Parquet tables, both paths support manifest.compression=null, deflate, snappy, zstd, bzip2 and xz, including manifests retained from earlier snapshots. Overwrite and truncate partition specs convert textual values to the declared partition type. None and the configured default partition name select NULL partitions; malformed values are rejected before publication.

Standard REST tables with query-auth.enabled=true can write and commit without requesting SELECT permission. The REST server still authorizes snapshot publication. Row filters and column masks continue to apply to explicit reads; internal sequence-number and HASH-index restoration reads manifest metadata. For query-authorized targets, Native row-ID updates, predicate updates/deletes, upserts, shard updates and MERGE require an unrestricted whole-table SELECT grant before reading target rows or evaluating assignments. A row filter, column mask or denied grant rejects the operation; ordinary writes still require no SELECT grant. MERGE table sources obtain their own grants independently from the target, including historical snapshots and branches. Query-authorized cross-partition index bootstrap remains unsupported.

Whole-partition predicate deletes can use Native on primary-key tables configured with input changelogs. Native upserts also accept fully NULL source key columns with a different physical type, including selected NULL dictionary values.

Native selection checks the formats used by the operation. An unused changelog-file.format does not prevent Native Parquet writes when no INPUT changelog is produced, including batch overwrite. Likewise, vector.file.format does not prevent Native updates on tables without VECTOR fields. These operations continue to write Parquet/BLOB files; an active dedicated format must still be supported by the Native path.

The same Native selection applies to direct construction of TableWrite, BatchTableWrite, StreamTableWrite and PostponeFixedBucketBatchTableWrite. Their public classes, constructor signatures and Arrow/Pandas batch overrides are preserved. Python execution is initialized only when that backend is selected.

write_ray() uses the shared Python coordinator and lets workers select Native writers. For postpone fixed-bucket writes, overwrite plans use the new input unless an explicit bucket plan was supplied. A complete primary-key with_write_type() leaves the writer unchanged; primary-key column subsets remain unsupported.

Append with_write_type() preserves the selected column order. Both the full table schema and the selected schema are accepted as Arrow input; only selected columns are written. Selected Arrow inputs can also carry partition, hash and configured row-kind columns used for routing and event handling. The selected columns must retain their declared order. Named Rows bind fields by name. Reads restore omitted partition columns from split metadata. Selected-schema inputs must include the routing columns they need; write_arrow(data, bucket=...) also accepts a bucket assigned upstream.

Configure with_write_type() before the first effective write. Empty batches and ignored events do not lock the selection. Row-to-Arrow encoding failures before execution starts also leave it configurable. After writing, the same ordered selection can be set again, but changing its columns or their order is rejected, including after prepare_commit() or idle writer cleanup. Create a new writer to use a different write type.

Changing with_blob_consumer() affects newly created partition/bucket writers. Active bucket writers retain their consumer across stream prepare_commit(identifier) calls. Unaware bucket writers retire on an idle prepare; fixed bucket writers also require a newer committed identifier from the same commit user. A reopened bucket uses the current consumer.

Batch prepare_commit() accepts no arguments and can be called once. Stream prepare_commit(identifier) requires an integer-like value in the signed 64-bit range. These call boundaries are the same for Python and Native writers.

Native commit selection requires a standard REST catalog and a reconstructible built-in FileIO. Other catalogs and custom Python FileIO implementations retain the Python path. Binary partition keys require partition.legacy-name=false for portable names across implementations.

To replace data, configure overwrite on the write builder before creating the writer and commit object:

# overwrite whole table
write_builder = table.new_batch_write_builder().overwrite()

# overwrite partition 'dt=2024-01-01'
write_builder = table.new_batch_write_builder().overwrite({'dt': '2024-01-01'})

Parquet Page Indexes​

Python Parquet writers write page indexes by default on PyArrow >= 13; older versions omit them. Set parquet.write-page-index.enabled=false to disable them, or true to require them. Explicit true on PyArrow < 13 fails before creating files.

This affects new files only, independently of read-side filtering. When enabled, page-level statistics move from page headers into the index. Check reader compatibility when upgrading; PyArrow reads do not currently use these indexes.

Manifest Merging​

Python commits retain existing manifest files without merging or rewriting them, including during commit retries. Run manifest compaction through a centralized maintenance service, for example using the Java engines' compact_manifest procedure.

The table options manifest.merge-min-count, manifest.merge.skip-on-write-only, and write-only do not enable manifest merging in Python. manifest.target-file-size still controls the size of newly written manifest files.

Commit Callback​

You can register CommitCallback instances on a TableCommit to be notified after each successful snapshot commit. This is useful for post-commit actions such as syncing metadata to external systems.

Implementations must be idempotent — a callback may be invoked more than once for the same commit if a failure occurs right after the commit succeeds.

from pypaimon.write.commit_callback import CommitCallback, CommitCallbackContext

class MyCallback(CommitCallback):
def call(self, context: CommitCallbackContext) -> None:
print(f"Committed snapshot {context.snapshot.id}, "
f"{len(context.commit_entries)} entries, "
f"identifier {context.identifier}")

def close(self) -> None:
pass # release resources if needed

write_builder = table.new_batch_write_builder()
table_write = write_builder.new_write()
table_commit = write_builder.new_commit()
table_commit.add_commit_callback(MyCallback())

table_write.write_arrow(data)
table_commit.commit(table_write.prepare_commit())
table_write.close()
table_commit.close()

CommitCallbackContext provides:

FieldTypeDescription
snapshotSnapshotThe committed snapshot (id, commit_kind, time_millis, next_row_id, …)
commit_entriesList[ManifestEntry]Delta manifest entries in this commit (each carries file.first_row_id when row-tracking is enabled)
identifierintCommit identifier