# Tables, columns, and scans

**Applies to:** typed rows are in the baseline product; scan commands and the latest mutation options require current development builds and SDKs. Upgrade all servers before introducing a record format they cannot replay.

## Model a row

A row is identified by table name and key. Tables are schemaless: rows can contain different columns and types. Supported values are bytes, UTF-8 strings, signed 64-bit integers, floats, and booleans. There is no null type; remove a column to represent its absence.

Table and column names are 1–64 UTF-8 bytes. A row has at most 64 columns. Encoded names, keys, and metadata count against record-size limits. Raw values and table rows occupy distinct key domains.

## Write and project columns

Inside a Go function with `client` and `ctx` already configured:

```go
users := client.Table("users")
err := users.Put(ctx, []byte("user-123"), smkv.Columns{
    "country": "US",
    "visits": int64(12),
    "active": true,
}, smkv.WithTTL(time.Hour))
if err != nil { return err }
row, err := users.Get(ctx, []byte("user-123"), "country", "visits")
if err != nil { return err }
fmt.Println(row)
```

`Put` replaces the whole row; omitted TTL means no expiration. `Get` without a projection returns every column. Missing projected columns are omitted; missing rows return `ErrNotFound`. Integers decode as `int64`.

## Update atomically within a row

```go
err = users.Update(ctx, []byte("user-123"),
    smkv.Columns{"visits": int64(13)}, []string{"active"})
if err != nil { return err }
```

This sets `visits` and removes `active` in one row mutation, preserving other columns. It is assignment, not a server-side increment. An update requires an existing unexpired row. Omitted TTL preserves its absolute expiration; a supplied TTL resets it. To clear expiration, replace the row with `Put` and no TTL.

Projection reduces network response size but still reads the full stored row. Updates rewrite and replicate the full row. There are no cross-row transactions.

## Scan a table

Current development builds provide bounded scans through `smkv-ctl`, the Go SDK, and the dashboard Tables view:

```sh
smkv-ctl --seed 10.0.0.11:7381 scan --table users \
  --columns country,visits --limit 50 \
  --filters '[{"column":"visits","op":"ge","value":{"type":"int","value":"10"}}]'
```

Pass the returned cursor verbatim with `--cursor` for the next page, keeping the same query. An empty row list can still have a continuation cursor. Stop only when the cursor is absent/empty. The CLI processes a bounded page, not an unlimited export.

Filters support typed comparisons and existence; they are not secondary-index lookups. Scans have no global ordering or transactional snapshot. Concurrent writes can cause omissions or duplicates; restart, compaction, or topology changes may invalidate the cursor. Restart explicitly and reconcile earlier results when that happens.

## Keep scans behind application traffic

Reads, writes, and replication take precedence. Scan pages yield between records, are capped at 100 rows and a bounded response size, and are admitted at most ten times per second per node. Continuous foreground load can starve a scan or make it time out.

For Busy, retry the same page after at least 100 ms within an overall deadline. Stop and investigate other failures. Use [backup export](backup.html), rather than scans, when you need a verified recovery archive.
