mirror of
https://github.com/lancedb/lancedb.git
synced 2026-08-25 23:48:23 +00:00
f1c4967eeb
## Why Four of the eight LSM methods are **remote-only in the core**. `impl BaseTable for NativeTable` implements only `set`/`unset`/`get_lsm_write_spec` and `close_lsm_writers`; `flush_lsm`, `compact_lsm` and `get_lsm_stats` fall through to trait defaults returning `NotSupported` (`rust/lancedb/src/table.rs:679,687,696`), and `checkpoint_lsm` is built on all three. That explains the state of the bindings: Node had bound the four that work against a local table and stopped, so a Cloud user could install an LSM write spec but had no way to observe fresh-tier state or drive a checkpoint. Java had none of it at all. | SDK | set/unset/get spec | closeWriters | flush | compact | getStats | checkpoint | |---|---|---|---|---|---|---| | Rust core | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Python | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Node *(before)* | ✅ | ✅ | — | — | — | — | | **Node (after)** | ✅ | ✅ | **new** | **new** | **new** | **new** | | Java *(before)* | — | — | — | — | — | — | | **Java (after)** | **new** | n/a | **new** | **new** | **new** | **new** | Go and C are separate repos and are out of scope here. `closeLsmWriters` drains cached in-process shard writers, so it has no meaning for Java, which is a pure REST client. ## Node Adds napi bindings for `flushLsm`, `compactLsm`, `checkpointLsm` and `getLsmStats`, plus typed `LsmStats` / `BucketStats` / `GenerationStats` / `MemtableStats` objects — typed rather than a JSON blob, matching the existing `LsmWriteSpec` object in the same file, with `u64` cast to `i64` per that file's convention. Because these four are remote-only, the new tests assert each binding reaches the core and surfaces `NotSupported` against a local table. That covers the wiring; behavior against a real endpoint stays covered by the mocked-endpoint tests in `rust/lancedb/src/remote/table.rs`. ## Python No new methods. All eight are on `LanceTable`, `AsyncTable` and `RemoteTable` — the last four landed on the sync `RemoteTable` in #3961, which is merged into this branch. What was missing here was reachability. `LsmWriteSpec` was importable only from the private `lancedb._lancedb`, appearing in `table.py` solely under `if TYPE_CHECKING:`, and `docs/src/python/python.md` had no mention of it, which per the repo's docs guidance means it rendered nowhere in the API reference. It is now `lancedb.LsmWriteSpec`, in `__all__`, and documented. ## Java Java reaches LanceDB purely over REST through the generated Lance Namespace client, and these routes are not in that spec, so they are issued through a small dedicated client rather than added to the spec. That call is revisitable — LSM is one of four unspecified route families alongside `multipart_write`, `page_cache/prewarm` and `branches/diff|merge`. If those are ever regularized into the spec as a group, `LanceDbTableLsm` is one file that gets deleted. `LsmWriteSpec` here is deliberately **not** `org.lance.memwal.InitializeMemWalParams`. That type defaults to maintaining *no* indexes where a spec here defaults to maintaining *every* index, and it cannot express the `null` that asks the server to resolve the set: | Value | On the wire | Meaning | |---|---|---| | unset (null) | `null` | Server resolves **every** maintainable index | | `Collections.emptyList()` | `[]` | Maintain **none** | | `Arrays.asList("id_idx")` | `["id_idx"]` | Exactly those | A dedicated test pins null and `[]` as distinct on the wire, since collapsing them is the failure mode that motivated a LanceDB-owned type. `checkpointLsm` is ported from `rust/lancedb/src/table/checkpoint.rs` with its constants and status semantics intact: 429/503 retried in place against an 8-budget, 421 restarting from flush against a 3-budget, 5s poll, and a target watermark fixed after the seal so it terminates under write load. `getLsmStats` returns typed `LsmStats` / `BucketStats` / `GenerationStats` / `MemtableStats`, mirroring the Rust structs in `rust/lancedb/src/table/lsm_stats.rs` and the objects Node exposes. Decoding is strict — see below. ## Review feedback Both gatekeeper findings were real. Each was reproduced against the scripted test server first, and each fix ships with the reproducer as a regression test. **The transport was doubling every checkpoint retry budget.** `HttpClients.createDefault()` installs Apache's default response retry strategy, whose retryable-status list is exactly 429 and 503 — the two statuses `isRetryable` owns. A 429 held against `flush_lsm` issued **18** wire requests where the loop intends 9, and `compact_lsm` was retried in place despite the loop being built to fall through to a fresh stats poll instead. Timing confirmed the mechanism: that run took 25.4s ≈ 16.3s of the loop's own backoff plus 9 × the transport's 1s retry interval. Automatic retries are now disabled, so the checkpoint loop is the sole owner of the 421/429/503 transitions. A side effect worth noting: `testCheckpointRetriesRetryableStatusInPlace` was passing on a transport-absorbed 429 and never reaching `issue()`'s retry branch at all. It now exercises the real path. **Stats decoding failed open.** `getLsmStats` read the response with Jackson's `path()`, which yields a missing node that iterates as an empty array — making "malformed" indistinguishable from "no buckets", which is indistinguishable from "drained". Four separate payloads made `checkpointLsm()` report convergence for a checkpoint that never ran: | Response | Before | Now | |---|---|---| | `{"lsm_stats": null}` or absent key | disabled ✓ | disabled ✓ | | `{"lsm_stats": {}}` | **reported success** | `IllegalStateException` | | empty response body | **reported success** | `IllegalStateException` | | bucket missing required fields | **reported success** | `IllegalStateException` | The empty-body row is the one to weight: a proxy 200 with no body is a realistic production event, and it silently reported a checkpoint that never happened. Decoding is now strict and fails closed, matching the serde contract on the Rust side exactly. One deliberate deviation from the review comment, which asked that *only* explicit JSON `null` count as disabled: Rust has `#[serde(default)]` on `lsm_stats`, so an **absent key** decodes to `None` there too. Java now matches that. It is an absent-or-malformed **`buckets`** that fails closed, which is the case the comment was actually protecting. ## Testing - Java: **33 passing** (8 existing + 25 LSM) against a scripted `com.sun.net.httpserver.HttpServer` — no new test dependency. Wire assertions mirror `rust/lancedb/src/remote/table.rs:6581-6748`; checkpoint tests cover convergence, not piling onto a latched bucket, 421 restart-from-flush, 429 retry-in-place, terminal-status propagation, reissue exhaustion, the exact wire-request count against the retry budget, and five malformed stats payloads. - Node: **19 LSM tests passing**; `cargo check`, `npm run build`, `npm run tsc`, `npm run lint`, `npm run docs` all clean. - Python: `ruff format --check` and `ruff check` clean. - Java formatting: `./mvnw -pl lancedb-core spotless:apply` and `spotless:check` both clean under a JDK 11 toolchain. ## Note: spotless needs a pre-16 JDK `./mvnw spotless:apply` fails on JDK 16+ with `JCTree$JCImport.getQualifiedIdentifier()` — google-java-format 1.7, pinned at `java/pom.xml:34`, predates JDK 16's compiler API change. **This is pre-existing** and reproduces on a pristine `main` checkout. It is not a blocker, just a toolchain requirement. Spotless was run against these sources under JDK 11 and both `spotless:apply` and `spotless:check` pass on the whole module: ```shell JAVA_HOME=/path/to/jdk11 ./mvnw -pl lancedb-core spotless:apply ``` Bumping the plugin so it works on modern JDKs is still worth doing, but separately from this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
615 lines
16 KiB
TypeScript
615 lines
16 KiB
TypeScript
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
|
|
import {
|
|
Connection,
|
|
LocalConnection,
|
|
cleanseStorageOptions,
|
|
} from "./connection";
|
|
|
|
import {
|
|
ConnectNamespaceOptions,
|
|
ConnectionOptions,
|
|
Connection as LanceDbConnection,
|
|
JsHeaderProvider as NativeJsHeaderProvider,
|
|
Session,
|
|
tokenize as nativeTokenize,
|
|
} from "./native.js";
|
|
|
|
import { HeaderProvider } from "./header";
|
|
import type { BaseTokenizer } from "./indices";
|
|
import type { FtsToken } from "./table";
|
|
|
|
// Re-export native header provider for use with connectWithHeaderProvider
|
|
export { JsHeaderProvider as NativeJsHeaderProvider } from "./native.js";
|
|
|
|
// OpenTelemetry metrics bridge. Only the high-level entry point is public; the
|
|
// underlying recorder/catalog/snapshot functions remain internal plumbing that
|
|
// `otel.ts` consumes from the native module.
|
|
export { instrumentLanceDbMetrics } from "./otel";
|
|
|
|
export {
|
|
AddColumnsSql,
|
|
ConnectionOptions,
|
|
ConnectNamespaceOptions,
|
|
IndexStatistics,
|
|
IndexConfig,
|
|
ClientConfig,
|
|
TimeoutConfig,
|
|
RetryConfig,
|
|
TlsConfig,
|
|
OptimizeStats,
|
|
CompactionStats,
|
|
RemovalStats,
|
|
TableStatistics,
|
|
FragmentStatistics,
|
|
FragmentSummaryStats,
|
|
Tags,
|
|
TagContents,
|
|
BranchContents,
|
|
MergeResult,
|
|
AddResult,
|
|
AddColumnsResult,
|
|
RefreshColumnResult,
|
|
AlterColumnsResult,
|
|
UpdateFieldMetadataResult,
|
|
DeleteResult,
|
|
DropColumnsResult,
|
|
UpdateResult,
|
|
SplitCalculatedOptions,
|
|
SplitRandomOptions,
|
|
SplitHashOptions,
|
|
SplitSequentialOptions,
|
|
ShuffleOptions,
|
|
OAuthConfig as NativeOAuthConfig,
|
|
} from "./native.js";
|
|
|
|
export {
|
|
makeArrowTable,
|
|
MakeArrowTableOptions,
|
|
Data,
|
|
VectorColumnOptions,
|
|
} from "./arrow";
|
|
|
|
export {
|
|
Connection,
|
|
CreateTableOptions,
|
|
TableNamesOptions,
|
|
OpenTableOptions,
|
|
ListNamespacesOptions,
|
|
CreateNamespaceOptions,
|
|
DropNamespaceOptions,
|
|
ListNamespacesResponse,
|
|
CreateNamespaceResponse,
|
|
DropNamespaceResponse,
|
|
DescribeNamespaceResponse,
|
|
RenameTableOptions,
|
|
} from "./connection";
|
|
|
|
export {
|
|
Job,
|
|
JobDescription,
|
|
JobFailureInfo,
|
|
JobInfo,
|
|
Session,
|
|
} from "./native.js";
|
|
|
|
export {
|
|
ExecutableQuery,
|
|
Query,
|
|
QueryBase,
|
|
VectorQuery,
|
|
TakeQuery,
|
|
AnalyzePlanDistributedMetrics,
|
|
QueryExecutionOptions,
|
|
ColumnOrdering,
|
|
FullTextSearchOptions,
|
|
RecordBatchIterator,
|
|
FullTextQuery,
|
|
MatchQuery,
|
|
PhraseQuery,
|
|
BoostQuery,
|
|
MultiMatchQuery,
|
|
BooleanQuery,
|
|
FullTextQueryType,
|
|
Operator,
|
|
Occur,
|
|
} from "./query";
|
|
|
|
export {
|
|
Index,
|
|
IndexOptions,
|
|
IvfPqOptions,
|
|
IvfRqOptions,
|
|
IvfFlatOptions,
|
|
HnswPqOptions,
|
|
HnswSqOptions,
|
|
FtsOptions,
|
|
BaseTokenizer,
|
|
} from "./indices";
|
|
|
|
export {
|
|
Table,
|
|
Branches,
|
|
BranchColumnSummary,
|
|
BranchColumnChange,
|
|
BranchIndexSummary,
|
|
BranchRowCountSummary,
|
|
MergeBlocker,
|
|
BranchDiff,
|
|
MergePreview,
|
|
MergeBranchResult,
|
|
AddDataOptions,
|
|
UpdateOptions,
|
|
OptimizeOptions,
|
|
Version,
|
|
WriteProgress,
|
|
FtsToken,
|
|
TokenizeTableOptions,
|
|
LsmWriteSpec,
|
|
LsmStats,
|
|
BucketStats,
|
|
GenerationStats,
|
|
MemtableStats,
|
|
ColumnAlteration,
|
|
FieldMetadataUpdate,
|
|
} from "./table";
|
|
|
|
export {
|
|
HeaderProvider,
|
|
StaticHeaderProvider,
|
|
OAuthHeaderProvider,
|
|
TokenResponse,
|
|
} from "./header";
|
|
|
|
export { OAuthConfig, OAuthFlowType } from "./oauth";
|
|
|
|
export { MergeInsertBuilder, WriteExecutionOptions } from "./merge";
|
|
|
|
export * as embedding from "./embedding";
|
|
export { permutationBuilder, PermutationBuilder } from "./permutation";
|
|
export { Scannable, ScannableOptions } from "./scannable";
|
|
export * as rerankers from "./rerankers";
|
|
export {
|
|
SchemaLike,
|
|
TableLike,
|
|
FieldLike,
|
|
RecordBatchLike,
|
|
DataLike,
|
|
IntoVector,
|
|
MultiVector,
|
|
} from "./arrow";
|
|
export { IntoSql, packBits } from "./util";
|
|
|
|
/**
|
|
* Options for tokenizing a full-text search query without a table index.
|
|
*/
|
|
export interface TokenizeOptions {
|
|
/**
|
|
* The tokenizer to use. The default is "simple".
|
|
*/
|
|
baseTokenizer?: BaseTokenizer;
|
|
|
|
/** Language for stemming and stop words. */
|
|
language?: string;
|
|
|
|
/** Maximum token length; tokens longer than this are ignored. */
|
|
maxTokenLength?: number;
|
|
|
|
/** Whether to lowercase tokens. */
|
|
lowercase?: boolean;
|
|
|
|
/** Whether to stem tokens. */
|
|
stem?: boolean;
|
|
|
|
/** Whether to remove stop words. */
|
|
removeStopWords?: boolean;
|
|
|
|
/**
|
|
* Custom stop words that replace the built-in list for `language`.
|
|
*
|
|
* This option only affects tokenization when `removeStopWords` is true.
|
|
*
|
|
* `undefined` keeps the built-in language list. An empty array explicitly
|
|
* replaces it with no stop words.
|
|
*/
|
|
customStopWords?: string[];
|
|
|
|
/** Whether to fold ASCII characters. */
|
|
asciiFolding?: boolean;
|
|
|
|
/** N-gram minimum length. */
|
|
ngramMinLength?: number;
|
|
|
|
/** N-gram maximum length. */
|
|
ngramMaxLength?: number;
|
|
|
|
/** Whether to only emit token prefixes for the n-gram tokenizer. */
|
|
prefixOnly?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Tokenize a full-text search query using an explicit tokenizer.
|
|
*
|
|
* This does not require a table or FTS index. The tokenizer options match
|
|
* {@link Index.fts}.
|
|
*/
|
|
export async function tokenize(
|
|
query: string,
|
|
options?: Partial<TokenizeOptions>,
|
|
): Promise<FtsToken[]> {
|
|
return await nativeTokenize(
|
|
query,
|
|
options?.baseTokenizer,
|
|
options?.language,
|
|
options?.maxTokenLength,
|
|
options?.lowercase,
|
|
options?.stem,
|
|
options?.removeStopWords,
|
|
options?.customStopWords,
|
|
options?.asciiFolding,
|
|
options?.ngramMinLength,
|
|
options?.ngramMaxLength,
|
|
options?.prefixOnly,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Connect to a LanceDB instance at the given URI.
|
|
*
|
|
* Accepted formats:
|
|
*
|
|
* - `/path/to/database` - local database
|
|
* - `s3://bucket/path/to/database` or `gs://bucket/path/to/database` - database on cloud storage
|
|
* - `db://host:port` - remote database (LanceDB cloud)
|
|
* @param {string} uri - The uri of the database. If the database uri starts
|
|
* with `db://` then it connects to a remote database.
|
|
* @see {@link ConnectionOptions} for more details on the URI format.
|
|
* @param options - The options to use when connecting to the database
|
|
* @example
|
|
* ```ts
|
|
* const conn = await connect("/path/to/database");
|
|
* ```
|
|
* @example
|
|
* ```ts
|
|
* const conn = await connect(
|
|
* "s3://bucket/path/to/database",
|
|
* {storageOptions: {timeout: "60s"}
|
|
* });
|
|
* ```
|
|
* @example
|
|
* Using with a header provider for per-request authentication:
|
|
* ```ts
|
|
* const provider = new StaticHeaderProvider({
|
|
* "X-API-Key": "my-key"
|
|
* });
|
|
* const conn = await connectWithHeaderProvider(
|
|
* "db://host:port",
|
|
* options,
|
|
* provider
|
|
* );
|
|
* ```
|
|
*/
|
|
export async function connect(
|
|
uri: string,
|
|
options?: Partial<ConnectionOptions>,
|
|
session?: Session,
|
|
headerProvider?:
|
|
| HeaderProvider
|
|
| (() => Record<string, string>)
|
|
| (() => Promise<Record<string, string>>),
|
|
): Promise<Connection>;
|
|
/**
|
|
* Connect to a LanceDB instance at the given URI.
|
|
*
|
|
* Accepted formats:
|
|
*
|
|
* - `/path/to/database` - local database
|
|
* - `s3://bucket/path/to/database` or `gs://bucket/path/to/database` - database on cloud storage
|
|
* - `db://host:port` - remote database (LanceDB cloud)
|
|
* @param options - The options to use when connecting to the database
|
|
* @see {@link ConnectionOptions} for more details on the URI format.
|
|
* @example
|
|
* ```ts
|
|
* const conn = await connect({
|
|
* uri: "/path/to/database",
|
|
* storageOptions: {timeout: "60s"}
|
|
* });
|
|
* ```
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const session = Session.default();
|
|
* const conn = await connect({
|
|
* uri: "/path/to/database",
|
|
* session: session
|
|
* });
|
|
* ```
|
|
*/
|
|
export async function connect(
|
|
options: Partial<ConnectionOptions> & { uri: string },
|
|
): Promise<Connection>;
|
|
export async function connect(
|
|
uriOrOptions: string | (Partial<ConnectionOptions> & { uri: string }),
|
|
optionsOrSession?: Partial<ConnectionOptions> | Session,
|
|
sessionOrHeaderProvider?:
|
|
| Session
|
|
| HeaderProvider
|
|
| (() => Record<string, string>)
|
|
| (() => Promise<Record<string, string>>),
|
|
headerProvider?:
|
|
| HeaderProvider
|
|
| (() => Record<string, string>)
|
|
| (() => Promise<Record<string, string>>),
|
|
): Promise<Connection> {
|
|
let uri: string | undefined;
|
|
let finalOptions: Partial<ConnectionOptions> = {};
|
|
let finalHeaderProvider:
|
|
| HeaderProvider
|
|
| (() => Record<string, string>)
|
|
| (() => Promise<Record<string, string>>)
|
|
| undefined;
|
|
|
|
if (typeof uriOrOptions !== "string") {
|
|
// First overload: connect(options)
|
|
const { uri: uri_, ...opts } = uriOrOptions;
|
|
uri = uri_;
|
|
finalOptions = opts;
|
|
} else {
|
|
// Second overload: connect(uri, options?, session?, headerProvider?)
|
|
uri = uriOrOptions;
|
|
|
|
// Handle optionsOrSession parameter
|
|
if (optionsOrSession && "inner" in optionsOrSession) {
|
|
// Second param is session, so no options provided
|
|
finalOptions = {};
|
|
} else {
|
|
// Second param is options
|
|
finalOptions = (optionsOrSession as Partial<ConnectionOptions>) || {};
|
|
}
|
|
|
|
// Handle sessionOrHeaderProvider parameter
|
|
if (
|
|
sessionOrHeaderProvider &&
|
|
(typeof sessionOrHeaderProvider === "function" ||
|
|
"getHeaders" in sessionOrHeaderProvider)
|
|
) {
|
|
// Third param is header provider
|
|
finalHeaderProvider = sessionOrHeaderProvider as
|
|
| HeaderProvider
|
|
| (() => Record<string, string>)
|
|
| (() => Promise<Record<string, string>>);
|
|
} else {
|
|
// Third param is session, header provider is fourth param
|
|
finalHeaderProvider = headerProvider;
|
|
}
|
|
}
|
|
|
|
if (!uri) {
|
|
throw new Error("uri is required");
|
|
}
|
|
|
|
finalOptions = (finalOptions as ConnectionOptions) ?? {};
|
|
(<ConnectionOptions>finalOptions).storageOptions = cleanseStorageOptions(
|
|
(<ConnectionOptions>finalOptions).storageOptions,
|
|
);
|
|
|
|
// Create native header provider if one was provided
|
|
let nativeProvider: NativeJsHeaderProvider | undefined;
|
|
if (finalHeaderProvider) {
|
|
if (typeof finalHeaderProvider === "function") {
|
|
nativeProvider = new NativeJsHeaderProvider(async () =>
|
|
finalHeaderProvider(),
|
|
);
|
|
} else if (
|
|
finalHeaderProvider &&
|
|
typeof finalHeaderProvider.getHeaders === "function"
|
|
) {
|
|
nativeProvider = new NativeJsHeaderProvider(async () =>
|
|
finalHeaderProvider.getHeaders(),
|
|
);
|
|
}
|
|
}
|
|
|
|
const nativeConn = await LanceDbConnection.new(
|
|
uri,
|
|
finalOptions,
|
|
nativeProvider,
|
|
);
|
|
return new LocalConnection(nativeConn);
|
|
}
|
|
|
|
/**
|
|
* Configuration for the built-in directory namespace (`"dir"`).
|
|
*
|
|
* The directory namespace stores tables under a single root path (local
|
|
* filesystem or object storage URI). See
|
|
* {@link https://docs.lancedb.com/namespaces} for the documented surface;
|
|
* less-common knobs live under {@link DirNamespaceConfig.extraProperties}.
|
|
*/
|
|
export interface DirNamespaceConfig {
|
|
/** Root path or URI containing the LanceDB tables. */
|
|
root: string;
|
|
/**
|
|
* Whether to maintain a namespace manifest at the root. Required for
|
|
* child namespaces. Defaults to true on the impl side.
|
|
*/
|
|
manifestEnabled?: boolean;
|
|
/**
|
|
* Additional raw properties passed verbatim to the namespace
|
|
* implementation (e.g. `storage.*`, `credential_vendor.*`). Typed
|
|
* fields above take precedence on key collision.
|
|
*/
|
|
extraProperties?: Record<string, string>;
|
|
}
|
|
|
|
/**
|
|
* Configuration for the built-in REST namespace (`"rest"`).
|
|
*
|
|
* The REST namespace talks to a remote catalog server over HTTP. See
|
|
* {@link https://docs.lancedb.com/namespaces} for the documented surface;
|
|
* less-common knobs (TLS, metrics) live under
|
|
* {@link RestNamespaceConfig.extraProperties}.
|
|
*/
|
|
export interface RestNamespaceConfig {
|
|
/** Catalog endpoint URL. */
|
|
uri: string;
|
|
/**
|
|
* HTTP headers forwarded with each request. Keys are passed through
|
|
* as-is (e.g. `"x-api-key"`, `"Authorization"`).
|
|
*/
|
|
headers?: Record<string, string>;
|
|
/**
|
|
* Additional raw properties passed verbatim to the namespace
|
|
* implementation (e.g. `tls.*`, `ops_metrics_enabled`, `delimiter`).
|
|
* Typed fields above take precedence on key collision.
|
|
*/
|
|
extraProperties?: Record<string, string>;
|
|
}
|
|
|
|
function dirConfigToProperties(
|
|
config: DirNamespaceConfig,
|
|
): Record<string, string> {
|
|
// Spread the whole input so that unknown keys (e.g. a raw `manifest_enabled`
|
|
// passed via the dynamic-impl path) flow through instead of being dropped.
|
|
// Typed transformations layer on top.
|
|
const { manifestEnabled, extraProperties, ...rest } = config;
|
|
const properties: Record<string, string> = {
|
|
...(extraProperties ?? {}),
|
|
...(rest as Record<string, string>),
|
|
};
|
|
if (manifestEnabled !== undefined) {
|
|
properties.manifest_enabled = String(manifestEnabled);
|
|
}
|
|
return properties;
|
|
}
|
|
|
|
function restConfigToProperties(
|
|
config: RestNamespaceConfig,
|
|
): Record<string, string> {
|
|
const { headers, extraProperties, ...rest } = config;
|
|
const properties: Record<string, string> = {
|
|
...(extraProperties ?? {}),
|
|
...(rest as Record<string, string>),
|
|
};
|
|
if (headers) {
|
|
for (const [name, value] of Object.entries(headers)) {
|
|
properties[`headers.${name}`] = value;
|
|
}
|
|
}
|
|
return properties;
|
|
}
|
|
|
|
/**
|
|
* Connect to a LanceDB database through a namespace.
|
|
*
|
|
* Unlike {@link connect}, which routes by URI scheme (local path vs.
|
|
* `db://` cloud), `connectNamespace` always returns a namespace-backed
|
|
* connection. The `implName` selects the namespace implementation:
|
|
*
|
|
* - `"dir"` — directory namespace, configured with {@link DirNamespaceConfig}.
|
|
* - `"rest"` — remote REST catalog, configured with {@link RestNamespaceConfig}.
|
|
* - Any other string — full module path for a custom implementation,
|
|
* configured with a free-form string-keyed `properties` map.
|
|
*
|
|
* @example Typed dir namespace
|
|
* ```ts
|
|
* const db = await connectNamespace("dir", { root: "/path/to/db" });
|
|
* await db.createTable("users", [{ id: 1 }]);
|
|
* ```
|
|
*
|
|
* @example Typed REST namespace with auth headers
|
|
* ```ts
|
|
* const db = await connectNamespace("rest", {
|
|
* uri: "https://catalog.example.com",
|
|
* headers: { "x-api-key": process.env.CATALOG_KEY ?? "" },
|
|
* });
|
|
* ```
|
|
*
|
|
* @example Custom implementation with raw properties
|
|
* ```ts
|
|
* const db = await connectNamespace("my.custom.Namespace", {
|
|
* endpoint: "...",
|
|
* });
|
|
* ```
|
|
*/
|
|
export function connectNamespace(
|
|
implName: "dir",
|
|
config: DirNamespaceConfig,
|
|
options?: Partial<ConnectNamespaceOptions>,
|
|
): Promise<Connection>;
|
|
/**
|
|
* Connect through the built-in REST namespace.
|
|
*
|
|
* Configured with {@link RestNamespaceConfig}. See the function-level
|
|
* documentation above for the full surface, examples, and how this
|
|
* relates to {@link connect}.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const db = await connectNamespace("rest", {
|
|
* uri: "https://catalog.example.com",
|
|
* headers: { "x-api-key": process.env.CATALOG_KEY ?? "" },
|
|
* });
|
|
* ```
|
|
*/
|
|
export function connectNamespace(
|
|
implName: "rest",
|
|
config: RestNamespaceConfig,
|
|
options?: Partial<ConnectNamespaceOptions>,
|
|
): Promise<Connection>;
|
|
/**
|
|
* Connect through a custom namespace implementation by full module path,
|
|
* configured with a free-form string-keyed `properties` map. Use the
|
|
* typed overloads above for the built-in `"dir"` and `"rest"` impls.
|
|
*
|
|
* See the function-level documentation above for examples and how this
|
|
* relates to {@link connect}.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const db = await connectNamespace("my.custom.Namespace", {
|
|
* endpoint: "...",
|
|
* });
|
|
* ```
|
|
*/
|
|
export function connectNamespace(
|
|
implName: string,
|
|
properties: Record<string, string>,
|
|
options?: Partial<ConnectNamespaceOptions>,
|
|
): Promise<Connection>;
|
|
export async function connectNamespace(
|
|
implName: string,
|
|
configOrProperties:
|
|
| DirNamespaceConfig
|
|
| RestNamespaceConfig
|
|
| Record<string, string>,
|
|
options?: Partial<ConnectNamespaceOptions>,
|
|
): Promise<Connection> {
|
|
let properties: Record<string, string>;
|
|
if (implName === "dir") {
|
|
properties = dirConfigToProperties(
|
|
configOrProperties as DirNamespaceConfig,
|
|
);
|
|
} else if (implName === "rest") {
|
|
properties = restConfigToProperties(
|
|
configOrProperties as RestNamespaceConfig,
|
|
);
|
|
} else {
|
|
properties = configOrProperties as Record<string, string>;
|
|
}
|
|
|
|
const finalOptions: ConnectNamespaceOptions = (options ??
|
|
{}) as ConnectNamespaceOptions;
|
|
finalOptions.storageOptions = cleanseStorageOptions(
|
|
finalOptions.storageOptions,
|
|
);
|
|
|
|
const nativeConn = await LanceDbConnection.newWithNamespace(
|
|
implName,
|
|
properties,
|
|
finalOptions,
|
|
);
|
|
return new LocalConnection(nativeConn);
|
|
}
|