feat: builder API for list_tables, deprecate table_names

`Connection::list_tables` took a `lance_namespace::models::ListTablesRequest`
directly, so its generated shape -- including `identity`, `context` and
`include_declared`, none of which lancedb reads -- was part of the public API,
and Node had no binding at all.

Replaces it with a `ListTablesBuilder` carrying `page_token`, `limit` and
`namespace`, matching every other operation on `Connection`. This is a breaking
change for Rust callers. Node gains `listTables` with `ListTablesOptions` and
`ListTablesResponse`; Python's public API is unchanged, since it already had
`list_tables` everywhere.

`table_names` and `TableNamesBuilder` are deprecated. Its `start_after` takes a
table name rather than an opaque token, which cannot be pushed down into a store
that resumes from a continuation token.

Also fixes the page boundary in `ListingDatabase::list_tables`: the token was the
first name of the next page while resuming skips names at or before the token, so
one table was dropped per boundary. Walking `[a, b, c, d, e]` with a limit of 2
returned `[a, b, d, e]`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Will Jones
2026-08-06 14:43:48 -07:00
parent 4148dfef72
commit 85fe831bcb
16 changed files with 506 additions and 38 deletions
+57 -1
View File
@@ -4,7 +4,13 @@
import { readdirSync } from "fs";
import { Field, Float64, Schema } from "apache-arrow";
import * as tmp from "tmp";
import { Connection, Table, connect, connectNamespace } from "../lancedb";
import {
Connection,
ListTablesResponse,
Table,
connect,
connectNamespace,
} from "../lancedb";
import { LocalTable } from "../lancedb/table";
describe("when connecting", () => {
@@ -129,6 +135,56 @@ describe("given a connection", () => {
expect(tables).toEqual(["b", "c"]);
});
it("should list tables with a page token", async () => {
const db = await connect(tmpDir.name);
await db.createTable("b", [{ id: 1 }]);
await db.createTable("a", [{ id: 1 }]);
await db.createTable("c", [{ id: 1 }]);
const all = await db.listTables();
expect(all.tables).toEqual(["a", "b", "c"]);
expect(all.pageToken).toBeUndefined();
const first = await db.listTables({ limit: 1 });
expect(first.tables).toEqual(["a"]);
expect(first.pageToken).toBeDefined();
const second = await db.listTables({
limit: 1,
pageToken: first.pageToken,
});
expect(second.tables).toEqual(["b"]);
});
it("should visit every table exactly once when paging", async () => {
const db = await connect(tmpDir.name);
const created = ["a", "b", "c", "d", "e"];
for (const name of created) {
await db.createTable(name, [{ id: 1 }]);
}
const seen: string[] = [];
let pageToken: string | undefined = undefined;
do {
const page: ListTablesResponse = await db.listTables({
limit: 2,
pageToken,
});
seen.push(...page.tables);
pageToken = page.pageToken;
} while (pageToken);
expect(seen.sort()).toEqual(created);
});
it("should reject listTables on a closed connection", async () => {
const db = await connect(tmpDir.name);
db.close();
await expect(db.listTables()).rejects.toThrow("Connection is closed");
});
it("should create tables in v2 mode", async () => {
const db = await connect(tmpDir.name);
const data = [...Array(10000).keys()].map((i) => ({ id: i }));
+89
View File
@@ -25,12 +25,14 @@ import type {
JobDescription,
JobInfo,
ListNamespacesResponse,
ListTablesResponse,
} from "./native";
export type {
CreateNamespaceResponse,
DescribeNamespaceResponse,
DropNamespaceResponse,
ListNamespacesResponse,
ListTablesResponse,
};
import { sanitizeTable } from "./sanitize";
import { LocalTable, Table } from "./table";
@@ -128,6 +130,10 @@ export interface OpenTableOptions {
indexCacheSize?: number;
}
/**
* @deprecated Use {@link ListTablesOptions} with {@link Connection.listTables}
* instead.
*/
export interface TableNamesOptions {
/**
* If present, only return names that come lexicographically after the
@@ -141,6 +147,23 @@ export interface TableNamesOptions {
limit?: number;
}
export interface ListTablesOptions {
/**
* Token from a previous response for pagination.
*
* The token is opaque: it carries whatever the database needs to resume, and
* callers should not construct or interpret one.
*/
pageToken?: string;
/**
* An upper bound on how many tables to return.
*
* A page may hold fewer than this and still not be the last one, so continue
* while the response carries a page token rather than while pages are full.
*/
limit?: number;
}
export interface ListNamespacesOptions {
/** Token from a previous response for pagination. */
pageToken?: string;
@@ -225,6 +248,7 @@ export abstract class Connection {
* @param {Partial<TableNamesOptions>} options - options to control the
* paging / start point (backwards compatibility)
*
* @deprecated Use {@link Connection.listTables} instead.
*/
abstract tableNames(options?: Partial<TableNamesOptions>): Promise<string[]>;
/**
@@ -235,12 +259,54 @@ export abstract class Connection {
* @param {Partial<TableNamesOptions>} options - options to control the
* paging / start point
*
* @deprecated Use {@link Connection.listTables} instead.
*/
abstract tableNames(
namespacePath?: string[],
options?: Partial<TableNamesOptions>,
): Promise<string[]>;
/**
* List a page of tables in this database.
*
* Results may be paginated. To retrieve subsequent pages, pass the
* `pageToken` returned by a previous call. A page may be shorter than
* `limit` without being the last one, so walk until the response carries no
* page token:
*
* ```ts
* const names = [];
* let pageToken = undefined;
* do {
* const page = await conn.listTables({ pageToken, limit: 100 });
* names.push(...page.tables);
* pageToken = page.pageToken;
* } while (pageToken);
* ```
*
* @param {Partial<ListTablesOptions>} options - Pagination options
* (`pageToken`, `limit`).
* @returns {Promise<ListTablesResponse>} Table names and an optional token
* for fetching the next page.
*/
abstract listTables(
options?: Partial<ListTablesOptions>,
): Promise<ListTablesResponse>;
/**
* List a page of tables in this database.
*
* @param {string[]} namespacePath - The namespace path to list tables from
* (defaults to root namespace)
* @param {Partial<ListTablesOptions>} options - Pagination options
* (`pageToken`, `limit`).
* @returns {Promise<ListTablesResponse>} Table names and an optional token
* for fetching the next page.
*/
abstract listTables(
namespacePath?: string[],
options?: Partial<ListTablesOptions>,
): Promise<ListTablesResponse>;
/**
* Open a table in the database.
* @param {string} name - The name of the table
@@ -531,6 +597,29 @@ export class LocalConnection extends Connection {
);
}
async listTables(
namespacePathOrOptions?: string[] | Partial<ListTablesOptions>,
options?: Partial<ListTablesOptions>,
): Promise<ListTablesResponse> {
// Detect if first argument is namespacePath array or options object
let namespacePath: string[] | undefined;
let listTablesOptions: Partial<ListTablesOptions> | undefined;
if (Array.isArray(namespacePathOrOptions)) {
namespacePath = namespacePathOrOptions;
listTablesOptions = options;
} else {
namespacePath = undefined;
listTablesOptions = namespacePathOrOptions;
}
return this.inner.listTables(
namespacePath ?? [],
listTablesOptions?.pageToken,
listTablesOptions?.limit,
);
}
async openTable(
name: string,
namespacePath?: string[],
+2
View File
@@ -74,11 +74,13 @@ export {
Connection,
CreateTableOptions,
TableNamesOptions,
ListTablesOptions,
OpenTableOptions,
ListNamespacesOptions,
CreateNamespaceOptions,
DropNamespaceOptions,
ListNamespacesResponse,
ListTablesResponse,
CreateNamespaceResponse,
DropNamespaceResponse,
DescribeNamespaceResponse,
+31
View File
@@ -36,6 +36,12 @@ pub struct ListNamespacesResponse {
pub page_token: Option<String>,
}
#[napi(object)]
pub struct ListTablesResponse {
pub tables: Vec<String>,
pub page_token: Option<String>,
}
#[napi(object)]
pub struct CreateNamespaceResponse {
pub properties: Option<HashMap<String, String>>,
@@ -189,6 +195,8 @@ impl Connection {
/// List all tables in the dataset.
#[napi(catch_unwind)]
// Deprecated in favour of `list_tables`, but still exposed to JavaScript.
#[allow(deprecated)]
pub async fn table_names(
&self,
namespace_path: Option<Vec<String>>,
@@ -206,6 +214,29 @@ impl Connection {
op.execute().await.default_error()
}
/// List a page of tables in the database.
#[napi(catch_unwind)]
pub async fn list_tables(
&self,
namespace_path: Option<Vec<String>>,
page_token: Option<String>,
limit: Option<u32>,
) -> napi::Result<ListTablesResponse> {
let mut op = self.get_inner()?.list_tables();
op = op.namespace(namespace_path.unwrap_or_default());
if let Some(page_token) = page_token {
op = op.page_token(page_token);
}
if let Some(limit) = limit {
op = op.limit(limit);
}
let resp = op.execute().await.default_error()?;
Ok(ListTablesResponse {
tables: resp.tables,
page_token: resp.page_token,
})
}
/// Create table from a Apache Arrow IPC (file) buffer.
///
/// Parameters: