# 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.