Skip to content

Commit ae7cdda

Browse files
committed
Merge a delta into an Index without visiting entries only the receiver holds (#786)
Index.mergeDifferences calls the merge function for every key present on only one side, so merging a one-entry index into a large one walked the whole receiver: 100,001 calls at 100,000 entries, resolving every child. Index.merge(Index, MergeFunction) is the function-taking form of AMap.merge: for each key of the other index it applies the function to the receiver's value (or null) and the other's, and keeps keys only the receiver holds as they are without calling it. It reuses the structural merge through a private marker on the function, so MergeFunction is unchanged; sub-tries off the paths to the merged keys are shared without being traversed. The stack-safe fallback walks only the other index. RowLWW.mergeRows uses it when the incoming side carries only rows, as a row delivery does, where the creation check cannot affect rows the table holds, and the live count is then adjusted from the delivered keys instead of recounting the table. Tests check the contract against a naive oracle, one call for one merged key into 10,000 entries, unreached sub-tries never read, the deep fallback, and a row delivery into a table whose other rows are not loaded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 447c584 commit ae7cdda

6 files changed

Lines changed: 274 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Let applications choose how each Convex DB database merges, with per-row last-write-wins provided as one policy. ([Merge Policy](convex-db/docs/LATTICE-DB-ARCHITECTURE.md#merge-policy), [#779](https://github.com/Convex-Dev/convex/issues/779), [#784](https://github.com/Convex-Dev/convex/issues/784))
1313
- Let applications supply the clock that stamps Convex DB writes. ([Clock](convex-db/docs/LATTICE-DB-ARCHITECTURE.md#clock), [#781](https://github.com/Convex-Dev/convex/issues/781))
14+
- Merge a small delta into a large `Index` without visiting the rest of it, with `Index.merge`; Convex DB row deliveries now cost the rows delivered, not the table. ([#786](https://github.com/Convex-Dev/convex/issues/786))
1415

1516
### Changed
1617

‎convex-core/src/main/java/convex/core/data/Index.java‎

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,6 +1489,59 @@ public Index<K, V> mergeDifferences(Index<K, V> b, MergeFunction<V> func) {
14891489
}
14901490
}
14911491

1492+
/**
1493+
* Merges another index into this one with a merge function, as {@link AMap#merge(AMap)}
1494+
* merges entries. For every key in {@code b}, the result holds {@code func} applied to
1495+
* this index's value (or {@code null}) and {@code b}'s value: a {@code null} result
1496+
* removes the key, and a key present in both with equal values is kept without calling
1497+
* {@code func}. Keys present only in this index are kept as they are and {@code func}
1498+
* is not called for them.
1499+
*
1500+
* <p>Sub-tries of this index off the paths to {@code b}'s keys are shared unchanged
1501+
* without being traversed: only nodes on those paths are rebuilt, reading their direct
1502+
* children. The cost is therefore proportional to {@code b} and the key depth, not to
1503+
* the size of this index. {@link #mergeDifferences(Index, MergeFunction)} instead
1504+
* applies {@code func} to every key present on only one side.</p>
1505+
*
1506+
* <p>Identity contract as for {@link #mergeDifferences(Index, MergeFunction)}: returns
1507+
* {@code this} by reference identity whenever the merged result equals {@code this}.</p>
1508+
*
1509+
* @param b Index whose entries are merged into this one
1510+
* @param func Merge function for Index values
1511+
* @return Merged Index, or {@code this} by reference identity if the result is unchanged
1512+
*/
1513+
public Index<K, V> merge(Index<K, V> b, MergeFunction<V> func) {
1514+
try {
1515+
return mergeNode(this, b, new KeepOwn<>(func));
1516+
} catch (StackOverflowError e) {
1517+
// Fully unwound, as for mergeDifferences
1518+
return mergeIntoDeep(this, b, func);
1519+
}
1520+
}
1521+
1522+
/**
1523+
* Marks a merge that keeps entries present only in its left (own) operand as they are,
1524+
* without calling the wrapped function. Only {@link #merge(Index, MergeFunction)}
1525+
* creates one; the structural merge recognises it in place of a flag.
1526+
*/
1527+
private static final class KeepOwn<V> implements MergeFunction<V> {
1528+
private final MergeFunction<V> func;
1529+
1530+
KeepOwn(MergeFunction<V> func) {
1531+
this.func = func;
1532+
}
1533+
1534+
@Override
1535+
public V merge(V a, V b) {
1536+
return func.merge(a, b);
1537+
}
1538+
1539+
@Override
1540+
public V merge(Object key, V a, V b) {
1541+
return func.merge(key, a, b);
1542+
}
1543+
}
1544+
14921545
/**
14931546
* Gets the child Ref for a specific digit, or null if not found.
14941547
*/
@@ -1543,6 +1596,7 @@ private static <K extends ABlobLike<?>, V extends ACell> Index<K, V> mergeAligne
15431596
if (aref == null) {
15441597
nc = applySide(bref.getValue(), func, false); // b-only child
15451598
} else if (bref == null) {
1599+
if (func instanceof KeepOwn) continue; // a-only child kept, without traversing it
15461600
ac = aref.getValue();
15471601
nc = applySide(ac, func, true); // a-only child
15481602
} else if (aref.equals(bref)) {
@@ -1576,7 +1630,8 @@ private static <K extends ABlobLike<?>, V extends ACell> Index<K, V> mergeNested
15761630
MapEntry<K, V> ne = singleEntry(shallow.entry, func, shallowIsLeft); // shallow entry is single-side
15771631
Index<K, V>[] kids = null; // built lazily on first change
15781632
boolean changed = (ne != shallow.entry);
1579-
int sm = shallow.mask & 0xFFFF;
1633+
// The shallow node's other children are single-side; an own side that is kept is not traversed
1634+
int sm = (shallowIsLeft && func instanceof KeepOwn) ? 0 : shallow.mask & 0xFFFF;
15801635
for (int d = 0; d < 16; d++) {
15811636
if (d == dig) continue;
15821637
if (((sm >> d) & 1) == 0) continue;
@@ -1620,9 +1675,13 @@ private static <K extends ABlobLike<?>, V extends ACell> Index<K, V> branchDisjo
16201675
return new Index<K, V>(m, null, refs, (short) mask, A.count + B.count);
16211676
}
16221677

1623-
/** Apply {@code func} to every entry of a node as a single side (other side null). Returns the same node if unchanged. */
1678+
/**
1679+
* Apply {@code func} to every entry of a node as a single side (other side null). Returns the
1680+
* same node if unchanged, and at once for an own side that a {@link KeepOwn} merge keeps.
1681+
*/
16241682
private static <K extends ABlobLike<?>, V extends ACell> Index<K, V> applySide(Index<K, V> node, MergeFunction<V> func, boolean left) {
16251683
if (node.count == 0) return node;
1684+
if (left && func instanceof KeepOwn) return node;
16261685
MapEntry<K, V> ne = singleEntry(node.entry, func, left);
16271686
Index<K, V>[] kids = null;
16281687
int m = node.mask & 0xFFFF;
@@ -1689,6 +1748,34 @@ static <K extends ABlobLike<?>, V extends ACell> Index<K,V> mergeDeep(
16891748
return result;
16901749
}
16911750

1751+
/**
1752+
* Merge of {@code b} into {@code a} with no recursion, as {@link #merge(Index, MergeFunction)},
1753+
* used once the structural merge has overflowed the thread stack. Walks {@code b} only,
1754+
* looking up each key in {@code a}, so entries only {@code a} holds are neither visited nor
1755+
* passed to {@code func}. Preserves the identity contract. Package-private so tests can
1756+
* reach it directly.
1757+
*/
1758+
static <K extends ABlobLike<?>, V extends ACell> Index<K,V> mergeIntoDeep(
1759+
Index<K,V> a, Index<K,V> b, MergeFunction<V> func) {
1760+
Index<K,V> result=a;
1761+
long n=b.count;
1762+
for (long i=0; i<n; i++) {
1763+
MapEntry<K,V> be=b.entryAt(i);
1764+
MapEntry<K,V> ae=a.getEntry(be.getKey());
1765+
K key=(ae==null) ? be.getKey() : ae.getKey();
1766+
V av=(ae==null) ? null : ae.getValue();
1767+
V bv=be.getValue();
1768+
if (Utils.equals(av,bv)) continue;
1769+
V nv=func.merge(key,av,bv);
1770+
if (nv==null) {
1771+
if (ae!=null) result=result.dissocDeep(key);
1772+
} else if (!Utils.equals(av,nv)) {
1773+
result=result.assocEntryDeep(MapEntry.create(key,nv),0);
1774+
}
1775+
}
1776+
return result;
1777+
}
1778+
16921779
/** Snapshot a node's children into a digit-indexed array of size 16 (absent digits are null). */
16931780
@SuppressWarnings("unchecked")
16941781
private static <K extends ABlobLike<?>, V extends ACell> Index<K, V>[] collectKids(Index<K, V> node) {
@@ -1746,6 +1833,7 @@ private static <K extends ABlobLike<?>, V extends ACell> MapEntry<K, V> mergeEnt
17461833
/** Apply {@code func} to a single-side entry (other side null). Returns the entry, or null if removed. */
17471834
private static <K extends ABlobLike<?>, V extends ACell> MapEntry<K, V> singleEntry(MapEntry<K, V> e, MergeFunction<V> func, boolean left) {
17481835
if (e == null) return null;
1836+
if (left && func instanceof KeepOwn) return e;
17491837
V v = e.getValue();
17501838
V nv = left ? func.merge(e.getKey(), v, null) : func.merge(e.getKey(), null, v);
17511839
if (nv == null) return null;

‎convex-core/src/test/java/convex/core/data/IndexMergeTest.java‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,117 @@ public void testNoOpNestedCase() {
275275
}
276276
}
277277

278+
// ---- merge: entries only the receiver holds are kept without calling the function ----
279+
280+
/** Reference for {@link Index#merge(Index, MergeFunction)}: each key of b in turn; keys only in a untouched. */
281+
static Index<ABlob, CVMLong> naiveInto(Index<ABlob, CVMLong> a, Index<ABlob, CVMLong> b, MergeFunction<CVMLong> func) {
282+
Index<ABlob, CVMLong> result = a;
283+
long nb = b.count();
284+
for (long i = 0; i < nb; i++) {
285+
MapEntry<ABlob, CVMLong> me = b.entryAt(i);
286+
MapEntry<ABlob, CVMLong> ae = a.getEntry(me.getKey());
287+
CVMLong av = (ae == null) ? null : ae.getValue();
288+
if (Utils.equals(av, me.getValue())) continue;
289+
CVMLong nv = func.merge(me.getKey(), av, me.getValue());
290+
if (nv == null) result = result.dissoc(me.getKey());
291+
else if (!Utils.equals(av, nv)) result = result.assoc(me.getKey(), nv);
292+
}
293+
return result;
294+
}
295+
296+
@Test
297+
public void testMergeMatchesNaive() {
298+
for (int seed = 3000; seed < 3300; seed++) {
299+
Random r = new Random(seed);
300+
Index<ABlob, CVMLong> a = Index.none();
301+
Index<ABlob, CVMLong> b = Index.none();
302+
int np = 1 + r.nextInt(40);
303+
for (int i = 0; i < np; i++) {
304+
ABlob k = randomKey(r);
305+
CVMLong va = CVMLong.create(r.nextInt(100));
306+
CVMLong vb = CVMLong.create(r.nextInt(100));
307+
switch (r.nextInt(4)) { // 0:a-only 1:b-only 2:both-equal 3:both-different
308+
case 0 -> a = a.assoc(k, va);
309+
case 1 -> b = b.assoc(k, vb);
310+
case 2 -> { a = a.assoc(k, va); b = b.assoc(k, va); }
311+
default -> { a = a.assoc(k, va); b = b.assoc(k, vb); }
312+
}
313+
}
314+
for (MergeFunction<CVMLong> f : FUNCS) {
315+
String ctx = "seed=" + seed + " func=" + FUNCS.indexOf(f);
316+
Index<ABlob, CVMLong> exp = naiveInto(a, b, f);
317+
Index<ABlob, CVMLong> got = a.merge(b, f);
318+
assertEquals(exp, got, ctx);
319+
assertEquals(exp.getHash(), got.getHash(), ctx);
320+
assertEquals(exp, Index.mergeIntoDeep(a, b, f), "deep " + ctx);
321+
try {
322+
got.validate();
323+
} catch (InvalidDataException e) {
324+
fail(ctx + " produced non-canonical Index: " + e.getMessage());
325+
}
326+
327+
// Merging a subset, or nothing, is a no-op for every function: a's own entries are not touched
328+
assertSame(a, a.merge(subsetOf(a, r), f), "subset " + ctx);
329+
assertSame(a, a.merge(Index.none(), f), "empty " + ctx);
330+
assertSame(a, Index.mergeIntoDeep(a, subsetOf(a, r), f), "deep subset " + ctx);
331+
}
332+
}
333+
}
334+
335+
private static ABlob longKey(long i) {
336+
return Blob.fromHex(String.format("%016x", i));
337+
}
338+
339+
/** The function is called once per key of the merged index, however large the receiver. */
340+
@Test
341+
public void testMergeCallsFunctionOnlyForMergedKeys() {
342+
Index<ABlob, CVMLong> a = Index.none();
343+
for (long i = 0; i < 10_000; i++) a = a.assoc(longKey(i), CVMLong.create(i));
344+
long[] calls = {0};
345+
MergeFunction<CVMLong> counting = (x, y) -> { calls[0]++; return y; };
346+
347+
Index<ABlob, CVMLong> added = a.merge(Index.of(longKey(20_000), CVMLong.ONE), counting);
348+
assertEquals(1, calls[0]);
349+
assertEquals(10_001, added.count());
350+
351+
calls[0] = 0;
352+
Index<ABlob, CVMLong> updated = a.merge(Index.of(longKey(5_000), CVMLong.create(-1)), counting);
353+
assertEquals(1, calls[0]);
354+
assertEquals(CVMLong.create(-1), updated.get(longKey(5_000)));
355+
assertEquals(10_000, updated.count());
356+
357+
calls[0] = 0;
358+
assertSame(a, a.merge(Index.of(longKey(7), CVMLong.create(7)), counting), "an equal value needs no call");
359+
assertEquals(0, calls[0]);
360+
}
361+
362+
/**
363+
* Sub-tries of the receiver off the paths to the merged keys are not traversed. Rebuilding a
364+
* node on a path reads its direct children, so the sibling here is resident, but its own
365+
* children are hash-only and would fail if resolved.
366+
*/
367+
@SuppressWarnings({ "rawtypes", "unchecked" })
368+
@Test
369+
public void testMergeDoesNotTraverseUnreachedSubtries() {
370+
Index<ABlob, CVMLong> g1 = Index.of(Blob.fromHex("ab1010"), CVMLong.create(11));
371+
Index<ABlob, CVMLong> g2 = Index.of(Blob.fromHex("ab1020"), CVMLong.create(12));
372+
Ref<Index<ABlob, CVMLong>>[] siblingChildren = new Ref[] { missingChildRef(g1), missingChildRef(g2) };
373+
Index<ABlob, CVMLong> sibling = Index.unsafeCreate(4, MapEntry.create(Blob.fromHex("ab10"), CVMLong.ONE),
374+
(Ref[]) siblingChildren, (1 << 1) | (1 << 2), 3);
375+
Index<ABlob, CVMLong> resident = Index.of(Blob.fromHex("ab20"), CVMLong.create(2));
376+
Ref<Index<ABlob, CVMLong>>[] rootChildren = new Ref[] { sibling.getRef(), resident.getRef() };
377+
Index<ABlob, CVMLong> a = Index.unsafeCreate(2, MapEntry.create(Blob.fromHex("ab"), CVMLong.ZERO),
378+
(Ref[]) rootChildren, (1 << 1) | (1 << 2), 5);
379+
380+
for (MergeFunction<CVMLong> f : FUNCS) {
381+
Index<ABlob, CVMLong> merged = a.merge(Index.of(Blob.fromHex("ab25"), CVMLong.create(5)), f);
382+
assertEquals(6, merged.count());
383+
assertEquals(CVMLong.ZERO, merged.get(Blob.fromHex("ab")), "own entries are kept, even by DROP_EVEN");
384+
}
385+
// A key under no existing child
386+
assertEquals(6, a.merge(Index.of(Blob.fromHex("ab35"), CVMLong.create(3)), PICK_B).count());
387+
}
388+
278389
@Test
279390
public void testDisjointAndNested() {
280391
// Disjoint prefixes (different first digit).

‎convex-core/src/test/java/convex/core/data/IndexTest.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,11 @@ public void testDeepVariantsMatchRecursive() {
13251325
assertEquals(m.mergeDifferences(changed,(a,b) -> b),Index.mergeDeep(m,changed,(a,b) -> b));
13261326
assertEquals(m.mergeDifferences(changed,(a,b) -> (a==null)?b:a),Index.mergeDeep(m,changed,(a,b) -> (a==null)?b:a));
13271327
assertEquals(m.mergeDifferences(changed,(a,b) -> null),Index.mergeDeep(m,changed,(a,b) -> null));
1328+
1329+
assertSame(m,Index.mergeIntoDeep(m,m,(a,b) -> b));
1330+
assertEquals(m.merge(changed,(a,b) -> b),Index.mergeIntoDeep(m,changed,(a,b) -> b));
1331+
assertEquals(m.merge(changed,(a,b) -> (a==null)?b:a),Index.mergeIntoDeep(m,changed,(a,b) -> (a==null)?b:a));
1332+
assertEquals(m.merge(changed,(a,b) -> null),Index.mergeIntoDeep(m,changed,(a,b) -> null));
13281333
}
13291334
}
13301335
}

‎convex-db/src/main/java/convex/db/lattice/RowLWW.java‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ public static AVector<ACell> mergeTable(AVector<ACell> own, AVector<ACell> other
149149
liveCount = SQLTable.getLiveCount(own);
150150
} else if (mergedRows == rowsB) {
151151
liveCount = SQLTable.getLiveCount(other);
152+
} else if (timeB == null && hasLiveCount(own)) {
153+
// Delivered rows only: adjust the count held without reading the rest of the table
154+
liveCount = SQLTable.getLiveCount(own) + liveChange(rowsA, mergedRows, rowsB);
152155
} else {
153156
// Rows actually merged — recompute
154157
liveCount = SQLTable.computeLiveCount(mergedRows);
@@ -178,6 +181,10 @@ public static AVector<ACell> mergeTable(AVector<ACell> own, AVector<ACell> other
178181
* at or after the other side's utime: written before that table was created, it belongs
179182
* to an earlier table of the same name, or before it was dropped, to the dropped table.
180183
*
184+
* <p>When {@code other} carries only rows, as a delivery below an entry does, every row
185+
* held only by {@code own} survives, so those rows are not visited: the cost is
186+
* proportional to the delivered rows, not to the table.</p>
187+
*
181188
* @param own Established rows; null if absent
182189
* @param other Rows to merge in; null if absent
183190
* @param ownTime Utime of the entry holding {@code own}; null if it carries only rows
@@ -189,14 +196,33 @@ public static Index<ABlob, AVector<ACell>> mergeRows(Index<ABlob, AVector<ACell>
189196
if (own == null && other == null) return null;
190197
Index<ABlob, AVector<ACell>> base = (own == null) ? Index.none() : own;
191198
Index<ABlob, AVector<ACell>> incoming = (other == null) ? Index.none() : other;
192-
return base.mergeDifferences(incoming, new MergeFunction<AVector<ACell>>() {
199+
MergeFunction<AVector<ACell>> rule = new MergeFunction<AVector<ACell>>() {
193200
@Override
194201
public AVector<ACell> merge(AVector<ACell> a, AVector<ACell> b) {
195202
if (a == null) return writtenSince(b, ownTime) ? b : null;
196203
if (b == null) return writtenSince(a, otherTime) ? a : null;
197204
return mergeRow(a, b);
198205
}
206+
};
207+
return (otherTime == null) ? base.merge(incoming, rule) : base.mergeDifferences(incoming, rule);
208+
}
209+
210+
/** Whether an entry holds a live row count, which entries written before the field existed lack. */
211+
private static boolean hasLiveCount(AVector<ACell> entry) {
212+
return entry.count() > SQLTable.POS_LIVE_COUNT && entry.get(SQLTable.POS_LIVE_COUNT) instanceof CVMLong;
213+
}
214+
215+
/** The change in live rows from merging {@code delivered} into {@code before}, giving {@code after}. */
216+
private static long liveChange(Index<ABlob, AVector<ACell>> before, Index<ABlob, AVector<ACell>> after,
217+
Index<ABlob, AVector<ACell>> delivered) {
218+
if (delivered == null) return 0;
219+
long[] change = {0};
220+
delivered.forEach((key, row) -> {
221+
boolean was = (before != null) && SQLRow.isLive(before.get(key));
222+
boolean is = (after != null) && SQLRow.isLive(after.get(key));
223+
change[0] += (is ? 1 : 0) - (was ? 1 : 0);
199224
});
225+
return change[0];
200226
}
201227

202228
/** Whether a row was written at or after a table's utime; always so when there is none. */

‎convex-db/src/test/java/convex/db/lattice/RowLWWTest.java‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@
1313
import convex.core.data.ABlob;
1414
import convex.core.data.ACell;
1515
import convex.core.data.AVector;
16+
import convex.core.data.Blob;
1617
import convex.core.data.Index;
18+
import convex.core.data.MapEntry;
19+
import convex.core.data.Ref;
20+
import convex.core.data.RefSoft;
1721
import convex.core.data.Strings;
1822
import convex.core.data.Vectors;
1923
import convex.core.data.prim.CVMLong;
24+
import convex.core.store.NullStore;
2025

2126
/**
2227
* The per-row last-write-wins policy. Rows merge by key whichever schema wins, and a row
@@ -142,6 +147,41 @@ public void testRowDeliveryMergesAsWholeEntry() {
142147
assertNull(RowLWW.mergeTable(null, rowsOnly(fresh)), "an absent table takes nothing");
143148
}
144149

150+
/**
151+
* A row delivery reads only the delivered rows' paths. Rows elsewhere in the table are
152+
* hash-only here and would fail if read, as a persisted table would load them.
153+
*/
154+
@SuppressWarnings({ "rawtypes", "unchecked" })
155+
@Test
156+
public void testRowDeliveryDoesNotReadTheRestOfTheTable() {
157+
ABlob nearKey = Blob.fromHex("ab10");
158+
Index<ABlob, AVector<ACell>> g1 = Index.of(Blob.fromHex("ab1010"), row("g1", 2));
159+
Index<ABlob, AVector<ACell>> g2 = Index.of(Blob.fromHex("ab1020"), row("g2", 2));
160+
Ref[] unread = { notLoaded(g1), notLoaded(g2) };
161+
Index<ABlob, AVector<ACell>> sibling = Index.unsafeCreate(4, MapEntry.create(nearKey, row("near", 2)),
162+
unread, (1 << 1) | (1 << 2), 3);
163+
Index<ABlob, AVector<ACell>> resident = Index.of(Blob.fromHex("ab20"), row("resident", 2));
164+
Index<ABlob, AVector<ACell>> rows = Index.unsafeCreate(2, MapEntry.create(Blob.fromHex("ab"), row("root", 2)),
165+
new Ref[] { sibling.getRef(), resident.getRef() }, (1 << 1) | (1 << 2), 5);
166+
AVector<ACell> table = Vectors.of(SCHEMA, rows, CVMLong.create(1), CVMLong.create(5));
167+
168+
// A new row, a newer version of a held row, and a deletion
169+
AVector<ACell> added = RowLWW.mergeTable(table, rowsOnly(Index.of(Blob.fromHex("ab25"), row("new", 3))));
170+
assertEquals(6, SQLTable.getLiveCount(added));
171+
AVector<ACell> updated = RowLWW.mergeTable(table, rowsOnly(Index.of(Blob.fromHex("ab20"), row("newer", 3))));
172+
assertEquals(5, SQLTable.getLiveCount(updated));
173+
assertEquals(row("newer", 3), rows(updated).get(Blob.fromHex("ab20")));
174+
AVector<ACell> deleted = RowLWW.mergeTable(table, rowsOnly(Index.of(Blob.fromHex("ab20"), SQLRow.createTombstone(CVMLong.create(3)))));
175+
assertEquals(4, SQLTable.getLiveCount(deleted));
176+
// A row from before the table's creation is refused, still without reading the rest
177+
assertEquals(table, RowLWW.mergeTable(table, rowsOnly(Index.of(Blob.fromHex("ab25"), row("old", 0)))));
178+
}
179+
180+
@SuppressWarnings({ "rawtypes", "unchecked" })
181+
private static Ref notLoaded(Index<ABlob, AVector<ACell>> node) {
182+
return RefSoft.createForHash(node.getHash(), NullStore.INSTANCE).markEmbedded(false);
183+
}
184+
145185
// ========== Lattice laws ==========
146186

147187
/**

0 commit comments

Comments
 (0)