# Tables and schemas
Nexus stores your data in tables, in the open Apache Iceberg format, grouped into schemas. The `default` schema always exists and is used unless you pass `schema=`. This page covers `nexus.tables` and `nexus.schemas`.
## Before you start [#before-you-start]
- Reading tables needs read access. Creating tables and changing columns needs a key whose **Access** includes schema changes.
- Every example below assumes `nexus = Nexus.connect()`.
## Schemas [#schemas]
```python
for schema in nexus.schemas.list(): # the default schema first
print(schema)
nexus.schemas.create("finance")
print(nexus.schemas.describe("finance"))
nexus.schemas.delete("finance") # only when it holds no tables and no files
```
## Find and describe tables [#find-and-describe-tables]
```python
for table in nexus.tables.list(search="order"):
print(table["name"], table["column_count"], table["doc"])
details = nexus.tables.describe("orders")
```
`list()` follows every page for you. Each entry has `name`, `schema`, `owner`, `doc`, `column_count`, `about`, `made_by` and `mine`. Pass a project ID as `made_by=` to filter.
## Create a table [#create-a-table]
```python title="create_orders.py"
from klarety import Nexus, RefusedError
with Nexus.connect() as nexus:
try:
created = nexus.tables.create(
"orders",
[
{"name": "order_id", "type": "string", "doc": "Order number."},
{"name": "customer", "type": "string", "doc": "Customer name."},
{"name": "amount", "type": "double", "doc": "Order total in US dollars."},
{"name": "ordered_at", "type": "timestamp", "doc": "When the order was placed."},
],
doc="One row per customer order.",
identifier_columns=["order_id"],
expectations=[{"column": "amount", "rule": "range", "minimum": 0}],
)
except RefusedError as refused:
raise SystemExit(f"Could not create the table: {refused}")
print(created)
```
- `doc` is required, on the table and on each column: one line saying what it holds. A column can also set `"required": True`.
- `identifier_columns` name the columns that identify a row (its key).
- `expectations` are rules every load checks; breaking rows are set aside under the rule's name.
- `relationships` (for example `{"columns": ["customer"], "references": "customers", "referenced_columns": ["id"]}`) help Nexus lay out both tables. They are not enforced on write.
- Nexus chooses the physical layout and reports it. `cluster_by` suggests the columns you filter on most; `pin_layout()` stops the layout changing as the table grows.
Usually you do not need `create()`: [ingesting a file](/docs/developers/ingest) creates its table.
## Describe what a table means [#describe-what-a-table-means]
```python
nexus.tables.describe_meaning(
"orders", grain="one order", time_column="ordered_at", status="ready"
)
nexus.tables.set_column_doc("orders", "amount", "Order total in US dollars, tax included.")
```
`status` is `draft`, `ready` or `deprecated`. Only the values you pass change. People and AI read these descriptions to understand your data.
## Read rows [#read-rows]
```python
for row in nexus.tables.rows("orders", where=["customer:eq:Acme"], order_by="ordered_at"):
print(row)
```
`rows()` pages as you loop. A `where` filter is `column:op:value`, where `op` is `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `in`, `contains`, `is_null` or `not_null`. To fetch many rows by key in one read, use `in`: `"order_id:in:A1,A2,A3"`. For anything more, use [SQL](/docs/developers/sql).
## Edit a few rows [#edit-a-few-rows]
`edit_rows()` adds, updates and deletes up to 500 rows in one commit:
```python
from klarety import ConflictError
try:
nexus.tables.edit_rows(
"orders",
added=[{"order_id": "A9", "customer": "Acme", "amount": "120.00"}],
updated=[{"row": {"order_id": "A1", "amount": "80.00"}, "values": {"amount": "85.00"}}],
write_id="fix-a1-2026-11-10",
)
except ConflictError as changed:
print("A row changed since you read it:", changed)
```
- In `updated`, `row` is the row as you read it and `values` the new values, written as text. If any value in `row` no longer matches what is stored, nothing is saved and `ConflictError` names each row that changed.
- `write_id` names the write (8 to 64 letters, digits, `-` or `_`). After a timeout, resend with the same `write_id`: a landed write is answered as saved, never made twice.
- For more than 500 rows, load a file with `mode="upsert"` instead.
## Change columns [#change-columns]
```python
nexus.tables.add_column("orders", {"name": "channel", "type": "string", "doc": "Where the order came from."})
nexus.tables.change_columns("orders", rename={"channel": "sales_channel"}, retype={"amount": "decimal(12,2)"})
nexus.tables.change_columns("orders", remove=["sales_channel"])
nexus.tables.restore_column("orders", "sales_channel")
```
All changes in one `change_columns()` call happen together. A removed column is kept until it is purged, so `restore_column()` can bring it back.
## History, rollback and restore [#history-rollback-and-restore]
Every change creates a snapshot of the table.
```python
history = nexus.tables.snapshots("orders")
nexus.tables.rollback("orders", snapshot_id=1234567890)
new_rows = nexus.tables.changes("orders", since=0) # pass back "through" next time
```
`delete()` removes a table, and it stays restorable for your plan's undelete window:
```python
nexus.tables.delete("orders")
print(nexus.tables.deleted())
nexus.tables.restore("orders")
```
`nexus.tables.erase("orders", "customer = 'Acme'")` removes the bytes of the matching rows, not just the rows, and returns a receipt. There is no undo: a rollback to before an erasure is refused, so erased rows never come back.
## Check quality and activity [#check-quality-and-activity]
```python
print(nexus.tables.quality("orders"))
print(nexus.tables.authors("orders", days=7)) # who added and changed rows, day by day
print(nexus.tables.lineage("orders"))
```
## What can go wrong [#what-can-go-wrong]
| Exception | Cause | What to do |
| --- | --- | --- |
| `NotFoundError` | No such table in this schema, or you cannot reach it. | Check the name and the `schema=`. |
| `RefusedError` | Your key cannot make this change. | Use a key with schema-change access. |
| `ConflictError` | A row changed since you read it, or the table is changing. | Read again, then retry. |
| `TableRuleError` | The change breaks a rule of the table; the message names it. | Fix the change; never resend it unchanged. |
## Next steps [#next-steps]
- [Ingest data](/docs/developers/ingest) to fill tables from files.
- [Run SQL](/docs/developers/sql) to query them.
- [Jobs and batch jobs](/docs/developers/jobs) to change many rows with SQL.