mirror of
https://github.com/lancedb/lancedb.git
synced 2026-11-15 07:06:52 +00:00
Ambiguous gateway failures can occur after a write commits, so replaying the operation can append duplicate rows. Non-idempotent writes now retry only known precommit failures. Multipart retries reuse the same upload ID, part ID, and encoded bytes; ambiguous completion returns an unknown-outcome error without abort or replay. A definite 409/429 completion rejection triggers best-effort cleanup using the call's retry budget. Replayable parts and the shared byte-based partition target default to 64 MiB. Dictionary-bearing batches are compacted before size estimation and again when actual compressed IPC framing requires further splitting: keys are remapped without changing their types or schema, view backing buffers are collected, and ListView/LargeListView child ranges are compacted as an interval union so overlapping views retain sharing. Parts are prepared on demand; a single row and its schema can still exceed the target. Multipart progress starts at zero and counts each acknowledged IPC body once. Rust write builders and Python/TypeScript `add` expose per-call retry overrides. Atomic `create_table` pre-encodes to a temporary file and rejects compressed IPC over 256 MiB before sending any request; the schema-first import guidance now includes Python, TypeScript, and Rust examples. The shared partition target also affects local automatic partitioning. At `cb2a79473bd1b51b950c9fa3e9f247f929c8ac3b`, 70 targeted Rust cases pass on source verified against the committed four-file patch. With a 16 KiB target and 128 unique 1 KiB dictionary values, the original ordinary dictionary reproducer drops from 128 requests / 16,942,080 bytes to 16 / 143,488; ListView and LargeListView drop from 128 / 17,056,768 to 16 / 149,632. Encoded HTTP bodies are decoded to check values, nulls, slices, metadata, changing dictionaries, and overlapping views; additional cases cover framing-driven splitting, oversized rows, stable retry bytes/IDs, acknowledged progress, and atomic create. Real HTTP fault tests commit Blob payloads to a local Lance table and verify byte equality, no duplicate append, and zero abort/replay after ambiguous completion. The earlier 811 remote and 7 progress cases at `f36d217110a25f6b501c0255df9aaa60e3c8000a`, Linux Python remote database/options results (437 cases plus 8 fixture consumers), Node retry-binding evidence, and Rust/TypeScript documentation example checks remain historical validation. Python/Node extensions were not rebuilt for the dictionary follow-up; these results do not establish current binding integration or fresh cross-platform/minimum-dependency CI acceptance. A separate real Query Node lifecycle experiment used SDK `b8efe7d2d63647d4962789ca5fc042b59efe073d` and Sophon `58b7895c7c287810fff4e89f13226fc5fa60d9ef`. After the actual listener forced a connection closed, a surviving QN accepted a retry with identical upload ID, part ID, and IPC bytes. Ordinary insert and complete response-loss scenarios sent no replay or abort; fresh reads observed exactly one commit and exact Blob bytes in all three scenarios. This local experiment used a 1-second drain grace, two 32 KiB payloads, and one distinct staged part. It validates acknowledgement-loss recovery at those revisions, not concurrent overlapping attempts, large uploads, physical orphan cleanup, the later dictionary encoding change, or the original sixth-insert EOF. The part target is not a process memory limit: source/ancestor buffers and temporary compaction allocations may coexist. Struct/FixedSizeList parent nulls can retain hidden child keys; this is not complete logical-reachability GC for every Arrow combination, and retained hidden values may affect wire size. The current local Lance 15.0.0-beta.17 rejects ListView/LargeListView persistence; the tests establish remote client IPC encoding behavior, not native or Cloud persistence support. Live Cloud, full workspace/Clippy, the original ordinary-insert disconnect root cause, and Sophon's asynchronous transaction cleanup behavior are not established. Cancelling during staging retains the existing server-session cleanup limitation. Function reader delivery is separate.
310 lines
14 KiB
Plaintext
310 lines
14 KiB
Plaintext
---
|
|
title: "Insert data"
|
|
sidebarTitle: "Insert"
|
|
description: "Append records, load file-backed data or batches, and verify ingestion."
|
|
icon: plus
|
|
availability:
|
|
embedded: available
|
|
enterprise: available
|
|
---
|
|
|
|
import {
|
|
TsCreateTableFromIterator as TsCreateTableFromIterator,
|
|
RsCreateTableFromIterator as RsCreateTableFromIterator,
|
|
} from '/snippets/tables.mdx';
|
|
import {
|
|
PyAddFromDataset as AddFromDataset,
|
|
PyCreateTableFromIterator as CreateTableFromIterator,
|
|
} from '/snippets/ingestion_examples.mdx';
|
|
|
|
import { RsExample as RsAddProgress } from '/snippets/ingestion_progress.mdx';
|
|
import { PyAddProgress, TsAddProgressBody, TsAddProgressTail } from '/snippets/ingestion_examples.mdx';
|
|
import { PyAppendRows } from '/snippets/ingestion_examples.mdx';
|
|
import { PyWriteWorkflow, TsWriteWorkflowBody, TsWriteWorkflowTail, RsWriteWorkflow } from '/snippets/ingestion_examples.mdx';
|
|
|
|
Use `add()` to append records to an existing table. It does not compare incoming
|
|
keys with stored keys. Choose [merge insert](/ingestion/merge-insert) when the
|
|
same record may arrive again and should update an existing row.
|
|
|
|
## Append a first batch {#append}
|
|
|
|
Install the Python SDK with `pip install lancedb`. This complete example starts
|
|
with Alice, appends Bob and Charlie, and prints all three records.
|
|
|
|
<CodeBlock filename="append_rows.py" language="python" icon="python">
|
|
{PyAppendRows}
|
|
</CodeBlock>
|
|
|
|
The program uses temporary local storage. For a complete TypeScript or Rust
|
|
example, see [Complete write workflow](#complete-example).
|
|
Enterprise SQL offers [`INSERT`](/sql/dml/insert), including `INSERT ... SELECT`.
|
|
|
|
## Prepare the input {#prepare}
|
|
|
|
Match the table's schema, including required fields and vector dimensions.
|
|
Supported input formats differ by SDK; see
|
|
[Create a table](/ingestion/create-table). Inspect conversion errors before
|
|
retrying, and verify the final row count and representative values.
|
|
|
|
## Choose input for a large load {#large-datasets}
|
|
|
|
Avoid building one large in-memory list when records already live in files
|
|
or arrive as batches. Create a table with the intended schema, then append
|
|
through the appropriate input path.
|
|
|
|
<span id="bulk-ingestion-for-data-you-already-have"></span>
|
|
### Read from files {#from-files-parquet-csv-etc}
|
|
|
|
Python accepts a `pyarrow.dataset.Dataset`. This example writes a small Parquet
|
|
file locally, creates an empty table with its schema, and appends the two records.
|
|
For your own data, point the dataset reader at the input files instead.
|
|
|
|
<CodeBlock filename="load_parquet.py" language="python" icon="python">
|
|
{AddFromDataset}
|
|
</CodeBlock>
|
|
|
|
A file-backed source can be scanned without first loading the whole dataset
|
|
into application memory. This Python input API is distinct from converting
|
|
files to Arrow batches yourself in another language.
|
|
|
|
<span id="iterator-ingestion-for-data-you-transform-on-the-fly"></span>
|
|
### Produce batches during ingestion {#from-iterators-custom-batch-generation}
|
|
|
|
Use batches when processing the input as it arrives. The Python example creates
|
|
an empty table and appends ten records produced by a batch iterator.
|
|
|
|
<CodeGroup>
|
|
<CodeBlock filename="Python" language="python" icon="python">
|
|
{CreateTableFromIterator}
|
|
</CodeBlock>
|
|
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
|
|
{TsCreateTableFromIterator}
|
|
</CodeBlock>
|
|
<CodeBlock filename="Rust" language="rust" icon="rust">
|
|
{RsCreateTableFromIterator}
|
|
</CodeBlock>
|
|
</CodeGroup>
|
|
|
|
The language APIs differ: the TypeScript example appends each constructed Arrow
|
|
batch, while the Rust example supplies an Arrow reader when creating the table.
|
|
These are input-format examples, not equivalent parallel-write benchmarks.
|
|
|
|
### Control write parallelism {#write-parallelism}
|
|
|
|
Python's `add()` can estimate parallelism for materialized inputs. A streaming
|
|
reader may not expose its total size or support replay. Use `write_parallelism`
|
|
when controlling the number of concurrent partitions; increasing it can also
|
|
increase memory use. TypeScript does not expose that manual option.
|
|
|
|
Measure with realistic row sizes and storage. Avoid one write per row when a
|
|
batch will do, and avoid a large worker count for a small input.
|
|
|
|
## Track completion and progress {#tracking-ingestion-progress}
|
|
|
|
Python, TypeScript and Rust support progress callbacks when adding data.
|
|
These examples start with Alice, append Bob and Charlie, and print the number
|
|
of rows written as progress updates arrive. After the write succeeds, they
|
|
query the table to show all three records.
|
|
|
|
Each program uses a temporary local database and removes it when finished.
|
|
|
|
- **Python:** install `lancedb`, save the code as `insert_progress.py`, and run
|
|
`python insert_progress.py`.
|
|
- **TypeScript:** install `@lancedb/lancedb`, save the code as `insert_progress.mts`,
|
|
and run `node insert_progress.mts` with Node.js 22.18 or later.
|
|
- **Rust:** download the [Rust examples](https://raw.githubusercontent.com/lancedb/lancedb/2af85bea8999c8cd23ec01ff62ba110c54652c10/docs/web/static/code/ingestion-feedback-rust.zip),
|
|
extract the project, and run `cargo run --locked --bin progress`.
|
|
|
|
<CodeGroup>
|
|
<CodeBlock filename="Python" language="python" icon="python">
|
|
{PyAddProgress}
|
|
</CodeBlock>
|
|
<CodeBlock filename="TypeScript" language="typescript" icon="square-js">
|
|
{TsAddProgressBody}
|
|
{TsAddProgressTail}
|
|
</CodeBlock>
|
|
<CodeBlock filename="Rust" language="rust" icon="rust">
|
|
{RsAddProgress}
|
|
</CodeBlock>
|
|
</CodeGroup>
|
|
|
|
For a progress bar in Python, install `tqdm` and pass `progress=True` instead
|
|
of a callback.
|
|
|
|
### Read progress updates
|
|
|
|
Updates include row and byte counts, elapsed time, and parallel write activity.
|
|
The field names differ by language:
|
|
|
|
| Meaning | Python dictionary key | TypeScript property | Rust method |
|
|
|:--------|:----------------------|:--------------------|:------------|
|
|
| Rows written so far | `output_rows` | `outputRows` | `output_rows()` |
|
|
| Bytes written so far | `output_bytes` | `outputBytes` | `output_bytes()` |
|
|
| Expected row count, when known | `total_rows` | `totalRows` | `total_rows()` |
|
|
| Elapsed time | `elapsed_seconds` | `elapsedSeconds` | `elapsed()` |
|
|
| Active write tasks | `active_tasks` | `activeTasks` | `active_tasks()` |
|
|
| Total write tasks | `total_tasks` | `totalTasks` | `total_tasks()` |
|
|
| Final update | `done` | `done` | `done()` |
|
|
|
|
Python and TypeScript report elapsed time in seconds; Rust returns a `Duration`.
|
|
The expected row count may be unknown for streaming input. On the final update,
|
|
it falls back to the number of rows written.
|
|
|
|
For remote multipart writes, rows count input scanning, while bytes count
|
|
acknowledged IPC parts. Bytes start at zero and a retried part is counted once,
|
|
after acknowledgement. Scanned rows and staged bytes do not imply a commit.
|
|
|
|
Keep callbacks short and use the write operation's result to determine success.
|
|
The final `done` update means progress reporting has ended; it can also occur
|
|
after a write fails or is cancelled. Input errors before writing starts may
|
|
produce no updates.
|
|
|
|
TypeScript delivers callbacks asynchronously. Slow callbacks can queue up,
|
|
and the `add()` promise may resolve before every callback has run. Exceptions
|
|
from a TypeScript callback are logged and do not abort the write.
|
|
|
|
## Verify writes and handle retries {#performance}
|
|
|
|
Wait for the write operation's result, then check the expected records.
|
|
Progress callbacks describe activity; they do not establish that a write
|
|
succeeded. If a request fails after the server may have committed it, blindly
|
|
repeating an append can duplicate rows. Current clients do not automatically
|
|
replay non-idempotent writes after `500`, `502`, `503`, or `504`, or after a
|
|
response is lost. A plain `503` does not prove that a write was rejected before
|
|
commit. Connection failures before sending, `409` transaction conflicts, and
|
|
explicit `429` rate-limit rejections may be retried within `RetryConfig` limits. Idempotent multipart
|
|
parts have a separate retry policy described below.
|
|
|
|
If multipart completion times out, loses its response, or receives a `5xx`, the
|
|
error reports **commit outcome unknown**. The client neither aborts the upload
|
|
nor resends completion or the input under
|
|
a new upload ID: aborting at this point could delete already committed data.
|
|
Check the table using your record IDs before retrying the operation.
|
|
|
|
Configure connection-wide limits with `client_config.retry_config` in Python
|
|
(`lancedb.remote.RetryConfig`) or `clientConfig.retryConfig` in TypeScript.
|
|
Override the budget for one `add()` with Python `table.add(data, retries=0)`
|
|
or TypeScript `table.add(data, { retries: 0 })`. The value must be an integer
|
|
between 0 and 255. Rust write builders also provide a per-call override, for
|
|
example `table.add(data).retries(0).execute().await?`,
|
|
`table.update().retries(0)`, and `table.merge_insert(&["id"]).retries(0)`.
|
|
This disables transport retries for that operation, including multipart part
|
|
retries; it does not change local transaction conflict handling. Increasing a
|
|
retry budget never makes ambiguous commits safe to replay.
|
|
|
|
## Configure large uploads {#bulk-ingests-into-a-remote-table}
|
|
|
|
<Badge color="red">Enterprise</Badge>
|
|
|
|
This section applies only to LanceDB Enterprise tables (`db://` connections). LanceDB Embedded writes are unaffected.
|
|
|
|
Large measured inputs, unknown-length streams, and explicit parallel uploads use
|
|
multipart writes when the server supports them. The client slices large batches
|
|
before IPC encoding and buffers each part for replay. A disconnected request,
|
|
read timeout, or configured retryable staging status retries exactly the same
|
|
upload ID, part ID, and IPC bytes. `RetryConfig.statuses` controls status retries;
|
|
an empty list disables them. The default includes `500`, `502`, `503`, and `504`.
|
|
The server replaces that part's staged transaction, so losing
|
|
its response does not append the rows twice. Exhausting the part retry budget
|
|
aborts the uncommitted session; it does not restart the entire upload.
|
|
A complete request rejected with a transaction conflict (`409`) or rate limit
|
|
(`429`) also triggers best-effort cleanup. Cleanup uses the call's retry budget;
|
|
its failure does not replace the original error. Ambiguous completion outcomes
|
|
never trigger cleanup or replay.
|
|
|
|
Part size still affects memory and HTTP timeouts. A single blob row cannot be
|
|
split across Arrow batches; a very large row may require a larger timeout and
|
|
more memory. No connection-pool or keepalive setting guarantees that a write
|
|
response will arrive.
|
|
|
|
Two environment variables control how a multipart write's parts are cut. Both are picked up automatically by the Python and TypeScript clients, for clients that support these settings:
|
|
|
|
| Variable | Default | What it controls |
|
|
|----------|---------|------------------|
|
|
| `LANCE_CLIENT_MAX_BYTES_PER_REQUEST` | 64 MiB | Target LZ4-compressed IPC bytes per replayable part. A single indivisible row and its IPC schema may exceed the target. `0` disables byte-based cuts and can require buffering a whole partition. |
|
|
| `LANCE_CLIENT_MAX_REQUEST_DURATION` | Half the read timeout | Time spent filling a replayable payload, in whole seconds. `0` disables time-based cuts. HTTP deadlines are configured separately in `timeout_config`. |
|
|
|
|
A part is cut after an encoding slice reaches either limit. Input batches are
|
|
sliced to approximately 8 MiB before encoding, preserving row order. Peak
|
|
additional memory scales with active writers and the part budget. Lower
|
|
`write_parallelism` or the byte budget to reduce memory use.
|
|
|
|
<CodeGroup>
|
|
```bash Bash icon="terminal"
|
|
export LANCE_CLIENT_MAX_BYTES_PER_REQUEST=33554432 # 32 MiB
|
|
export LANCE_CLIENT_MAX_REQUEST_DURATION=30 # payload preparation budget
|
|
```
|
|
</CodeGroup>
|
|
|
|
Splitting into more parts does not change the final table. The server stages the parts under their shared upload ID before committing
|
|
the write. This upload protocol is separate from repeating an application-level
|
|
`add()` after a timeout.
|
|
|
|
Remote `create_table(name, data)` remains one atomic request, including overwrite
|
|
and `exist_ok` modes. The client encodes into an automatically removed temporary
|
|
file before sending, rejects payloads larger than 256 MiB of compressed IPC, and
|
|
never silently creates an empty table as a fallback. Encoding and input failures
|
|
therefore issue no create request. Temporary disk usage is bounded by this budget;
|
|
individual oversized rows can still need substantial encoding memory.
|
|
|
|
For larger imports, create the schema first and upload with progress reporting.
|
|
The empty table becomes visible immediately; `add()` commits its rows atomically.
|
|
The following examples use an existing connection `db`, an Arrow `schema`, and
|
|
input `data` matching that schema (an Arrow reader in Rust):
|
|
|
|
<CodeGroup>
|
|
```python Python icon="python"
|
|
table = db.create_table("items", schema=schema)
|
|
table.add(data, progress=True)
|
|
```
|
|
|
|
```typescript TypeScript icon="square-js"
|
|
const table = await db.createEmptyTable("items", schema);
|
|
await table.add(data, {
|
|
progress: (p) => console.log(`Rows written: ${p.outputRows}`),
|
|
});
|
|
```
|
|
|
|
```rust Rust icon="rust"
|
|
let table = db.create_empty_table("items", schema).execute().await?;
|
|
table
|
|
.add(data)
|
|
.progress(|p| println!("Rows written: {}", p.output_rows()))
|
|
.execute()
|
|
.await?;
|
|
```
|
|
</CodeGroup>
|
|
|
|
## Complete write workflow {#complete-example}
|
|
|
|
<Accordion title="Complete examples: create, append, update, upsert and delete">
|
|
|
|
Each program creates a temporary local database, performs the five operations,
|
|
prints the result, and removes its temporary data when finished.
|
|
|
|
- **Python:** install `lancedb`, save the code as `ingestion.py`, and run
|
|
`python ingestion.py`.
|
|
- **TypeScript:** install `@lancedb/lancedb`, save the code as `ingestion.mts`,
|
|
and run `node ingestion.mts` with Node.js 22.18 or later.
|
|
- **Rust:** download the [complete Rust project](https://raw.githubusercontent.com/lancedb/lancedb/2af85bea8999c8cd23ec01ff62ba110c54652c10/docs/web/static/code/ingestion-rust.zip),
|
|
extract it, and run `cargo run --locked`. It includes `src/main.rs`, the Cargo
|
|
manifest and a lockfile for the example's dependencies. The code is shown below.
|
|
|
|
<CodeGroup>
|
|
<CodeBlock filename="ingestion.py" language="python" icon="python">
|
|
{PyWriteWorkflow}
|
|
</CodeBlock>
|
|
<CodeBlock filename="ingestion.mts" language="typescript" icon="square-js">
|
|
{TsWriteWorkflowBody}
|
|
{TsWriteWorkflowTail}
|
|
</CodeBlock>
|
|
<CodeBlock filename="src/main.rs" language="rust" icon="rust">
|
|
{RsWriteWorkflow}
|
|
</CodeBlock>
|
|
</CodeGroup>
|
|
|
|
After the operations, Alice has 10 logins, Bobby has 21, and Charlie has 6.
|
|
Dana is selected for deletion and is absent from the final result.
|
|
|
|
</Accordion>
|