diff --git a/docs/src/js/classes/Connection.md b/docs/src/js/classes/Connection.md index bb990ec62..ad24644b6 100644 --- a/docs/src/js/classes/Connection.md +++ b/docs/src/js/classes/Connection.md @@ -441,18 +441,28 @@ Open a table in the database. ```ts abstract renameTable( - oldName, + currentName, newName, - namespacePath?): Promise + options?): Promise ``` +Rename a table. + +Currently only supported by LanceDB Cloud. Local OSS connections and +namespace-backed connections (via [connectNamespace](../functions/connectNamespace.md)) reject with +a "not supported" error. + #### Parameters -* **oldName**: `string` +* **currentName**: `string` + The current name of the table. * **newName**: `string` + The new name for the table. -* **namespacePath?**: `string`[] +* **options?**: [`RenameTableOptions`](../interfaces/RenameTableOptions.md) + Optional namespace paths. When + `newNamespacePath` is omitted the table stays in `namespacePath`. #### Returns diff --git a/docs/src/js/globals.md b/docs/src/js/globals.md index 09afacec5..2e7254020 100644 --- a/docs/src/js/globals.md +++ b/docs/src/js/globals.md @@ -87,6 +87,7 @@ - [OptimizeStats](interfaces/OptimizeStats.md) - [QueryExecutionOptions](interfaces/QueryExecutionOptions.md) - [RemovalStats](interfaces/RemovalStats.md) +- [RenameTableOptions](interfaces/RenameTableOptions.md) - [RestNamespaceConfig](interfaces/RestNamespaceConfig.md) - [RetryConfig](interfaces/RetryConfig.md) - [ScannableOptions](interfaces/ScannableOptions.md) diff --git a/docs/src/js/interfaces/RenameTableOptions.md b/docs/src/js/interfaces/RenameTableOptions.md new file mode 100644 index 000000000..f3eaaa723 --- /dev/null +++ b/docs/src/js/interfaces/RenameTableOptions.md @@ -0,0 +1,29 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / RenameTableOptions + +# Interface: RenameTableOptions + +## Properties + +### namespacePath? + +```ts +optional namespacePath: string[]; +``` + +The namespace path of the table being renamed. Defaults to the root +namespace (`[]`) when omitted. + +*** + +### newNamespacePath? + +```ts +optional newNamespacePath: string[]; +``` + +The namespace path to move the table to as part of the rename. When +omitted the table stays in `namespacePath`. diff --git a/nodejs/__test__/connection.test.ts b/nodejs/__test__/connection.test.ts index 21cc1c4fd..d195a0ca4 100644 --- a/nodejs/__test__/connection.test.ts +++ b/nodejs/__test__/connection.test.ts @@ -47,6 +47,14 @@ describe("given a connection", () => { await db.close(); expect(db.isOpen()).toBe(false); await expect(db.tableNames()).rejects.toThrow("Connection is closed"); + await expect(db.renameTable("a", "b")).rejects.toThrow( + "Connection is closed", + ); + }); + + it("should report renameTable as unsupported on an OSS connection", async () => { + await db.createTable("a", [{ id: 1 }]); + await expect(db.renameTable("a", "b")).rejects.toThrow(/not supported/); }); it("should be able to create a table from an object arg `createTable(options)`, or args `createTable(name, data, options)`", async () => { let tbl = await db.createTable("test", [{ id: 1 }, { id: 2 }]); @@ -81,16 +89,6 @@ describe("given a connection", () => { await db.createTable("test4", [{ id: 1 }, { id: 2 }]); }); - it("should expose renameTable and reject on OSS listing DB", async () => { - await db.createTable("old_name", [{ id: 1 }]); - - await expect(db.renameTable("old_name", "new_name")).rejects.toThrow( - "rename_table is not supported in LanceDB OSS", - ); - - await expect(db.tableNames()).resolves.toEqual(["old_name"]); - }); - it("should fail if creating table twice, unless overwrite is true", async () => { let tbl = await db.createTable("test", [{ id: 1 }, { id: 2 }]); await expect(tbl.countRows()).resolves.toBe(2); diff --git a/nodejs/__test__/remote.test.ts b/nodejs/__test__/remote.test.ts index ff3826da2..cbddf9673 100644 --- a/nodejs/__test__/remote.test.ts +++ b/nodejs/__test__/remote.test.ts @@ -617,4 +617,68 @@ describe("remote connection", () => { ); }); }); + + describe("renameTable", () => { + async function captureRenameRequest( + call: (db: Connection) => Promise, + ): Promise<{ url: string; body: Record }> { + let captured: { url: string; body: Record } | undefined; + await withMockDatabase((req, res) => { + let raw = ""; + req.on("data", (chunk) => { + raw += chunk; + }); + req.on("end", () => { + captured = { + url: req.url ?? "", + body: raw ? JSON.parse(raw) : {}, + }; + res.writeHead(200, { "Content-Type": "application/json" }).end(""); + }); + }, call); + if (!captured) { + throw new Error("mock server never saw a request"); + } + return captured; + } + + it("sends rename request for a table in the root namespace", async () => { + const { url, body } = await captureRenameRequest(async (db) => { + await db.renameTable("table1", "table2"); + }); + expect(url).toBe("/v1/table/table1/rename/"); + // biome-ignore lint/style/useNamingConvention: snake_case mandated by the server wire format + expect(body).toEqual({ new_table_name: "table2" }); + }); + + it("omits new_namespace when only the current namespace is supplied", async () => { + // Safe-default check: passing namespacePath alone must not send + // `new_namespace`, so the server keeps the table in its current + // namespace instead of silently moving it to root. + const { url, body } = await captureRenameRequest(async (db) => { + await db.renameTable("table1", "table2", { + namespacePath: ["ns1"], + }); + }); + expect(url).toBe("/v1/table/ns1$table1/rename/"); + // biome-ignore lint/style/useNamingConvention: snake_case mandated by the server wire format + expect(body).toEqual({ new_table_name: "table2" }); + }); + + it("includes new_namespace in the body for a cross-namespace rename", async () => { + const { url, body } = await captureRenameRequest(async (db) => { + await db.renameTable("table1", "table2", { + namespacePath: ["ns1"], + newNamespacePath: ["ns2"], + }); + }); + expect(url).toBe("/v1/table/ns1$table1/rename/"); + expect(body).toEqual({ + // biome-ignore lint/style/useNamingConvention: snake_case mandated by the server wire format + new_table_name: "table2", + // biome-ignore lint/style/useNamingConvention: snake_case mandated by the server wire format + new_namespace: ["ns2"], + }); + }); + }); }); diff --git a/nodejs/lancedb/connection.ts b/nodejs/lancedb/connection.ts index a3a84c268..f6eacb20f 100644 --- a/nodejs/lancedb/connection.ts +++ b/nodejs/lancedb/connection.ts @@ -144,6 +144,19 @@ export interface DropNamespaceOptions { behavior?: "restrict" | "cascade"; } +export interface RenameTableOptions { + /** + * The namespace path of the table being renamed. Defaults to the root + * namespace (`[]`) when omitted. + */ + namespacePath?: string[]; + /** + * The namespace path to move the table to as part of the rename. When + * omitted the table stays in `namespacePath`. + */ + newNamespacePath?: string[]; +} + /** * A LanceDB Connection that allows you to open tables and create new ones. * @@ -296,12 +309,6 @@ export abstract class Connection { */ abstract dropTable(name: string, namespacePath?: string[]): Promise; - abstract renameTable( - oldName: string, - newName: string, - namespacePath?: string[], - ): Promise; - /** * Drop all tables in the database. * @param {string[]} namespacePath The namespace path to drop tables from (defaults to root namespace). @@ -397,6 +404,24 @@ export abstract class Connection { isShallow?: boolean; }, ): Promise; + + /** + * Rename a table. + * + * Currently only supported by LanceDB Cloud. Local OSS connections and + * namespace-backed connections (via {@link connectNamespace}) reject with + * a "not supported" error. + * + * @param {string} currentName - The current name of the table. + * @param {string} newName - The new name for the table. + * @param {RenameTableOptions} options - Optional namespace paths. When + * `newNamespacePath` is omitted the table stays in `namespacePath`. + */ + abstract renameTable( + currentName: string, + newName: string, + options?: RenameTableOptions, + ): Promise; } /** @hideconstructor */ @@ -615,14 +640,6 @@ export class LocalConnection extends Connection { return this.inner.dropTable(name, namespacePath ?? []); } - async renameTable( - oldName: string, - newName: string, - namespacePath?: string[], - ): Promise { - return this.inner.renameTable(oldName, newName, namespacePath ?? []); - } - async dropAllTables(namespacePath?: string[]): Promise { return this.inner.dropAllTables(namespacePath ?? []); } @@ -665,6 +682,19 @@ export class LocalConnection extends Connection { options?.behavior, ); } + + async renameTable( + currentName: string, + newName: string, + options?: RenameTableOptions, + ): Promise { + return this.inner.renameTable( + currentName, + newName, + options?.namespacePath ?? [], + options?.newNamespacePath, + ); + } } /** diff --git a/nodejs/lancedb/index.ts b/nodejs/lancedb/index.ts index 1110e427e..56b86ac5f 100644 --- a/nodejs/lancedb/index.ts +++ b/nodejs/lancedb/index.ts @@ -71,6 +71,7 @@ export { CreateNamespaceResponse, DropNamespaceResponse, DescribeNamespaceResponse, + RenameTableOptions, } from "./connection"; export { Session } from "./native.js"; diff --git a/nodejs/src/connection.rs b/nodejs/src/connection.rs index faf42699a..18d6644de 100644 --- a/nodejs/src/connection.rs +++ b/nodejs/src/connection.rs @@ -328,20 +328,6 @@ impl Connection { .default_error() } - #[napi(catch_unwind)] - pub async fn rename_table( - &self, - old_name: String, - new_name: String, - namespace_path: Option>, - ) -> napi::Result<()> { - let ns = namespace_path.unwrap_or_default(); - self.get_inner()? - .rename_table(&old_name, &new_name, &ns, &ns) - .await - .default_error() - } - #[napi(catch_unwind)] pub async fn drop_all_tables(&self, namespace_path: Option>) -> napi::Result<()> { let ns = namespace_path.unwrap_or_default(); @@ -473,4 +459,23 @@ impl Connection { transaction_id: resp.transaction_id, }) } + + /// Rename a table. `current_namespace_path` and `new_namespace_path` default to + /// the root namespace when omitted; the caller is expected to either pass both + /// or pass neither. + #[napi(catch_unwind)] + pub async fn rename_table( + &self, + current_name: String, + new_name: String, + current_namespace_path: Option>, + new_namespace_path: Option>, + ) -> napi::Result<()> { + let cur_ns = current_namespace_path.unwrap_or_default(); + let new_ns = new_namespace_path.unwrap_or_default(); + self.get_inner()? + .rename_table(¤t_name, &new_name, &cur_ns, &new_ns) + .await + .default_error() + } }