# iceberg.catalog.rest

> A module of iceberg's API.

Iceberg REST catalog — URLs, headers, responses, and the socket.

The socket is objectstore.mojo's `HttpClient`, a libcurl-backed client behind a
fixed-arity C shim. It exists because no Mojo HTTP package resolves from conda
on either of this repo's environments (floki, flare, lightbug-http and
fire-http were all tried; all reported "no candidates were found"), and an
Iceberg REST catalog is HTTPS or nothing.

Three layers, separable and separately tested:

* `RestCatalogConfig` builds the URLs and headers for `GET /v1/config`,
  `loadTable`, `listNamespaces` and `listTables`, including the `prefix` a
  config response can prepend to every subsequent path, bearer authorization,
  and the `X-Iceberg-Access-Delegation: vended-credentials` header.
* `LoadTableResult` parses a `loadTable` response body — the inline
  `metadata`, its `metadata-location`, per-table `config`, and
  `storage-credentials` — into a real `TableMetadata`.
* `RestCatalog` puts a client behind them, maps HTTP status codes onto Iceberg
  error types, and hands back a `Table` whose `FileIO` is already configured
  with whatever the catalog vended.

## Structs

### `StorageCredential`

```mojo
struct StorageCredential
```

One entry of a `loadTable` response's `storage-credentials`.

Implements: `Copyable`, `Movable`

Fields:

- `prefix: String`
- `config: Dict[String, String]`

### `LoadTableResult`

```mojo
struct LoadTableResult
```

A parsed `LoadTableResult` body.

Implements: `Copyable`, `Movable`

Fields:

- `metadata_location: String`
- `has_metadata_location: Bool`
- `metadata: TableMetadata`
- `config: Dict[String, String]`
- `storage_credentials: List[StorageCredential]`

#### `__init__`

```mojo
def __init__(out self, var metadata_location: String, has_metadata_location: Bool, var metadata: TableMetadata, var config: Dict[String, String], var storage_credentials: List[StorageCredential])
```

#### `parse`

```mojo
def parse(body: String) -> Self
```

### `RestCatalogConfig`

```mojo
struct RestCatalogConfig
```

URL and header construction for the REST catalog API.

Implements: `Copyable`, `Movable`

Fields:

- `uri: String`
- `prefix: String`: The `prefix` a `GET /v1/config` response asks clients to insert.
- `token: String`
- `has_token: Bool`
- `warehouse: String`
- `vend_credentials: Bool`
- `extra_headers: List[Header]`

#### `__init__`

```mojo
def __init__(out self, var uri: String)
```

#### `with_token`

```mojo
def with_token(mut self, var token: String)
```

#### `with_warehouse`

```mojo
def with_warehouse(mut self, var warehouse: String)
```

#### `config_url`

```mojo
def config_url(self) -> String
```

`GET /v1/config` — always unprefixed; it is what supplies a prefix.

#### `namespaces_url`

```mojo
def namespaces_url(self, parent: String = "") -> String
```

#### `tables_url`

```mojo
def tables_url(self, namespace: String) -> String
```

#### `commit_table_url`

```mojo
def commit_table_url(self, namespace: String, table: String) -> String
```

#### `load_table_url`

```mojo
def load_table_url(self, namespace: String, table: String) -> String
```

#### `headers`

```mojo
def headers(self) -> List[Header]
```

#### `apply_config`

```mojo
def apply_config(mut self, body: String)
```

Absorb a `GET /v1/config` response: `overrides`, `defaults`, `prefix`.

#### `parse_namespaces`

```mojo
def parse_namespaces(body: String) -> List[String]
```

`ListNamespacesResponse` -> dotted namespace names.

#### `parse_tables`

```mojo
def parse_tables(body: String) -> List[String]
```

`ListTablesResponse` -> table names.

### `RestCatalog`

```mojo
struct RestCatalog
```

An Iceberg REST catalog reachable over HTTP(S).

```mojo
var cat = RestCatalog("https://catalog.example.com")
cat.config.with_token(token)
cat.config.vend_credentials = True
cat.connect()                        # GET /v1/config, absorb the prefix
var t = cat.load_table("db", "orders")
```

`connect()` is separate from construction because it does I/O, and a caller
that already knows the prefix (or is talking to a server without one) can
skip it.

Implements: `Copyable`, `Movable`

Fields:

- `config: RestCatalogConfig`
- `client: HttpClient`
- `io: FileIO`: The `FileIO` template every loaded table starts from — rebases and storage properties set here survive into each table.

#### `__init__`

```mojo
def __init__(out self, var uri: String)
```

```mojo
def __init__(out self, var config: RestCatalogConfig, var io: FileIO)
```

#### `connect`

```mojo
def connect(mut self)
```

`GET /v1/config` — absorbs `overrides`, `defaults` and `prefix`.

#### `list_namespaces`

```mojo
def list_namespaces(self, parent: String = "") -> List[String]
```

#### `list_tables`

```mojo
def list_tables(self, namespace: String) -> List[String]
```

#### `load_table_result`

```mojo
def load_table_result(self, namespace: String, table: String) -> LoadTableResult
```

#### `table_exists`

```mojo
def table_exists(self, namespace: String, table: String) -> Bool
```

#### `load_table`

```mojo
def load_table(self, namespace: String, table: String) -> Table
```

Load a table, configuring its `FileIO` from what the catalog says.

