This is an automated email from the ASF dual-hosted git repository.

rdblue pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new e055c68fff Core: Add MutableBitmap.merge (#18704)
e055c68fff is described below

commit e055c68fff4472dbd79fde05711b238d22b04c7d
Author: Ryan Blue <[email protected]>
AuthorDate: Tue Nov 3 11:46:18 2026 -0800

    Core: Add MutableBitmap.merge (#18704)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/iceberg/mumbling/MumblingBitmap.java    | 111 +++++-
 .../org/apache/iceberg/mumbling/MutableBitmap.java |   4 +
 .../iceberg/mumbling/TestMumblingBitmap.java       | 440 +++++++++++++++++++++
 3 files changed, 540 insertions(+), 15 deletions(-)

diff --git a/core/src/main/java/org/apache/iceberg/mumbling/MumblingBitmap.java 
b/core/src/main/java/org/apache/iceberg/mumbling/MumblingBitmap.java
index aea57c518e..ef86f581bc 100644
--- a/core/src/main/java/org/apache/iceberg/mumbling/MumblingBitmap.java
+++ b/core/src/main/java/org/apache/iceberg/mumbling/MumblingBitmap.java
@@ -19,6 +19,7 @@
 package org.apache.iceberg.mumbling;
 
 import java.nio.ByteBuffer;
+import java.util.Arrays;
 import java.util.Map;
 import java.util.NavigableMap;
 import org.apache.iceberg.ManifestBitmap;
@@ -54,7 +55,7 @@ class MumblingBitmap implements ManifestBitmap {
   private static final int MAX_CONTAINER_COUNT = 8_192;
   private static final int MAX_CARDINALITY = 2_097_152;
 
-  static MutableBitmap newMutableBitmap() {
+  static Mutable newMutableBitmap() {
     return new MumblingBitmap(EMPTY_BITMAP).asMutable();
   }
 
@@ -161,7 +162,7 @@ class MumblingBitmap implements ManifestBitmap {
     return offsets[containerIndex];
   }
 
-  public MutableBitmap asMutable() {
+  public Mutable asMutable() {
     return new Mutable(this);
   }
 
@@ -172,7 +173,8 @@ class MumblingBitmap implements ManifestBitmap {
    *
    * <p>The initial buffer is not modified.
    */
-  private static class Mutable implements MutableBitmap {
+  @VisibleForTesting
+  static class Mutable implements MutableBitmap {
     private final NavigableMap<Integer, Container> overlays = 
Maps.newTreeMap();
     private final MumblingBitmap base;
     private final int baseContainerCount;
@@ -206,17 +208,7 @@ class MumblingBitmap implements ManifestBitmap {
           pos < MAX_CARDINALITY, "Cannot set invalid bit position: %s (must be 
< 2,097,152)", pos);
 
       int containerIndex = containerIndex(pos);
-      Container overlay = overlays.get(containerIndex);
-      if (overlay == null) {
-        if (containerIndex < baseContainerCount) {
-          ByteBuffer src = base.buffer();
-          int offset = src.position() + base.offset(containerIndex);
-          overlay = new Container(src, offset, 
base.descriptor(containerIndex));
-        } else {
-          overlay = new Container();
-        }
-        overlays.put(containerIndex, overlay);
-      }
+      Container overlay = overlay(containerIndex);
 
       if (overlay.set(posInContainer(pos))) {
         this.cardinality += 1;
@@ -226,6 +218,18 @@ class MumblingBitmap implements ManifestBitmap {
       return false;
     }
 
+    @Override
+    public void merge(ManifestBitmap bitmap) {
+      if (bitmap instanceof Mutable) {
+        merge((Mutable) bitmap);
+      } else if (bitmap instanceof MumblingBitmap) {
+        merge((MumblingBitmap) bitmap);
+      } else {
+        // use the buffer to get the bitmap as a supported class
+        merge(new MumblingBitmap(bitmap.buffer()));
+      }
+    }
+
     @Override
     public ByteBuffer buffer() {
       int[] descriptorArray = descriptorArray();
@@ -255,7 +259,8 @@ class MumblingBitmap implements ManifestBitmap {
       return buffer;
     }
 
-    private int[] descriptorArray() {
+    @VisibleForTesting
+    int[] descriptorArray() {
       int[] baseDescriptors = base.descriptors();
       int lastOverlayIndex = overlays.isEmpty() ? -1 : overlays.lastKey();
       int numContainers = Math.max(lastOverlayIndex + 1, 
baseDescriptors.length);
@@ -307,9 +312,49 @@ class MumblingBitmap implements ManifestBitmap {
 
       return 0;
     }
+
+    private Container overlay(int containerIndex) {
+      Container overlay = overlays.get(containerIndex);
+      if (overlay == null) {
+        overlay = Container.newOverlay(base, containerIndex);
+        overlays.put(containerIndex, overlay);
+      }
+
+      return overlay;
+    }
+
+    private void merge(Mutable other) {
+      int[] descriptors = other.descriptorArray();
+      for (int containerIndex = 0; containerIndex < descriptors.length; 
containerIndex += 1) {
+        if (descriptors[containerIndex] != 0) {
+          Container overlay = overlay(containerIndex);
+          this.cardinality += overlay.merge(other.overlay(containerIndex));
+        }
+      }
+    }
+
+    private void merge(MumblingBitmap other) {
+      int[] descriptors = other.descriptors();
+      for (int containerIndex = 0; containerIndex < descriptors.length; 
containerIndex += 1) {
+        if (descriptors[containerIndex] != 0) {
+          Container overlay = overlay(containerIndex);
+          this.cardinality += overlay.merge(Container.newOverlay(other, 
containerIndex));
+        }
+      }
+    }
   }
 
   private static class Container {
+    private static Container newOverlay(MumblingBitmap bitmap, int 
containerIndex) {
+      if (containerIndex < bitmap.containerCount) {
+        ByteBuffer src = bitmap.buffer();
+        int offset = src.position() + bitmap.offset(containerIndex);
+        return new Container(src, offset, bitmap.descriptor(containerIndex));
+      }
+
+      return new Container();
+    }
+
     private byte[] dense = null;
     private byte[] sparse = null;
     private int cardinality = 0;
@@ -371,6 +416,42 @@ class MumblingBitmap implements ManifestBitmap {
       }
     }
 
+    private int merge(Container other) {
+      int initialCardinality = cardinality;
+
+      if (other.dense != null) {
+        // other may be sparse and have a dense array
+        if (dense != null) {
+          // OR the dense bytes together
+          for (int i = 0; i < dense.length; i += 1) {
+            this.dense[i] |= other.dense[i];
+          }
+        } else {
+          // Copy the dense bytes and set the positions from this container
+          this.dense = Arrays.copyOf(other.dense, other.dense.length);
+          for (int i = 0; i < cardinality; i += 1) {
+            denseSet(dense, (sparse[i] & 0xFF));
+          }
+        }
+
+        this.cardinality = denseCardinality(dense);
+
+        // copy the sparse array if needed; other was sparse and merge set no 
new bits
+        if (cardinality < DENSE_CONTAINER_SIZE) {
+          this.sparse = Arrays.copyOf(other.sparse, other.sparse.length);
+        } else {
+          this.sparse = null;
+        }
+      } else {
+        // Set the positions from the other container in this container
+        for (int i = 0; i < other.cardinality; i += 1) {
+          set(other.sparse[i] & 0xFF);
+        }
+      }
+
+      return cardinality - initialCardinality;
+    }
+
     private boolean setInternal(int posInContainer) {
       if (cardinality < MAX_SPARSE_CARDINALITY) {
         return sparseSet(sparse, cardinality, posInContainer);
diff --git a/core/src/main/java/org/apache/iceberg/mumbling/MutableBitmap.java 
b/core/src/main/java/org/apache/iceberg/mumbling/MutableBitmap.java
index 7c46ed5d24..078cb7aad1 100644
--- a/core/src/main/java/org/apache/iceberg/mumbling/MutableBitmap.java
+++ b/core/src/main/java/org/apache/iceberg/mumbling/MutableBitmap.java
@@ -22,5 +22,9 @@ import org.apache.iceberg.ManifestBitmap;
 
 /** Mutation methods for ManifestBitmap. */
 public interface MutableBitmap extends ManifestBitmap {
+  /** Set the bit at {@code position}. */
   boolean set(int position);
+
+  /** Set all bits in this bitmap that are set in {@code bitmap}. */
+  void merge(ManifestBitmap bitmap);
 }
diff --git 
a/core/src/test/java/org/apache/iceberg/mumbling/TestMumblingBitmap.java 
b/core/src/test/java/org/apache/iceberg/mumbling/TestMumblingBitmap.java
index ed61fed195..4feaf229f1 100644
--- a/core/src/test/java/org/apache/iceberg/mumbling/TestMumblingBitmap.java
+++ b/core/src/test/java/org/apache/iceberg/mumbling/TestMumblingBitmap.java
@@ -28,6 +28,9 @@ import static 
org.assertj.core.api.Assertions.assertThatThrownBy;
 import java.nio.ByteBuffer;
 import java.util.Arrays;
 import java.util.Random;
+import java.util.stream.IntStream;
+import org.apache.iceberg.ManifestBitmap;
+import org.apache.iceberg.mumbling.MumblingBitmap.Mutable;
 import org.junit.jupiter.api.Test;
 
 class TestMumblingBitmap {
@@ -776,6 +779,443 @@ class TestMumblingBitmap {
     assertThat(serialized.slice(containersOffset + 35, 2)).isEqualTo(sparse(3, 
7).buffer());
   }
 
+  @Test
+  public void testMergeSparseIntoSparse() {
+    int[] positions = new int[] {0, 10, 20, 255};
+    int[] otherPositions = new int[] {0, 15, 20, 30, 255};
+
+    Mutable bitmap = bitmap(sparse(positions)).asMutable();
+    bitmap.merge(bitmap(sparse(otherPositions)));
+
+    assertThat(bitmap.cardinality()).isEqualTo(6);
+    assertThat(bitmap.descriptorArray()).containsExactly(6);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, otherPositions);
+  }
+
+  @Test
+  public void testMergeSparseIntoSparseConvertsToDense() {
+    int[] positions = range(0, 20);
+    int[] otherPositions = range(10, 40);
+
+    Mutable bitmap = bitmap(sparse(positions)).asMutable();
+    bitmap.merge(bitmap(sparse(otherPositions)));
+
+    assertThat(bitmap.cardinality()).isEqualTo(40);
+    assertThat(bitmap.descriptorArray()).containsExactly(32);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, otherPositions);
+  }
+
+  @Test
+  public void testMergeSparseIntoDense() {
+    int[] otherPositions = new int[] {0, 15, 40, 100, 255};
+
+    Mutable bitmap = bitmap(dense(DENSE_CONTAINER)).asMutable();
+    bitmap.merge(bitmap(sparse(otherPositions)));
+
+    assertThat(bitmap.cardinality()).isEqualTo(35);
+    assertThat(bitmap.descriptorArray()).containsExactly(32);
+    assertSet(bitmap, 0, DENSE_CONTAINER);
+    assertSet(bitmap, 0, otherPositions);
+  }
+
+  @Test
+  public void testMergeDenseIntoSparse() {
+    int[] positions = new int[] {0, 15, 40, 100, 255};
+
+    Mutable bitmap = bitmap(sparse(positions)).asMutable();
+    bitmap.merge(bitmap(dense(DENSE_CONTAINER)));
+
+    assertThat(bitmap.cardinality()).isEqualTo(35);
+    assertThat(bitmap.descriptorArray()).containsExactly(32);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, DENSE_CONTAINER);
+  }
+
+  @Test
+  public void testMergeDenseIntoDense() {
+    // positions 48-79
+    byte[] otherDense = new byte[32];
+    otherDense[6] = (byte) 0xFF;
+    otherDense[7] = (byte) 0xFF;
+    otherDense[8] = (byte) 0xFF;
+    otherDense[9] = (byte) 0xFF;
+
+    Mutable bitmap = bitmap(dense(DENSE_CONTAINER)).asMutable();
+    bitmap.merge(bitmap(dense(otherDense)));
+
+    assertThat(bitmap.cardinality()).isEqualTo(56);
+    assertThat(bitmap.descriptorArray()).containsExactly(32);
+    assertSet(bitmap, 0, DENSE_CONTAINER);
+    assertSet(bitmap, 0, otherDense);
+  }
+
+  @Test
+  public void testMergeSparseIntoSparseWithDenseArray() {
+    int[] positions = range(0, 31);
+    int[] otherPositions = new int[] {0, 255};
+
+    Mutable bitmap = bitmap(sparse(positions)).asMutable();
+    assertThat(bitmap.set(0)).as("Converts to dense but does not set a new 
bit").isFalse();
+
+    bitmap.merge(bitmap(sparse(otherPositions)));
+
+    assertThat(bitmap.cardinality()).isEqualTo(32);
+    assertThat(bitmap.descriptorArray()).containsExactly(32);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, otherPositions);
+  }
+
+  @Test
+  public void testMergeSparseWithDenseArrayIntoEmpty() {
+    int[] otherPositions = range(0, 31);
+
+    Mutable other = bitmap(sparse(otherPositions)).asMutable();
+    assertThat(other.set(0)).as("Converts to dense but does not set a new 
bit").isFalse();
+
+    Mutable bitmap = MumblingBitmap.newMutableBitmap();
+    bitmap.merge(other);
+
+    assertThat(bitmap.cardinality()).isEqualTo(31);
+    assertThat(bitmap.descriptorArray()).containsExactly(31);
+    assertSet(bitmap, 0, otherPositions);
+  }
+
+  @Test
+  public void testMergeSparseWithDenseArrayIntoSparse() {
+    int[] positions = new int[] {0, 30};
+    int[] otherPositions = range(0, 31);
+
+    Mutable other = bitmap(sparse(otherPositions)).asMutable();
+    assertThat(other.set(0)).as("Converts to dense but does not set a new 
bit").isFalse();
+
+    Mutable bitmap = bitmap(sparse(positions)).asMutable();
+    bitmap.merge(other);
+
+    assertThat(bitmap.cardinality()).isEqualTo(31);
+    assertThat(bitmap.descriptorArray()).containsExactly(31);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, otherPositions);
+  }
+
+  @Test
+  public void testMergeDenseIntoSparseWithDenseArray() {
+    int[] positions = range(0, 31);
+
+    Mutable bitmap = bitmap(sparse(positions)).asMutable();
+    assertThat(bitmap.set(0)).as("Converts to dense but does not set a new 
bit").isFalse();
+
+    bitmap.merge(bitmap(dense(DENSE_CONTAINER)));
+
+    assertThat(bitmap.cardinality()).isEqualTo(48);
+    assertThat(bitmap.descriptorArray()).containsExactly(32);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, DENSE_CONTAINER);
+  }
+
+  @Test
+  public void testMergeSparseWithDenseArrayIntoSparseWithDenseArray() {
+    int[] positions = range(0, 31);
+    int[] otherPositions = range(1, 32);
+
+    Mutable bitmap = bitmap(sparse(positions)).asMutable();
+    assertThat(bitmap.set(0)).as("Converts to dense but does not set a new 
bit").isFalse();
+
+    Mutable other = bitmap(sparse(otherPositions)).asMutable();
+    assertThat(other.set(1)).as("Converts to dense but does not set a new 
bit").isFalse();
+
+    bitmap.merge(other);
+
+    assertThat(bitmap.cardinality()).isEqualTo(32);
+    assertThat(bitmap.descriptorArray()).containsExactly(32);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, otherPositions);
+  }
+
+  @Test
+  public void testMergeSparseWithDenseArrayIntoDense() {
+    int[] otherPositions = range(0, 31);
+
+    Mutable other = bitmap(sparse(otherPositions)).asMutable();
+    assertThat(other.set(0)).as("Converts to dense but does not set a new 
bit").isFalse();
+
+    Mutable bitmap = bitmap(dense(DENSE_CONTAINER)).asMutable();
+    bitmap.merge(other);
+
+    assertThat(bitmap.cardinality()).isEqualTo(48);
+    assertThat(bitmap.descriptorArray()).containsExactly(32);
+    assertSet(bitmap, 0, DENSE_CONTAINER);
+    assertSet(bitmap, 0, otherPositions);
+  }
+
+  @Test
+  public void testMergeEmptyContainers() {
+    int[] positions = new int[] {0, 10, 255};
+    int[] otherPositions = new int[] {0, 15, 255};
+
+    Mutable bitmap =
+        bitmap(sparse(), dense(DENSE_CONTAINER), sparse(positions), 
sparse()).asMutable();
+    bitmap.merge(bitmap(sparse(otherPositions), sparse(), sparse(), 
dense(DENSE_CONTAINER)));
+
+    assertThat(bitmap.cardinality()).isEqualTo(70);
+    assertThat(bitmap.descriptorArray()).containsExactly(3, 32, 3, 32);
+    assertSet(bitmap, 0, otherPositions);
+    assertSet(bitmap, 1, DENSE_CONTAINER);
+    assertSet(bitmap, 2, positions);
+    assertSet(bitmap, 3, DENSE_CONTAINER);
+  }
+
+  @Test
+  public void testMergeIntoEmptyBitmap() {
+    int[] otherPositions = new int[] {0, 15, 255};
+
+    Mutable bitmap = MumblingBitmap.newMutableBitmap();
+    bitmap.merge(bitmap(sparse(otherPositions), dense(DENSE_CONTAINER)));
+
+    assertThat(bitmap.cardinality()).isEqualTo(35);
+    assertThat(bitmap.descriptorArray()).containsExactly(3, 32);
+    assertSet(bitmap, 0, otherPositions);
+    assertSet(bitmap, 1, DENSE_CONTAINER);
+  }
+
+  @Test
+  public void testMergeEmptyBitmap() {
+    int[] positions = new int[] {0, 10, 255};
+
+    Mutable bitmap = bitmap(sparse(positions), 
dense(DENSE_CONTAINER)).asMutable();
+    bitmap.merge(MumblingBitmap.newMutableBitmap());
+
+    assertThat(bitmap.cardinality()).isEqualTo(35);
+    assertThat(bitmap.descriptorArray()).containsExactly(3, 32);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 1, DENSE_CONTAINER);
+  }
+
+  @Test
+  public void testMergeWithMoreContainers() {
+    int[] positions = new int[] {0, 10, 255};
+    int[] otherPositions = new int[] {0, 15, 255};
+
+    Mutable bitmap = bitmap(sparse(positions)).asMutable();
+    bitmap.merge(
+        bitmap(sparse(otherPositions), dense(DENSE_CONTAINER), sparse(), 
sparse(otherPositions)));
+
+    assertThat(bitmap.cardinality()).isEqualTo(39);
+    assertThat(bitmap.descriptorArray()).containsExactly(4, 32, 0, 3);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, otherPositions);
+    assertSet(bitmap, 1, DENSE_CONTAINER);
+    assertSet(bitmap, 3, otherPositions);
+  }
+
+  @Test
+  public void testMergeArgumentWithFewerContainers() {
+    int[] positions = new int[] {0, 10, 255};
+    int[] otherPositions = new int[] {0, 15, 255};
+
+    Mutable bitmap =
+        bitmap(sparse(positions), dense(DENSE_CONTAINER), sparse(), 
sparse(positions)).asMutable();
+    bitmap.merge(bitmap(sparse(otherPositions)));
+
+    assertThat(bitmap.cardinality()).isEqualTo(39);
+    assertThat(bitmap.descriptorArray()).containsExactly(4, 32, 0, 3);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, otherPositions);
+    assertSet(bitmap, 1, DENSE_CONTAINER);
+    assertSet(bitmap, 3, positions);
+  }
+
+  @Test
+  public void testMergeWithEmptyContainerGaps() {
+    int[] positions = new int[] {0, 10, 255};
+    int[] otherPositions = new int[] {0, 15, 255};
+
+    Mutable bitmap =
+        bitmap(sparse(positions), sparse(), sparse(), 
dense(DENSE_CONTAINER)).asMutable();
+    bitmap.merge(
+        bitmap(sparse(), sparse(otherPositions), sparse(), sparse(), 
dense(DENSE_CONTAINER)));
+
+    assertThat(bitmap.cardinality()).isEqualTo(70);
+    assertThat(bitmap.descriptorArray()).containsExactly(3, 3, 0, 32, 32);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 1, otherPositions);
+    assertSet(bitmap, 3, DENSE_CONTAINER);
+    assertSet(bitmap, 4, DENSE_CONTAINER);
+  }
+
+  @Test
+  public void testMergeIntoOverlayAndBaseContainers() {
+    int[] positions = new int[] {0, 10, 255};
+    int[] otherPositions = new int[] {0, 15, 255};
+
+    Mutable bitmap = bitmap(sparse(positions), 
dense(DENSE_CONTAINER)).asMutable();
+    assertThat(bitmap.set(5)).isTrue(); // creates an overlay for container 0 
only
+
+    bitmap.merge(bitmap(sparse(otherPositions), sparse(otherPositions)));
+
+    assertThat(bitmap.cardinality()).isEqualTo(39);
+    assertThat(bitmap.descriptorArray()).containsExactly(5, 32);
+    assertThat(bitmap.isSet(5)).isTrue();
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, otherPositions);
+    assertSet(bitmap, 1, DENSE_CONTAINER);
+    assertSet(bitmap, 1, otherPositions);
+  }
+
+  @Test
+  public void testMergeBitmapWithBufferOffset() {
+    int[] positions = new int[] {0, 10, 255};
+    int[] otherPositions = new int[] {0, 15, 255};
+
+    // positions 48-79
+    byte[] otherDense = new byte[32];
+    otherDense[6] = (byte) 0xFF;
+    otherDense[7] = (byte) 0xFF;
+    otherDense[8] = (byte) 0xFF;
+    otherDense[9] = (byte) 0xFF;
+
+    Mutable bitmap = bitmap(sparse(positions), 
dense(DENSE_CONTAINER)).asMutable();
+    bitmap.merge(new MumblingBitmap(relocate(build(sparse(otherPositions), 
dense(otherDense)))));
+
+    assertThat(bitmap.cardinality()).isEqualTo(60);
+    assertThat(bitmap.descriptorArray()).containsExactly(4, 32);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, otherPositions);
+    assertSet(bitmap, 1, DENSE_CONTAINER);
+    assertSet(bitmap, 1, otherDense);
+  }
+
+  @Test
+  public void testMergeMutableWithoutOverlays() {
+    int[] positions = new int[] {0, 10, 255};
+    int[] otherPositions = new int[] {0, 15, 255};
+
+    // positions 48-79
+    byte[] otherDense = new byte[32];
+    otherDense[6] = (byte) 0xFF;
+    otherDense[7] = (byte) 0xFF;
+    otherDense[8] = (byte) 0xFF;
+    otherDense[9] = (byte) 0xFF;
+
+    Mutable other = bitmap(sparse(otherPositions), 
dense(otherDense)).asMutable();
+
+    Mutable bitmap = bitmap(sparse(positions), 
dense(DENSE_CONTAINER)).asMutable();
+    bitmap.merge(other);
+
+    assertThat(bitmap.cardinality()).isEqualTo(60);
+    assertThat(bitmap.descriptorArray()).containsExactly(4, 32);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, otherPositions);
+    assertSet(bitmap, 1, DENSE_CONTAINER);
+    assertSet(bitmap, 1, otherDense);
+  }
+
+  @Test
+  public void testMergeMutableWithOverlays() {
+    int[] positions = new int[] {0, 10, 255};
+    int[] otherPositions = new int[] {0, 15, 255};
+    int[] otherDensePositions = range(48, 80);
+
+    Mutable other = MumblingBitmap.newMutableBitmap();
+    for (int pos : otherPositions) {
+      assertThat(other.set(pos)).isTrue();
+    }
+
+    for (int pos : otherDensePositions) {
+      assertThat(other.set(256 + pos)).isTrue();
+    }
+
+    Mutable bitmap = bitmap(sparse(positions), 
dense(DENSE_CONTAINER)).asMutable();
+    bitmap.merge(other);
+
+    assertThat(bitmap.cardinality()).isEqualTo(60);
+    assertThat(bitmap.descriptorArray()).containsExactly(4, 32);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, otherPositions);
+    assertSet(bitmap, 1, DENSE_CONTAINER);
+    assertSet(bitmap, 1, otherDensePositions);
+  }
+
+  @Test
+  public void testMergeMutableWithMixedOverlays() {
+    int[] positions = new int[] {0, 10, 255};
+    int[] otherPositions = new int[] {0, 15, 255};
+
+    // positions 48-79
+    byte[] otherDense = new byte[32];
+    otherDense[6] = (byte) 0xFF;
+    otherDense[7] = (byte) 0xFF;
+    otherDense[8] = (byte) 0xFF;
+    otherDense[9] = (byte) 0xFF;
+
+    Mutable other = bitmap(sparse(otherPositions), 
dense(otherDense)).asMutable();
+    assertThat(other.set(356)).isTrue(); // creates an overlay for container 1 
only
+
+    Mutable bitmap = bitmap(sparse(positions), 
dense(DENSE_CONTAINER)).asMutable();
+    bitmap.merge(other);
+
+    assertThat(bitmap.cardinality()).isEqualTo(61);
+    assertThat(bitmap.descriptorArray()).containsExactly(4, 32);
+    assertSet(bitmap, 0, positions);
+    assertSet(bitmap, 0, otherPositions);
+    assertSet(bitmap, 1, DENSE_CONTAINER);
+    assertSet(bitmap, 1, otherDense);
+    assertThat(bitmap.isSet(356)).isTrue();
+  }
+
+  @Test
+  public void testMergeDoesNotShareContainers() {
+    int[] otherPositions = range(0, 31);
+
+    Mutable other = bitmap(sparse(otherPositions), 
dense(DENSE_CONTAINER)).asMutable();
+    assertThat(other.set(0)).as("Converts to dense but does not set a new 
bit").isFalse();
+
+    // merging into empty containers copies the sparse and dense arrays
+    Mutable bitmap = MumblingBitmap.newMutableBitmap();
+    bitmap.merge(other);
+    assertThat(bitmap.cardinality()).isEqualTo(63);
+    assertThat(bitmap.descriptorArray()).containsExactly(31, 32);
+
+    assertThat(bitmap.set(100)).isTrue();
+    assertThat(bitmap.set(356)).isTrue();
+    assertThat(bitmap.cardinality()).isEqualTo(65);
+    assertThat(other.isSet(100)).isFalse();
+    assertThat(other.isSet(356)).isFalse();
+    assertThat(other.cardinality()).isEqualTo(63);
+
+    assertThat(other.set(101)).isTrue();
+    assertThat(other.set(357)).isTrue();
+    assertThat(other.cardinality()).isEqualTo(65);
+    assertThat(bitmap.isSet(101)).isFalse();
+    assertThat(bitmap.isSet(357)).isFalse();
+    assertThat(bitmap.cardinality()).isEqualTo(65);
+  }
+
+  /** Returns the positions from {@code start} (inclusive) to {@code end} 
(exclusive). */
+  private static int[] range(int start, int end) {
+    return IntStream.range(start, end).toArray();
+  }
+
+  private static void assertSet(ManifestBitmap bitmap, int containerIndex, 
int[] positions) {
+    for (int pos : positions) {
+      assertThat(bitmap.isSet(containerIndex * 256 + pos))
+          .as("Position %s in container %s", pos, containerIndex)
+          .isTrue();
+    }
+  }
+
+  /** Asserts that the positions set in a dense container's bytes are set in 
the bitmap. */
+  private static void assertSet(ManifestBitmap bitmap, int containerIndex, 
byte[] dense) {
+    for (int pos = 0; pos < 256; pos += 1) {
+      if ((dense[pos >>> 3] & (0x80 >>> (pos & 0b111))) != 0) {
+        assertThat(bitmap.isSet(containerIndex * 256 + pos))
+            .as("Position %s in container %s", pos, containerIndex)
+            .isTrue();
+      }
+    }
+  }
+
   /** Copy into a buffer with garbage padding outside of position and limit. */
   private static ByteBuffer relocate(ByteBuffer serialized) {
     int prefix = 17;

Reply via email to