From 8c8d34f019f0adbb6a58b82e12de309c37356773 Mon Sep 17 00:00:00 2001 From: Brendan Clement Date: Mon, 1 Jun 2026 19:25:18 -0700 Subject: [PATCH] feat(nodejs): add update_field_metadata bindings --- docs/src/js/classes/Table.md | 23 ++++++++ docs/src/js/globals.md | 2 + docs/src/js/interfaces/FieldMetadataUpdate.md | 41 +++++++++++++ .../interfaces/UpdateFieldMetadataResult.md | 15 +++++ nodejs/__test__/table.test.ts | 27 +++++++++ nodejs/lancedb/index.ts | 2 + nodejs/lancedb/table.ts | 35 +++++++++++ nodejs/src/table.rs | 58 ++++++++++++++++++- 8 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 docs/src/js/interfaces/FieldMetadataUpdate.md create mode 100644 docs/src/js/interfaces/UpdateFieldMetadataResult.md diff --git a/docs/src/js/classes/Table.md b/docs/src/js/classes/Table.md index 62b962daf..1675f2c93 100644 --- a/docs/src/js/classes/Table.md +++ b/docs/src/js/classes/Table.md @@ -994,6 +994,29 @@ based on the row being updated (e.g. "my_col + 1") *** +### updateFieldMetadata() + +```ts +abstract updateFieldMetadata(updates): Promise +``` + +Update per-field (column) metadata. + +#### Parameters + +* **updates**: [`FieldMetadataUpdate`](../interfaces/FieldMetadataUpdate.md)[] + One or more per-field updates. Each + update's metadata is merged into the field's existing metadata by default; + a value of `null` deletes that key, and `replace: true` swaps the whole map. + +#### Returns + +`Promise`<[`UpdateFieldMetadataResult`](../interfaces/UpdateFieldMetadataResult.md)> + +resolves to the new table version. + +*** + ### vectorSearch() ```ts diff --git a/docs/src/js/globals.md b/docs/src/js/globals.md index 2e7254020..7d26fd75b 100644 --- a/docs/src/js/globals.md +++ b/docs/src/js/globals.md @@ -65,6 +65,7 @@ - [DropNamespaceOptions](interfaces/DropNamespaceOptions.md) - [DropNamespaceResponse](interfaces/DropNamespaceResponse.md) - [ExecutableQuery](interfaces/ExecutableQuery.md) +- [FieldMetadataUpdate](interfaces/FieldMetadataUpdate.md) - [FragmentStatistics](interfaces/FragmentStatistics.md) - [FragmentSummaryStats](interfaces/FragmentSummaryStats.md) - [FtsOptions](interfaces/FtsOptions.md) @@ -101,6 +102,7 @@ - [TimeoutConfig](interfaces/TimeoutConfig.md) - [TlsConfig](interfaces/TlsConfig.md) - [TokenResponse](interfaces/TokenResponse.md) +- [UpdateFieldMetadataResult](interfaces/UpdateFieldMetadataResult.md) - [UpdateOptions](interfaces/UpdateOptions.md) - [UpdateResult](interfaces/UpdateResult.md) - [Version](interfaces/Version.md) diff --git a/docs/src/js/interfaces/FieldMetadataUpdate.md b/docs/src/js/interfaces/FieldMetadataUpdate.md new file mode 100644 index 000000000..38c675630 --- /dev/null +++ b/docs/src/js/interfaces/FieldMetadataUpdate.md @@ -0,0 +1,41 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / FieldMetadataUpdate + +# Interface: FieldMetadataUpdate + +A per-field metadata update, addressed by dot-path. + +## Properties + +### metadata + +```ts +metadata: Record; +``` + +Metadata key/value pairs. Merged into the field's existing metadata by +default; a value of `null` deletes that key. + +*** + +### path + +```ts +path: string; +``` + +Dot-separated path to the field. For a top-level column this is just its +name; for a nested field it's the path, e.g. "a.b.c". + +*** + +### replace? + +```ts +optional replace: boolean; +``` + +If true, replace the field's entire metadata map instead of merging. diff --git a/docs/src/js/interfaces/UpdateFieldMetadataResult.md b/docs/src/js/interfaces/UpdateFieldMetadataResult.md new file mode 100644 index 000000000..e5433c5db --- /dev/null +++ b/docs/src/js/interfaces/UpdateFieldMetadataResult.md @@ -0,0 +1,15 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / UpdateFieldMetadataResult + +# Interface: UpdateFieldMetadataResult + +## Properties + +### version + +```ts +version: number; +``` diff --git a/nodejs/__test__/table.test.ts b/nodejs/__test__/table.test.ts index 3be56d3c7..12fee4733 100644 --- a/nodejs/__test__/table.test.ts +++ b/nodejs/__test__/table.test.ts @@ -1571,6 +1571,33 @@ describe("schema evolution", function () { expect(await table.schema()).toEqual(expectedSchema3); }); + it("can update field metadata", async function () { + const con = await connect(tmpDir.name); + const table = await con.createTable("fm", [ + { id: 1, category: "a" }, + { id: 2, category: "b" }, + ]); + + const res = await table.updateFieldMetadata([ + { path: "category", metadata: { unit: "label", pii: "false" } }, + ]); + expect(res).toHaveProperty("version"); + expect(res.version).toBe(2); + + let cat = (await table.schema()).fields.find((f) => f.name === "category"); + expect(cat?.metadata.get("unit")).toBe("label"); + expect(cat?.metadata.get("pii")).toBe("false"); + + // merge: add a key, delete one via null, keep the rest + await table.updateFieldMetadata([ + { path: "category", metadata: { source: "import", pii: null } }, + ]); + cat = (await table.schema()).fields.find((f) => f.name === "category"); + expect(cat?.metadata.get("unit")).toBe("label"); // preserved + expect(cat?.metadata.get("source")).toBe("import"); // added + expect(cat?.metadata.has("pii")).toBe(false); // deleted + }); + it("can cast to various types", async function () { const con = await connect(tmpDir.name); diff --git a/nodejs/lancedb/index.ts b/nodejs/lancedb/index.ts index 56b86ac5f..f4f724e8a 100644 --- a/nodejs/lancedb/index.ts +++ b/nodejs/lancedb/index.ts @@ -42,6 +42,7 @@ export { AddResult, AddColumnsResult, AlterColumnsResult, + UpdateFieldMetadataResult, DeleteResult, DropColumnsResult, UpdateResult, @@ -117,6 +118,7 @@ export { WriteProgress, LsmWriteSpec, ColumnAlteration, + FieldMetadataUpdate, } from "./table"; export { diff --git a/nodejs/lancedb/table.ts b/nodejs/lancedb/table.ts index ae2e86995..e3821bd81 100644 --- a/nodejs/lancedb/table.ts +++ b/nodejs/lancedb/table.ts @@ -32,6 +32,7 @@ import { OptimizeStats, TableStatistics, Tags, + UpdateFieldMetadataResult, UpdateResult, Table as _NativeTable, } from "./native"; @@ -508,6 +509,18 @@ export abstract class Table { abstract alterColumns( columnAlterations: ColumnAlteration[], ): Promise; + + /** + * Update per-field (column) metadata. + * @param {FieldMetadataUpdate[]} updates One or more per-field updates. Each + * update's metadata is merged into the field's existing metadata by default; + * a value of `null` deletes that key, and `replace: true` swaps the whole map. + * @returns {Promise} resolves to the new table version. + */ + abstract updateFieldMetadata( + updates: FieldMetadataUpdate[], + ): Promise; + /** * Drop one or more columns from the dataset * @@ -1037,6 +1050,12 @@ export class LocalTable extends Table { return await this.inner.alterColumns(processedAlterations); } + async updateFieldMetadata( + updates: FieldMetadataUpdate[], + ): Promise { + return await this.inner.updateFieldMetadata(updates); + } + async dropColumns(columnNames: string[]): Promise { return await this.inner.dropColumns(columnNames); } @@ -1203,3 +1222,19 @@ export interface ColumnAlteration { /** Set the new nullability. Note that a nullable column cannot be made non-nullable. */ nullable?: boolean; } + +/** A per-field metadata update, addressed by dot-path. */ +export interface FieldMetadataUpdate { + /** + * Dot-separated path to the field. For a top-level column this is just its + * name; for a nested field it's the path, e.g. "a.b.c". + */ + path: string; + /** + * Metadata key/value pairs. Merged into the field's existing metadata by + * default; a value of `null` deletes that key. + */ + metadata: Record; + /** If true, replace the field's entire metadata map instead of merging. */ + replace?: boolean; +} diff --git a/nodejs/src/table.rs b/nodejs/src/table.rs index 16cde35d8..2add2f3f3 100644 --- a/nodejs/src/table.rs +++ b/nodejs/src/table.rs @@ -5,8 +5,9 @@ use std::collections::HashMap; use lancedb::ipc::{ipc_file_to_batches, ipc_file_to_schema}; use lancedb::table::{ - AddDataMode, ColumnAlteration as LanceColumnAlteration, Duration, NewColumnTransform, - OptimizeAction, OptimizeOptions, Table as LanceDbTable, + AddDataMode, ColumnAlteration as LanceColumnAlteration, Duration, + FieldMetadataUpdate as LanceFieldMetadataUpdate, NewColumnTransform, OptimizeAction, + OptimizeOptions, Table as LanceDbTable, }; use napi::bindgen_prelude::*; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; @@ -355,6 +356,23 @@ impl Table { Ok(res.into()) } + #[napi(catch_unwind)] + pub async fn update_field_metadata( + &self, + updates: Vec, + ) -> napi::Result { + let updates = updates + .into_iter() + .map(LanceFieldMetadataUpdate::from) + .collect::>(); + let res = self + .inner_ref()? + .update_field_metadata(&updates) + .await + .default_error()?; + Ok(res.into()) + } + #[napi(catch_unwind)] pub async fn drop_columns(&self, columns: Vec) -> napi::Result { let col_refs = columns.iter().map(String::as_str).collect::>(); @@ -747,6 +765,29 @@ pub struct ColumnAlteration { pub nullable: Option, } +/// A per-field metadata update, addressed by dot-path. Merges into the field's +/// existing metadata by default; a `null` value deletes a key, and `replace` +/// swaps the field's entire metadata map. +#[napi(object)] +pub struct FieldMetadataUpdate { + /// Dot-separated path to the field (e.g. "embedding" or "a.b.c"). + pub path: String, + /// Metadata keys to set; a `null` value deletes that key. + pub metadata: HashMap>, + /// If true, replace the field's entire metadata map instead of merging. + pub replace: Option, +} + +impl From for LanceFieldMetadataUpdate { + fn from(js: FieldMetadataUpdate) -> Self { + Self { + path: js.path, + metadata: js.metadata, + replace: js.replace.unwrap_or(false), + } + } +} + impl TryFrom for LanceColumnAlteration { type Error = String; fn try_from(js: ColumnAlteration) -> std::result::Result { @@ -987,6 +1028,19 @@ impl From for AlterColumnsResult { } } +#[napi(object)] +pub struct UpdateFieldMetadataResult { + pub version: i64, +} + +impl From for UpdateFieldMetadataResult { + fn from(value: lancedb::table::UpdateFieldMetadataResult) -> Self { + Self { + version: value.version as i64, + } + } +} + #[napi(object)] pub struct DropColumnsResult { pub version: i64,