|
26 | 26 | import pyiceberg.manifest as manifest_module |
27 | 27 | from pyiceberg.avro.codecs import AvroCompressionCodec |
28 | 28 | from pyiceberg.avro.file import AvroFile, AvroOutputFile |
| 29 | +from pyiceberg.exceptions import ValidationError |
29 | 30 | from pyiceberg.io import load_file_io |
30 | 31 | from pyiceberg.io.pyarrow import PyArrowFileIO |
31 | 32 | from pyiceberg.manifest import ( |
@@ -1376,3 +1377,104 @@ def test_negative_manifest_cache_size_raises_value_error(monkeypatch: pytest.Mon |
1376 | 1377 | finally: |
1377 | 1378 | monkeypatch.delenv("PYICEBERG_MANIFEST_CACHE_SIZE", raising=False) |
1378 | 1379 | importlib.reload(manifest_module) |
| 1380 | + |
| 1381 | + |
| 1382 | +@pytest.mark.parametrize("content", [ManifestContent.DATA, ManifestContent.DELETES]) |
| 1383 | +def test_write_manifest_content( |
| 1384 | + generated_manifest_file_file_v2: str, |
| 1385 | + test_schema: Schema, |
| 1386 | + test_partition_spec: PartitionSpec, |
| 1387 | + content: ManifestContent, |
| 1388 | +) -> None: |
| 1389 | + """A v2 manifest must record the content it was asked to write. |
| 1390 | +
|
| 1391 | + Without this the writer always claims `data`, so a delete manifest written by |
| 1392 | + PyIceberg would be read back as a data manifest and its entries applied as |
| 1393 | + live data files. |
| 1394 | + """ |
| 1395 | + io = load_file_io() |
| 1396 | + snapshot = Snapshot( |
| 1397 | + snapshot_id=25, |
| 1398 | + parent_snapshot_id=19, |
| 1399 | + timestamp_ms=1602638573590, |
| 1400 | + manifest_list=generated_manifest_file_file_v2, |
| 1401 | + summary=Summary(Operation.APPEND), |
| 1402 | + schema_id=3, |
| 1403 | + ) |
| 1404 | + manifest_entries = snapshot.manifests(io)[0].fetch_manifest_entry(io) |
| 1405 | + |
| 1406 | + with TemporaryDirectory() as tmpdir: |
| 1407 | + tmp_avro_file = tmpdir + "/test_write_manifest_content.avro" |
| 1408 | + with write_manifest( |
| 1409 | + format_version=2, |
| 1410 | + spec=test_partition_spec, |
| 1411 | + schema=test_schema, |
| 1412 | + output_file=io.new_output(tmp_avro_file), |
| 1413 | + snapshot_id=8744736658442914487, |
| 1414 | + avro_compression="deflate", |
| 1415 | + content=content, |
| 1416 | + ) as writer: |
| 1417 | + for entry in manifest_entries: |
| 1418 | + writer.add_entry(entry) |
| 1419 | + new_manifest = writer.to_manifest_file() |
| 1420 | + |
| 1421 | + assert new_manifest.content == content |
| 1422 | + _verify_metadata_with_fastavro( |
| 1423 | + tmp_avro_file, |
| 1424 | + {"content": "data" if content == ManifestContent.DATA else "deletes"}, |
| 1425 | + ) |
| 1426 | + |
| 1427 | + |
| 1428 | +def test_write_manifest_defaults_to_data_content( |
| 1429 | + generated_manifest_file_file_v2: str, |
| 1430 | + test_schema: Schema, |
| 1431 | + test_partition_spec: PartitionSpec, |
| 1432 | +) -> None: |
| 1433 | + """Callers that do not ask for a content type still get a data manifest.""" |
| 1434 | + io = load_file_io() |
| 1435 | + snapshot = Snapshot( |
| 1436 | + snapshot_id=25, |
| 1437 | + parent_snapshot_id=19, |
| 1438 | + timestamp_ms=1602638573590, |
| 1439 | + manifest_list=generated_manifest_file_file_v2, |
| 1440 | + summary=Summary(Operation.APPEND), |
| 1441 | + schema_id=3, |
| 1442 | + ) |
| 1443 | + manifest_entries = snapshot.manifests(io)[0].fetch_manifest_entry(io) |
| 1444 | + |
| 1445 | + with TemporaryDirectory() as tmpdir: |
| 1446 | + tmp_avro_file = tmpdir + "/test_write_manifest_default_content.avro" |
| 1447 | + with write_manifest( |
| 1448 | + format_version=2, |
| 1449 | + spec=test_partition_spec, |
| 1450 | + schema=test_schema, |
| 1451 | + output_file=io.new_output(tmp_avro_file), |
| 1452 | + snapshot_id=8744736658442914487, |
| 1453 | + avro_compression="deflate", |
| 1454 | + ) as writer: |
| 1455 | + for entry in manifest_entries: |
| 1456 | + writer.add_entry(entry) |
| 1457 | + assert writer.to_manifest_file().content == ManifestContent.DATA |
| 1458 | + |
| 1459 | + |
| 1460 | +def test_write_manifest_v1_rejects_delete_content( |
| 1461 | + test_schema: Schema, |
| 1462 | + test_partition_spec: PartitionSpec, |
| 1463 | +) -> None: |
| 1464 | + """v1 has no delete files, so asking for a delete manifest must fail loudly. |
| 1465 | +
|
| 1466 | + `ManifestListWriterV1` already refuses to store one; rejecting it at the |
| 1467 | + writer keeps a v1 table from producing a manifest it could never reference. |
| 1468 | + """ |
| 1469 | + io = load_file_io() |
| 1470 | + with TemporaryDirectory() as tmpdir: |
| 1471 | + with pytest.raises(ValidationError, match="Cannot write delete manifests in a v1 table"): |
| 1472 | + write_manifest( |
| 1473 | + format_version=1, |
| 1474 | + spec=test_partition_spec, |
| 1475 | + schema=test_schema, |
| 1476 | + output_file=io.new_output(tmpdir + "/test_write_manifest_v1_deletes.avro"), |
| 1477 | + snapshot_id=8744736658442914487, |
| 1478 | + avro_compression="deflate", |
| 1479 | + content=ManifestContent.DELETES, |
| 1480 | + ) |
0 commit comments