The table's own `config` becomes storage properties (`s3.endpoint`,
`s3.region`, …) and every `storage-credentials` entry is registered
under its prefix, so a vended, per-prefix STS credential is used for
exactly the objects it was issued for.

#### `commit_append`

```mojo
def commit_append(self, namespace: String, table: String, data_files: List[DataFile], retries: Int = Int(4)) -> Table
```

Commit already-written data files as one append snapshot.

Optimistic concurrency, the way the REST spec defines it: the request
carries `assert-table-uuid` and `assert-ref-snapshot-id` for `main`,
so a server whose `main` has moved answers **409** and this reloads,
rebuilds the snapshot against the new parent — the data files are
still valid, only the manifest list has to be rewritten — and tries
again. A **5xx** is *not* retried: the commit may well have been
applied, and the caller is told the state is unknown rather than
being handed a second attempt at the same append.

Each attempt writes a fresh manifest and manifest list; the ones a
refused attempt wrote are orphaned, exactly as in every other
implementation, and are nobody's to read.

There is no `next-row-id` update in the spec's `TableUpdate` list. A
v3 server derives it from the snapshot's `first-row-id` plus its
`added-rows`, both of which `add-snapshot` carries.

#### `commit_change`

```mojo
def commit_change(self, namespace: String, table: String, kind: Int, filter_dsl: String, batches: List[RecordBatch], mode: String, retries: Int) -> Table
```

A delete or an overwrite, committed the way an append is.

The `CommitTableRequest` is identical — one `add-snapshot` and one
`set-snapshot-ref` behind the same two requirements — because the spec
has no per-operation update action: what the snapshot *is* lives
entirely in the manifest list it names. So the retry story is the
same too, and one 409 reload re-plans the whole operation against the
new parent, because a delete planned against a snapshot that has moved
may now be missing rows a concurrent writer added.

#### `update_schema`

```mojo
def update_schema(self, namespace: String, table: String, update: UpdateSchema, retries: Int = Int(4)) -> Table
```

Commit schema changes through the catalog.

The request asserts the table's uuid, its current schema id and its
last assigned column id, and sends `add-schema` + `set-current-schema`
(see `iceberg.update_schema`). A 409 reloads and applies the same
changes to the schema that won; a 5xx is reported as unknown state,
as a commit of data is. Changes that leave the schema as it is send
nothing.

#### `delete_where`

```mojo
def delete_where(self, namespace: String, table: String, filter_dsl: String, mode: String = "", retries: Int = Int(4)) -> Table
```

`DELETE FROM t WHERE <filter>`, committed through the catalog.

#### `overwrite`

```mojo
def overwrite(self, namespace: String, table: String, batches: List[RecordBatch], filter_dsl: String = "[\22true\22]", retries: Int = Int(4)) -> Table
```

Delete what the filter matches and add `batches`, in one snapshot.

#### `dynamic_partition_overwrite`

```mojo
def dynamic_partition_overwrite(self, namespace: String, table: String, batches: List[RecordBatch], retries: Int = Int(4)) -> Table
```

Replace exactly the partitions the new rows land in.

#### `append`

```mojo
def append(self, namespace: String, table: String, batches: List[RecordBatch], retries: Int = Int(4)) -> Table
```

Write `batches` into the table's location, then commit them.

#### `create_table`

```mojo
def create_table(mut self, namespace: String, table: String, schema: Schema, spec: PartitionSpec = PartitionSpec.unpartitioned(Int(0)), var properties: Dict[String, String] = Dict(), format_version: Int = Int(2), location: String = "") -> Table
```

`POST .../tables` — a `CreateTableRequest`.

The format version travels as the `format-version` property, which is
how every REST catalog is told; there is no field for it in the
request body.

## Functions

### `encode_namespace`

```mojo
def encode_namespace(dotted: String) -> String
```

Multipart namespaces travel as one path segment joined by 0x1F.

### `url_encode`

```mojo
def url_encode(s: String) -> String
```

Percent-encode everything outside the unreserved set.

### `rest_error`

```mojo
def rest_error(what: String, resp: Response) -> Error
```

Turn a REST error response into an Error naming what actually happened.

The spec's `ErrorModel` carries `{"error": {"message", "type", "code"}}`;
servers that do not send one still leave a status code worth reporting.

### `commit_append_body`

```mojo
def commit_append_body(namespace: String, table: String, current: TableMetadata, result: AppendResult) -> String
```

The `CommitTableRequest` an append produces.

Two requirements, which together are exactly what an optimistic append
needs: the table must still be the one we read (`assert-table-uuid`), and
`main` must still point where it did when the snapshot's parent was
chosen (`assert-ref-snapshot-id`, with a null id when the table has no
snapshot yet).

## Aliases

### `ACCESS_DELEGATION_HEADER`

```mojo
comptime ACCESS_DELEGATION_HEADER = String("X-Iceberg-Access-Delegation")
```

### `COMMIT_DELETE`

```mojo
comptime COMMIT_DELETE = 0
```

### `COMMIT_OVERWRITE`

```mojo
comptime COMMIT_OVERWRITE = 1
```

### `COMMIT_DYNAMIC`

```mojo
comptime COMMIT_DYNAMIC = 2
```

### `VENDED_CREDENTIALS`

```mojo
comptime VENDED_CREDENTIALS = String("vended-credentials")
```

### `REMOTE_SIGNING`

```mojo
comptime REMOTE_SIGNING = String("remote-signing")
```
