refactor!: implement lazy iterators for several APIs (#4459)

Implement lazy iterators for:
- secrets
- views
- jobs
- functions
- materialized views
This commit is contained in:
Colin Patrick McCabe
2026-10-07 10:24:20 -07:00
committed by GitHub
parent 069b2f7d31
commit de005f1d61
32 changed files with 2185 additions and 454 deletions
+39 -11
View File
@@ -17,6 +17,7 @@ import {
} from "./arrow";
import { EmbeddingFunctionConfig, getRegistry } from "./embedding/registry";
import { Job } from "./job";
import { Listing, ListingOptions, validateListingOptions } from "./listing";
import {
MaterializedView,
MaterializedViewSelect,
@@ -347,9 +348,11 @@ export abstract class Connection {
/**
* The names of the materialized views in this database.
*
* Found by reading every table's schema, so this costs an open per table.
* Iterate lazily with `for await...of`. Local listings inspect table schemas
* one page at a time. Options control page size and the starting token.
* See {@link Listing} for pagination state and error behavior.
*/
abstract listMaterializedViews(): Promise<string[]>;
abstract listMaterializedViews(options?: ListingOptions): Listing<string>;
/**
* Drop the materialized view named `name`.
@@ -421,8 +424,13 @@ export abstract class Connection {
* The names of the views in one namespace.
*
* Names only; a definition comes from {@link describeView}.
* Iterate lazily with `for await...of`. See {@link Listing} for pagination
* state and error behavior; options control page size and the starting token.
*/
abstract listViews(namespacePath?: string[]): Promise<string[]>;
abstract listViews(
namespacePath?: string[],
options?: ListingOptions,
): Listing<string>;
abstract openTable(
name: string,
@@ -637,8 +645,12 @@ export abstract class Connection {
*/
abstract openJob(jobId: string): Promise<Job>;
/** List server-side jobs across the database's tables. */
abstract listJobs(): Promise<JobInfo[]>;
/**
* Iterate lazily over server-side jobs across the database's tables.
* Use `for await...of`. Options control page size and the starting token.
* See {@link Listing} for pagination state and error behavior.
*/
abstract listJobs(options?: ListingOptions): Listing<JobInfo>;
/**
* Request cancellation of a server-side job by id.
@@ -741,8 +753,11 @@ export class LocalConnection extends Connection {
return new MaterializedView(new LocalTable(innerTable));
}
async listMaterializedViews(): Promise<string[]> {
return await this.inner.listMaterializedViews();
listMaterializedViews(options: ListingOptions = {}): Listing<string> {
validateListingOptions(options);
return new Listing(
this.inner.listMaterializedViews(options.pageToken, options.pageLimit),
);
}
async dropMaterializedView(
@@ -788,8 +803,18 @@ export class LocalConnection extends Connection {
return new Job(await this.inner.dropViewAsync(name, namespacePath ?? []));
}
async listViews(namespacePath?: string[]): Promise<string[]> {
return this.inner.listViews(namespacePath ?? []);
listViews(
namespacePath?: string[],
options: ListingOptions = {},
): Listing<string> {
validateListingOptions(options);
return new Listing(
this.inner.listViews(
namespacePath ?? [],
options.pageToken,
options.pageLimit,
),
);
}
async listTables(
@@ -1057,8 +1082,11 @@ export class LocalConnection extends Connection {
return new Job(await this.inner.openJob(jobId));
}
async listJobs(): Promise<JobInfo[]> {
return this.inner.listJobs();
listJobs(options: ListingOptions = {}): Listing<JobInfo> {
validateListingOptions(options);
return new Listing(
this.inner.listJobs(options.pageToken, options.pageLimit),
);
}
async cancelJob(jobId: string): Promise<boolean> {
+2
View File
@@ -649,3 +649,5 @@ export {
DatabaseNames,
connectCatalog,
} from "./catalog";
export { Listing, ListingOptions } from "./listing";
+73
View File
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
/** Pagination controls for lazy resource listings. */
export interface ListingOptions {
/** Starting continuation token; omitted starts at the beginning. */
pageToken?: string;
/** Maximum results per request, not a limit on the whole listing. */
pageLimit?: number;
}
/** @internal */
export function validateListingOptions(options: ListingOptions): void {
if (
options.pageLimit !== undefined &&
(!Number.isInteger(options.pageLimit) ||
options.pageLimit <= 0 ||
options.pageLimit > 2147483647)
) {
throw new Error(
"Listing page limit must be an integer between 1 and 2147483647",
);
}
}
/**
* A lazy async iterator over database resources with pagination state.
* Requests occur during iteration; request errors terminate the iterator.
*
* @example
* ```ts
* for await (const name of connection.listViews([], { pageLimit: 20 })) {
* console.log(name);
* }
* ```
*/
export class Listing<T> implements AsyncIterableIterator<T> {
/** @hidden */
constructor(
private readonly inner: {
next(): Promise<T | null | undefined>;
numPageResults(): number;
pageToken(): string | null | undefined;
},
) {}
[Symbol.asyncIterator](): AsyncIterableIterator<T> {
return this;
}
/** Fetch the next item, requesting another page only when needed. */
async next(): Promise<IteratorResult<T>> {
const value = await this.inner.next();
return value == null
? { done: true, value: undefined }
: { done: false, value };
}
/** Number of cached items available without another request. */
numPageResults(): number {
return this.inner.numPageResults();
}
/**
* Token for the next request. Initially this is the supplied starting token.
* Returns undefined after fetching the final page, even with cached items.
* Drain the cache before saving a token to avoid skipping items on resumption.
* Failed requests terminate iteration and retain their token for a new iterator.
*/
pageToken(): string | undefined {
return this.inner.pageToken() ?? undefined;
}
}