Merge branch 'main' into refactor/wal-naming

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel Rammer
2026-10-12 15:30:00 -05:00
co-authored by Claude Opus 5.5
636 changed files with 118151 additions and 22369 deletions
+25 -2
View File
@@ -8,7 +8,7 @@
<parent>
<groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId>
<version>0.38.0-beta.11</version>
<version>0.41.0-beta.6</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -44,7 +44,30 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.1</version>
</dependency>
<!-- Declared directly so downstream consumers resolve the patched versions:
the parent's jackson-bom only manages our own build, and otherwise Maven's
nearest-wins mediation picks older versions via arrow-vector and
lance-namespace-apache-client. -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>
<dependency>
@@ -117,9 +117,13 @@ public class LanceDbTableLsm {
/**
* Read the {@link LsmWriteSpec} currently installed on this table.
*
* <p>Empty when the LSM write path is not enabled. The returned spec mirrors what was installed,
* except that {@link LsmWriteSpec#maintainedIndexes()} always reports the concrete list resolved
* when the spec was set — a null selection never round-trips.
* <p>Empty when the LSM write path is not enabled at all — no spec has been set, or one was
* removed with {@link #unsetLsmWriteSpec()}. That is a different answer from a present spec
* whose {@link LsmWriteSpec#maintainedIndexes()} is null, which selects indexes automatically.
*
* <p>The spec read back is the one that was installed, selection included: null maintains every
* supported index the table has now or gains later, an empty list maintains none, and a
* non-empty list maintains exactly those. All three round-trip.
*/
public Optional<LsmWriteSpec> getLsmWriteSpec() {
JsonNode response = client.post(route("get_lsm_write_spec"), null);
@@ -36,6 +36,8 @@ import java.util.Map;
*/
public class LsmWriteSpec {
private static final String CONTEXT = "get_lsm_write_spec response";
/** How writes are routed to MemWAL shards. */
public enum Sharding {
/** Hash-bucket writes by a scalar column. */
@@ -124,9 +126,8 @@ public class LsmWriteSpec {
/**
* Set the indexes the MemWAL keeps up to date as rows are appended.
*
* <p>Pass {@code null} — the default for a fresh spec — to maintain every index the MemWAL can,
* resolved when the spec is installed. That is a snapshot: indexes created later are not
* maintained until the spec is unset and set again. Pass an empty list to maintain none.
* <p>Pass {@code null} — the default for a fresh spec — to maintain every index the table has,
* including ones created later. Pass an empty list to maintain none.
*
* <p>Note that {@code null} and the empty list mean opposite things here.
*/
@@ -176,8 +177,10 @@ public class LsmWriteSpec {
}
/**
* The indexes the MemWAL maintains, or null to have the server resolve every maintainable index
* on install. An empty list means none.
* Which indexes the MemWAL maintains.
*
* <p>Null selects automatically: every supported index the table has now or gains later. An empty
* list maintains none. A non-empty list maintains exactly those.
*/
public List<String> maintainedIndexes() {
return maintainedIndexes == null ? null : Collections.unmodifiableList(maintainedIndexes);
@@ -210,8 +213,8 @@ public class LsmWriteSpec {
/**
* Rebuild a spec from a {@code get_lsm_write_spec} response body.
*
* <p>The server always reports a concrete maintained-index list, so a null selection never
* round-trips.
* <p>A null {@code maintained_indexes} selects every index the table has; an empty list selects
* none.
*/
static LsmWriteSpec fromJson(JsonNode node) {
JsonNode shardingNode = node.get("sharding");
@@ -224,10 +227,19 @@ public class LsmWriteSpec {
Integer numBuckets =
shardingNode.hasNonNull("num_buckets") ? shardingNode.get("num_buckets").asInt() : null;
List<String> maintainedIndexes = new ArrayList<String>();
JsonNode indexesNode = node.get("maintained_indexes");
if (indexesNode != null && indexesNode.isArray()) {
// Absent or null is the automatic selection; anything else has to be an
// array of names. Reading a string or an object as automatic would turn a
// malformed response into "maintain every index", which is a policy, not a
// parse failure.
JsonNode indexesNode = JsonFields.optionalArray(node, "maintained_indexes", CONTEXT);
List<String> maintainedIndexes = null;
if (indexesNode != null) {
maintainedIndexes = new ArrayList<String>();
for (JsonNode index : indexesNode) {
if (!index.isTextual()) {
throw new IllegalStateException(
CONTEXT + ": maintained_indexes must hold index names, got " + index);
}
maintainedIndexes.add(index.asText());
}
}
@@ -215,6 +215,61 @@ public class LanceDbTableLsmTest {
assertEquals("true", spec.get().writerConfigDefaults().get("durable_write"));
}
/** A null selection, every index the table has, reads back as null rather than an empty list. */
@Test
public void testGetLsmWriteSpecRoundTripsEveryIndexSelection() {
enqueue(
"get_lsm_write_spec",
200,
"{\"lsm_write_spec\":{\"sharding\":{\"mode\":\"unsharded\"},"
+ "\"maintained_indexes\":null,\"writer_config_defaults\":{}}}");
Optional<LsmWriteSpec> spec = lsm.getLsmWriteSpec();
assertTrue(spec.isPresent());
assertNull(spec.get().maintainedIndexes());
}
@Test
public void testGetLsmWriteSpecRoundTripsNoIndexSelection() {
enqueue(
"get_lsm_write_spec",
200,
"{\"lsm_write_spec\":{\"sharding\":{\"mode\":\"unsharded\"},"
+ "\"maintained_indexes\":[],\"writer_config_defaults\":{}}}");
Optional<LsmWriteSpec> spec = lsm.getLsmWriteSpec();
assertTrue(spec.isPresent());
assertEquals(Collections.emptyList(), spec.get().maintainedIndexes());
}
/**
* Null is the automatic selection, so anything that is neither null nor an array has to be
* rejected: reading a string or an object as null would turn a malformed response into a policy.
*/
@Test
public void testGetLsmWriteSpecRejectsANonArrayMaintainedIndexes() {
enqueue(
"get_lsm_write_spec",
200,
"{\"lsm_write_spec\":{\"sharding\":{\"mode\":\"unsharded\"},"
+ "\"maintained_indexes\":\"id_idx\",\"writer_config_defaults\":{}}}");
assertThrows(IllegalStateException.class, () -> lsm.getLsmWriteSpec());
}
@Test
public void testGetLsmWriteSpecRejectsNonStringIndexNames() {
enqueue(
"get_lsm_write_spec",
200,
"{\"lsm_write_spec\":{\"sharding\":{\"mode\":\"unsharded\"},"
+ "\"maintained_indexes\":[7],\"writer_config_defaults\":{}}}");
assertThrows(IllegalStateException.class, () -> lsm.getLsmWriteSpec());
}
@Test
public void testGetLsmWriteSpecAbsent() {
enqueue("get_lsm_write_spec", 200, "{\"lsm_write_spec\":null}");