17. PyIceberg の制約を実証する
これまでのサンプルは「動くこと」を確認してきました。samples/07_pitfalls.py は逆に「動かないこと」を確認します。PyIceberg でできないことを実際に踏んで、エラーメッセージや戻り値として記録するのが目的です。
対象は PyIceberg 0.11.1、確認日は 2026-07-17(2026-07-18 に再確認)です。ここで挙げる項目は PyIceberg の欠陥ではなく、実装の進行状況です。Iceberg の仕様は Java 実装が先行しており、Python 実装は追随している途中にあります。ただし、知らずに設計すると後から構成を組み直すことになるため、事前に把握しておく必要があります。
確かめたいこと
- format-version 3 へのアップグレードが通らないこと
write.delete.mode = merge-on-readを設定しても copy-on-write で実行されること- equality delete が書けないだけでなく読めないこと
- メンテナンス機能に compaction / orphan file 削除が無いこと
s3.path-style-accessが PyIceberg に存在せず、黙って無視されることupsertが SQL の MERGE INTO と同等ではないこと
背景
Iceberg のテーブル仕様は format-version で世代管理されています。v2 で position delete / equality delete による merge-on-read が入り、v3 で deletion vector と row lineage が加わりました。PyIceberg は v3 のメタデータ型(TableMetadataV3)を持っており読み取りは可能ですが、書き込みは制限されています。追跡 issue は apache/iceberg-python#1551 です。
merge-on-read(MoR)と copy-on-write(CoW)は更新の実装方式の違いです。CoW は対象行を含むデータファイルを丸ごと書き直します。MoR は delete ファイルを追加するだけで、読み取り時に適用します。書き込みが軽い代わりに読み取りが重くなり、compaction が前提になります。
equality delete は「この列がこの値の行を消す」という形式の delete ファイルです。Flink の UPSERT(write.upsert.enabled)はこの形式を使います。
処理の流れ
PyIceberg のバージョンを表示し、テーブル
lab.pitfallsを作って5行 append します。upgrade_table_version(3)を試みます。
with tbl.transaction() as tx:
tx.upgrade_table_version(3)write.delete.mode = merge-on-readを設定してからdeleteを実行し、warnings.catch_warnings(record=True)で警告を捕捉します。そのうえでinspect.delete_files()の行数を確認します。equality delete については、書けないため実際には作れません。PyIceberg のソースにある2つの raise を提示して代替します。1つは書き込み経路の
ValueError、もう1つは読み取り(REST scan planning)経路のNotImplementedErrorです。tbl.maintenanceの公開メソッドを列挙し、compaction / orphan 系が無いことを確認します。pyiceberg.ioにs3.path-style-accessに相当する定数が定義されていないことを、モジュールの大文字定数を走査して確認します。
has_path_style = any(
getattr(io_mod, n, None) == "s3.path-style-access"
for n in dir(io_mod) if n.isupper()
)tbl.upsertのシグネチャを表示し、実際に upsert を1回実行します。
実測結果
1. format-version 3
PyIceberg バージョン: 0.11.1
--- 【落とし穴 1】 format-version 3 は読めるが書けない
現在の format-version: 2
→ 既定は 2 です (TableProperties.DEFAULT_FORMAT_VERSION = 2)
v3 にアップグレードしてみます:
OK 期待どおり拒否されました: ValueError: Unsupported table format version: 3
このサンプルは当初 NotImplementedError を想定して書かれていましたが、実際にはその手前で ValueError になりました。0.11.1 のソースを追うと、v3 は二重にガードされています。
| 順 | 場所 | 例外 |
|---|---|---|
| 1 | table/__init__.py L338 の upgrade_table_version(format_version not in {1, 2}) |
ValueError: Unsupported table format version: 3 |
| 1’ | table/update/__init__.py L316(SUPPORTED_TABLE_FORMAT_VERSION = 2 と比較) |
同上 |
| 2 | table/metadata.py L578 の TableMetadataV3.model_dump_json |
NotImplementedError: Writing V3 is not yet supported |
手前の 1 で弾かれるため、2 には通常到達しません。到達する例外の種類は想定と異なりましたが、v3 へアップグレードできないという結論は変わりません。
型定義を見ると typedef.py L212 の TableVersion = Literal[1, 2, 3] は v3 を許容しているように読めます。しかし実際に書けるのは v2 までです。型定義だけを見て対応状況を判断すると誤ります。
実務上は、PyIceberg では deletion vector も row lineage も使えず、v3 を前提とした設計は現時点で成り立ちません。
2. merge-on-read
プロパティの設定自体は成功します。しかし delete の実行時に警告が出て、copy-on-write で処理されます。
テーブルプロパティを設定しました: write.delete.mode = merge-on-read
この状態で delete を実行します:
OK 警告が出ました: Merge on read is not yet supported, falling back to copy-on-write
OK delete ファイル数: 0 (CoW なので 0 が期待値)
inspect.delete_files() が 0 行であることから、position delete が1つも書かれていないと確認できます。プロパティは受け付けられて tbl.properties にも残るため、テーブル定義だけを見ると MoR で運用しているように見えます。実際の挙動との差は警告でしか現れません。
3. equality delete
書き込みができないため、このサンプルでは実データを作れていません。ソース上の raise で示しています。
ソース (table/__init__.py L2114):
raise ValueError("PyIceberg does not yet support equality deletes: ...")
ソース (table/__init__.py L1886, REST scan planning 経路):
raise NotImplementedError(f"PyIceberg does not yet support equality deletes: {delete_file.file_path}")
上の2つはそれぞれ 書き込み経路 L2114 と 読み取り経路 L1886 にあります。
後者が読み取りパスにあることが実務上の要点です。Flink や Spark が equality delete を書いたテーブルは、PyIceberg では開けません。この経路自体はラボ内で実行できていないため、実際の失敗を再現した結果としては未確認です。上流の書き込みエンジンが equality delete を使うかどうかは、PyIceberg を読み取り側に据える前に確認する必要があります。
4. メンテナンス機能
MaintenanceTable のメソッド: ['expire_snapshots', 'tbl']
OK expire_snapshots は存在する
OK compaction / rewrite_data_files は存在しない
OK remove_orphan_files は存在しない
公開されているのは expire_snapshots だけでした。実行自体は通ります。
expire_snapshots は使えます:
スナップショット数: 2 -> 2
(10年前より古いものを対象にしたので、何も消えません)
compaction が無いことは、15. メタデータ三層構造を覗く で見た小ファイル問題に直結します。あの実験では manifest ファイルのほうが data file より大きいという状態が発生していました。これを解消する手段が PyIceberg 側にありません。
5. s3.path-style-access
実在するキー: S3_ENDPOINT = 's3.endpoint'
実在するキー: S3_FORCE_VIRTUAL_ADDRESSING = 's3.force-virtual-addressing'
OK s3.path-style-access は定義されていない
s3.path-style-access は Java 版 Iceberg / Spark のキーです。PyIceberg の設定に書いてもエラーにはならず、未知のキーとして無視されます。プロパティが単なる dict 参照であるため、設定ミスが表面化しません。
一方で、MinIO や RustFS のような S3 互換ストレージに対しては、何も設定しなくても path-style になります。PyArrow の S3FileSystem は force_virtual_addressing の既定が False で、公式ドキュメントは次のように述べています。
If false, then virtual addressing is only enabled if endpoint_override is empty
s3.endpoint を設定した時点で endpoint_override が非空になるため、自動的に path-style が選ばれます。
混乱しやすいのは、同じ compose ファイルの中に2つの流儀が同居する点です。Java 側のコンテナには CATALOG_S3_PATH__STYLE__ACCESS=true が効き、Python 側には対応するキーが存在しません。
6. upsert
upsert のシグネチャ:
df = (必須)
join_cols = None
when_matched_update_all = True
when_not_matched_insert_all = True
case_sensitive = True
branch = main
snapshot_properties = {}
分岐の制御は when_matched_update_all と when_not_matched_insert_all の真偽値2つだけです。SQL の MERGE INTO にある「WHEN MATCHED AND 条件 THEN UPDATE SET 特定列」「WHEN MATCHED THEN DELETE」「複数の WHEN 句」は表現できません。
実行自体は期待どおり動きます。
更新: 1 行, 挿入: 1 行
id val
1 NEW
2 keep
3 inserted
id=1 が更新され、id=3 が挿入され、id=2 はそのまま残りました。ただし内部は delete ファイルを使わない CoW であり、対象を含むデータファイルが書き直されます。
ここから分かること
PyIceberg 0.11.1 は、読み取りと軽量な書き込み、メタデータの調査に向いています。inspect.* は充実しており、15. メタデータ三層構造を覗く のような調査はこれだけで完結します。DuckDB / Polars / pandas との連携も同様です。REST カタログ仕様の学習と検証にも使えます。
一方で次の用途には現時点で適しません。
- format-version 3 の機能(deletion vector、row lineage)を使う設計
- Flink / Spark が equality delete を書くテーブルの読み取り
- merge-on-read での更新(CoW に落ちる)
- compaction や orphan file 削除を含む運用の自動化
- 複数条件を持つ MERGE INTO 相当のロジック
現実的な構成は、PyIceberg で読み取りと軽い書き込みを担い、メンテナンスと重い DML は Spark に任せる形になります。「Python だけで Iceberg を運用する」は、少なくとも 0.11.1 の時点では成立しません。ブランチ運用についても、14. タイムトラベルと branch / tag で触れたとおり fast-forward が Spark procedure であるため、同じ結論になります。
これらの状況は実装の進行に伴って変わります。バージョンを上げる際は、ここで確認した各項目を再度実行して現況を確かめてください。運用面の背景は姉妹リポジトリの調査報告書(https://dobachi.github.io/iceberg-research/ )にまとめてあります。