Files
lancedb/nodejs/__test__/connection.test.ts
T
adenzhou1350andaden b205e7c628 fix(node): prevent table listing limits from wrapping (#4492)
## Summary

Prevent large JavaScript table-listing limits from wrapping during the
native `u32` conversion. With two tables, `limit: 2 ** 32` currently
returns no tables and `2 ** 32 + 1` returns only one.

Validate nonnegative safe integers using the existing helper and clamp
valid limits above `u32::MAX` before calling the native binding. Apply
the same normalization to `tableNames` and `listTables`, including
namespace overloads. Undefined, zero, small limits and `u32::MAX` keep
their existing behavior.

## Validation

Built the real Node native extension from main
`0aa842b17f13d1f8ffd2851bc57c222bc5a376ba` with the repository's `pnpm
build`, using locked dependencies and reusable build caches. No
mocked/released native extension or disabled TypeScript diagnostics.

With identical regression tests and that native library in fresh Jest
processes:

- Baseline: 40 passed, 8 failed; candidate: **48 passed**, no failures.
- Both arms: `pnpm run tsc` and `pnpm run docs` passed.
- Changed-file Biome check passed.

Tests cover wrapping limits, invalid numbers, zero/small/u32 limits,
defaults and both overload forms. Validation is the focused connection
suite, not the complete repository suite. No performance claim.

AI-assisted implementation and verification; the patch and exact tested
source bytes were reviewed before publication.

Signed-off-by: aden <aden@example.com>
Co-authored-by: aden <aden@example.com>
2026-10-23 03:14:17 +08:00

880 lines
30 KiB
TypeScript

// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
import { mkdirSync, readdirSync } from "fs";
import { join } from "path";
import { Field, Float64, Schema } from "apache-arrow";
import * as tmp from "tmp";
import {
Connection,
ListTablesResponse,
Table,
connect,
connectNamespace,
} from "../lancedb";
import { LocalTable } from "../lancedb/table";
describe("when connecting", () => {
let tmpDir: tmp.DirResult;
beforeEach(() => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
});
afterEach(() => tmpDir.removeCallback());
it("should connect", async () => {
const db = await connect(tmpDir.name);
expect(db.display()).toBe(
`ListingDatabase(uri=${tmpDir.name}, read_consistency_interval=None)`,
);
});
it("should allow read consistency interval to be specified", async () => {
const db = await connect(tmpDir.name, { readConsistencyInterval: 5 });
expect(db.display()).toBe(
`ListingDatabase(uri=${tmpDir.name}, read_consistency_interval=5s)`,
);
});
});
describe.each([
[
"URI overload",
(uri: string, options: { storageOptions?: Record<string, string> }) =>
connect(uri, options),
],
[
"object overload",
async (
uri: string,
options: { storageOptions?: Record<string, string> },
) => {
const input = { uri, ...options };
if (Object.isFrozen(options)) {
Object.freeze(input);
}
const db = await connect(input);
expect(input).toEqual({ uri, ...options });
expect(input.storageOptions).toBe(options.storageOptions);
return db;
},
],
[
"namespace",
(uri: string, options: { storageOptions?: Record<string, string> }) =>
connectNamespace("dir", { root: uri }, options),
],
])("%s connection options", (_name, open) => {
let tmpDir: tmp.DirResult;
beforeEach(() => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
});
afterEach(() => tmpDir.removeCallback());
it("should accept frozen options without storage options", async () => {
const options = Object.freeze({});
const db = await open(tmpDir.name, options);
try {
await db.createTable("frozen", [{ id: 1 }]);
await expect(db.tableNames()).resolves.toContain("frozen");
expect(options).toEqual({});
} finally {
await db.close();
}
});
it("should accept frozen options with storage options", async () => {
const storageOptions = Object.freeze({
newTableDataStorageVersion: "stable",
});
const options = Object.freeze({ storageOptions });
const db = await open(tmpDir.name, options);
try {
const table = await db.createTable("frozen", [{ id: 1 }]);
await expect(table.countRows()).resolves.toBe(1);
expect(options.storageOptions).toBe(storageOptions);
expect(storageOptions).toEqual({ newTableDataStorageVersion: "stable" });
} finally {
await db.close();
}
});
it("should preserve reused options and their storage options", async () => {
const storageOptions = { newTableDataStorageVersion: "stable" };
const options = { storageOptions };
for (let i = 0; i < 2; i++) {
const db = await open(tmpDir.name, options);
try {
const table = await db.createTable(`reused_${i}`, [{ id: i }]);
await expect(table.countRows()).resolves.toBe(1);
expect(options.storageOptions).toBe(storageOptions);
expect(storageOptions).toEqual({
newTableDataStorageVersion: "stable",
});
} finally {
await db.close();
}
}
});
});
describe("given a connection", () => {
let tmpDir: tmp.DirResult;
let db: Connection;
beforeEach(async () => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
db = await connect(tmpDir.name);
});
afterEach(() => tmpDir.removeCallback());
it("should raise an error if opening a non-existent table", async () => {
await expect(db.openTable("non-existent")).rejects.toThrow("was not found");
});
it("should raise an error if any operation is tried after it is closed", async () => {
expect(db.isOpen()).toBe(true);
await db.close();
expect(db.isOpen()).toBe(false);
await expect(db.tableNames()).rejects.toThrow("Connection is closed");
await expect(db.listTables()).rejects.toThrow("Connection is closed");
await expect(db.renameTable("a", "b")).rejects.toThrow(
"Connection is closed",
);
});
it("should report renameTable as unsupported on an OSS connection", async () => {
await db.createTable("a", [{ id: 1 }]);
await expect(db.renameTable("a", "b")).rejects.toThrow(/not supported/);
});
it("should be able to create a table from an object arg `createTable(options)`, or args `createTable(name, data, options)`", async () => {
let tbl = await db.createTable("test", [{ id: 1 }, { id: 2 }]);
await expect(tbl.countRows()).resolves.toBe(2);
tbl = await db.createTable({
name: "test",
data: [{ id: 3 }],
mode: "overwrite",
});
await expect(tbl.countRows()).resolves.toBe(1);
});
it("should be able to drop tables`", async () => {
await db.createTable("test", [{ id: 1 }, { id: 2 }]);
await db.createTable("test2", [{ id: 1 }, { id: 2 }]);
await db.createTable("test3", [{ id: 1 }, { id: 2 }]);
await expect(db.tableNames()).resolves.toEqual(["test", "test2", "test3"]);
await db.dropTable("test2");
await expect(db.tableNames()).resolves.toEqual(["test", "test3"]);
await db.dropAllTables();
await expect(db.tableNames()).resolves.toEqual([]);
// Make sure we can still create more tables after dropping all
await db.createTable("test4", [{ id: 1 }, { id: 2 }]);
});
it("should return a completed job when dropping a local table", async () => {
await db.createTable("async-drop", [{ id: 1 }]);
const job = await db.dropTableAsync("async-drop");
expect(job.id).toBeNull();
await expect(job.status()).resolves.toBe("finished");
await job.wait();
await expect(db.tableNames()).resolves.toEqual([]);
});
it("should fail if creating table twice, unless overwrite is true", async () => {
let tbl = await db.createTable("test", [{ id: 1 }, { id: 2 }]);
await expect(tbl.countRows()).resolves.toBe(2);
await expect(
db.createTable("test", [{ id: 1 }, { id: 2 }]),
).rejects.toThrow();
tbl = await db.createTable("test", [{ id: 3 }], { mode: "overwrite" });
await expect(tbl.countRows()).resolves.toBe(1);
});
describe.each([undefined, []])("root namespace %p", (namespacePath) => {
it("should preserve createTable options after the namespace", async () => {
await db.createTable("test", [{ id: 1 }, { id: 2 }]);
const table = await db.createTable("test", [{ id: 3 }], namespacePath, {
mode: "overwrite",
});
const rows = await table.query().toArray();
expect(rows.map((row) => row.id)).toEqual([3]);
});
it("should preserve createEmptyTable overwrite after the namespace", async () => {
await db.createTable("test", [{ id: 1 }]);
const schema = new Schema([new Field("id", new Float64(), true)]);
const table = await db.createEmptyTable("test", schema, namespacePath, {
mode: "overwrite",
});
await expect(table.countRows()).resolves.toBe(0);
});
it("should preserve createEmptyTable existOk after the namespace", async () => {
await db.createTable("test", [{ id: 1 }]);
const schema = new Schema([new Field("id", new Float64(), true)]);
const table = await db.createEmptyTable("test", schema, namespacePath, {
existOk: true,
});
await expect(table.countRows()).resolves.toBe(1);
});
it("should preserve tableNames options after the namespace", async () => {
for (const name of ["a", "b", "c"]) {
await db.createTable(name, [{ id: 1 }]);
}
await expect(
db.tableNames(namespacePath, { limit: 1, startAfter: "a" }),
).resolves.toEqual(["b"]);
});
it("should preserve listTables options after the namespace", async () => {
for (const name of ["a", "b", "c"]) {
await db.createTable(name, [{ id: 1 }]);
}
const first = await db.listTables(namespacePath, { limit: 1 });
expect(first.tables).toEqual(["a"]);
expect(first.pageToken).toBeDefined();
const second = await db.listTables(namespacePath, {
limit: 1,
pageToken: first.pageToken,
});
expect(second.tables).toEqual(["b"]);
});
});
it("should keep the existing table when mode is exist_ok", async () => {
const created = await db.createTable("test", [{ id: 1 }, { id: 2 }], {
mode: "exist_ok",
});
await expect(created.countRows()).resolves.toBe(2);
const reopened = await db.createTable("test", [{ id: 9 }], {
mode: "exist_ok",
});
await expect(reopened.countRows()).resolves.toBe(2);
const rows = (await reopened.query().toArray()) as { id: number }[];
expect(rows.map((row) => row.id).sort()).toEqual([1, 2]);
});
it("should respect limit and page token when listing tables", 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 }]);
let tables = await db.tableNames();
expect(tables).toEqual(["a", "b", "c"]);
tables = await db.tableNames({ limit: 1 });
expect(tables).toEqual(["a"]);
tables = await db.tableNames({ limit: 1, startAfter: "a" });
expect(tables).toEqual(["b"]);
tables = await db.tableNames({ startAfter: "a" });
expect(tables).toEqual(["b", "c"]);
});
it("should respect limit and page token when listing tables", 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 walking pages", 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).toEqual(created);
});
it.each([2 ** 32, 2 ** 32 + 1, Number.MAX_SAFE_INTEGER])(
"should not wrap a large table listing limit %s",
async (limit) => {
await db.createTable("a", [{ id: 1 }]);
await db.createTable("b", [{ id: 2 }]);
for (const names of [
db.tableNames({ limit }),
db.tableNames([], { limit }),
]) {
await expect(names).resolves.toEqual(["a", "b"]);
}
for (const page of [
db.listTables({ limit }),
db.listTables([], { limit }),
]) {
await expect(page).resolves.toEqual({ tables: ["a", "b"] });
}
},
);
it.each([
-1,
1.5,
Number.NaN,
Number.POSITIVE_INFINITY,
Number.NEGATIVE_INFINITY,
Number.MAX_SAFE_INTEGER + 1,
])("should reject an invalid table listing limit %s", async (limit) => {
await expect(db.tableNames({ limit })).rejects.toThrow(/limit/);
await expect(db.tableNames([], { limit })).rejects.toThrow(/limit/);
await expect(db.listTables({ limit })).rejects.toThrow(/limit/);
await expect(db.listTables([], { limit })).rejects.toThrow(/limit/);
});
it("should preserve zero, small and unsigned-32-bit table listing limits", async () => {
await db.createTable("a", [{ id: 1 }]);
await db.createTable("b", [{ id: 2 }]);
for (const [limit, names] of [
[0, []],
[1, ["a"]],
[2 ** 32 - 1, ["a", "b"]],
] as const) {
await expect(db.tableNames({ limit })).resolves.toEqual(names);
const page = await db.listTables({ limit });
expect(page.tables).toEqual(names);
}
await expect(db.tableNames()).resolves.toEqual(["a", "b"]);
await expect(db.listTables()).resolves.toEqual({ tables: ["a", "b"] });
});
it("should list all names and use a default page size of 100", async () => {
const names = Array.from(
{ length: 130 },
(_, i) => `t${i.toString().padStart(3, "0")}`,
);
for (const name of names) {
mkdirSync(join(tmpDir.name, `${name}.lance`));
}
await expect(db.tableNames()).resolves.toEqual(names);
await expect(db.tableNames({ limit: 0 })).resolves.toEqual([]);
const first = await db.listTables();
expect(first.tables).toEqual(names.slice(0, 100));
expect(first.pageToken).toBeDefined();
const second = await db.listTables({ pageToken: first.pageToken });
expect(second.tables).toEqual(names.slice(100));
expect(second.pageToken).toBeUndefined();
await expect(db.listTables({ limit: 200 })).resolves.toEqual({
tables: names,
pageToken: undefined,
});
await expect(db.listTables({ limit: 0 })).resolves.toEqual({
tables: [],
pageToken: undefined,
});
});
it("should list tables in a namespace", async () => {
const db = await connect(tmpDir.name, {
// biome-ignore lint/style/useNamingConvention: opaque backend property key, must match Rust
namespaceClientProperties: { manifest_enabled: "true" },
});
await db.createNamespace(["child"]);
await db.createTable("nested", [{ id: 1 }], ["child"]);
await expect(db.listTables(["child"])).resolves.toEqual(
expect.objectContaining({ tables: ["nested"] }),
);
await expect(db.listTables()).resolves.toEqual(
expect.objectContaining({ tables: [] }),
);
});
it("should create tables in v2 mode", async () => {
const db = await connect(tmpDir.name);
const data = [...Array(10000).keys()].map((i) => ({ id: i }));
// Create in v1 mode
let table = await db.createTable("test", data, {
storageOptions: { newTableDataStorageVersion: "legacy" },
});
const isV2 = async (table: Table) => {
const data = await table
.query()
.limit(10000)
.toArrow({ maxBatchLength: 100000 });
return data.batches.length < 5;
};
await expect(isV2(table)).resolves.toBe(false);
// Create in v2 mode
table = await db.createTable("test_v2", data);
await expect(isV2(table)).resolves.toBe(true);
await table.add(data);
await expect(isV2(table)).resolves.toBe(true);
// Create empty in v2 mode
const schema = new Schema([new Field("id", new Float64(), true)]);
table = await db.createEmptyTable("test_v2_empty", schema, {
storageOptions: { newTableDataStorageVersion: "stable" },
});
await table.add(data);
await expect(isV2(table)).resolves.toBe(true);
});
it("should be able to create tables with V2 manifest paths", async () => {
const db = await connect(tmpDir.name);
let table = (await db.createEmptyTable(
"test_manifest_paths_v2_empty",
new Schema([new Field("id", new Float64(), true)]),
{
enableV2ManifestPaths: true,
},
)) as LocalTable;
expect(await table.usesV2ManifestPaths()).toBe(true);
let manifestDir =
tmpDir.name + "/test_manifest_paths_v2_empty.lance/_versions";
readdirSync(manifestDir)
.filter((f) => f.endsWith(".manifest"))
.forEach((file) => {
expect(file).toMatch(/^\d{20}\.manifest$/);
});
table = (await db.createTable("test_manifest_paths_v2", [{ id: 1 }], {
enableV2ManifestPaths: true,
})) as LocalTable;
expect(await table.usesV2ManifestPaths()).toBe(true);
manifestDir = tmpDir.name + "/test_manifest_paths_v2.lance/_versions";
readdirSync(manifestDir)
.filter((f) => f.endsWith(".manifest"))
.forEach((file) => {
expect(file).toMatch(/^\d{20}\.manifest$/);
});
});
describe.each(["createTable", "createEmptyTable"] as const)(
"%s storage options",
(method) => {
const schema = new Schema([new Field("id", new Float64(), true)]);
it("should accept frozen creation options without storage options", async () => {
const options = Object.freeze({
dataStorageVersion: "stable",
enableV2ManifestPaths: true,
});
const table =
method === "createTable"
? await db.createTable("frozen_options", [{ id: 1 }], options)
: await db.createEmptyTable("frozen_options", schema, options);
expect(await (table as LocalTable).usesV2ManifestPaths()).toBe(true);
expect(options).not.toHaveProperty("storageOptions");
});
it("should accept frozen storage options and preserve override precedence", async () => {
const storageOptions = Object.freeze({
newTableDataStorageVersion: "legacy",
newTableEnableV2ManifestPaths: "false",
});
const options = Object.freeze({
storageOptions,
dataStorageVersion: "stable",
enableV2ManifestPaths: true,
});
const table =
method === "createTable"
? await db.createTable("frozen_storage", [{ id: 1 }], options)
: await db.createEmptyTable("frozen_storage", schema, options);
expect(await (table as LocalTable).usesV2ManifestPaths()).toBe(true);
expect(options.storageOptions).toBe(storageOptions);
expect(storageOptions).toEqual({
newTableDataStorageVersion: "legacy",
newTableEnableV2ManifestPaths: "false",
});
});
it("should not retain a previous override when reusing storage options", async () => {
const storageOptions = { newTableEnableV2ManifestPaths: "false" };
const create = async (name: string, enableV2ManifestPaths?: boolean) =>
method === "createTable"
? db.createTable(name, [{ id: 1 }], {
storageOptions,
enableV2ManifestPaths,
})
: db.createEmptyTable(name, schema, {
storageOptions,
enableV2ManifestPaths,
});
const first = await create("override", true);
expect(await (first as LocalTable).usesV2ManifestPaths()).toBe(true);
const second = await create("without_override");
expect(await (second as LocalTable).usesV2ManifestPaths()).toBe(false);
expect(storageOptions).toEqual({
newTableEnableV2ManifestPaths: "false",
});
});
},
);
it("should be able to migrate tables to the V2 manifest paths", async () => {
const db = await connect(tmpDir.name);
const table = (await db.createEmptyTable(
"test_manifest_path_migration",
new Schema([new Field("id", new Float64(), true)]),
{
enableV2ManifestPaths: false,
},
)) as LocalTable;
expect(await table.usesV2ManifestPaths()).toBe(false);
const manifestDir =
tmpDir.name + "/test_manifest_path_migration.lance/_versions";
readdirSync(manifestDir)
.filter((f) => f.endsWith(".manifest"))
.forEach((file) => {
expect(file).toMatch(/^\d\.manifest$/);
});
await table.migrateManifestPathsV2();
expect(await table.usesV2ManifestPaths()).toBe(true);
readdirSync(manifestDir)
.filter((f) => f.endsWith(".manifest"))
.forEach((file) => {
expect(file).toMatch(/^\d{20}\.manifest$/);
});
});
});
describe("clone table functionality", () => {
let tmpDir: tmp.DirResult;
let db: Connection;
beforeEach(async () => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
db = await connect(tmpDir.name);
});
afterEach(() => tmpDir.removeCallback());
it("should clone a table with latest version (default behavior)", async () => {
// Create source table with some data
const data = [
{ id: 1, text: "hello", vector: [1.0, 2.0] },
{ id: 2, text: "world", vector: [3.0, 4.0] },
];
const sourceTable = await db.createTable("source", data);
// Add more data to create a new version
const moreData = [{ id: 3, text: "test", vector: [5.0, 6.0] }];
await sourceTable.add(moreData);
// Clone the table (should get latest version with 3 rows)
const sourceUri = `${tmpDir.name}/source.lance`;
const clonedTable = await db.cloneTable("cloned", sourceUri);
// Verify cloned table has all 3 rows
expect(await clonedTable.countRows()).toBe(3);
expect((await db.tableNames()).includes("cloned")).toBe(true);
});
it("should clone a table from a specific version", async () => {
// Create source table with initial data
const data = [
{ id: 1, text: "hello", vector: [1.0, 2.0] },
{ id: 2, text: "world", vector: [3.0, 4.0] },
];
const sourceTable = await db.createTable("source", data);
// Get the initial version
const initialVersion = await sourceTable.version();
// Add more data to create a new version
const moreData = [{ id: 3, text: "test", vector: [5.0, 6.0] }];
await sourceTable.add(moreData);
// Verify source now has 3 rows
expect(await sourceTable.countRows()).toBe(3);
// Clone from the initial version (should have only 2 rows)
const sourceUri = `${tmpDir.name}/source.lance`;
const clonedTable = await db.cloneTable("cloned", sourceUri, {
sourceVersion: initialVersion,
});
// Verify cloned table has only the initial 2 rows
expect(await clonedTable.countRows()).toBe(2);
});
it("should clone a table from a tagged version", async () => {
// Create source table with initial data
const data = [
{ id: 1, text: "hello", vector: [1.0, 2.0] },
{ id: 2, text: "world", vector: [3.0, 4.0] },
];
const sourceTable = await db.createTable("source", data);
// Create a tag for the current version
const tags = await sourceTable.tags();
await tags.create("v1.0", await sourceTable.version());
// Add more data after the tag
const moreData = [{ id: 3, text: "test", vector: [5.0, 6.0] }];
await sourceTable.add(moreData);
// Verify source now has 3 rows
expect(await sourceTable.countRows()).toBe(3);
// Clone from the tagged version (should have only 2 rows)
const sourceUri = `${tmpDir.name}/source.lance`;
const clonedTable = await db.cloneTable("cloned", sourceUri, {
sourceTag: "v1.0",
});
// Verify cloned table has only the tagged version's 2 rows
expect(await clonedTable.countRows()).toBe(2);
});
it("should fail when attempting deep clone", async () => {
// Create source table with some data
const data = [
{ id: 1, text: "hello", vector: [1.0, 2.0] },
{ id: 2, text: "world", vector: [3.0, 4.0] },
];
await db.createTable("source", data);
// Try to create a deep clone (should fail)
const sourceUri = `${tmpDir.name}/source.lance`;
await expect(
db.cloneTable("cloned", sourceUri, { isShallow: false }),
).rejects.toThrow("Deep clone is not yet implemented");
});
});
describe("namespaces", () => {
let tmpDir: tmp.DirResult;
let db: Connection;
beforeEach(async () => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
// The local DirectoryNamespace backend only supports child namespaces
// when manifest mode is enabled (see lance-namespace-impls/src/dir.rs).
db = await connect(tmpDir.name, {
// biome-ignore lint/style/useNamingConvention: opaque backend property key, must match Rust
namespaceClientProperties: { manifest_enabled: "true" },
});
});
afterEach(() => tmpDir.removeCallback());
it("should create and describe a namespace", async () => {
await db.createNamespace(["myns"]);
const desc = await db.describeNamespace(["myns"]);
expect(desc).toBeDefined();
});
it("should list namespaces created at the root", async () => {
await db.createNamespace(["alpha"]);
await db.createNamespace(["beta"]);
const list = await db.listNamespaces();
expect(list.namespaces).toEqual(expect.arrayContaining(["alpha", "beta"]));
});
it("should list child namespaces under a parent", async () => {
await db.createNamespace(["parent"]);
await db.createNamespace(["parent", "child"]);
const list = await db.listNamespaces(["parent"]);
expect(list.namespaces).toContain("child");
});
it("should drop a namespace", async () => {
await db.createNamespace(["ephemeral"]);
await db.dropNamespace(["ephemeral"]);
const list = await db.listNamespaces();
expect(list.namespaces).not.toContain("ephemeral");
});
it("should raise an error on any namespace op after close", async () => {
await db.close();
await expect(db.describeNamespace(["foo"])).rejects.toThrow(
"Connection is closed",
);
await expect(db.listNamespaces()).rejects.toThrow("Connection is closed");
await expect(db.createNamespace(["foo"])).rejects.toThrow(
"Connection is closed",
);
await expect(db.dropNamespace(["foo"])).rejects.toThrow(
"Connection is closed",
);
});
it("should raise an understandable error when describing a non-existent namespace", async () => {
await expect(db.describeNamespace(["does-not-exist"])).rejects.toThrow(
/not found/i,
);
});
it("should raise an error when creating a namespace that already exists", async () => {
await db.createNamespace(["dup"]);
await expect(db.createNamespace(["dup"])).rejects.toThrow();
});
it("should reject an unrecognized createNamespace mode with a clear error", async () => {
await expect(
// biome-ignore lint/suspicious/noExplicitAny: deliberately bypass TS to test runtime validation
db.createNamespace(["x"], { mode: "frobnicate" as any }),
).rejects.toThrow(/Invalid mode 'frobnicate'/);
});
it("should reject an unrecognized dropNamespace mode with a clear error", async () => {
await db.createNamespace(["x"]);
await expect(
// biome-ignore lint/suspicious/noExplicitAny: deliberately bypass TS to test runtime validation
db.dropNamespace(["x"], { mode: "frobnicate" as any }),
).rejects.toThrow(/Invalid mode 'frobnicate'/);
});
it("should reject an unrecognized dropNamespace behavior with a clear error", async () => {
await db.createNamespace(["x"]);
await expect(
// biome-ignore lint/suspicious/noExplicitAny: deliberately bypass TS to test runtime validation
db.dropNamespace(["x"], { behavior: "frobnicate" as any }),
).rejects.toThrow(/Invalid behavior 'frobnicate'/);
});
});
describe("connectNamespace", () => {
let tmpDir: tmp.DirResult;
beforeEach(() => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
});
afterEach(() => tmpDir.removeCallback());
it("connects via the dir implementation and supports table ops", async () => {
const db = await connectNamespace("dir", { root: tmpDir.name });
await db.createTable("users", [{ id: 1 }, { id: 2 }]);
await expect(db.tableNames()).resolves.toContain("users");
});
it("throws a clear error when implName is empty", async () => {
await expect(connectNamespace("", {})).rejects.toThrow(
"implName must be a non-empty string",
);
});
it("throws when the namespace implementation is unknown", async () => {
await expect(connectNamespace("not-a-real-impl", {})).rejects.toThrow();
});
it("passes storage options through to the namespace", async () => {
const db = await connectNamespace(
"dir",
{ root: tmpDir.name },
{ storageOptions: { newTableDataStorageVersion: "stable" } },
);
await db.createTable("plumbing", [{ id: 1 }]);
await expect(db.tableNames()).resolves.toContain("plumbing");
});
it("supports child namespaces when manifestEnabled is true on the dir config", async () => {
const writer = await connectNamespace("dir", {
root: tmpDir.name,
manifestEnabled: true,
});
await writer.createNamespace(["analytics"]);
await writer.createTable("orders", [{ id: 1 }, { id: 2 }], ["analytics"]);
await writer.close();
const reader = await connectNamespace("dir", {
root: tmpDir.name,
manifestEnabled: true,
});
await expect(reader.tableNames(["analytics"])).resolves.toContain("orders");
const orders = await reader.openTable("orders", ["analytics"]);
await expect(orders.countRows()).resolves.toBe(2);
});
it("merges extraProperties into the dir config and is overridden by typed fields", async () => {
// Two observable assertions:
// - Typed `root` overrides extraProperties.root: createTable would fail
// under the bogus path if the override didn't happen.
// - extraProperties.manifest_enabled="false" is honored end-to-end. Child
// namespaces require manifest mode (default true), so explicitly
// disabling it via extraProperties must make createNamespace reject. If
// extraProperties pass-through were silently broken, the default would
// let createNamespace succeed.
const db = await connectNamespace("dir", {
root: tmpDir.name,
extraProperties: {
root: "/should/be/overridden",
// biome-ignore lint/style/useNamingConvention: backend property key
manifest_enabled: "false",
},
});
await db.createTable("base", [{ id: 1 }]);
await expect(db.tableNames()).resolves.toContain("base");
await expect(db.createNamespace(["analytics"])).rejects.toThrow();
});
it("flows unknown top-level keys through when implName is dynamic (no silent drop)", async () => {
// Routes via the third overload because `impl` is `string`, not the
// literal `"dir"`. The dispatcher still notices the runtime value is
// "dir", but unknown keys like `manifest_enabled` must not be silently
// dropped during the conversion.
//
// Asserting a *negative* outcome (manifest disabled -> createNamespace
// rejects) is required for observability, since the backend default for
// `manifest_enabled` is true.
const impl: string = "dir";
const db = await connectNamespace(impl, {
root: tmpDir.name,
// biome-ignore lint/style/useNamingConvention: backend property key
manifest_enabled: "false",
});
await expect(db.createNamespace(["mixed"])).rejects.toThrow();
});
});