|
27 | 27 | from pyiceberg.schema import Schema |
28 | 28 | from pyiceberg.table.inspect import InspectTable, _readable_bound |
29 | 29 | from pyiceberg.table.snapshots import Snapshot |
30 | | -from pyiceberg.transforms import IdentityTransform |
| 30 | +from pyiceberg.transforms import BucketTransform, IdentityTransform |
31 | 31 | from pyiceberg.typedef import Record |
32 | | -from pyiceberg.types import NestedField, StringType |
| 32 | +from pyiceberg.types import DoubleType, FloatType, IntegerType, LongType, NestedField, PrimitiveType, StringType |
33 | 33 | from tests.catalog.test_base import InMemoryCatalog |
34 | 34 |
|
35 | 35 |
|
@@ -106,3 +106,148 @@ def test_inspect_manifests_preserves_empty_string_bounds(catalog: InMemoryCatalo |
106 | 106 | partition_summary = tbl.inspect.manifests().to_pydict()["partition_summaries"][0][0] |
107 | 107 | assert partition_summary["lower_bound"] == "" |
108 | 108 | assert partition_summary["upper_bound"] == "" |
| 109 | + |
| 110 | + |
| 111 | +def test_inspect_manifests_snapshot_selection(catalog: InMemoryCatalog) -> None: |
| 112 | + tbl = catalog.create_table("default.manifests", Schema(NestedField(1, "s", StringType()))) |
| 113 | + empty = tbl.inspect.manifests() |
| 114 | + assert empty.num_rows == 0 |
| 115 | + assert empty.equals(tbl.inspect.manifests(snapshot_id=None)) |
| 116 | + assert tbl.inspect.all_manifests().num_rows == 0 |
| 117 | + |
| 118 | + data = pa.table({"s": ["first"]}) |
| 119 | + tbl.append(data) |
| 120 | + first = tbl.current_snapshot() |
| 121 | + assert first is not None |
| 122 | + first_rows = tbl.inspect.manifests() |
| 123 | + tbl.append(data) |
| 124 | + current = tbl.current_snapshot() |
| 125 | + assert current is not None |
| 126 | + current_rows = tbl.inspect.manifests() |
| 127 | + |
| 128 | + assert empty.schema == first_rows.schema == current_rows.schema |
| 129 | + assert first_rows.equals(tbl.inspect.manifests(snapshot_id=first.snapshot_id)) |
| 130 | + assert current_rows.equals(tbl.inspect.manifests(snapshot_id=current.snapshot_id)) |
| 131 | + assert first_rows["path"].to_pylist() == [manifest.manifest_path for manifest in first.manifests(tbl.io)] |
| 132 | + assert current_rows["path"].to_pylist() == [manifest.manifest_path for manifest in current.manifests(tbl.io)] |
| 133 | + assert first_rows.num_rows == 1 |
| 134 | + assert current_rows.num_rows == 2 |
| 135 | + assert first_rows["partition_summaries"].to_pylist() == [[]] |
| 136 | + |
| 137 | + all_rows = tbl.inspect.all_manifests() |
| 138 | + assert ( |
| 139 | + all_rows.schema.remove(all_rows.schema.get_field_index("key_metadata")).remove( |
| 140 | + all_rows.schema.get_field_index("reference_snapshot_id") |
| 141 | + ) |
| 142 | + == empty.schema |
| 143 | + ) |
| 144 | + for snapshot, expected in [(first, first_rows), (current, current_rows)]: |
| 145 | + rows = [row for row in all_rows.to_pylist() if row.pop("reference_snapshot_id") == snapshot.snapshot_id] |
| 146 | + assert all(row.pop("key_metadata") is None for row in rows) |
| 147 | + assert rows == expected.to_pylist() |
| 148 | + assert all_rows.num_rows == 3 # Shared manifests remain repeated for each reference snapshot. |
| 149 | + |
| 150 | + tbl.maintenance.expire_snapshots().by_id(first.snapshot_id).commit() |
| 151 | + with pytest.raises(ValueError, match=f"Cannot find snapshot with ID {first.snapshot_id}"): |
| 152 | + tbl.inspect.manifests(snapshot_id=first.snapshot_id) |
| 153 | + |
| 154 | + |
| 155 | +@pytest.mark.parametrize("snapshot_id", [0, -1, 9223372036854775807]) |
| 156 | +@pytest.mark.parametrize("populated", [False, True]) |
| 157 | +def test_inspect_manifests_invalid_snapshot(catalog: InMemoryCatalog, snapshot_id: int, populated: bool) -> None: |
| 158 | + tbl = catalog.create_table("default.invalid_snapshot", Schema(NestedField(1, "s", StringType()))) |
| 159 | + if populated: |
| 160 | + tbl.append(pa.table({"s": ["value"]})) |
| 161 | + with pytest.raises(ValueError, match=f"Cannot find snapshot with ID {snapshot_id}"): |
| 162 | + tbl.inspect.manifests(snapshot_id=snapshot_id) |
| 163 | + |
| 164 | + |
| 165 | +@pytest.mark.parametrize("value", ["old", "", None]) |
| 166 | +def test_inspect_manifests_schema_and_partition_evolution(catalog: InMemoryCatalog, value: str | None) -> None: |
| 167 | + schema = Schema(NestedField(1, "s", StringType()), NestedField(2, "id", IntegerType())) |
| 168 | + spec = PartitionSpec(PartitionField(1, 1000, IdentityTransform(), "s_part")) |
| 169 | + tbl = catalog.create_table("default.evolved_manifests", schema, partition_spec=spec) |
| 170 | + tbl.append(pa.table({"s": [value], "id": [7]}, schema=pa.schema([("s", pa.string()), ("id", pa.int32())]))) |
| 171 | + first = tbl.current_snapshot() |
| 172 | + assert first is not None |
| 173 | + first_rows = tbl.inspect.manifests() |
| 174 | + assert first_rows["partition_summaries"].to_pylist() == [ |
| 175 | + [{"contains_null": value is None, "contains_nan": False, "lower_bound": value, "upper_bound": value}] |
| 176 | + ] |
| 177 | + |
| 178 | + with tbl.update_schema() as update: |
| 179 | + update.rename_column("s", "renamed") |
| 180 | + assert tbl.inspect.manifests(first.snapshot_id).equals(first_rows) |
| 181 | + with tbl.update_spec() as update: |
| 182 | + update.remove_field("s_part") |
| 183 | + update.add_field("id", BucketTransform(8), "id_bucket") |
| 184 | + with tbl.update_schema() as update: |
| 185 | + update.delete_column("renamed") |
| 186 | + # Reusing a name must not cause the old source ID to be resolved to the new type. |
| 187 | + update.add_column("s", LongType()) |
| 188 | + tbl.append(pa.table({"s": [99], "id": [8]}, schema=pa.schema([("s", pa.int64()), ("id", pa.int32())]))) |
| 189 | + current = tbl.current_snapshot() |
| 190 | + assert current is not None |
| 191 | + tbl = catalog.load_table(tbl.name()) |
| 192 | + |
| 193 | + assert tbl.inspect.manifests(first.snapshot_id).equals(first_rows) |
| 194 | + current_rows = tbl.inspect.manifests() |
| 195 | + assert current_rows.equals(tbl.inspect.manifests(current.snapshot_id)) |
| 196 | + by_spec = {row["partition_spec_id"]: row for row in current_rows.to_pylist()} |
| 197 | + assert by_spec[spec.spec_id] == first_rows.to_pylist()[0] |
| 198 | + bucket = str(BucketTransform(8).transform(IntegerType())(8)) |
| 199 | + assert by_spec[tbl.spec().spec_id]["partition_summaries"] == [ |
| 200 | + {"contains_null": False, "contains_nan": False, "lower_bound": bucket, "upper_bound": bucket} |
| 201 | + ] |
| 202 | + all_rows = tbl.inspect.all_manifests().to_pylist() |
| 203 | + assert len(all_rows) == 3 |
| 204 | + assert [row["partition_summaries"] for row in all_rows if row["path"] == first_rows["path"][0].as_py()] == [ |
| 205 | + first_rows["partition_summaries"][0].as_py(), |
| 206 | + first_rows["partition_summaries"][0].as_py(), |
| 207 | + ] |
| 208 | + |
| 209 | + |
| 210 | +@pytest.mark.parametrize("missing_schema_id", [None, 999]) |
| 211 | +def test_inspect_manifests_schema_fallback(catalog: InMemoryCatalog, missing_schema_id: int | None) -> None: |
| 212 | + schema = Schema(NestedField(1, "s", StringType())) |
| 213 | + spec = PartitionSpec(PartitionField(1, 1000, IdentityTransform(), "s")) |
| 214 | + tbl = catalog.create_table("default.legacy_manifests", schema, partition_spec=spec) |
| 215 | + tbl.append(pa.table({"s": ["legacy"]})) |
| 216 | + snapshot = tbl.current_snapshot() |
| 217 | + assert snapshot is not None |
| 218 | + expected = tbl.inspect.manifests() |
| 219 | + # Emulate legacy metadata while retaining the real local manifest and data files. |
| 220 | + tbl.metadata = tbl.metadata.model_copy(update={"snapshots": [snapshot.model_copy(update={"schema_id": missing_schema_id})]}) |
| 221 | + if missing_schema_id is None: |
| 222 | + assert tbl.inspect.manifests(snapshot.snapshot_id).equals(expected) |
| 223 | + assert tbl.inspect.manifests().equals(expected) |
| 224 | + else: |
| 225 | + with pytest.warns(UserWarning, match=f"Metadata does not contain schema with id: {missing_schema_id}"): |
| 226 | + assert tbl.inspect.manifests(snapshot.snapshot_id).equals(expected) |
| 227 | + |
| 228 | + |
| 229 | +@pytest.mark.parametrize( |
| 230 | + ("original_type", "promoted_type", "arrow_type", "value"), |
| 231 | + [(IntegerType(), LongType(), pa.int32(), 7), (FloatType(), DoubleType(), pa.float32(), 1.5)], |
| 232 | +) |
| 233 | +def test_inspect_manifests_promoted_partition_source( |
| 234 | + catalog: InMemoryCatalog, |
| 235 | + original_type: PrimitiveType, |
| 236 | + promoted_type: PrimitiveType, |
| 237 | + arrow_type: pa.DataType, |
| 238 | + value: int | float, |
| 239 | +) -> None: |
| 240 | + schema = Schema(NestedField(1, "p", original_type)) |
| 241 | + spec = PartitionSpec(PartitionField(1, 1000, IdentityTransform(), "p")) |
| 242 | + tbl = catalog.create_table("default.promoted_manifests", schema, partition_spec=spec) |
| 243 | + tbl.append(pa.table({"p": [value]}, schema=pa.schema([("p", arrow_type)]))) |
| 244 | + first = tbl.current_snapshot() |
| 245 | + assert first is not None |
| 246 | + expected = tbl.inspect.manifests() |
| 247 | + with tbl.update_schema() as update: |
| 248 | + update.update_column("p", field_type=promoted_type) |
| 249 | + tbl.append(pa.table({"p": [value]}, schema=pa.schema([("p", pa.int64() if isinstance(value, int) else pa.float64())]))) |
| 250 | + |
| 251 | + assert tbl.inspect.manifests(first.snapshot_id).equals(expected) |
| 252 | + for row in tbl.inspect.manifests().to_pylist() + tbl.inspect.all_manifests().to_pylist(): |
| 253 | + assert row["partition_summaries"] == expected["partition_summaries"][0].as_py() |
0 commit comments