From 02de07576e58df4227cf31bc0d70df320d5c23f3 Mon Sep 17 00:00:00 2001 From: Brendan Clement Date: Wed, 13 May 2026 11:49:27 -0700 Subject: [PATCH] feat(nodejs): add namespace management methods on Connection (#3371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary Closes #3363 Adds the four namespace management methods to the NodeJS `Connection`, bringing parity with the Rust core and Python bindings: - `listNamespaces(parent?, options?)` - `createNamespace(namespacePath, options?)` - `dropNamespace(namespacePath, options?)` - `describeNamespace(namespacePath)` ### Test plan - npm test - Ran a smoke test script ```typescript import { connect } from '' import { tmpdir } from "os"; import { mkdtempSync } from "fs"; import { join } from "path"; const dir = mkdtempSync(join(tmpdir(), "lancedb-smoke-")); console.log(`Using temp dir: ${dir}\n`); const db = await connect(dir, { namespaceClientProperties: { manifest_enabled: "true" }, }); console.log("Creating namespaces..."); await db.createNamespace(["analytics"]); await db.createNamespace(["analytics", "sales"], { properties: { owner: "brendan", purpose: "smoke-test" }, }); await db.createNamespace(["marketing"]); const root = await db.listNamespaces(); console.log("Root namespaces:", root.namespaces); const children = await db.listNamespaces(["analytics"]); console.log("Children of 'analytics':", children.namespaces); const descWithProps = await db.describeNamespace(["analytics", "sales"]); console.log("Describe analytics/sales (with properties):", descWithProps); const descNoProps = await db.describeNamespace(["analytics"]); console.log("Describe analytics (no properties):", descNoProps); console.log("Describing a non-existent namespace (expect error)..."); try { await db.describeNamespace(["does-not-exist"]); console.error(" UNEXPECTED: describe succeeded for non-existent namespace"); } catch (err) { console.log(` ✓ Got expected error: ${err.message.split("\n")[0]}`); } await db.dropNamespace(["marketing"]); const afterDrop = await db.listNamespaces(); console.log("Root after dropping marketing:", afterDrop.namespaces); await db.close(); console.log("\nAll operations completed successfully."); ``` ``` Using temp dir: /var/folders/bj/hn6jv9c50y301d1nx0y8xmn00000gn/T/lancedb-smoke-MUC5NI Creating namespaces... Root namespaces: [ 'analytics', 'marketing' ] Children of 'analytics': [ 'sales' ] Describe analytics/sales (with properties): { properties: { purpose: 'smoke-test', owner: 'brendan' } } Describe analytics (no properties): {} Describing a non-existent namespace (expect error)... ✓ Got expected error: lance error: Namespace error: Namespace not found: does-not-exist, rust/lance-namespace-impls/src/dir/manifest.rs:2495:14 Caused by: Namespace error: Namespace not found: does-not-exist, rust/lance-namespace-impls/src/dir/manifest.rs:2495:14 Caused by: Namespace not found: does-not-exist Root after dropping marketing: [ 'analytics' ] All operations completed successfully. ``` ### Documentation - regenerated docs --- Cargo.lock | 1 + docs/src/js/classes/Connection.md | 110 +++++++++++++ docs/src/js/globals.md | 7 + .../js/interfaces/CreateNamespaceOptions.md | 27 ++++ .../js/interfaces/CreateNamespaceResponse.md | 23 +++ .../interfaces/DescribeNamespaceResponse.md | 15 ++ .../src/js/interfaces/DropNamespaceOptions.md | 27 ++++ .../js/interfaces/DropNamespaceResponse.md | 23 +++ .../js/interfaces/ListNamespacesOptions.md | 27 ++++ .../js/interfaces/ListNamespacesResponse.md | 23 +++ nodejs/Cargo.toml | 1 + nodejs/__test__/connection.test.ts | 91 +++++++++++ nodejs/lancedb/connection.ts | 136 ++++++++++++++++ nodejs/lancedb/index.ts | 7 + nodejs/src/connection.rs | 152 ++++++++++++++++++ 15 files changed, 670 insertions(+) create mode 100644 docs/src/js/interfaces/CreateNamespaceOptions.md create mode 100644 docs/src/js/interfaces/CreateNamespaceResponse.md create mode 100644 docs/src/js/interfaces/DescribeNamespaceResponse.md create mode 100644 docs/src/js/interfaces/DropNamespaceOptions.md create mode 100644 docs/src/js/interfaces/DropNamespaceResponse.md create mode 100644 docs/src/js/interfaces/ListNamespacesOptions.md create mode 100644 docs/src/js/interfaces/ListNamespacesResponse.md diff --git a/Cargo.lock b/Cargo.lock index 6c6da7cb6..4b9d561bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5099,6 +5099,7 @@ dependencies = [ "env_logger", "futures", "half", + "lance-namespace", "lancedb", "log", "lzma-sys", diff --git a/docs/src/js/classes/Connection.md b/docs/src/js/classes/Connection.md index a594e94aa..f79eb5232 100644 --- a/docs/src/js/classes/Connection.md +++ b/docs/src/js/classes/Connection.md @@ -148,6 +148,33 @@ Creates a new empty Table *** +### createNamespace() + +```ts +abstract createNamespace(namespacePath, options?): Promise +``` + +Create a new namespace at the given path. + +#### Parameters + +* **namespacePath**: `string`[] + The namespace path to create. + +* **options?**: `Partial`<[`CreateNamespaceOptions`](../interfaces/CreateNamespaceOptions.md)> + Creation `mode` + ("create" | "exist_ok" | "overwrite") and optional `properties` + to attach to the namespace. + +#### Returns + +`Promise`<[`CreateNamespaceResponse`](../interfaces/CreateNamespaceResponse.md)> + +The properties of the + created namespace and an optional transaction id. + +*** + ### createTable() #### createTable(options, namespacePath) @@ -230,6 +257,29 @@ Creates a new Table and initialize it with new data. *** +### describeNamespace() + +```ts +abstract describeNamespace(namespacePath): Promise +``` + +Describe a namespace, returning its properties. + +#### Parameters + +* **namespacePath**: `string`[] + The namespace path to describe, in + parent → child order, e.g. `["analytics", "sales"]`. + +#### Returns + +`Promise`<[`DescribeNamespaceResponse`](../interfaces/DescribeNamespaceResponse.md)> + +The namespace's properties + (may be undefined if the namespace has none). + +*** + ### display() ```ts @@ -263,6 +313,36 @@ Drop all tables in the database. *** +### dropNamespace() + +```ts +abstract dropNamespace(namespacePath, options?): Promise +``` + +Drop a namespace. + +Use `behavior: "cascade"` to also drop everything contained in the +namespace (sub-namespaces and tables). The default `"restrict"` +behavior refuses to drop a non-empty namespace. + +#### Parameters + +* **namespacePath**: `string`[] + The namespace path to drop. + +* **options?**: `Partial`<[`DropNamespaceOptions`](../interfaces/DropNamespaceOptions.md)> + `mode` ("skip" | "fail" + for missing-namespace handling) and `behavior` ("restrict" | "cascade"). + +#### Returns + +`Promise`<[`DropNamespaceResponse`](../interfaces/DropNamespaceResponse.md)> + +Any properties returned by + the server and an optional transaction id. + +*** + ### dropTable() ```ts @@ -299,6 +379,36 @@ Return true if the connection has not been closed *** +### listNamespaces() + +```ts +abstract listNamespaces(namespacePath?, options?): Promise +``` + +List the immediate child namespaces under the given parent. + +Results may be paginated. To retrieve subsequent pages, pass the +`pageToken` returned by a previous call. + +#### Parameters + +* **namespacePath?**: `string`[] + The parent namespace path. Defaults + to the root namespace if omitted. + +* **options?**: `Partial`<[`ListNamespacesOptions`](../interfaces/ListNamespacesOptions.md)> + Pagination options + (`pageToken`, `limit`). + +#### Returns + +`Promise`<[`ListNamespacesResponse`](../interfaces/ListNamespacesResponse.md)> + +Child namespace names and + an optional token for fetching the next page. + +*** + ### openTable() ```ts diff --git a/docs/src/js/globals.md b/docs/src/js/globals.md index 988f35dc6..f6f84f4c7 100644 --- a/docs/src/js/globals.md +++ b/docs/src/js/globals.md @@ -52,9 +52,14 @@ - [ColumnAlteration](interfaces/ColumnAlteration.md) - [CompactionStats](interfaces/CompactionStats.md) - [ConnectionOptions](interfaces/ConnectionOptions.md) +- [CreateNamespaceOptions](interfaces/CreateNamespaceOptions.md) +- [CreateNamespaceResponse](interfaces/CreateNamespaceResponse.md) - [CreateTableOptions](interfaces/CreateTableOptions.md) - [DeleteResult](interfaces/DeleteResult.md) +- [DescribeNamespaceResponse](interfaces/DescribeNamespaceResponse.md) - [DropColumnsResult](interfaces/DropColumnsResult.md) +- [DropNamespaceOptions](interfaces/DropNamespaceOptions.md) +- [DropNamespaceResponse](interfaces/DropNamespaceResponse.md) - [ExecutableQuery](interfaces/ExecutableQuery.md) - [FragmentStatistics](interfaces/FragmentStatistics.md) - [FragmentSummaryStats](interfaces/FragmentSummaryStats.md) @@ -69,6 +74,8 @@ - [IvfFlatOptions](interfaces/IvfFlatOptions.md) - [IvfPqOptions](interfaces/IvfPqOptions.md) - [IvfRqOptions](interfaces/IvfRqOptions.md) +- [ListNamespacesOptions](interfaces/ListNamespacesOptions.md) +- [ListNamespacesResponse](interfaces/ListNamespacesResponse.md) - [MergeResult](interfaces/MergeResult.md) - [OpenTableOptions](interfaces/OpenTableOptions.md) - [OptimizeOptions](interfaces/OptimizeOptions.md) diff --git a/docs/src/js/interfaces/CreateNamespaceOptions.md b/docs/src/js/interfaces/CreateNamespaceOptions.md new file mode 100644 index 000000000..26b02ce8b --- /dev/null +++ b/docs/src/js/interfaces/CreateNamespaceOptions.md @@ -0,0 +1,27 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / CreateNamespaceOptions + +# Interface: CreateNamespaceOptions + +## Properties + +### mode? + +```ts +optional mode: "overwrite" | "create" | "exist_ok"; +``` + +Creation mode. + +*** + +### properties? + +```ts +optional properties: Record; +``` + +Properties to set on the new namespace. diff --git a/docs/src/js/interfaces/CreateNamespaceResponse.md b/docs/src/js/interfaces/CreateNamespaceResponse.md new file mode 100644 index 000000000..5bcc6fcbf --- /dev/null +++ b/docs/src/js/interfaces/CreateNamespaceResponse.md @@ -0,0 +1,23 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / CreateNamespaceResponse + +# Interface: CreateNamespaceResponse + +## Properties + +### properties? + +```ts +optional properties: Record; +``` + +*** + +### transactionId? + +```ts +optional transactionId: string; +``` diff --git a/docs/src/js/interfaces/DescribeNamespaceResponse.md b/docs/src/js/interfaces/DescribeNamespaceResponse.md new file mode 100644 index 000000000..9f2daa78e --- /dev/null +++ b/docs/src/js/interfaces/DescribeNamespaceResponse.md @@ -0,0 +1,15 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / DescribeNamespaceResponse + +# Interface: DescribeNamespaceResponse + +## Properties + +### properties? + +```ts +optional properties: Record; +``` diff --git a/docs/src/js/interfaces/DropNamespaceOptions.md b/docs/src/js/interfaces/DropNamespaceOptions.md new file mode 100644 index 000000000..1863bb0cd --- /dev/null +++ b/docs/src/js/interfaces/DropNamespaceOptions.md @@ -0,0 +1,27 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / DropNamespaceOptions + +# Interface: DropNamespaceOptions + +## Properties + +### behavior? + +```ts +optional behavior: "restrict" | "cascade"; +``` + +Refuse to drop if non-empty (restrict) or drop recursively (cascade). + +*** + +### mode? + +```ts +optional mode: "fail" | "skip"; +``` + +Whether to skip if the namespace doesn't exist, or fail. diff --git a/docs/src/js/interfaces/DropNamespaceResponse.md b/docs/src/js/interfaces/DropNamespaceResponse.md new file mode 100644 index 000000000..b03408647 --- /dev/null +++ b/docs/src/js/interfaces/DropNamespaceResponse.md @@ -0,0 +1,23 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / DropNamespaceResponse + +# Interface: DropNamespaceResponse + +## Properties + +### properties? + +```ts +optional properties: Record; +``` + +*** + +### transactionId? + +```ts +optional transactionId: string[]; +``` diff --git a/docs/src/js/interfaces/ListNamespacesOptions.md b/docs/src/js/interfaces/ListNamespacesOptions.md new file mode 100644 index 000000000..79680417f --- /dev/null +++ b/docs/src/js/interfaces/ListNamespacesOptions.md @@ -0,0 +1,27 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / ListNamespacesOptions + +# Interface: ListNamespacesOptions + +## Properties + +### limit? + +```ts +optional limit: number; +``` + +An optional limit to the number of results to return. + +*** + +### pageToken? + +```ts +optional pageToken: string; +``` + +Token from a previous response for pagination. diff --git a/docs/src/js/interfaces/ListNamespacesResponse.md b/docs/src/js/interfaces/ListNamespacesResponse.md new file mode 100644 index 000000000..a99ab480f --- /dev/null +++ b/docs/src/js/interfaces/ListNamespacesResponse.md @@ -0,0 +1,23 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / ListNamespacesResponse + +# Interface: ListNamespacesResponse + +## Properties + +### namespaces + +```ts +namespaces: string[]; +``` + +*** + +### pageToken? + +```ts +optional pageToken: string; +``` diff --git a/nodejs/Cargo.toml b/nodejs/Cargo.toml index c73121607..b9c3c8727 100644 --- a/nodejs/Cargo.toml +++ b/nodejs/Cargo.toml @@ -22,6 +22,7 @@ arrow-schema.workspace = true env_logger.workspace = true futures.workspace = true lancedb = { path = "../rust/lancedb", default-features = false } +lance-namespace.workspace = true napi = { version = "3.8.3", default-features = false, features = [ "napi9", "async" diff --git a/nodejs/__test__/connection.test.ts b/nodejs/__test__/connection.test.ts index b8e5def8a..f03796e9b 100644 --- a/nodejs/__test__/connection.test.ts +++ b/nodejs/__test__/connection.test.ts @@ -306,3 +306,94 @@ describe("clone table functionality", () => { ).rejects.toThrow("Deep clone is not yet implemented"); }); }); + +describe("namespaces", () => { + let tmpDir: tmp.DirResult; + let db: Connection; + + beforeEach(async () => { + tmpDir = tmp.dirSync({ unsafeCleanup: true }); + // The local DirectoryNamespace backend only supports child namespaces + // when manifest mode is enabled (see lance-namespace-impls/src/dir.rs). + db = await connect(tmpDir.name, { + // biome-ignore lint/style/useNamingConvention: opaque backend property key, must match Rust + namespaceClientProperties: { manifest_enabled: "true" }, + }); + }); + afterEach(() => tmpDir.removeCallback()); + + it("should create and describe a namespace", async () => { + await db.createNamespace(["myns"]); + const desc = await db.describeNamespace(["myns"]); + expect(desc).toBeDefined(); + }); + + it("should list namespaces created at the root", async () => { + await db.createNamespace(["alpha"]); + await db.createNamespace(["beta"]); + const list = await db.listNamespaces(); + expect(list.namespaces).toEqual(expect.arrayContaining(["alpha", "beta"])); + }); + + it("should list child namespaces under a parent", async () => { + await db.createNamespace(["parent"]); + await db.createNamespace(["parent", "child"]); + const list = await db.listNamespaces(["parent"]); + expect(list.namespaces).toContain("child"); + }); + + it("should drop a namespace", async () => { + await db.createNamespace(["ephemeral"]); + await db.dropNamespace(["ephemeral"]); + const list = await db.listNamespaces(); + expect(list.namespaces).not.toContain("ephemeral"); + }); + + it("should raise an error on any namespace op after close", async () => { + await db.close(); + await expect(db.describeNamespace(["foo"])).rejects.toThrow( + "Connection is closed", + ); + await expect(db.listNamespaces()).rejects.toThrow("Connection is closed"); + await expect(db.createNamespace(["foo"])).rejects.toThrow( + "Connection is closed", + ); + await expect(db.dropNamespace(["foo"])).rejects.toThrow( + "Connection is closed", + ); + }); + + it("should raise an understandable error when describing a non-existent namespace", async () => { + await expect(db.describeNamespace(["does-not-exist"])).rejects.toThrow( + /not found/i, + ); + }); + + it("should raise an error when creating a namespace that already exists", async () => { + await db.createNamespace(["dup"]); + await expect(db.createNamespace(["dup"])).rejects.toThrow(); + }); + + it("should reject an unrecognized createNamespace mode with a clear error", async () => { + await expect( + // biome-ignore lint/suspicious/noExplicitAny: deliberately bypass TS to test runtime validation + db.createNamespace(["x"], { mode: "frobnicate" as any }), + ).rejects.toThrow(/Invalid mode 'frobnicate'/); + }); + + it("should reject an unrecognized dropNamespace mode with a clear error", async () => { + await db.createNamespace(["x"]); + await expect( + // biome-ignore lint/suspicious/noExplicitAny: deliberately bypass TS to test runtime validation + db.dropNamespace(["x"], { mode: "frobnicate" as any }), + ).rejects.toThrow(/Invalid mode 'frobnicate'/); + }); + + it("should reject an unrecognized dropNamespace behavior with a clear error", async () => { + await db.createNamespace(["x"]); + await expect( + // biome-ignore lint/suspicious/noExplicitAny: deliberately bypass TS to test runtime validation + db.dropNamespace(["x"], { behavior: "frobnicate" as any }), + ).rejects.toThrow(/Invalid behavior 'frobnicate'/); + }); +}); diff --git a/nodejs/lancedb/connection.ts b/nodejs/lancedb/connection.ts index 397edd3a0..45513f77c 100644 --- a/nodejs/lancedb/connection.ts +++ b/nodejs/lancedb/connection.ts @@ -16,6 +16,18 @@ import { } from "./arrow"; import { EmbeddingFunctionConfig, getRegistry } from "./embedding/registry"; import { Connection as LanceDbConnection } from "./native"; +import type { + CreateNamespaceResponse, + DescribeNamespaceResponse, + DropNamespaceResponse, + ListNamespacesResponse, +} from "./native"; +export type { + CreateNamespaceResponse, + DescribeNamespaceResponse, + DropNamespaceResponse, + ListNamespacesResponse, +}; import { sanitizeTable } from "./sanitize"; import { LocalTable, Table } from "./table"; @@ -110,6 +122,28 @@ export interface TableNamesOptions { /** An optional limit to the number of results to return. */ limit?: number; } + +export interface ListNamespacesOptions { + /** Token from a previous response for pagination. */ + pageToken?: string; + /** An optional limit to the number of results to return. */ + limit?: number; +} + +export interface CreateNamespaceOptions { + /** Creation mode. */ + mode?: "create" | "exist_ok" | "overwrite"; + /** Properties to set on the new namespace. */ + properties?: Record; +} + +export interface DropNamespaceOptions { + /** Whether to skip if the namespace doesn't exist, or fail. */ + mode?: "skip" | "fail"; + /** Refuse to drop if non-empty (restrict) or drop recursively (cascade). */ + behavior?: "restrict" | "cascade"; +} + /** * A LanceDB Connection that allows you to open tables and create new ones. * @@ -268,6 +302,69 @@ export abstract class Connection { */ abstract dropAllTables(namespacePath?: string[]): Promise; + /** + * Describe a namespace, returning its properties. + * + * @param {string[]} namespacePath - The namespace path to describe, in + * parent → child order, e.g. `["analytics", "sales"]`. + * @returns {Promise} The namespace's properties + * (may be undefined if the namespace has none). + */ + abstract describeNamespace( + namespacePath: string[], + ): Promise; + + /** + * List the immediate child namespaces under the given parent. + * + * Results may be paginated. To retrieve subsequent pages, pass the + * `pageToken` returned by a previous call. + * + * @param {string[]} namespacePath - The parent namespace path. Defaults + * to the root namespace if omitted. + * @param {Partial} options - Pagination options + * (`pageToken`, `limit`). + * @returns {Promise} Child namespace names and + * an optional token for fetching the next page. + */ + abstract listNamespaces( + namespacePath?: string[], + options?: Partial, + ): Promise; + + /** + * Create a new namespace at the given path. + * + * @param {string[]} namespacePath - The namespace path to create. + * @param {Partial} options - Creation `mode` + * ("create" | "exist_ok" | "overwrite") and optional `properties` + * to attach to the namespace. + * @returns {Promise} The properties of the + * created namespace and an optional transaction id. + */ + abstract createNamespace( + namespacePath: string[], + options?: Partial, + ): Promise; + + /** + * Drop a namespace. + * + * Use `behavior: "cascade"` to also drop everything contained in the + * namespace (sub-namespaces and tables). The default `"restrict"` + * behavior refuses to drop a non-empty namespace. + * + * @param {string[]} namespacePath - The namespace path to drop. + * @param {Partial} options - `mode` ("skip" | "fail" + * for missing-namespace handling) and `behavior` ("restrict" | "cascade"). + * @returns {Promise} Any properties returned by + * the server and an optional transaction id. + */ + abstract dropNamespace( + namespacePath: string[], + options?: Partial, + ): Promise; + /** * Clone a table from a source table. * @@ -515,6 +612,45 @@ export class LocalConnection extends Connection { async dropAllTables(namespacePath?: string[]): Promise { return this.inner.dropAllTables(namespacePath ?? []); } + + describeNamespace( + namespacePath: string[], + ): Promise { + return this.inner.describeNamespace(namespacePath); + } + + listNamespaces( + namespacePath?: string[], + options?: Partial, + ): Promise { + return this.inner.listNamespaces( + namespacePath ?? [], + options?.pageToken, + options?.limit, + ); + } + + createNamespace( + namespacePath: string[], + options?: Partial, + ): Promise { + return this.inner.createNamespace( + namespacePath, + options?.mode, + options?.properties, + ); + } + + dropNamespace( + namespacePath: string[], + options?: Partial, + ): Promise { + return this.inner.dropNamespace( + namespacePath, + options?.mode, + options?.behavior, + ); + } } /** diff --git a/nodejs/lancedb/index.ts b/nodejs/lancedb/index.ts index 648af58ef..b56055383 100644 --- a/nodejs/lancedb/index.ts +++ b/nodejs/lancedb/index.ts @@ -62,6 +62,13 @@ export { CreateTableOptions, TableNamesOptions, OpenTableOptions, + ListNamespacesOptions, + CreateNamespaceOptions, + DropNamespaceOptions, + ListNamespacesResponse, + CreateNamespaceResponse, + DropNamespaceResponse, + DescribeNamespaceResponse, } from "./connection"; export { Session } from "./native.js"; diff --git a/nodejs/src/connection.rs b/nodejs/src/connection.rs index 09be9465f..058b74f96 100644 --- a/nodejs/src/connection.rs +++ b/nodejs/src/connection.rs @@ -14,6 +14,9 @@ use crate::header::JsHeaderProvider; use crate::table::Table; use lancedb::connection::{ConnectBuilder, Connection as LanceDBConnection}; +use lance_namespace::models::{ + CreateNamespaceRequest, DescribeNamespaceRequest, DropNamespaceRequest, ListNamespacesRequest, +}; use lancedb::ipc::{ipc_file_to_batches, ipc_file_to_schema}; #[napi] @@ -21,6 +24,29 @@ pub struct Connection { inner: Option, } +#[napi(object)] +pub struct DescribeNamespaceResponse { + pub properties: Option>, +} + +#[napi(object)] +pub struct ListNamespacesResponse { + pub namespaces: Vec, + pub page_token: Option, +} + +#[napi(object)] +pub struct CreateNamespaceResponse { + pub properties: Option>, + pub transaction_id: Option, +} + +#[napi(object)] +pub struct DropNamespaceResponse { + pub properties: Option>, + pub transaction_id: Option>, +} + impl Connection { pub(crate) fn inner_new(inner: LanceDBConnection) -> Self { Self { inner: Some(inner) } @@ -273,4 +299,130 @@ impl Connection { let ns = namespace_path.unwrap_or_default(); self.get_inner()?.drop_all_tables(&ns).await.default_error() } + + #[napi(catch_unwind)] + /// Describe a namespace and return its properties. + pub async fn describe_namespace( + &self, + namespace_path: Vec, + ) -> napi::Result { + let req = DescribeNamespaceRequest { + id: Some(namespace_path), + ..Default::default() + }; + let resp = self + .get_inner()? + .describe_namespace(req) + .await + .default_error()?; + Ok(DescribeNamespaceResponse { + properties: resp.properties, + }) + } + + #[napi(catch_unwind)] + /// List child namespaces under the given namespace path + pub async fn list_namespaces( + &self, + namespace_path: Option>, + page_token: Option, + limit: Option, + ) -> napi::Result { + let req = ListNamespacesRequest { + id: namespace_path, + page_token, + limit: limit.map(|l| l as i32), + ..Default::default() + }; + let resp = self + .get_inner()? + .list_namespaces(req) + .await + .default_error()?; + Ok(ListNamespacesResponse { + namespaces: resp.namespaces, + page_token: resp.page_token, + }) + } + + #[napi(catch_unwind)] + /// Create a new namespace with optional properties. + pub async fn create_namespace( + &self, + namespace_path: Vec, + mode: Option, + properties: Option>, + ) -> napi::Result { + let mode_str = mode + .map(|m| match m.to_lowercase().as_str() { + "create" => Ok("Create".to_string()), + "exist_ok" => Ok("ExistOk".to_string()), + "overwrite" => Ok("Overwrite".to_string()), + _ => Err(napi::Error::from_reason(format!( + "Invalid mode '{}': expected one of 'create', 'exist_ok', 'overwrite'", + m + ))), + }) + .transpose()?; + let req = CreateNamespaceRequest { + id: Some(namespace_path), + mode: mode_str, + properties, + ..Default::default() + }; + let resp = self + .get_inner()? + .create_namespace(req) + .await + .default_error()?; + Ok(CreateNamespaceResponse { + properties: resp.properties, + transaction_id: resp.transaction_id, + }) + } + + #[napi(catch_unwind)] + /// Drop a namespace. + pub async fn drop_namespace( + &self, + namespace_path: Vec, + mode: Option, + behavior: Option, + ) -> napi::Result { + let mode_str = mode + .map(|m| match m.to_lowercase().as_str() { + "skip" => Ok("Skip".to_string()), + "fail" => Ok("Fail".to_string()), + _ => Err(napi::Error::from_reason(format!( + "Invalid mode '{}': expected one of 'skip', 'fail'", + m + ))), + }) + .transpose()?; + let behavior_str = behavior + .map(|b| match b.to_lowercase().as_str() { + "restrict" => Ok("Restrict".to_string()), + "cascade" => Ok("Cascade".to_string()), + _ => Err(napi::Error::from_reason(format!( + "Invalid behavior '{}': expected one of 'restrict', 'cascade'", + b + ))), + }) + .transpose()?; + let req = DropNamespaceRequest { + id: Some(namespace_path), + mode: mode_str, + behavior: behavior_str, + ..Default::default() + }; + let resp = self + .get_inner()? + .drop_namespace(req) + .await + .default_error()?; + Ok(DropNamespaceResponse { + properties: resp.properties, + transaction_id: resp.transaction_id, + }) + } }