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, + }) + } }