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

# Tables: schemas, rows, updates, and compaction

> Manage Infino tables. Define schemas, append rows, update and delete by predicate, compact Parquet files, reclaim storage, and inspect the table catalog.

A table is a **schema** plus an **index spec**: which columns are full-text- and
vector-indexed (see [Indexing](https://infino.ai/docs/guides/indexing.md)). Create it once, then append rows.

## Create and append

```python Python icon="python" theme={null}
import infino
import pyarrow as pa

# Durable storage (a path or s3:// URI), required if you'll update/delete later.
db = infino.connect("./data")

schema = pa.schema([
    pa.field("doc_id", pa.large_utf8(), nullable=False),
    pa.field("body", pa.large_utf8(), nullable=False),
    pa.field("embedding", pa.list_(pa.float32(), 384), nullable=False),
])
docs = db.create_table(
    "docs", schema,
    infino.IndexSpec().fts("body").vector("embedding", 384, "cosine"),
)

# One append is one atomic commit, durable when the call returns.
docs.append([
    {"doc_id": "1", "body": "To cancel, open Settings then Billing.", "embedding": embed("...")},
])
```

```typescript Node.js icon="node-js" theme={null}
import { connect, IndexSpec } from "@infino-ai/infino";

// Durable storage (a path or s3:// URI), required if you'll update/delete later.
const db = connect("./data");

const docs = db.createTable(
  "docs",
  { doc_id: "large_utf8", body: "large_utf8", embedding: { vector: 384 } },
  new IndexSpec().fts("body").vector("embedding", 384, "cosine"),
);

// One append is one atomic commit, durable when the call returns.
docs.append([
  { doc_id: "1", body: "To cancel, open Settings then Billing.", embedding: embed("...") },
]);
```

```rust Rust icon="rust" theme={null}
use std::sync::Arc;
use infino::arrow_schema::{DataType, Field, Schema};
use infino::{connect, IndexSpec, Metric};

// Durable storage (a path or s3:// URI), required if you'll update/delete later.
let db = connect("./data")?;

let item = Arc::new(Field::new("item", DataType::Float32, true));
let schema = Arc::new(Schema::new(vec![
    Field::new("doc_id", DataType::LargeUtf8, false),
    Field::new("body", DataType::LargeUtf8, false),
    Field::new("embedding", DataType::FixedSizeList(item, 384), false),
]));
let docs = db.create_table(
    "docs", schema,
    IndexSpec::new().fts("body").vector("embedding", 384, Metric::Cosine),
)?;

// One append is one atomic commit. Build `batch` as an Arrow RecordBatch
// matching the schema (see the Quickstart).
docs.append(&batch)?;
```

> [!NOTE]
>
> One `append` == one atomic commit, durable on return. Append in batches rather than
> row-by-row for throughput. On Infino Cloud, one append request is capped at 128 MiB of
> Arrow data; target 100 MiB or less per batch (see [Limits](https://infino.ai/docs/cloud/limits.md)).

## Open an existing table

A table persists, so you create it once and open it on later runs with `open_table`.

```python Python icon="python" theme={null}
docs = db.open_table("docs")
```

```typescript Node.js icon="node-js" theme={null}
const docs = db.openTable("docs");
```

```rust Rust icon="rust" theme={null}
let docs = db.open_table("docs")?;
```

## Update and delete

In **Python and Node.js**, `update` and `delete` match rows by a **SQL predicate**
string. In **Rust**, they take a DataFusion `Expr` (`col(...).eq(lit(...))`), where `col`
and `lit` come from the `datafusion-expr` crate (see the [Quickstart](https://infino.ai/docs/quickstart.md) for the
companion crates Rust needs). `update` replaces matched rows 1:1 with the new rows you
supply.

```python Python icon="python" theme={null}
# Replace the row(s) matching the predicate (1:1 with the new rows):
docs.update("doc_id = '1'", [
    {"doc_id": "1", "body": "Refunds go to the original payment method.", "embedding": embed("...")},
])

# Delete rows matching a predicate:
stats = docs.delete("doc_id = '1'")
print(stats.matched, stats.n_tombstoned)
```

```typescript Node.js icon="node-js" theme={null}
// Replace the row(s) matching the predicate (1:1 with the new rows):
docs.update("doc_id = '1'", [
  { doc_id: "1", body: "Refunds go to the original payment method.", embedding: embed("...") },
]);

// Delete rows matching a predicate:
const stats = docs.delete("doc_id = '1'");
console.log(stats.matched, stats.nTombstoned);
```

```rust Rust icon="rust" theme={null}
use datafusion_expr::{col, lit};   // build predicates as DataFusion expressions

// Replace the row(s) matching the predicate (1:1 with the new rows in `batch`):
docs.update(col("doc_id").eq(lit("1")), &batch)?;

// Delete rows matching a predicate:
let stats = docs.delete(col("doc_id").eq(lit("1")))?;
println!("{} matched, {} tombstoned", stats.matched(), stats.n_tombstoned());
```

> [!WARNING]
>
> `update` and `delete` require **durable storage**: a path or `s3://` URI, not
> `memory://`. `update` is 1:1, so the matched row count must equal the replacement count.

## Compact, list, and drop

```python Python icon="python" theme={null}
docs.optimize()                    # merge small / underfilled superfiles
db.list_tables()                   # ["docs"]
db.drop_table("docs")              # drop + reclaim storage (purge defaults to True)
db.drop_table("docs", purge=False) # …or unregister only, leaving the bytes
```

```typescript Node.js icon="node-js" theme={null}
docs.optimize();                   // merge small / underfilled superfiles
db.listTables();                   // ["docs"]
db.dropTable("docs");              // drop + reclaim storage (purge defaults to true)
db.dropTable("docs", false);       // …or unregister only, leaving the bytes
```

```rust Rust icon="rust" theme={null}
use infino::OptimizeOptions;

docs.optimize(&OptimizeOptions::default())?;  // merge small / underfilled superfiles
db.list_tables()?;                            // Vec<String>
db.drop_table("docs", true)?;                 // true reclaims storage; false unregisters only
```

`optimize` accepts options (omit for engine defaults):

| Option (Python / Node.js) | Description |
| - | - |
| `max_merge_input_mb` / `maxMergeInputMb` | cap on the raw input bytes one merge packs, in MB; not a memory budget (`max_memory_mb` / `maxMemoryMb`, the old name, is still accepted) |
| `min_fill_percent` / `minFillPercent` | only compact superfiles below this fill percent (0 to 100) |
| `target_superfile_size_mb` / `targetSuperfileSizeMb` | target merged-superfile size, in MB |
| `stale_seal_timeout_ms` / `staleSealTimeoutMs` | how old a sealed tombstone sidecar must be, in ms, before compaction treats its owner as dead |
| `recalibrate` | vector probe-law recalibration: `auto` (default), `force`, or `skip` |

## Reclaim storage (gc)

Compaction and interrupted writes can leave behind storage objects that the
table no longer references. `gc` deletes them and reports what it reclaimed. It
only removes objects older than a grace window (in seconds), so it never races a
concurrent reader or writer; requires durable storage.

```python Python icon="python" theme={null}
report = docs.gc(3600.0)                        # delete orphans older than 1 hour
print(report.bytes_freed, report.objects_deleted)
```

```typescript Node.js icon="node-js" theme={null}
const report = docs.gc(3600);                   // delete orphans older than 1 hour
console.log(report.bytesFreed, report.objectsDeleted);
```

```rust Rust icon="rust" theme={null}
use std::time::Duration;

let report = docs.gc(Duration::from_secs(3600))?;  // delete orphans older than 1 hour
println!("{} {}", report.bytes_freed, report.objects_deleted);
```

`optimize` already runs a best-effort `gc` after compacting, so calling `gc`
directly is mainly for reclaiming on a schedule or after bulk deletes.

## Limitations

* **One writer per table at a time.** Appends/updates/deletes are serialized through a
  single atomic commit (concurrent writers from other processes retry).
* **`update` is 1:1.** The matched row count must equal the replacement-row count.
* **`update` / `delete` need durable storage,** not `memory://`.
* **Schema changes go through `schema(name, patch)`** — add, rename, retype or drop columns on a local connection; see the binding reference.

## See also

* [Indexing](https://infino.ai/docs/guides/indexing.md)
* [Search](https://infino.ai/docs/guides/search.md#full-text-bm25)
* [Connect & storage](https://infino.ai/docs/guides/storage.md)


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