# iceberg.maintain

> A module of iceberg's API.

Expiring snapshots, and deleting the files that nothing points at any more.

Expiry is two decisions, in this order:

1. **Which snapshots go.** Never the current one, never one a branch or tag
   names, and never so many that fewer than `keep_last` are left. Of what
   remains, the ones older than `older_than_ms`.
2. **Which files go with them.** A file is deleted only when *no retained
   snapshot still lists it live*. That is stronger than "it was in an expired
   snapshot": a data file added five snapshots ago is in every snapshot since,
   and expiring the oldest of them must not touch it. So the retained
   snapshots are walked first and every manifest list, manifest and live entry
   they name is put in a keep set; only then is the expired snapshots' own
   inventory subtracted from it.

A `DELETED` entry does not protect a file — that is the entry that says it is
gone — but an older *retained* snapshot that still lists it `ADDED` or
`EXISTING` does, which is why the keep set is a union over all of them.

The new metadata is written **before** any file is deleted. A crash in between
leaves orphans, which cost storage; the other order leaves a snapshot pointing
at files that are not there, which costs the table. `dry_run` stops after the
decision and returns the list, which is the only safe way to find out what an
expiry would do.

Metadata files (`*.metadata.json`) are left alone. Java deletes them under
`write.metadata.delete-after-commit.enabled` with its own retention count, and
that is a separate policy from snapshot expiry: the metadata log is how a
reader reaches a snapshot by timestamp in the first place.

## Structs

### `ExpireResult`

```mojo
struct ExpireResult
```

What an expiry decided, and what it did about it.

Implements: `Copyable`, `Defaultable`, `Movable`

Fields:

- `expired: List[Int64]`: Snapshot ids removed from the metadata.
- `retained: List[Int64]`
- `deleted_files: List[String]`: Data and delete files nothing retained points at any more.
- `deleted_manifests: List[String]`: Manifests and manifest lists, likewise.
- `dry_run: Bool`

#### `__init__`

```mojo
def __init__(out self)
```

#### `total_deleted`

```mojo
def total_deleted(self) -> Int
```

## Functions

### `choose_expired`

```mojo
def choose_expired(metadata: TableMetadata, older_than_ms: Int64, keep_last: Int) -> List[Int64]
```

The snapshot ids an expiry would remove, oldest first.

The current snapshot and every snapshot a ref names are never candidates,
and `keep_last` is a floor on how many survive in total.

### `expire_snapshots`

```mojo
def expire_snapshots(io: FileIO, metadata: TableMetadata, older_than_ms: Int64 = Int64(-1), keep_last: Int = Int(1), dry_run: Bool = False) -> Tuple[TableMetadata, ExpireResult]
```

Decide the expiry and, unless `dry_run`, carry out the deletions.

Returns the pruned metadata and a report. The caller commits the metadata
— and must do so *before* the files go, which is why deletion is the last
thing this does rather than the first.

### `delete_expired_files`

```mojo
def delete_expired_files(io: FileIO, result: ExpireResult) -> Int
```

Remove what an expiry decided is unreachable. Content first.

A file that is already gone is not an error: an expiry that was
interrupted, or two of them racing, both end at the same place.
