# iceberg.update_schema

> A module of iceberg's API.

Schema evolution: add columns, rename them, make them optional, widen them.

var u = UpdateSchema()
    u.add_column("note", "string")
    u.add_nested_column("addr", "zip", "int")
    u.rename_column("amount", "total")
    u.make_column_optional("region")
    u.update_column_type("qty", "long")
    _ = table.update_schema(u)                 # a filesystem table
    _ = rest.update_schema("db", "orders", u)  # or through a catalog

The shape is Java's and PyIceberg's `UpdateSchema`: a list of changes that
is applied to the table's current schema when the commit is prepared, so a
commit that loses a race re-applies the same changes to whatever schema won.
What it produces is what the spec asks for:

* every new field — the column itself, then the fields, elements, keys and
  values inside its type — gets a fresh id above the table's
  `last-column-id`, which moves up to the highest id handed out;
* the result is a **new** schema with the next free `schema-id`, appended to
  `schemas`, and `current-schema-id` points at it. A result equal to a
  schema the table already has reuses that schema's id instead, as Java does;
  a result equal to the current schema commits nothing at all;
* through a REST catalog the request carries `assert-table-uuid`,
  `assert-current-schema-id` and `assert-last-assigned-field-id`, and the
  updates are `add-schema` then `set-current-schema` with `-1` ("the schema
  just added").

Nothing here rewrites a data file. A file written before a column existed has
no column with that id, and the reader's projection by field id already
produces null for it; a widened column is read at the file's width and cast.

**Only compatible changes.** An added column is optional — rows already in the
table have no value for it, and there is no `initial-default` support on the
write side. Type changes are the spec's promotions only: `int`→`long`,
`float`→`double`, and `decimal(P, S)`→`decimal(P', S)` with `P' > P`. Columns
are not deleted or moved. `union_by_name` refuses any type difference rather
than promoting, so a caller handing it a schema always learns about a type
change instead of having it applied.

## Structs

### `UpdateSchema`

```mojo
struct UpdateSchema
```

Schema changes, recorded now and applied at commit time.

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

Fields:

- `ops: List[_SchemaOp]`

#### `__init__`

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

#### `is_empty`

```mojo
def is_empty(self) -> Bool
```

#### `add_column`

```mojo
def add_column(mut self, name: String, type: String, doc: String = "")
```

Add an optional top-level column.

`type` is the Iceberg JSON type: a primitive's spelling (`long`,
`decimal(10, 2)`, quoted or not) or a `struct`/`list`/`map` object.
Field ids inside a nested type are placeholders; they are replaced by
fresh ones.

#### `add_nested_column`

```mojo
def add_nested_column(mut self, parent: String, name: String, type: String, doc: String = "")
```

Add an optional field to the struct at `parent` (`addr`, `a.b`).

A `parent` that is a list adds to its element struct, and a map to its
value struct, the way Java resolves a parent.

#### `rename_column`

```mojo
def rename_column(mut self, path: String, new_name: String)
```

Rename a field. Its id, and so every data file, is unaffected.

#### `make_column_optional`

```mojo
def make_column_optional(mut self, path: String)
```

Let a required field (or a list element, or a map value) be null.

#### `update_column_type`

```mojo
def update_column_type(mut self, path: String, new_type: String)
```

Widen a primitive the way the spec allows: `int`→`long`, `float`→`double`, `decimal(P, S)`→`decimal(P', S)` with `P' > P`.

#### `union_by_name`

```mojo
def union_by_name(mut self, schema: Schema)
```

Make the table's schema hold every field of `schema`, matched by name: a missing field is added (it must be optional), a field `schema` marks optional becomes optional, a changed `doc` is taken, and fields `schema` does not mention are kept. Structs are merged at any depth, including a list's element and a map's value. A field whose type differs raises: no promotion, no replacement.

`schema`'s own field ids are ignored — only names count.

#### `apply`

```mojo
def apply(self, base: Schema, last_column_id: Int) -> Tuple[Schema, Int]
```

The changes applied to `base`: the new schema (still carrying `base`'s id) and the new `last-column-id`.

### `SchemaCommit`

```mojo
struct SchemaCommit
```

What committing an `UpdateSchema` would do to one table metadata.

Implements: `Copyable`, `Movable`

Fields:

- `metadata: TableMetadata`: The table metadata after the change. Not persisted.
- `schema: Schema`: The new current schema.
- `added: Bool`: A new schema was appended to `schemas` (else an existing one is current again).
- `changed: Bool`: False when the changes leave the current schema as it was; there is then nothing to commit.

## Functions

### `prepare_schema_update`

```mojo
def prepare_schema_update(metadata: TableMetadata, update: UpdateSchema) -> SchemaCommit
```

Apply `update` to `metadata`'s current schema and build the metadata a commit would write. Nothing is persisted: the caller commits it.

### `schema_update_body`

```mojo
def schema_update_body(namespace: String, table: String, current: TableMetadata, commit: SchemaCommit) -> String
```

The REST `CommitTableRequest` for a schema change.

The requirements are Java's: the table is the one we read, its current
schema is the one the changes were applied to, and no column id has been
handed out since — so a concurrent schema change is a 409, not two columns
with the same id.
