These examples use db and table handles. See the API guide for setup.
Match the table schema to each example.
Input formats and commits
Select the input format and commit mode separately:
alias LanceDB.{Connection, Query, Source, Table}
{:ok, conn} = Connection.connect("./data")
{:ok, table} = Connection.create_table(conn, "items", [%{id: 1}])
source = Source.rows(Stream.map(2..100, &%{id: &1}), batch_size: 25)
{:ok, %LanceDB.WriteResult{inserted: 99, commits: 1}} = Table.add(table, source)
batches = table |> Query.new(limit: nil) |> Query.stream_batches()
{:ok, copy} = Connection.create_empty_table(conn, "copy", id: :int64)
{:ok, %LanceDB.WriteResult{commits: 1}} = Table.add(copy, Source.batches(batches))Insertion uses one commit by default. Use commit: :batch to commit each
input batch separately. An empty append changes no rows. An empty overwrite
removes all rows. WriteResult.commits counts insertion commits that the
operation confirms. It does not count input batches.
Other writes report nil if the SDK cannot give a commit count. Public
writes inside callbacks return the same result structs as direct calls. Each
public API call inside a callback creates a separate telemetry span.
Streaming reads and ingestion
LanceDB.Query.stream/2 returns a lazy Enumerable of row maps from a query
value. Use Elixir Stream transformations and Enum consumers:
LanceDB.Query.stream(
LanceDB.Query.new(
table,
:scan,
columns: [:id, :text],
filter: "id > 100"
),
batch_size: 256
)
|> Stream.map(& &1["text"])
|> Enum.each(&IO.puts/1)
first_ten =
LanceDB.Query.stream(LanceDB.Query.new(table, :scan, []), batch_size: 16) |> Enum.take(10)
matches =
LanceDB.Query.stream(LanceDB.Query.new(table, {:vector, [0.9, 0.1, 0.2]}, limit: 100),
batch_size: 20
)
text_matches =
LanceDB.Query.stream(
LanceDB.Query.new(table, {:text, LanceDB.FTS.match("elixir", column: :text)}, []),
batch_size: 1024
)
hybrid_matches =
LanceDB.Query.stream(LanceDB.Query.new(table, {:hybrid, [0.9, 0.1, 0.2], "elixir"}, limit: 50),
batch_size: 1024
)Plain scans have a default limit of 100 rows for immediate reads and
streams. Use limit: nil for an unlimited scan. Search streams keep the
search default of 10 rows. Supply :limit to set the maximum result count.
Other options follow the selected query or search API. These include
projection, supported ordering, offsets, filters, row IDs, and search
settings.
Streams use the same metadata and timestamp formats as immediate reads.
:batch_size defaults to 1024. It limits the rows returned by each native
batch request. The native cursor keeps one query execution and reads its
Arrow stream. It does not repeat queries with increasing offsets. Elixir
requests the next batch after it consumes the current batch. The SDK can
read data ahead of these requests.
Sorting and ranking can use more memory. Hybrid search collects both
candidate sets before it returns results. Its memory use also depends on
limit + offset. Enum.to_list/1 collects all results in the caller. For
large scans, process each batch as you read it.
Stream creation checks options but does not access the database. Reading the stream opens a new cursor. Each execution keeps its snapshot. Reading the same stream again starts a new execution. That execution can read newer data according to the handle's read-consistency settings. One process can create a stream that another process reads.
Resume a suspended stream read in the process that started it.
Database errors raise LanceDB.Error when the caller reads the stream. A
cursor closes when its stream ends or stops early, for example with
Enum.take/2 or Enum.find/2. It also closes on consumer exceptions,
consumer process exit, or resource garbage collection. Explicit stream
cleanup waits for native cleanup and read slot release. Process exit and
garbage collection request cancellation without waiting. :timeout_ms
limits the complete cursor lifetime from the start of the stream read.
It includes consumer pauses between batch requests. A timeout closes native execution and raises an error on a later batch request. It does not interrupt Elixir callbacks or remove rows already fetched into a batch.
For ingestion, Table.add/3 reads a Source.rows/2 enumerable in batches
with a size limit:
{:ok, target} = LanceDB.Connection.create_empty_table(db, "imported", id: :int64, text: :string)
records = Stream.map(1..10000, &%{id: &1, text: "document #{&1}"})
{:ok, %LanceDB.WriteResult{inserted: 10000}} =
LanceDB.Table.add(target, LanceDB.Source.rows(records, batch_size: 500), [])
rows = LanceDB.Query.stream(LanceDB.Query.new(target, :scan, filter: "id <= 100"), batch_size: 32)
{:ok, copy} = LanceDB.Connection.create_empty_table(db, "copy", id: :int64, text: :string)
{:ok, %LanceDB.WriteResult{inserted: 100}} =
LanceDB.Table.add(copy, LanceDB.Source.rows(rows, batch_size: 25), [])Table.add/3 reads the source immediately and returns a WriteResult. It
uses the stored schema. It uses the same producer loop as
Table.add(table, Source.rows(rows)). The default is one commit. Use
commit: :batch to keep confirmed earlier commits if a later batch fails.
Error.details then includes inserted rows, confirmed commits, and the last
confirmed version.
An empty append reports zero rows, zero commits, and a nil version. An
empty overwrite clears rows in one commit. See
Ingestion controls and write
results.
Upserts and updates
upsert/3 inserts new rows and replaces rows that match the :on columns.
It uses one merge operation. The default key is :id; :timeout_ms and
:use_index follow the conditional merge controls:
{:ok, %{inserted: inserted, updated: updated}} =
LanceDB.Table.upsert(table, [
%{id: "1", text: "Updated Elixir document", category: "code", vector: [0.8, 0.2, 0.1]},
%{id: "4", text: "New document", category: "code", vector: [0.7, 0.3, 0.2]}
], on: :id)Use on: [:tenant_id, :id] for a composite key. Incoming keys must be
unique, non-null strings or integers. Supply every table column with
matching types. Upserts replace complete rows. Rows absent from the incoming
batch stay unchanged. Repeating a batch with the same keys adds no rows if
existing keys are unique.
Upsert does not add a uniqueness constraint. It does not remove existing duplicate keys.
Use update/3 for partial changes:
{:ok, %{updated: updated_count, version: version}} =
LanceDB.Table.update(table, %{text: "Elixir's concurrency model", category: "code"},
filter: "id = '1'"
)
# Explicit SQL expressions support calculations against the previous row values.
# For a table with a numeric `views` column:
{:ok, %{updated: updated_count, version: version}} =
LanceDB.Table.update(table, %{views: {:sql, "views + 1"}}, filter: "category = 'code'")Update results are LanceDB.WriteResult structs. They include the affected
row count and the version returned by the operation. This also applies when
no rows match.
Literal values can be strings, binaries, integers, floats, booleans, nil,
vectors, lists, struct maps, or Calendar.ISO date or date-time structs.
Timestamp literals use the precision and timezone rules.
The operation
escapes strings as SQL literals. It checks literal types and vector
dimensions against the table schema before writing.
Use LanceDB.Expr or {:sql, expression} for SQL expressions. These
expressions use the SDK's type conversion rules. Use LanceDB.Expr to
combine expression values. Do not insert untrusted text into SQL strings.
The current SDK cannot update rows in tables that contain blob columns. This
includes updates to other columns. These updates return an unsupported error
before execution. Use upsert/3 or merge/3 with complete records to
replace blob rows.
Without :filter, the operation updates every row. A filter with no
matches returns {:ok, %LanceDB.WriteResult{updated: 0, version: version}}. Invalid options or update value structures raise ArgumentError. Invalid
upsert records and database errors return {:error, reason}.
Conditional merges
Use Table.merge/3 for conditional updates and synchronization. Set each
action explicitly. Table.merge/3 disables every action by default.
upsert/3 enables insert and update:
{:ok, result} =
LanceDB.Table.merge(table, incoming_records,
on: [:tenant, :id],
when_matched_update_all: "source.revision > target.revision",
when_not_matched_insert_all: true,
timeout_ms: 30_000,
use_index: true
)Enable at least one action:
:when_matched_update_all:trueupdates all matched rows. A SQL string updates only matches that meet the condition. Usesource.columnfor incoming values andtarget.columnfor stored values. If the condition is false, the row stays unchanged. The row does not become an insert candidate.:when_not_matched_insert_all:trueinserts source rows with no target match.:when_not_matched_by_source_delete:truedeletes target rows absent from the source batch. A SQL string limits deletion. Use unqualified target column names, such as"tenant = 'a'".
For example, synchronize the complete incoming snapshot for one tenant:
{:ok, result} =
LanceDB.Table.merge(table, complete_tenant_snapshot,
on: [:tenant, :id],
when_matched_update_all: true,
when_not_matched_insert_all: true,
when_not_matched_by_source_delete: "tenant = 'a'"
)Deletion compares against the complete source in one merge. Pass a Source
to stream a snapshot. Separate merge calls on its chunks each apply deletion
independently. Every enabled action accepts an empty source. Unconditional
deletion with an empty source removes every target row. A deletion condition
does not limit updates or inserts.
Supply the source rows and update condition separately.
Incoming rows must include every table field. Keys must be unique, non-null
strings or integers. :on defaults to :id and also accepts composite
keys. Use unique keys in the target table. The merge does not add a
uniqueness constraint.
Results are LanceDB.WriteResult structs with :inserted, :updated,
:deleted, :version, and :attempts fields. :timeout_ms is a
positive uint32 integer in milliseconds (at most 4,294,967,295); explicit
nil is invalid. If you omit the option, the SDK default applies. This is
the SDK merge timeout, separate from query and connect/open deadlines.
:use_index defaults to true and controls the join-key
index. Do not insert untrusted text into SQL expressions. Invalid options
raise ArgumentError.
Invalid records and native failures return {:error, reason}.
Ingestion controls and write results
Writes always return %LanceDB.WriteResult{} with the commit version and
applicable row counts. Unavailable fields are nil. Measured zero counts are
zero:
{:ok, %{inserted: 2, version: version}} =
LanceDB.Table.add(table, records,
write_parallelism: 2,
max_rows_per_file: 100_000,
max_rows_per_group: 1024,
max_bytes_per_file: 64_000_000
)
{:ok, %{deleted: deleted, version: deletion_version}} =
LanceDB.Table.delete(table, "id < 100")File and row limits are positive uint32 values. Byte limits are targets, not
absolute limits. Omitted controls keep SDK defaults. mode: :overwrite
replaces rows through the existing handle with its stored schema. Adds,
deletes, updates, upserts, and conditional merges share the same result
type.
For one commit over an Enumerable, use commit: :once:
{:ok, %{inserted: count, version: version}} =
LanceDB.Table.add(
table,
LanceDB.Source.rows(record_stream,
batch_size: 512
),
commit: :once,
write_parallelism: 1
)The default commit: :once commits the whole input. Use commit: :batch to
keep independently committed batches if a failure occurs. Single-commit
imports abort on input or validation failures without committing partial
rows. Producer exceptions return an error with kind :source after
cancellation. A producer interrupted during the final commit might not know
whether the commit succeeded. The native channel holds at most one pending
input batch, in addition to native writer buffers.
Aborted writes can leave unreferenced files for normal cleanup. Empty append
makes no changes and returns version nil. Empty overwrite clears rows.
Use commit: :once for overwrite. This prevents later batches from
overwriting earlier batches.
To observe native progress from another process, pass progress: {pid, tag}
to Table.add/3. The recipient receives
{:lancedb_write_progress, tag, progress}. Progress contains cumulative
output_rows, output_bytes, elapsed_ms, nullable total_rows,
active_tasks, total_tasks, and done. The tag can be any Elixir
term, including a reference. Drain these messages regularly. done means
processing ended, including failure.
The API result determines whether a commit succeeded. Batch-mode counters reset per batch.
Streaming creation, merges, and column backfills
Define an explicit schema to create a table from a Source. It returns a
table after one commit. Creation from a plain row list can infer its schema:
alias LanceDB.{Batch, Connection, Query, Schema, Table}
schema = Schema.new(id: :int64, text: :string)
rows = Stream.map(1..10000, &%{id: &1, text: "document #{&1}"})
{:ok, table} =
LanceDB.Connection.create_table(
db,
"documents",
LanceDB.Source.rows(rows, schema: schema, batch_size: 256),
[]
)
batches = table |> Query.new(limit: nil) |> Query.stream_batches(batch_size: 256)
{:ok, copy} =
LanceDB.Connection.create_table(
db,
"documents_copy",
LanceDB.Source.batches(batches, schema: schema),
[]
)
{:ok, result} =
LanceDB.Table.upsert(
copy,
LanceDB.Source.rows([%{id: 1, text: "updated"}, %{id: 10001, text: "new"}], batch_size: 256),
[]
)Connection.create_table/4 accepts Source.rows/2 or Source.batches/2
with an explicit schema. Creation supports :create, :overwrite,
:exist_ok, schema metadata, and the creation controls.
Empty input creates an empty table. :exist_ok can return the existing
table without consuming the source.
Table.merge/3 accepts either Source representation with conditions, join
keys, index control, and timeout. Table.upsert/3 enables insert and update
together. Every operation uses one commit. The merge evaluates deletion
against the complete source. Merge keys must be non-null and unique across
the whole source. Native validation keeps a set of keys.
Memory use thus grows with distinct keys, although row batches use bounded
producer buffering. Streamed merges return WriteResult.
For computed columns, a callback receives each native batch from an independent snapshot. It returns a native batch or a list of row maps for new columns:
{:ok, result} = LanceDB.SchemaEvolution.backfill_columns(table, [text_length: :int64], fn batch ->
{:ok, rows} = Batch.to_rows(batch)
Enum.map(rows, &%{text_length: String.length(&1["text"])})
end, read_columns: [:text], batch_size: 256)The callback runs in the caller's BEAM process. It must keep row count and
order. read_columns limits its input fields. Output columns use the
supplied schema, including nested types and field metadata. Before it
starts, the operation checks that the target still matches the source
snapshot. It commits against that dataset version.
The SDK's commit-conflict rules apply to concurrent writes.
For precomputed values, use SchemaEvolution.add_from_source/3. Supply the
output schema on Source.rows/2 or Source.batches/2. You can also supply
expected_version. Supply exactly one output row per visible table row in
physical scan order. Exclude deleted rows. The callback API handles this
alignment for you.
These operations return WriteResult.
A failed producer aborts input without committing a partial source. Backfills check exact row counts and wait for successful EOF, even after receiving enough rows. Failed writes can leave uncommitted storage files, as with SDK writes. An error after the final commit starts can leave the commit outcome unknown.