mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-17 09:12:28 +00:00
## What Renames the MemWAL surface in two passes, across Rust, Python, TypeScript, and Java, including the wire format. ### 1. MemWAL APIs drop "LSM" The write path these APIs drive is Lance's MemWAL; "LSM" described its shape rather than naming it. Turning it on and off now reads as enabling a feature rather than installing a spec. | Before | After | |---|---| | `set_lsm_write_spec` | `enable_memwal` | | `unset_lsm_write_spec` | `disable_memwal` | | `get_lsm_write_spec` | `get_memwal_spec` | | `flush_lsm` / `compact_lsm` / `checkpoint_lsm` | `flush_memwal` / `compact_memwal` / `checkpoint_memwal` | | `get_lsm_stats` | `get_memwal_stats` | | `close_lsm_writers` | `close_memwal_writers` | | `use_lsm` (query, merge insert) | `use_memwal` | | `LsmWriteSpec` / `LsmStats` | `MemWalWriteSpec` / `MemWalStats` | | Java `LanceDbTableLsm` | `LanceDbTableMemWal` | TypeScript and Java use camelCase (`enableMemWal`, `getMemWalStats`, ...); the napi methods set `js_name` so the generated names keep the `MemWal` casing. flush/compact/checkpoint keep a `_memwal` suffix so `compact_memwal` is not confused with `compact_files` / `optimize`, which compact the base table. Internal names (`LsmMode`, `LsmScanner`, the `merge::lsm` / `query::lsm` modules) are unchanged. ### 2. Stats use SSTable and table shard Aligns the stats surface with the MemWAL naming settled upstream in lance-format/lance#7943 and #7957, where the persisted unit became an **SSTable**. **Generation → SSTable.** A `GenerationStats` describes one flushed MemTable, which is an SSTable. The generation *number* is kept — an SSTable is identified by its generation — so only the noun moved. **Bucket → table shard.** Each entry is one MemWAL shard. "Bucket" names only the hash sharding *transform*, so it was already wrong for `identity` and `year` sharding, which produce shards and no buckets at all. | Before | After | |---|---| | `GenerationStats` | `SsTableStats` | | `BucketStats` | `TableShardStats` | | `stats.buckets` | `stats.table_shards` | | `BucketStats.generations` | `TableShardStats.sstables` | | `include_generation_rows` | `include_sstable_rows` | | `newest_generation` | `newest_sstable_generation` | | `outstanding_generations` | `outstanding_sstables` | Docs and comments follow suit: "seal" → "freeze", "L0" / "flushed generations" → SSTables, "bucket" (meaning a shard) → table shard. `MemWalWriteSpec::bucket`, `num_buckets`, and the `"Sealed"` shard status value are **unchanged** — those name the sharding transform or a server value, not the shard. ## Compatibility Breaking, for an experimental API. The wire format changes with the SDKs, so this must roll out together with the matching server change: - Routes: `/set_lsm_write_spec/` → `/enable_memwal/`, `/unset_lsm_write_spec/` → `/disable_memwal/`, `/get_lsm_write_spec/` → `/get_memwal_spec/`, `/flush_lsm/` → `/flush_memwal/`, `/compact_lsm/` → `/compact_memwal/`, `/get_lsm_stats/` → `/get_memwal_stats/` - Response keys: `lsm_write_spec` → `memwal_spec`, `lsm_stats` → `memwal_stats`, `buckets` → `table_shards`, `generations` → `sstables` - Request fields: `include_generation_rows` → `include_sstable_rows`; `use_lsm` → `use_memwal` on query and merge-insert bodies ## Validation - Rust: `cargo test --features remote -p lancedb --lib --tests` — 1467 lib tests and all integration test targets pass; `cargo clippy` clean - Python: rebuilt the extension; `test_remote_db`, `test_memwal_spec`, `test_merge_insert_lsm`, `test_query`, `test_permutation`, `test_table` — 655 passed; `ruff format` / `ruff check` clean - TypeScript: `pnpm build`; `table`, `merge`, `query` tests — 593 passed; `biome check` clean; `docs/src/js` regenerated with `npm run docs` - Java: `mvn test` — 37 passed (Spotless applied with JDK 11; the pinned google-java-format does not run on JDK 21) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
162 lines
5.7 KiB
TypeScript
162 lines
5.7 KiB
TypeScript
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
import { Data, Schema, fromDataToBuffer } from "./arrow";
|
|
import { MergeResult, NativeMergeInsertBuilder } from "./native";
|
|
|
|
/** A builder used to create and run a merge insert operation */
|
|
export class MergeInsertBuilder {
|
|
#native: NativeMergeInsertBuilder;
|
|
#schema: Schema | Promise<Schema>;
|
|
|
|
/** Construct a MergeInsertBuilder. __Internal use only.__ */
|
|
constructor(
|
|
native: NativeMergeInsertBuilder,
|
|
schema: Schema | Promise<Schema>,
|
|
) {
|
|
this.#native = native;
|
|
this.#schema = schema;
|
|
}
|
|
|
|
/**
|
|
* Rows that exist in both the source table (new data) and
|
|
* the target table (old data) will be updated, replacing
|
|
* the old row with the corresponding matching row.
|
|
*
|
|
* If there are multiple matches then the behavior is undefined.
|
|
* Currently this causes multiple copies of the row to be created
|
|
* but that behavior is subject to change.
|
|
*
|
|
* An optional condition may be specified. If it is, then only
|
|
* matched rows that satisfy the condition will be updated. Any
|
|
* rows that do not satisfy the condition will be left as they
|
|
* are. Failing to satisfy the condition does not cause a
|
|
* "matched row" to become a "not matched" row.
|
|
*
|
|
* The condition should be an SQL string. Use the prefix
|
|
* target. to refer to rows in the target table (old data)
|
|
* and the prefix source. to refer to rows in the source
|
|
* table (new data).
|
|
*
|
|
* For example, "target.last_update < source.last_update"
|
|
*/
|
|
whenMatchedUpdateAll(options?: { where: string }): MergeInsertBuilder {
|
|
return new MergeInsertBuilder(
|
|
this.#native.whenMatchedUpdateAll(options?.where),
|
|
this.#schema,
|
|
);
|
|
}
|
|
/**
|
|
* Rows that exist only in the source table (new data) should
|
|
* be inserted into the target table.
|
|
*/
|
|
whenNotMatchedInsertAll(): MergeInsertBuilder {
|
|
return new MergeInsertBuilder(
|
|
this.#native.whenNotMatchedInsertAll(),
|
|
this.#schema,
|
|
);
|
|
}
|
|
/**
|
|
* Rows that exist only in the target table (old data) will be
|
|
* deleted. An optional condition can be provided to limit what
|
|
* data is deleted.
|
|
*
|
|
* @param options.where - An optional condition to limit what data is deleted
|
|
*/
|
|
whenNotMatchedBySourceDelete(options?: {
|
|
where: string;
|
|
}): MergeInsertBuilder {
|
|
return new MergeInsertBuilder(
|
|
this.#native.whenNotMatchedBySourceDelete(options?.where),
|
|
this.#schema,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Controls whether to use indexes for the merge operation.
|
|
*
|
|
* When set to `true` (the default), the operation will use an index if available
|
|
* on the join key for improved performance. When set to `false`, it forces a full
|
|
* table scan even if an index exists. This can be useful for benchmarking or when
|
|
* the query optimizer chooses a suboptimal path.
|
|
*
|
|
* @param useIndex - Whether to use indices for the merge operation. Defaults to `true`.
|
|
*/
|
|
useIndex(useIndex: boolean): MergeInsertBuilder {
|
|
return new MergeInsertBuilder(
|
|
this.#native.useIndex(useIndex),
|
|
this.#schema,
|
|
);
|
|
}
|
|
/**
|
|
* Control MemWAL routing for this merge.
|
|
*
|
|
* By default (unset), a `mergeInsert` on a table with a MemWAL write spec is
|
|
* routed through Lance's MemWAL shard writer, and a table without one uses the
|
|
* standard path.
|
|
*
|
|
* @param enable - `true` forces MemWAL routing and errors if the table has no
|
|
* MemWAL write spec. `false` forces the standard write path even when a spec is set.
|
|
*/
|
|
useMemWal(enable: boolean): MergeInsertBuilder {
|
|
return new MergeInsertBuilder(this.#native.useMemWal(enable), this.#schema);
|
|
}
|
|
/**
|
|
* Controls how an LSM merge checks that its input targets a single shard.
|
|
*
|
|
* When a table has a MemWAL write spec, every row in a `mergeInsert` call must
|
|
* route to the same shard. When `true` (the default), every row is inspected
|
|
* to verify this. When `false`, only the first row is inspected and the
|
|
* shard it routes to is used for the whole input — a faster path for callers
|
|
* that have already pre-sharded their input. Has no effect on tables without
|
|
* a MemWAL write spec.
|
|
*
|
|
* @param validateSingleShard - Whether to check every row routes to one shard. Defaults to `true`.
|
|
*/
|
|
validateSingleShard(validateSingleShard: boolean): MergeInsertBuilder {
|
|
return new MergeInsertBuilder(
|
|
this.#native.validateSingleShard(validateSingleShard),
|
|
this.#schema,
|
|
);
|
|
}
|
|
/**
|
|
* Executes the merge insert operation
|
|
*
|
|
* @returns {Promise<MergeResult>} the merge result
|
|
*/
|
|
async execute(
|
|
data: Data,
|
|
execOptions?: Partial<WriteExecutionOptions>,
|
|
): Promise<MergeResult> {
|
|
let schema: Schema;
|
|
if (this.#schema instanceof Promise) {
|
|
schema = await this.#schema;
|
|
this.#schema = schema; // In case of future calls
|
|
} else {
|
|
schema = this.#schema;
|
|
}
|
|
|
|
if (execOptions?.timeoutMs !== undefined) {
|
|
this.#native.setTimeout(execOptions.timeoutMs);
|
|
}
|
|
|
|
const buffer = await fromDataToBuffer(data, undefined, schema);
|
|
return await this.#native.execute(buffer);
|
|
}
|
|
}
|
|
|
|
export interface WriteExecutionOptions {
|
|
/**
|
|
* Maximum time to run the operation before cancelling it.
|
|
*
|
|
* By default, there is a 30-second timeout that is only enforced after the
|
|
* first attempt. This is to prevent spending too long retrying to resolve
|
|
* conflicts. For example, if a write attempt takes 20 seconds and fails,
|
|
* the second attempt will be cancelled after 10 seconds, hitting the
|
|
* 30-second timeout. However, a write that takes one hour and succeeds on the
|
|
* first attempt will not be cancelled.
|
|
*
|
|
* When this is set, the timeout is enforced on all attempts, including the first.
|
|
*/
|
|
timeoutMs?: number;
|
|
}
|