Skip to content

Commit cf99451

Browse files
sfkamathcodex
andauthored
Fix spatial index access after reopening MVStore files (#1334)
- Read persisted R-tree cursor keys through H2's Spatial interface. - Compare MVSpatialKey bounds with H2's deserialized spatial keys without unsafe casts. - Verify intersecting and contained queries, updates, removals and a second reopen on a real file. - Record the fix under Unreleased while preserving the on-disk format and public signatures. - Validate all 5455 MVStore adapter tests with zero failures or errors and one skip. Co-authored-by: GPT-6.1-Sol high <noreply@openai.com>
1 parent 78d1638 commit cf99451

4 files changed

Lines changed: 100 additions & 6 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## Unreleased
2+
3+
### Issue Fixes
4+
5+
- Fix MVStore spatial-index queries, updates and removals after reopening a file. H2 reconstructs
6+
persisted R-tree keys as its own `Spatial` implementation. Cursors and bounds comparisons now
7+
accept that implementation instead of casting every key to `MVSpatialKey`. The on-disk format
8+
and public method signatures are unchanged.
9+
110
## Release 5.4.2 - Oct 19, 2026
211

312
### Security Fixes

‎nitrite-mvstore-adapter/src/main/java/org/dizitart/no2/mvstore/MVSpatialKey.java‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ public Spatial clone(long id) {
4040

4141
@Override
4242
public boolean equalsIgnoringId(Spatial o) {
43-
return Arrays.equals(minMax, ((MVSpatialKey)o).minMax);
43+
if (o instanceof MVSpatialKey) {
44+
return Arrays.equals(minMax, ((MVSpatialKey) o).minMax);
45+
}
46+
// Persisted keys are reconstructed by H2 as its own Spatial implementation.
47+
if (isNull() || o.isNull()) {
48+
return isNull() && o.isNull();
49+
}
50+
for (int dimension = 0; dimension < minMax.length / 2; dimension++) {
51+
if (Float.compare(min(dimension), o.min(dimension)) != 0
52+
|| Float.compare(max(dimension), o.max(dimension)) != 0) {
53+
return false;
54+
}
55+
}
56+
return true;
4457
}
4558
}

‎nitrite-mvstore-adapter/src/main/java/org/dizitart/no2/mvstore/NitriteMVRTreeMap.java‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.dizitart.no2.store.NitriteStore;
3232
import org.h2.mvstore.MVStore;
3333
import org.h2.mvstore.rtree.MVRTreeMap;
34+
import org.h2.mvstore.rtree.Spatial;
3435

3536
/**
3637
* @author Anindya Chatterjee
@@ -53,7 +54,7 @@ class NitriteMVRTreeMap<Key extends BoundingBox, Value> implements NitriteRTree<
5354
@Override
5455
public void add(final Key key, final NitriteId nitriteId) {
5556
if (nitriteId != null) {
56-
final MVSpatialKey spatialKey = getKey(key, nitriteId.getIdValue());
57+
final Spatial spatialKey = getKey(key, nitriteId.getIdValue());
5758
final MVStore.TxCounter txCounter = mvStore.registerVersionUsage();
5859
try {
5960
mvMap.add(spatialKey, key);
@@ -66,7 +67,7 @@ public void add(final Key key, final NitriteId nitriteId) {
6667
@Override
6768
public void remove(final Key key, final NitriteId nitriteId) {
6869
if (nitriteId != null) {
69-
final MVSpatialKey spatialKey = getKey(key, nitriteId.getIdValue());
70+
final Spatial spatialKey = getKey(key, nitriteId.getIdValue());
7071
final MVStore.TxCounter txCounter = mvStore.registerVersionUsage();
7172
try {
7273
mvMap.remove(spatialKey);
@@ -78,13 +79,13 @@ public void remove(final Key key, final NitriteId nitriteId) {
7879

7980
@Override
8081
public RecordStream<NitriteId> findIntersectingKeys(final Key key) {
81-
final MVSpatialKey spatialKey = getKey(key, 0L);
82+
final Spatial spatialKey = getKey(key, 0L);
8283
return getRecordStream(() -> mvMap.findIntersectingKeys(spatialKey));
8384
}
8485

8586
@Override
8687
public RecordStream<NitriteId> findContainedKeys(final Key key) {
87-
final MVSpatialKey spatialKey = getKey(key, 0L);
88+
final Spatial spatialKey = getKey(key, 0L);
8889
return getRecordStream(() -> mvMap.findContainedKeys(spatialKey));
8990
}
9091

@@ -179,7 +180,7 @@ public NitriteId next() {
179180
}
180181
ensureOpen();
181182
try {
182-
final MVSpatialKey next = (MVSpatialKey) treeCursor.next();
183+
final Spatial next = treeCursor.next();
183184
if (next == null) {
184185
exhausted = true;
185186
cleanable.clean();
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package org.dizitart.no2.mvstore;
2+
3+
import org.dizitart.no2.Nitrite;
4+
import org.dizitart.no2.collection.NitriteId;
5+
import org.dizitart.no2.common.RecordStream;
6+
import org.dizitart.no2.index.BoundingBox;
7+
import org.dizitart.no2.store.NitriteRTree;
8+
import org.junit.Rule;
9+
import org.junit.Test;
10+
import org.junit.rules.TemporaryFolder;
11+
12+
import java.io.File;
13+
import java.util.Arrays;
14+
import java.util.HashSet;
15+
import java.util.Set;
16+
import java.util.stream.Collectors;
17+
18+
import static org.junit.Assert.assertEquals;
19+
20+
/** H2 reads persisted R-tree keys as DefaultSpatial rather than Nitrite's MVSpatialKey. */
21+
public class ReopenedRTreeTest {
22+
@Rule
23+
public TemporaryFolder directory = new TemporaryFolder();
24+
25+
@Test
26+
public void testPersistedKeysCanBeQueriedUpdatedAndRemoved() {
27+
File path = new File(directory.getRoot(), "spatial.db");
28+
BoundingBox first = new BoundingBox(1, 2, 1, 2);
29+
BoundingBox second = new BoundingBox(10, 11, 10, 11);
30+
BoundingBox moved = new BoundingBox(20, 21, 20, 21);
31+
BoundingBox all = new BoundingBox(0, 30, 0, 30);
32+
NitriteId firstId = NitriteId.createId("1");
33+
NitriteId secondId = NitriteId.createId("2");
34+
35+
try (Nitrite db = open(path)) {
36+
NitriteRTree<BoundingBox, BoundingBox> tree = tree(db);
37+
tree.add(first, firstId);
38+
tree.add(second, secondId);
39+
db.commit();
40+
}
41+
try (Nitrite db = open(path)) {
42+
NitriteRTree<BoundingBox, BoundingBox> tree = tree(db);
43+
assertIds(tree.findIntersectingKeys(all), "1", "2");
44+
assertIds(tree.findContainedKeys(all), "1", "2");
45+
tree.remove(first, firstId);
46+
tree.add(moved, firstId);
47+
assertIds(tree.findIntersectingKeys(first));
48+
assertIds(tree.findIntersectingKeys(moved), "1");
49+
tree.remove(second, secondId);
50+
assertEquals(1, tree.size());
51+
db.commit();
52+
}
53+
try (Nitrite db = open(path)) {
54+
assertIds(tree(db).findContainedKeys(all), "1");
55+
}
56+
}
57+
58+
private static Nitrite open(File path) {
59+
return Nitrite.builder().loadModule(MVStoreModule.withConfig().filePath(path)
60+
.autoCompact(false).build()).openOrCreate();
61+
}
62+
63+
private static NitriteRTree<BoundingBox, BoundingBox> tree(Nitrite db) {
64+
return db.getStore().openRTree("bounds", BoundingBox.class, BoundingBox.class);
65+
}
66+
67+
private static void assertIds(RecordStream<NitriteId> stream, String... ids) {
68+
Set<NitriteId> expected = Arrays.stream(ids).map(NitriteId::createId).collect(Collectors.toSet());
69+
assertEquals(expected, new HashSet<>(stream.toList()));
70+
}
71+
}

0 commit comments

Comments
 (0)