blob: ca8a82a09bb7f586fda9851b308e2040c3eda190 [file] [view]
---
sidebar_position: 5
---
# Primary Key Tables
Primary key tables support upsert, delete, and point lookup operations.
## Creating a Primary Key Table
Pass `primary_keys` to `fluss.Schema`:
```python
import pyarrow as pa
schema = fluss.Schema(
pa.schema([
pa.field("id", pa.int32()),
pa.field("name", pa.string()),
pa.field("age", pa.int64()),
]),
primary_keys=["id"],
)
table_path = fluss.TablePath("fluss", "users")
await admin.create_table(table_path, fluss.TableDescriptor(schema, bucket_count=3), ignore_if_exists=True)
```
## Upsert, Delete, Lookup
```python
table = await conn.get_table(table_path)
# Upsert (fire-and-forget, flush at the end)
writer = table.new_upsert().create_writer()
writer.upsert({"id": 1, "name": "Alice", "age": 25})
writer.upsert({"id": 2, "name": "Bob", "age": 30})
await writer.flush()
# Per-record acknowledgment (for read-after-write)
handle = writer.upsert({"id": 3, "name": "Charlie", "age": 35})
await handle.wait()
# Delete by primary key
handle = writer.delete({"id": 2})
await handle.wait()
# Lookup
lookuper = table.new_lookup().create_lookuper()
result = await lookuper.lookup({"id": 1})
if result:
print(f"Found: name={result['name']}, age={result['age']}")
```
## Partial Updates
Update specific columns while preserving others:
```python
partial_writer = table.new_upsert().partial_update_by_name(["id", "age"]).create_writer()
partial_writer.upsert({"id": 1, "age": 27}) # only updates age
await partial_writer.flush()
```
## Subscribing to the Changelog (CDC)
Every primary key table maintains a changelog of its row-level changes. Read it with a record-mode scanner the same API used for [log tables](./log-tables.md) to stream CDC events. Each `ScanRecord` carries a `change_type`: `+I` (insert), `-U` / `+U` (the old and new images of an update), and `-D` (delete, carrying the old row).
```python
table = await conn.get_table(table_path)
scanner = await table.new_scan().create_log_scanner()
# Subscribe to every bucket from the start of the changelog.
num_buckets = (await admin.get_table_info(table_path)).num_buckets
scanner.subscribe_buckets({i: fluss.EARLIEST_OFFSET for i in range(num_buckets)})
while True:
records = await scanner.poll(3000)
if records.is_empty():
break
for record in records:
print(record.change_type.short_string(), record.row) # +I / -U / +U / -D
```
With the default changelog mode (`'table.changelog.image' = 'FULL'`), updating a key emits a `-U`/`+U` pair and deleting it emits `-D`; the `WAL` mode emits only `+U` on update. To resume from a committed position instead of the start, `subscribe` at a specific offset.
## Limit Scan
To read up to `n` rows of a bucket's current state without supplying keys, use a batch scanner. The server returns the deduplicated current rows as Arrow batches — convenient for previews or DataFusion sources.
```python
bucket = fluss.TableBucket(table.get_table_info().table_id, 0)
scanner = table.new_scan().limit(10).create_bucket_batch_scanner(bucket)
arrow_table = await scanner.to_arrow()
```
Limit applies per bucket; scan each bucket to cover a multi-bucket table.