mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-29 06:56:15 +00:00
205 lines
7.0 KiB
TypeScript
205 lines
7.0 KiB
TypeScript
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { chdir, cwd } from "node:process";
|
|
import { test } from "node:test";
|
|
import * as arrow from "apache-arrow";
|
|
import { Buffer } from "node:buffer";
|
|
import * as lancedb from "@lancedb/lancedb";
|
|
|
|
const INLINE_MAX = 64 * 1024;
|
|
const DEDICATED_MIN = 4 * 1024 * 1024;
|
|
|
|
function marked(size: number, marker: number): Buffer {
|
|
const payload = Buffer.alloc(size);
|
|
payload[0] = marker;
|
|
payload[payload.length - 1] = marker;
|
|
return payload;
|
|
}
|
|
|
|
function kindOf(value: unknown): number {
|
|
assert.equal(typeof value, "object");
|
|
assert.ok(value !== null);
|
|
const kind = (value as { kind?: unknown }).kind;
|
|
assert.equal(typeof kind, "number");
|
|
return kind;
|
|
}
|
|
|
|
test("blob column tutorial", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "blob-v2-"));
|
|
const previous = cwd();
|
|
chdir(directory);
|
|
try {
|
|
// --8<-- [start:blob_api_setup]
|
|
const db = await lancedb.connect("./blob_db");
|
|
// --8<-- [end:blob_api_setup]
|
|
|
|
// --8<-- [start:blob_api_schema]
|
|
const blobSchema = new arrow.Schema([
|
|
new arrow.Field("id", new arrow.Int64()),
|
|
new arrow.Field("label", new arrow.Utf8()),
|
|
lancedb.blob("image"),
|
|
]);
|
|
// --8<-- [end:blob_api_schema]
|
|
|
|
// --8<-- [start:blob_api_ingest]
|
|
const blobTable = await db.createEmptyTable("photos", blobSchema, {
|
|
mode: "overwrite",
|
|
});
|
|
|
|
await blobTable.add([
|
|
{ id: 1n, label: "door", image: Buffer.from("inline_image_bytes") },
|
|
{ id: 2n, label: "window", image: Buffer.from("second_image_bytes") },
|
|
]);
|
|
// --8<-- [end:blob_api_ingest]
|
|
assert.equal(await blobTable.countRows(), 2);
|
|
|
|
// --8<-- [start:blob_api_files]
|
|
const rows = await blobTable
|
|
.query()
|
|
.select(["id", "label"])
|
|
.withRowId()
|
|
.toArray();
|
|
const rowIds = rows.map((row) => row._rowid as bigint);
|
|
|
|
const [image] = await blobTable.fetchBlobFiles("image", [rowIds[0]!]);
|
|
const header = await image!.readRange(0n, 6n);
|
|
await image!.close();
|
|
// --8<-- [end:blob_api_files]
|
|
assert.equal(header.toString("utf8"), "inline");
|
|
|
|
// --8<-- [start:blob_api_fetch]
|
|
const payloads = await blobTable.fetchBlobs("image", rowIds);
|
|
// --8<-- [end:blob_api_fetch]
|
|
assert.equal(payloads.length, 2);
|
|
|
|
// --8<-- [start:blob_api_ranges]
|
|
const [secondImage] = await blobTable.fetchBlobFiles("image", [rowIds[1]!]);
|
|
const parts = await secondImage!.readRanges([
|
|
{ start: 0n, end: 6n },
|
|
{ start: 7n, end: 12n },
|
|
]);
|
|
await secondImage!.close();
|
|
// --8<-- [end:blob_api_ranges]
|
|
assert.deepEqual(
|
|
parts.map((part) => part.toString("utf8")),
|
|
["second", "image"],
|
|
);
|
|
|
|
// --8<-- [start:blob_api_bytes_mode]
|
|
const withBytes = await blobTable
|
|
.query()
|
|
.select(["id", "image"])
|
|
.toArray({ blobMode: "bytes" });
|
|
const firstImage: Uint8Array | null = withBytes[0].image;
|
|
// --8<-- [end:blob_api_bytes_mode]
|
|
assert.equal(Buffer.from(firstImage!).toString("utf8"), "inline_image_bytes");
|
|
assert.equal(Object.hasOwn(withBytes[0], "_rowid"), false);
|
|
} finally {
|
|
chdir(previous);
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("eager query returns blob bytes without a row id", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "blob-v2-eager-"));
|
|
try {
|
|
const db = await lancedb.connect(directory);
|
|
const schema = new arrow.Schema([
|
|
new arrow.Field("id", new arrow.Int64()),
|
|
lancedb.blob("image"),
|
|
]);
|
|
const table = await db.createEmptyTable("photos", schema);
|
|
const payload = Buffer.from("inline-bytes");
|
|
await table.add([{ id: 1n, image: payload }]);
|
|
|
|
const rows = await table
|
|
.query()
|
|
.where("id = 1")
|
|
.select(["id", "image"])
|
|
.toArray({ blobMode: "bytes" });
|
|
|
|
assert.equal(rows.length, 1);
|
|
assert.equal(Object.hasOwn(rows[0], "_rowid"), false);
|
|
assert.equal(Buffer.from(rows[0].image).toString("utf8"), "inline-bytes");
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("deferred fetch reads a blob by row id", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "blob-v2-deferred-"));
|
|
try {
|
|
const db = await lancedb.connect(directory);
|
|
const schema = new arrow.Schema([
|
|
new arrow.Field("id", new arrow.Int64()),
|
|
lancedb.blob("image"),
|
|
]);
|
|
const table = await db.createEmptyTable("photos", schema);
|
|
await table.add([{ id: 1n, image: Buffer.from("deferred-bytes") }]);
|
|
|
|
const described = await table
|
|
.query()
|
|
.where("id = 1")
|
|
.select(["id", "image"])
|
|
.toArray();
|
|
assert.equal(kindOf(described[0].image), 0);
|
|
assert.equal(Object.hasOwn(described[0], "_rowid"), false);
|
|
|
|
const found = await table.query().where("id = 1").select(["id"]).withRowId().toArray();
|
|
const rowId = found[0]._rowid as bigint;
|
|
const payloads = await table.fetchBlobs("image", [rowId]);
|
|
const [handle] = await table.fetchBlobFiles("image", [rowId]);
|
|
const streamed = await handle!.readRange(0n, handle!.size());
|
|
await handle!.close();
|
|
|
|
assert.equal(Buffer.from(payloads[0]).toString("utf8"), "deferred-bytes");
|
|
assert.equal(Buffer.from(streamed).toString("utf8"), "deferred-bytes");
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("storage tiers round-trip", async () => {
|
|
const cases = [
|
|
{ id: 1n, image: marked(INLINE_MAX, 11), kind: 0 },
|
|
{ id: 2n, image: marked(INLINE_MAX + 1, 22), kind: 1 },
|
|
{ id: 3n, image: marked(DEDICATED_MIN, 33), kind: 1 },
|
|
{ id: 4n, image: marked(DEDICATED_MIN + 1, 44), kind: 2 },
|
|
];
|
|
const directory = mkdtempSync(join(tmpdir(), "blob-v2-tiers-"));
|
|
try {
|
|
const db = await lancedb.connect(directory);
|
|
const schema = new arrow.Schema([
|
|
new arrow.Field("id", new arrow.Int64()),
|
|
lancedb.blob("image"),
|
|
]);
|
|
const table = await db.createEmptyTable("tiers", schema);
|
|
await table.add(cases.map(({ id, image }) => ({ id, image })));
|
|
|
|
const described = await table.query().select(["id", "image"]).toArray();
|
|
const located = await table.query().select(["id"]).withRowId().toArray();
|
|
const rowIdByValue = new Map(located.map((row) => [row.id as bigint, row._rowid as bigint]));
|
|
|
|
for (const item of cases) {
|
|
const descriptor = described.find((row) => row.id === item.id);
|
|
assert.ok(descriptor);
|
|
assert.equal(kindOf(descriptor.image), item.kind);
|
|
const rowId = rowIdByValue.get(item.id);
|
|
assert.ok(rowId !== undefined);
|
|
const [payload] = await table.fetchBlobs("image", [rowId]);
|
|
assert.deepEqual(Buffer.from(payload), item.image);
|
|
const [handle] = await table.fetchBlobFiles("image", [rowId]);
|
|
const streamed = await handle!.read();
|
|
await handle!.close();
|
|
assert.deepEqual(streamed, item.image);
|
|
}
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|