feat: add_bases registers extra table storage prefixes

TableBase plus add_bases on native, memory, namespace, and Cloud
clients.
This commit is contained in:
geruh
2026-08-21 00:48:38 -07:00
parent 928c3dde2d
commit 047f431837
17 changed files with 677 additions and 4 deletions
+45
View File
@@ -1001,4 +1001,49 @@ describe("remote connection jobs surface", () => {
},
);
});
it("addBases posts the bases array", async () => {
const postedBodies: unknown[] = [];
await withMockDatabase(
(req, res) => {
const path = req.url ?? "";
if (path.endsWith("/describe/")) {
res.writeHead(200, { "Content-Type": "application/json" }).end(
JSON.stringify({
name: "photos",
version: 1,
schema: { fields: [] },
}),
);
return;
}
if (path.endsWith("/bases/")) {
const chunks: Buffer[] = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
postedBodies.push(JSON.parse(Buffer.concat(chunks).toString()));
res
.writeHead(200, { "Content-Type": "application/json" })
.end(JSON.stringify({ version: 2 }));
});
return;
}
res.writeHead(404).end();
},
async (db) => {
const table = await db.openTable("photos");
await table.addBases({ path: "s3://bucket/media/" });
},
);
expect(postedBodies).toEqual([
{
bases: [
{
path: "s3://bucket/media/",
isDatasetRoot: false,
},
],
},
]);
});
});
+20
View File
@@ -4,6 +4,7 @@
import * as fs from "fs";
import * as path from "path";
import * as tmp from "tmp";
import { pathToFileURL } from "url";
import * as arrow15 from "apache-arrow-15";
import * as arrow16 from "apache-arrow-16";
@@ -3404,3 +3405,22 @@ describe("computed columns", () => {
expect(rows.map((r) => r.doubled).sort()).toEqual([10, 2]);
});
});
describe("table bases", () => {
let tmpDir: tmp.DirResult;
beforeEach(() => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
});
afterEach(() => tmpDir.removeCallback());
it("addBases accepts a file uri", async () => {
const conn = await connect(tmpDir.name);
const table = await conn.createEmptyTable(
"photos",
new arrow.Schema([new arrow.Field("id", new arrow.Int64(), false)]),
);
const media = path.join(tmpDir.name, "media");
fs.mkdirSync(media);
await table.addBases(pathToFileURL(media).toString());
});
});
+1
View File
@@ -130,6 +130,7 @@ export {
export {
Table,
TableBase,
Branches,
BranchColumnSummary,
BranchColumnChange,
+49
View File
@@ -78,6 +78,25 @@ export interface WriteProgress {
done: boolean;
}
/**
* An extra storage prefix registered on a table.
*
* `path` is an object-store URI. `name` is an optional alias. `isDatasetRoot`
* is true when `path` points to a Lance dataset root. When false, `path`
* points directly to the directory containing the referenced files.
*/
export interface TableBase {
/** Object store URI such as `s3://bucket/media/`. */
path: string;
/** Optional alias. */
name?: string;
/**
* True when `path` is a Lance dataset root. When false, `path` is the
* directory containing the referenced files.
*/
isDatasetRoot?: boolean;
}
/**
* Options for adding data to a table.
*/
@@ -563,6 +582,15 @@ export abstract class Table {
| { computed: AddColumnsSql[] },
): Promise<AddColumnsResult>;
/**
* Register additional storage bases for this table.
*
* A URI string is a non-root base with no alias.
*/
abstract addBases(
bases: string | TableBase | Array<string | TableBase>,
): Promise<void>;
/**
* Fill the rows of a computed column that hold no value yet.
*
@@ -1196,6 +1224,12 @@ export class LocalTable extends Table {
throw new Error("Invalid input type for addColumns");
}
async addBases(
bases: string | TableBase | Array<string | TableBase>,
): Promise<void> {
await this.inner.addBases(normalizeBases(bases));
}
async refreshColumn(column: string): Promise<RefreshColumnResult> {
return await this.inner.refreshColumn(column);
}
@@ -1396,6 +1430,21 @@ export class LocalTable extends Table {
}
}
function normalizeBases(
bases: string | TableBase | Array<string | TableBase>,
): TableBase[] {
const baseInputs = Array.isArray(bases) ? bases : [bases];
return baseInputs.map((base) =>
typeof base === "string"
? { path: base, isDatasetRoot: false }
: {
path: base.path,
name: base.name,
isDatasetRoot: base.isDatasetRoot ?? false,
},
);
}
/**
* A definition of a column alteration. The alteration changes the column at
* `path` to have the new name `name`, to be nullable if `nullable` is true,
+25
View File
@@ -10,6 +10,7 @@ use lancedb::table::{
AddDataMode, ColumnAlteration as LanceColumnAlteration, Duration,
FieldMetadataUpdate as LanceFieldMetadataUpdate, FtsToken as LanceDbFtsToken,
NewColumnTransform, OptimizeAction, OptimizeOptions, Ref, Table as LanceDbTable,
TableBase as LanceTableBase,
};
use napi::bindgen_prelude::*;
use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
@@ -446,6 +447,18 @@ impl Table {
Ok(res.into())
}
#[napi(catch_unwind)]
pub async fn add_bases(&self, bases: Vec<TableBase>) -> napi::Result<()> {
self.inner_ref()?
.add_bases(bases.into_iter().map(|base| LanceTableBase {
path: base.path,
name: base.name,
is_dataset_root: base.is_dataset_root,
}))
.await
.default_error()
}
#[napi(catch_unwind)]
pub async fn drop_columns(&self, columns: Vec<String>) -> napi::Result<DropColumnsResult> {
let col_refs = columns.iter().map(String::as_str).collect::<Vec<_>>();
@@ -700,6 +713,18 @@ impl Table {
}
}
#[napi(object)]
/// An extra storage prefix registered on a table.
pub struct TableBase {
/// Object store URI such as `s3://bucket/media/`.
pub path: String,
/// Optional alias.
pub name: Option<String>,
/// True when `path` is a Lance dataset root. When false, `path` is the
/// directory containing the referenced files.
pub is_dataset_root: bool,
}
#[napi(object)]
/// A description of an index currently configured on a column
pub struct IndexConfig {