Files
lancedb/docs/web-tests/ts/tables.test.ts
T
Jack YeandBrendan Clement 47b7babc08 docs: bring the open-source documentation into this repository (#4122)
Move the public website pages into `docs/web` beside the SDK, with
shared navigation and executable example sources in `docs/web-tests`.
The combined website is assembled by
[lancedb/docs#355](https://github.com/lancedb/docs/pull/355), with six
Enterprise replacements from
[lancedb/sophon#7681](https://github.com/lancedb/sophon/pull/7681).

This includes Function guidance, runnable Quickstarts, regenerated
snippets, production contact/demo fixes, the legacy SQL route, and the
LeRobot redirect. LeRobot remains attributed to Geneva; all 34 Geneva
pages remain in the docs repository until Function officially launches.

Materialized-view guidance follows the full rebuild refresh introduced
by #4373: local refresh writes the complete result, has no `full`
argument, supports `source_version`, and no longer requires stable row
IDs. Deployment SQL refresh options and stable-ID setup are qualified by
version/configuration. Function input guidance distinguishes ordinary
stored columns from computed inputs supported by newer deployments.

**Validation:** independently accepted local checks include 95 Python
tests (real Enterprise excluded), eight TypeScript tests, Rust
Quickstart, snippet parity, typos, deterministic assembly, Mint
validation and links/anchors/redirects. The combined baseline retains
all 181 production routes and 17 legacy redirects. Examples ran locally
against source-built Python SDK 0.40.0b13; existing GitHub workflows do
not run `docs/web-tests`. New main at `069b2f7d` is 0.41.0b0 and adds
computed Function inputs; that SDK was not exercised by the accepted
tests. Latest prose corrections receive focused site validation.

**Merge/rollout:** merge this and #7681 before #355's combined CI, which
reads producer `main`. This is migration infrastructure: section
overlays, source-owned skills and automatic versioned paired releases
remain follow-ups. The current manual publisher is transitional. Hosted
validation, a supported SDK/Enterprise release pair and a real
Enterprise walkthrough remain pending.

Production Mintlify watches `lancedb/docs:deploy-freeze`, directory
`docs`; preserve that source through review. The separate SDK GitHub
Pages reference workflow rebuilds on main merges.

**Updated-head CI (9a50f8db):**
[Typos](https://github.com/lancedb/lancedb/actions/runs/37654185443/job/112904990558),
[PR
title](https://github.com/lancedb/lancedb/actions/runs/37654179507/job/112904974595)
and
[Aikido](https://app.aikido.dev/featurebranch/scan/215386145?groupId=74229)
pass. Critical findings in the example lockfiles were patched: all Sharp
copies use 0.35.5, protobufjs 7.6.6, tar 7.5.22, quinn-proto 0.11.19,
zerovec 0.11.8 and zerovec-derive 0.11.6. The SDK's product dependencies
are unchanged. The revised lockfiles pass eight TypeScript examples,
HuggingFace decode/resize/crop/rescale/normalize plus AVIF roundtrip,
and Rust Quickstart. Aikido passing does not mean every advisory is
cleared; npm still reports 29 high and five moderate advisories in the
example dependency graph. No scanner bypass or suppression was added.

The Function prose correction passes combined/standalone Mint,
deterministic assembly, routes and assets. The revised 325-file site
checksum is
`175bec06864abb400dd292e667ba93448c0479b8359fd7bf6146afabf9040647`. This
PR remains open for independent review; further merges are on hold.

---------

Co-authored-by: Brendan Clement <brendan.clement@lancedb.com>
2026-10-07 11:15:00 -07:00

1062 lines
34 KiB
TypeScript

// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
import { expect, jest, test } from "@jest/globals";
import * as arrow from "apache-arrow";
import * as lancedb from "@lancedb/lancedb";
import { withTempDirectory } from "./util.ts";
async function updateConnectEnterpriseExample() {
// --8<-- [start:update_connect_enterprise]
const db = await lancedb.connect("db://your-project-slug", {
apiKey: "your-api-key",
region: "us-east-1",
});
// --8<-- [end:update_connect_enterprise]
return db;
}
async function updateConnectLocalExample() {
// --8<-- [start:update_connect_local]
const db = await lancedb.connect("./data");
// --8<-- [end:update_connect_local]
return db;
}
test("table creation snippets (async)", async () => {
await withTempDirectory(async (databaseDir) => {
const db = await lancedb.connect(databaseDir);
// --8<-- [start:create_table_from_dicts]
type Location = {
vector: number[];
lat: number;
long: number;
};
const data: Location[] = [
{ vector: [1.1, 1.2], lat: 45.5, long: -122.7 },
{ vector: [0.2, 1.8], lat: 40.1, long: -74.1 },
];
const table = await db.createTable("test_table", data, {
mode: "overwrite",
});
// --8<-- [end:create_table_from_dicts]
expect(await table.countRows()).toBe(2);
await db.createTable("conflict_table", data);
// --8<-- [start:create_table_conflict_handling]
// Idempotent open: reuse the existing table if it exists.
// The provided data is ignored; the schema is validated against the
// existing table and a mismatch raises an error.
let conflictTable = await db.createTable("conflict_table", data, {
existOk: true,
});
// Overwrite: drop the existing table and create a new one with the
// provided data. This permanently discards the old table's data.
conflictTable = await db.createTable("conflict_table", data, {
mode: "overwrite",
});
// --8<-- [end:create_table_conflict_handling]
expect(await conflictTable.countRows()).toBe(2);
// --8<-- [start:create_table_custom_schema]
const customSchema = new arrow.Schema([
new arrow.Field(
"vector",
new arrow.FixedSizeList(
4,
new arrow.Field("item", new arrow.Float32(), true),
),
),
new arrow.Field("lat", new arrow.Float32()),
new arrow.Field("long", new arrow.Float32()),
]);
const customSchemaData = lancedb.makeArrowTable(
[
{ vector: [1.1, 1.2, 1.3, 1.4], lat: 45.5, long: -122.7 },
{ vector: [0.2, 1.8, 0.4, 3.6], lat: 40.1, long: -74.1 },
],
{ schema: customSchema },
);
const customSchemaTable = await db.createTable(
"my_table_custom_schema",
customSchemaData,
{ mode: "overwrite" },
);
// --8<-- [end:create_table_custom_schema]
expect(await customSchemaTable.countRows()).toBe(2);
// --8<-- [start:create_table_from_arrow]
const arrowSchema = new arrow.Schema([
new arrow.Field(
"vector",
new arrow.FixedSizeList(
16,
new arrow.Field("item", new arrow.Float32(), true),
),
),
new arrow.Field("text", new arrow.Utf8()),
]);
const arrowData = lancedb.makeArrowTable(
[
{ vector: Array(16).fill(0.1), text: "foo" },
{ vector: Array(16).fill(0.2), text: "bar" },
],
{ schema: arrowSchema },
);
const arrowTable = await db.createTable("f32_tbl", arrowData, {
mode: "overwrite",
});
// --8<-- [end:create_table_from_arrow]
expect(await arrowTable.countRows()).toBe(2);
// --8<-- [start:create_table_from_iterator]
const batchSchema = new arrow.Schema([
new arrow.Field(
"vector",
new arrow.FixedSizeList(
4,
new arrow.Field("item", new arrow.Float32(), true),
),
),
new arrow.Field("item", new arrow.Utf8()),
new arrow.Field("price", new arrow.Float32()),
]);
const tableForBatches = await db.createEmptyTable(
"batched_table",
batchSchema,
{
mode: "overwrite",
},
);
const rows = Array.from({ length: 10 }, (_, i) => ({
vector: [i + 0.1, i + 0.2, i + 0.3, i + 0.4],
item: `item-${i + 1}`,
price: (i + 1) * 10,
}));
const chunkSize = 2;
for (let i = 0; i < rows.length; i += chunkSize) {
const batch = lancedb.makeArrowTable(rows.slice(i, i + chunkSize), {
schema: batchSchema,
});
await tableForBatches.add(batch);
}
// --8<-- [end:create_table_from_iterator]
expect(await tableForBatches.countRows()).toBe(10);
// --8<-- [start:open_existing_table]
const openTableData = [{ vector: [1.1, 1.2], lat: 45.5, long: -122.7 }];
await db.createTable("test_table_open", openTableData, {
mode: "overwrite",
});
console.log(await db.tableNames());
const openedTable = await db.openTable("test_table_open");
// --8<-- [end:open_existing_table]
expect(await openedTable.countRows()).toBe(1);
// --8<-- [start:create_empty_table]
const emptySchema = new arrow.Schema([
new arrow.Field(
"vector",
new arrow.FixedSizeList(
2,
new arrow.Field("item", new arrow.Float32(), true),
),
),
new arrow.Field("item", new arrow.Utf8()),
new arrow.Field("price", new arrow.Float32()),
]);
const emptyTable = await db.createEmptyTable(
"test_empty_table",
emptySchema,
{
mode: "overwrite",
},
);
// --8<-- [end:create_empty_table]
expect(await emptyTable.countRows()).toBe(0);
// --8<-- [start:drop_table]
await db.createTable("my_table", [{ vector: [1.1, 1.2], lat: 45.5 }], {
mode: "overwrite",
});
await db.dropTable("my_table");
// --8<-- [end:drop_table]
expect(await db.tableNames()).not.toContain("my_table");
});
});
test("add progress snippet (async)", async () => {
await withTempDirectory(async (databaseDir) => {
const db = await lancedb.connect(databaseDir);
const table = await db.createTable("progress_table", [
{ vector: [1.1, 1.2], item: "foo" },
]);
const moreData = [
{ vector: [2.1, 2.2], item: "bar" },
{ vector: [3.1, 3.2], item: "baz" },
];
const log = jest.spyOn(console, "log").mockImplementation(() => {});
try {
// --8<-- [start:add_progress]
// Track ingestion progress as batches are written.
await table.add(moreData, {
progress: (p) => {
const rows = `${p.outputRows}/${p.totalRows ?? "?"} rows`;
const size = `${p.outputBytes} bytes, ${p.elapsedSeconds.toFixed(1)}s`;
const tasks = `${p.activeTasks}/${p.totalTasks} tasks active`;
const done = p.done ? " \u2014 done" : "";
console.log(`wrote ${rows} (${size}, ${tasks})${done}`);
},
});
// --8<-- [end:add_progress]
expect(log.mock.calls.at(-1)?.[0]).toMatch(/^wrote 2\/2 rows .* — done$/);
} finally {
log.mockRestore();
}
expect(await table.countRows()).toBe(3);
});
});
test("schema evolution snippets (async)", async () => {
await withTempDirectory(async (databaseDir) => {
const db = await lancedb.connect(databaseDir);
// --8<-- [start:schema_add_setup]
const schemaAddData = [
{
id: 1,
name: "Laptop",
price: 1200.0,
vector: Array.from({ length: 128 }, () => Math.random()),
},
{
id: 2,
name: "Smartphone",
price: 800.0,
vector: Array.from({ length: 128 }, () => Math.random()),
},
{
id: 3,
name: "Headphones",
price: 150.0,
vector: Array.from({ length: 128 }, () => Math.random()),
},
];
const schemaAddTable = await db.createTable(
"schema_evolution_add_example",
schemaAddData,
{ mode: "overwrite" },
);
// --8<-- [end:schema_add_setup]
expect(await schemaAddTable.countRows()).toBe(3);
// --8<-- [start:add_columns_calculated]
// Add a discounted price column (10% discount)
await schemaAddTable.addColumns([
{
name: "discounted_price",
valueSql: "cast((price * 0.9) as float)",
},
]);
// --8<-- [end:add_columns_calculated]
// --8<-- [start:add_columns_default_values]
// Add a stock status column with default value
await schemaAddTable.addColumns([
{
name: "in_stock",
valueSql: "cast(true as boolean)",
},
]);
// --8<-- [end:add_columns_default_values]
// --8<-- [start:add_columns_nullable]
// Add a nullable timestamp column
await schemaAddTable.addColumns([
{
name: "last_ordered",
valueSql: "cast(NULL as timestamp)",
},
]);
// --8<-- [end:add_columns_nullable]
// --8<-- [start:add_feature_columns_sql]
await schemaAddTable.addColumns([
{
name: "price_per_id",
valueSql: "cast(price / id as float)",
},
{
name: "price_log",
valueSql: "ln(price)",
},
{
name: "price_score",
valueSql: "cast(price / (price + 100.0) as float)",
},
]);
// --8<-- [end:add_feature_columns_sql]
expect((await schemaAddTable.schema()).fields.map((field) => field.name)).toEqual(
expect.arrayContaining(["price_per_id", "price_log", "price_score"]),
);
// --8<-- [start:schema_alter_setup]
const schemaAlter = new arrow.Schema([
new arrow.Field("id", new arrow.Int64()),
new arrow.Field("name", new arrow.Utf8()),
new arrow.Field("price", new arrow.Int32()),
new arrow.Field("discount_price", new arrow.Float64()),
new arrow.Field(
"vector",
new arrow.FixedSizeList(
128,
new arrow.Field("item", new arrow.Float32(), true),
),
),
]);
const schemaAlterData = lancedb.makeArrowTable(
[
{
id: 1,
name: "Laptop",
price: 1200,
discount_price: 1080.0,
vector: Array.from({ length: 128 }, () => Math.random()),
},
{
id: 2,
name: "Smartphone",
price: 800,
discount_price: 720.0,
vector: Array.from({ length: 128 }, () => Math.random()),
},
],
{ schema: schemaAlter },
);
const schemaAlterTable = await db.createTable(
"schema_evolution_alter_example",
schemaAlterData,
{ mode: "overwrite" },
);
// --8<-- [end:schema_alter_setup]
expect(await schemaAlterTable.countRows()).toBe(2);
// --8<-- [start:alter_columns_rename]
// Rename discount_price to sale_price
await schemaAlterTable.alterColumns([
{ path: "discount_price", rename: "sale_price" },
]);
// --8<-- [end:alter_columns_rename]
// --8<-- [start:alter_columns_data_type]
// Change price from int32 to int64 for larger numbers
await schemaAlterTable.alterColumns([
{ path: "price", dataType: new arrow.Int64() },
]);
// --8<-- [end:alter_columns_data_type]
// --8<-- [start:alter_columns_nullable]
// Make the name column nullable
await schemaAlterTable.alterColumns([{ path: "name", nullable: true }]);
// --8<-- [end:alter_columns_nullable]
// --8<-- [start:alter_columns_multiple]
// Rename, change type, and make nullable in one operation
await schemaAlterTable.alterColumns([
{
path: "sale_price",
rename: "final_price",
dataType: new arrow.Float64(),
nullable: true,
},
]);
// --8<-- [end:alter_columns_multiple]
// --8<-- [start:alter_columns_with_expression]
// For custom transforms, create a new column from a SQL expression.
const expressionTable = await db.createTable(
"schema_evolution_expression_example",
[{ id: 1, price_text: "$100" }],
{ mode: "overwrite" },
);
await expressionTable.addColumns([
{
name: "price_numeric",
valueSql: "cast(replace(price_text, '$', '') as int)",
},
]);
await expressionTable.dropColumns(["price_text"]);
await expressionTable.alterColumns([
{ path: "price_numeric", rename: "price" },
]);
// --8<-- [end:alter_columns_with_expression]
expect(await expressionTable.countRows()).toBe(1);
// --8<-- [start:schema_drop_setup]
const schemaDropData = [
{
id: 1,
name: "Laptop",
price: 1200.0,
temp_col1: "X",
temp_col2: 100,
vector: Array.from({ length: 128 }, () => Math.random()),
},
{
id: 2,
name: "Smartphone",
price: 800.0,
temp_col1: "Y",
temp_col2: 200,
vector: Array.from({ length: 128 }, () => Math.random()),
},
{
id: 3,
name: "Headphones",
price: 150.0,
temp_col1: "Z",
temp_col2: 300,
vector: Array.from({ length: 128 }, () => Math.random()),
},
];
const schemaDropTable = await db.createTable(
"schema_evolution_drop_example",
schemaDropData,
{ mode: "overwrite" },
);
// --8<-- [end:schema_drop_setup]
expect(await schemaDropTable.countRows()).toBe(3);
// --8<-- [start:drop_columns_single]
// Remove the first temporary column
await schemaDropTable.dropColumns(["temp_col1"]);
// --8<-- [end:drop_columns_single]
// --8<-- [start:drop_columns_multiple]
// Remove the second temporary column
await schemaDropTable.dropColumns(["temp_col2"]);
// --8<-- [end:drop_columns_multiple]
// --8<-- [start:alter_vector_column]
const oldDim = 384;
const newDim = 1024;
const vectorSchema = new arrow.Schema([
new arrow.Field("id", new arrow.Int64()),
new arrow.Field(
"embedding",
new arrow.FixedSizeList(
oldDim,
new arrow.Field("item", new arrow.Float16(), true),
),
true,
),
]);
const vectorData = lancedb.makeArrowTable(
[{ id: 1, embedding: Array.from({ length: oldDim }, () => Math.random()) }],
{ schema: vectorSchema },
);
const vectorTable = await db.createTable("vector_alter_example", vectorData, {
mode: "overwrite",
});
// Changing FixedSizeList dimensions (384 -> 1024) is not supported via alterColumns.
// Use addColumns + dropColumns + alterColumns(rename) to replace the column.
await vectorTable.addColumns([
{
name: "embedding_v2",
valueSql: `arrow_cast(NULL, 'FixedSizeList(${newDim}, Float16)')`,
},
]);
await vectorTable.dropColumns(["embedding"]);
await vectorTable.alterColumns([{ path: "embedding_v2", rename: "embedding" }]);
// --8<-- [end:alter_vector_column]
expect(await vectorTable.countRows()).toBe(1);
const fieldMetadataTable = await db.createTable(
"schema_field_metadata_example",
[
{ id: 0, category: "a" },
{ id: 1, category: "b" },
],
{ mode: "overwrite" },
);
// --8<-- [start:schema_field_metadata_merge]
// Set two metadata keys on the `category` field.
const res = await fieldMetadataTable.updateFieldMetadata([
{ path: "category", metadata: { unit: "label", pii: "false" } },
]);
console.log(res.version);
// Merge: add a new key, delete one via null, keep the rest.
await fieldMetadataTable.updateFieldMetadata([
{ path: "category", metadata: { source: "import", pii: null } },
]);
// --8<-- [end:schema_field_metadata_merge]
// --8<-- [start:schema_field_metadata_replace]
await fieldMetadataTable.updateFieldMetadata([
{
path: "category",
metadata: { owner: "search-team" },
replace: true,
},
]);
// --8<-- [end:schema_field_metadata_replace]
});
});
test("update snippets (async)", async () => {
// Keep connection snippets in this file, but do not run enterprise/local examples in CI.
void updateConnectEnterpriseExample;
void updateConnectLocalExample;
await withTempDirectory(async (databaseDir) => {
const db = await lancedb.connect(databaseDir);
{
// --8<-- [start:update_example_table_setup]
const table = await db.createTable(
"users_example",
[
{ id: 1, name: "Alice", login_count: 10 },
{ id: 2, name: "Bob", login_count: 20 },
],
{ mode: "overwrite" },
);
// --8<-- [end:update_example_table_setup]
await table.countRows();
}
{
// --8<-- [start:update_operation]
const table = await db.createTable(
"users_example",
[
{ id: 1, name: "Alice", login_count: 10 },
{ id: 2, name: "Bob", login_count: 20 },
],
{ mode: "overwrite" },
);
await table.update({ where: "id = 2", values: { name: "Bobby" } });
// --8<-- [end:update_operation]
await table.countRows();
}
{
// --8<-- [start:update_using_sql]
const table = await db.createTable(
"users_example",
[
{ id: 1, name: "Alice", login_count: 10 },
{ id: 2, name: "Bob", login_count: 20 },
],
{ mode: "overwrite" },
);
await table.update({
where: "id = 2",
valuesSql: { login_count: "login_count + 1" },
});
// --8<-- [end:update_using_sql]
await table.countRows();
}
{
// --8<-- [start:merge_matched_update_only]
const table = await db.createTable(
"users_example",
[
{ id: 1, name: "Alice", login_count: 10 },
{ id: 2, name: "Bob", login_count: 20 },
],
{ mode: "overwrite" },
);
const incomingUsers = [
{ id: 2, name: "Bobby", login_count: 21 },
{ id: 3, name: "Charlie", login_count: 5 },
];
await table
.mergeInsert("id")
.whenMatchedUpdateAll()
.execute(incomingUsers);
// --8<-- [end:merge_matched_update_only]
await table.countRows();
}
{
// --8<-- [start:insert_if_not_exists]
const table = await db.createTable(
"users_example",
[
{ id: 1, name: "Alice", login_count: 10 },
{ id: 2, name: "Bob", login_count: 20 },
],
{ mode: "overwrite" },
);
const incomingUsers = [
{ id: 2, name: "Bobby", login_count: 21 },
{ id: 3, name: "Charlie", login_count: 5 },
];
await table
.mergeInsert("id")
.whenNotMatchedInsertAll()
.execute(incomingUsers);
// --8<-- [end:insert_if_not_exists]
await table.countRows();
}
{
// --8<-- [start:merge_update_insert]
const table = await db.createTable(
"users_example",
[
{ id: 1, name: "Alice", login_count: 10 },
{ id: 2, name: "Bob", login_count: 20 },
],
{ mode: "overwrite" },
);
const incomingUsers = [
{ id: 2, name: "Bobby", login_count: 21 },
{ id: 3, name: "Charlie", login_count: 5 },
];
await table
.mergeInsert("id")
.whenMatchedUpdateAll()
.whenNotMatchedInsertAll()
.execute(incomingUsers);
// --8<-- [end:merge_update_insert]
await table.countRows();
}
{
// --8<-- [start:merge_delete_missing_by_source]
const table = await db.createTable(
"users_example",
[
{ id: 1, name: "Alice", login_count: 10 },
{ id: 2, name: "Bob", login_count: 20 },
{ id: 3, name: "Charlie", login_count: 5 },
],
{ mode: "overwrite" },
);
const incomingUsers = [
{ id: 2, name: "Bobby", login_count: 21 },
{ id: 3, name: "Charlie", login_count: 5 },
];
await table
.mergeInsert("id")
.whenMatchedUpdateAll()
.whenNotMatchedInsertAll()
.whenNotMatchedBySourceDelete()
.execute(incomingUsers);
// --8<-- [end:merge_delete_missing_by_source]
await table.countRows();
}
{
// --8<-- [start:merge_partial_columns]
const table = await db.createTable(
"users_example",
[
{ id: 1, name: "Alice", login_count: 10 },
{ id: 2, name: "Bob", login_count: 20 },
],
{ mode: "overwrite" },
);
const incomingUsers = [
{ id: 2, name: "Bobby" },
{ id: 3, name: "Charlie" },
];
await table
.mergeInsert("id")
.whenMatchedUpdateAll()
.whenNotMatchedInsertAll()
.execute(incomingUsers);
// --8<-- [end:merge_partial_columns]
await table.countRows();
}
{
const table = await db.createTable(
"users_example",
[
{ id: 1, name: "Alice", login_count: 10 },
{ id: 2, name: "Bob", login_count: 20 },
{ id: 3, name: "Charlie", login_count: 5 },
],
{ mode: "overwrite" },
);
// --8<-- [start:delete_operation]
// delete data
const predicate = "id = 3";
await table.delete(predicate);
// --8<-- [end:delete_operation]
await table.countRows();
}
{
const table = await db.createTable(
"users_cleanup_example",
[
{ id: 1, name: "Alice", login_count: 10 },
{ id: 2, name: "Bob", login_count: 20 },
{ id: 3, name: "Charlie", login_count: 5 },
],
{ mode: "overwrite" },
);
// --8<-- [start:update_optimize_cleanup]
const olderThan = new Date();
olderThan.setDate(olderThan.getDate() - 1);
await table.optimize({ cleanupOlderThan: olderThan });
// --8<-- [end:update_optimize_cleanup]
}
});
});
test("versioning snippets (async)", async () => {
await withTempDirectory(async (databaseDir) => {
const db = await lancedb.connect(databaseDir);
// --8<-- [start:versioning_basic_setup]
const tableName = "quotes_versioning_example";
const data = [
{ id: 1, author: "Richard", quote: "Wubba Lubba Dub Dub!" },
{ id: 2, author: "Morty", quote: "Rick, what's going on?" },
{
id: 3,
author: "Richard",
quote: "I turned myself into a pickle, Morty!",
},
];
const table = await db.createTable(tableName, data, { mode: "overwrite" });
// --8<-- [end:versioning_basic_setup]
expect(await table.countRows()).toBe(3);
// --8<-- [start:versioning_check_initial_version]
const versions = await table.listVersions();
const currentVersion = await table.version();
console.log(`Number of versions after creation: ${versions.length}`);
console.log(`Current version: ${currentVersion}`);
// --8<-- [end:versioning_check_initial_version]
expect(versions.length).toBe(1);
expect(currentVersion).toBe(versions[versions.length - 1].version);
// --8<-- [start:versioning_update_data]
await table.update({
where: "author = 'Richard'",
values: { author: "Richard Daniel Sanchez" },
});
const rowsAfterUpdate = await table.countRows(
"author = 'Richard Daniel Sanchez'",
);
console.log(`Rows updated to Richard Daniel Sanchez: ${rowsAfterUpdate}`);
// --8<-- [end:versioning_update_data]
expect(rowsAfterUpdate).toBe(2);
// --8<-- [start:versioning_add_data]
const moreData = [
{
id: 4,
author: "Richard Daniel Sanchez",
quote: "That's the way the news goes!",
},
{ id: 5, author: "Morty", quote: "Aww geez, Rick!" },
];
await table.add(moreData);
// --8<-- [end:versioning_add_data]
expect(await table.countRows()).toBe(5);
// --8<-- [start:versioning_check_versions_after_mod]
const versionsAfterMod = await table.listVersions();
const versionCountAfterMod = versionsAfterMod.length;
const versionAfterMod = await table.version();
console.log(
`Number of versions after modifications: ${versionCountAfterMod}`,
);
console.log(`Current version: ${versionAfterMod}`);
// --8<-- [end:versioning_check_versions_after_mod]
expect(versionCountAfterMod).toBeGreaterThanOrEqual(2);
expect(versionAfterMod).toBe(versionsAfterMod[versionsAfterMod.length - 1].version);
// --8<-- [start:versioning_list_all_versions]
const allVersions = await table.listVersions();
for (const v of allVersions) {
console.log(`Version ${v.version}, created at ${v.timestamp}`);
}
// --8<-- [end:versioning_list_all_versions]
expect(allVersions.length).toBeGreaterThanOrEqual(1);
// --8<-- [start:versioning_rollback]
await table.checkout(versionAfterMod);
await table.restore();
const versionsAfterRollback = await table.listVersions();
const versionCountAfterRollback = versionsAfterRollback.length;
console.log(
`Total number of versions after rollback: ${versionCountAfterRollback}`,
);
// --8<-- [end:versioning_rollback]
expect(versionCountAfterRollback).toBe(versionCountAfterMod + 1);
expect(await table.countRows()).toBe(5);
// --8<-- [start:versioning_checkout_latest]
await table.checkoutLatest();
// --8<-- [end:versioning_checkout_latest]
const latestVersion = await table.version();
const versionsAfterCheckout = await table.listVersions();
expect(latestVersion).toBe(
versionsAfterCheckout[versionsAfterCheckout.length - 1].version,
);
// --8<-- [start:versioning_delete_data]
await table.delete("author = 'Morty'");
const rowsAfterDeletion = await table.countRows();
console.log(`Number of rows after deletion: ${rowsAfterDeletion}`);
// --8<-- [end:versioning_delete_data]
expect(rowsAfterDeletion).toBe(3);
const tagsTable = await db.createTable(
"quotes_tags_example",
[{ id: 1, author: "Richard", quote: "Wubba Lubba Dub Dub!" }],
{ mode: "overwrite" },
); // v1
await tagsTable.add([
{ id: 2, author: "Morty", quote: "Aww geez, Rick!" },
]); // v2
await tagsTable.add([
{ id: 3, author: "Summer", quote: "Whatever, Grandpa" },
]); // v3
// --8<-- [start:versioning_tags]
const tags = await tagsTable.tags();
// Create a tag pointing at a specific version
await tags.create("baseline", 1);
await tags.create("with-edits", await tagsTable.version());
// List all tags on this table
console.log(await tags.list());
// Look up the version a tag points at
console.log(await tags.getVersion("baseline"));
// Move an existing tag to a different version
await tags.update("baseline", 2);
// Check out a version by tag name
await tagsTable.checkout("baseline");
console.log(await tagsTable.version());
// Delete a tag (does not delete the underlying version)
await tags.delete("with-edits");
// Return to the latest version
await tagsTable.checkoutLatest();
// --8<-- [end:versioning_tags]
expect(await tagsTable.version()).toBe(3);
const remainingTags = await tags.list();
expect(remainingTags).toHaveProperty("baseline");
expect(remainingTags).not.toHaveProperty("with-edits");
});
});
test("branch snippets (async)", async () => {
await withTempDirectory(async (databaseDir) => {
const db = await lancedb.connect(databaseDir);
const table = await db.createTable(
"quotes_branches_example",
[
{ id: 1, author: "Lancelot", quote: "My lance never fails." },
{ id: 2, author: "Arthur", quote: "Long live Camelot!" },
{ id: 3, author: "Merlin", quote: "Magic always has a price." },
],
{ mode: "overwrite" },
);
const branches = await table.branches();
// --8<-- [start:branch_create]
// Fork an isolated, writable branch from main's latest version.
// `create` returns a table handle scoped to the new branch.
const branch = await branches.create("exp");
// --8<-- [end:branch_create]
// --8<-- [start:branch_write]
// Writes land on the branch handle only; main is left untouched.
await branch.add([{ id: 4, author: "Lancelot", quote: "For the realm!" }]);
console.log(await branch.countRows()); // 4 rows on the branch
console.log(await table.countRows()); // 3 rows; main is unaffected
// List every branch, each mapped to its metadata (including its fork point).
console.log(await branches.list());
// --8<-- [end:branch_write]
// --8<-- [start:branch_reopen]
// Reopen an existing branch by name from the table handle...
const checkedOut = await branches.checkout("exp");
// ...or open it directly from the database connection.
const branchHandle = await db.openTable(
"quotes_branches_example",
undefined,
{ branch: "exp" },
);
console.log(await checkedOut.countRows(), await branchHandle.countRows()); // both 4
// --8<-- [end:branch_reopen]
// --8<-- [start:branch_delete]
// Delete the branch and its branch-local history. Data on main is safe.
await branches.delete("exp");
// --8<-- [end:branch_delete]
expect(await table.countRows()).toBe(3);
expect(await branches.list()).not.toHaveProperty("exp");
// Setup: a branch with row results that we want to apply to main.
const candidate = await branches.create("candidate");
await candidate.update({
where: "id = 1",
values: { quote: "Revised on the branch" },
});
await candidate.add([
{ id: 4, author: "Galahad", quote: "The grail awaits." },
]);
// --8<-- [start:branch_upsert_to_main]
// This is a row-level upsert, not a merge of branch histories.
// `mergeInsert` updates matching rows and inserts new rows using a stable
// unique key. Filter the branch read if you only want to apply some results.
const rowsToApply = await candidate.toArrow();
await table
.mergeInsert("id")
.whenMatchedUpdateAll() // update rows that already exist on main
.whenNotMatchedInsertAll() // insert rows that are new on the branch
.execute(rowsToApply);
// --8<-- [end:branch_upsert_to_main]
expect(await table.countRows()).toBe(4);
await branches.delete("candidate");
// Setup: a larger table with a vector and a text column to index.
const products = await db.createTable(
"products_branch_index",
Array.from({ length: 512 }, (_, i) => ({
id: i,
vector: Array.from({ length: 4 }, () => Math.random()),
text: `product number ${i}`,
})),
{ mode: "overwrite" },
);
const productBranches = await products.branches();
// --8<-- [start:branch_index]
// Build and validate indexes on a branch before using the configuration on
// main.
const dev = await productBranches.create("index-dev");
// A vector (ANN) index and a full-text search index, both branch-scoped.
await dev.createIndex("vector", {
config: lancedb.Index.ivfPq({
distanceType: "cosine",
numPartitions: 1,
numSubVectors: 2,
}),
});
await dev.createIndex("text", { config: lancedb.Index.fts() });
// Both indexes live only on the branch; main still has none.
console.log((await dev.listIndices()).map((ix) => ix.name)); // branch: two indexes
console.log((await products.listIndices()).map((ix) => ix.name)); // main: [] (untouched)
// --8<-- [end:branch_index]
expect(await dev.listIndices()).toHaveLength(2);
expect(await products.listIndices()).toHaveLength(0);
await productBranches.delete("index-dev");
});
});
test("consistency snippets (async)", async () => {
await withTempDirectory(async (databaseDir) => {
// --8<-- [start:consistency_strong]
const strongWriterDb = await lancedb.connect(databaseDir);
const strongReaderDb = await lancedb.connect(databaseDir, {
readConsistencyInterval: 0,
});
const strongWriterTable = await strongWriterDb.createTable(
"consistency_strong_table",
[{ id: 1 }],
{ mode: "overwrite" },
);
const strongReaderTable = await strongReaderDb.openTable(
"consistency_strong_table",
);
await strongWriterTable.add([{ id: 2 }]);
const strongRowsAfterWrite = await strongReaderTable.countRows();
console.log(`Rows visible with strong consistency: ${strongRowsAfterWrite}`);
// --8<-- [end:consistency_strong]
expect(strongRowsAfterWrite).toBe(2);
// --8<-- [start:consistency_eventual]
const eventualWriterDb = await lancedb.connect(databaseDir);
const eventualReaderDb = await lancedb.connect(databaseDir, {
readConsistencyInterval: 3600,
});
const eventualWriterTable = await eventualWriterDb.createTable(
"consistency_eventual_table",
[{ id: 1 }],
{ mode: "overwrite" },
);
const eventualReaderTable = await eventualReaderDb.openTable(
"consistency_eventual_table",
);
await eventualWriterTable.add([{ id: 2 }]);
const eventualRowsAfterWrite = await eventualReaderTable.countRows();
console.log(
`Rows visible before eventual refresh interval: ${eventualRowsAfterWrite}`,
);
// --8<-- [end:consistency_eventual]
expect(eventualRowsAfterWrite).toBe(1);
// --8<-- [start:consistency_checkout_latest]
const checkoutWriterDb = await lancedb.connect(databaseDir);
const checkoutReaderDb = await lancedb.connect(databaseDir);
const checkoutWriterTable = await checkoutWriterDb.createTable(
"consistency_checkout_latest_table",
[{ id: 1 }],
{ mode: "overwrite" },
);
const checkoutReaderTable = await checkoutReaderDb.openTable(
"consistency_checkout_latest_table",
);
await checkoutWriterTable.add([{ id: 2 }]);
const rowsBeforeRefresh = await checkoutReaderTable.countRows();
console.log(`Rows before checkoutLatest: ${rowsBeforeRefresh}`);
await checkoutReaderTable.checkoutLatest();
const rowsAfterRefresh = await checkoutReaderTable.countRows();
console.log(`Rows after checkoutLatest: ${rowsAfterRefresh}`);
// --8<-- [end:consistency_checkout_latest]
expect(rowsBeforeRefresh).toBe(1);
expect(rowsAfterRefresh).toBe(2);
});
});