mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-12 23:02:28 +00:00
feat(node): expose seek, tell, bounded read, readRanges and close on BlobFile (#4348)
Closes #4300. The TypeScript `BlobFile` only had `size()`, `read()` and `readRange()`, while the Rust `BlobFile` behind it also has `seek`, `tell`, `read_up_to`, `read_ranges` and `close`. This exposes those on the Node binding: - `read(maxBytes?)` reads at most `maxBytes` from the cursor (Rust `read_up_to`). Without the argument it reads to the end, as before. - `readRanges([{ start, end }, ...])` reads several half-open ranges and returns one buffer per range, in order. Like `readRange`, it does not move the cursor. - `seek(position)` and `tell()` move and report the cursor. - `close()` releases the handle, and `isClosed()` reports it. Every read, `seek` and `tell` after `close()` fails with "blob file is already closed". A second `close()` does nothing. The binding keeps its own `closed` flag, the same way the Python `BlobFile` does. Offsets and lengths are `bigint`, like `readRange` and `size()`. `BlobRange` is exported as a type. The new tests in `table.test.ts` cover chunked reads with `read(maxBytes)`, `seek` and `tell`, a negative seek position, `readRanges` order and cursor, a `readRanges` end past the blob size, and reads after `close()`. The Node docs under `docs/src/js` are regenerated with `pnpm run docs`. I left the `ReadableStream`/async iterator idea from the issue out of this PR. The AbortSignal work in #4288 touches the same methods, so it can follow on top of this.
This commit is contained in:
@@ -2625,6 +2625,74 @@ describe("when dealing with blob columns", () => {
|
||||
).rejects.toThrow(/exceeds blob size/);
|
||||
});
|
||||
|
||||
it("reads at most maxBytes and advances the cursor", async () => {
|
||||
const { table, rowIds } = await openBlobTable();
|
||||
const [handle] = await table.fetchBlobFiles("image", rowIds);
|
||||
expect((await handle!.read(2n)).toString()).toBe("al");
|
||||
expect(await handle!.tell()).toBe(2n);
|
||||
expect((await handle!.read(2n)).toString()).toBe("ph");
|
||||
expect((await handle!.read(10n)).toString()).toBe("a");
|
||||
expect(await handle!.tell()).toBe(5n);
|
||||
expect(await handle!.read(1n)).toEqual(Buffer.alloc(0));
|
||||
});
|
||||
|
||||
it("seeks the cursor", async () => {
|
||||
const { table, rowIds, alpha } = await openBlobTable();
|
||||
const [handle] = await table.fetchBlobFiles("image", rowIds);
|
||||
await handle!.seek(3n);
|
||||
expect(await handle!.tell()).toBe(3n);
|
||||
expect((await handle!.read()).toString()).toBe("ha");
|
||||
await handle!.seek(0n);
|
||||
expect(await handle!.read()).toEqual(alpha);
|
||||
});
|
||||
|
||||
it("rejects a negative seek position", async () => {
|
||||
const { table, rowIds } = await openBlobTable();
|
||||
const [handle] = await table.fetchBlobFiles("image", rowIds);
|
||||
await expect(handle!.seek(-1n)).rejects.toThrow(/cannot be negative/);
|
||||
});
|
||||
|
||||
it("readRanges returns one buffer per range in order", async () => {
|
||||
const { table, rowIds } = await openBlobTable();
|
||||
const [handle] = await table.fetchBlobFiles("image", rowIds);
|
||||
const buffers = await handle!.readRanges([
|
||||
{ start: 3n, end: 5n },
|
||||
{ start: 0n, end: 2n },
|
||||
{ start: 3n, end: 5n },
|
||||
]);
|
||||
expect(buffers.map((b) => b.toString())).toEqual(["ha", "al", "ha"]);
|
||||
expect(await handle!.tell()).toBe(0n);
|
||||
});
|
||||
|
||||
it("fails when a readRanges end is past the blob size", async () => {
|
||||
const { table, rowIds, alpha } = await openBlobTable();
|
||||
const [handle] = await table.fetchBlobFiles("image", rowIds);
|
||||
await expect(
|
||||
handle!.readRanges([
|
||||
{ start: 0n, end: 1n },
|
||||
{ start: 0n, end: BigInt(alpha.length + 1) },
|
||||
]),
|
||||
).rejects.toThrow(/exceeds blob size/);
|
||||
});
|
||||
|
||||
it("fails reads after close and closes once", async () => {
|
||||
const { table, rowIds } = await openBlobTable();
|
||||
const [handle] = await table.fetchBlobFiles("image", rowIds);
|
||||
expect(handle!.isClosed()).toBe(false);
|
||||
await handle!.close();
|
||||
expect(handle!.isClosed()).toBe(true);
|
||||
await expect(handle!.close()).resolves.toBeUndefined();
|
||||
await expect(handle!.read()).rejects.toThrow(/already closed/);
|
||||
await expect(handle!.read(1n)).rejects.toThrow(/already closed/);
|
||||
await expect(handle!.readRange(0n, 1n)).rejects.toThrow(/already closed/);
|
||||
await expect(handle!.readRanges([{ start: 0n, end: 1n }])).rejects.toThrow(
|
||||
/already closed/,
|
||||
);
|
||||
await expect(handle!.seek(0n)).rejects.toThrow(/already closed/);
|
||||
await expect(handle!.tell()).rejects.toThrow(/already closed/);
|
||||
expect(handle!.size()).toBe(5n);
|
||||
});
|
||||
|
||||
it("rejects fetchBlobs on a non-blob column", async () => {
|
||||
const { table, rowIds } = await openBlobTable();
|
||||
await expect(table.fetchBlobs("id", rowIds)).rejects.toThrow(/blob/i);
|
||||
|
||||
Reference in New Issue
Block a user