LanceDB Node.js 表枚举指南:从弃用的 TableNamesOptions 迁移到 listTables 分页
【免费下载链接】lancedbDeveloper-friendly OSS embedded retrieval library for multimodal AI. Search More; Manage Less.项目地址: https://gitcode.com/gh_mirrors/la/lancedb
导读
TableNamesOptions是 LanceDB Node.js SDK 中用于控制"列出数据库中所有表"行为的选项接口。随着 SDK 演进,该接口已被标记为 Deprecated(弃用),取而代之的是基于不透明pageToken的新分页方案ListTablesOptions+Connection.listTables。本文以该接口为切入点,完整讲解旧接口的字段语义与分页用法、迁移到新 API 的具体步骤、底层 Rust 实现原理以及测试用例验证,帮助你写出健壮、可维护的表枚举代码。
TableNamesOptions 是什么
在 nodejs/lancedb/connection.ts 中,TableNamesOptions被定义为Connection.tableNames()方法的可选参数类型:
/** * @deprecated Use {@link ListTablesOptions} with {@link Connection.listTables} * instead. */ export interface TableNamesOptions { /** * If present, only return names that come lexicographically after the * supplied value. * * This can be combined with limit to implement pagination by setting this to * the last table name from the previous page. */ startAfter?: string; /** An optional limit to the number of results to return. */ limit?: number; }它包含两个可选字段,均与分页/裁剪相关:
| 字段 | 类型 | 语义 |
|---|---|---|
startAfter | string | 仅返回按字典序(lexicographical order)排列在该值之后的表名。与limit组合可实现分页:将上一页的最后一个表名作为下一页的startAfter |
limit | number | 可选的结果数量上限 |
Connection.tableNames()的签名同样标注了@deprecated(见 nodejs/lancedb/connection.ts),并明确说明:"Tables will be returned in lexicographical order"(表按字典序返回)。同时支持两种调用形式:
tableNames(options?)—— 向后兼容的旧调用方式;tableNames(namespacePath?, options?)—— 传入命名空间路径后再传分页选项。
在实现层(nodejs/lancedb/connection.ts),tableNames会先判断第一个参数是string[](命名空间路径)还是普通对象(选项),再把startAfter与limit透传给底层inner.tableNames。
旧接口的典型用法与分页模式
TableNamesOptions的经典使用场景是"取前 N 张表"与"按字典序游标翻页"。仓库测试 nodejs/test/connection.test.ts 给出了最直接的可运行示例:
const db = await connect(tmpDir.name); await db.createTable("b", [{ id: 1 }]); await db.createTable("a", [{ id: 1 }]); await db.createTable("c", [{ id: 1 }]); // 不带选项:按字典序返回全部表名 let tables = await db.tableNames(); expect(tables).toEqual(["a", "b", "c"]); // limit: 只返回前 1 张表 tables = await db.tableNames({ limit: 1 }); expect(tables).toEqual(["a"]); // limit + startAfter: 从 "a" 之后取 1 张表 tables = await db.tableNames({ limit: 1, startAfter: "a" }); expect(tables).toEqual(["b"]); // 仅 startAfter: 取 "a" 之后的全部表 tables = await db.tableNames({ startAfter: "a" }); expect(tables).toEqual(["b", "c"]);由此可以总结出基于TableNamesOptions的手工分页套路:每页读取limit条,记录本页最后一个表名,作为下一页的startAfter,直到返回结果少于limit(说明已到末尾)。
底层实现:startAfter 与 limit 如何生效
TableNamesOptions的两个字段最终会在 Rust 的 napi 绑定层被翻译为查询操作。见 nodejs/src/connection.rs:
#[napi(catch_unwind)] pub async fn table_names( &self, namespace_path: Option<Vec<String>>, start_after: Option<String>, limit: Option<u32>, ) -> napi::Result<Vec<String>> { let mut op = self.get_inner()?.table_names(); op = op.namespace(namespace_path.unwrap_or_default()); if let Some(start_after) = start_after { op = op.start_after(start_after); } if let Some(limit) = limit { op = op.limit(limit); } op.execute().await.default_error() }即:startAfter映射为 Rust 侧的start_after,limit映射为limit,随后交由底层ListingDatabase执行。在 rust/lancedb/src/database/listing.rs 中可以看到字典序过滤与截断的具体逻辑——先按名称排序,再跳过所有<= start_after的条目,最后按limit截断:
if let Some(start_after) = request.start_after { // 定位第一个字典序大于 start_after 的位置 let position = f.iter().position(|name| name.as_str() > start_after.as_str()); // ... } if let Some(limit) = request.limit { f.truncate(limit as usize); }这也解释了为什么startAfter被称为"游标":它本质上把"上一页最后一条的字符串值"当作一个字典序位置标记,从而保证翻页时表名不会重复或遗漏(前提是列表内容不发生变更)。
为什么弃用:TableNamesOptions 的缺陷
TableNamesOptions被标记为 Deprecated 并非因为它"不能工作",而是因为它的分页模型存在两个结构性弱点:
- 游标泄漏内部细节:
startAfter要求调用方理解"字典序"这一底层排序约定,并自行维护"最后一页最后一条表名"的状态。表名的具体形态、排序规则一旦变化,分页逻辑就会出错。 - 无法表达"继续翻页"的通用语义:对命名空间(namespace)数据库而言,翻页需要的不只是名称游标,而是一个能由服务端任意编码的续传标记。用表名当游标过于脆弱。
因此 SDK 引入了全新的ListTablesOptions+Connection.listTables()方案(见 nodejs/lancedb/connection.ts):
export interface ListTablesOptions { /** * Token from a previous response, to resume listing where it left off. * * 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 keep * going while the response carries a page token rather than while pages are * full. */ limit?: number; }对应的响应类型为 ListTablesResponse,包含tables: string[]与可选的pageToken: string。
新旧两代选项的对照关系:
| 能力 | TableNamesOptions(已弃用) | ListTablesOptions(推荐) |
|---|---|---|
| 结果上限 | limit?: number | limit?: number |
| 续传方式 | startAfter?: string(字典序游标) | pageToken?: string(不透明令牌) |
| 翻页终止判定 | 返回条数< limit | 响应中pageToken为undefined |
| 返回值 | Promise<string[]> | Promise<ListTablesResponse>(含tables与pageToken) |
官方文档对TableNamesOptions的弃用说明位于 docs/src/js/interfaces/TableNamesOptions.md,指向的替代方案文档为 ListTablesOptions。
迁移指南:改用 listTables 与 pageToken
Connection.listTables的新签名(见 nodejs/lancedb/connection.ts)与tableNames保持了一致的重载结构,支持listTables(options?)与listTables(namespacePath?, options?)两种形式。
新版分页的核心要点(源码 JSDoc 明确强调):
"A page can be shorter than
limitwithout being the last one, so walk until a response carries no page token" —— 页面可能少于limit却并非最后一页,因此必须以pageToken是否存在来判断是否翻页结束,而不是看返回条数是否够limit。
官方推荐的翻页循环写法:
const names = []; let pageToken = undefined; do { const page = await conn.listTables({ pageToken, limit: 100 }); names.push(...page.tables); pageToken = page.pageToken; } while (pageToken);逐步迁移对照
迁移前(旧 API):
let startAfter: string | undefined; for (;;) { const page = await conn.tableNames({ limit: 100, startAfter }); names.push(...page); if (page.length < 100) break; startAfter = page[page.length - 1]; }迁移后(新 API):
let pageToken: string | undefined; do { const page = await conn.listTables({ limit: 100, pageToken }); names.push(...page.tables); pageToken = page.pageToken; // undefined 即表示没有更多数据 } while (pageToken);测试用例验证
仓库测试 nodejs/test/connection.test.ts 对listTables的分页行为做了完整验证:
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"]);以及"翻遍每一页且每张表恰好出现一次"的断言:
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).toEqual(["a", "b", "c", "d", "e"]);在 Rust 侧,rust/lancedb/src/database/listing.rs 明确区分了两种游标:注释指出 "The page_token is opaque, unlike thestart_afterparameter ofSelf::table_names()",并说明当没有更多结果时返回的page_token为None;limit是响应中表数量的上限,但响应可能少于limit且仍有后续数据,客户端应通过pageToken判断是否继续。同文件还包含一组针对分页边界的单元测试(如 listing.rs 中的test_list_tables_pages_over_every_table_once、test_the_page_token_is_not_a_table_name、test_a_limit_the_listing_does_not_fill_leaves_no_token_behind),进一步佐证了上述语义。
使用建议与注意事项
- 新代码一律使用
listTables:tableNames与TableNamesOptions已标注@deprecated,虽然出于向后兼容仍可用,但不应在新代码中引入。 - 以
pageToken是否为空判断翻页终点:旧接口中"返回条数< limit即结束"的判断在新接口下不成立——一页可能未填满但后面仍有数据,务必按照"响应无令牌才停止"的模式编写循环。 - 不要把
pageToken当表名使用:令牌是不透明的(opaque),"callers should not construct or interpret one",不要尝试解析、拼接或持久化复用跨会话的令牌。 - 命名空间支持:
listTables(namespacePath, options)与tableNames(namespacePath, options)均支持传入命名空间路径;对于命名空间数据库,listTables的底层请求会显式携带id(根命名空间为空数组而非缺省,见 nodejs/src/connection.rs 的注释),测试 nodejs/test/connection.test.ts 演示了在子命名空间下列表的行为。 limit为 0 的边界:从 Rust 实现看,limit == Some(0)会直接返回空页且无令牌(rust/lancedb/src/database/listing.rs),调用时传入合理的正整数即可避免歧义。
小结
TableNamesOptions是 LanceDB Node.js SDK 早期基于"字典序游标"的表枚举方案,其startAfter+limit组合在小规模、纯本地场景下足够直观;但随着命名空间数据库与远端数据库的引入,这种把底层排序细节暴露给调用方的分页模型不再适用。官方以ListTablesOptions+Connection.listTables()取代之,用不透明的pageToken统一了分页语义。理解这一迁移脉络,不仅能让你写出符合当前 SDK 规范的枚举代码,也能更准确地把握 LanceDB 在"列表 / 分页"这类基础能力上的设计取舍。
【免费下载链接】lancedbDeveloper-friendly OSS embedded retrieval library for multimodal AI. Search More; Manage Less.项目地址: https://gitcode.com/gh_mirrors/la/lancedb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考