> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blockdb.chaintable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Read and write data

> Read and write data using the Table, BlockTable, and TimeTable model wrappers.

# Read and write data

The BlockDB Python SDK wraps the three logical table types in corresponding classes: `Table` for regular tables, `BlockTable` for Block tables, and `TimeTable` for Time tables. Each class is a thin wrapper whose methods ultimately delegate to gRPC. Query methods return Python `dict` or `list[dict]`; write methods return a `job_id`.

## The three table models and how to choose

| Class | Logical table | Data dimension | Read anchor |
| - | - | - | - |
| `Table(name)` | Regular table | Row-level | Primary key `row_id` |
| `BlockTable(name)` | Block State / Event table | Block height | `current_block` / `block` |
| `TimeTable(name)` | Time table | Physical time bucket | `time_at` |

Which model you choose depends on your data shape: regular tables for row-level data, Block tables for state and events that change with block height, Time tables for metrics that change with physical time. See [Data model](/concepts/data-model).

## Regular tables (Table)

For row-level data, with primary-key reads/writes and full-table scans:

```python theme={null}
from blockdb import Table

t = Table("demo.event_log")

# Write: real-time write
job_id = t.upsert_rows([{"id": "evt_001", "name": "login", "value": 1}])

# Read: single row / batch / filter
row = t.get_row("evt_001")
rows = t.batch_get_rows(["evt_001", "evt_002"])
hits = t.filter_rows(filters="value > 0", order_by="value desc", limit=100)

# Scan: full table / streaming, returns list[dict]
all_rows = t.scan("SELECT * FROM demo.event_log")
for row in t.scan_iter("SELECT * FROM demo.event_log WHERE name = 'login'"):
    ...  # yields row by row for large tables, constant memory

# Delete
t.delete_rows(["evt_001"])
```

<Info>
  For large-table retrieval use `scan_iter` (streaming, row by row) and avoid `list(scan_iter(...))`, which would exhaust memory. The SQL you pass is executed as a stream and can handle result sets in the millions.
</Info>

## Block tables (BlockTable)

For block-oriented state and events, reading and writing with a block as the anchor:

```python theme={null}
from blockdb import BlockTable, Block

bt = BlockTable("token.token.eth")
block = Block(id="0xabc...", height=18000000, timestamp="2024-06-08T00:00:00Z")

# Read: state / event at a block, or a whole block's rows
state = bt.get_state("acct_1", block)
event = bt.get_event("evt_1", block)
rows = bt.get_block_rows(block)

# Write: atomic write at block granularity
bt.upsert_block(block, [{"id": "acct_1", "balance": 1000}])
```

`get_state` / `get_event` use `current_block` as the anchor and automatically inject `height ≤ target height`, returning the most recent snapshot at or below that height. The current block height must not exceed the table's consensus height for the result to be correct — see [Block State Table and Block Event Table](/concepts/block-tables).

## Time tables (TimeTable)

For metrics that change with physical time, reading and writing by time bucket:

```python theme={null}
from blockdb import TimeTable

tt = TimeTable("token_time_demo")

# Write: batch write into a time bucket (time_at is rounded up to the whole minute)
tt.upsert_time_rows("2026-05-18 17:04:35", [{"id": "0xabc...", "value": 2543.12}])

# Read: an object's state at a time bucket
row = tt.get_value("0xabc...", "2026-05-18 17:04:40")
```

`TimeTable` performs the 60-second bucket alignment in the SDK: `17:04:35` aligns to `17:05:00`, so reads and writes use the same convention. See [Time Table](/concepts/time-table).

## Advanced writes

* **Conditional updates**: `upsert_rows` on regular tables accepts `condition=(column, "if_smaller"/"if_larger")`, updating only when the new value is smaller/larger. Useful for monotonic values such as balances and heights:

  ```python theme={null}
  t.upsert_rows(rows, condition=("value", "if_larger"))
  ```

* **Batch writes**: backfilling millions of rows goes through the S3-staged batch write interfaces, which return a `job_id` you can query or cancel. See [Write architecture](/architecture/batch-write).

## Related topics

* [API overview](/reference/api)
* [Real-time subscription](/guides/subscribe)
* [Filters and operator pipelines](/guides/filter-operator)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.