You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by gi...@apache.org on 2026/10/23 20:04:17 UTC

(iceberg-rust) branch gh-pages updated: deploy: d2679a029f50a25f00850731650d7fbab1b03ca8

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

github-actions[bot] pushed a commit to branch gh-pages
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git


The following commit(s) were added to refs/heads/gh-pages by this push:
     new bd1b3cd75 deploy: d2679a029f50a25f00850731650d7fbab1b03ca8
bd1b3cd75 is described below

commit bd1b3cd7539244583c2d9c7c4e99bce8d5c836fe
Author: github-merge-queue[bot] <gi...@users.noreply.github.com>
AuthorDate: Fri Oct 23 20:04:11 2026 +0000

    deploy: d2679a029f50a25f00850731650d7fbab1b03ca8
---
 api/iceberg/delete_vector/index.html               |    2 +-
 api/iceberg/delete_vector/struct.DeleteVector.html |    6 +-
 .../delete_vector/struct.DeleteVectorIterator.html |    8 +-
 .../arrow/reader/positional_deletes.rs.html        | 2295 +++++++++++---------
 api/src/iceberg/delete_vector.rs.html              | 1223 +++++++----
 5 files changed, 2054 insertions(+), 1480 deletions(-)

diff --git a/api/iceberg/delete_vector/index.html b/api/iceberg/delete_vector/index.html
index 20a148353..3d269b0da 100644
--- a/api/iceberg/delete_vector/index.html
+++ b/api/iceberg/delete_vector/index.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="Deletion vectors: the deleted row positions of one data file, as a Puffin `deletion-vector-v1` blob decodes to. Public so an engine that reads Parquet with a reader of its own can still decode the vectors Iceberg writes."><title>iceberg::delete_vector - Rust</title><script>if(window.loca [...]
+<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="Deletion vectors: the deleted row positions of one data file, as a Puffin `deletion-vector-v1` blob decodes to. Public so an engine that reads Parquet with a reader of its own can still decode the vectors Iceberg writes."><title>iceberg::delete_vector - Rust</title><script>if(window.loca [...]
 <code>deletion-vector-v1</code> blob decodes to. Public so an engine that reads
 Parquet with a reader of its own can still decode the vectors Iceberg
 writes.</p>
diff --git a/api/iceberg/delete_vector/struct.DeleteVector.html b/api/iceberg/delete_vector/struct.DeleteVector.html
index 961405e9a..7e1ff6edb 100644
--- a/api/iceberg/delete_vector/struct.DeleteVector.html
+++ b/api/iceberg/delete_vector/struct.DeleteVector.html
@@ -1,6 +1,6 @@
 <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="The deleted row positions of one data file: a set of 64-bit positions, kept as a `RoaringTreemap`."><title>DeleteVector in iceberg::delete_vector - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.wo [...]
 kept as a [<code>RoaringTreemap</code>].</p>
-</div></details><h2 id="implementations" class="section-header">Implementations<a href="#implementations" class="anchor">§</a></h2><div id="implementations-list"><details class="toggle implementors-toggle" open><summary><section id="impl-DeleteVector" class="impl"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#48-138">Source</a><a href="#impl-DeleteVector" class="anchor">§</a><h3 class="code-header">impl <a class="struct" href="struct.DeleteVector.html" title="str [...]
+</div></details><h2 id="implementations" class="section-header">Implementations<a href="#implementations" class="anchor">§</a></h2><div id="implementations-list"><details class="toggle implementors-toggle" open><summary><section id="impl-DeleteVector" class="impl"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#48-152">Source</a><a href="#impl-DeleteVector" class="anchor">§</a><h3 class="code-header">impl <a class="struct" href="struct.DeleteVector.html" title="str [...]
 </div></details><details class="toggle method-toggle" open><summary><section id="method.iter" class="method"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#57-60">Source</a><h4 class="code-header">pub fn <a href="#method.iter" class="fn">iter</a>(&amp;self) -&gt; <a class="struct" href="struct.DeleteVectorIterator.html" title="struct iceberg::delete_vector::DeleteVectorIterator">DeleteVectorIterator</a>&lt;'_&gt; <a href="#" class="tooltip" data-notable-ty="Delete [...]
 </div></details><details class="toggle method-toggle" open><summary><section id="method.insert" class="method"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#63-65">Source</a><h4 class="code-header">pub fn <a href="#method.insert" class="fn">insert</a>(&amp;mut self, pos: <a class="primitive" href="https://doc.rust-lang.org/nightly/std/primitive.u64.html">u64</a>) -&gt; <a class="primitive" href="https://doc.rust-lang.org/nightly/std/primitive.bool.html">bool</a>< [...]
 </div></details><details class="toggle method-toggle" open><summary><section id="method.insert_positions" class="method"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#74-83">Source</a><h4 class="code-header">pub fn <a href="#method.insert_positions" class="fn">insert_positions</a>(&amp;mut self, positions: &amp;[<a class="primitive" href="https://doc.rust-lang.org/nightly/std/primitive.u64.html">u64</a>]) -&gt; <a class="type" href="../type.Result.html" title="ty [...]
@@ -9,7 +9,7 @@ kept as a [<code>RoaringTreemap</code>].</p>
 <p>Returns an error if the precondition is not met.</p>
 </div></details><details class="toggle method-toggle" open><summary><section id="method.len" class="method"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#86-88">Source</a><h4 class="code-header">pub fn <a href="#method.len" class="fn">len</a>(&amp;self) -&gt; <a class="primitive" href="https://doc.rust-lang.org/nightly/std/primitive.u64.html">u64</a></h4></section></summary><div class="docblock"><p>How many positions are deleted.</p>
 </div></details><details class="toggle method-toggle" open><summary><section id="method.is_empty" class="method"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#91-93">Source</a><h4 class="code-header">pub fn <a href="#method.is_empty" class="fn">is_empty</a>(&amp;self) -&gt; <a class="primitive" href="https://doc.rust-lang.org/nightly/std/primitive.bool.html">bool</a></h4></section></summary><div class="docblock"><p>Whether no position is deleted.</p>
-</div></details><details class="toggle method-toggle" open><summary><section id="method.deserialize" class="method"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#115-137">Source</a><h4 class="code-header">pub fn <a href="#method.deserialize" class="fn">deserialize</a>(blob: &amp;[<a class="primitive" href="https://doc.rust-lang.org/nightly/std/primitive.u8.html">u8</a>]) -&gt; <a class="type" href="../type.Result.html" title="type iceberg::Result">Result</a>&lt;S [...]
+</div></details><details class="toggle method-toggle" open><summary><section id="method.deserialize" class="method"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#129-151">Source</a><h4 class="code-header">pub fn <a href="#method.deserialize" class="fn">deserialize</a>(blob: &amp;[<a class="primitive" href="https://doc.rust-lang.org/nightly/std/primitive.u8.html">u8</a>]) -&gt; <a class="type" href="../type.Result.html" title="type iceberg::Result">Result</a>&lt;S [...]
 <p>The layout, defined by the Iceberg Puffin spec and matching Iceberg-Java’s
 <code>BitmapPositionDeleteIndex</code>, is:</p>
 <div class="example-wrap"><pre class="language-text"><code>[length: u32 big-endian][magic: D1 D3 39 64][vector][crc: u32 big-endian]</code></pre></div>
@@ -22,7 +22,7 @@ comparison of the keys, one bitmap per key.</p>
 prefix or CRC does not match, the magic is wrong, the roaring bitmap count exceeds the
 portable format’s maximum, the roaring directory’s keys are not ordered by unsigned
 comparison, or the roaring payload fails to decode.</p>
-</div></details></div></details></div><h2 id="trait-implementations" class="section-header">Trait Implementations<a href="#trait-implementations" class="anchor">§</a></h2><div id="trait-implementations-list"><details class="toggle implementors-toggle" open><summary><section id="impl-BitOrAssign-for-DeleteVector" class="impl"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#297-301">Source</a><a href="#impl-BitOrAssign-for-DeleteVector" class="anchor">§</a><h3 class= [...]
+</div></details></div></details></div><h2 id="trait-implementations" class="section-header">Trait Implementations<a href="#trait-implementations" class="anchor">§</a></h2><div id="trait-implementations-list"><details class="toggle implementors-toggle" open><summary><section id="impl-BitOrAssign-for-DeleteVector" class="impl"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#441-445">Source</a><a href="#impl-BitOrAssign-for-DeleteVector" class="anchor">§</a><h3 class= [...]
     T: 'static + ?<a class="trait" href="https://doc.rust-lang.org/nightly/core/marker/trait.Sized.html" title="trait core::marker::Sized">Sized</a>,</div></h3></section></summary><div class="impl-items"><details class="toggle method-toggle" open><summary><section id="method.type_id" class="method trait-impl"><a class="src rightside" href="https://doc.rust-lang.org/nightly/src/core/any.rs.html#142">Source</a><a href="#method.type_id" class="anchor">§</a><h4 class="code-header">fn <a href [...]
     T: CastFrom&lt;U&gt;,</div></h4></section></summary><div class='docblock'>Casts <code>self</code> to type <code>T</code>. The <a href="https://doc.rust-lang.org/reference/expressions/operator-expr.html#semantics">semantics of numeric casting</a> with the <code>as</code> operator are followed, so <code>&lt;T as As&gt;::as_::&lt;U&gt;</code> can be used in the same way as <code>T as U</code> for numeric conversions. <a>Read more</a></div></details></div></details><details class="toggle [...]
     T: <a class="trait" href="https://doc.rust-lang.org/nightly/core/any/trait.Any.html" title="trait core::any::Any">Any</a>,</div></h3></section></summary><div class="impl-items"><section id="method.as_any" class="method trait-impl"><a href="#method.as_any" class="anchor">§</a><h4 class="code-header">fn <a class="fn">as_any</a>(&amp;self) -&gt; &amp;(dyn <a class="trait" href="https://doc.rust-lang.org/nightly/core/any/trait.Any.html" title="trait core::any::Any">Any</a> + 'static)</h4 [...]
diff --git a/api/iceberg/delete_vector/struct.DeleteVectorIterator.html b/api/iceberg/delete_vector/struct.DeleteVectorIterator.html
index 197e3ba11..6fdbbc250 100644
--- a/api/iceberg/delete_vector/struct.DeleteVectorIterator.html
+++ b/api/iceberg/delete_vector/struct.DeleteVectorIterator.html
@@ -1,7 +1,7 @@
-<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="Iterates the deleted positions of a `DeleteVector` in ascending order."><title>DeleteVectorIterator in iceberg::delete_vector - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic- [...]
-</div></details><h2 id="implementations" class="section-header">Implementations<a href="#implementations" class="anchor">§</a></h2><div id="implementations-list"><details class="toggle implementors-toggle" open><summary><section id="impl-DeleteVectorIterator%3C'_%3E" class="impl"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#271-295">Source</a><a href="#impl-DeleteVectorIterator%3C'_%3E" class="anchor">§</a><h3 class="code-header">impl <a class="struct" href="str [...]
-at or after it. Does nothing before the first position is yielded.</p>
-</div></details></div></details></div><h2 id="trait-implementations" class="section-header">Trait Implementations<a href="#trait-implementations" class="anchor">§</a></h2><div id="trait-implementations-list"><details class="toggle implementors-toggle" open><summary><section id="impl-Iterator-for-DeleteVectorIterator%3C'_%3E" class="impl"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#248-269">Source</a><a href="#impl-Iterator-for-DeleteVectorIterator%3C'_%3E" clas [...]
+<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="Iterates the deleted positions of a `DeleteVector` in ascending order."><title>DeleteVectorIterator in iceberg::delete_vector - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic- [...]
+</div></details><h2 id="implementations" class="section-header">Implementations<a href="#implementations" class="anchor">§</a></h2><div id="implementations-list"><details class="toggle implementors-toggle" open><summary><section id="impl-DeleteVectorIterator%3C'_%3E" class="impl"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#276-310">Source</a><a href="#impl-DeleteVectorIterator%3C'_%3E" class="anchor">§</a><h3 class="code-header">impl <a class="struct" href="str [...]
+at or after it. A <code>pos</code> below the next position does nothing.</p>
+</div></details></div></details></div><h2 id="trait-implementations" class="section-header">Trait Implementations<a href="#trait-implementations" class="anchor">§</a></h2><div id="trait-implementations-list"><details class="toggle implementors-toggle" open><summary><section id="impl-Iterator-for-DeleteVectorIterator%3C'_%3E" class="impl"><a class="src rightside" href="../../src/iceberg/delete_vector.rs.html#253-274">Source</a><a href="#impl-Iterator-for-DeleteVectorIterator%3C'_%3E" clas [...]
     &amp;mut self,
 ) -&gt; <a class="enum" href="https://doc.rust-lang.org/nightly/core/result/enum.Result.html" title="enum core::result::Result">Result</a>&lt;[Self::<a class="associatedtype" href="https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html#associatedtype.Item" title="type core::iter::traits::iterator::Iterator::Item">Item</a>; <a class="primitive" href="https://doc.rust-lang.org/nightly/std/primitive.array.html">N</a>], <a class="struct" href="https://doc.rust-lang.o [...]
     Self: <a class="trait" href="https://doc.rust-lang.org/nightly/core/marker/trait.Sized.html" title="trait core::marker::Sized">Sized</a>,</div></h4></section></summary><span class="item-info"><div class="stab unstable"><span class="emoji">🔬</span><span>This is a nightly-only experimental API. (<code>iter_next_chunk</code>)</span></div></span><div class='docblock'>Advances the iterator and returns an array containing the next <code>N</code> values. <a href="https://doc.rust-lang.org/n [...]
diff --git a/api/src/iceberg/arrow/reader/positional_deletes.rs.html b/api/src/iceberg/arrow/reader/positional_deletes.rs.html
index 6167901a2..3a22b39a3 100644
--- a/api/src/iceberg/arrow/reader/positional_deletes.rs.html
+++ b/api/src/iceberg/arrow/reader/positional_deletes.rs.html
@@ -40,1087 +40,1256 @@
 <a href=#40 id=40 data-nosnippet>40</a>        <span class="kw">let </span><span class="kw-2">mut </span>results: Vec&lt;RowSelector&gt; = Vec::new();
 <a href=#41 id=41 data-nosnippet>41</a>        <span class="kw">let </span><span class="kw-2">mut </span>selected_row_groups_idx = <span class="number">0</span>;
 <a href=#42 id=42 data-nosnippet>42</a>        <span class="kw">let </span><span class="kw-2">mut </span>current_row_group_base_idx: u64 = <span class="number">0</span>;
-<a href=#43 id=43 data-nosnippet>43</a>        <span class="kw">let </span><span class="kw-2">mut </span>delete_vector_iter = positional_deletes.iter();
-<a href=#44 id=44 data-nosnippet>44</a>        <span class="kw">let </span><span class="kw-2">mut </span>next_deleted_row_idx_opt = delete_vector_iter.next();
-<a href=#45 id=45 data-nosnippet>45</a>
-<a href=#46 id=46 data-nosnippet>46</a>        <span class="kw">for </span>(idx, row_group_metadata) <span class="kw">in </span>row_group_metadata_list.iter().enumerate() {
-<a href=#47 id=47 data-nosnippet>47</a>            <span class="kw">let </span>row_group_num_rows = row_group_metadata.num_rows() <span class="kw">as </span>u64;
-<a href=#48 id=48 data-nosnippet>48</a>            <span class="kw">let </span>next_row_group_base_idx = current_row_group_base_idx + row_group_num_rows;
-<a href=#49 id=49 data-nosnippet>49</a>
-<a href=#50 id=50 data-nosnippet>50</a>            <span class="comment">// if row group selection is enabled,
-<a href=#51 id=51 data-nosnippet>51</a>            </span><span class="kw">if let </span><span class="prelude-val">Some</span>(selected_row_groups) = selected_row_groups {
-<a href=#52 id=52 data-nosnippet>52</a>                <span class="comment">// if we've consumed all the selected row groups, we're done
-<a href=#53 id=53 data-nosnippet>53</a>                </span><span class="kw">if </span>selected_row_groups_idx == selected_row_groups.len() {
-<a href=#54 id=54 data-nosnippet>54</a>                    <span class="kw">break</span>;
-<a href=#55 id=55 data-nosnippet>55</a>                }
-<a href=#56 id=56 data-nosnippet>56</a>
-<a href=#57 id=57 data-nosnippet>57</a>                <span class="kw">if </span>idx == selected_row_groups[selected_row_groups_idx] {
-<a href=#58 id=58 data-nosnippet>58</a>                    <span class="comment">// we're in a selected row group. Increment selected_row_groups_idx
-<a href=#59 id=59 data-nosnippet>59</a>                    // so that next time around the for loop we're looking for the next
-<a href=#60 id=60 data-nosnippet>60</a>                    // selected row group
-<a href=#61 id=61 data-nosnippet>61</a>                    </span>selected_row_groups_idx += <span class="number">1</span>;
-<a href=#62 id=62 data-nosnippet>62</a>                } <span class="kw">else </span>{
-<a href=#63 id=63 data-nosnippet>63</a>                    <span class="comment">// Advance iterator past all deletes in the skipped row group.
-<a href=#64 id=64 data-nosnippet>64</a>                    // advance_to() positions the iterator to the first delete &gt;= next_row_group_base_idx.
-<a href=#65 id=65 data-nosnippet>65</a>                    // However, if our cached next_deleted_row_idx_opt is in the skipped range,
-<a href=#66 id=66 data-nosnippet>66</a>                    // we need to call next() to update the cache with the newly positioned value.
-<a href=#67 id=67 data-nosnippet>67</a>                    </span>delete_vector_iter.advance_to(next_row_group_base_idx);
-<a href=#68 id=68 data-nosnippet>68</a>                    <span class="comment">// Only update the cache if the cached value is stale (in the skipped range)
-<a href=#69 id=69 data-nosnippet>69</a>                    </span><span class="kw">if let </span><span class="prelude-val">Some</span>(cached_idx) = next_deleted_row_idx_opt
-<a href=#70 id=70 data-nosnippet>70</a>                        &amp;&amp; cached_idx &lt; next_row_group_base_idx
-<a href=#71 id=71 data-nosnippet>71</a>                    {
-<a href=#72 id=72 data-nosnippet>72</a>                        next_deleted_row_idx_opt = delete_vector_iter.next();
-<a href=#73 id=73 data-nosnippet>73</a>                    }
-<a href=#74 id=74 data-nosnippet>74</a>
-<a href=#75 id=75 data-nosnippet>75</a>                    <span class="comment">// still increment the current page base index but then skip to the next row group
-<a href=#76 id=76 data-nosnippet>76</a>                    // in the file
-<a href=#77 id=77 data-nosnippet>77</a>                    </span>current_row_group_base_idx += row_group_num_rows;
-<a href=#78 id=78 data-nosnippet>78</a>                    <span class="kw">continue</span>;
-<a href=#79 id=79 data-nosnippet>79</a>                }
-<a href=#80 id=80 data-nosnippet>80</a>            }
-<a href=#81 id=81 data-nosnippet>81</a>
-<a href=#82 id=82 data-nosnippet>82</a>            <span class="kw">let </span><span class="kw-2">mut </span>next_deleted_row_idx = <span class="kw">match </span>next_deleted_row_idx_opt {
-<a href=#83 id=83 data-nosnippet>83</a>                <span class="prelude-val">Some</span>(next_deleted_row_idx) =&gt; {
-<a href=#84 id=84 data-nosnippet>84</a>                    <span class="comment">// if the index of the next deleted row is beyond this row group, add a selection for
-<a href=#85 id=85 data-nosnippet>85</a>                    // the remainder of this row group and skip to the next row group
-<a href=#86 id=86 data-nosnippet>86</a>                    </span><span class="kw">if </span>next_deleted_row_idx &gt;= next_row_group_base_idx {
-<a href=#87 id=87 data-nosnippet>87</a>                        results.push(RowSelector::select(row_group_num_rows <span class="kw">as </span>usize));
-<a href=#88 id=88 data-nosnippet>88</a>                        current_row_group_base_idx += row_group_num_rows;
-<a href=#89 id=89 data-nosnippet>89</a>                        <span class="kw">continue</span>;
-<a href=#90 id=90 data-nosnippet>90</a>                    }
-<a href=#91 id=91 data-nosnippet>91</a>
-<a href=#92 id=92 data-nosnippet>92</a>                    next_deleted_row_idx
-<a href=#93 id=93 data-nosnippet>93</a>                }
-<a href=#94 id=94 data-nosnippet>94</a>
-<a href=#95 id=95 data-nosnippet>95</a>                <span class="comment">// If there are no more pos deletes, add a selector for the entirety of this row group.
-<a href=#96 id=96 data-nosnippet>96</a>                </span><span class="kw">_ </span>=&gt; {
-<a href=#97 id=97 data-nosnippet>97</a>                    results.push(RowSelector::select(row_group_num_rows <span class="kw">as </span>usize));
-<a href=#98 id=98 data-nosnippet>98</a>                    current_row_group_base_idx += row_group_num_rows;
-<a href=#99 id=99 data-nosnippet>99</a>                    <span class="kw">continue</span>;
+<a href=#43 id=43 data-nosnippet>43</a>        <span class="comment">// Deleted positions as half-open ranges, so a run of deletes costs one step rather than
+<a href=#44 id=44 data-nosnippet>44</a>        // one per row.
+<a href=#45 id=45 data-nosnippet>45</a>        </span><span class="kw">let </span><span class="kw-2">mut </span>deleted_ranges = positional_deletes.ranges();
+<a href=#46 id=46 data-nosnippet>46</a>        <span class="kw">let </span><span class="kw-2">mut </span>next_deleted_range = deleted_ranges.next();
+<a href=#47 id=47 data-nosnippet>47</a>
+<a href=#48 id=48 data-nosnippet>48</a>        <span class="kw">for </span>(idx, row_group_metadata) <span class="kw">in </span>row_group_metadata_list.iter().enumerate() {
+<a href=#49 id=49 data-nosnippet>49</a>            <span class="kw">let </span>row_group_num_rows = row_group_metadata.num_rows() <span class="kw">as </span>u64;
+<a href=#50 id=50 data-nosnippet>50</a>            <span class="kw">let </span>next_row_group_base_idx = current_row_group_base_idx + row_group_num_rows;
+<a href=#51 id=51 data-nosnippet>51</a>
+<a href=#52 id=52 data-nosnippet>52</a>            <span class="comment">// if row group selection is enabled,
+<a href=#53 id=53 data-nosnippet>53</a>            </span><span class="kw">if let </span><span class="prelude-val">Some</span>(selected_row_groups) = selected_row_groups {
+<a href=#54 id=54 data-nosnippet>54</a>                <span class="comment">// if we've consumed all the selected row groups, we're done
+<a href=#55 id=55 data-nosnippet>55</a>                </span><span class="kw">if </span>selected_row_groups_idx == selected_row_groups.len() {
+<a href=#56 id=56 data-nosnippet>56</a>                    <span class="kw">break</span>;
+<a href=#57 id=57 data-nosnippet>57</a>                }
+<a href=#58 id=58 data-nosnippet>58</a>
+<a href=#59 id=59 data-nosnippet>59</a>                <span class="kw">if </span>idx == selected_row_groups[selected_row_groups_idx] {
+<a href=#60 id=60 data-nosnippet>60</a>                    <span class="comment">// we're in a selected row group. Increment selected_row_groups_idx
+<a href=#61 id=61 data-nosnippet>61</a>                    // so that next time around the for loop we're looking for the next
+<a href=#62 id=62 data-nosnippet>62</a>                    // selected row group
+<a href=#63 id=63 data-nosnippet>63</a>                    </span>selected_row_groups_idx += <span class="number">1</span>;
+<a href=#64 id=64 data-nosnippet>64</a>                } <span class="kw">else </span>{
+<a href=#65 id=65 data-nosnippet>65</a>                    <span class="comment">// Skip the deletes in this row group without walking them. A range that
+<a href=#66 id=66 data-nosnippet>66</a>                    // continues into the next row group is cut to start there.
+<a href=#67 id=67 data-nosnippet>67</a>                    </span><span class="kw">match </span>next_deleted_range {
+<a href=#68 id=68 data-nosnippet>68</a>                        <span class="prelude-val">Some</span>((<span class="kw">_</span>, end)) <span class="kw">if </span>end &lt;= next_row_group_base_idx =&gt; {
+<a href=#69 id=69 data-nosnippet>69</a>                            deleted_ranges.skip_to(next_row_group_base_idx);
+<a href=#70 id=70 data-nosnippet>70</a>                            next_deleted_range = deleted_ranges.next();
+<a href=#71 id=71 data-nosnippet>71</a>                        }
+<a href=#72 id=72 data-nosnippet>72</a>                        <span class="prelude-val">Some</span>((start, end)) <span class="kw">if </span>start &lt; next_row_group_base_idx =&gt; {
+<a href=#73 id=73 data-nosnippet>73</a>                            next_deleted_range = <span class="prelude-val">Some</span>((next_row_group_base_idx, end));
+<a href=#74 id=74 data-nosnippet>74</a>                        }
+<a href=#75 id=75 data-nosnippet>75</a>                        <span class="kw">_ </span>=&gt; {}
+<a href=#76 id=76 data-nosnippet>76</a>                    }
+<a href=#77 id=77 data-nosnippet>77</a>
+<a href=#78 id=78 data-nosnippet>78</a>                    <span class="comment">// still increment the current page base index but then skip to the next row group
+<a href=#79 id=79 data-nosnippet>79</a>                    // in the file
+<a href=#80 id=80 data-nosnippet>80</a>                    </span>current_row_group_base_idx += row_group_num_rows;
+<a href=#81 id=81 data-nosnippet>81</a>                    <span class="kw">continue</span>;
+<a href=#82 id=82 data-nosnippet>82</a>                }
+<a href=#83 id=83 data-nosnippet>83</a>            }
+<a href=#84 id=84 data-nosnippet>84</a>
+<a href=#85 id=85 data-nosnippet>85</a>            <span class="kw">let </span><span class="kw-2">mut </span>current_idx = current_row_group_base_idx;
+<a href=#86 id=86 data-nosnippet>86</a>            <span class="kw">while let </span><span class="prelude-val">Some</span>((start, end)) = next_deleted_range {
+<a href=#87 id=87 data-nosnippet>87</a>                <span class="kw">if </span>start &gt;= next_row_group_base_idx {
+<a href=#88 id=88 data-nosnippet>88</a>                    <span class="kw">break</span>;
+<a href=#89 id=89 data-nosnippet>89</a>                }
+<a href=#90 id=90 data-nosnippet>90</a>
+<a href=#91 id=91 data-nosnippet>91</a>                <span class="comment">// `select` the rows before the deleted range, then `skip` the part of the range
+<a href=#92 id=92 data-nosnippet>92</a>                // that lies in this row group
+<a href=#93 id=93 data-nosnippet>93</a>                </span><span class="kw">let </span>skip_start = start.max(current_idx);
+<a href=#94 id=94 data-nosnippet>94</a>                <span class="kw">let </span>skip_end = end.min(next_row_group_base_idx);
+<a href=#95 id=95 data-nosnippet>95</a>                <span class="kw">if </span>skip_start &gt; current_idx {
+<a href=#96 id=96 data-nosnippet>96</a>                    results.push(RowSelector::select((skip_start - current_idx) <span class="kw">as </span>usize));
+<a href=#97 id=97 data-nosnippet>97</a>                }
+<a href=#98 id=98 data-nosnippet>98</a>                <span class="kw">if </span>skip_end &gt; skip_start {
+<a href=#99 id=99 data-nosnippet>99</a>                    results.push(RowSelector::skip((skip_end - skip_start) <span class="kw">as </span>usize));
 <a href=#100 id=100 data-nosnippet>100</a>                }
-<a href=#101 id=101 data-nosnippet>101</a>            };
+<a href=#101 id=101 data-nosnippet>101</a>                current_idx = current_idx.max(skip_end);
 <a href=#102 id=102 data-nosnippet>102</a>
-<a href=#103 id=103 data-nosnippet>103</a>            <span class="kw">let </span><span class="kw-2">mut </span>current_idx = current_row_group_base_idx;
-<a href=#104 id=104 data-nosnippet>104</a>            <span class="lifetime">'chunks</span>: <span class="kw">while </span>next_deleted_row_idx &lt; next_row_group_base_idx {
-<a href=#105 id=105 data-nosnippet>105</a>                <span class="comment">// `select` all rows that precede the next delete index
-<a href=#106 id=106 data-nosnippet>106</a>                </span><span class="kw">if </span>current_idx &lt; next_deleted_row_idx {
-<a href=#107 id=107 data-nosnippet>107</a>                    <span class="kw">let </span>run_length = next_deleted_row_idx - current_idx;
-<a href=#108 id=108 data-nosnippet>108</a>                    results.push(RowSelector::select(run_length <span class="kw">as </span>usize));
-<a href=#109 id=109 data-nosnippet>109</a>                    current_idx += run_length;
-<a href=#110 id=110 data-nosnippet>110</a>                }
-<a href=#111 id=111 data-nosnippet>111</a>
-<a href=#112 id=112 data-nosnippet>112</a>                <span class="comment">// `skip` all consecutive deleted rows in the current row group
-<a href=#113 id=113 data-nosnippet>113</a>                </span><span class="kw">let </span><span class="kw-2">mut </span>run_length = <span class="number">0</span>;
-<a href=#114 id=114 data-nosnippet>114</a>                <span class="kw">while </span>next_deleted_row_idx == current_idx
-<a href=#115 id=115 data-nosnippet>115</a>                    &amp;&amp; next_deleted_row_idx &lt; next_row_group_base_idx
-<a href=#116 id=116 data-nosnippet>116</a>                {
-<a href=#117 id=117 data-nosnippet>117</a>                    run_length += <span class="number">1</span>;
-<a href=#118 id=118 data-nosnippet>118</a>                    current_idx += <span class="number">1</span>;
-<a href=#119 id=119 data-nosnippet>119</a>
-<a href=#120 id=120 data-nosnippet>120</a>                    next_deleted_row_idx_opt = delete_vector_iter.next();
-<a href=#121 id=121 data-nosnippet>121</a>                    next_deleted_row_idx = <span class="kw">match </span>next_deleted_row_idx_opt {
-<a href=#122 id=122 data-nosnippet>122</a>                        <span class="prelude-val">Some</span>(next_deleted_row_idx) =&gt; next_deleted_row_idx,
-<a href=#123 id=123 data-nosnippet>123</a>                        <span class="kw">_ </span>=&gt; {
-<a href=#124 id=124 data-nosnippet>124</a>                            <span class="comment">// We've processed the final positional delete.
-<a href=#125 id=125 data-nosnippet>125</a>                            // Conclude the skip and then break so that we select the remaining
-<a href=#126 id=126 data-nosnippet>126</a>                            // rows in the row group and move on to the next row group
-<a href=#127 id=127 data-nosnippet>127</a>                            </span>results.push(RowSelector::skip(run_length));
-<a href=#128 id=128 data-nosnippet>128</a>                            <span class="kw">break </span><span class="lifetime">'chunks</span>;
-<a href=#129 id=129 data-nosnippet>129</a>                        }
-<a href=#130 id=130 data-nosnippet>130</a>                    };
-<a href=#131 id=131 data-nosnippet>131</a>                }
-<a href=#132 id=132 data-nosnippet>132</a>                <span class="kw">if </span>run_length &gt; <span class="number">0 </span>{
-<a href=#133 id=133 data-nosnippet>133</a>                    results.push(RowSelector::skip(run_length));
-<a href=#134 id=134 data-nosnippet>134</a>                }
-<a href=#135 id=135 data-nosnippet>135</a>            }
-<a href=#136 id=136 data-nosnippet>136</a>
-<a href=#137 id=137 data-nosnippet>137</a>            <span class="kw">if </span>current_idx &lt; next_row_group_base_idx {
-<a href=#138 id=138 data-nosnippet>138</a>                results.push(RowSelector::select(
-<a href=#139 id=139 data-nosnippet>139</a>                    (next_row_group_base_idx - current_idx) <span class="kw">as </span>usize,
-<a href=#140 id=140 data-nosnippet>140</a>                ));
-<a href=#141 id=141 data-nosnippet>141</a>            }
-<a href=#142 id=142 data-nosnippet>142</a>
-<a href=#143 id=143 data-nosnippet>143</a>            current_row_group_base_idx += row_group_num_rows;
-<a href=#144 id=144 data-nosnippet>144</a>        }
-<a href=#145 id=145 data-nosnippet>145</a>
-<a href=#146 id=146 data-nosnippet>146</a>        <span class="prelude-val">Ok</span>(results.into())
-<a href=#147 id=147 data-nosnippet>147</a>    }
-<a href=#148 id=148 data-nosnippet>148</a>}
+<a href=#103 id=103 data-nosnippet>103</a>                <span class="kw">if </span>end &gt; next_row_group_base_idx {
+<a href=#104 id=104 data-nosnippet>104</a>                    <span class="comment">// the range continues into the next row group
+<a href=#105 id=105 data-nosnippet>105</a>                    </span><span class="kw">break</span>;
+<a href=#106 id=106 data-nosnippet>106</a>                }
+<a href=#107 id=107 data-nosnippet>107</a>                next_deleted_range = deleted_ranges.next();
+<a href=#108 id=108 data-nosnippet>108</a>            }
+<a href=#109 id=109 data-nosnippet>109</a>
+<a href=#110 id=110 data-nosnippet>110</a>            <span class="kw">if </span>current_idx &lt; next_row_group_base_idx {
+<a href=#111 id=111 data-nosnippet>111</a>                results.push(RowSelector::select(
+<a href=#112 id=112 data-nosnippet>112</a>                    (next_row_group_base_idx - current_idx) <span class="kw">as </span>usize,
+<a href=#113 id=113 data-nosnippet>113</a>                ));
+<a href=#114 id=114 data-nosnippet>114</a>            }
+<a href=#115 id=115 data-nosnippet>115</a>
+<a href=#116 id=116 data-nosnippet>116</a>            current_row_group_base_idx += row_group_num_rows;
+<a href=#117 id=117 data-nosnippet>117</a>        }
+<a href=#118 id=118 data-nosnippet>118</a>
+<a href=#119 id=119 data-nosnippet>119</a>        <span class="prelude-val">Ok</span>(results.into())
+<a href=#120 id=120 data-nosnippet>120</a>    }
+<a href=#121 id=121 data-nosnippet>121</a>}
+<a href=#122 id=122 data-nosnippet>122</a>
+<a href=#123 id=123 data-nosnippet>123</a><span class="attr">#[cfg(test)]
+<a href=#124 id=124 data-nosnippet>124</a></span><span class="kw">mod </span>tests {
+<a href=#125 id=125 data-nosnippet>125</a>    <span class="kw">use </span>std::collections::HashMap;
+<a href=#126 id=126 data-nosnippet>126</a>    <span class="kw">use </span>std::fs::File;
+<a href=#127 id=127 data-nosnippet>127</a>    <span class="kw">use </span>std::sync::Arc;
+<a href=#128 id=128 data-nosnippet>128</a>
+<a href=#129 id=129 data-nosnippet>129</a>    <span class="kw">use </span>arrow_array::cast::AsArray;
+<a href=#130 id=130 data-nosnippet>130</a>    <span class="kw">use </span>arrow_array::{RecordBatch, StringArray};
+<a href=#131 id=131 data-nosnippet>131</a>    <span class="kw">use </span>arrow_schema::{DataType, Field, Schema <span class="kw">as </span>ArrowSchema};
+<a href=#132 id=132 data-nosnippet>132</a>    <span class="kw">use </span>futures::TryStreamExt;
+<a href=#133 id=133 data-nosnippet>133</a>    <span class="kw">use </span>parquet::arrow::arrow_reader::{RowSelection, RowSelector};
+<a href=#134 id=134 data-nosnippet>134</a>    <span class="kw">use </span>parquet::arrow::{ArrowWriter, PARQUET_FIELD_ID_META_KEY};
+<a href=#135 id=135 data-nosnippet>135</a>    <span class="kw">use </span>parquet::basic::Compression;
+<a href=#136 id=136 data-nosnippet>136</a>    <span class="kw">use </span>parquet::file::metadata::{ColumnChunkMetaData, RowGroupMetaData};
+<a href=#137 id=137 data-nosnippet>137</a>    <span class="kw">use </span>parquet::file::properties::WriterProperties;
+<a href=#138 id=138 data-nosnippet>138</a>    <span class="kw">use </span>parquet::schema::types::{SchemaDescPtr, SchemaDescriptor};
+<a href=#139 id=139 data-nosnippet>139</a>    <span class="kw">use </span>roaring::RoaringTreemap;
+<a href=#140 id=140 data-nosnippet>140</a>    <span class="kw">use </span>tempfile::TempDir;
+<a href=#141 id=141 data-nosnippet>141</a>
+<a href=#142 id=142 data-nosnippet>142</a>    <span class="kw">use </span><span class="kw">crate</span>::Runtime;
+<a href=#143 id=143 data-nosnippet>143</a>    <span class="kw">use </span><span class="kw">crate</span>::arrow::{ArrowReader, ArrowReaderBuilder};
+<a href=#144 id=144 data-nosnippet>144</a>    <span class="kw">use </span><span class="kw">crate</span>::delete_vector::{DeleteVector, mixed_delete_vectors};
+<a href=#145 id=145 data-nosnippet>145</a>    <span class="kw">use </span><span class="kw">crate</span>::io::FileIO;
+<a href=#146 id=146 data-nosnippet>146</a>    <span class="kw">use </span><span class="kw">crate</span>::scan::{FileScanTask, FileScanTaskDeleteFile, FileScanTaskStream};
+<a href=#147 id=147 data-nosnippet>147</a>    <span class="kw">use </span><span class="kw">crate</span>::spec::{DataContentType, DataFileFormat, NestedField, PrimitiveType, Schema, Type};
+<a href=#148 id=148 data-nosnippet>148</a>    <span class="kw">use </span><span class="kw">crate</span>::test_utils::encode_dv_blob;
 <a href=#149 id=149 data-nosnippet>149</a>
-<a href=#150 id=150 data-nosnippet>150</a><span class="attr">#[cfg(test)]
-<a href=#151 id=151 data-nosnippet>151</a></span><span class="kw">mod </span>tests {
-<a href=#152 id=152 data-nosnippet>152</a>    <span class="kw">use </span>std::collections::HashMap;
-<a href=#153 id=153 data-nosnippet>153</a>    <span class="kw">use </span>std::fs::File;
-<a href=#154 id=154 data-nosnippet>154</a>    <span class="kw">use </span>std::sync::Arc;
-<a href=#155 id=155 data-nosnippet>155</a>
-<a href=#156 id=156 data-nosnippet>156</a>    <span class="kw">use </span>arrow_array::cast::AsArray;
-<a href=#157 id=157 data-nosnippet>157</a>    <span class="kw">use </span>arrow_array::{RecordBatch, StringArray};
-<a href=#158 id=158 data-nosnippet>158</a>    <span class="kw">use </span>arrow_schema::{DataType, Field, Schema <span class="kw">as </span>ArrowSchema};
-<a href=#159 id=159 data-nosnippet>159</a>    <span class="kw">use </span>futures::TryStreamExt;
-<a href=#160 id=160 data-nosnippet>160</a>    <span class="kw">use </span>parquet::arrow::arrow_reader::{RowSelection, RowSelector};
-<a href=#161 id=161 data-nosnippet>161</a>    <span class="kw">use </span>parquet::arrow::{ArrowWriter, PARQUET_FIELD_ID_META_KEY};
-<a href=#162 id=162 data-nosnippet>162</a>    <span class="kw">use </span>parquet::basic::Compression;
-<a href=#163 id=163 data-nosnippet>163</a>    <span class="kw">use </span>parquet::file::metadata::{ColumnChunkMetaData, RowGroupMetaData};
-<a href=#164 id=164 data-nosnippet>164</a>    <span class="kw">use </span>parquet::file::properties::WriterProperties;
-<a href=#165 id=165 data-nosnippet>165</a>    <span class="kw">use </span>parquet::schema::types::{SchemaDescPtr, SchemaDescriptor};
-<a href=#166 id=166 data-nosnippet>166</a>    <span class="kw">use </span>roaring::RoaringTreemap;
-<a href=#167 id=167 data-nosnippet>167</a>    <span class="kw">use </span>tempfile::TempDir;
-<a href=#168 id=168 data-nosnippet>168</a>
-<a href=#169 id=169 data-nosnippet>169</a>    <span class="kw">use </span><span class="kw">crate</span>::Runtime;
-<a href=#170 id=170 data-nosnippet>170</a>    <span class="kw">use </span><span class="kw">crate</span>::arrow::{ArrowReader, ArrowReaderBuilder};
-<a href=#171 id=171 data-nosnippet>171</a>    <span class="kw">use </span><span class="kw">crate</span>::delete_vector::DeleteVector;
-<a href=#172 id=172 data-nosnippet>172</a>    <span class="kw">use </span><span class="kw">crate</span>::io::FileIO;
-<a href=#173 id=173 data-nosnippet>173</a>    <span class="kw">use </span><span class="kw">crate</span>::scan::{FileScanTask, FileScanTaskDeleteFile, FileScanTaskStream};
-<a href=#174 id=174 data-nosnippet>174</a>    <span class="kw">use </span><span class="kw">crate</span>::spec::{DataContentType, DataFileFormat, NestedField, PrimitiveType, Schema, Type};
-<a href=#175 id=175 data-nosnippet>175</a>    <span class="kw">use </span><span class="kw">crate</span>::test_utils::encode_dv_blob;
-<a href=#176 id=176 data-nosnippet>176</a>
-<a href=#177 id=177 data-nosnippet>177</a>    <span class="kw">fn </span>build_test_row_group_meta(
-<a href=#178 id=178 data-nosnippet>178</a>        schema_descr: SchemaDescPtr,
-<a href=#179 id=179 data-nosnippet>179</a>        columns: Vec&lt;ColumnChunkMetaData&gt;,
-<a href=#180 id=180 data-nosnippet>180</a>        num_rows: i64,
-<a href=#181 id=181 data-nosnippet>181</a>        ordinal: i16,
-<a href=#182 id=182 data-nosnippet>182</a>    ) -&gt; RowGroupMetaData {
-<a href=#183 id=183 data-nosnippet>183</a>        RowGroupMetaData::builder(schema_descr.clone())
-<a href=#184 id=184 data-nosnippet>184</a>            .set_num_rows(num_rows)
-<a href=#185 id=185 data-nosnippet>185</a>            .set_total_byte_size(<span class="number">2000</span>)
-<a href=#186 id=186 data-nosnippet>186</a>            .set_column_metadata(columns)
-<a href=#187 id=187 data-nosnippet>187</a>            .set_ordinal(ordinal)
-<a href=#188 id=188 data-nosnippet>188</a>            .build()
-<a href=#189 id=189 data-nosnippet>189</a>            .unwrap()
-<a href=#190 id=190 data-nosnippet>190</a>    }
-<a href=#191 id=191 data-nosnippet>191</a>
-<a href=#192 id=192 data-nosnippet>192</a>    <span class="kw">fn </span>get_test_schema_descr() -&gt; SchemaDescPtr {
-<a href=#193 id=193 data-nosnippet>193</a>        <span class="kw">use </span>parquet::schema::types::Type <span class="kw">as </span>SchemaType;
-<a href=#194 id=194 data-nosnippet>194</a>
-<a href=#195 id=195 data-nosnippet>195</a>        <span class="kw">let </span>schema = SchemaType::group_type_builder(<span class="string">"schema"</span>)
-<a href=#196 id=196 data-nosnippet>196</a>            .with_fields(<span class="macro">vec!</span>[
-<a href=#197 id=197 data-nosnippet>197</a>                Arc::new(
-<a href=#198 id=198 data-nosnippet>198</a>                    SchemaType::primitive_type_builder(<span class="string">"a"</span>, parquet::basic::Type::INT32)
-<a href=#199 id=199 data-nosnippet>199</a>                        .build()
-<a href=#200 id=200 data-nosnippet>200</a>                        .unwrap(),
-<a href=#201 id=201 data-nosnippet>201</a>                ),
-<a href=#202 id=202 data-nosnippet>202</a>                Arc::new(
-<a href=#203 id=203 data-nosnippet>203</a>                    SchemaType::primitive_type_builder(<span class="string">"b"</span>, parquet::basic::Type::INT32)
-<a href=#204 id=204 data-nosnippet>204</a>                        .build()
-<a href=#205 id=205 data-nosnippet>205</a>                        .unwrap(),
-<a href=#206 id=206 data-nosnippet>206</a>                ),
-<a href=#207 id=207 data-nosnippet>207</a>            ])
-<a href=#208 id=208 data-nosnippet>208</a>            .build()
-<a href=#209 id=209 data-nosnippet>209</a>            .unwrap();
+<a href=#150 id=150 data-nosnippet>150</a>    <span class="kw">fn </span>build_test_row_group_meta(
+<a href=#151 id=151 data-nosnippet>151</a>        schema_descr: SchemaDescPtr,
+<a href=#152 id=152 data-nosnippet>152</a>        columns: Vec&lt;ColumnChunkMetaData&gt;,
+<a href=#153 id=153 data-nosnippet>153</a>        num_rows: i64,
+<a href=#154 id=154 data-nosnippet>154</a>        ordinal: i16,
+<a href=#155 id=155 data-nosnippet>155</a>    ) -&gt; RowGroupMetaData {
+<a href=#156 id=156 data-nosnippet>156</a>        RowGroupMetaData::builder(schema_descr.clone())
+<a href=#157 id=157 data-nosnippet>157</a>            .set_num_rows(num_rows)
+<a href=#158 id=158 data-nosnippet>158</a>            .set_total_byte_size(<span class="number">2000</span>)
+<a href=#159 id=159 data-nosnippet>159</a>            .set_column_metadata(columns)
+<a href=#160 id=160 data-nosnippet>160</a>            .set_ordinal(ordinal)
+<a href=#161 id=161 data-nosnippet>161</a>            .build()
+<a href=#162 id=162 data-nosnippet>162</a>            .unwrap()
+<a href=#163 id=163 data-nosnippet>163</a>    }
+<a href=#164 id=164 data-nosnippet>164</a>
+<a href=#165 id=165 data-nosnippet>165</a>    <span class="kw">fn </span>get_test_schema_descr() -&gt; SchemaDescPtr {
+<a href=#166 id=166 data-nosnippet>166</a>        <span class="kw">use </span>parquet::schema::types::Type <span class="kw">as </span>SchemaType;
+<a href=#167 id=167 data-nosnippet>167</a>
+<a href=#168 id=168 data-nosnippet>168</a>        <span class="kw">let </span>schema = SchemaType::group_type_builder(<span class="string">"schema"</span>)
+<a href=#169 id=169 data-nosnippet>169</a>            .with_fields(<span class="macro">vec!</span>[
+<a href=#170 id=170 data-nosnippet>170</a>                Arc::new(
+<a href=#171 id=171 data-nosnippet>171</a>                    SchemaType::primitive_type_builder(<span class="string">"a"</span>, parquet::basic::Type::INT32)
+<a href=#172 id=172 data-nosnippet>172</a>                        .build()
+<a href=#173 id=173 data-nosnippet>173</a>                        .unwrap(),
+<a href=#174 id=174 data-nosnippet>174</a>                ),
+<a href=#175 id=175 data-nosnippet>175</a>                Arc::new(
+<a href=#176 id=176 data-nosnippet>176</a>                    SchemaType::primitive_type_builder(<span class="string">"b"</span>, parquet::basic::Type::INT32)
+<a href=#177 id=177 data-nosnippet>177</a>                        .build()
+<a href=#178 id=178 data-nosnippet>178</a>                        .unwrap(),
+<a href=#179 id=179 data-nosnippet>179</a>                ),
+<a href=#180 id=180 data-nosnippet>180</a>            ])
+<a href=#181 id=181 data-nosnippet>181</a>            .build()
+<a href=#182 id=182 data-nosnippet>182</a>            .unwrap();
+<a href=#183 id=183 data-nosnippet>183</a>
+<a href=#184 id=184 data-nosnippet>184</a>        Arc::new(SchemaDescriptor::new(Arc::new(schema)))
+<a href=#185 id=185 data-nosnippet>185</a>    }
+<a href=#186 id=186 data-nosnippet>186</a>
+<a href=#187 id=187 data-nosnippet>187</a>    <span class="doccomment">/// Row groups and deleted runs that cross the 2^32 position boundary, with and without row
+<a href=#188 id=188 data-nosnippet>188</a>    /// group selection.
+<a href=#189 id=189 data-nosnippet>189</a>    </span><span class="attr">#[test]
+<a href=#190 id=190 data-nosnippet>190</a>    </span><span class="kw">fn </span>test_build_deletes_row_selection_across_2_pow_32() {
+<a href=#191 id=191 data-nosnippet>191</a>        <span class="kw">let </span>schema_descr = get_test_schema_descr();
+<a href=#192 id=192 data-nosnippet>192</a>        <span class="kw">let </span>columns: Vec&lt;<span class="kw">_</span>&gt; = schema_descr
+<a href=#193 id=193 data-nosnippet>193</a>            .columns()
+<a href=#194 id=194 data-nosnippet>194</a>            .iter()
+<a href=#195 id=195 data-nosnippet>195</a>            .map(|ptr| ColumnChunkMetaData::builder(ptr.clone()).build().unwrap())
+<a href=#196 id=196 data-nosnippet>196</a>            .collect();
+<a href=#197 id=197 data-nosnippet>197</a>        <span class="kw">let </span>row_groups = |sizes: <span class="kw-2">&amp;</span>[i64]| -&gt; Vec&lt;RowGroupMetaData&gt; {
+<a href=#198 id=198 data-nosnippet>198</a>            sizes
+<a href=#199 id=199 data-nosnippet>199</a>                .iter()
+<a href=#200 id=200 data-nosnippet>200</a>                .enumerate()
+<a href=#201 id=201 data-nosnippet>201</a>                .map(|(i, <span class="kw-2">&amp;</span>n)| {
+<a href=#202 id=202 data-nosnippet>202</a>                    build_test_row_group_meta(schema_descr.clone(), columns.clone(), n, i <span class="kw">as </span>i16)
+<a href=#203 id=203 data-nosnippet>203</a>                })
+<a href=#204 id=204 data-nosnippet>204</a>                .collect()
+<a href=#205 id=205 data-nosnippet>205</a>        };
+<a href=#206 id=206 data-nosnippet>206</a>        <span class="kw">let </span>select = RowSelector::select;
+<a href=#207 id=207 data-nosnippet>207</a>        <span class="kw">let </span>skip = RowSelector::skip;
+<a href=#208 id=208 data-nosnippet>208</a>        <span class="kw">let </span>high = <span class="number">1u64 </span>&lt;&lt; <span class="number">32</span>;
+<a href=#209 id=209 data-nosnippet>209</a>        <span class="kw">let </span>big = high <span class="kw">as </span>usize;
 <a href=#210 id=210 data-nosnippet>210</a>
-<a href=#211 id=211 data-nosnippet>211</a>        Arc::new(SchemaDescriptor::new(Arc::new(schema)))
-<a href=#212 id=212 data-nosnippet>212</a>    }
-<a href=#213 id=213 data-nosnippet>213</a>
-<a href=#214 id=214 data-nosnippet>214</a>    <span class="attr">#[test]
-<a href=#215 id=215 data-nosnippet>215</a>    </span><span class="kw">fn </span>test_build_deletes_row_selection() {
-<a href=#216 id=216 data-nosnippet>216</a>        <span class="kw">let </span>schema_descr = get_test_schema_descr();
-<a href=#217 id=217 data-nosnippet>217</a>
-<a href=#218 id=218 data-nosnippet>218</a>        <span class="kw">let </span><span class="kw-2">mut </span>columns = <span class="macro">vec!</span>[];
-<a href=#219 id=219 data-nosnippet>219</a>        <span class="kw">for </span>ptr <span class="kw">in </span>schema_descr.columns() {
-<a href=#220 id=220 data-nosnippet>220</a>            <span class="kw">let </span>column = ColumnChunkMetaData::builder(ptr.clone()).build().unwrap();
-<a href=#221 id=221 data-nosnippet>221</a>            columns.push(column);
-<a href=#222 id=222 data-nosnippet>222</a>        }
-<a href=#223 id=223 data-nosnippet>223</a>
-<a href=#224 id=224 data-nosnippet>224</a>        <span class="kw">let </span>row_groups_metadata = <span class="macro">vec!</span>[
-<a href=#225 id=225 data-nosnippet>225</a>            build_test_row_group_meta(schema_descr.clone(), columns.clone(), <span class="number">1000</span>, <span class="number">0</span>),
-<a href=#226 id=226 data-nosnippet>226</a>            build_test_row_group_meta(schema_descr.clone(), columns.clone(), <span class="number">500</span>, <span class="number">1</span>),
-<a href=#227 id=227 data-nosnippet>227</a>            build_test_row_group_meta(schema_descr.clone(), columns.clone(), <span class="number">500</span>, <span class="number">2</span>),
-<a href=#228 id=228 data-nosnippet>228</a>            build_test_row_group_meta(schema_descr.clone(), columns.clone(), <span class="number">1000</span>, <span class="number">3</span>),
-<a href=#229 id=229 data-nosnippet>229</a>            build_test_row_group_meta(schema_descr.clone(), columns.clone(), <span class="number">500</span>, <span class="number">4</span>),
-<a href=#230 id=230 data-nosnippet>230</a>        ];
-<a href=#231 id=231 data-nosnippet>231</a>
-<a href=#232 id=232 data-nosnippet>232</a>        <span class="kw">let </span>selected_row_groups = <span class="prelude-val">Some</span>(<span class="macro">vec!</span>[<span class="number">1</span>, <span class="number">3</span>]);
-<a href=#233 id=233 data-nosnippet>233</a>
-<a href=#234 id=234 data-nosnippet>234</a>        <span class="comment">/* cases to cover:
-<a href=#235 id=235 data-nosnippet>235</a>           * {skip|select} {first|intermediate|last} {one row|multiple rows} in
-<a href=#236 id=236 data-nosnippet>236</a>             {first|intermediate|last} {skipped|selected} row group
-<a href=#237 id=237 data-nosnippet>237</a>           * row group selection disabled
-<a href=#238 id=238 data-nosnippet>238</a>        */
-<a href=#239 id=239 data-nosnippet>239</a>
-<a href=#240 id=240 data-nosnippet>240</a>        </span><span class="kw">let </span>positional_deletes = RoaringTreemap::from_iter(<span class="kw-2">&amp;</span>[
-<a href=#241 id=241 data-nosnippet>241</a>            <span class="number">1</span>, <span class="comment">// in skipped rg 0, should be ignored
-<a href=#242 id=242 data-nosnippet>242</a>            </span><span class="number">3</span>, <span class="comment">// run of three consecutive items in skipped rg0
-<a href=#243 id=243 data-nosnippet>243</a>            </span><span class="number">4</span>, <span class="number">5</span>, <span class="number">998</span>, <span class="comment">// two consecutive items at end of skipped rg0
-<a href=#244 id=244 data-nosnippet>244</a>            </span><span class="number">999</span>, <span class="number">1000</span>, <span class="comment">// solitary row at start of selected rg1 (1, 9)
-<a href=#245 id=245 data-nosnippet>245</a>            </span><span class="number">1010</span>, <span class="comment">// run of 3 rows in selected rg1
-<a href=#246 id=246 data-nosnippet>246</a>            </span><span class="number">1011</span>, <span class="number">1012</span>, <span class="comment">// (3, 485)
-<a href=#247 id=247 data-nosnippet>247</a>            </span><span class="number">1498</span>, <span class="comment">// run of two items at end of selected rg1
-<a href=#248 id=248 data-nosnippet>248</a>            </span><span class="number">1499</span>, <span class="number">1500</span>, <span class="comment">// run of two items at start of skipped rg2
-<a href=#249 id=249 data-nosnippet>249</a>            </span><span class="number">1501</span>, <span class="number">1600</span>, <span class="comment">// should ignore, in skipped rg2
-<a href=#250 id=250 data-nosnippet>250</a>            </span><span class="number">1999</span>, <span class="comment">// single row at end of skipped rg2
-<a href=#251 id=251 data-nosnippet>251</a>            </span><span class="number">2000</span>, <span class="comment">// run of two items at start of selected rg3
-<a href=#252 id=252 data-nosnippet>252</a>            </span><span class="number">2001</span>, <span class="comment">// (4, 98)
-<a href=#253 id=253 data-nosnippet>253</a>            </span><span class="number">2100</span>, <span class="comment">// single row in selected row group 3 (1, 99)
-<a href=#254 id=254 data-nosnippet>254</a>            </span><span class="number">2200</span>, <span class="comment">// run of 3 consecutive rows in selected row group 3
-<a href=#255 id=255 data-nosnippet>255</a>            </span><span class="number">2201</span>, <span class="number">2202</span>, <span class="comment">// (3, 796)
-<a href=#256 id=256 data-nosnippet>256</a>            </span><span class="number">2999</span>, <span class="comment">// single item at end of selected rg3 (1)
-<a href=#257 id=257 data-nosnippet>257</a>            </span><span class="number">3000</span>, <span class="comment">// single item at start of skipped rg4
-<a href=#258 id=258 data-nosnippet>258</a>        </span>]);
-<a href=#259 id=259 data-nosnippet>259</a>
-<a href=#260 id=260 data-nosnippet>260</a>        <span class="kw">let </span>positional_deletes = DeleteVector::new(positional_deletes);
-<a href=#261 id=261 data-nosnippet>261</a>
-<a href=#262 id=262 data-nosnippet>262</a>        <span class="comment">// using selected row groups 1 and 3
-<a href=#263 id=263 data-nosnippet>263</a>        </span><span class="kw">let </span>result = ArrowReader::build_deletes_row_selection(
-<a href=#264 id=264 data-nosnippet>264</a>            <span class="kw-2">&amp;</span>row_groups_metadata,
-<a href=#265 id=265 data-nosnippet>265</a>            <span class="kw-2">&amp;</span>selected_row_groups,
-<a href=#266 id=266 data-nosnippet>266</a>            <span class="kw-2">&amp;</span>positional_deletes,
-<a href=#267 id=267 data-nosnippet>267</a>        )
-<a href=#268 id=268 data-nosnippet>268</a>        .unwrap();
-<a href=#269 id=269 data-nosnippet>269</a>
-<a href=#270 id=270 data-nosnippet>270</a>        <span class="kw">let </span>expected = RowSelection::from(<span class="macro">vec!</span>[
-<a href=#271 id=271 data-nosnippet>271</a>            RowSelector::skip(<span class="number">1</span>),
-<a href=#272 id=272 data-nosnippet>272</a>            RowSelector::select(<span class="number">9</span>),
-<a href=#273 id=273 data-nosnippet>273</a>            RowSelector::skip(<span class="number">3</span>),
-<a href=#274 id=274 data-nosnippet>274</a>            RowSelector::select(<span class="number">485</span>),
-<a href=#275 id=275 data-nosnippet>275</a>            RowSelector::skip(<span class="number">4</span>),
-<a href=#276 id=276 data-nosnippet>276</a>            RowSelector::select(<span class="number">98</span>),
-<a href=#277 id=277 data-nosnippet>277</a>            RowSelector::skip(<span class="number">1</span>),
-<a href=#278 id=278 data-nosnippet>278</a>            RowSelector::select(<span class="number">99</span>),
-<a href=#279 id=279 data-nosnippet>279</a>            RowSelector::skip(<span class="number">3</span>),
-<a href=#280 id=280 data-nosnippet>280</a>            RowSelector::select(<span class="number">796</span>),
-<a href=#281 id=281 data-nosnippet>281</a>            RowSelector::skip(<span class="number">1</span>),
-<a href=#282 id=282 data-nosnippet>282</a>        ]);
-<a href=#283 id=283 data-nosnippet>283</a>
-<a href=#284 id=284 data-nosnippet>284</a>        <span class="macro">assert_eq!</span>(result, expected);
-<a href=#285 id=285 data-nosnippet>285</a>
-<a href=#286 id=286 data-nosnippet>286</a>        <span class="comment">// selecting all row groups
-<a href=#287 id=287 data-nosnippet>287</a>        </span><span class="kw">let </span>result = ArrowReader::build_deletes_row_selection(
-<a href=#288 id=288 data-nosnippet>288</a>            <span class="kw-2">&amp;</span>row_groups_metadata,
-<a href=#289 id=289 data-nosnippet>289</a>            <span class="kw-2">&amp;</span><span class="prelude-val">None</span>,
-<a href=#290 id=290 data-nosnippet>290</a>            <span class="kw-2">&amp;</span>positional_deletes,
-<a href=#291 id=291 data-nosnippet>291</a>        )
-<a href=#292 id=292 data-nosnippet>292</a>        .unwrap();
-<a href=#293 id=293 data-nosnippet>293</a>
-<a href=#294 id=294 data-nosnippet>294</a>        <span class="kw">let </span>expected = RowSelection::from(<span class="macro">vec!</span>[
-<a href=#295 id=295 data-nosnippet>295</a>            RowSelector::select(<span class="number">1</span>),
-<a href=#296 id=296 data-nosnippet>296</a>            RowSelector::skip(<span class="number">1</span>),
-<a href=#297 id=297 data-nosnippet>297</a>            RowSelector::select(<span class="number">1</span>),
-<a href=#298 id=298 data-nosnippet>298</a>            RowSelector::skip(<span class="number">3</span>),
-<a href=#299 id=299 data-nosnippet>299</a>            RowSelector::select(<span class="number">992</span>),
-<a href=#300 id=300 data-nosnippet>300</a>            RowSelector::skip(<span class="number">3</span>),
-<a href=#301 id=301 data-nosnippet>301</a>            RowSelector::select(<span class="number">9</span>),
-<a href=#302 id=302 data-nosnippet>302</a>            RowSelector::skip(<span class="number">3</span>),
-<a href=#303 id=303 data-nosnippet>303</a>            RowSelector::select(<span class="number">485</span>),
-<a href=#304 id=304 data-nosnippet>304</a>            RowSelector::skip(<span class="number">4</span>),
-<a href=#305 id=305 data-nosnippet>305</a>            RowSelector::select(<span class="number">98</span>),
-<a href=#306 id=306 data-nosnippet>306</a>            RowSelector::skip(<span class="number">1</span>),
-<a href=#307 id=307 data-nosnippet>307</a>            RowSelector::select(<span class="number">398</span>),
-<a href=#308 id=308 data-nosnippet>308</a>            RowSelector::skip(<span class="number">3</span>),
-<a href=#309 id=309 data-nosnippet>309</a>            RowSelector::select(<span class="number">98</span>),
-<a href=#310 id=310 data-nosnippet>310</a>            RowSelector::skip(<span class="number">1</span>),
-<a href=#311 id=311 data-nosnippet>311</a>            RowSelector::select(<span class="number">99</span>),
-<a href=#312 id=312 data-nosnippet>312</a>            RowSelector::skip(<span class="number">3</span>),
-<a href=#313 id=313 data-nosnippet>313</a>            RowSelector::select(<span class="number">796</span>),
-<a href=#314 id=314 data-nosnippet>314</a>            RowSelector::skip(<span class="number">2</span>),
-<a href=#315 id=315 data-nosnippet>315</a>            RowSelector::select(<span class="number">499</span>),
-<a href=#316 id=316 data-nosnippet>316</a>        ]);
-<a href=#317 id=317 data-nosnippet>317</a>
-<a href=#318 id=318 data-nosnippet>318</a>        <span class="macro">assert_eq!</span>(result, expected);
-<a href=#319 id=319 data-nosnippet>319</a>    }
-<a href=#320 id=320 data-nosnippet>320</a>
-<a href=#321 id=321 data-nosnippet>321</a>    <span class="doccomment">/// Test for bug where position deletes in later row groups are not applied correctly.
-<a href=#322 id=322 data-nosnippet>322</a>    ///
-<a href=#323 id=323 data-nosnippet>323</a>    /// When a file has multiple row groups and a position delete targets a row in a later
-<a href=#324 id=324 data-nosnippet>324</a>    /// row group, the `build_deletes_row_selection` function had a bug where it would
-<a href=#325 id=325 data-nosnippet>325</a>    /// fail to increment `current_row_group_base_idx` when skipping row groups.
-<a href=#326 id=326 data-nosnippet>326</a>    ///
-<a href=#327 id=327 data-nosnippet>327</a>    /// This test creates:
-<a href=#328 id=328 data-nosnippet>328</a>    /// - A data file with 200 rows split into 2 row groups (0-99, 100-199)
-<a href=#329 id=329 data-nosnippet>329</a>    /// - A position delete file that deletes row 199 (last row in second row group)
-<a href=#330 id=330 data-nosnippet>330</a>    ///
-<a href=#331 id=331 data-nosnippet>331</a>    /// Expected behavior: Should return 199 rows (with id=200 deleted)
-<a href=#332 id=332 data-nosnippet>332</a>    /// Bug behavior: Returns 200 rows (delete is not applied)
-<a href=#333 id=333 data-nosnippet>333</a>    ///
-<a href=#334 id=334 data-nosnippet>334</a>    /// This bug was discovered while running Apache Spark + Apache Iceberg integration tests
-<a href=#335 id=335 data-nosnippet>335</a>    /// through DataFusion Comet. The following Iceberg Java tests failed due to this bug:
-<a href=#336 id=336 data-nosnippet>336</a>    /// - `org.apache.iceberg.spark.extensions.TestMergeOnReadDelete::testDeleteWithMultipleRowGroupsParquet`
-<a href=#337 id=337 data-nosnippet>337</a>    /// - `org.apache.iceberg.spark.extensions.TestMergeOnReadUpdate::testUpdateWithMultipleRowGroupsParquet`
-<a href=#338 id=338 data-nosnippet>338</a>    </span><span class="attr">#[tokio::test]
-<a href=#339 id=339 data-nosnippet>339</a>    </span><span class="kw">async fn </span>test_position_delete_across_multiple_row_groups() {
-<a href=#340 id=340 data-nosnippet>340</a>        <span class="kw">use </span>arrow_array::{Int32Array, Int64Array};
-<a href=#341 id=341 data-nosnippet>341</a>        <span class="kw">use </span>parquet::file::reader::{FileReader, SerializedFileReader};
-<a href=#342 id=342 data-nosnippet>342</a>
-<a href=#343 id=343 data-nosnippet>343</a>        <span class="comment">// Field IDs for positional delete schema
-<a href=#344 id=344 data-nosnippet>344</a>        </span><span class="kw">const </span>FIELD_ID_POSITIONAL_DELETE_FILE_PATH: u64 = <span class="number">2147483546</span>;
-<a href=#345 id=345 data-nosnippet>345</a>        <span class="kw">const </span>FIELD_ID_POSITIONAL_DELETE_POS: u64 = <span class="number">2147483545</span>;
-<a href=#346 id=346 data-nosnippet>346</a>
-<a href=#347 id=347 data-nosnippet>347</a>        <span class="kw">let </span>tmp_dir = TempDir::new().unwrap();
-<a href=#348 id=348 data-nosnippet>348</a>        <span class="kw">let </span>table_location = tmp_dir.path().to_str().unwrap().to_string();
-<a href=#349 id=349 data-nosnippet>349</a>
-<a href=#350 id=350 data-nosnippet>350</a>        <span class="comment">// Create table schema with a single 'id' column
-<a href=#351 id=351 data-nosnippet>351</a>        </span><span class="kw">let </span>table_schema = Arc::new(
-<a href=#352 id=352 data-nosnippet>352</a>            Schema::builder()
-<a href=#353 id=353 data-nosnippet>353</a>                .with_schema_id(<span class="number">1</span>)
-<a href=#354 id=354 data-nosnippet>354</a>                .with_fields(<span class="macro">vec!</span>[
-<a href=#355 id=355 data-nosnippet>355</a>                    NestedField::required(<span class="number">1</span>, <span class="string">"id"</span>, Type::Primitive(PrimitiveType::Int)).into(),
-<a href=#356 id=356 data-nosnippet>356</a>                ])
-<a href=#357 id=357 data-nosnippet>357</a>                .build()
-<a href=#358 id=358 data-nosnippet>358</a>                .unwrap(),
-<a href=#359 id=359 data-nosnippet>359</a>        );
-<a href=#360 id=360 data-nosnippet>360</a>
-<a href=#361 id=361 data-nosnippet>361</a>        <span class="kw">let </span>arrow_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
-<a href=#362 id=362 data-nosnippet>362</a>            Field::new(<span class="string">"id"</span>, DataType::Int32, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
-<a href=#363 id=363 data-nosnippet>363</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
-<a href=#364 id=364 data-nosnippet>364</a>                <span class="string">"1"</span>.to_string(),
-<a href=#365 id=365 data-nosnippet>365</a>            )])),
-<a href=#366 id=366 data-nosnippet>366</a>        ]));
-<a href=#367 id=367 data-nosnippet>367</a>
-<a href=#368 id=368 data-nosnippet>368</a>        <span class="comment">// Step 1: Create data file with 200 rows in 2 row groups
-<a href=#369 id=369 data-nosnippet>369</a>        // Row group 0: rows 0-99 (ids 1-100)
-<a href=#370 id=370 data-nosnippet>370</a>        // Row group 1: rows 100-199 (ids 101-200)
-<a href=#371 id=371 data-nosnippet>371</a>        </span><span class="kw">let </span>data_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/data.parquet"</span>);
+<a href=#211 id=211 data-nosnippet>211</a>        <span class="comment">// Row group 1 starts at 2^32 - 2. The run 2^32 - 3 ..= 2^32 crosses both boundaries.
+<a href=#212 id=212 data-nosnippet>212</a>        </span><span class="kw">let </span>meta = row_groups(<span class="kw-2">&amp;</span>[(<span class="number">1 </span>&lt;&lt; <span class="number">32</span>) - <span class="number">2</span>, <span class="number">8</span>]);
+<a href=#213 id=213 data-nosnippet>213</a>        <span class="kw">let </span>deletes = DeleteVector::new(RoaringTreemap::from_iter([
+<a href=#214 id=214 data-nosnippet>214</a>            <span class="number">1</span>,
+<a href=#215 id=215 data-nosnippet>215</a>            high - <span class="number">3</span>,
+<a href=#216 id=216 data-nosnippet>216</a>            high - <span class="number">2</span>,
+<a href=#217 id=217 data-nosnippet>217</a>            high - <span class="number">1</span>,
+<a href=#218 id=218 data-nosnippet>218</a>            high,
+<a href=#219 id=219 data-nosnippet>219</a>            high + <span class="number">3</span>,
+<a href=#220 id=220 data-nosnippet>220</a>        ]));
+<a href=#221 id=221 data-nosnippet>221</a>        <span class="kw">let </span>build = |selected: <span class="prelude-ty">Option</span>&lt;Vec&lt;usize&gt;&gt;| {
+<a href=#222 id=222 data-nosnippet>222</a>            ArrowReader::build_deletes_row_selection(<span class="kw-2">&amp;</span>meta, <span class="kw-2">&amp;</span>selected, <span class="kw-2">&amp;</span>deletes).unwrap()
+<a href=#223 id=223 data-nosnippet>223</a>        };
+<a href=#224 id=224 data-nosnippet>224</a>        <span class="macro">assert_eq!</span>(
+<a href=#225 id=225 data-nosnippet>225</a>            build(<span class="prelude-val">None</span>),
+<a href=#226 id=226 data-nosnippet>226</a>            RowSelection::from(<span class="macro">vec!</span>[
+<a href=#227 id=227 data-nosnippet>227</a>                select(<span class="number">1</span>),
+<a href=#228 id=228 data-nosnippet>228</a>                skip(<span class="number">1</span>),
+<a href=#229 id=229 data-nosnippet>229</a>                select(big - <span class="number">5</span>),
+<a href=#230 id=230 data-nosnippet>230</a>                skip(<span class="number">4</span>),
+<a href=#231 id=231 data-nosnippet>231</a>                select(<span class="number">2</span>),
+<a href=#232 id=232 data-nosnippet>232</a>                skip(<span class="number">1</span>),
+<a href=#233 id=233 data-nosnippet>233</a>                select(<span class="number">2</span>),
+<a href=#234 id=234 data-nosnippet>234</a>            ])
+<a href=#235 id=235 data-nosnippet>235</a>        );
+<a href=#236 id=236 data-nosnippet>236</a>        <span class="macro">assert_eq!</span>(
+<a href=#237 id=237 data-nosnippet>237</a>            build(<span class="prelude-val">Some</span>(<span class="macro">vec!</span>[<span class="number">0</span>])),
+<a href=#238 id=238 data-nosnippet>238</a>            RowSelection::from(<span class="macro">vec!</span>[select(<span class="number">1</span>), skip(<span class="number">1</span>), select(big - <span class="number">5</span>), skip(<span class="number">1</span>)])
+<a href=#239 id=239 data-nosnippet>239</a>        );
+<a href=#240 id=240 data-nosnippet>240</a>        <span class="macro">assert_eq!</span>(
+<a href=#241 id=241 data-nosnippet>241</a>            build(<span class="prelude-val">Some</span>(<span class="macro">vec!</span>[<span class="number">1</span>])),
+<a href=#242 id=242 data-nosnippet>242</a>            RowSelection::from(<span class="macro">vec!</span>[skip(<span class="number">3</span>), select(<span class="number">2</span>), skip(<span class="number">1</span>), select(<span class="number">2</span>)])
+<a href=#243 id=243 data-nosnippet>243</a>        );
+<a href=#244 id=244 data-nosnippet>244</a>
+<a href=#245 id=245 data-nosnippet>245</a>        <span class="comment">// Skipping row group 0 lands past 2^32, in the next bitmap.
+<a href=#246 id=246 data-nosnippet>246</a>        </span><span class="kw">let </span>meta = row_groups(<span class="kw-2">&amp;</span>[(<span class="number">1 </span>&lt;&lt; <span class="number">32</span>) + <span class="number">1</span>, <span class="number">4</span>]);
+<a href=#247 id=247 data-nosnippet>247</a>        <span class="kw">let </span>deletes = DeleteVector::new(RoaringTreemap::from_iter([<span class="number">5</span>, high - <span class="number">1</span>, high, high + <span class="number">2</span>]));
+<a href=#248 id=248 data-nosnippet>248</a>        <span class="macro">assert_eq!</span>(
+<a href=#249 id=249 data-nosnippet>249</a>            ArrowReader::build_deletes_row_selection(<span class="kw-2">&amp;</span>meta, <span class="kw-2">&amp;</span><span class="prelude-val">Some</span>(<span class="macro">vec!</span>[<span class="number">1</span>]), <span class="kw-2">&amp;</span>deletes).unwrap(),
+<a href=#250 id=250 data-nosnippet>250</a>            RowSelection::from(<span class="macro">vec!</span>[select(<span class="number">1</span>), skip(<span class="number">1</span>), select(<span class="number">2</span>)])
+<a href=#251 id=251 data-nosnippet>251</a>        );
+<a href=#252 id=252 data-nosnippet>252</a>    }
+<a href=#253 id=253 data-nosnippet>253</a>
+<a href=#254 id=254 data-nosnippet>254</a>    <span class="doccomment">/// The selection for `deleted` built one deleted position at a time.
+<a href=#255 id=255 data-nosnippet>255</a>    </span><span class="kw">fn </span>expected_deletes_row_selection(
+<a href=#256 id=256 data-nosnippet>256</a>        row_group_sizes: <span class="kw-2">&amp;</span>[u64],
+<a href=#257 id=257 data-nosnippet>257</a>        selected_row_groups: <span class="kw-2">&amp;</span><span class="prelude-ty">Option</span>&lt;Vec&lt;usize&gt;&gt;,
+<a href=#258 id=258 data-nosnippet>258</a>        deleted: <span class="kw-2">&amp;</span>[u64],
+<a href=#259 id=259 data-nosnippet>259</a>    ) -&gt; RowSelection {
+<a href=#260 id=260 data-nosnippet>260</a>        <span class="kw">let </span><span class="kw-2">mut </span>selectors = Vec::new();
+<a href=#261 id=261 data-nosnippet>261</a>        <span class="kw">let </span><span class="kw-2">mut </span>base = <span class="number">0</span>;
+<a href=#262 id=262 data-nosnippet>262</a>        <span class="kw">for </span>(idx, <span class="kw-2">&amp;</span>num_rows) <span class="kw">in </span>row_group_sizes.iter().enumerate() {
+<a href=#263 id=263 data-nosnippet>263</a>            <span class="kw">let </span>end = base + num_rows;
+<a href=#264 id=264 data-nosnippet>264</a>            <span class="kw">if </span>selected_row_groups
+<a href=#265 id=265 data-nosnippet>265</a>                .as_ref()
+<a href=#266 id=266 data-nosnippet>266</a>                .is_none_or(|selected| selected.contains(<span class="kw-2">&amp;</span>idx))
+<a href=#267 id=267 data-nosnippet>267</a>            {
+<a href=#268 id=268 data-nosnippet>268</a>                <span class="kw">let </span><span class="kw-2">mut </span>row = base;
+<a href=#269 id=269 data-nosnippet>269</a>                <span class="kw">let </span>first = deleted.partition_point(|<span class="kw-2">&amp;</span>p| p &lt; base);
+<a href=#270 id=270 data-nosnippet>270</a>                <span class="kw">for </span><span class="kw-2">&amp;</span>pos <span class="kw">in </span>deleted[first..].iter().take_while(|&amp;&amp;p| p &lt; end) {
+<a href=#271 id=271 data-nosnippet>271</a>                    selectors.push(RowSelector::select((pos - row) <span class="kw">as </span>usize));
+<a href=#272 id=272 data-nosnippet>272</a>                    selectors.push(RowSelector::skip(<span class="number">1</span>));
+<a href=#273 id=273 data-nosnippet>273</a>                    row = pos + <span class="number">1</span>;
+<a href=#274 id=274 data-nosnippet>274</a>                }
+<a href=#275 id=275 data-nosnippet>275</a>                selectors.push(RowSelector::select((end - row) <span class="kw">as </span>usize));
+<a href=#276 id=276 data-nosnippet>276</a>            }
+<a href=#277 id=277 data-nosnippet>277</a>            base = end;
+<a href=#278 id=278 data-nosnippet>278</a>        }
+<a href=#279 id=279 data-nosnippet>279</a>        selectors.into()
+<a href=#280 id=280 data-nosnippet>280</a>    }
+<a href=#281 id=281 data-nosnippet>281</a>
+<a href=#282 id=282 data-nosnippet>282</a>    <span class="doccomment">/// An empty row group inside a deleted run, once selected and once skipped.
+<a href=#283 id=283 data-nosnippet>283</a>    </span><span class="attr">#[test]
+<a href=#284 id=284 data-nosnippet>284</a>    </span><span class="kw">fn </span>test_build_deletes_row_selection_empty_row_group_inside_run() {
+<a href=#285 id=285 data-nosnippet>285</a>        <span class="kw">let </span>schema_descr = get_test_schema_descr();
+<a href=#286 id=286 data-nosnippet>286</a>        <span class="kw">let </span>columns: Vec&lt;<span class="kw">_</span>&gt; = schema_descr
+<a href=#287 id=287 data-nosnippet>287</a>            .columns()
+<a href=#288 id=288 data-nosnippet>288</a>            .iter()
+<a href=#289 id=289 data-nosnippet>289</a>            .map(|ptr| ColumnChunkMetaData::builder(ptr.clone()).build().unwrap())
+<a href=#290 id=290 data-nosnippet>290</a>            .collect();
+<a href=#291 id=291 data-nosnippet>291</a>        <span class="kw">let </span>row_groups: Vec&lt;RowGroupMetaData&gt; = [<span class="number">3</span>, <span class="number">0</span>, <span class="number">3</span>]
+<a href=#292 id=292 data-nosnippet>292</a>            .iter()
+<a href=#293 id=293 data-nosnippet>293</a>            .enumerate()
+<a href=#294 id=294 data-nosnippet>294</a>            .map(|(i, <span class="kw-2">&amp;</span>n)| {
+<a href=#295 id=295 data-nosnippet>295</a>                build_test_row_group_meta(schema_descr.clone(), columns.clone(), n, i <span class="kw">as </span>i16)
+<a href=#296 id=296 data-nosnippet>296</a>            })
+<a href=#297 id=297 data-nosnippet>297</a>            .collect();
+<a href=#298 id=298 data-nosnippet>298</a>        <span class="comment">// Rows 1..5 are deleted: two in row group 0 and two in row group 2.
+<a href=#299 id=299 data-nosnippet>299</a>        </span><span class="kw">let </span>deletes = DeleteVector::new(RoaringTreemap::from_iter(<span class="number">1</span>..<span class="number">5</span>));
+<a href=#300 id=300 data-nosnippet>300</a>        <span class="kw">let </span>select = RowSelector::select;
+<a href=#301 id=301 data-nosnippet>301</a>        <span class="kw">let </span>skip = RowSelector::skip;
+<a href=#302 id=302 data-nosnippet>302</a>        <span class="kw">for </span>(selected, expected) <span class="kw">in </span>[
+<a href=#303 id=303 data-nosnippet>303</a>            (<span class="prelude-val">None</span>, <span class="macro">vec!</span>[select(<span class="number">1</span>), skip(<span class="number">4</span>), select(<span class="number">1</span>)]),
+<a href=#304 id=304 data-nosnippet>304</a>            (<span class="prelude-val">Some</span>(<span class="macro">vec!</span>[<span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>]), <span class="macro">vec!</span>[select(<span class="number">1</span>), skip(<span class="number">4</span>), select(<span class="number">1</span>)]),
+<a href=#305 id=305 data-nosnippet>305</a>            (<span class="prelude-val">Some</span>(<span class="macro">vec!</span>[<span class="number">0</span>, <span class="number">2</span>]), <span class="macro">vec!</span>[select(<span class="number">1</span>), skip(<span class="number">4</span>), select(<span class="number">1</span>)]),
+<a href=#306 id=306 data-nosnippet>306</a>            (<span class="prelude-val">Some</span>(<span class="macro">vec!</span>[<span class="number">1</span>, <span class="number">2</span>]), <span class="macro">vec!</span>[skip(<span class="number">2</span>), select(<span class="number">1</span>)]),
+<a href=#307 id=307 data-nosnippet>307</a>        ] {
+<a href=#308 id=308 data-nosnippet>308</a>            <span class="macro">assert_eq!</span>(
+<a href=#309 id=309 data-nosnippet>309</a>                ArrowReader::build_deletes_row_selection(<span class="kw-2">&amp;</span>row_groups, <span class="kw-2">&amp;</span>selected, <span class="kw-2">&amp;</span>deletes).unwrap(),
+<a href=#310 id=310 data-nosnippet>310</a>                RowSelection::from(expected),
+<a href=#311 id=311 data-nosnippet>311</a>                <span class="string">"selected {selected:?}"
+<a href=#312 id=312 data-nosnippet>312</a>            </span>);
+<a href=#313 id=313 data-nosnippet>313</a>        }
+<a href=#314 id=314 data-nosnippet>314</a>    }
+<a href=#315 id=315 data-nosnippet>315</a>
+<a href=#316 id=316 data-nosnippet>316</a>    <span class="doccomment">/// Random row group layouts and selections over the delete vectors the iterator tests use,
+<a href=#317 id=317 data-nosnippet>317</a>    /// compared with a selection built one deleted position at a time.
+<a href=#318 id=318 data-nosnippet>318</a>    </span><span class="attr">#[test]
+<a href=#319 id=319 data-nosnippet>319</a>    </span><span class="kw">fn </span>test_build_deletes_row_selection_matches_model() {
+<a href=#320 id=320 data-nosnippet>320</a>        <span class="kw">use </span>rand::rngs::StdRng;
+<a href=#321 id=321 data-nosnippet>321</a>        <span class="kw">use </span>rand::{Rng, SeedableRng};
+<a href=#322 id=322 data-nosnippet>322</a>
+<a href=#323 id=323 data-nosnippet>323</a>        <span class="kw">let </span><span class="kw-2">mut </span>rng = StdRng::seed_from_u64(<span class="number">3</span>);
+<a href=#324 id=324 data-nosnippet>324</a>        <span class="kw">let </span>schema_descr = get_test_schema_descr();
+<a href=#325 id=325 data-nosnippet>325</a>        <span class="kw">let </span>columns: Vec&lt;<span class="kw">_</span>&gt; = schema_descr
+<a href=#326 id=326 data-nosnippet>326</a>            .columns()
+<a href=#327 id=327 data-nosnippet>327</a>            .iter()
+<a href=#328 id=328 data-nosnippet>328</a>            .map(|ptr| ColumnChunkMetaData::builder(ptr.clone()).build().unwrap())
+<a href=#329 id=329 data-nosnippet>329</a>            .collect();
+<a href=#330 id=330 data-nosnippet>330</a>
+<a href=#331 id=331 data-nosnippet>331</a>        <span class="kw">for </span>deletes <span class="kw">in </span>mixed_delete_vectors() {
+<a href=#332 id=332 data-nosnippet>332</a>            <span class="kw">let </span>deleted: Vec&lt;u64&gt; = deletes.iter().collect();
+<a href=#333 id=333 data-nosnippet>333</a>            <span class="kw">for _ in </span><span class="number">0</span>..<span class="number">8 </span>{
+<a href=#334 id=334 data-nosnippet>334</a>                <span class="comment">// Half the time the file ends at or before a deleted position, so the deletes
+<a href=#335 id=335 data-nosnippet>335</a>                // past its end must be ignored.
+<a href=#336 id=336 data-nosnippet>336</a>                </span><span class="kw">let </span>num_rows = <span class="kw">match </span>deleted.len() {
+<a href=#337 id=337 data-nosnippet>337</a>                    n <span class="kw">if </span>n &gt; <span class="number">0 </span>&amp;&amp; rng.random_bool(<span class="number">0.5</span>) =&gt; {
+<a href=#338 id=338 data-nosnippet>338</a>                        deleted[rng.random_range(<span class="number">0</span>..n)].saturating_add(rng.random_range(<span class="number">0</span>..<span class="number">2</span>))
+<a href=#339 id=339 data-nosnippet>339</a>                    }
+<a href=#340 id=340 data-nosnippet>340</a>                    <span class="kw">_ </span>=&gt; deleted.last().map_or(<span class="number">0</span>, |<span class="kw-2">&amp;</span>last| last + <span class="number">1</span>) + rng.random_range(<span class="number">0</span>..<span class="number">100</span>),
+<a href=#341 id=341 data-nosnippet>341</a>                }
+<a href=#342 id=342 data-nosnippet>342</a>                .max(<span class="number">1</span>);
+<a href=#343 id=343 data-nosnippet>343</a>                <span class="comment">// Row group boundaries, half of them next to a deleted position so that they
+<a href=#344 id=344 data-nosnippet>344</a>                // fall inside runs. Repeated cuts leave empty row groups.
+<a href=#345 id=345 data-nosnippet>345</a>                </span><span class="kw">let </span><span class="kw-2">mut </span>cuts: Vec&lt;u64&gt; = (<span class="number">0</span>..rng.random_range(<span class="number">0</span>..<span class="number">8</span>))
+<a href=#346 id=346 data-nosnippet>346</a>                    .map(|<span class="kw">_</span>| <span class="kw">match </span>deleted.len() {
+<a href=#347 id=347 data-nosnippet>347</a>                        n <span class="kw">if </span>n &gt; <span class="number">0 </span>&amp;&amp; rng.random_bool(<span class="number">0.5</span>) =&gt; (deleted[rng.random_range(<span class="number">0</span>..n)]
+<a href=#348 id=348 data-nosnippet>348</a>                            + rng.random_range(<span class="number">0</span>..<span class="number">3</span>))
+<a href=#349 id=349 data-nosnippet>349</a>                        .saturating_sub(<span class="number">1</span>),
+<a href=#350 id=350 data-nosnippet>350</a>                        <span class="kw">_ </span>=&gt; rng.random_range(<span class="number">0</span>..num_rows),
+<a href=#351 id=351 data-nosnippet>351</a>                    })
+<a href=#352 id=352 data-nosnippet>352</a>                    .filter(|<span class="kw-2">&amp;</span>cut| cut &gt; <span class="number">0 </span>&amp;&amp; cut &lt; num_rows)
+<a href=#353 id=353 data-nosnippet>353</a>                    .collect();
+<a href=#354 id=354 data-nosnippet>354</a>                cuts.extend([<span class="number">0</span>, num_rows]);
+<a href=#355 id=355 data-nosnippet>355</a>                cuts.sort_unstable();
+<a href=#356 id=356 data-nosnippet>356</a>                <span class="kw">let </span>sizes: Vec&lt;u64&gt; = cuts.windows(<span class="number">2</span>).map(|w| w[<span class="number">1</span>] - w[<span class="number">0</span>]).collect();
+<a href=#357 id=357 data-nosnippet>357</a>                <span class="kw">let </span>row_groups: Vec&lt;RowGroupMetaData&gt; = sizes
+<a href=#358 id=358 data-nosnippet>358</a>                    .iter()
+<a href=#359 id=359 data-nosnippet>359</a>                    .enumerate()
+<a href=#360 id=360 data-nosnippet>360</a>                    .map(|(i, <span class="kw-2">&amp;</span>n)| {
+<a href=#361 id=361 data-nosnippet>361</a>                        build_test_row_group_meta(
+<a href=#362 id=362 data-nosnippet>362</a>                            schema_descr.clone(),
+<a href=#363 id=363 data-nosnippet>363</a>                            columns.clone(),
+<a href=#364 id=364 data-nosnippet>364</a>                            n <span class="kw">as </span>i64,
+<a href=#365 id=365 data-nosnippet>365</a>                            i <span class="kw">as </span>i16,
+<a href=#366 id=366 data-nosnippet>366</a>                        )
+<a href=#367 id=367 data-nosnippet>367</a>                    })
+<a href=#368 id=368 data-nosnippet>368</a>                    .collect();
+<a href=#369 id=369 data-nosnippet>369</a>                <span class="kw">let </span>selected = rng
+<a href=#370 id=370 data-nosnippet>370</a>                    .random_bool(<span class="number">0.75</span>)
+<a href=#371 id=371 data-nosnippet>371</a>                    .then(|| (<span class="number">0</span>..sizes.len()).filter(|<span class="kw">_</span>| rng.random_bool(<span class="number">0.5</span>)).collect());
 <a href=#372 id=372 data-nosnippet>372</a>
-<a href=#373 id=373 data-nosnippet>373</a>        <span class="kw">let </span>batch1 = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
-<a href=#374 id=374 data-nosnippet>374</a>            Int32Array::from_iter_values(<span class="number">1</span>..=<span class="number">100</span>),
-<a href=#375 id=375 data-nosnippet>375</a>        )])
-<a href=#376 id=376 data-nosnippet>376</a>        .unwrap();
-<a href=#377 id=377 data-nosnippet>377</a>
-<a href=#378 id=378 data-nosnippet>378</a>        <span class="kw">let </span>batch2 = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
-<a href=#379 id=379 data-nosnippet>379</a>            Int32Array::from_iter_values(<span class="number">101</span>..=<span class="number">200</span>),
-<a href=#380 id=380 data-nosnippet>380</a>        )])
-<a href=#381 id=381 data-nosnippet>381</a>        .unwrap();
+<a href=#373 id=373 data-nosnippet>373</a>                <span class="macro">assert_eq!</span>(
+<a href=#374 id=374 data-nosnippet>374</a>                    ArrowReader::build_deletes_row_selection(<span class="kw-2">&amp;</span>row_groups, <span class="kw-2">&amp;</span>selected, <span class="kw-2">&amp;</span>deletes)
+<a href=#375 id=375 data-nosnippet>375</a>                        .unwrap(),
+<a href=#376 id=376 data-nosnippet>376</a>                    expected_deletes_row_selection(<span class="kw-2">&amp;</span>sizes, <span class="kw-2">&amp;</span>selected, <span class="kw-2">&amp;</span>deleted),
+<a href=#377 id=377 data-nosnippet>377</a>                    <span class="string">"row groups {sizes:?}, selected {selected:?}"
+<a href=#378 id=378 data-nosnippet>378</a>                </span>);
+<a href=#379 id=379 data-nosnippet>379</a>            }
+<a href=#380 id=380 data-nosnippet>380</a>        }
+<a href=#381 id=381 data-nosnippet>381</a>    }
 <a href=#382 id=382 data-nosnippet>382</a>
-<a href=#383 id=383 data-nosnippet>383</a>        <span class="comment">// Force each batch into its own row group
-<a href=#384 id=384 data-nosnippet>384</a>        </span><span class="kw">let </span>props = WriterProperties::builder()
-<a href=#385 id=385 data-nosnippet>385</a>            .set_compression(Compression::SNAPPY)
-<a href=#386 id=386 data-nosnippet>386</a>            .set_max_row_group_row_count(<span class="prelude-val">Some</span>(<span class="number">100</span>))
-<a href=#387 id=387 data-nosnippet>387</a>            .build();
-<a href=#388 id=388 data-nosnippet>388</a>
-<a href=#389 id=389 data-nosnippet>389</a>        <span class="kw">let </span>file = File::create(<span class="kw-2">&amp;</span>data_file_path).unwrap();
-<a href=#390 id=390 data-nosnippet>390</a>        <span class="kw">let </span><span class="kw-2">mut </span>writer = ArrowWriter::try_new(file, arrow_schema.clone(), <span class="prelude-val">Some</span>(props)).unwrap();
-<a href=#391 id=391 data-nosnippet>391</a>        writer.write(<span class="kw-2">&amp;</span>batch1).expect(<span class="string">"Writing batch 1"</span>);
-<a href=#392 id=392 data-nosnippet>392</a>        writer.write(<span class="kw-2">&amp;</span>batch2).expect(<span class="string">"Writing batch 2"</span>);
-<a href=#393 id=393 data-nosnippet>393</a>        writer.close().unwrap();
-<a href=#394 id=394 data-nosnippet>394</a>
-<a href=#395 id=395 data-nosnippet>395</a>        <span class="comment">// Verify we created 2 row groups
-<a href=#396 id=396 data-nosnippet>396</a>        </span><span class="kw">let </span>verify_file = File::open(<span class="kw-2">&amp;</span>data_file_path).unwrap();
-<a href=#397 id=397 data-nosnippet>397</a>        <span class="kw">let </span>verify_reader = SerializedFileReader::new(verify_file).unwrap();
-<a href=#398 id=398 data-nosnippet>398</a>        <span class="macro">assert_eq!</span>(
-<a href=#399 id=399 data-nosnippet>399</a>            verify_reader.metadata().num_row_groups(),
-<a href=#400 id=400 data-nosnippet>400</a>            <span class="number">2</span>,
-<a href=#401 id=401 data-nosnippet>401</a>            <span class="string">"Should have 2 row groups"
-<a href=#402 id=402 data-nosnippet>402</a>        </span>);
-<a href=#403 id=403 data-nosnippet>403</a>
-<a href=#404 id=404 data-nosnippet>404</a>        <span class="comment">// Step 2: Create position delete file that deletes row 199 (id=200, last row in row group 1)
-<a href=#405 id=405 data-nosnippet>405</a>        </span><span class="kw">let </span>delete_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/deletes.parquet"</span>);
-<a href=#406 id=406 data-nosnippet>406</a>
-<a href=#407 id=407 data-nosnippet>407</a>        <span class="kw">let </span>delete_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
-<a href=#408 id=408 data-nosnippet>408</a>            Field::new(<span class="string">"file_path"</span>, DataType::Utf8, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
-<a href=#409 id=409 data-nosnippet>409</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
-<a href=#410 id=410 data-nosnippet>410</a>                FIELD_ID_POSITIONAL_DELETE_FILE_PATH.to_string(),
-<a href=#411 id=411 data-nosnippet>411</a>            )])),
-<a href=#412 id=412 data-nosnippet>412</a>            Field::new(<span class="string">"pos"</span>, DataType::Int64, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
-<a href=#413 id=413 data-nosnippet>413</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
-<a href=#414 id=414 data-nosnippet>414</a>                FIELD_ID_POSITIONAL_DELETE_POS.to_string(),
-<a href=#415 id=415 data-nosnippet>415</a>            )])),
-<a href=#416 id=416 data-nosnippet>416</a>        ]));
-<a href=#417 id=417 data-nosnippet>417</a>
-<a href=#418 id=418 data-nosnippet>418</a>        <span class="comment">// Delete row at position 199 (0-indexed, so it's the last row: id=200)
-<a href=#419 id=419 data-nosnippet>419</a>        </span><span class="kw">let </span>delete_batch = RecordBatch::try_new(delete_schema.clone(), <span class="macro">vec!</span>[
-<a href=#420 id=420 data-nosnippet>420</a>            Arc::new(StringArray::from_iter_values(<span class="macro">vec!</span>[data_file_path.clone()])),
-<a href=#421 id=421 data-nosnippet>421</a>            Arc::new(Int64Array::from_iter_values(<span class="macro">vec!</span>[<span class="number">199i64</span>])),
-<a href=#422 id=422 data-nosnippet>422</a>        ])
-<a href=#423 id=423 data-nosnippet>423</a>        .unwrap();
-<a href=#424 id=424 data-nosnippet>424</a>
-<a href=#425 id=425 data-nosnippet>425</a>        <span class="kw">let </span>delete_props = WriterProperties::builder()
-<a href=#426 id=426 data-nosnippet>426</a>            .set_compression(Compression::SNAPPY)
-<a href=#427 id=427 data-nosnippet>427</a>            .build();
+<a href=#383 id=383 data-nosnippet>383</a>    <span class="attr">#[test]
+<a href=#384 id=384 data-nosnippet>384</a>    </span><span class="kw">fn </span>test_build_deletes_row_selection() {
+<a href=#385 id=385 data-nosnippet>385</a>        <span class="kw">let </span>schema_descr = get_test_schema_descr();
+<a href=#386 id=386 data-nosnippet>386</a>
+<a href=#387 id=387 data-nosnippet>387</a>        <span class="kw">let </span><span class="kw-2">mut </span>columns = <span class="macro">vec!</span>[];
+<a href=#388 id=388 data-nosnippet>388</a>        <span class="kw">for </span>ptr <span class="kw">in </span>schema_descr.columns() {
+<a href=#389 id=389 data-nosnippet>389</a>            <span class="kw">let </span>column = ColumnChunkMetaData::builder(ptr.clone()).build().unwrap();
+<a href=#390 id=390 data-nosnippet>390</a>            columns.push(column);
+<a href=#391 id=391 data-nosnippet>391</a>        }
+<a href=#392 id=392 data-nosnippet>392</a>
+<a href=#393 id=393 data-nosnippet>393</a>        <span class="kw">let </span>row_groups_metadata = <span class="macro">vec!</span>[
+<a href=#394 id=394 data-nosnippet>394</a>            build_test_row_group_meta(schema_descr.clone(), columns.clone(), <span class="number">1000</span>, <span class="number">0</span>),
+<a href=#395 id=395 data-nosnippet>395</a>            build_test_row_group_meta(schema_descr.clone(), columns.clone(), <span class="number">500</span>, <span class="number">1</span>),
+<a href=#396 id=396 data-nosnippet>396</a>            build_test_row_group_meta(schema_descr.clone(), columns.clone(), <span class="number">500</span>, <span class="number">2</span>),
+<a href=#397 id=397 data-nosnippet>397</a>            build_test_row_group_meta(schema_descr.clone(), columns.clone(), <span class="number">1000</span>, <span class="number">3</span>),
+<a href=#398 id=398 data-nosnippet>398</a>            build_test_row_group_meta(schema_descr.clone(), columns.clone(), <span class="number">500</span>, <span class="number">4</span>),
+<a href=#399 id=399 data-nosnippet>399</a>        ];
+<a href=#400 id=400 data-nosnippet>400</a>
+<a href=#401 id=401 data-nosnippet>401</a>        <span class="kw">let </span>selected_row_groups = <span class="prelude-val">Some</span>(<span class="macro">vec!</span>[<span class="number">1</span>, <span class="number">3</span>]);
+<a href=#402 id=402 data-nosnippet>402</a>
+<a href=#403 id=403 data-nosnippet>403</a>        <span class="comment">/* cases to cover:
+<a href=#404 id=404 data-nosnippet>404</a>           * {skip|select} {first|intermediate|last} {one row|multiple rows} in
+<a href=#405 id=405 data-nosnippet>405</a>             {first|intermediate|last} {skipped|selected} row group
+<a href=#406 id=406 data-nosnippet>406</a>           * row group selection disabled
+<a href=#407 id=407 data-nosnippet>407</a>        */
+<a href=#408 id=408 data-nosnippet>408</a>
+<a href=#409 id=409 data-nosnippet>409</a>        </span><span class="kw">let </span>positional_deletes = RoaringTreemap::from_iter(<span class="kw-2">&amp;</span>[
+<a href=#410 id=410 data-nosnippet>410</a>            <span class="number">1</span>, <span class="comment">// in skipped rg 0, should be ignored
+<a href=#411 id=411 data-nosnippet>411</a>            </span><span class="number">3</span>, <span class="comment">// run of three consecutive items in skipped rg0
+<a href=#412 id=412 data-nosnippet>412</a>            </span><span class="number">4</span>, <span class="number">5</span>, <span class="number">998</span>, <span class="comment">// two consecutive items at end of skipped rg0
+<a href=#413 id=413 data-nosnippet>413</a>            </span><span class="number">999</span>, <span class="number">1000</span>, <span class="comment">// solitary row at start of selected rg1 (1, 9)
+<a href=#414 id=414 data-nosnippet>414</a>            </span><span class="number">1010</span>, <span class="comment">// run of 3 rows in selected rg1
+<a href=#415 id=415 data-nosnippet>415</a>            </span><span class="number">1011</span>, <span class="number">1012</span>, <span class="comment">// (3, 485)
+<a href=#416 id=416 data-nosnippet>416</a>            </span><span class="number">1498</span>, <span class="comment">// run of two items at end of selected rg1
+<a href=#417 id=417 data-nosnippet>417</a>            </span><span class="number">1499</span>, <span class="number">1500</span>, <span class="comment">// run of two items at start of skipped rg2
+<a href=#418 id=418 data-nosnippet>418</a>            </span><span class="number">1501</span>, <span class="number">1600</span>, <span class="comment">// should ignore, in skipped rg2
+<a href=#419 id=419 data-nosnippet>419</a>            </span><span class="number">1999</span>, <span class="comment">// single row at end of skipped rg2
+<a href=#420 id=420 data-nosnippet>420</a>            </span><span class="number">2000</span>, <span class="comment">// run of two items at start of selected rg3
+<a href=#421 id=421 data-nosnippet>421</a>            </span><span class="number">2001</span>, <span class="comment">// (4, 98)
+<a href=#422 id=422 data-nosnippet>422</a>            </span><span class="number">2100</span>, <span class="comment">// single row in selected row group 3 (1, 99)
+<a href=#423 id=423 data-nosnippet>423</a>            </span><span class="number">2200</span>, <span class="comment">// run of 3 consecutive rows in selected row group 3
+<a href=#424 id=424 data-nosnippet>424</a>            </span><span class="number">2201</span>, <span class="number">2202</span>, <span class="comment">// (3, 796)
+<a href=#425 id=425 data-nosnippet>425</a>            </span><span class="number">2999</span>, <span class="comment">// single item at end of selected rg3 (1)
+<a href=#426 id=426 data-nosnippet>426</a>            </span><span class="number">3000</span>, <span class="comment">// single item at start of skipped rg4
+<a href=#427 id=427 data-nosnippet>427</a>        </span>]);
 <a href=#428 id=428 data-nosnippet>428</a>
-<a href=#429 id=429 data-nosnippet>429</a>        <span class="kw">let </span>delete_file = File::create(<span class="kw-2">&amp;</span>delete_file_path).unwrap();
-<a href=#430 id=430 data-nosnippet>430</a>        <span class="kw">let </span><span class="kw-2">mut </span>delete_writer =
-<a href=#431 id=431 data-nosnippet>431</a>            ArrowWriter::try_new(delete_file, delete_schema, <span class="prelude-val">Some</span>(delete_props)).unwrap();
-<a href=#432 id=432 data-nosnippet>432</a>        delete_writer.write(<span class="kw-2">&amp;</span>delete_batch).unwrap();
-<a href=#433 id=433 data-nosnippet>433</a>        delete_writer.close().unwrap();
-<a href=#434 id=434 data-nosnippet>434</a>
-<a href=#435 id=435 data-nosnippet>435</a>        <span class="comment">// Step 3: Read the data file with the delete applied
-<a href=#436 id=436 data-nosnippet>436</a>        </span><span class="kw">let </span>file_io = FileIO::new_with_fs();
-<a href=#437 id=437 data-nosnippet>437</a>        <span class="kw">let </span>reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
+<a href=#429 id=429 data-nosnippet>429</a>        <span class="kw">let </span>positional_deletes = DeleteVector::new(positional_deletes);
+<a href=#430 id=430 data-nosnippet>430</a>
+<a href=#431 id=431 data-nosnippet>431</a>        <span class="comment">// using selected row groups 1 and 3
+<a href=#432 id=432 data-nosnippet>432</a>        </span><span class="kw">let </span>result = ArrowReader::build_deletes_row_selection(
+<a href=#433 id=433 data-nosnippet>433</a>            <span class="kw-2">&amp;</span>row_groups_metadata,
+<a href=#434 id=434 data-nosnippet>434</a>            <span class="kw-2">&amp;</span>selected_row_groups,
+<a href=#435 id=435 data-nosnippet>435</a>            <span class="kw-2">&amp;</span>positional_deletes,
+<a href=#436 id=436 data-nosnippet>436</a>        )
+<a href=#437 id=437 data-nosnippet>437</a>        .unwrap();
 <a href=#438 id=438 data-nosnippet>438</a>
-<a href=#439 id=439 data-nosnippet>439</a>        <span class="kw">let </span>task = FileScanTask::builder()
-<a href=#440 id=440 data-nosnippet>440</a>            .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>data_file_path).unwrap().len())
-<a href=#441 id=441 data-nosnippet>441</a>            .with_start(<span class="number">0</span>)
-<a href=#442 id=442 data-nosnippet>442</a>            .with_length(<span class="number">0</span>)
-<a href=#443 id=443 data-nosnippet>443</a>            .with_record_count(<span class="prelude-val">Some</span>(<span class="number">200</span>))
-<a href=#444 id=444 data-nosnippet>444</a>            .with_data_file_path(data_file_path.clone())
-<a href=#445 id=445 data-nosnippet>445</a>            .with_data_file_format(DataFileFormat::Parquet)
-<a href=#446 id=446 data-nosnippet>446</a>            .with_schema(table_schema.clone())
-<a href=#447 id=447 data-nosnippet>447</a>            .with_project_field_ids(<span class="macro">vec!</span>[<span class="number">1</span>])
-<a href=#448 id=448 data-nosnippet>448</a>            .with_deletes(<span class="macro">vec!</span>[
-<a href=#449 id=449 data-nosnippet>449</a>                FileScanTaskDeleteFile::builder()
-<a href=#450 id=450 data-nosnippet>450</a>                    .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>delete_file_path).unwrap().len())
-<a href=#451 id=451 data-nosnippet>451</a>                    .with_file_path(delete_file_path)
-<a href=#452 id=452 data-nosnippet>452</a>                    .with_file_type(DataContentType::PositionDeletes)
-<a href=#453 id=453 data-nosnippet>453</a>                    .with_file_format(DataFileFormat::Parquet)
-<a href=#454 id=454 data-nosnippet>454</a>                    .with_partition_spec_id(<span class="number">0</span>)
-<a href=#455 id=455 data-nosnippet>455</a>                    .build()
-<a href=#456 id=456 data-nosnippet>456</a>                    .unwrap(),
-<a href=#457 id=457 data-nosnippet>457</a>            ])
-<a href=#458 id=458 data-nosnippet>458</a>            .with_case_sensitive(<span class="bool-val">false</span>)
-<a href=#459 id=459 data-nosnippet>459</a>            .build()
-<a href=#460 id=460 data-nosnippet>460</a>            .unwrap();
-<a href=#461 id=461 data-nosnippet>461</a>
-<a href=#462 id=462 data-nosnippet>462</a>        <span class="kw">let </span>tasks = Box::pin(futures::stream::iter(<span class="macro">vec!</span>[<span class="prelude-val">Ok</span>(task)])) <span class="kw">as </span>FileScanTaskStream;
-<a href=#463 id=463 data-nosnippet>463</a>        <span class="kw">let </span>result = reader
-<a href=#464 id=464 data-nosnippet>464</a>            .read(tasks)
-<a href=#465 id=465 data-nosnippet>465</a>            .unwrap()
-<a href=#466 id=466 data-nosnippet>466</a>            .stream()
-<a href=#467 id=467 data-nosnippet>467</a>            .try_collect::&lt;Vec&lt;RecordBatch&gt;&gt;()
-<a href=#468 id=468 data-nosnippet>468</a>            .<span class="kw">await
-<a href=#469 id=469 data-nosnippet>469</a>            </span>.unwrap();
-<a href=#470 id=470 data-nosnippet>470</a>
-<a href=#471 id=471 data-nosnippet>471</a>        <span class="comment">// Step 4: Verify we got 199 rows (not 200)
-<a href=#472 id=472 data-nosnippet>472</a>        </span><span class="kw">let </span>total_rows: usize = result.iter().map(|b| b.num_rows()).sum();
-<a href=#473 id=473 data-nosnippet>473</a>
-<a href=#474 id=474 data-nosnippet>474</a>        <span class="macro">println!</span>(<span class="string">"Total rows read: {total_rows}"</span>);
-<a href=#475 id=475 data-nosnippet>475</a>        <span class="macro">println!</span>(<span class="string">"Expected: 199 rows (deleted row 199 which had id=200)"</span>);
-<a href=#476 id=476 data-nosnippet>476</a>
-<a href=#477 id=477 data-nosnippet>477</a>        <span class="comment">// This assertion will FAIL before the fix and PASS after the fix
-<a href=#478 id=478 data-nosnippet>478</a>        </span><span class="macro">assert_eq!</span>(
-<a href=#479 id=479 data-nosnippet>479</a>            total_rows, <span class="number">199</span>,
-<a href=#480 id=480 data-nosnippet>480</a>            <span class="string">"Expected 199 rows after deleting row 199, but got {total_rows} rows. \
-<a href=#481 id=481 data-nosnippet>481</a>             The bug causes position deletes in later row groups to be ignored."
-<a href=#482 id=482 data-nosnippet>482</a>        </span>);
-<a href=#483 id=483 data-nosnippet>483</a>
-<a href=#484 id=484 data-nosnippet>484</a>        <span class="comment">// Verify the deleted row (id=200) is not present
-<a href=#485 id=485 data-nosnippet>485</a>        </span><span class="kw">let </span>all_ids: Vec&lt;i32&gt; = result
-<a href=#486 id=486 data-nosnippet>486</a>            .iter()
-<a href=#487 id=487 data-nosnippet>487</a>            .flat_map(|batch| {
-<a href=#488 id=488 data-nosnippet>488</a>                batch
-<a href=#489 id=489 data-nosnippet>489</a>                    .column(<span class="number">0</span>)
-<a href=#490 id=490 data-nosnippet>490</a>                    .as_primitive::&lt;arrow_array::types::Int32Type&gt;()
-<a href=#491 id=491 data-nosnippet>491</a>                    .values()
-<a href=#492 id=492 data-nosnippet>492</a>                    .iter()
-<a href=#493 id=493 data-nosnippet>493</a>                    .copied()
-<a href=#494 id=494 data-nosnippet>494</a>            })
-<a href=#495 id=495 data-nosnippet>495</a>            .collect();
-<a href=#496 id=496 data-nosnippet>496</a>
-<a href=#497 id=497 data-nosnippet>497</a>        <span class="macro">assert!</span>(
-<a href=#498 id=498 data-nosnippet>498</a>            !all_ids.contains(<span class="kw-2">&amp;</span><span class="number">200</span>),
-<a href=#499 id=499 data-nosnippet>499</a>            <span class="string">"Row with id=200 should be deleted but was found in results"
-<a href=#500 id=500 data-nosnippet>500</a>        </span>);
-<a href=#501 id=501 data-nosnippet>501</a>
-<a href=#502 id=502 data-nosnippet>502</a>        <span class="comment">// Verify we have all other ids (1-199)
-<a href=#503 id=503 data-nosnippet>503</a>        </span><span class="kw">let </span>expected_ids: Vec&lt;i32&gt; = (<span class="number">1</span>..=<span class="number">199</span>).collect();
-<a href=#504 id=504 data-nosnippet>504</a>        <span class="macro">assert_eq!</span>(
-<a href=#505 id=505 data-nosnippet>505</a>            all_ids, expected_ids,
-<a href=#506 id=506 data-nosnippet>506</a>            <span class="string">"Should have ids 1-199 but got different values"
-<a href=#507 id=507 data-nosnippet>507</a>        </span>);
-<a href=#508 id=508 data-nosnippet>508</a>    }
-<a href=#509 id=509 data-nosnippet>509</a>
-<a href=#510 id=510 data-nosnippet>510</a>    <span class="doccomment">/// Test for bug where position deletes are lost when skipping unselected row groups.
-<a href=#511 id=511 data-nosnippet>511</a>    ///
-<a href=#512 id=512 data-nosnippet>512</a>    /// This is a variant of `test_position_delete_across_multiple_row_groups` that exercises
-<a href=#513 id=513 data-nosnippet>513</a>    /// the row group selection code path (`selected_row_groups: Some([...])`).
-<a href=#514 id=514 data-nosnippet>514</a>    ///
-<a href=#515 id=515 data-nosnippet>515</a>    /// When a file has multiple row groups and only some are selected for reading,
-<a href=#516 id=516 data-nosnippet>516</a>    /// the `build_deletes_row_selection` function must correctly skip over deletes in
-<a href=#517 id=517 data-nosnippet>517</a>    /// unselected row groups WITHOUT consuming deletes that belong to selected row groups.
-<a href=#518 id=518 data-nosnippet>518</a>    ///
-<a href=#519 id=519 data-nosnippet>519</a>    /// This test creates:
-<a href=#520 id=520 data-nosnippet>520</a>    /// - A data file with 200 rows split into 2 row groups (0-99, 100-199)
-<a href=#521 id=521 data-nosnippet>521</a>    /// - A position delete file that deletes row 199 (last row in second row group)
-<a href=#522 id=522 data-nosnippet>522</a>    /// - Row group selection that reads ONLY row group 1 (rows 100-199)
-<a href=#523 id=523 data-nosnippet>523</a>    ///
-<a href=#524 id=524 data-nosnippet>524</a>    /// Expected behavior: Should return 99 rows (with row 199 deleted)
-<a href=#525 id=525 data-nosnippet>525</a>    /// Bug behavior: Returns 100 rows (delete is lost when skipping row group 0)
-<a href=#526 id=526 data-nosnippet>526</a>    ///
-<a href=#527 id=527 data-nosnippet>527</a>    /// The bug occurs when processing row group 0 (unselected):
-<a href=#528 id=528 data-nosnippet>528</a>    /// ```rust
-<a href=#529 id=529 data-nosnippet>529</a>    /// delete_vector_iter.advance_to(next_row_group_base_idx); // Position at first delete &gt;= 100
-<a href=#530 id=530 data-nosnippet>530</a>    /// next_deleted_row_idx_opt = delete_vector_iter.next(); // BUG: Consumes delete at 199!
-<a href=#531 id=531 data-nosnippet>531</a>    /// ```
-<a href=#532 id=532 data-nosnippet>532</a>    ///
-<a href=#533 id=533 data-nosnippet>533</a>    /// The fix is to NOT call `next()` after `advance_to()` when skipping unselected row groups,
-<a href=#534 id=534 data-nosnippet>534</a>    /// because `advance_to()` already positions the iterator correctly without consuming elements.
-<a href=#535 id=535 data-nosnippet>535</a>    </span><span class="attr">#[tokio::test]
-<a href=#536 id=536 data-nosnippet>536</a>    </span><span class="kw">async fn </span>test_position_delete_with_row_group_selection() {
-<a href=#537 id=537 data-nosnippet>537</a>        <span class="kw">use </span>arrow_array::{Int32Array, Int64Array};
-<a href=#538 id=538 data-nosnippet>538</a>        <span class="kw">use </span>parquet::file::reader::{FileReader, SerializedFileReader};
-<a href=#539 id=539 data-nosnippet>539</a>
-<a href=#540 id=540 data-nosnippet>540</a>        <span class="comment">// Field IDs for positional delete schema
-<a href=#541 id=541 data-nosnippet>541</a>        </span><span class="kw">const </span>FIELD_ID_POSITIONAL_DELETE_FILE_PATH: u64 = <span class="number">2147483546</span>;
-<a href=#542 id=542 data-nosnippet>542</a>        <span class="kw">const </span>FIELD_ID_POSITIONAL_DELETE_POS: u64 = <span class="number">2147483545</span>;
-<a href=#543 id=543 data-nosnippet>543</a>
-<a href=#544 id=544 data-nosnippet>544</a>        <span class="kw">let </span>tmp_dir = TempDir::new().unwrap();
-<a href=#545 id=545 data-nosnippet>545</a>        <span class="kw">let </span>table_location = tmp_dir.path().to_str().unwrap().to_string();
+<a href=#439 id=439 data-nosnippet>439</a>        <span class="kw">let </span>expected = RowSelection::from(<span class="macro">vec!</span>[
+<a href=#440 id=440 data-nosnippet>440</a>            RowSelector::skip(<span class="number">1</span>),
+<a href=#441 id=441 data-nosnippet>441</a>            RowSelector::select(<span class="number">9</span>),
+<a href=#442 id=442 data-nosnippet>442</a>            RowSelector::skip(<span class="number">3</span>),
+<a href=#443 id=443 data-nosnippet>443</a>            RowSelector::select(<span class="number">485</span>),
+<a href=#444 id=444 data-nosnippet>444</a>            RowSelector::skip(<span class="number">4</span>),
+<a href=#445 id=445 data-nosnippet>445</a>            RowSelector::select(<span class="number">98</span>),
+<a href=#446 id=446 data-nosnippet>446</a>            RowSelector::skip(<span class="number">1</span>),
+<a href=#447 id=447 data-nosnippet>447</a>            RowSelector::select(<span class="number">99</span>),
+<a href=#448 id=448 data-nosnippet>448</a>            RowSelector::skip(<span class="number">3</span>),
+<a href=#449 id=449 data-nosnippet>449</a>            RowSelector::select(<span class="number">796</span>),
+<a href=#450 id=450 data-nosnippet>450</a>            RowSelector::skip(<span class="number">1</span>),
+<a href=#451 id=451 data-nosnippet>451</a>        ]);
+<a href=#452 id=452 data-nosnippet>452</a>
+<a href=#453 id=453 data-nosnippet>453</a>        <span class="macro">assert_eq!</span>(result, expected);
+<a href=#454 id=454 data-nosnippet>454</a>
+<a href=#455 id=455 data-nosnippet>455</a>        <span class="comment">// selecting all row groups
+<a href=#456 id=456 data-nosnippet>456</a>        </span><span class="kw">let </span>result = ArrowReader::build_deletes_row_selection(
+<a href=#457 id=457 data-nosnippet>457</a>            <span class="kw-2">&amp;</span>row_groups_metadata,
+<a href=#458 id=458 data-nosnippet>458</a>            <span class="kw-2">&amp;</span><span class="prelude-val">None</span>,
+<a href=#459 id=459 data-nosnippet>459</a>            <span class="kw-2">&amp;</span>positional_deletes,
+<a href=#460 id=460 data-nosnippet>460</a>        )
+<a href=#461 id=461 data-nosnippet>461</a>        .unwrap();
+<a href=#462 id=462 data-nosnippet>462</a>
+<a href=#463 id=463 data-nosnippet>463</a>        <span class="kw">let </span>expected = RowSelection::from(<span class="macro">vec!</span>[
+<a href=#464 id=464 data-nosnippet>464</a>            RowSelector::select(<span class="number">1</span>),
+<a href=#465 id=465 data-nosnippet>465</a>            RowSelector::skip(<span class="number">1</span>),
+<a href=#466 id=466 data-nosnippet>466</a>            RowSelector::select(<span class="number">1</span>),
+<a href=#467 id=467 data-nosnippet>467</a>            RowSelector::skip(<span class="number">3</span>),
+<a href=#468 id=468 data-nosnippet>468</a>            RowSelector::select(<span class="number">992</span>),
+<a href=#469 id=469 data-nosnippet>469</a>            RowSelector::skip(<span class="number">3</span>),
+<a href=#470 id=470 data-nosnippet>470</a>            RowSelector::select(<span class="number">9</span>),
+<a href=#471 id=471 data-nosnippet>471</a>            RowSelector::skip(<span class="number">3</span>),
+<a href=#472 id=472 data-nosnippet>472</a>            RowSelector::select(<span class="number">485</span>),
+<a href=#473 id=473 data-nosnippet>473</a>            RowSelector::skip(<span class="number">4</span>),
+<a href=#474 id=474 data-nosnippet>474</a>            RowSelector::select(<span class="number">98</span>),
+<a href=#475 id=475 data-nosnippet>475</a>            RowSelector::skip(<span class="number">1</span>),
+<a href=#476 id=476 data-nosnippet>476</a>            RowSelector::select(<span class="number">398</span>),
+<a href=#477 id=477 data-nosnippet>477</a>            RowSelector::skip(<span class="number">3</span>),
+<a href=#478 id=478 data-nosnippet>478</a>            RowSelector::select(<span class="number">98</span>),
+<a href=#479 id=479 data-nosnippet>479</a>            RowSelector::skip(<span class="number">1</span>),
+<a href=#480 id=480 data-nosnippet>480</a>            RowSelector::select(<span class="number">99</span>),
+<a href=#481 id=481 data-nosnippet>481</a>            RowSelector::skip(<span class="number">3</span>),
+<a href=#482 id=482 data-nosnippet>482</a>            RowSelector::select(<span class="number">796</span>),
+<a href=#483 id=483 data-nosnippet>483</a>            RowSelector::skip(<span class="number">2</span>),
+<a href=#484 id=484 data-nosnippet>484</a>            RowSelector::select(<span class="number">499</span>),
+<a href=#485 id=485 data-nosnippet>485</a>        ]);
+<a href=#486 id=486 data-nosnippet>486</a>
+<a href=#487 id=487 data-nosnippet>487</a>        <span class="macro">assert_eq!</span>(result, expected);
+<a href=#488 id=488 data-nosnippet>488</a>    }
+<a href=#489 id=489 data-nosnippet>489</a>
+<a href=#490 id=490 data-nosnippet>490</a>    <span class="doccomment">/// Test for bug where position deletes in later row groups are not applied correctly.
+<a href=#491 id=491 data-nosnippet>491</a>    ///
+<a href=#492 id=492 data-nosnippet>492</a>    /// When a file has multiple row groups and a position delete targets a row in a later
+<a href=#493 id=493 data-nosnippet>493</a>    /// row group, the `build_deletes_row_selection` function had a bug where it would
+<a href=#494 id=494 data-nosnippet>494</a>    /// fail to increment `current_row_group_base_idx` when skipping row groups.
+<a href=#495 id=495 data-nosnippet>495</a>    ///
+<a href=#496 id=496 data-nosnippet>496</a>    /// This test creates:
+<a href=#497 id=497 data-nosnippet>497</a>    /// - A data file with 200 rows split into 2 row groups (0-99, 100-199)
+<a href=#498 id=498 data-nosnippet>498</a>    /// - A position delete file that deletes row 199 (last row in second row group)
+<a href=#499 id=499 data-nosnippet>499</a>    ///
+<a href=#500 id=500 data-nosnippet>500</a>    /// Expected behavior: Should return 199 rows (with id=200 deleted)
+<a href=#501 id=501 data-nosnippet>501</a>    /// Bug behavior: Returns 200 rows (delete is not applied)
+<a href=#502 id=502 data-nosnippet>502</a>    ///
+<a href=#503 id=503 data-nosnippet>503</a>    /// This bug was discovered while running Apache Spark + Apache Iceberg integration tests
+<a href=#504 id=504 data-nosnippet>504</a>    /// through DataFusion Comet. The following Iceberg Java tests failed due to this bug:
+<a href=#505 id=505 data-nosnippet>505</a>    /// - `org.apache.iceberg.spark.extensions.TestMergeOnReadDelete::testDeleteWithMultipleRowGroupsParquet`
+<a href=#506 id=506 data-nosnippet>506</a>    /// - `org.apache.iceberg.spark.extensions.TestMergeOnReadUpdate::testUpdateWithMultipleRowGroupsParquet`
+<a href=#507 id=507 data-nosnippet>507</a>    </span><span class="attr">#[tokio::test]
+<a href=#508 id=508 data-nosnippet>508</a>    </span><span class="kw">async fn </span>test_position_delete_across_multiple_row_groups() {
+<a href=#509 id=509 data-nosnippet>509</a>        <span class="kw">use </span>arrow_array::{Int32Array, Int64Array};
+<a href=#510 id=510 data-nosnippet>510</a>        <span class="kw">use </span>parquet::file::reader::{FileReader, SerializedFileReader};
+<a href=#511 id=511 data-nosnippet>511</a>
+<a href=#512 id=512 data-nosnippet>512</a>        <span class="comment">// Field IDs for positional delete schema
+<a href=#513 id=513 data-nosnippet>513</a>        </span><span class="kw">const </span>FIELD_ID_POSITIONAL_DELETE_FILE_PATH: u64 = <span class="number">2147483546</span>;
+<a href=#514 id=514 data-nosnippet>514</a>        <span class="kw">const </span>FIELD_ID_POSITIONAL_DELETE_POS: u64 = <span class="number">2147483545</span>;
+<a href=#515 id=515 data-nosnippet>515</a>
+<a href=#516 id=516 data-nosnippet>516</a>        <span class="kw">let </span>tmp_dir = TempDir::new().unwrap();
+<a href=#517 id=517 data-nosnippet>517</a>        <span class="kw">let </span>table_location = tmp_dir.path().to_str().unwrap().to_string();
+<a href=#518 id=518 data-nosnippet>518</a>
+<a href=#519 id=519 data-nosnippet>519</a>        <span class="comment">// Create table schema with a single 'id' column
+<a href=#520 id=520 data-nosnippet>520</a>        </span><span class="kw">let </span>table_schema = Arc::new(
+<a href=#521 id=521 data-nosnippet>521</a>            Schema::builder()
+<a href=#522 id=522 data-nosnippet>522</a>                .with_schema_id(<span class="number">1</span>)
+<a href=#523 id=523 data-nosnippet>523</a>                .with_fields(<span class="macro">vec!</span>[
+<a href=#524 id=524 data-nosnippet>524</a>                    NestedField::required(<span class="number">1</span>, <span class="string">"id"</span>, Type::Primitive(PrimitiveType::Int)).into(),
+<a href=#525 id=525 data-nosnippet>525</a>                ])
+<a href=#526 id=526 data-nosnippet>526</a>                .build()
+<a href=#527 id=527 data-nosnippet>527</a>                .unwrap(),
+<a href=#528 id=528 data-nosnippet>528</a>        );
+<a href=#529 id=529 data-nosnippet>529</a>
+<a href=#530 id=530 data-nosnippet>530</a>        <span class="kw">let </span>arrow_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
+<a href=#531 id=531 data-nosnippet>531</a>            Field::new(<span class="string">"id"</span>, DataType::Int32, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
+<a href=#532 id=532 data-nosnippet>532</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
+<a href=#533 id=533 data-nosnippet>533</a>                <span class="string">"1"</span>.to_string(),
+<a href=#534 id=534 data-nosnippet>534</a>            )])),
+<a href=#535 id=535 data-nosnippet>535</a>        ]));
+<a href=#536 id=536 data-nosnippet>536</a>
+<a href=#537 id=537 data-nosnippet>537</a>        <span class="comment">// Step 1: Create data file with 200 rows in 2 row groups
+<a href=#538 id=538 data-nosnippet>538</a>        // Row group 0: rows 0-99 (ids 1-100)
+<a href=#539 id=539 data-nosnippet>539</a>        // Row group 1: rows 100-199 (ids 101-200)
+<a href=#540 id=540 data-nosnippet>540</a>        </span><span class="kw">let </span>data_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/data.parquet"</span>);
+<a href=#541 id=541 data-nosnippet>541</a>
+<a href=#542 id=542 data-nosnippet>542</a>        <span class="kw">let </span>batch1 = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
+<a href=#543 id=543 data-nosnippet>543</a>            Int32Array::from_iter_values(<span class="number">1</span>..=<span class="number">100</span>),
+<a href=#544 id=544 data-nosnippet>544</a>        )])
+<a href=#545 id=545 data-nosnippet>545</a>        .unwrap();
 <a href=#546 id=546 data-nosnippet>546</a>
-<a href=#547 id=547 data-nosnippet>547</a>        <span class="comment">// Create table schema with a single 'id' column
-<a href=#548 id=548 data-nosnippet>548</a>        </span><span class="kw">let </span>table_schema = Arc::new(
-<a href=#549 id=549 data-nosnippet>549</a>            Schema::builder()
-<a href=#550 id=550 data-nosnippet>550</a>                .with_schema_id(<span class="number">1</span>)
-<a href=#551 id=551 data-nosnippet>551</a>                .with_fields(<span class="macro">vec!</span>[
-<a href=#552 id=552 data-nosnippet>552</a>                    NestedField::required(<span class="number">1</span>, <span class="string">"id"</span>, Type::Primitive(PrimitiveType::Int)).into(),
-<a href=#553 id=553 data-nosnippet>553</a>                ])
-<a href=#554 id=554 data-nosnippet>554</a>                .build()
-<a href=#555 id=555 data-nosnippet>555</a>                .unwrap(),
-<a href=#556 id=556 data-nosnippet>556</a>        );
+<a href=#547 id=547 data-nosnippet>547</a>        <span class="kw">let </span>batch2 = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
+<a href=#548 id=548 data-nosnippet>548</a>            Int32Array::from_iter_values(<span class="number">101</span>..=<span class="number">200</span>),
+<a href=#549 id=549 data-nosnippet>549</a>        )])
+<a href=#550 id=550 data-nosnippet>550</a>        .unwrap();
+<a href=#551 id=551 data-nosnippet>551</a>
+<a href=#552 id=552 data-nosnippet>552</a>        <span class="comment">// Force each batch into its own row group
+<a href=#553 id=553 data-nosnippet>553</a>        </span><span class="kw">let </span>props = WriterProperties::builder()
+<a href=#554 id=554 data-nosnippet>554</a>            .set_compression(Compression::SNAPPY)
+<a href=#555 id=555 data-nosnippet>555</a>            .set_max_row_group_row_count(<span class="prelude-val">Some</span>(<span class="number">100</span>))
+<a href=#556 id=556 data-nosnippet>556</a>            .build();
 <a href=#557 id=557 data-nosnippet>557</a>
-<a href=#558 id=558 data-nosnippet>558</a>        <span class="kw">let </span>arrow_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
-<a href=#559 id=559 data-nosnippet>559</a>            Field::new(<span class="string">"id"</span>, DataType::Int32, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
-<a href=#560 id=560 data-nosnippet>560</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
-<a href=#561 id=561 data-nosnippet>561</a>                <span class="string">"1"</span>.to_string(),
-<a href=#562 id=562 data-nosnippet>562</a>            )])),
-<a href=#563 id=563 data-nosnippet>563</a>        ]));
-<a href=#564 id=564 data-nosnippet>564</a>
-<a href=#565 id=565 data-nosnippet>565</a>        <span class="comment">// Step 1: Create data file with 200 rows in 2 row groups
-<a href=#566 id=566 data-nosnippet>566</a>        // Row group 0: rows 0-99 (ids 1-100)
-<a href=#567 id=567 data-nosnippet>567</a>        // Row group 1: rows 100-199 (ids 101-200)
-<a href=#568 id=568 data-nosnippet>568</a>        </span><span class="kw">let </span>data_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/data.parquet"</span>);
-<a href=#569 id=569 data-nosnippet>569</a>
-<a href=#570 id=570 data-nosnippet>570</a>        <span class="kw">let </span>batch1 = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
-<a href=#571 id=571 data-nosnippet>571</a>            Int32Array::from_iter_values(<span class="number">1</span>..=<span class="number">100</span>),
-<a href=#572 id=572 data-nosnippet>572</a>        )])
-<a href=#573 id=573 data-nosnippet>573</a>        .unwrap();
-<a href=#574 id=574 data-nosnippet>574</a>
-<a href=#575 id=575 data-nosnippet>575</a>        <span class="kw">let </span>batch2 = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
-<a href=#576 id=576 data-nosnippet>576</a>            Int32Array::from_iter_values(<span class="number">101</span>..=<span class="number">200</span>),
-<a href=#577 id=577 data-nosnippet>577</a>        )])
-<a href=#578 id=578 data-nosnippet>578</a>        .unwrap();
-<a href=#579 id=579 data-nosnippet>579</a>
-<a href=#580 id=580 data-nosnippet>580</a>        <span class="comment">// Force each batch into its own row group
-<a href=#581 id=581 data-nosnippet>581</a>        </span><span class="kw">let </span>props = WriterProperties::builder()
-<a href=#582 id=582 data-nosnippet>582</a>            .set_compression(Compression::SNAPPY)
-<a href=#583 id=583 data-nosnippet>583</a>            .set_max_row_group_row_count(<span class="prelude-val">Some</span>(<span class="number">100</span>))
-<a href=#584 id=584 data-nosnippet>584</a>            .build();
-<a href=#585 id=585 data-nosnippet>585</a>
-<a href=#586 id=586 data-nosnippet>586</a>        <span class="kw">let </span>file = File::create(<span class="kw-2">&amp;</span>data_file_path).unwrap();
-<a href=#587 id=587 data-nosnippet>587</a>        <span class="kw">let </span><span class="kw-2">mut </span>writer = ArrowWriter::try_new(file, arrow_schema.clone(), <span class="prelude-val">Some</span>(props)).unwrap();
-<a href=#588 id=588 data-nosnippet>588</a>        writer.write(<span class="kw-2">&amp;</span>batch1).expect(<span class="string">"Writing batch 1"</span>);
-<a href=#589 id=589 data-nosnippet>589</a>        writer.write(<span class="kw-2">&amp;</span>batch2).expect(<span class="string">"Writing batch 2"</span>);
-<a href=#590 id=590 data-nosnippet>590</a>        writer.close().unwrap();
-<a href=#591 id=591 data-nosnippet>591</a>
-<a href=#592 id=592 data-nosnippet>592</a>        <span class="comment">// Verify we created 2 row groups
-<a href=#593 id=593 data-nosnippet>593</a>        </span><span class="kw">let </span>verify_file = File::open(<span class="kw-2">&amp;</span>data_file_path).unwrap();
-<a href=#594 id=594 data-nosnippet>594</a>        <span class="kw">let </span>verify_reader = SerializedFileReader::new(verify_file).unwrap();
-<a href=#595 id=595 data-nosnippet>595</a>        <span class="macro">assert_eq!</span>(
-<a href=#596 id=596 data-nosnippet>596</a>            verify_reader.metadata().num_row_groups(),
-<a href=#597 id=597 data-nosnippet>597</a>            <span class="number">2</span>,
-<a href=#598 id=598 data-nosnippet>598</a>            <span class="string">"Should have 2 row groups"
-<a href=#599 id=599 data-nosnippet>599</a>        </span>);
-<a href=#600 id=600 data-nosnippet>600</a>
-<a href=#601 id=601 data-nosnippet>601</a>        <span class="comment">// Step 2: Create position delete file that deletes row 199 (id=200, last row in row group 1)
-<a href=#602 id=602 data-nosnippet>602</a>        </span><span class="kw">let </span>delete_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/deletes.parquet"</span>);
+<a href=#558 id=558 data-nosnippet>558</a>        <span class="kw">let </span>file = File::create(<span class="kw-2">&amp;</span>data_file_path).unwrap();
+<a href=#559 id=559 data-nosnippet>559</a>        <span class="kw">let </span><span class="kw-2">mut </span>writer = ArrowWriter::try_new(file, arrow_schema.clone(), <span class="prelude-val">Some</span>(props)).unwrap();
+<a href=#560 id=560 data-nosnippet>560</a>        writer.write(<span class="kw-2">&amp;</span>batch1).expect(<span class="string">"Writing batch 1"</span>);
+<a href=#561 id=561 data-nosnippet>561</a>        writer.write(<span class="kw-2">&amp;</span>batch2).expect(<span class="string">"Writing batch 2"</span>);
+<a href=#562 id=562 data-nosnippet>562</a>        writer.close().unwrap();
+<a href=#563 id=563 data-nosnippet>563</a>
+<a href=#564 id=564 data-nosnippet>564</a>        <span class="comment">// Verify we created 2 row groups
+<a href=#565 id=565 data-nosnippet>565</a>        </span><span class="kw">let </span>verify_file = File::open(<span class="kw-2">&amp;</span>data_file_path).unwrap();
+<a href=#566 id=566 data-nosnippet>566</a>        <span class="kw">let </span>verify_reader = SerializedFileReader::new(verify_file).unwrap();
+<a href=#567 id=567 data-nosnippet>567</a>        <span class="macro">assert_eq!</span>(
+<a href=#568 id=568 data-nosnippet>568</a>            verify_reader.metadata().num_row_groups(),
+<a href=#569 id=569 data-nosnippet>569</a>            <span class="number">2</span>,
+<a href=#570 id=570 data-nosnippet>570</a>            <span class="string">"Should have 2 row groups"
+<a href=#571 id=571 data-nosnippet>571</a>        </span>);
+<a href=#572 id=572 data-nosnippet>572</a>
+<a href=#573 id=573 data-nosnippet>573</a>        <span class="comment">// Step 2: Create position delete file that deletes row 199 (id=200, last row in row group 1)
+<a href=#574 id=574 data-nosnippet>574</a>        </span><span class="kw">let </span>delete_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/deletes.parquet"</span>);
+<a href=#575 id=575 data-nosnippet>575</a>
+<a href=#576 id=576 data-nosnippet>576</a>        <span class="kw">let </span>delete_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
+<a href=#577 id=577 data-nosnippet>577</a>            Field::new(<span class="string">"file_path"</span>, DataType::Utf8, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
+<a href=#578 id=578 data-nosnippet>578</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
+<a href=#579 id=579 data-nosnippet>579</a>                FIELD_ID_POSITIONAL_DELETE_FILE_PATH.to_string(),
+<a href=#580 id=580 data-nosnippet>580</a>            )])),
+<a href=#581 id=581 data-nosnippet>581</a>            Field::new(<span class="string">"pos"</span>, DataType::Int64, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
+<a href=#582 id=582 data-nosnippet>582</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
+<a href=#583 id=583 data-nosnippet>583</a>                FIELD_ID_POSITIONAL_DELETE_POS.to_string(),
+<a href=#584 id=584 data-nosnippet>584</a>            )])),
+<a href=#585 id=585 data-nosnippet>585</a>        ]));
+<a href=#586 id=586 data-nosnippet>586</a>
+<a href=#587 id=587 data-nosnippet>587</a>        <span class="comment">// Delete row at position 199 (0-indexed, so it's the last row: id=200)
+<a href=#588 id=588 data-nosnippet>588</a>        </span><span class="kw">let </span>delete_batch = RecordBatch::try_new(delete_schema.clone(), <span class="macro">vec!</span>[
+<a href=#589 id=589 data-nosnippet>589</a>            Arc::new(StringArray::from_iter_values(<span class="macro">vec!</span>[data_file_path.clone()])),
+<a href=#590 id=590 data-nosnippet>590</a>            Arc::new(Int64Array::from_iter_values(<span class="macro">vec!</span>[<span class="number">199i64</span>])),
+<a href=#591 id=591 data-nosnippet>591</a>        ])
+<a href=#592 id=592 data-nosnippet>592</a>        .unwrap();
+<a href=#593 id=593 data-nosnippet>593</a>
+<a href=#594 id=594 data-nosnippet>594</a>        <span class="kw">let </span>delete_props = WriterProperties::builder()
+<a href=#595 id=595 data-nosnippet>595</a>            .set_compression(Compression::SNAPPY)
+<a href=#596 id=596 data-nosnippet>596</a>            .build();
+<a href=#597 id=597 data-nosnippet>597</a>
+<a href=#598 id=598 data-nosnippet>598</a>        <span class="kw">let </span>delete_file = File::create(<span class="kw-2">&amp;</span>delete_file_path).unwrap();
+<a href=#599 id=599 data-nosnippet>599</a>        <span class="kw">let </span><span class="kw-2">mut </span>delete_writer =
+<a href=#600 id=600 data-nosnippet>600</a>            ArrowWriter::try_new(delete_file, delete_schema, <span class="prelude-val">Some</span>(delete_props)).unwrap();
+<a href=#601 id=601 data-nosnippet>601</a>        delete_writer.write(<span class="kw-2">&amp;</span>delete_batch).unwrap();
+<a href=#602 id=602 data-nosnippet>602</a>        delete_writer.close().unwrap();
 <a href=#603 id=603 data-nosnippet>603</a>
-<a href=#604 id=604 data-nosnippet>604</a>        <span class="kw">let </span>delete_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
-<a href=#605 id=605 data-nosnippet>605</a>            Field::new(<span class="string">"file_path"</span>, DataType::Utf8, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
-<a href=#606 id=606 data-nosnippet>606</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
-<a href=#607 id=607 data-nosnippet>607</a>                FIELD_ID_POSITIONAL_DELETE_FILE_PATH.to_string(),
-<a href=#608 id=608 data-nosnippet>608</a>            )])),
-<a href=#609 id=609 data-nosnippet>609</a>            Field::new(<span class="string">"pos"</span>, DataType::Int64, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
-<a href=#610 id=610 data-nosnippet>610</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
-<a href=#611 id=611 data-nosnippet>611</a>                FIELD_ID_POSITIONAL_DELETE_POS.to_string(),
-<a href=#612 id=612 data-nosnippet>612</a>            )])),
-<a href=#613 id=613 data-nosnippet>613</a>        ]));
-<a href=#614 id=614 data-nosnippet>614</a>
-<a href=#615 id=615 data-nosnippet>615</a>        <span class="comment">// Delete row at position 199 (0-indexed, so it's the last row: id=200)
-<a href=#616 id=616 data-nosnippet>616</a>        </span><span class="kw">let </span>delete_batch = RecordBatch::try_new(delete_schema.clone(), <span class="macro">vec!</span>[
-<a href=#617 id=617 data-nosnippet>617</a>            Arc::new(StringArray::from_iter_values(<span class="macro">vec!</span>[data_file_path.clone()])),
-<a href=#618 id=618 data-nosnippet>618</a>            Arc::new(Int64Array::from_iter_values(<span class="macro">vec!</span>[<span class="number">199i64</span>])),
-<a href=#619 id=619 data-nosnippet>619</a>        ])
-<a href=#620 id=620 data-nosnippet>620</a>        .unwrap();
-<a href=#621 id=621 data-nosnippet>621</a>
-<a href=#622 id=622 data-nosnippet>622</a>        <span class="kw">let </span>delete_props = WriterProperties::builder()
-<a href=#623 id=623 data-nosnippet>623</a>            .set_compression(Compression::SNAPPY)
-<a href=#624 id=624 data-nosnippet>624</a>            .build();
-<a href=#625 id=625 data-nosnippet>625</a>
-<a href=#626 id=626 data-nosnippet>626</a>        <span class="kw">let </span>delete_file = File::create(<span class="kw-2">&amp;</span>delete_file_path).unwrap();
-<a href=#627 id=627 data-nosnippet>627</a>        <span class="kw">let </span><span class="kw-2">mut </span>delete_writer =
-<a href=#628 id=628 data-nosnippet>628</a>            ArrowWriter::try_new(delete_file, delete_schema, <span class="prelude-val">Some</span>(delete_props)).unwrap();
-<a href=#629 id=629 data-nosnippet>629</a>        delete_writer.write(<span class="kw-2">&amp;</span>delete_batch).unwrap();
-<a href=#630 id=630 data-nosnippet>630</a>        delete_writer.close().unwrap();
-<a href=#631 id=631 data-nosnippet>631</a>
-<a href=#632 id=632 data-nosnippet>632</a>        <span class="comment">// Step 3: Get byte ranges to read ONLY row group 1 (rows 100-199)
-<a href=#633 id=633 data-nosnippet>633</a>        // This exercises the row group selection code path where row group 0 is skipped
-<a href=#634 id=634 data-nosnippet>634</a>        </span><span class="kw">let </span>metadata_file = File::open(<span class="kw-2">&amp;</span>data_file_path).unwrap();
-<a href=#635 id=635 data-nosnippet>635</a>        <span class="kw">let </span>metadata_reader = SerializedFileReader::new(metadata_file).unwrap();
-<a href=#636 id=636 data-nosnippet>636</a>        <span class="kw">let </span>metadata = metadata_reader.metadata();
-<a href=#637 id=637 data-nosnippet>637</a>
-<a href=#638 id=638 data-nosnippet>638</a>        <span class="kw">let </span>row_group_0 = metadata.row_group(<span class="number">0</span>);
-<a href=#639 id=639 data-nosnippet>639</a>        <span class="kw">let </span>row_group_1 = metadata.row_group(<span class="number">1</span>);
-<a href=#640 id=640 data-nosnippet>640</a>
-<a href=#641 id=641 data-nosnippet>641</a>        <span class="kw">let </span>rg0_start = <span class="number">4u64</span>; <span class="comment">// Parquet files start with 4-byte magic "PAR1"
-<a href=#642 id=642 data-nosnippet>642</a>        </span><span class="kw">let </span>rg1_start = rg0_start + row_group_0.compressed_size() <span class="kw">as </span>u64;
-<a href=#643 id=643 data-nosnippet>643</a>        <span class="kw">let </span>rg1_length = row_group_1.compressed_size() <span class="kw">as </span>u64;
-<a href=#644 id=644 data-nosnippet>644</a>
-<a href=#645 id=645 data-nosnippet>645</a>        <span class="macro">println!</span>(
-<a href=#646 id=646 data-nosnippet>646</a>            <span class="string">"Row group 0: starts at byte {}, {} bytes compressed"</span>,
-<a href=#647 id=647 data-nosnippet>647</a>            rg0_start,
-<a href=#648 id=648 data-nosnippet>648</a>            row_group_0.compressed_size()
-<a href=#649 id=649 data-nosnippet>649</a>        );
-<a href=#650 id=650 data-nosnippet>650</a>        <span class="macro">println!</span>(
-<a href=#651 id=651 data-nosnippet>651</a>            <span class="string">"Row group 1: starts at byte {}, {} bytes compressed"</span>,
-<a href=#652 id=652 data-nosnippet>652</a>            rg1_start,
-<a href=#653 id=653 data-nosnippet>653</a>            row_group_1.compressed_size()
-<a href=#654 id=654 data-nosnippet>654</a>        );
-<a href=#655 id=655 data-nosnippet>655</a>
-<a href=#656 id=656 data-nosnippet>656</a>        <span class="kw">let </span>file_io = FileIO::new_with_fs();
-<a href=#657 id=657 data-nosnippet>657</a>        <span class="kw">let </span>reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
-<a href=#658 id=658 data-nosnippet>658</a>
-<a href=#659 id=659 data-nosnippet>659</a>        <span class="comment">// Create FileScanTask that reads ONLY row group 1 via byte range filtering
-<a href=#660 id=660 data-nosnippet>660</a>        </span><span class="kw">let </span>task = FileScanTask::builder()
-<a href=#661 id=661 data-nosnippet>661</a>            .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>data_file_path).unwrap().len())
-<a href=#662 id=662 data-nosnippet>662</a>            .with_start(rg1_start)
-<a href=#663 id=663 data-nosnippet>663</a>            .with_length(rg1_length)
-<a href=#664 id=664 data-nosnippet>664</a>            .with_record_count(<span class="prelude-val">Some</span>(<span class="number">100</span>)) <span class="comment">// Row group 1 has 100 rows
-<a href=#665 id=665 data-nosnippet>665</a>            </span>.with_data_file_path(data_file_path.clone())
-<a href=#666 id=666 data-nosnippet>666</a>            .with_data_file_format(DataFileFormat::Parquet)
-<a href=#667 id=667 data-nosnippet>667</a>            .with_schema(table_schema.clone())
-<a href=#668 id=668 data-nosnippet>668</a>            .with_project_field_ids(<span class="macro">vec!</span>[<span class="number">1</span>])
-<a href=#669 id=669 data-nosnippet>669</a>            .with_deletes(<span class="macro">vec!</span>[
-<a href=#670 id=670 data-nosnippet>670</a>                FileScanTaskDeleteFile::builder()
-<a href=#671 id=671 data-nosnippet>671</a>                    .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>delete_file_path).unwrap().len())
-<a href=#672 id=672 data-nosnippet>672</a>                    .with_file_path(delete_file_path)
-<a href=#673 id=673 data-nosnippet>673</a>                    .with_file_type(DataContentType::PositionDeletes)
-<a href=#674 id=674 data-nosnippet>674</a>                    .with_file_format(DataFileFormat::Parquet)
-<a href=#675 id=675 data-nosnippet>675</a>                    .with_partition_spec_id(<span class="number">0</span>)
-<a href=#676 id=676 data-nosnippet>676</a>                    .build()
-<a href=#677 id=677 data-nosnippet>677</a>                    .unwrap(),
-<a href=#678 id=678 data-nosnippet>678</a>            ])
-<a href=#679 id=679 data-nosnippet>679</a>            .with_case_sensitive(<span class="bool-val">false</span>)
-<a href=#680 id=680 data-nosnippet>680</a>            .build()
-<a href=#681 id=681 data-nosnippet>681</a>            .unwrap();
-<a href=#682 id=682 data-nosnippet>682</a>
-<a href=#683 id=683 data-nosnippet>683</a>        <span class="kw">let </span>tasks = Box::pin(futures::stream::iter(<span class="macro">vec!</span>[<span class="prelude-val">Ok</span>(task)])) <span class="kw">as </span>FileScanTaskStream;
-<a href=#684 id=684 data-nosnippet>684</a>        <span class="kw">let </span>result = reader
-<a href=#685 id=685 data-nosnippet>685</a>            .read(tasks)
-<a href=#686 id=686 data-nosnippet>686</a>            .unwrap()
-<a href=#687 id=687 data-nosnippet>687</a>            .stream()
-<a href=#688 id=688 data-nosnippet>688</a>            .try_collect::&lt;Vec&lt;RecordBatch&gt;&gt;()
-<a href=#689 id=689 data-nosnippet>689</a>            .<span class="kw">await
-<a href=#690 id=690 data-nosnippet>690</a>            </span>.unwrap();
-<a href=#691 id=691 data-nosnippet>691</a>
-<a href=#692 id=692 data-nosnippet>692</a>        <span class="comment">// Step 4: Verify we got 99 rows (not 100)
-<a href=#693 id=693 data-nosnippet>693</a>        // Row group 1 has 100 rows (ids 101-200), minus 1 delete (id=200) = 99 rows
-<a href=#694 id=694 data-nosnippet>694</a>        </span><span class="kw">let </span>total_rows: usize = result.iter().map(|b| b.num_rows()).sum();
-<a href=#695 id=695 data-nosnippet>695</a>
-<a href=#696 id=696 data-nosnippet>696</a>        <span class="macro">println!</span>(<span class="string">"Total rows read from row group 1: {total_rows}"</span>);
-<a href=#697 id=697 data-nosnippet>697</a>        <span class="macro">println!</span>(<span class="string">"Expected: 99 rows (row group 1 has 100 rows, 1 delete at position 199)"</span>);
-<a href=#698 id=698 data-nosnippet>698</a>
-<a href=#699 id=699 data-nosnippet>699</a>        <span class="comment">// This assertion will FAIL before the fix and PASS after the fix
-<a href=#700 id=700 data-nosnippet>700</a>        </span><span class="macro">assert_eq!</span>(
-<a href=#701 id=701 data-nosnippet>701</a>            total_rows, <span class="number">99</span>,
-<a href=#702 id=702 data-nosnippet>702</a>            <span class="string">"Expected 99 rows from row group 1 after deleting position 199, but got {total_rows} rows. \
-<a href=#703 id=703 data-nosnippet>703</a>             The bug causes position deletes to be lost when advance_to() is followed by next() \
-<a href=#704 id=704 data-nosnippet>704</a>             when skipping unselected row groups."
-<a href=#705 id=705 data-nosnippet>705</a>        </span>);
-<a href=#706 id=706 data-nosnippet>706</a>
-<a href=#707 id=707 data-nosnippet>707</a>        <span class="comment">// Verify the deleted row (id=200) is not present
-<a href=#708 id=708 data-nosnippet>708</a>        </span><span class="kw">let </span>all_ids: Vec&lt;i32&gt; = result
-<a href=#709 id=709 data-nosnippet>709</a>            .iter()
-<a href=#710 id=710 data-nosnippet>710</a>            .flat_map(|batch| {
-<a href=#711 id=711 data-nosnippet>711</a>                batch
-<a href=#712 id=712 data-nosnippet>712</a>                    .column(<span class="number">0</span>)
-<a href=#713 id=713 data-nosnippet>713</a>                    .as_primitive::&lt;arrow_array::types::Int32Type&gt;()
-<a href=#714 id=714 data-nosnippet>714</a>                    .values()
-<a href=#715 id=715 data-nosnippet>715</a>                    .iter()
-<a href=#716 id=716 data-nosnippet>716</a>                    .copied()
-<a href=#717 id=717 data-nosnippet>717</a>            })
-<a href=#718 id=718 data-nosnippet>718</a>            .collect();
-<a href=#719 id=719 data-nosnippet>719</a>
-<a href=#720 id=720 data-nosnippet>720</a>        <span class="macro">assert!</span>(
-<a href=#721 id=721 data-nosnippet>721</a>            !all_ids.contains(<span class="kw-2">&amp;</span><span class="number">200</span>),
-<a href=#722 id=722 data-nosnippet>722</a>            <span class="string">"Row with id=200 should be deleted but was found in results"
-<a href=#723 id=723 data-nosnippet>723</a>        </span>);
-<a href=#724 id=724 data-nosnippet>724</a>
-<a href=#725 id=725 data-nosnippet>725</a>        <span class="comment">// Verify we have ids 101-199 (not 101-200)
-<a href=#726 id=726 data-nosnippet>726</a>        </span><span class="kw">let </span>expected_ids: Vec&lt;i32&gt; = (<span class="number">101</span>..=<span class="number">199</span>).collect();
-<a href=#727 id=727 data-nosnippet>727</a>        <span class="macro">assert_eq!</span>(
-<a href=#728 id=728 data-nosnippet>728</a>            all_ids, expected_ids,
-<a href=#729 id=729 data-nosnippet>729</a>            <span class="string">"Should have ids 101-199 but got different values"
-<a href=#730 id=730 data-nosnippet>730</a>        </span>);
-<a href=#731 id=731 data-nosnippet>731</a>    }
-<a href=#732 id=732 data-nosnippet>732</a>
-<a href=#733 id=733 data-nosnippet>733</a>    <span class="doccomment">/// Test for bug where stale cached delete causes infinite loop when skipping row groups.
-<a href=#734 id=734 data-nosnippet>734</a>    ///
-<a href=#735 id=735 data-nosnippet>735</a>    /// This test exposes the inverse scenario of `test_position_delete_with_row_group_selection`:
-<a href=#736 id=736 data-nosnippet>736</a>    /// - Position delete targets a row in the SKIPPED row group (not the selected one)
-<a href=#737 id=737 data-nosnippet>737</a>    /// - After calling advance_to(), the cached delete index is stale
-<a href=#738 id=738 data-nosnippet>738</a>    /// - Without updating the cache, the code enters an infinite loop
-<a href=#739 id=739 data-nosnippet>739</a>    ///
-<a href=#740 id=740 data-nosnippet>740</a>    /// This test creates:
-<a href=#741 id=741 data-nosnippet>741</a>    /// - A data file with 200 rows split into 2 row groups (0-99, 100-199)
-<a href=#742 id=742 data-nosnippet>742</a>    /// - A position delete file that deletes row 0 (first row in SKIPPED row group 0)
-<a href=#743 id=743 data-nosnippet>743</a>    /// - Row group selection that reads ONLY row group 1 (rows 100-199)
-<a href=#744 id=744 data-nosnippet>744</a>    ///
-<a href=#745 id=745 data-nosnippet>745</a>    /// The bug occurs when skipping row group 0:
-<a href=#746 id=746 data-nosnippet>746</a>    /// ```rust
-<a href=#747 id=747 data-nosnippet>747</a>    /// let mut next_deleted_row_idx_opt = delete_vector_iter.next(); // Some(0)
-<a href=#748 id=748 data-nosnippet>748</a>    /// // ... skip to row group 1 ...
-<a href=#749 id=749 data-nosnippet>749</a>    /// delete_vector_iter.advance_to(100); // Iterator advances past delete at 0
-<a href=#750 id=750 data-nosnippet>750</a>    /// // BUG: next_deleted_row_idx_opt is still Some(0) - STALE!
-<a href=#751 id=751 data-nosnippet>751</a>    /// // When processing row group 1:
-<a href=#752 id=752 data-nosnippet>752</a>    /// //   current_idx = 100, next_deleted_row_idx = 0, next_row_group_base_idx = 200
-<a href=#753 id=753 data-nosnippet>753</a>    /// //   Loop condition: 0 &lt; 200 (true)
-<a href=#754 id=754 data-nosnippet>754</a>    /// //   But: current_idx (100) &gt; next_deleted_row_idx (0)
-<a href=#755 id=755 data-nosnippet>755</a>    /// //   And: current_idx (100) != next_deleted_row_idx (0)
-<a href=#756 id=756 data-nosnippet>756</a>    /// //   Neither branch executes -&gt; INFINITE LOOP!
-<a href=#757 id=757 data-nosnippet>757</a>    /// ```
-<a href=#758 id=758 data-nosnippet>758</a>    ///
-<a href=#759 id=759 data-nosnippet>759</a>    /// Expected behavior: Should return 100 rows (delete at 0 doesn't affect row group 1)
-<a href=#760 id=760 data-nosnippet>760</a>    /// Bug behavior: Infinite loop in build_deletes_row_selection
-<a href=#761 id=761 data-nosnippet>761</a>    </span><span class="attr">#[tokio::test]
-<a href=#762 id=762 data-nosnippet>762</a>    </span><span class="kw">async fn </span>test_position_delete_in_skipped_row_group() {
-<a href=#763 id=763 data-nosnippet>763</a>        <span class="kw">use </span>arrow_array::{Int32Array, Int64Array};
-<a href=#764 id=764 data-nosnippet>764</a>        <span class="kw">use </span>parquet::file::reader::{FileReader, SerializedFileReader};
-<a href=#765 id=765 data-nosnippet>765</a>
-<a href=#766 id=766 data-nosnippet>766</a>        <span class="comment">// Field IDs for positional delete schema
-<a href=#767 id=767 data-nosnippet>767</a>        </span><span class="kw">const </span>FIELD_ID_POSITIONAL_DELETE_FILE_PATH: u64 = <span class="number">2147483546</span>;
-<a href=#768 id=768 data-nosnippet>768</a>        <span class="kw">const </span>FIELD_ID_POSITIONAL_DELETE_POS: u64 = <span class="number">2147483545</span>;
+<a href=#604 id=604 data-nosnippet>604</a>        <span class="comment">// Step 3: Read the data file with the delete applied
+<a href=#605 id=605 data-nosnippet>605</a>        </span><span class="kw">let </span>file_io = FileIO::new_with_fs();
+<a href=#606 id=606 data-nosnippet>606</a>        <span class="kw">let </span>reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
+<a href=#607 id=607 data-nosnippet>607</a>
+<a href=#608 id=608 data-nosnippet>608</a>        <span class="kw">let </span>task = FileScanTask::builder()
+<a href=#609 id=609 data-nosnippet>609</a>            .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>data_file_path).unwrap().len())
+<a href=#610 id=610 data-nosnippet>610</a>            .with_start(<span class="number">0</span>)
+<a href=#611 id=611 data-nosnippet>611</a>            .with_length(<span class="number">0</span>)
+<a href=#612 id=612 data-nosnippet>612</a>            .with_record_count(<span class="prelude-val">Some</span>(<span class="number">200</span>))
+<a href=#613 id=613 data-nosnippet>613</a>            .with_data_file_path(data_file_path.clone())
+<a href=#614 id=614 data-nosnippet>614</a>            .with_data_file_format(DataFileFormat::Parquet)
+<a href=#615 id=615 data-nosnippet>615</a>            .with_schema(table_schema.clone())
+<a href=#616 id=616 data-nosnippet>616</a>            .with_project_field_ids(<span class="macro">vec!</span>[<span class="number">1</span>])
+<a href=#617 id=617 data-nosnippet>617</a>            .with_deletes(<span class="macro">vec!</span>[
+<a href=#618 id=618 data-nosnippet>618</a>                FileScanTaskDeleteFile::builder()
+<a href=#619 id=619 data-nosnippet>619</a>                    .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>delete_file_path).unwrap().len())
+<a href=#620 id=620 data-nosnippet>620</a>                    .with_file_path(delete_file_path)
+<a href=#621 id=621 data-nosnippet>621</a>                    .with_file_type(DataContentType::PositionDeletes)
+<a href=#622 id=622 data-nosnippet>622</a>                    .with_file_format(DataFileFormat::Parquet)
+<a href=#623 id=623 data-nosnippet>623</a>                    .with_partition_spec_id(<span class="number">0</span>)
+<a href=#624 id=624 data-nosnippet>624</a>                    .build()
+<a href=#625 id=625 data-nosnippet>625</a>                    .unwrap(),
+<a href=#626 id=626 data-nosnippet>626</a>            ])
+<a href=#627 id=627 data-nosnippet>627</a>            .with_case_sensitive(<span class="bool-val">false</span>)
+<a href=#628 id=628 data-nosnippet>628</a>            .build()
+<a href=#629 id=629 data-nosnippet>629</a>            .unwrap();
+<a href=#630 id=630 data-nosnippet>630</a>
+<a href=#631 id=631 data-nosnippet>631</a>        <span class="kw">let </span>tasks = Box::pin(futures::stream::iter(<span class="macro">vec!</span>[<span class="prelude-val">Ok</span>(task)])) <span class="kw">as </span>FileScanTaskStream;
+<a href=#632 id=632 data-nosnippet>632</a>        <span class="kw">let </span>result = reader
+<a href=#633 id=633 data-nosnippet>633</a>            .read(tasks)
+<a href=#634 id=634 data-nosnippet>634</a>            .unwrap()
+<a href=#635 id=635 data-nosnippet>635</a>            .stream()
+<a href=#636 id=636 data-nosnippet>636</a>            .try_collect::&lt;Vec&lt;RecordBatch&gt;&gt;()
+<a href=#637 id=637 data-nosnippet>637</a>            .<span class="kw">await
+<a href=#638 id=638 data-nosnippet>638</a>            </span>.unwrap();
+<a href=#639 id=639 data-nosnippet>639</a>
+<a href=#640 id=640 data-nosnippet>640</a>        <span class="comment">// Step 4: Verify we got 199 rows (not 200)
+<a href=#641 id=641 data-nosnippet>641</a>        </span><span class="kw">let </span>total_rows: usize = result.iter().map(|b| b.num_rows()).sum();
+<a href=#642 id=642 data-nosnippet>642</a>
+<a href=#643 id=643 data-nosnippet>643</a>        <span class="macro">println!</span>(<span class="string">"Total rows read: {total_rows}"</span>);
+<a href=#644 id=644 data-nosnippet>644</a>        <span class="macro">println!</span>(<span class="string">"Expected: 199 rows (deleted row 199 which had id=200)"</span>);
+<a href=#645 id=645 data-nosnippet>645</a>
+<a href=#646 id=646 data-nosnippet>646</a>        <span class="comment">// This assertion will FAIL before the fix and PASS after the fix
+<a href=#647 id=647 data-nosnippet>647</a>        </span><span class="macro">assert_eq!</span>(
+<a href=#648 id=648 data-nosnippet>648</a>            total_rows, <span class="number">199</span>,
+<a href=#649 id=649 data-nosnippet>649</a>            <span class="string">"Expected 199 rows after deleting row 199, but got {total_rows} rows. \
+<a href=#650 id=650 data-nosnippet>650</a>             The bug causes position deletes in later row groups to be ignored."
+<a href=#651 id=651 data-nosnippet>651</a>        </span>);
+<a href=#652 id=652 data-nosnippet>652</a>
+<a href=#653 id=653 data-nosnippet>653</a>        <span class="comment">// Verify the deleted row (id=200) is not present
+<a href=#654 id=654 data-nosnippet>654</a>        </span><span class="kw">let </span>all_ids: Vec&lt;i32&gt; = result
+<a href=#655 id=655 data-nosnippet>655</a>            .iter()
+<a href=#656 id=656 data-nosnippet>656</a>            .flat_map(|batch| {
+<a href=#657 id=657 data-nosnippet>657</a>                batch
+<a href=#658 id=658 data-nosnippet>658</a>                    .column(<span class="number">0</span>)
+<a href=#659 id=659 data-nosnippet>659</a>                    .as_primitive::&lt;arrow_array::types::Int32Type&gt;()
+<a href=#660 id=660 data-nosnippet>660</a>                    .values()
+<a href=#661 id=661 data-nosnippet>661</a>                    .iter()
+<a href=#662 id=662 data-nosnippet>662</a>                    .copied()
+<a href=#663 id=663 data-nosnippet>663</a>            })
+<a href=#664 id=664 data-nosnippet>664</a>            .collect();
+<a href=#665 id=665 data-nosnippet>665</a>
+<a href=#666 id=666 data-nosnippet>666</a>        <span class="macro">assert!</span>(
+<a href=#667 id=667 data-nosnippet>667</a>            !all_ids.contains(<span class="kw-2">&amp;</span><span class="number">200</span>),
+<a href=#668 id=668 data-nosnippet>668</a>            <span class="string">"Row with id=200 should be deleted but was found in results"
+<a href=#669 id=669 data-nosnippet>669</a>        </span>);
+<a href=#670 id=670 data-nosnippet>670</a>
+<a href=#671 id=671 data-nosnippet>671</a>        <span class="comment">// Verify we have all other ids (1-199)
+<a href=#672 id=672 data-nosnippet>672</a>        </span><span class="kw">let </span>expected_ids: Vec&lt;i32&gt; = (<span class="number">1</span>..=<span class="number">199</span>).collect();
+<a href=#673 id=673 data-nosnippet>673</a>        <span class="macro">assert_eq!</span>(
+<a href=#674 id=674 data-nosnippet>674</a>            all_ids, expected_ids,
+<a href=#675 id=675 data-nosnippet>675</a>            <span class="string">"Should have ids 1-199 but got different values"
+<a href=#676 id=676 data-nosnippet>676</a>        </span>);
+<a href=#677 id=677 data-nosnippet>677</a>    }
+<a href=#678 id=678 data-nosnippet>678</a>
+<a href=#679 id=679 data-nosnippet>679</a>    <span class="doccomment">/// Test for bug where position deletes are lost when skipping unselected row groups.
+<a href=#680 id=680 data-nosnippet>680</a>    ///
+<a href=#681 id=681 data-nosnippet>681</a>    /// This is a variant of `test_position_delete_across_multiple_row_groups` that exercises
+<a href=#682 id=682 data-nosnippet>682</a>    /// the row group selection code path (`selected_row_groups: Some([...])`).
+<a href=#683 id=683 data-nosnippet>683</a>    ///
+<a href=#684 id=684 data-nosnippet>684</a>    /// When a file has multiple row groups and only some are selected for reading,
+<a href=#685 id=685 data-nosnippet>685</a>    /// the `build_deletes_row_selection` function must correctly skip over deletes in
+<a href=#686 id=686 data-nosnippet>686</a>    /// unselected row groups WITHOUT consuming deletes that belong to selected row groups.
+<a href=#687 id=687 data-nosnippet>687</a>    ///
+<a href=#688 id=688 data-nosnippet>688</a>    /// This test creates:
+<a href=#689 id=689 data-nosnippet>689</a>    /// - A data file with 200 rows split into 2 row groups (0-99, 100-199)
+<a href=#690 id=690 data-nosnippet>690</a>    /// - A position delete file that deletes row 199 (last row in second row group)
+<a href=#691 id=691 data-nosnippet>691</a>    /// - Row group selection that reads ONLY row group 1 (rows 100-199)
+<a href=#692 id=692 data-nosnippet>692</a>    ///
+<a href=#693 id=693 data-nosnippet>693</a>    /// Expected behavior: Should return 99 rows (with row 199 deleted)
+<a href=#694 id=694 data-nosnippet>694</a>    /// Bug behavior: Returns 100 rows (delete is lost when skipping row group 0)
+<a href=#695 id=695 data-nosnippet>695</a>    ///
+<a href=#696 id=696 data-nosnippet>696</a>    /// The bug occurs when processing row group 0 (unselected):
+<a href=#697 id=697 data-nosnippet>697</a>    /// ```rust
+<a href=#698 id=698 data-nosnippet>698</a>    /// delete_vector_iter.advance_to(next_row_group_base_idx); // Position at first delete &gt;= 100
+<a href=#699 id=699 data-nosnippet>699</a>    /// next_deleted_row_idx_opt = delete_vector_iter.next(); // BUG: Consumes delete at 199!
+<a href=#700 id=700 data-nosnippet>700</a>    /// ```
+<a href=#701 id=701 data-nosnippet>701</a>    ///
+<a href=#702 id=702 data-nosnippet>702</a>    /// The fix is to NOT call `next()` after `advance_to()` when skipping unselected row groups,
+<a href=#703 id=703 data-nosnippet>703</a>    /// because `advance_to()` already positions the iterator correctly without consuming elements.
+<a href=#704 id=704 data-nosnippet>704</a>    </span><span class="attr">#[tokio::test]
+<a href=#705 id=705 data-nosnippet>705</a>    </span><span class="kw">async fn </span>test_position_delete_with_row_group_selection() {
+<a href=#706 id=706 data-nosnippet>706</a>        <span class="kw">use </span>arrow_array::{Int32Array, Int64Array};
+<a href=#707 id=707 data-nosnippet>707</a>        <span class="kw">use </span>parquet::file::reader::{FileReader, SerializedFileReader};
+<a href=#708 id=708 data-nosnippet>708</a>
+<a href=#709 id=709 data-nosnippet>709</a>        <span class="comment">// Field IDs for positional delete schema
+<a href=#710 id=710 data-nosnippet>710</a>        </span><span class="kw">const </span>FIELD_ID_POSITIONAL_DELETE_FILE_PATH: u64 = <span class="number">2147483546</span>;
+<a href=#711 id=711 data-nosnippet>711</a>        <span class="kw">const </span>FIELD_ID_POSITIONAL_DELETE_POS: u64 = <span class="number">2147483545</span>;
+<a href=#712 id=712 data-nosnippet>712</a>
+<a href=#713 id=713 data-nosnippet>713</a>        <span class="kw">let </span>tmp_dir = TempDir::new().unwrap();
+<a href=#714 id=714 data-nosnippet>714</a>        <span class="kw">let </span>table_location = tmp_dir.path().to_str().unwrap().to_string();
+<a href=#715 id=715 data-nosnippet>715</a>
+<a href=#716 id=716 data-nosnippet>716</a>        <span class="comment">// Create table schema with a single 'id' column
+<a href=#717 id=717 data-nosnippet>717</a>        </span><span class="kw">let </span>table_schema = Arc::new(
+<a href=#718 id=718 data-nosnippet>718</a>            Schema::builder()
+<a href=#719 id=719 data-nosnippet>719</a>                .with_schema_id(<span class="number">1</span>)
+<a href=#720 id=720 data-nosnippet>720</a>                .with_fields(<span class="macro">vec!</span>[
+<a href=#721 id=721 data-nosnippet>721</a>                    NestedField::required(<span class="number">1</span>, <span class="string">"id"</span>, Type::Primitive(PrimitiveType::Int)).into(),
+<a href=#722 id=722 data-nosnippet>722</a>                ])
+<a href=#723 id=723 data-nosnippet>723</a>                .build()
+<a href=#724 id=724 data-nosnippet>724</a>                .unwrap(),
+<a href=#725 id=725 data-nosnippet>725</a>        );
+<a href=#726 id=726 data-nosnippet>726</a>
+<a href=#727 id=727 data-nosnippet>727</a>        <span class="kw">let </span>arrow_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
+<a href=#728 id=728 data-nosnippet>728</a>            Field::new(<span class="string">"id"</span>, DataType::Int32, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
+<a href=#729 id=729 data-nosnippet>729</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
+<a href=#730 id=730 data-nosnippet>730</a>                <span class="string">"1"</span>.to_string(),
+<a href=#731 id=731 data-nosnippet>731</a>            )])),
+<a href=#732 id=732 data-nosnippet>732</a>        ]));
+<a href=#733 id=733 data-nosnippet>733</a>
+<a href=#734 id=734 data-nosnippet>734</a>        <span class="comment">// Step 1: Create data file with 200 rows in 2 row groups
+<a href=#735 id=735 data-nosnippet>735</a>        // Row group 0: rows 0-99 (ids 1-100)
+<a href=#736 id=736 data-nosnippet>736</a>        // Row group 1: rows 100-199 (ids 101-200)
+<a href=#737 id=737 data-nosnippet>737</a>        </span><span class="kw">let </span>data_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/data.parquet"</span>);
+<a href=#738 id=738 data-nosnippet>738</a>
+<a href=#739 id=739 data-nosnippet>739</a>        <span class="kw">let </span>batch1 = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
+<a href=#740 id=740 data-nosnippet>740</a>            Int32Array::from_iter_values(<span class="number">1</span>..=<span class="number">100</span>),
+<a href=#741 id=741 data-nosnippet>741</a>        )])
+<a href=#742 id=742 data-nosnippet>742</a>        .unwrap();
+<a href=#743 id=743 data-nosnippet>743</a>
+<a href=#744 id=744 data-nosnippet>744</a>        <span class="kw">let </span>batch2 = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
+<a href=#745 id=745 data-nosnippet>745</a>            Int32Array::from_iter_values(<span class="number">101</span>..=<span class="number">200</span>),
+<a href=#746 id=746 data-nosnippet>746</a>        )])
+<a href=#747 id=747 data-nosnippet>747</a>        .unwrap();
+<a href=#748 id=748 data-nosnippet>748</a>
+<a href=#749 id=749 data-nosnippet>749</a>        <span class="comment">// Force each batch into its own row group
+<a href=#750 id=750 data-nosnippet>750</a>        </span><span class="kw">let </span>props = WriterProperties::builder()
+<a href=#751 id=751 data-nosnippet>751</a>            .set_compression(Compression::SNAPPY)
+<a href=#752 id=752 data-nosnippet>752</a>            .set_max_row_group_row_count(<span class="prelude-val">Some</span>(<span class="number">100</span>))
+<a href=#753 id=753 data-nosnippet>753</a>            .build();
+<a href=#754 id=754 data-nosnippet>754</a>
+<a href=#755 id=755 data-nosnippet>755</a>        <span class="kw">let </span>file = File::create(<span class="kw-2">&amp;</span>data_file_path).unwrap();
+<a href=#756 id=756 data-nosnippet>756</a>        <span class="kw">let </span><span class="kw-2">mut </span>writer = ArrowWriter::try_new(file, arrow_schema.clone(), <span class="prelude-val">Some</span>(props)).unwrap();
+<a href=#757 id=757 data-nosnippet>757</a>        writer.write(<span class="kw-2">&amp;</span>batch1).expect(<span class="string">"Writing batch 1"</span>);
+<a href=#758 id=758 data-nosnippet>758</a>        writer.write(<span class="kw-2">&amp;</span>batch2).expect(<span class="string">"Writing batch 2"</span>);
+<a href=#759 id=759 data-nosnippet>759</a>        writer.close().unwrap();
+<a href=#760 id=760 data-nosnippet>760</a>
+<a href=#761 id=761 data-nosnippet>761</a>        <span class="comment">// Verify we created 2 row groups
+<a href=#762 id=762 data-nosnippet>762</a>        </span><span class="kw">let </span>verify_file = File::open(<span class="kw-2">&amp;</span>data_file_path).unwrap();
+<a href=#763 id=763 data-nosnippet>763</a>        <span class="kw">let </span>verify_reader = SerializedFileReader::new(verify_file).unwrap();
+<a href=#764 id=764 data-nosnippet>764</a>        <span class="macro">assert_eq!</span>(
+<a href=#765 id=765 data-nosnippet>765</a>            verify_reader.metadata().num_row_groups(),
+<a href=#766 id=766 data-nosnippet>766</a>            <span class="number">2</span>,
+<a href=#767 id=767 data-nosnippet>767</a>            <span class="string">"Should have 2 row groups"
+<a href=#768 id=768 data-nosnippet>768</a>        </span>);
 <a href=#769 id=769 data-nosnippet>769</a>
-<a href=#770 id=770 data-nosnippet>770</a>        <span class="kw">let </span>tmp_dir = TempDir::new().unwrap();
-<a href=#771 id=771 data-nosnippet>771</a>        <span class="kw">let </span>table_location = tmp_dir.path().to_str().unwrap().to_string();
+<a href=#770 id=770 data-nosnippet>770</a>        <span class="comment">// Step 2: Create position delete file that deletes row 199 (id=200, last row in row group 1)
+<a href=#771 id=771 data-nosnippet>771</a>        </span><span class="kw">let </span>delete_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/deletes.parquet"</span>);
 <a href=#772 id=772 data-nosnippet>772</a>
-<a href=#773 id=773 data-nosnippet>773</a>        <span class="comment">// Create table schema with a single 'id' column
-<a href=#774 id=774 data-nosnippet>774</a>        </span><span class="kw">let </span>table_schema = Arc::new(
-<a href=#775 id=775 data-nosnippet>775</a>            Schema::builder()
-<a href=#776 id=776 data-nosnippet>776</a>                .with_schema_id(<span class="number">1</span>)
-<a href=#777 id=777 data-nosnippet>777</a>                .with_fields(<span class="macro">vec!</span>[
-<a href=#778 id=778 data-nosnippet>778</a>                    NestedField::required(<span class="number">1</span>, <span class="string">"id"</span>, Type::Primitive(PrimitiveType::Int)).into(),
-<a href=#779 id=779 data-nosnippet>779</a>                ])
-<a href=#780 id=780 data-nosnippet>780</a>                .build()
-<a href=#781 id=781 data-nosnippet>781</a>                .unwrap(),
-<a href=#782 id=782 data-nosnippet>782</a>        );
+<a href=#773 id=773 data-nosnippet>773</a>        <span class="kw">let </span>delete_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
+<a href=#774 id=774 data-nosnippet>774</a>            Field::new(<span class="string">"file_path"</span>, DataType::Utf8, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
+<a href=#775 id=775 data-nosnippet>775</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
+<a href=#776 id=776 data-nosnippet>776</a>                FIELD_ID_POSITIONAL_DELETE_FILE_PATH.to_string(),
+<a href=#777 id=777 data-nosnippet>777</a>            )])),
+<a href=#778 id=778 data-nosnippet>778</a>            Field::new(<span class="string">"pos"</span>, DataType::Int64, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
+<a href=#779 id=779 data-nosnippet>779</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
+<a href=#780 id=780 data-nosnippet>780</a>                FIELD_ID_POSITIONAL_DELETE_POS.to_string(),
+<a href=#781 id=781 data-nosnippet>781</a>            )])),
+<a href=#782 id=782 data-nosnippet>782</a>        ]));
 <a href=#783 id=783 data-nosnippet>783</a>
-<a href=#784 id=784 data-nosnippet>784</a>        <span class="kw">let </span>arrow_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
-<a href=#785 id=785 data-nosnippet>785</a>            Field::new(<span class="string">"id"</span>, DataType::Int32, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
-<a href=#786 id=786 data-nosnippet>786</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
-<a href=#787 id=787 data-nosnippet>787</a>                <span class="string">"1"</span>.to_string(),
-<a href=#788 id=788 data-nosnippet>788</a>            )])),
-<a href=#789 id=789 data-nosnippet>789</a>        ]));
+<a href=#784 id=784 data-nosnippet>784</a>        <span class="comment">// Delete row at position 199 (0-indexed, so it's the last row: id=200)
+<a href=#785 id=785 data-nosnippet>785</a>        </span><span class="kw">let </span>delete_batch = RecordBatch::try_new(delete_schema.clone(), <span class="macro">vec!</span>[
+<a href=#786 id=786 data-nosnippet>786</a>            Arc::new(StringArray::from_iter_values(<span class="macro">vec!</span>[data_file_path.clone()])),
+<a href=#787 id=787 data-nosnippet>787</a>            Arc::new(Int64Array::from_iter_values(<span class="macro">vec!</span>[<span class="number">199i64</span>])),
+<a href=#788 id=788 data-nosnippet>788</a>        ])
+<a href=#789 id=789 data-nosnippet>789</a>        .unwrap();
 <a href=#790 id=790 data-nosnippet>790</a>
-<a href=#791 id=791 data-nosnippet>791</a>        <span class="comment">// Step 1: Create data file with 200 rows in 2 row groups
-<a href=#792 id=792 data-nosnippet>792</a>        // Row group 0: rows 0-99 (ids 1-100)
-<a href=#793 id=793 data-nosnippet>793</a>        // Row group 1: rows 100-199 (ids 101-200)
-<a href=#794 id=794 data-nosnippet>794</a>        </span><span class="kw">let </span>data_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/data.parquet"</span>);
-<a href=#795 id=795 data-nosnippet>795</a>
-<a href=#796 id=796 data-nosnippet>796</a>        <span class="kw">let </span>batch1 = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
-<a href=#797 id=797 data-nosnippet>797</a>            Int32Array::from_iter_values(<span class="number">1</span>..=<span class="number">100</span>),
-<a href=#798 id=798 data-nosnippet>798</a>        )])
-<a href=#799 id=799 data-nosnippet>799</a>        .unwrap();
+<a href=#791 id=791 data-nosnippet>791</a>        <span class="kw">let </span>delete_props = WriterProperties::builder()
+<a href=#792 id=792 data-nosnippet>792</a>            .set_compression(Compression::SNAPPY)
+<a href=#793 id=793 data-nosnippet>793</a>            .build();
+<a href=#794 id=794 data-nosnippet>794</a>
+<a href=#795 id=795 data-nosnippet>795</a>        <span class="kw">let </span>delete_file = File::create(<span class="kw-2">&amp;</span>delete_file_path).unwrap();
+<a href=#796 id=796 data-nosnippet>796</a>        <span class="kw">let </span><span class="kw-2">mut </span>delete_writer =
+<a href=#797 id=797 data-nosnippet>797</a>            ArrowWriter::try_new(delete_file, delete_schema, <span class="prelude-val">Some</span>(delete_props)).unwrap();
+<a href=#798 id=798 data-nosnippet>798</a>        delete_writer.write(<span class="kw-2">&amp;</span>delete_batch).unwrap();
+<a href=#799 id=799 data-nosnippet>799</a>        delete_writer.close().unwrap();
 <a href=#800 id=800 data-nosnippet>800</a>
-<a href=#801 id=801 data-nosnippet>801</a>        <span class="kw">let </span>batch2 = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
-<a href=#802 id=802 data-nosnippet>802</a>            Int32Array::from_iter_values(<span class="number">101</span>..=<span class="number">200</span>),
-<a href=#803 id=803 data-nosnippet>803</a>        )])
-<a href=#804 id=804 data-nosnippet>804</a>        .unwrap();
-<a href=#805 id=805 data-nosnippet>805</a>
-<a href=#806 id=806 data-nosnippet>806</a>        <span class="comment">// Force each batch into its own row group
-<a href=#807 id=807 data-nosnippet>807</a>        </span><span class="kw">let </span>props = WriterProperties::builder()
-<a href=#808 id=808 data-nosnippet>808</a>            .set_compression(Compression::SNAPPY)
-<a href=#809 id=809 data-nosnippet>809</a>            .set_max_row_group_row_count(<span class="prelude-val">Some</span>(<span class="number">100</span>))
-<a href=#810 id=810 data-nosnippet>810</a>            .build();
-<a href=#811 id=811 data-nosnippet>811</a>
-<a href=#812 id=812 data-nosnippet>812</a>        <span class="kw">let </span>file = File::create(<span class="kw-2">&amp;</span>data_file_path).unwrap();
-<a href=#813 id=813 data-nosnippet>813</a>        <span class="kw">let </span><span class="kw-2">mut </span>writer = ArrowWriter::try_new(file, arrow_schema.clone(), <span class="prelude-val">Some</span>(props)).unwrap();
-<a href=#814 id=814 data-nosnippet>814</a>        writer.write(<span class="kw-2">&amp;</span>batch1).expect(<span class="string">"Writing batch 1"</span>);
-<a href=#815 id=815 data-nosnippet>815</a>        writer.write(<span class="kw-2">&amp;</span>batch2).expect(<span class="string">"Writing batch 2"</span>);
-<a href=#816 id=816 data-nosnippet>816</a>        writer.close().unwrap();
-<a href=#817 id=817 data-nosnippet>817</a>
-<a href=#818 id=818 data-nosnippet>818</a>        <span class="comment">// Verify we created 2 row groups
-<a href=#819 id=819 data-nosnippet>819</a>        </span><span class="kw">let </span>verify_file = File::open(<span class="kw-2">&amp;</span>data_file_path).unwrap();
-<a href=#820 id=820 data-nosnippet>820</a>        <span class="kw">let </span>verify_reader = SerializedFileReader::new(verify_file).unwrap();
-<a href=#821 id=821 data-nosnippet>821</a>        <span class="macro">assert_eq!</span>(
-<a href=#822 id=822 data-nosnippet>822</a>            verify_reader.metadata().num_row_groups(),
-<a href=#823 id=823 data-nosnippet>823</a>            <span class="number">2</span>,
-<a href=#824 id=824 data-nosnippet>824</a>            <span class="string">"Should have 2 row groups"
-<a href=#825 id=825 data-nosnippet>825</a>        </span>);
-<a href=#826 id=826 data-nosnippet>826</a>
-<a href=#827 id=827 data-nosnippet>827</a>        <span class="comment">// Step 2: Create position delete file that deletes row 0 (id=1, first row in row group 0)
-<a href=#828 id=828 data-nosnippet>828</a>        </span><span class="kw">let </span>delete_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/deletes.parquet"</span>);
-<a href=#829 id=829 data-nosnippet>829</a>
-<a href=#830 id=830 data-nosnippet>830</a>        <span class="kw">let </span>delete_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
-<a href=#831 id=831 data-nosnippet>831</a>            Field::new(<span class="string">"file_path"</span>, DataType::Utf8, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
-<a href=#832 id=832 data-nosnippet>832</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
-<a href=#833 id=833 data-nosnippet>833</a>                FIELD_ID_POSITIONAL_DELETE_FILE_PATH.to_string(),
-<a href=#834 id=834 data-nosnippet>834</a>            )])),
-<a href=#835 id=835 data-nosnippet>835</a>            Field::new(<span class="string">"pos"</span>, DataType::Int64, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
-<a href=#836 id=836 data-nosnippet>836</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
-<a href=#837 id=837 data-nosnippet>837</a>                FIELD_ID_POSITIONAL_DELETE_POS.to_string(),
-<a href=#838 id=838 data-nosnippet>838</a>            )])),
-<a href=#839 id=839 data-nosnippet>839</a>        ]));
-<a href=#840 id=840 data-nosnippet>840</a>
-<a href=#841 id=841 data-nosnippet>841</a>        <span class="comment">// Delete row at position 0 (0-indexed, so it's the first row: id=1)
-<a href=#842 id=842 data-nosnippet>842</a>        </span><span class="kw">let </span>delete_batch = RecordBatch::try_new(delete_schema.clone(), <span class="macro">vec!</span>[
-<a href=#843 id=843 data-nosnippet>843</a>            Arc::new(StringArray::from_iter_values(<span class="macro">vec!</span>[data_file_path.clone()])),
-<a href=#844 id=844 data-nosnippet>844</a>            Arc::new(Int64Array::from_iter_values(<span class="macro">vec!</span>[<span class="number">0i64</span>])),
-<a href=#845 id=845 data-nosnippet>845</a>        ])
-<a href=#846 id=846 data-nosnippet>846</a>        .unwrap();
-<a href=#847 id=847 data-nosnippet>847</a>
-<a href=#848 id=848 data-nosnippet>848</a>        <span class="kw">let </span>delete_props = WriterProperties::builder()
-<a href=#849 id=849 data-nosnippet>849</a>            .set_compression(Compression::SNAPPY)
-<a href=#850 id=850 data-nosnippet>850</a>            .build();
+<a href=#801 id=801 data-nosnippet>801</a>        <span class="comment">// Step 3: Get byte ranges to read ONLY row group 1 (rows 100-199)
+<a href=#802 id=802 data-nosnippet>802</a>        // This exercises the row group selection code path where row group 0 is skipped
+<a href=#803 id=803 data-nosnippet>803</a>        </span><span class="kw">let </span>metadata_file = File::open(<span class="kw-2">&amp;</span>data_file_path).unwrap();
+<a href=#804 id=804 data-nosnippet>804</a>        <span class="kw">let </span>metadata_reader = SerializedFileReader::new(metadata_file).unwrap();
+<a href=#805 id=805 data-nosnippet>805</a>        <span class="kw">let </span>metadata = metadata_reader.metadata();
+<a href=#806 id=806 data-nosnippet>806</a>
+<a href=#807 id=807 data-nosnippet>807</a>        <span class="kw">let </span>row_group_0 = metadata.row_group(<span class="number">0</span>);
+<a href=#808 id=808 data-nosnippet>808</a>        <span class="kw">let </span>row_group_1 = metadata.row_group(<span class="number">1</span>);
+<a href=#809 id=809 data-nosnippet>809</a>
+<a href=#810 id=810 data-nosnippet>810</a>        <span class="kw">let </span>rg0_start = <span class="number">4u64</span>; <span class="comment">// Parquet files start with 4-byte magic "PAR1"
+<a href=#811 id=811 data-nosnippet>811</a>        </span><span class="kw">let </span>rg1_start = rg0_start + row_group_0.compressed_size() <span class="kw">as </span>u64;
+<a href=#812 id=812 data-nosnippet>812</a>        <span class="kw">let </span>rg1_length = row_group_1.compressed_size() <span class="kw">as </span>u64;
+<a href=#813 id=813 data-nosnippet>813</a>
+<a href=#814 id=814 data-nosnippet>814</a>        <span class="macro">println!</span>(
+<a href=#815 id=815 data-nosnippet>815</a>            <span class="string">"Row group 0: starts at byte {}, {} bytes compressed"</span>,
+<a href=#816 id=816 data-nosnippet>816</a>            rg0_start,
+<a href=#817 id=817 data-nosnippet>817</a>            row_group_0.compressed_size()
+<a href=#818 id=818 data-nosnippet>818</a>        );
+<a href=#819 id=819 data-nosnippet>819</a>        <span class="macro">println!</span>(
+<a href=#820 id=820 data-nosnippet>820</a>            <span class="string">"Row group 1: starts at byte {}, {} bytes compressed"</span>,
+<a href=#821 id=821 data-nosnippet>821</a>            rg1_start,
+<a href=#822 id=822 data-nosnippet>822</a>            row_group_1.compressed_size()
+<a href=#823 id=823 data-nosnippet>823</a>        );
+<a href=#824 id=824 data-nosnippet>824</a>
+<a href=#825 id=825 data-nosnippet>825</a>        <span class="kw">let </span>file_io = FileIO::new_with_fs();
+<a href=#826 id=826 data-nosnippet>826</a>        <span class="kw">let </span>reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
+<a href=#827 id=827 data-nosnippet>827</a>
+<a href=#828 id=828 data-nosnippet>828</a>        <span class="comment">// Create FileScanTask that reads ONLY row group 1 via byte range filtering
+<a href=#829 id=829 data-nosnippet>829</a>        </span><span class="kw">let </span>task = FileScanTask::builder()
+<a href=#830 id=830 data-nosnippet>830</a>            .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>data_file_path).unwrap().len())
+<a href=#831 id=831 data-nosnippet>831</a>            .with_start(rg1_start)
+<a href=#832 id=832 data-nosnippet>832</a>            .with_length(rg1_length)
+<a href=#833 id=833 data-nosnippet>833</a>            .with_record_count(<span class="prelude-val">Some</span>(<span class="number">100</span>)) <span class="comment">// Row group 1 has 100 rows
+<a href=#834 id=834 data-nosnippet>834</a>            </span>.with_data_file_path(data_file_path.clone())
+<a href=#835 id=835 data-nosnippet>835</a>            .with_data_file_format(DataFileFormat::Parquet)
+<a href=#836 id=836 data-nosnippet>836</a>            .with_schema(table_schema.clone())
+<a href=#837 id=837 data-nosnippet>837</a>            .with_project_field_ids(<span class="macro">vec!</span>[<span class="number">1</span>])
+<a href=#838 id=838 data-nosnippet>838</a>            .with_deletes(<span class="macro">vec!</span>[
+<a href=#839 id=839 data-nosnippet>839</a>                FileScanTaskDeleteFile::builder()
+<a href=#840 id=840 data-nosnippet>840</a>                    .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>delete_file_path).unwrap().len())
+<a href=#841 id=841 data-nosnippet>841</a>                    .with_file_path(delete_file_path)
+<a href=#842 id=842 data-nosnippet>842</a>                    .with_file_type(DataContentType::PositionDeletes)
+<a href=#843 id=843 data-nosnippet>843</a>                    .with_file_format(DataFileFormat::Parquet)
+<a href=#844 id=844 data-nosnippet>844</a>                    .with_partition_spec_id(<span class="number">0</span>)
+<a href=#845 id=845 data-nosnippet>845</a>                    .build()
+<a href=#846 id=846 data-nosnippet>846</a>                    .unwrap(),
+<a href=#847 id=847 data-nosnippet>847</a>            ])
+<a href=#848 id=848 data-nosnippet>848</a>            .with_case_sensitive(<span class="bool-val">false</span>)
+<a href=#849 id=849 data-nosnippet>849</a>            .build()
+<a href=#850 id=850 data-nosnippet>850</a>            .unwrap();
 <a href=#851 id=851 data-nosnippet>851</a>
-<a href=#852 id=852 data-nosnippet>852</a>        <span class="kw">let </span>delete_file = File::create(<span class="kw-2">&amp;</span>delete_file_path).unwrap();
-<a href=#853 id=853 data-nosnippet>853</a>        <span class="kw">let </span><span class="kw-2">mut </span>delete_writer =
-<a href=#854 id=854 data-nosnippet>854</a>            ArrowWriter::try_new(delete_file, delete_schema, <span class="prelude-val">Some</span>(delete_props)).unwrap();
-<a href=#855 id=855 data-nosnippet>855</a>        delete_writer.write(<span class="kw-2">&amp;</span>delete_batch).unwrap();
-<a href=#856 id=856 data-nosnippet>856</a>        delete_writer.close().unwrap();
-<a href=#857 id=857 data-nosnippet>857</a>
-<a href=#858 id=858 data-nosnippet>858</a>        <span class="comment">// Step 3: Get byte ranges to read ONLY row group 1 (rows 100-199)
-<a href=#859 id=859 data-nosnippet>859</a>        // This exercises the row group selection code path where row group 0 is skipped
-<a href=#860 id=860 data-nosnippet>860</a>        </span><span class="kw">let </span>metadata_file = File::open(<span class="kw-2">&amp;</span>data_file_path).unwrap();
-<a href=#861 id=861 data-nosnippet>861</a>        <span class="kw">let </span>metadata_reader = SerializedFileReader::new(metadata_file).unwrap();
-<a href=#862 id=862 data-nosnippet>862</a>        <span class="kw">let </span>metadata = metadata_reader.metadata();
-<a href=#863 id=863 data-nosnippet>863</a>
-<a href=#864 id=864 data-nosnippet>864</a>        <span class="kw">let </span>row_group_0 = metadata.row_group(<span class="number">0</span>);
-<a href=#865 id=865 data-nosnippet>865</a>        <span class="kw">let </span>row_group_1 = metadata.row_group(<span class="number">1</span>);
-<a href=#866 id=866 data-nosnippet>866</a>
-<a href=#867 id=867 data-nosnippet>867</a>        <span class="kw">let </span>rg0_start = <span class="number">4u64</span>; <span class="comment">// Parquet files start with 4-byte magic "PAR1"
-<a href=#868 id=868 data-nosnippet>868</a>        </span><span class="kw">let </span>rg1_start = rg0_start + row_group_0.compressed_size() <span class="kw">as </span>u64;
-<a href=#869 id=869 data-nosnippet>869</a>        <span class="kw">let </span>rg1_length = row_group_1.compressed_size() <span class="kw">as </span>u64;
-<a href=#870 id=870 data-nosnippet>870</a>
-<a href=#871 id=871 data-nosnippet>871</a>        <span class="kw">let </span>file_io = FileIO::new_with_fs();
-<a href=#872 id=872 data-nosnippet>872</a>        <span class="kw">let </span>reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
-<a href=#873 id=873 data-nosnippet>873</a>
-<a href=#874 id=874 data-nosnippet>874</a>        <span class="comment">// Create FileScanTask that reads ONLY row group 1 via byte range filtering
-<a href=#875 id=875 data-nosnippet>875</a>        </span><span class="kw">let </span>task = FileScanTask::builder()
-<a href=#876 id=876 data-nosnippet>876</a>            .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>data_file_path).unwrap().len())
-<a href=#877 id=877 data-nosnippet>877</a>            .with_start(rg1_start)
-<a href=#878 id=878 data-nosnippet>878</a>            .with_length(rg1_length)
-<a href=#879 id=879 data-nosnippet>879</a>            .with_record_count(<span class="prelude-val">Some</span>(<span class="number">100</span>)) <span class="comment">// Row group 1 has 100 rows
-<a href=#880 id=880 data-nosnippet>880</a>            </span>.with_data_file_path(data_file_path.clone())
-<a href=#881 id=881 data-nosnippet>881</a>            .with_data_file_format(DataFileFormat::Parquet)
-<a href=#882 id=882 data-nosnippet>882</a>            .with_schema(table_schema.clone())
-<a href=#883 id=883 data-nosnippet>883</a>            .with_project_field_ids(<span class="macro">vec!</span>[<span class="number">1</span>])
-<a href=#884 id=884 data-nosnippet>884</a>            .with_deletes(<span class="macro">vec!</span>[
-<a href=#885 id=885 data-nosnippet>885</a>                FileScanTaskDeleteFile::builder()
-<a href=#886 id=886 data-nosnippet>886</a>                    .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>delete_file_path).unwrap().len())
-<a href=#887 id=887 data-nosnippet>887</a>                    .with_file_path(delete_file_path)
-<a href=#888 id=888 data-nosnippet>888</a>                    .with_file_type(DataContentType::PositionDeletes)
-<a href=#889 id=889 data-nosnippet>889</a>                    .with_file_format(DataFileFormat::Parquet)
-<a href=#890 id=890 data-nosnippet>890</a>                    .with_partition_spec_id(<span class="number">0</span>)
-<a href=#891 id=891 data-nosnippet>891</a>                    .build()
-<a href=#892 id=892 data-nosnippet>892</a>                    .unwrap(),
-<a href=#893 id=893 data-nosnippet>893</a>            ])
-<a href=#894 id=894 data-nosnippet>894</a>            .with_case_sensitive(<span class="bool-val">false</span>)
-<a href=#895 id=895 data-nosnippet>895</a>            .build()
-<a href=#896 id=896 data-nosnippet>896</a>            .unwrap();
-<a href=#897 id=897 data-nosnippet>897</a>
-<a href=#898 id=898 data-nosnippet>898</a>        <span class="kw">let </span>tasks = Box::pin(futures::stream::iter(<span class="macro">vec!</span>[<span class="prelude-val">Ok</span>(task)])) <span class="kw">as </span>FileScanTaskStream;
-<a href=#899 id=899 data-nosnippet>899</a>        <span class="kw">let </span>result = reader
-<a href=#900 id=900 data-nosnippet>900</a>            .read(tasks)
-<a href=#901 id=901 data-nosnippet>901</a>            .unwrap()
-<a href=#902 id=902 data-nosnippet>902</a>            .stream()
-<a href=#903 id=903 data-nosnippet>903</a>            .try_collect::&lt;Vec&lt;RecordBatch&gt;&gt;()
-<a href=#904 id=904 data-nosnippet>904</a>            .<span class="kw">await
-<a href=#905 id=905 data-nosnippet>905</a>            </span>.unwrap();
-<a href=#906 id=906 data-nosnippet>906</a>
-<a href=#907 id=907 data-nosnippet>907</a>        <span class="comment">// Step 4: Verify we got 100 rows (all of row group 1)
-<a href=#908 id=908 data-nosnippet>908</a>        // The delete at position 0 is in row group 0, which is skipped, so it doesn't affect us
-<a href=#909 id=909 data-nosnippet>909</a>        </span><span class="kw">let </span>total_rows: usize = result.iter().map(|b| b.num_rows()).sum();
-<a href=#910 id=910 data-nosnippet>910</a>
-<a href=#911 id=911 data-nosnippet>911</a>        <span class="macro">assert_eq!</span>(
-<a href=#912 id=912 data-nosnippet>912</a>            total_rows, <span class="number">100</span>,
-<a href=#913 id=913 data-nosnippet>913</a>            <span class="string">"Expected 100 rows from row group 1 (delete at position 0 is in skipped row group 0). \
-<a href=#914 id=914 data-nosnippet>914</a>             If this hangs or fails, it indicates the cached delete index was not updated after advance_to()."
-<a href=#915 id=915 data-nosnippet>915</a>        </span>);
-<a href=#916 id=916 data-nosnippet>916</a>
-<a href=#917 id=917 data-nosnippet>917</a>        <span class="comment">// Verify we have all ids from row group 1 (101-200)
-<a href=#918 id=918 data-nosnippet>918</a>        </span><span class="kw">let </span>all_ids: Vec&lt;i32&gt; = result
-<a href=#919 id=919 data-nosnippet>919</a>            .iter()
-<a href=#920 id=920 data-nosnippet>920</a>            .flat_map(|batch| {
-<a href=#921 id=921 data-nosnippet>921</a>                batch
-<a href=#922 id=922 data-nosnippet>922</a>                    .column(<span class="number">0</span>)
-<a href=#923 id=923 data-nosnippet>923</a>                    .as_primitive::&lt;arrow_array::types::Int32Type&gt;()
-<a href=#924 id=924 data-nosnippet>924</a>                    .values()
-<a href=#925 id=925 data-nosnippet>925</a>                    .iter()
-<a href=#926 id=926 data-nosnippet>926</a>                    .copied()
-<a href=#927 id=927 data-nosnippet>927</a>            })
-<a href=#928 id=928 data-nosnippet>928</a>            .collect();
-<a href=#929 id=929 data-nosnippet>929</a>
-<a href=#930 id=930 data-nosnippet>930</a>        <span class="kw">let </span>expected_ids: Vec&lt;i32&gt; = (<span class="number">101</span>..=<span class="number">200</span>).collect();
-<a href=#931 id=931 data-nosnippet>931</a>        <span class="macro">assert_eq!</span>(
-<a href=#932 id=932 data-nosnippet>932</a>            all_ids, expected_ids,
-<a href=#933 id=933 data-nosnippet>933</a>            <span class="string">"Should have ids 101-200 (all of row group 1)"
-<a href=#934 id=934 data-nosnippet>934</a>        </span>);
-<a href=#935 id=935 data-nosnippet>935</a>    }
-<a href=#936 id=936 data-nosnippet>936</a>
-<a href=#937 id=937 data-nosnippet>937</a>    <span class="doccomment">/// End-to-end read of a data file with a V3 deletion vector applied. Exercises the whole
-<a href=#938 id=938 data-nosnippet>938</a>    /// deletion-vector read path together: the DV coordinates on the scan task, the loader
-<a href=#939 id=939 data-nosnippet>939</a>    /// reading the blob by range from its Puffin file, decoding it with DeleteVector::deserialize,
-<a href=#940 id=940 data-nosnippet>940</a>    /// validating cardinality against record_count, and ArrowReader filtering the deleted rows.
-<a href=#941 id=941 data-nosnippet>941</a>    </span><span class="attr">#[tokio::test]
-<a href=#942 id=942 data-nosnippet>942</a>    </span><span class="kw">async fn </span>test_deletion_vector_applied_end_to_end() {
-<a href=#943 id=943 data-nosnippet>943</a>        <span class="kw">use </span>arrow_array::Int32Array;
-<a href=#944 id=944 data-nosnippet>944</a>
-<a href=#945 id=945 data-nosnippet>945</a>        <span class="kw">let </span>tmp_dir = TempDir::new().unwrap();
-<a href=#946 id=946 data-nosnippet>946</a>        <span class="kw">let </span>table_location = tmp_dir.path().to_str().unwrap().to_string();
-<a href=#947 id=947 data-nosnippet>947</a>
-<a href=#948 id=948 data-nosnippet>948</a>        <span class="kw">let </span>table_schema = Arc::new(
-<a href=#949 id=949 data-nosnippet>949</a>            Schema::builder()
-<a href=#950 id=950 data-nosnippet>950</a>                .with_schema_id(<span class="number">1</span>)
-<a href=#951 id=951 data-nosnippet>951</a>                .with_fields(<span class="macro">vec!</span>[
-<a href=#952 id=952 data-nosnippet>952</a>                    NestedField::required(<span class="number">1</span>, <span class="string">"id"</span>, Type::Primitive(PrimitiveType::Int)).into(),
-<a href=#953 id=953 data-nosnippet>953</a>                ])
-<a href=#954 id=954 data-nosnippet>954</a>                .build()
-<a href=#955 id=955 data-nosnippet>955</a>                .unwrap(),
-<a href=#956 id=956 data-nosnippet>956</a>        );
-<a href=#957 id=957 data-nosnippet>957</a>        <span class="kw">let </span>arrow_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
-<a href=#958 id=958 data-nosnippet>958</a>            Field::new(<span class="string">"id"</span>, DataType::Int32, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
-<a href=#959 id=959 data-nosnippet>959</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
-<a href=#960 id=960 data-nosnippet>960</a>                <span class="string">"1"</span>.to_string(),
-<a href=#961 id=961 data-nosnippet>961</a>            )])),
-<a href=#962 id=962 data-nosnippet>962</a>        ]));
-<a href=#963 id=963 data-nosnippet>963</a>
-<a href=#964 id=964 data-nosnippet>964</a>        <span class="comment">// Data file: ids 1..=5 at positions 0..=4.
-<a href=#965 id=965 data-nosnippet>965</a>        </span><span class="kw">let </span>data_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/data.parquet"</span>);
-<a href=#966 id=966 data-nosnippet>966</a>        <span class="kw">let </span>batch = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
-<a href=#967 id=967 data-nosnippet>967</a>            Int32Array::from_iter_values(<span class="number">1</span>..=<span class="number">5</span>),
-<a href=#968 id=968 data-nosnippet>968</a>        )])
-<a href=#969 id=969 data-nosnippet>969</a>        .unwrap();
-<a href=#970 id=970 data-nosnippet>970</a>        <span class="kw">let </span>props = WriterProperties::builder()
-<a href=#971 id=971 data-nosnippet>971</a>            .set_compression(Compression::SNAPPY)
-<a href=#972 id=972 data-nosnippet>972</a>            .build();
-<a href=#973 id=973 data-nosnippet>973</a>        <span class="kw">let </span>file = File::create(<span class="kw-2">&amp;</span>data_file_path).unwrap();
-<a href=#974 id=974 data-nosnippet>974</a>        <span class="kw">let </span><span class="kw-2">mut </span>writer = ArrowWriter::try_new(file, arrow_schema.clone(), <span class="prelude-val">Some</span>(props)).unwrap();
-<a href=#975 id=975 data-nosnippet>975</a>        writer.write(<span class="kw-2">&amp;</span>batch).unwrap();
-<a href=#976 id=976 data-nosnippet>976</a>        writer.close().unwrap();
-<a href=#977 id=977 data-nosnippet>977</a>
-<a href=#978 id=978 data-nosnippet>978</a>        <span class="comment">// Deletion vector deleting positions 1 and 3 (ids 2 and 4), serialized as a
-<a href=#979 id=979 data-nosnippet>979</a>        // deletion-vector-v1 blob and embedded in a Puffin-like file at a non-zero offset.
-<a href=#980 id=980 data-nosnippet>980</a>        </span><span class="kw">let </span>blob = encode_dv_blob([<span class="number">1u64</span>, <span class="number">3</span>]);
-<a href=#981 id=981 data-nosnippet>981</a>        <span class="kw">let </span>content_offset = <span class="number">12u64</span>;
-<a href=#982 id=982 data-nosnippet>982</a>        <span class="kw">let </span>content_size = blob.len() <span class="kw">as </span>u64;
-<a href=#983 id=983 data-nosnippet>983</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv_file_bytes = <span class="macro">vec!</span>[<span class="number">0u8</span>; content_offset <span class="kw">as </span>usize];
-<a href=#984 id=984 data-nosnippet>984</a>        dv_file_bytes.extend_from_slice(<span class="kw-2">&amp;</span>blob);
-<a href=#985 id=985 data-nosnippet>985</a>        <span class="kw">let </span>dv_path = <span class="macro">format!</span>(<span class="string">"{table_location}/deletes.puffin"</span>);
-<a href=#986 id=986 data-nosnippet>986</a>        std::fs::write(<span class="kw-2">&amp;</span>dv_path, <span class="kw-2">&amp;</span>dv_file_bytes).unwrap();
-<a href=#987 id=987 data-nosnippet>987</a>
-<a href=#988 id=988 data-nosnippet>988</a>        <span class="kw">let </span>file_io = FileIO::new_with_fs();
-<a href=#989 id=989 data-nosnippet>989</a>        <span class="kw">let </span>reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
-<a href=#990 id=990 data-nosnippet>990</a>
-<a href=#991 id=991 data-nosnippet>991</a>        <span class="kw">let </span>task = FileScanTask::builder()
-<a href=#992 id=992 data-nosnippet>992</a>            .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>data_file_path).unwrap().len())
-<a href=#993 id=993 data-nosnippet>993</a>            .with_start(<span class="number">0</span>)
-<a href=#994 id=994 data-nosnippet>994</a>            .with_length(<span class="number">0</span>)
-<a href=#995 id=995 data-nosnippet>995</a>            .with_record_count(<span class="prelude-val">Some</span>(<span class="number">5</span>))
-<a href=#996 id=996 data-nosnippet>996</a>            .with_data_file_path(data_file_path.clone())
-<a href=#997 id=997 data-nosnippet>997</a>            .with_data_file_format(DataFileFormat::Parquet)
-<a href=#998 id=998 data-nosnippet>998</a>            .with_schema(table_schema.clone())
-<a href=#999 id=999 data-nosnippet>999</a>            .with_project_field_ids(<span class="macro">vec!</span>[<span class="number">1</span>])
-<a href=#1000 id=1000 data-nosnippet>1000</a>            .with_deletes(<span class="macro">vec!</span>[
-<a href=#1001 id=1001 data-nosnippet>1001</a>                FileScanTaskDeleteFile::builder()
-<a href=#1002 id=1002 data-nosnippet>1002</a>                    .with_file_path(dv_path.clone())
-<a href=#1003 id=1003 data-nosnippet>1003</a>                    .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>dv_path).unwrap().len())
-<a href=#1004 id=1004 data-nosnippet>1004</a>                    .with_file_type(DataContentType::PositionDeletes)
-<a href=#1005 id=1005 data-nosnippet>1005</a>                    .with_file_format(DataFileFormat::Puffin)
-<a href=#1006 id=1006 data-nosnippet>1006</a>                    .with_partition_spec_id(<span class="number">0</span>)
-<a href=#1007 id=1007 data-nosnippet>1007</a>                    .with_referenced_data_file(<span class="prelude-val">Some</span>(data_file_path.clone()))
-<a href=#1008 id=1008 data-nosnippet>1008</a>                    .with_content_offset(<span class="prelude-val">Some</span>(content_offset))
-<a href=#1009 id=1009 data-nosnippet>1009</a>                    .with_content_size_in_bytes(<span class="prelude-val">Some</span>(content_size))
-<a href=#1010 id=1010 data-nosnippet>1010</a>                    .with_record_count(<span class="prelude-val">Some</span>(<span class="number">2</span>))
-<a href=#1011 id=1011 data-nosnippet>1011</a>                    .build()
-<a href=#1012 id=1012 data-nosnippet>1012</a>                    .unwrap(),
-<a href=#1013 id=1013 data-nosnippet>1013</a>            ])
-<a href=#1014 id=1014 data-nosnippet>1014</a>            .with_case_sensitive(<span class="bool-val">false</span>)
-<a href=#1015 id=1015 data-nosnippet>1015</a>            .build()
-<a href=#1016 id=1016 data-nosnippet>1016</a>            .unwrap();
-<a href=#1017 id=1017 data-nosnippet>1017</a>
-<a href=#1018 id=1018 data-nosnippet>1018</a>        <span class="kw">let </span>tasks = Box::pin(futures::stream::iter(<span class="macro">vec!</span>[<span class="prelude-val">Ok</span>(task)])) <span class="kw">as </span>FileScanTaskStream;
-<a href=#1019 id=1019 data-nosnippet>1019</a>        <span class="kw">let </span>result = reader
-<a href=#1020 id=1020 data-nosnippet>1020</a>            .read(tasks)
-<a href=#1021 id=1021 data-nosnippet>1021</a>            .unwrap()
-<a href=#1022 id=1022 data-nosnippet>1022</a>            .stream()
-<a href=#1023 id=1023 data-nosnippet>1023</a>            .try_collect::&lt;Vec&lt;RecordBatch&gt;&gt;()
-<a href=#1024 id=1024 data-nosnippet>1024</a>            .<span class="kw">await
-<a href=#1025 id=1025 data-nosnippet>1025</a>            </span>.unwrap();
+<a href=#852 id=852 data-nosnippet>852</a>        <span class="kw">let </span>tasks = Box::pin(futures::stream::iter(<span class="macro">vec!</span>[<span class="prelude-val">Ok</span>(task)])) <span class="kw">as </span>FileScanTaskStream;
+<a href=#853 id=853 data-nosnippet>853</a>        <span class="kw">let </span>result = reader
+<a href=#854 id=854 data-nosnippet>854</a>            .read(tasks)
+<a href=#855 id=855 data-nosnippet>855</a>            .unwrap()
+<a href=#856 id=856 data-nosnippet>856</a>            .stream()
+<a href=#857 id=857 data-nosnippet>857</a>            .try_collect::&lt;Vec&lt;RecordBatch&gt;&gt;()
+<a href=#858 id=858 data-nosnippet>858</a>            .<span class="kw">await
+<a href=#859 id=859 data-nosnippet>859</a>            </span>.unwrap();
+<a href=#860 id=860 data-nosnippet>860</a>
+<a href=#861 id=861 data-nosnippet>861</a>        <span class="comment">// Step 4: Verify we got 99 rows (not 100)
+<a href=#862 id=862 data-nosnippet>862</a>        // Row group 1 has 100 rows (ids 101-200), minus 1 delete (id=200) = 99 rows
+<a href=#863 id=863 data-nosnippet>863</a>        </span><span class="kw">let </span>total_rows: usize = result.iter().map(|b| b.num_rows()).sum();
+<a href=#864 id=864 data-nosnippet>864</a>
+<a href=#865 id=865 data-nosnippet>865</a>        <span class="macro">println!</span>(<span class="string">"Total rows read from row group 1: {total_rows}"</span>);
+<a href=#866 id=866 data-nosnippet>866</a>        <span class="macro">println!</span>(<span class="string">"Expected: 99 rows (row group 1 has 100 rows, 1 delete at position 199)"</span>);
+<a href=#867 id=867 data-nosnippet>867</a>
+<a href=#868 id=868 data-nosnippet>868</a>        <span class="comment">// This assertion will FAIL before the fix and PASS after the fix
+<a href=#869 id=869 data-nosnippet>869</a>        </span><span class="macro">assert_eq!</span>(
+<a href=#870 id=870 data-nosnippet>870</a>            total_rows, <span class="number">99</span>,
+<a href=#871 id=871 data-nosnippet>871</a>            <span class="string">"Expected 99 rows from row group 1 after deleting position 199, but got {total_rows} rows. \
+<a href=#872 id=872 data-nosnippet>872</a>             The bug causes position deletes to be lost when advance_to() is followed by next() \
+<a href=#873 id=873 data-nosnippet>873</a>             when skipping unselected row groups."
+<a href=#874 id=874 data-nosnippet>874</a>        </span>);
+<a href=#875 id=875 data-nosnippet>875</a>
+<a href=#876 id=876 data-nosnippet>876</a>        <span class="comment">// Verify the deleted row (id=200) is not present
+<a href=#877 id=877 data-nosnippet>877</a>        </span><span class="kw">let </span>all_ids: Vec&lt;i32&gt; = result
+<a href=#878 id=878 data-nosnippet>878</a>            .iter()
+<a href=#879 id=879 data-nosnippet>879</a>            .flat_map(|batch| {
+<a href=#880 id=880 data-nosnippet>880</a>                batch
+<a href=#881 id=881 data-nosnippet>881</a>                    .column(<span class="number">0</span>)
+<a href=#882 id=882 data-nosnippet>882</a>                    .as_primitive::&lt;arrow_array::types::Int32Type&gt;()
+<a href=#883 id=883 data-nosnippet>883</a>                    .values()
+<a href=#884 id=884 data-nosnippet>884</a>                    .iter()
+<a href=#885 id=885 data-nosnippet>885</a>                    .copied()
+<a href=#886 id=886 data-nosnippet>886</a>            })
+<a href=#887 id=887 data-nosnippet>887</a>            .collect();
+<a href=#888 id=888 data-nosnippet>888</a>
+<a href=#889 id=889 data-nosnippet>889</a>        <span class="macro">assert!</span>(
+<a href=#890 id=890 data-nosnippet>890</a>            !all_ids.contains(<span class="kw-2">&amp;</span><span class="number">200</span>),
+<a href=#891 id=891 data-nosnippet>891</a>            <span class="string">"Row with id=200 should be deleted but was found in results"
+<a href=#892 id=892 data-nosnippet>892</a>        </span>);
+<a href=#893 id=893 data-nosnippet>893</a>
+<a href=#894 id=894 data-nosnippet>894</a>        <span class="comment">// Verify we have ids 101-199 (not 101-200)
+<a href=#895 id=895 data-nosnippet>895</a>        </span><span class="kw">let </span>expected_ids: Vec&lt;i32&gt; = (<span class="number">101</span>..=<span class="number">199</span>).collect();
+<a href=#896 id=896 data-nosnippet>896</a>        <span class="macro">assert_eq!</span>(
+<a href=#897 id=897 data-nosnippet>897</a>            all_ids, expected_ids,
+<a href=#898 id=898 data-nosnippet>898</a>            <span class="string">"Should have ids 101-199 but got different values"
+<a href=#899 id=899 data-nosnippet>899</a>        </span>);
+<a href=#900 id=900 data-nosnippet>900</a>    }
+<a href=#901 id=901 data-nosnippet>901</a>
+<a href=#902 id=902 data-nosnippet>902</a>    <span class="doccomment">/// Test for bug where stale cached delete causes infinite loop when skipping row groups.
+<a href=#903 id=903 data-nosnippet>903</a>    ///
+<a href=#904 id=904 data-nosnippet>904</a>    /// This test exposes the inverse scenario of `test_position_delete_with_row_group_selection`:
+<a href=#905 id=905 data-nosnippet>905</a>    /// - Position delete targets a row in the SKIPPED row group (not the selected one)
+<a href=#906 id=906 data-nosnippet>906</a>    /// - After calling advance_to(), the cached delete index is stale
+<a href=#907 id=907 data-nosnippet>907</a>    /// - Without updating the cache, the code enters an infinite loop
+<a href=#908 id=908 data-nosnippet>908</a>    ///
+<a href=#909 id=909 data-nosnippet>909</a>    /// This test creates:
+<a href=#910 id=910 data-nosnippet>910</a>    /// - A data file with 200 rows split into 2 row groups (0-99, 100-199)
+<a href=#911 id=911 data-nosnippet>911</a>    /// - A position delete file that deletes row 0 (first row in SKIPPED row group 0)
+<a href=#912 id=912 data-nosnippet>912</a>    /// - Row group selection that reads ONLY row group 1 (rows 100-199)
+<a href=#913 id=913 data-nosnippet>913</a>    ///
+<a href=#914 id=914 data-nosnippet>914</a>    /// The bug occurs when skipping row group 0:
+<a href=#915 id=915 data-nosnippet>915</a>    /// ```rust
+<a href=#916 id=916 data-nosnippet>916</a>    /// let mut next_deleted_row_idx_opt = delete_vector_iter.next(); // Some(0)
+<a href=#917 id=917 data-nosnippet>917</a>    /// // ... skip to row group 1 ...
+<a href=#918 id=918 data-nosnippet>918</a>    /// delete_vector_iter.advance_to(100); // Iterator advances past delete at 0
+<a href=#919 id=919 data-nosnippet>919</a>    /// // BUG: next_deleted_row_idx_opt is still Some(0) - STALE!
+<a href=#920 id=920 data-nosnippet>920</a>    /// // When processing row group 1:
+<a href=#921 id=921 data-nosnippet>921</a>    /// //   current_idx = 100, next_deleted_row_idx = 0, next_row_group_base_idx = 200
+<a href=#922 id=922 data-nosnippet>922</a>    /// //   Loop condition: 0 &lt; 200 (true)
+<a href=#923 id=923 data-nosnippet>923</a>    /// //   But: current_idx (100) &gt; next_deleted_row_idx (0)
+<a href=#924 id=924 data-nosnippet>924</a>    /// //   And: current_idx (100) != next_deleted_row_idx (0)
+<a href=#925 id=925 data-nosnippet>925</a>    /// //   Neither branch executes -&gt; INFINITE LOOP!
+<a href=#926 id=926 data-nosnippet>926</a>    /// ```
+<a href=#927 id=927 data-nosnippet>927</a>    ///
+<a href=#928 id=928 data-nosnippet>928</a>    /// Expected behavior: Should return 100 rows (delete at 0 doesn't affect row group 1)
+<a href=#929 id=929 data-nosnippet>929</a>    /// Bug behavior: Infinite loop in build_deletes_row_selection
+<a href=#930 id=930 data-nosnippet>930</a>    </span><span class="attr">#[tokio::test]
+<a href=#931 id=931 data-nosnippet>931</a>    </span><span class="kw">async fn </span>test_position_delete_in_skipped_row_group() {
+<a href=#932 id=932 data-nosnippet>932</a>        <span class="kw">use </span>arrow_array::{Int32Array, Int64Array};
+<a href=#933 id=933 data-nosnippet>933</a>        <span class="kw">use </span>parquet::file::reader::{FileReader, SerializedFileReader};
+<a href=#934 id=934 data-nosnippet>934</a>
+<a href=#935 id=935 data-nosnippet>935</a>        <span class="comment">// Field IDs for positional delete schema
+<a href=#936 id=936 data-nosnippet>936</a>        </span><span class="kw">const </span>FIELD_ID_POSITIONAL_DELETE_FILE_PATH: u64 = <span class="number">2147483546</span>;
+<a href=#937 id=937 data-nosnippet>937</a>        <span class="kw">const </span>FIELD_ID_POSITIONAL_DELETE_POS: u64 = <span class="number">2147483545</span>;
+<a href=#938 id=938 data-nosnippet>938</a>
+<a href=#939 id=939 data-nosnippet>939</a>        <span class="kw">let </span>tmp_dir = TempDir::new().unwrap();
+<a href=#940 id=940 data-nosnippet>940</a>        <span class="kw">let </span>table_location = tmp_dir.path().to_str().unwrap().to_string();
+<a href=#941 id=941 data-nosnippet>941</a>
+<a href=#942 id=942 data-nosnippet>942</a>        <span class="comment">// Create table schema with a single 'id' column
+<a href=#943 id=943 data-nosnippet>943</a>        </span><span class="kw">let </span>table_schema = Arc::new(
+<a href=#944 id=944 data-nosnippet>944</a>            Schema::builder()
+<a href=#945 id=945 data-nosnippet>945</a>                .with_schema_id(<span class="number">1</span>)
+<a href=#946 id=946 data-nosnippet>946</a>                .with_fields(<span class="macro">vec!</span>[
+<a href=#947 id=947 data-nosnippet>947</a>                    NestedField::required(<span class="number">1</span>, <span class="string">"id"</span>, Type::Primitive(PrimitiveType::Int)).into(),
+<a href=#948 id=948 data-nosnippet>948</a>                ])
+<a href=#949 id=949 data-nosnippet>949</a>                .build()
+<a href=#950 id=950 data-nosnippet>950</a>                .unwrap(),
+<a href=#951 id=951 data-nosnippet>951</a>        );
+<a href=#952 id=952 data-nosnippet>952</a>
+<a href=#953 id=953 data-nosnippet>953</a>        <span class="kw">let </span>arrow_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
+<a href=#954 id=954 data-nosnippet>954</a>            Field::new(<span class="string">"id"</span>, DataType::Int32, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
+<a href=#955 id=955 data-nosnippet>955</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
+<a href=#956 id=956 data-nosnippet>956</a>                <span class="string">"1"</span>.to_string(),
+<a href=#957 id=957 data-nosnippet>957</a>            )])),
+<a href=#958 id=958 data-nosnippet>958</a>        ]));
+<a href=#959 id=959 data-nosnippet>959</a>
+<a href=#960 id=960 data-nosnippet>960</a>        <span class="comment">// Step 1: Create data file with 200 rows in 2 row groups
+<a href=#961 id=961 data-nosnippet>961</a>        // Row group 0: rows 0-99 (ids 1-100)
+<a href=#962 id=962 data-nosnippet>962</a>        // Row group 1: rows 100-199 (ids 101-200)
+<a href=#963 id=963 data-nosnippet>963</a>        </span><span class="kw">let </span>data_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/data.parquet"</span>);
+<a href=#964 id=964 data-nosnippet>964</a>
+<a href=#965 id=965 data-nosnippet>965</a>        <span class="kw">let </span>batch1 = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
+<a href=#966 id=966 data-nosnippet>966</a>            Int32Array::from_iter_values(<span class="number">1</span>..=<span class="number">100</span>),
+<a href=#967 id=967 data-nosnippet>967</a>        )])
+<a href=#968 id=968 data-nosnippet>968</a>        .unwrap();
+<a href=#969 id=969 data-nosnippet>969</a>
+<a href=#970 id=970 data-nosnippet>970</a>        <span class="kw">let </span>batch2 = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
+<a href=#971 id=971 data-nosnippet>971</a>            Int32Array::from_iter_values(<span class="number">101</span>..=<span class="number">200</span>),
+<a href=#972 id=972 data-nosnippet>972</a>        )])
+<a href=#973 id=973 data-nosnippet>973</a>        .unwrap();
+<a href=#974 id=974 data-nosnippet>974</a>
+<a href=#975 id=975 data-nosnippet>975</a>        <span class="comment">// Force each batch into its own row group
+<a href=#976 id=976 data-nosnippet>976</a>        </span><span class="kw">let </span>props = WriterProperties::builder()
+<a href=#977 id=977 data-nosnippet>977</a>            .set_compression(Compression::SNAPPY)
+<a href=#978 id=978 data-nosnippet>978</a>            .set_max_row_group_row_count(<span class="prelude-val">Some</span>(<span class="number">100</span>))
+<a href=#979 id=979 data-nosnippet>979</a>            .build();
+<a href=#980 id=980 data-nosnippet>980</a>
+<a href=#981 id=981 data-nosnippet>981</a>        <span class="kw">let </span>file = File::create(<span class="kw-2">&amp;</span>data_file_path).unwrap();
+<a href=#982 id=982 data-nosnippet>982</a>        <span class="kw">let </span><span class="kw-2">mut </span>writer = ArrowWriter::try_new(file, arrow_schema.clone(), <span class="prelude-val">Some</span>(props)).unwrap();
+<a href=#983 id=983 data-nosnippet>983</a>        writer.write(<span class="kw-2">&amp;</span>batch1).expect(<span class="string">"Writing batch 1"</span>);
+<a href=#984 id=984 data-nosnippet>984</a>        writer.write(<span class="kw-2">&amp;</span>batch2).expect(<span class="string">"Writing batch 2"</span>);
+<a href=#985 id=985 data-nosnippet>985</a>        writer.close().unwrap();
+<a href=#986 id=986 data-nosnippet>986</a>
+<a href=#987 id=987 data-nosnippet>987</a>        <span class="comment">// Verify we created 2 row groups
+<a href=#988 id=988 data-nosnippet>988</a>        </span><span class="kw">let </span>verify_file = File::open(<span class="kw-2">&amp;</span>data_file_path).unwrap();
+<a href=#989 id=989 data-nosnippet>989</a>        <span class="kw">let </span>verify_reader = SerializedFileReader::new(verify_file).unwrap();
+<a href=#990 id=990 data-nosnippet>990</a>        <span class="macro">assert_eq!</span>(
+<a href=#991 id=991 data-nosnippet>991</a>            verify_reader.metadata().num_row_groups(),
+<a href=#992 id=992 data-nosnippet>992</a>            <span class="number">2</span>,
+<a href=#993 id=993 data-nosnippet>993</a>            <span class="string">"Should have 2 row groups"
+<a href=#994 id=994 data-nosnippet>994</a>        </span>);
+<a href=#995 id=995 data-nosnippet>995</a>
+<a href=#996 id=996 data-nosnippet>996</a>        <span class="comment">// Step 2: Create position delete file that deletes row 0 (id=1, first row in row group 0)
+<a href=#997 id=997 data-nosnippet>997</a>        </span><span class="kw">let </span>delete_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/deletes.parquet"</span>);
+<a href=#998 id=998 data-nosnippet>998</a>
+<a href=#999 id=999 data-nosnippet>999</a>        <span class="kw">let </span>delete_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
+<a href=#1000 id=1000 data-nosnippet>1000</a>            Field::new(<span class="string">"file_path"</span>, DataType::Utf8, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
+<a href=#1001 id=1001 data-nosnippet>1001</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
+<a href=#1002 id=1002 data-nosnippet>1002</a>                FIELD_ID_POSITIONAL_DELETE_FILE_PATH.to_string(),
+<a href=#1003 id=1003 data-nosnippet>1003</a>            )])),
+<a href=#1004 id=1004 data-nosnippet>1004</a>            Field::new(<span class="string">"pos"</span>, DataType::Int64, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
+<a href=#1005 id=1005 data-nosnippet>1005</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
+<a href=#1006 id=1006 data-nosnippet>1006</a>                FIELD_ID_POSITIONAL_DELETE_POS.to_string(),
+<a href=#1007 id=1007 data-nosnippet>1007</a>            )])),
+<a href=#1008 id=1008 data-nosnippet>1008</a>        ]));
+<a href=#1009 id=1009 data-nosnippet>1009</a>
+<a href=#1010 id=1010 data-nosnippet>1010</a>        <span class="comment">// Delete row at position 0 (0-indexed, so it's the first row: id=1)
+<a href=#1011 id=1011 data-nosnippet>1011</a>        </span><span class="kw">let </span>delete_batch = RecordBatch::try_new(delete_schema.clone(), <span class="macro">vec!</span>[
+<a href=#1012 id=1012 data-nosnippet>1012</a>            Arc::new(StringArray::from_iter_values(<span class="macro">vec!</span>[data_file_path.clone()])),
+<a href=#1013 id=1013 data-nosnippet>1013</a>            Arc::new(Int64Array::from_iter_values(<span class="macro">vec!</span>[<span class="number">0i64</span>])),
+<a href=#1014 id=1014 data-nosnippet>1014</a>        ])
+<a href=#1015 id=1015 data-nosnippet>1015</a>        .unwrap();
+<a href=#1016 id=1016 data-nosnippet>1016</a>
+<a href=#1017 id=1017 data-nosnippet>1017</a>        <span class="kw">let </span>delete_props = WriterProperties::builder()
+<a href=#1018 id=1018 data-nosnippet>1018</a>            .set_compression(Compression::SNAPPY)
+<a href=#1019 id=1019 data-nosnippet>1019</a>            .build();
+<a href=#1020 id=1020 data-nosnippet>1020</a>
+<a href=#1021 id=1021 data-nosnippet>1021</a>        <span class="kw">let </span>delete_file = File::create(<span class="kw-2">&amp;</span>delete_file_path).unwrap();
+<a href=#1022 id=1022 data-nosnippet>1022</a>        <span class="kw">let </span><span class="kw-2">mut </span>delete_writer =
+<a href=#1023 id=1023 data-nosnippet>1023</a>            ArrowWriter::try_new(delete_file, delete_schema, <span class="prelude-val">Some</span>(delete_props)).unwrap();
+<a href=#1024 id=1024 data-nosnippet>1024</a>        delete_writer.write(<span class="kw-2">&amp;</span>delete_batch).unwrap();
+<a href=#1025 id=1025 data-nosnippet>1025</a>        delete_writer.close().unwrap();
 <a href=#1026 id=1026 data-nosnippet>1026</a>
-<a href=#1027 id=1027 data-nosnippet>1027</a>        <span class="kw">let </span>ids: Vec&lt;i32&gt; = result
-<a href=#1028 id=1028 data-nosnippet>1028</a>            .iter()
-<a href=#1029 id=1029 data-nosnippet>1029</a>            .flat_map(|batch| {
-<a href=#1030 id=1030 data-nosnippet>1030</a>                batch
-<a href=#1031 id=1031 data-nosnippet>1031</a>                    .column(<span class="number">0</span>)
-<a href=#1032 id=1032 data-nosnippet>1032</a>                    .as_primitive::&lt;arrow_array::types::Int32Type&gt;()
-<a href=#1033 id=1033 data-nosnippet>1033</a>                    .values()
-<a href=#1034 id=1034 data-nosnippet>1034</a>                    .iter()
-<a href=#1035 id=1035 data-nosnippet>1035</a>                    .copied()
-<a href=#1036 id=1036 data-nosnippet>1036</a>            })
-<a href=#1037 id=1037 data-nosnippet>1037</a>            .collect();
-<a href=#1038 id=1038 data-nosnippet>1038</a>
-<a href=#1039 id=1039 data-nosnippet>1039</a>        <span class="comment">// Positions 1 and 3 (ids 2 and 4) are deleted; ids 1, 3, 5 remain.
-<a href=#1040 id=1040 data-nosnippet>1040</a>        </span><span class="macro">assert_eq!</span>(ids, <span class="macro">vec!</span>[<span class="number">1</span>, <span class="number">3</span>, <span class="number">5</span>]);
-<a href=#1041 id=1041 data-nosnippet>1041</a>    }
+<a href=#1027 id=1027 data-nosnippet>1027</a>        <span class="comment">// Step 3: Get byte ranges to read ONLY row group 1 (rows 100-199)
+<a href=#1028 id=1028 data-nosnippet>1028</a>        // This exercises the row group selection code path where row group 0 is skipped
+<a href=#1029 id=1029 data-nosnippet>1029</a>        </span><span class="kw">let </span>metadata_file = File::open(<span class="kw-2">&amp;</span>data_file_path).unwrap();
+<a href=#1030 id=1030 data-nosnippet>1030</a>        <span class="kw">let </span>metadata_reader = SerializedFileReader::new(metadata_file).unwrap();
+<a href=#1031 id=1031 data-nosnippet>1031</a>        <span class="kw">let </span>metadata = metadata_reader.metadata();
+<a href=#1032 id=1032 data-nosnippet>1032</a>
+<a href=#1033 id=1033 data-nosnippet>1033</a>        <span class="kw">let </span>row_group_0 = metadata.row_group(<span class="number">0</span>);
+<a href=#1034 id=1034 data-nosnippet>1034</a>        <span class="kw">let </span>row_group_1 = metadata.row_group(<span class="number">1</span>);
+<a href=#1035 id=1035 data-nosnippet>1035</a>
+<a href=#1036 id=1036 data-nosnippet>1036</a>        <span class="kw">let </span>rg0_start = <span class="number">4u64</span>; <span class="comment">// Parquet files start with 4-byte magic "PAR1"
+<a href=#1037 id=1037 data-nosnippet>1037</a>        </span><span class="kw">let </span>rg1_start = rg0_start + row_group_0.compressed_size() <span class="kw">as </span>u64;
+<a href=#1038 id=1038 data-nosnippet>1038</a>        <span class="kw">let </span>rg1_length = row_group_1.compressed_size() <span class="kw">as </span>u64;
+<a href=#1039 id=1039 data-nosnippet>1039</a>
+<a href=#1040 id=1040 data-nosnippet>1040</a>        <span class="kw">let </span>file_io = FileIO::new_with_fs();
+<a href=#1041 id=1041 data-nosnippet>1041</a>        <span class="kw">let </span>reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
 <a href=#1042 id=1042 data-nosnippet>1042</a>
-<a href=#1043 id=1043 data-nosnippet>1043</a>    <span class="doccomment">/// A deletion vector whose decoded cardinality disagrees with the manifest entry's
-<a href=#1044 id=1044 data-nosnippet>1044</a>    /// `record_count` must fail the read rather than silently applying the wrong deletes, the
-<a href=#1045 id=1045 data-nosnippet>1045</a>    /// same invariant Iceberg-Java enforces in `BitmapPositionDeleteIndex.deserializeBitmap`.
-<a href=#1046 id=1046 data-nosnippet>1046</a>    </span><span class="attr">#[tokio::test]
-<a href=#1047 id=1047 data-nosnippet>1047</a>    </span><span class="kw">async fn </span>test_deletion_vector_cardinality_mismatch_fails_read() {
-<a href=#1048 id=1048 data-nosnippet>1048</a>        <span class="kw">use </span>arrow_array::Int32Array;
-<a href=#1049 id=1049 data-nosnippet>1049</a>
-<a href=#1050 id=1050 data-nosnippet>1050</a>        <span class="kw">let </span>tmp_dir = TempDir::new().unwrap();
-<a href=#1051 id=1051 data-nosnippet>1051</a>        <span class="kw">let </span>table_location = tmp_dir.path().to_str().unwrap().to_string();
-<a href=#1052 id=1052 data-nosnippet>1052</a>
-<a href=#1053 id=1053 data-nosnippet>1053</a>        <span class="kw">let </span>table_schema = Arc::new(
-<a href=#1054 id=1054 data-nosnippet>1054</a>            Schema::builder()
-<a href=#1055 id=1055 data-nosnippet>1055</a>                .with_schema_id(<span class="number">1</span>)
-<a href=#1056 id=1056 data-nosnippet>1056</a>                .with_fields(<span class="macro">vec!</span>[
-<a href=#1057 id=1057 data-nosnippet>1057</a>                    NestedField::required(<span class="number">1</span>, <span class="string">"id"</span>, Type::Primitive(PrimitiveType::Int)).into(),
-<a href=#1058 id=1058 data-nosnippet>1058</a>                ])
-<a href=#1059 id=1059 data-nosnippet>1059</a>                .build()
-<a href=#1060 id=1060 data-nosnippet>1060</a>                .unwrap(),
-<a href=#1061 id=1061 data-nosnippet>1061</a>        );
-<a href=#1062 id=1062 data-nosnippet>1062</a>        <span class="kw">let </span>arrow_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
-<a href=#1063 id=1063 data-nosnippet>1063</a>            Field::new(<span class="string">"id"</span>, DataType::Int32, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
-<a href=#1064 id=1064 data-nosnippet>1064</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
-<a href=#1065 id=1065 data-nosnippet>1065</a>                <span class="string">"1"</span>.to_string(),
-<a href=#1066 id=1066 data-nosnippet>1066</a>            )])),
-<a href=#1067 id=1067 data-nosnippet>1067</a>        ]));
-<a href=#1068 id=1068 data-nosnippet>1068</a>
-<a href=#1069 id=1069 data-nosnippet>1069</a>        <span class="kw">let </span>data_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/data.parquet"</span>);
-<a href=#1070 id=1070 data-nosnippet>1070</a>        <span class="kw">let </span>batch = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
-<a href=#1071 id=1071 data-nosnippet>1071</a>            Int32Array::from_iter_values(<span class="number">1</span>..=<span class="number">5</span>),
-<a href=#1072 id=1072 data-nosnippet>1072</a>        )])
-<a href=#1073 id=1073 data-nosnippet>1073</a>        .unwrap();
-<a href=#1074 id=1074 data-nosnippet>1074</a>        <span class="kw">let </span>props = WriterProperties::builder()
-<a href=#1075 id=1075 data-nosnippet>1075</a>            .set_compression(Compression::SNAPPY)
-<a href=#1076 id=1076 data-nosnippet>1076</a>            .build();
-<a href=#1077 id=1077 data-nosnippet>1077</a>        <span class="kw">let </span>file = File::create(<span class="kw-2">&amp;</span>data_file_path).unwrap();
-<a href=#1078 id=1078 data-nosnippet>1078</a>        <span class="kw">let </span><span class="kw-2">mut </span>writer = ArrowWriter::try_new(file, arrow_schema.clone(), <span class="prelude-val">Some</span>(props)).unwrap();
-<a href=#1079 id=1079 data-nosnippet>1079</a>        writer.write(<span class="kw-2">&amp;</span>batch).unwrap();
-<a href=#1080 id=1080 data-nosnippet>1080</a>        writer.close().unwrap();
-<a href=#1081 id=1081 data-nosnippet>1081</a>
-<a href=#1082 id=1082 data-nosnippet>1082</a>        <span class="comment">// The blob decodes to 2 positions, but the manifest entry claims 5.
-<a href=#1083 id=1083 data-nosnippet>1083</a>        </span><span class="kw">let </span>blob = encode_dv_blob([<span class="number">1u64</span>, <span class="number">3</span>]);
-<a href=#1084 id=1084 data-nosnippet>1084</a>        <span class="kw">let </span>dv_path = <span class="macro">format!</span>(<span class="string">"{table_location}/deletes.puffin"</span>);
-<a href=#1085 id=1085 data-nosnippet>1085</a>        std::fs::write(<span class="kw-2">&amp;</span>dv_path, <span class="kw-2">&amp;</span>blob).unwrap();
-<a href=#1086 id=1086 data-nosnippet>1086</a>
-<a href=#1087 id=1087 data-nosnippet>1087</a>        <span class="kw">let </span>file_io = FileIO::new_with_fs();
-<a href=#1088 id=1088 data-nosnippet>1088</a>        <span class="kw">let </span>reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
-<a href=#1089 id=1089 data-nosnippet>1089</a>
-<a href=#1090 id=1090 data-nosnippet>1090</a>        <span class="kw">let </span>task = FileScanTask::builder()
-<a href=#1091 id=1091 data-nosnippet>1091</a>            .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>data_file_path).unwrap().len())
-<a href=#1092 id=1092 data-nosnippet>1092</a>            .with_start(<span class="number">0</span>)
-<a href=#1093 id=1093 data-nosnippet>1093</a>            .with_length(<span class="number">0</span>)
-<a href=#1094 id=1094 data-nosnippet>1094</a>            .with_record_count(<span class="prelude-val">Some</span>(<span class="number">5</span>))
-<a href=#1095 id=1095 data-nosnippet>1095</a>            .with_data_file_path(data_file_path.clone())
-<a href=#1096 id=1096 data-nosnippet>1096</a>            .with_data_file_format(DataFileFormat::Parquet)
-<a href=#1097 id=1097 data-nosnippet>1097</a>            .with_schema(table_schema.clone())
-<a href=#1098 id=1098 data-nosnippet>1098</a>            .with_project_field_ids(<span class="macro">vec!</span>[<span class="number">1</span>])
-<a href=#1099 id=1099 data-nosnippet>1099</a>            .with_deletes(<span class="macro">vec!</span>[
-<a href=#1100 id=1100 data-nosnippet>1100</a>                FileScanTaskDeleteFile::builder()
-<a href=#1101 id=1101 data-nosnippet>1101</a>                    .with_file_path(dv_path.clone())
-<a href=#1102 id=1102 data-nosnippet>1102</a>                    .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>dv_path).unwrap().len())
-<a href=#1103 id=1103 data-nosnippet>1103</a>                    .with_file_type(DataContentType::PositionDeletes)
-<a href=#1104 id=1104 data-nosnippet>1104</a>                    .with_file_format(DataFileFormat::Puffin)
-<a href=#1105 id=1105 data-nosnippet>1105</a>                    .with_partition_spec_id(<span class="number">0</span>)
-<a href=#1106 id=1106 data-nosnippet>1106</a>                    .with_referenced_data_file(<span class="prelude-val">Some</span>(data_file_path.clone()))
-<a href=#1107 id=1107 data-nosnippet>1107</a>                    .with_content_offset(<span class="prelude-val">Some</span>(<span class="number">0</span>))
-<a href=#1108 id=1108 data-nosnippet>1108</a>                    .with_content_size_in_bytes(<span class="prelude-val">Some</span>(blob.len() <span class="kw">as </span>u64))
-<a href=#1109 id=1109 data-nosnippet>1109</a>                    .with_record_count(<span class="prelude-val">Some</span>(<span class="number">5</span>))
-<a href=#1110 id=1110 data-nosnippet>1110</a>                    .build()
-<a href=#1111 id=1111 data-nosnippet>1111</a>                    .unwrap(),
-<a href=#1112 id=1112 data-nosnippet>1112</a>            ])
-<a href=#1113 id=1113 data-nosnippet>1113</a>            .with_case_sensitive(<span class="bool-val">false</span>)
-<a href=#1114 id=1114 data-nosnippet>1114</a>            .build()
-<a href=#1115 id=1115 data-nosnippet>1115</a>            .unwrap();
+<a href=#1043 id=1043 data-nosnippet>1043</a>        <span class="comment">// Create FileScanTask that reads ONLY row group 1 via byte range filtering
+<a href=#1044 id=1044 data-nosnippet>1044</a>        </span><span class="kw">let </span>task = FileScanTask::builder()
+<a href=#1045 id=1045 data-nosnippet>1045</a>            .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>data_file_path).unwrap().len())
+<a href=#1046 id=1046 data-nosnippet>1046</a>            .with_start(rg1_start)
+<a href=#1047 id=1047 data-nosnippet>1047</a>            .with_length(rg1_length)
+<a href=#1048 id=1048 data-nosnippet>1048</a>            .with_record_count(<span class="prelude-val">Some</span>(<span class="number">100</span>)) <span class="comment">// Row group 1 has 100 rows
+<a href=#1049 id=1049 data-nosnippet>1049</a>            </span>.with_data_file_path(data_file_path.clone())
+<a href=#1050 id=1050 data-nosnippet>1050</a>            .with_data_file_format(DataFileFormat::Parquet)
+<a href=#1051 id=1051 data-nosnippet>1051</a>            .with_schema(table_schema.clone())
+<a href=#1052 id=1052 data-nosnippet>1052</a>            .with_project_field_ids(<span class="macro">vec!</span>[<span class="number">1</span>])
+<a href=#1053 id=1053 data-nosnippet>1053</a>            .with_deletes(<span class="macro">vec!</span>[
+<a href=#1054 id=1054 data-nosnippet>1054</a>                FileScanTaskDeleteFile::builder()
+<a href=#1055 id=1055 data-nosnippet>1055</a>                    .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>delete_file_path).unwrap().len())
+<a href=#1056 id=1056 data-nosnippet>1056</a>                    .with_file_path(delete_file_path)
+<a href=#1057 id=1057 data-nosnippet>1057</a>                    .with_file_type(DataContentType::PositionDeletes)
+<a href=#1058 id=1058 data-nosnippet>1058</a>                    .with_file_format(DataFileFormat::Parquet)
+<a href=#1059 id=1059 data-nosnippet>1059</a>                    .with_partition_spec_id(<span class="number">0</span>)
+<a href=#1060 id=1060 data-nosnippet>1060</a>                    .build()
+<a href=#1061 id=1061 data-nosnippet>1061</a>                    .unwrap(),
+<a href=#1062 id=1062 data-nosnippet>1062</a>            ])
+<a href=#1063 id=1063 data-nosnippet>1063</a>            .with_case_sensitive(<span class="bool-val">false</span>)
+<a href=#1064 id=1064 data-nosnippet>1064</a>            .build()
+<a href=#1065 id=1065 data-nosnippet>1065</a>            .unwrap();
+<a href=#1066 id=1066 data-nosnippet>1066</a>
+<a href=#1067 id=1067 data-nosnippet>1067</a>        <span class="kw">let </span>tasks = Box::pin(futures::stream::iter(<span class="macro">vec!</span>[<span class="prelude-val">Ok</span>(task)])) <span class="kw">as </span>FileScanTaskStream;
+<a href=#1068 id=1068 data-nosnippet>1068</a>        <span class="kw">let </span>result = reader
+<a href=#1069 id=1069 data-nosnippet>1069</a>            .read(tasks)
+<a href=#1070 id=1070 data-nosnippet>1070</a>            .unwrap()
+<a href=#1071 id=1071 data-nosnippet>1071</a>            .stream()
+<a href=#1072 id=1072 data-nosnippet>1072</a>            .try_collect::&lt;Vec&lt;RecordBatch&gt;&gt;()
+<a href=#1073 id=1073 data-nosnippet>1073</a>            .<span class="kw">await
+<a href=#1074 id=1074 data-nosnippet>1074</a>            </span>.unwrap();
+<a href=#1075 id=1075 data-nosnippet>1075</a>
+<a href=#1076 id=1076 data-nosnippet>1076</a>        <span class="comment">// Step 4: Verify we got 100 rows (all of row group 1)
+<a href=#1077 id=1077 data-nosnippet>1077</a>        // The delete at position 0 is in row group 0, which is skipped, so it doesn't affect us
+<a href=#1078 id=1078 data-nosnippet>1078</a>        </span><span class="kw">let </span>total_rows: usize = result.iter().map(|b| b.num_rows()).sum();
+<a href=#1079 id=1079 data-nosnippet>1079</a>
+<a href=#1080 id=1080 data-nosnippet>1080</a>        <span class="macro">assert_eq!</span>(
+<a href=#1081 id=1081 data-nosnippet>1081</a>            total_rows, <span class="number">100</span>,
+<a href=#1082 id=1082 data-nosnippet>1082</a>            <span class="string">"Expected 100 rows from row group 1 (delete at position 0 is in skipped row group 0). \
+<a href=#1083 id=1083 data-nosnippet>1083</a>             If this hangs or fails, it indicates the cached delete index was not updated after advance_to()."
+<a href=#1084 id=1084 data-nosnippet>1084</a>        </span>);
+<a href=#1085 id=1085 data-nosnippet>1085</a>
+<a href=#1086 id=1086 data-nosnippet>1086</a>        <span class="comment">// Verify we have all ids from row group 1 (101-200)
+<a href=#1087 id=1087 data-nosnippet>1087</a>        </span><span class="kw">let </span>all_ids: Vec&lt;i32&gt; = result
+<a href=#1088 id=1088 data-nosnippet>1088</a>            .iter()
+<a href=#1089 id=1089 data-nosnippet>1089</a>            .flat_map(|batch| {
+<a href=#1090 id=1090 data-nosnippet>1090</a>                batch
+<a href=#1091 id=1091 data-nosnippet>1091</a>                    .column(<span class="number">0</span>)
+<a href=#1092 id=1092 data-nosnippet>1092</a>                    .as_primitive::&lt;arrow_array::types::Int32Type&gt;()
+<a href=#1093 id=1093 data-nosnippet>1093</a>                    .values()
+<a href=#1094 id=1094 data-nosnippet>1094</a>                    .iter()
+<a href=#1095 id=1095 data-nosnippet>1095</a>                    .copied()
+<a href=#1096 id=1096 data-nosnippet>1096</a>            })
+<a href=#1097 id=1097 data-nosnippet>1097</a>            .collect();
+<a href=#1098 id=1098 data-nosnippet>1098</a>
+<a href=#1099 id=1099 data-nosnippet>1099</a>        <span class="kw">let </span>expected_ids: Vec&lt;i32&gt; = (<span class="number">101</span>..=<span class="number">200</span>).collect();
+<a href=#1100 id=1100 data-nosnippet>1100</a>        <span class="macro">assert_eq!</span>(
+<a href=#1101 id=1101 data-nosnippet>1101</a>            all_ids, expected_ids,
+<a href=#1102 id=1102 data-nosnippet>1102</a>            <span class="string">"Should have ids 101-200 (all of row group 1)"
+<a href=#1103 id=1103 data-nosnippet>1103</a>        </span>);
+<a href=#1104 id=1104 data-nosnippet>1104</a>    }
+<a href=#1105 id=1105 data-nosnippet>1105</a>
+<a href=#1106 id=1106 data-nosnippet>1106</a>    <span class="doccomment">/// End-to-end read of a data file with a V3 deletion vector applied. Exercises the whole
+<a href=#1107 id=1107 data-nosnippet>1107</a>    /// deletion-vector read path together: the DV coordinates on the scan task, the loader
+<a href=#1108 id=1108 data-nosnippet>1108</a>    /// reading the blob by range from its Puffin file, decoding it with DeleteVector::deserialize,
+<a href=#1109 id=1109 data-nosnippet>1109</a>    /// validating cardinality against record_count, and ArrowReader filtering the deleted rows.
+<a href=#1110 id=1110 data-nosnippet>1110</a>    </span><span class="attr">#[tokio::test]
+<a href=#1111 id=1111 data-nosnippet>1111</a>    </span><span class="kw">async fn </span>test_deletion_vector_applied_end_to_end() {
+<a href=#1112 id=1112 data-nosnippet>1112</a>        <span class="kw">use </span>arrow_array::Int32Array;
+<a href=#1113 id=1113 data-nosnippet>1113</a>
+<a href=#1114 id=1114 data-nosnippet>1114</a>        <span class="kw">let </span>tmp_dir = TempDir::new().unwrap();
+<a href=#1115 id=1115 data-nosnippet>1115</a>        <span class="kw">let </span>table_location = tmp_dir.path().to_str().unwrap().to_string();
 <a href=#1116 id=1116 data-nosnippet>1116</a>
-<a href=#1117 id=1117 data-nosnippet>1117</a>        <span class="kw">let </span>tasks = Box::pin(futures::stream::iter(<span class="macro">vec!</span>[<span class="prelude-val">Ok</span>(task)])) <span class="kw">as </span>FileScanTaskStream;
-<a href=#1118 id=1118 data-nosnippet>1118</a>        <span class="kw">let </span>result: <span class="prelude-ty">Result</span>&lt;Vec&lt;RecordBatch&gt;, <span class="kw">_</span>&gt; =
-<a href=#1119 id=1119 data-nosnippet>1119</a>            reader.read(tasks).unwrap().stream().try_collect().<span class="kw">await</span>;
-<a href=#1120 id=1120 data-nosnippet>1120</a>
-<a href=#1121 id=1121 data-nosnippet>1121</a>        <span class="kw">let </span>err = result.unwrap_err();
-<a href=#1122 id=1122 data-nosnippet>1122</a>        <span class="macro">assert_eq!</span>(err.kind(), <span class="kw">crate</span>::ErrorKind::DataInvalid);
-<a href=#1123 id=1123 data-nosnippet>1123</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"expected 5 from record_count"</span>));
-<a href=#1124 id=1124 data-nosnippet>1124</a>    }
-<a href=#1125 id=1125 data-nosnippet>1125</a>}
+<a href=#1117 id=1117 data-nosnippet>1117</a>        <span class="kw">let </span>table_schema = Arc::new(
+<a href=#1118 id=1118 data-nosnippet>1118</a>            Schema::builder()
+<a href=#1119 id=1119 data-nosnippet>1119</a>                .with_schema_id(<span class="number">1</span>)
+<a href=#1120 id=1120 data-nosnippet>1120</a>                .with_fields(<span class="macro">vec!</span>[
+<a href=#1121 id=1121 data-nosnippet>1121</a>                    NestedField::required(<span class="number">1</span>, <span class="string">"id"</span>, Type::Primitive(PrimitiveType::Int)).into(),
+<a href=#1122 id=1122 data-nosnippet>1122</a>                ])
+<a href=#1123 id=1123 data-nosnippet>1123</a>                .build()
+<a href=#1124 id=1124 data-nosnippet>1124</a>                .unwrap(),
+<a href=#1125 id=1125 data-nosnippet>1125</a>        );
+<a href=#1126 id=1126 data-nosnippet>1126</a>        <span class="kw">let </span>arrow_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
+<a href=#1127 id=1127 data-nosnippet>1127</a>            Field::new(<span class="string">"id"</span>, DataType::Int32, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
+<a href=#1128 id=1128 data-nosnippet>1128</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
+<a href=#1129 id=1129 data-nosnippet>1129</a>                <span class="string">"1"</span>.to_string(),
+<a href=#1130 id=1130 data-nosnippet>1130</a>            )])),
+<a href=#1131 id=1131 data-nosnippet>1131</a>        ]));
+<a href=#1132 id=1132 data-nosnippet>1132</a>
+<a href=#1133 id=1133 data-nosnippet>1133</a>        <span class="comment">// Data file: ids 1..=5 at positions 0..=4.
+<a href=#1134 id=1134 data-nosnippet>1134</a>        </span><span class="kw">let </span>data_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/data.parquet"</span>);
+<a href=#1135 id=1135 data-nosnippet>1135</a>        <span class="kw">let </span>batch = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
+<a href=#1136 id=1136 data-nosnippet>1136</a>            Int32Array::from_iter_values(<span class="number">1</span>..=<span class="number">5</span>),
+<a href=#1137 id=1137 data-nosnippet>1137</a>        )])
+<a href=#1138 id=1138 data-nosnippet>1138</a>        .unwrap();
+<a href=#1139 id=1139 data-nosnippet>1139</a>        <span class="kw">let </span>props = WriterProperties::builder()
+<a href=#1140 id=1140 data-nosnippet>1140</a>            .set_compression(Compression::SNAPPY)
+<a href=#1141 id=1141 data-nosnippet>1141</a>            .build();
+<a href=#1142 id=1142 data-nosnippet>1142</a>        <span class="kw">let </span>file = File::create(<span class="kw-2">&amp;</span>data_file_path).unwrap();
+<a href=#1143 id=1143 data-nosnippet>1143</a>        <span class="kw">let </span><span class="kw-2">mut </span>writer = ArrowWriter::try_new(file, arrow_schema.clone(), <span class="prelude-val">Some</span>(props)).unwrap();
+<a href=#1144 id=1144 data-nosnippet>1144</a>        writer.write(<span class="kw-2">&amp;</span>batch).unwrap();
+<a href=#1145 id=1145 data-nosnippet>1145</a>        writer.close().unwrap();
+<a href=#1146 id=1146 data-nosnippet>1146</a>
+<a href=#1147 id=1147 data-nosnippet>1147</a>        <span class="comment">// Deletion vector deleting positions 1 and 3 (ids 2 and 4), serialized as a
+<a href=#1148 id=1148 data-nosnippet>1148</a>        // deletion-vector-v1 blob and embedded in a Puffin-like file at a non-zero offset.
+<a href=#1149 id=1149 data-nosnippet>1149</a>        </span><span class="kw">let </span>blob = encode_dv_blob([<span class="number">1u64</span>, <span class="number">3</span>]);
+<a href=#1150 id=1150 data-nosnippet>1150</a>        <span class="kw">let </span>content_offset = <span class="number">12u64</span>;
+<a href=#1151 id=1151 data-nosnippet>1151</a>        <span class="kw">let </span>content_size = blob.len() <span class="kw">as </span>u64;
+<a href=#1152 id=1152 data-nosnippet>1152</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv_file_bytes = <span class="macro">vec!</span>[<span class="number">0u8</span>; content_offset <span class="kw">as </span>usize];
+<a href=#1153 id=1153 data-nosnippet>1153</a>        dv_file_bytes.extend_from_slice(<span class="kw-2">&amp;</span>blob);
+<a href=#1154 id=1154 data-nosnippet>1154</a>        <span class="kw">let </span>dv_path = <span class="macro">format!</span>(<span class="string">"{table_location}/deletes.puffin"</span>);
+<a href=#1155 id=1155 data-nosnippet>1155</a>        std::fs::write(<span class="kw-2">&amp;</span>dv_path, <span class="kw-2">&amp;</span>dv_file_bytes).unwrap();
+<a href=#1156 id=1156 data-nosnippet>1156</a>
+<a href=#1157 id=1157 data-nosnippet>1157</a>        <span class="kw">let </span>file_io = FileIO::new_with_fs();
+<a href=#1158 id=1158 data-nosnippet>1158</a>        <span class="kw">let </span>reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
+<a href=#1159 id=1159 data-nosnippet>1159</a>
+<a href=#1160 id=1160 data-nosnippet>1160</a>        <span class="kw">let </span>task = FileScanTask::builder()
+<a href=#1161 id=1161 data-nosnippet>1161</a>            .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>data_file_path).unwrap().len())
+<a href=#1162 id=1162 data-nosnippet>1162</a>            .with_start(<span class="number">0</span>)
+<a href=#1163 id=1163 data-nosnippet>1163</a>            .with_length(<span class="number">0</span>)
+<a href=#1164 id=1164 data-nosnippet>1164</a>            .with_record_count(<span class="prelude-val">Some</span>(<span class="number">5</span>))
+<a href=#1165 id=1165 data-nosnippet>1165</a>            .with_data_file_path(data_file_path.clone())
+<a href=#1166 id=1166 data-nosnippet>1166</a>            .with_data_file_format(DataFileFormat::Parquet)
+<a href=#1167 id=1167 data-nosnippet>1167</a>            .with_schema(table_schema.clone())
+<a href=#1168 id=1168 data-nosnippet>1168</a>            .with_project_field_ids(<span class="macro">vec!</span>[<span class="number">1</span>])
+<a href=#1169 id=1169 data-nosnippet>1169</a>            .with_deletes(<span class="macro">vec!</span>[
+<a href=#1170 id=1170 data-nosnippet>1170</a>                FileScanTaskDeleteFile::builder()
+<a href=#1171 id=1171 data-nosnippet>1171</a>                    .with_file_path(dv_path.clone())
+<a href=#1172 id=1172 data-nosnippet>1172</a>                    .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>dv_path).unwrap().len())
+<a href=#1173 id=1173 data-nosnippet>1173</a>                    .with_file_type(DataContentType::PositionDeletes)
+<a href=#1174 id=1174 data-nosnippet>1174</a>                    .with_file_format(DataFileFormat::Puffin)
+<a href=#1175 id=1175 data-nosnippet>1175</a>                    .with_partition_spec_id(<span class="number">0</span>)
+<a href=#1176 id=1176 data-nosnippet>1176</a>                    .with_referenced_data_file(<span class="prelude-val">Some</span>(data_file_path.clone()))
+<a href=#1177 id=1177 data-nosnippet>1177</a>                    .with_content_offset(<span class="prelude-val">Some</span>(content_offset))
+<a href=#1178 id=1178 data-nosnippet>1178</a>                    .with_content_size_in_bytes(<span class="prelude-val">Some</span>(content_size))
+<a href=#1179 id=1179 data-nosnippet>1179</a>                    .with_record_count(<span class="prelude-val">Some</span>(<span class="number">2</span>))
+<a href=#1180 id=1180 data-nosnippet>1180</a>                    .build()
+<a href=#1181 id=1181 data-nosnippet>1181</a>                    .unwrap(),
+<a href=#1182 id=1182 data-nosnippet>1182</a>            ])
+<a href=#1183 id=1183 data-nosnippet>1183</a>            .with_case_sensitive(<span class="bool-val">false</span>)
+<a href=#1184 id=1184 data-nosnippet>1184</a>            .build()
+<a href=#1185 id=1185 data-nosnippet>1185</a>            .unwrap();
+<a href=#1186 id=1186 data-nosnippet>1186</a>
+<a href=#1187 id=1187 data-nosnippet>1187</a>        <span class="kw">let </span>tasks = Box::pin(futures::stream::iter(<span class="macro">vec!</span>[<span class="prelude-val">Ok</span>(task)])) <span class="kw">as </span>FileScanTaskStream;
+<a href=#1188 id=1188 data-nosnippet>1188</a>        <span class="kw">let </span>result = reader
+<a href=#1189 id=1189 data-nosnippet>1189</a>            .read(tasks)
+<a href=#1190 id=1190 data-nosnippet>1190</a>            .unwrap()
+<a href=#1191 id=1191 data-nosnippet>1191</a>            .stream()
+<a href=#1192 id=1192 data-nosnippet>1192</a>            .try_collect::&lt;Vec&lt;RecordBatch&gt;&gt;()
+<a href=#1193 id=1193 data-nosnippet>1193</a>            .<span class="kw">await
+<a href=#1194 id=1194 data-nosnippet>1194</a>            </span>.unwrap();
+<a href=#1195 id=1195 data-nosnippet>1195</a>
+<a href=#1196 id=1196 data-nosnippet>1196</a>        <span class="kw">let </span>ids: Vec&lt;i32&gt; = result
+<a href=#1197 id=1197 data-nosnippet>1197</a>            .iter()
+<a href=#1198 id=1198 data-nosnippet>1198</a>            .flat_map(|batch| {
+<a href=#1199 id=1199 data-nosnippet>1199</a>                batch
+<a href=#1200 id=1200 data-nosnippet>1200</a>                    .column(<span class="number">0</span>)
+<a href=#1201 id=1201 data-nosnippet>1201</a>                    .as_primitive::&lt;arrow_array::types::Int32Type&gt;()
+<a href=#1202 id=1202 data-nosnippet>1202</a>                    .values()
+<a href=#1203 id=1203 data-nosnippet>1203</a>                    .iter()
+<a href=#1204 id=1204 data-nosnippet>1204</a>                    .copied()
+<a href=#1205 id=1205 data-nosnippet>1205</a>            })
+<a href=#1206 id=1206 data-nosnippet>1206</a>            .collect();
+<a href=#1207 id=1207 data-nosnippet>1207</a>
+<a href=#1208 id=1208 data-nosnippet>1208</a>        <span class="comment">// Positions 1 and 3 (ids 2 and 4) are deleted; ids 1, 3, 5 remain.
+<a href=#1209 id=1209 data-nosnippet>1209</a>        </span><span class="macro">assert_eq!</span>(ids, <span class="macro">vec!</span>[<span class="number">1</span>, <span class="number">3</span>, <span class="number">5</span>]);
+<a href=#1210 id=1210 data-nosnippet>1210</a>    }
+<a href=#1211 id=1211 data-nosnippet>1211</a>
+<a href=#1212 id=1212 data-nosnippet>1212</a>    <span class="doccomment">/// A deletion vector whose decoded cardinality disagrees with the manifest entry's
+<a href=#1213 id=1213 data-nosnippet>1213</a>    /// `record_count` must fail the read rather than silently applying the wrong deletes, the
+<a href=#1214 id=1214 data-nosnippet>1214</a>    /// same invariant Iceberg-Java enforces in `BitmapPositionDeleteIndex.deserializeBitmap`.
+<a href=#1215 id=1215 data-nosnippet>1215</a>    </span><span class="attr">#[tokio::test]
+<a href=#1216 id=1216 data-nosnippet>1216</a>    </span><span class="kw">async fn </span>test_deletion_vector_cardinality_mismatch_fails_read() {
+<a href=#1217 id=1217 data-nosnippet>1217</a>        <span class="kw">use </span>arrow_array::Int32Array;
+<a href=#1218 id=1218 data-nosnippet>1218</a>
+<a href=#1219 id=1219 data-nosnippet>1219</a>        <span class="kw">let </span>tmp_dir = TempDir::new().unwrap();
+<a href=#1220 id=1220 data-nosnippet>1220</a>        <span class="kw">let </span>table_location = tmp_dir.path().to_str().unwrap().to_string();
+<a href=#1221 id=1221 data-nosnippet>1221</a>
+<a href=#1222 id=1222 data-nosnippet>1222</a>        <span class="kw">let </span>table_schema = Arc::new(
+<a href=#1223 id=1223 data-nosnippet>1223</a>            Schema::builder()
+<a href=#1224 id=1224 data-nosnippet>1224</a>                .with_schema_id(<span class="number">1</span>)
+<a href=#1225 id=1225 data-nosnippet>1225</a>                .with_fields(<span class="macro">vec!</span>[
+<a href=#1226 id=1226 data-nosnippet>1226</a>                    NestedField::required(<span class="number">1</span>, <span class="string">"id"</span>, Type::Primitive(PrimitiveType::Int)).into(),
+<a href=#1227 id=1227 data-nosnippet>1227</a>                ])
+<a href=#1228 id=1228 data-nosnippet>1228</a>                .build()
+<a href=#1229 id=1229 data-nosnippet>1229</a>                .unwrap(),
+<a href=#1230 id=1230 data-nosnippet>1230</a>        );
+<a href=#1231 id=1231 data-nosnippet>1231</a>        <span class="kw">let </span>arrow_schema = Arc::new(ArrowSchema::new(<span class="macro">vec!</span>[
+<a href=#1232 id=1232 data-nosnippet>1232</a>            Field::new(<span class="string">"id"</span>, DataType::Int32, <span class="bool-val">false</span>).with_metadata(HashMap::from([(
+<a href=#1233 id=1233 data-nosnippet>1233</a>                PARQUET_FIELD_ID_META_KEY.to_string(),
+<a href=#1234 id=1234 data-nosnippet>1234</a>                <span class="string">"1"</span>.to_string(),
+<a href=#1235 id=1235 data-nosnippet>1235</a>            )])),
+<a href=#1236 id=1236 data-nosnippet>1236</a>        ]));
+<a href=#1237 id=1237 data-nosnippet>1237</a>
+<a href=#1238 id=1238 data-nosnippet>1238</a>        <span class="kw">let </span>data_file_path = <span class="macro">format!</span>(<span class="string">"{table_location}/data.parquet"</span>);
+<a href=#1239 id=1239 data-nosnippet>1239</a>        <span class="kw">let </span>batch = RecordBatch::try_new(arrow_schema.clone(), <span class="macro">vec!</span>[Arc::new(
+<a href=#1240 id=1240 data-nosnippet>1240</a>            Int32Array::from_iter_values(<span class="number">1</span>..=<span class="number">5</span>),
+<a href=#1241 id=1241 data-nosnippet>1241</a>        )])
+<a href=#1242 id=1242 data-nosnippet>1242</a>        .unwrap();
+<a href=#1243 id=1243 data-nosnippet>1243</a>        <span class="kw">let </span>props = WriterProperties::builder()
+<a href=#1244 id=1244 data-nosnippet>1244</a>            .set_compression(Compression::SNAPPY)
+<a href=#1245 id=1245 data-nosnippet>1245</a>            .build();
+<a href=#1246 id=1246 data-nosnippet>1246</a>        <span class="kw">let </span>file = File::create(<span class="kw-2">&amp;</span>data_file_path).unwrap();
+<a href=#1247 id=1247 data-nosnippet>1247</a>        <span class="kw">let </span><span class="kw-2">mut </span>writer = ArrowWriter::try_new(file, arrow_schema.clone(), <span class="prelude-val">Some</span>(props)).unwrap();
+<a href=#1248 id=1248 data-nosnippet>1248</a>        writer.write(<span class="kw-2">&amp;</span>batch).unwrap();
+<a href=#1249 id=1249 data-nosnippet>1249</a>        writer.close().unwrap();
+<a href=#1250 id=1250 data-nosnippet>1250</a>
+<a href=#1251 id=1251 data-nosnippet>1251</a>        <span class="comment">// The blob decodes to 2 positions, but the manifest entry claims 5.
+<a href=#1252 id=1252 data-nosnippet>1252</a>        </span><span class="kw">let </span>blob = encode_dv_blob([<span class="number">1u64</span>, <span class="number">3</span>]);
+<a href=#1253 id=1253 data-nosnippet>1253</a>        <span class="kw">let </span>dv_path = <span class="macro">format!</span>(<span class="string">"{table_location}/deletes.puffin"</span>);
+<a href=#1254 id=1254 data-nosnippet>1254</a>        std::fs::write(<span class="kw-2">&amp;</span>dv_path, <span class="kw-2">&amp;</span>blob).unwrap();
+<a href=#1255 id=1255 data-nosnippet>1255</a>
+<a href=#1256 id=1256 data-nosnippet>1256</a>        <span class="kw">let </span>file_io = FileIO::new_with_fs();
+<a href=#1257 id=1257 data-nosnippet>1257</a>        <span class="kw">let </span>reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
+<a href=#1258 id=1258 data-nosnippet>1258</a>
+<a href=#1259 id=1259 data-nosnippet>1259</a>        <span class="kw">let </span>task = FileScanTask::builder()
+<a href=#1260 id=1260 data-nosnippet>1260</a>            .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>data_file_path).unwrap().len())
+<a href=#1261 id=1261 data-nosnippet>1261</a>            .with_start(<span class="number">0</span>)
+<a href=#1262 id=1262 data-nosnippet>1262</a>            .with_length(<span class="number">0</span>)
+<a href=#1263 id=1263 data-nosnippet>1263</a>            .with_record_count(<span class="prelude-val">Some</span>(<span class="number">5</span>))
+<a href=#1264 id=1264 data-nosnippet>1264</a>            .with_data_file_path(data_file_path.clone())
+<a href=#1265 id=1265 data-nosnippet>1265</a>            .with_data_file_format(DataFileFormat::Parquet)
+<a href=#1266 id=1266 data-nosnippet>1266</a>            .with_schema(table_schema.clone())
+<a href=#1267 id=1267 data-nosnippet>1267</a>            .with_project_field_ids(<span class="macro">vec!</span>[<span class="number">1</span>])
+<a href=#1268 id=1268 data-nosnippet>1268</a>            .with_deletes(<span class="macro">vec!</span>[
+<a href=#1269 id=1269 data-nosnippet>1269</a>                FileScanTaskDeleteFile::builder()
+<a href=#1270 id=1270 data-nosnippet>1270</a>                    .with_file_path(dv_path.clone())
+<a href=#1271 id=1271 data-nosnippet>1271</a>                    .with_file_size_in_bytes(std::fs::metadata(<span class="kw-2">&amp;</span>dv_path).unwrap().len())
+<a href=#1272 id=1272 data-nosnippet>1272</a>                    .with_file_type(DataContentType::PositionDeletes)
+<a href=#1273 id=1273 data-nosnippet>1273</a>                    .with_file_format(DataFileFormat::Puffin)
+<a href=#1274 id=1274 data-nosnippet>1274</a>                    .with_partition_spec_id(<span class="number">0</span>)
+<a href=#1275 id=1275 data-nosnippet>1275</a>                    .with_referenced_data_file(<span class="prelude-val">Some</span>(data_file_path.clone()))
+<a href=#1276 id=1276 data-nosnippet>1276</a>                    .with_content_offset(<span class="prelude-val">Some</span>(<span class="number">0</span>))
+<a href=#1277 id=1277 data-nosnippet>1277</a>                    .with_content_size_in_bytes(<span class="prelude-val">Some</span>(blob.len() <span class="kw">as </span>u64))
+<a href=#1278 id=1278 data-nosnippet>1278</a>                    .with_record_count(<span class="prelude-val">Some</span>(<span class="number">5</span>))
+<a href=#1279 id=1279 data-nosnippet>1279</a>                    .build()
+<a href=#1280 id=1280 data-nosnippet>1280</a>                    .unwrap(),
+<a href=#1281 id=1281 data-nosnippet>1281</a>            ])
+<a href=#1282 id=1282 data-nosnippet>1282</a>            .with_case_sensitive(<span class="bool-val">false</span>)
+<a href=#1283 id=1283 data-nosnippet>1283</a>            .build()
+<a href=#1284 id=1284 data-nosnippet>1284</a>            .unwrap();
+<a href=#1285 id=1285 data-nosnippet>1285</a>
+<a href=#1286 id=1286 data-nosnippet>1286</a>        <span class="kw">let </span>tasks = Box::pin(futures::stream::iter(<span class="macro">vec!</span>[<span class="prelude-val">Ok</span>(task)])) <span class="kw">as </span>FileScanTaskStream;
+<a href=#1287 id=1287 data-nosnippet>1287</a>        <span class="kw">let </span>result: <span class="prelude-ty">Result</span>&lt;Vec&lt;RecordBatch&gt;, <span class="kw">_</span>&gt; =
+<a href=#1288 id=1288 data-nosnippet>1288</a>            reader.read(tasks).unwrap().stream().try_collect().<span class="kw">await</span>;
+<a href=#1289 id=1289 data-nosnippet>1289</a>
+<a href=#1290 id=1290 data-nosnippet>1290</a>        <span class="kw">let </span>err = result.unwrap_err();
+<a href=#1291 id=1291 data-nosnippet>1291</a>        <span class="macro">assert_eq!</span>(err.kind(), <span class="kw">crate</span>::ErrorKind::DataInvalid);
+<a href=#1292 id=1292 data-nosnippet>1292</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"expected 5 from record_count"</span>));
+<a href=#1293 id=1293 data-nosnippet>1293</a>    }
+<a href=#1294 id=1294 data-nosnippet>1294</a>}
 </code></pre></div></section></main></body></html>
\ No newline at end of file
diff --git a/api/src/iceberg/delete_vector.rs.html b/api/src/iceberg/delete_vector.rs.html
index 507c63787..e6456eabc 100644
--- a/api/src/iceberg/delete_vector.rs.html
+++ b/api/src/iceberg/delete_vector.rs.html
@@ -92,425 +92,830 @@
 <a href=#92 id=92 data-nosnippet>92</a>        <span class="self">self</span>.inner.is_empty()
 <a href=#93 id=93 data-nosnippet>93</a>    }
 <a href=#94 id=94 data-nosnippet>94</a>
-<a href=#95 id=95 data-nosnippet>95</a>    <span class="doccomment">/// Parses a `deletion-vector-v1` Puffin blob into a `DeleteVector`.
+<a href=#95 id=95 data-nosnippet>95</a>    <span class="doccomment">/// Returns the deleted positions as ascending, non-overlapping half-open ranges.
 <a href=#96 id=96 data-nosnippet>96</a>    ///
-<a href=#97 id=97 data-nosnippet>97</a>    /// The layout, defined by the Iceberg Puffin spec and matching Iceberg-Java's
-<a href=#98 id=98 data-nosnippet>98</a>    /// `BitmapPositionDeleteIndex`, is:
-<a href=#99 id=99 data-nosnippet>99</a>    ///
-<a href=#100 id=100 data-nosnippet>100</a>    /// ```text
-<a href=#101 id=101 data-nosnippet>101</a>    /// [length: u32 big-endian][magic: D1 D3 39 64][vector][crc: u32 big-endian]
-<a href=#102 id=102 data-nosnippet>102</a>    /// ```
-<a href=#103 id=103 data-nosnippet>103</a>    ///
-<a href=#104 id=104 data-nosnippet>104</a>    /// `length` counts the magic and vector bytes (not itself or the CRC). The CRC-32 is
-<a href=#105 id=105 data-nosnippet>105</a>    /// computed over the magic and vector. `vector` is a roaring bitmap in the portable 64-bit
-<a href=#106 id=106 data-nosnippet>106</a>    /// format: a directory of 32-bit key / 32-bit roaring bitmap pairs, ordered by unsigned
-<a href=#107 id=107 data-nosnippet>107</a>    /// comparison of the keys, one bitmap per key.
-<a href=#108 id=108 data-nosnippet>108</a>    ///
-<a href=#109 id=109 data-nosnippet>109</a>    /// # Errors
+<a href=#97 id=97 data-nosnippet>97</a>    /// Adjacent ranges may not be merged (a run is split at each 2^32 boundary), so callers
+<a href=#98 id=98 data-nosnippet>98</a>    /// shouldn't assume a range is maximal. A range holding only `u64::MAX` is dropped, because a
+<a href=#99 id=99 data-nosnippet>99</a>    /// half-open `u64` range can't hold it and no Parquet row can be there.
+<a href=#100 id=100 data-nosnippet>100</a>    </span><span class="kw">pub</span>(<span class="kw">crate</span>) <span class="kw">fn </span>ranges(<span class="kw-2">&amp;</span><span class="self">self</span>) -&gt; DeleteVectorRanges&lt;<span class="lifetime">'_</span>&gt; {
+<a href=#101 id=101 data-nosnippet>101</a>        DeleteVectorRanges {
+<a href=#102 id=102 data-nosnippet>102</a>            bitmaps: <span class="self">self</span>.inner.bitmaps(),
+<a href=#103 id=103 data-nosnippet>103</a>            high_bits: <span class="number">0</span>,
+<a href=#104 id=104 data-nosnippet>104</a>            bitmap_iter: <span class="prelude-val">None</span>,
+<a href=#105 id=105 data-nosnippet>105</a>            buffered: <span class="prelude-val">None</span>,
+<a href=#106 id=106 data-nosnippet>106</a>        }
+<a href=#107 id=107 data-nosnippet>107</a>    }
+<a href=#108 id=108 data-nosnippet>108</a>
+<a href=#109 id=109 data-nosnippet>109</a>    <span class="doccomment">/// Parses a `deletion-vector-v1` Puffin blob into a `DeleteVector`.
 <a href=#110 id=110 data-nosnippet>110</a>    ///
-<a href=#111 id=111 data-nosnippet>111</a>    /// Returns [`ErrorKind::DataInvalid`] if the blob is shorter than the minimum, the length
-<a href=#112 id=112 data-nosnippet>112</a>    /// prefix or CRC does not match, the magic is wrong, the roaring bitmap count exceeds the
-<a href=#113 id=113 data-nosnippet>113</a>    /// portable format's maximum, the roaring directory's keys are not ordered by unsigned
-<a href=#114 id=114 data-nosnippet>114</a>    /// comparison, or the roaring payload fails to decode.
-<a href=#115 id=115 data-nosnippet>115</a>    </span><span class="kw">pub fn </span>deserialize(blob: <span class="kw-2">&amp;</span>[u8]) -&gt; <span class="prelude-ty">Result</span>&lt;<span class="self">Self</span>&gt; {
-<a href=#116 id=116 data-nosnippet>116</a>        <span class="kw">if </span>blob.len() &lt; DV_MIN_BLOB_BYTES {
-<a href=#117 id=117 data-nosnippet>117</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(<span class="macro">invalid_data!</span>(
-<a href=#118 id=118 data-nosnippet>118</a>                <span class="string">"deletion-vector-v1 blob is {} bytes, shorter than the {DV_MIN_BLOB_BYTES}-byte minimum"</span>,
-<a href=#119 id=119 data-nosnippet>119</a>                blob.len()
-<a href=#120 id=120 data-nosnippet>120</a>            ));
-<a href=#121 id=121 data-nosnippet>121</a>        }
-<a href=#122 id=122 data-nosnippet>122</a>
-<a href=#123 id=123 data-nosnippet>123</a>        <span class="comment">// The magic and vector, i.e. the bytes covered by both the length prefix and the CRC.
-<a href=#124 id=124 data-nosnippet>124</a>        </span><span class="kw">let </span>body = <span class="kw-2">&amp;</span>blob[DV_LENGTH_PREFIX_BYTES..blob.len() - DV_CRC_BYTES];
-<a href=#125 id=125 data-nosnippet>125</a>        verify_length_prefix(<span class="kw-2">&amp;</span>blob[..DV_LENGTH_PREFIX_BYTES], body)<span class="question-mark">?</span>;
-<a href=#126 id=126 data-nosnippet>126</a>
-<a href=#127 id=127 data-nosnippet>127</a>        <span class="comment">// Verify the CRC before interpreting any bytes so a corrupt blob yields a single clear
-<a href=#128 id=128 data-nosnippet>128</a>        // error rather than an opaque roaring decode failure.
-<a href=#129 id=129 data-nosnippet>129</a>        </span>verify_crc(body, <span class="kw-2">&amp;</span>blob[blob.len() - DV_CRC_BYTES..])<span class="question-mark">?</span>;
-<a href=#130 id=130 data-nosnippet>130</a>
-<a href=#131 id=131 data-nosnippet>131</a>        <span class="kw">let </span>(magic, vector) = body.split_at(DV_MAGIC_BYTES);
-<a href=#132 id=132 data-nosnippet>132</a>        verify_magic(magic)<span class="question-mark">?</span>;
-<a href=#133 id=133 data-nosnippet>133</a>
-<a href=#134 id=134 data-nosnippet>134</a>        <span class="kw">let </span>inner = decode_roaring_directory(vector)<span class="question-mark">?</span>;
-<a href=#135 id=135 data-nosnippet>135</a>
-<a href=#136 id=136 data-nosnippet>136</a>        <span class="prelude-val">Ok</span>(DeleteVector { inner })
-<a href=#137 id=137 data-nosnippet>137</a>    }
-<a href=#138 id=138 data-nosnippet>138</a>}
-<a href=#139 id=139 data-nosnippet>139</a>
-<a href=#140 id=140 data-nosnippet>140</a><span class="kw">fn </span>verify_length_prefix(<span class="kw-2">mut </span>prefix: <span class="kw-2">&amp;</span>[u8], body: <span class="kw-2">&amp;</span>[u8]) -&gt; <span class="prelude-ty">Result</span>&lt;()&gt; {
-<a href=#141 id=141 data-nosnippet>141</a>    <span class="kw">let </span>declared_len = prefix.try_get_u32().map_err(|e| {
-<a href=#142 id=142 data-nosnippet>142</a>        Error::new(
-<a href=#143 id=143 data-nosnippet>143</a>            ErrorKind::Unexpected,
-<a href=#144 id=144 data-nosnippet>144</a>            <span class="string">"failed to read the deletion-vector-v1 length prefix"</span>,
-<a href=#145 id=145 data-nosnippet>145</a>        )
-<a href=#146 id=146 data-nosnippet>146</a>        .with_source(e)
-<a href=#147 id=147 data-nosnippet>147</a>    })<span class="question-mark">? </span><span class="kw">as </span>usize;
-<a href=#148 id=148 data-nosnippet>148</a>    <span class="kw">if </span>declared_len != body.len() {
-<a href=#149 id=149 data-nosnippet>149</a>        <span class="kw">return </span><span class="prelude-val">Err</span>(<span class="macro">invalid_data!</span>(
-<a href=#150 id=150 data-nosnippet>150</a>            <span class="string">"deletion-vector-v1 length prefix is {declared_len}, expected {}"</span>,
-<a href=#151 id=151 data-nosnippet>151</a>            body.len()
-<a href=#152 id=152 data-nosnippet>152</a>        ));
-<a href=#153 id=153 data-nosnippet>153</a>    }
-<a href=#154 id=154 data-nosnippet>154</a>    <span class="prelude-val">Ok</span>(())
-<a href=#155 id=155 data-nosnippet>155</a>}
-<a href=#156 id=156 data-nosnippet>156</a>
-<a href=#157 id=157 data-nosnippet>157</a><span class="kw">fn </span>verify_crc(body: <span class="kw-2">&amp;</span>[u8], <span class="kw-2">mut </span>crc_bytes: <span class="kw-2">&amp;</span>[u8]) -&gt; <span class="prelude-ty">Result</span>&lt;()&gt; {
-<a href=#158 id=158 data-nosnippet>158</a>    <span class="kw">let </span>stored_crc = crc_bytes.try_get_u32().map_err(|e| {
-<a href=#159 id=159 data-nosnippet>159</a>        Error::new(
-<a href=#160 id=160 data-nosnippet>160</a>            ErrorKind::Unexpected,
-<a href=#161 id=161 data-nosnippet>161</a>            <span class="string">"failed to read the deletion-vector-v1 CRC"</span>,
-<a href=#162 id=162 data-nosnippet>162</a>        )
-<a href=#163 id=163 data-nosnippet>163</a>        .with_source(e)
-<a href=#164 id=164 data-nosnippet>164</a>    })<span class="question-mark">?</span>;
-<a href=#165 id=165 data-nosnippet>165</a>    <span class="kw">let </span>computed_crc = crc32fast::hash(body);
-<a href=#166 id=166 data-nosnippet>166</a>    <span class="kw">if </span>computed_crc != stored_crc {
-<a href=#167 id=167 data-nosnippet>167</a>        <span class="kw">return </span><span class="prelude-val">Err</span>(<span class="macro">invalid_data!</span>(
-<a href=#168 id=168 data-nosnippet>168</a>            <span class="string">"deletion-vector-v1 CRC mismatch: computed {computed_crc:#010x}, stored {stored_crc:#010x}"
-<a href=#169 id=169 data-nosnippet>169</a>        </span>));
-<a href=#170 id=170 data-nosnippet>170</a>    }
-<a href=#171 id=171 data-nosnippet>171</a>    <span class="prelude-val">Ok</span>(())
-<a href=#172 id=172 data-nosnippet>172</a>}
-<a href=#173 id=173 data-nosnippet>173</a>
-<a href=#174 id=174 data-nosnippet>174</a><span class="kw">fn </span>verify_magic(magic: <span class="kw-2">&amp;</span>[u8]) -&gt; <span class="prelude-ty">Result</span>&lt;()&gt; {
-<a href=#175 id=175 data-nosnippet>175</a>    <span class="kw">if </span>magic != DV_MAGIC {
-<a href=#176 id=176 data-nosnippet>176</a>        <span class="kw">return </span><span class="prelude-val">Err</span>(<span class="macro">invalid_data!</span>(
-<a href=#177 id=177 data-nosnippet>177</a>            <span class="string">"deletion-vector-v1 magic mismatch: {magic:02x?}, expected {DV_MAGIC:02x?}"
-<a href=#178 id=178 data-nosnippet>178</a>        </span>));
-<a href=#179 id=179 data-nosnippet>179</a>    }
-<a href=#180 id=180 data-nosnippet>180</a>    <span class="prelude-val">Ok</span>(())
-<a href=#181 id=181 data-nosnippet>181</a>}
-<a href=#182 id=182 data-nosnippet>182</a>
-<a href=#183 id=183 data-nosnippet>183</a><span class="comment">// The Puffin spec defines the roaring directory as the bitmaps "ordered by unsigned comparison
-<a href=#184 id=184 data-nosnippet>184</a>// of the 32-bit keys", with one bitmap per key. Walk it ourselves (rather than
-<a href=#185 id=185 data-nosnippet>185</a>// `RoaringTreemap::deserialize_from`, which stores keys in a `BTreeMap` via a plain insert and
-<a href=#186 id=186 data-nosnippet>186</a>// would silently accept a stream with duplicate or out-of-order keys, discarding the earlier
-<a href=#187 id=187 data-nosnippet>187</a>// bitmap on a duplicate) so a non-conformant blob is rejected instead of decoded into a value
-<a href=#188 id=188 data-nosnippet>188</a>// that doesn't match what was actually written.
-<a href=#189 id=189 data-nosnippet>189</a></span><span class="kw">fn </span>decode_roaring_directory(<span class="kw-2">mut </span>reader: <span class="kw-2">&amp;</span>[u8]) -&gt; <span class="prelude-ty">Result</span>&lt;RoaringTreemap&gt; {
-<a href=#190 id=190 data-nosnippet>190</a>    <span class="kw">let </span>bitmap_count = reader.try_get_u64_le().map_err(|e| {
-<a href=#191 id=191 data-nosnippet>191</a>        <span class="macro">invalid_data!</span>(<span class="string">"failed to decode deletion-vector-v1 roaring payload"</span>).with_source(e)
-<a href=#192 id=192 data-nosnippet>192</a>    })<span class="question-mark">?</span>;
-<a href=#193 id=193 data-nosnippet>193</a>    <span class="comment">// The roaring portable format restricts the bitmap count to [0, 2^32 - 1] (it is stored as a
-<a href=#194 id=194 data-nosnippet>194</a>    // u64 with the upper 32 bits reserved as zero padding).
-<a href=#195 id=195 data-nosnippet>195</a>    </span><span class="kw">if </span>bitmap_count &gt; u32::MAX <span class="kw">as </span>u64 {
-<a href=#196 id=196 data-nosnippet>196</a>        <span class="kw">return </span><span class="prelude-val">Err</span>(<span class="macro">invalid_data!</span>(
-<a href=#197 id=197 data-nosnippet>197</a>            <span class="string">"deletion-vector-v1 roaring bitmap count {bitmap_count} exceeds the {}-key maximum"</span>,
-<a href=#198 id=198 data-nosnippet>198</a>            u32::MAX
-<a href=#199 id=199 data-nosnippet>199</a>        ));
-<a href=#200 id=200 data-nosnippet>200</a>    }
-<a href=#201 id=201 data-nosnippet>201</a>
-<a href=#202 id=202 data-nosnippet>202</a>    <span class="kw">let </span><span class="kw-2">mut </span>bitmaps = Vec::new();
-<a href=#203 id=203 data-nosnippet>203</a>    <span class="kw">let </span><span class="kw-2">mut </span>last_key: <span class="prelude-ty">Option</span>&lt;u32&gt; = <span class="prelude-val">None</span>;
-<a href=#204 id=204 data-nosnippet>204</a>    <span class="kw">for _ in </span><span class="number">0</span>..bitmap_count {
-<a href=#205 id=205 data-nosnippet>205</a>        <span class="kw">let </span>key = reader.try_get_u32_le().map_err(|e| {
-<a href=#206 id=206 data-nosnippet>206</a>            <span class="macro">invalid_data!</span>(<span class="string">"failed to decode deletion-vector-v1 roaring payload"</span>).with_source(e)
-<a href=#207 id=207 data-nosnippet>207</a>        })<span class="question-mark">?</span>;
-<a href=#208 id=208 data-nosnippet>208</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(last) = last_key
-<a href=#209 id=209 data-nosnippet>209</a>            &amp;&amp; key &lt;= last
-<a href=#210 id=210 data-nosnippet>210</a>        {
-<a href=#211 id=211 data-nosnippet>211</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(<span class="macro">invalid_data!</span>(
-<a href=#212 id=212 data-nosnippet>212</a>                <span class="string">"deletion-vector-v1 roaring keys must be ordered by unsigned comparison, got key {key} after {last}"
-<a href=#213 id=213 data-nosnippet>213</a>            </span>));
-<a href=#214 id=214 data-nosnippet>214</a>        }
-<a href=#215 id=215 data-nosnippet>215</a>        last_key = <span class="prelude-val">Some</span>(key);
-<a href=#216 id=216 data-nosnippet>216</a>
-<a href=#217 id=217 data-nosnippet>217</a>        <span class="kw">let </span>bitmap = RoaringBitmap::deserialize_from(<span class="kw-2">&amp;mut </span>reader).map_err(|e| {
-<a href=#218 id=218 data-nosnippet>218</a>            <span class="macro">invalid_data!</span>(<span class="string">"failed to decode deletion-vector-v1 roaring payload"</span>).with_source(e)
-<a href=#219 id=219 data-nosnippet>219</a>        })<span class="question-mark">?</span>;
-<a href=#220 id=220 data-nosnippet>220</a>        bitmaps.push((key, bitmap));
-<a href=#221 id=221 data-nosnippet>221</a>    }
-<a href=#222 id=222 data-nosnippet>222</a>
-<a href=#223 id=223 data-nosnippet>223</a>    <span class="comment">// `bitmaps` is already sorted by key, but `roaring` has no constructor that accepts
-<a href=#224 id=224 data-nosnippet>224</a>    // pre-sorted pairs without re-sorting them; revisit if that changes upstream.
-<a href=#225 id=225 data-nosnippet>225</a>    </span><span class="prelude-val">Ok</span>(RoaringTreemap::from_bitmaps(bitmaps))
-<a href=#226 id=226 data-nosnippet>226</a>}
-<a href=#227 id=227 data-nosnippet>227</a>
-<a href=#228 id=228 data-nosnippet>228</a><span class="comment">// Ideally, we'd just wrap `roaring::RoaringTreemap`'s iterator, `roaring::treemap::Iter` here.
-<a href=#229 id=229 data-nosnippet>229</a>// But right now, it does not have a corresponding implementation of `roaring::bitmap::Iter::advance_to`,
-<a href=#230 id=230 data-nosnippet>230</a>// which is very handy in ArrowReader::build_deletes_row_selection.
-<a href=#231 id=231 data-nosnippet>231</a>// There is a PR open on roaring to add this (https://github.com/RoaringBitmap/roaring-rs/pull/314)
-<a href=#232 id=232 data-nosnippet>232</a>// and if that gets merged then we can simplify `DeleteVectorIterator` here, refactoring `advance_to`
-<a href=#233 id=233 data-nosnippet>233</a>// to just a wrapper around the underlying iterator's method.
-<a href=#234 id=234 data-nosnippet>234</a></span><span class="doccomment">/// Iterates the deleted positions of a [`DeleteVector`] in ascending order.
-<a href=#235 id=235 data-nosnippet>235</a></span><span class="kw">pub struct </span>DeleteVectorIterator&lt;<span class="lifetime">'a</span>&gt; {
-<a href=#236 id=236 data-nosnippet>236</a>    <span class="comment">// NB: `BitMapIter` was only exposed publicly in https://github.com/RoaringBitmap/roaring-rs/pull/316
-<a href=#237 id=237 data-nosnippet>237</a>    // which is not yet released. As a consequence our Cargo.toml temporarily uses a git reference for
-<a href=#238 id=238 data-nosnippet>238</a>    // the roaring dependency.
-<a href=#239 id=239 data-nosnippet>239</a>    </span>outer: BitmapIter&lt;<span class="lifetime">'a</span>&gt;,
-<a href=#240 id=240 data-nosnippet>240</a>    inner: <span class="prelude-ty">Option</span>&lt;DeleteVectorIteratorInner&lt;<span class="lifetime">'a</span>&gt;&gt;,
-<a href=#241 id=241 data-nosnippet>241</a>}
-<a href=#242 id=242 data-nosnippet>242</a>
-<a href=#243 id=243 data-nosnippet>243</a><span class="kw">struct </span>DeleteVectorIteratorInner&lt;<span class="lifetime">'a</span>&gt; {
-<a href=#244 id=244 data-nosnippet>244</a>    high_bits: u32,
-<a href=#245 id=245 data-nosnippet>245</a>    bitmap_iter: Iter&lt;<span class="lifetime">'a</span>&gt;,
+<a href=#111 id=111 data-nosnippet>111</a>    /// The layout, defined by the Iceberg Puffin spec and matching Iceberg-Java's
+<a href=#112 id=112 data-nosnippet>112</a>    /// `BitmapPositionDeleteIndex`, is:
+<a href=#113 id=113 data-nosnippet>113</a>    ///
+<a href=#114 id=114 data-nosnippet>114</a>    /// ```text
+<a href=#115 id=115 data-nosnippet>115</a>    /// [length: u32 big-endian][magic: D1 D3 39 64][vector][crc: u32 big-endian]
+<a href=#116 id=116 data-nosnippet>116</a>    /// ```
+<a href=#117 id=117 data-nosnippet>117</a>    ///
+<a href=#118 id=118 data-nosnippet>118</a>    /// `length` counts the magic and vector bytes (not itself or the CRC). The CRC-32 is
+<a href=#119 id=119 data-nosnippet>119</a>    /// computed over the magic and vector. `vector` is a roaring bitmap in the portable 64-bit
+<a href=#120 id=120 data-nosnippet>120</a>    /// format: a directory of 32-bit key / 32-bit roaring bitmap pairs, ordered by unsigned
+<a href=#121 id=121 data-nosnippet>121</a>    /// comparison of the keys, one bitmap per key.
+<a href=#122 id=122 data-nosnippet>122</a>    ///
+<a href=#123 id=123 data-nosnippet>123</a>    /// # Errors
+<a href=#124 id=124 data-nosnippet>124</a>    ///
+<a href=#125 id=125 data-nosnippet>125</a>    /// Returns [`ErrorKind::DataInvalid`] if the blob is shorter than the minimum, the length
+<a href=#126 id=126 data-nosnippet>126</a>    /// prefix or CRC does not match, the magic is wrong, the roaring bitmap count exceeds the
+<a href=#127 id=127 data-nosnippet>127</a>    /// portable format's maximum, the roaring directory's keys are not ordered by unsigned
+<a href=#128 id=128 data-nosnippet>128</a>    /// comparison, or the roaring payload fails to decode.
+<a href=#129 id=129 data-nosnippet>129</a>    </span><span class="kw">pub fn </span>deserialize(blob: <span class="kw-2">&amp;</span>[u8]) -&gt; <span class="prelude-ty">Result</span>&lt;<span class="self">Self</span>&gt; {
+<a href=#130 id=130 data-nosnippet>130</a>        <span class="kw">if </span>blob.len() &lt; DV_MIN_BLOB_BYTES {
+<a href=#131 id=131 data-nosnippet>131</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(<span class="macro">invalid_data!</span>(
+<a href=#132 id=132 data-nosnippet>132</a>                <span class="string">"deletion-vector-v1 blob is {} bytes, shorter than the {DV_MIN_BLOB_BYTES}-byte minimum"</span>,
+<a href=#133 id=133 data-nosnippet>133</a>                blob.len()
+<a href=#134 id=134 data-nosnippet>134</a>            ));
+<a href=#135 id=135 data-nosnippet>135</a>        }
+<a href=#136 id=136 data-nosnippet>136</a>
+<a href=#137 id=137 data-nosnippet>137</a>        <span class="comment">// The magic and vector, i.e. the bytes covered by both the length prefix and the CRC.
+<a href=#138 id=138 data-nosnippet>138</a>        </span><span class="kw">let </span>body = <span class="kw-2">&amp;</span>blob[DV_LENGTH_PREFIX_BYTES..blob.len() - DV_CRC_BYTES];
+<a href=#139 id=139 data-nosnippet>139</a>        verify_length_prefix(<span class="kw-2">&amp;</span>blob[..DV_LENGTH_PREFIX_BYTES], body)<span class="question-mark">?</span>;
+<a href=#140 id=140 data-nosnippet>140</a>
+<a href=#141 id=141 data-nosnippet>141</a>        <span class="comment">// Verify the CRC before interpreting any bytes so a corrupt blob yields a single clear
+<a href=#142 id=142 data-nosnippet>142</a>        // error rather than an opaque roaring decode failure.
+<a href=#143 id=143 data-nosnippet>143</a>        </span>verify_crc(body, <span class="kw-2">&amp;</span>blob[blob.len() - DV_CRC_BYTES..])<span class="question-mark">?</span>;
+<a href=#144 id=144 data-nosnippet>144</a>
+<a href=#145 id=145 data-nosnippet>145</a>        <span class="kw">let </span>(magic, vector) = body.split_at(DV_MAGIC_BYTES);
+<a href=#146 id=146 data-nosnippet>146</a>        verify_magic(magic)<span class="question-mark">?</span>;
+<a href=#147 id=147 data-nosnippet>147</a>
+<a href=#148 id=148 data-nosnippet>148</a>        <span class="kw">let </span>inner = decode_roaring_directory(vector)<span class="question-mark">?</span>;
+<a href=#149 id=149 data-nosnippet>149</a>
+<a href=#150 id=150 data-nosnippet>150</a>        <span class="prelude-val">Ok</span>(DeleteVector { inner })
+<a href=#151 id=151 data-nosnippet>151</a>    }
+<a href=#152 id=152 data-nosnippet>152</a>}
+<a href=#153 id=153 data-nosnippet>153</a>
+<a href=#154 id=154 data-nosnippet>154</a><span class="kw">fn </span>verify_length_prefix(<span class="kw-2">mut </span>prefix: <span class="kw-2">&amp;</span>[u8], body: <span class="kw-2">&amp;</span>[u8]) -&gt; <span class="prelude-ty">Result</span>&lt;()&gt; {
+<a href=#155 id=155 data-nosnippet>155</a>    <span class="kw">let </span>declared_len = prefix.try_get_u32().map_err(|e| {
+<a href=#156 id=156 data-nosnippet>156</a>        Error::new(
+<a href=#157 id=157 data-nosnippet>157</a>            ErrorKind::Unexpected,
+<a href=#158 id=158 data-nosnippet>158</a>            <span class="string">"failed to read the deletion-vector-v1 length prefix"</span>,
+<a href=#159 id=159 data-nosnippet>159</a>        )
+<a href=#160 id=160 data-nosnippet>160</a>        .with_source(e)
+<a href=#161 id=161 data-nosnippet>161</a>    })<span class="question-mark">? </span><span class="kw">as </span>usize;
+<a href=#162 id=162 data-nosnippet>162</a>    <span class="kw">if </span>declared_len != body.len() {
+<a href=#163 id=163 data-nosnippet>163</a>        <span class="kw">return </span><span class="prelude-val">Err</span>(<span class="macro">invalid_data!</span>(
+<a href=#164 id=164 data-nosnippet>164</a>            <span class="string">"deletion-vector-v1 length prefix is {declared_len}, expected {}"</span>,
+<a href=#165 id=165 data-nosnippet>165</a>            body.len()
+<a href=#166 id=166 data-nosnippet>166</a>        ));
+<a href=#167 id=167 data-nosnippet>167</a>    }
+<a href=#168 id=168 data-nosnippet>168</a>    <span class="prelude-val">Ok</span>(())
+<a href=#169 id=169 data-nosnippet>169</a>}
+<a href=#170 id=170 data-nosnippet>170</a>
+<a href=#171 id=171 data-nosnippet>171</a><span class="kw">fn </span>verify_crc(body: <span class="kw-2">&amp;</span>[u8], <span class="kw-2">mut </span>crc_bytes: <span class="kw-2">&amp;</span>[u8]) -&gt; <span class="prelude-ty">Result</span>&lt;()&gt; {
+<a href=#172 id=172 data-nosnippet>172</a>    <span class="kw">let </span>stored_crc = crc_bytes.try_get_u32().map_err(|e| {
+<a href=#173 id=173 data-nosnippet>173</a>        Error::new(
+<a href=#174 id=174 data-nosnippet>174</a>            ErrorKind::Unexpected,
+<a href=#175 id=175 data-nosnippet>175</a>            <span class="string">"failed to read the deletion-vector-v1 CRC"</span>,
+<a href=#176 id=176 data-nosnippet>176</a>        )
+<a href=#177 id=177 data-nosnippet>177</a>        .with_source(e)
+<a href=#178 id=178 data-nosnippet>178</a>    })<span class="question-mark">?</span>;
+<a href=#179 id=179 data-nosnippet>179</a>    <span class="kw">let </span>computed_crc = crc32fast::hash(body);
+<a href=#180 id=180 data-nosnippet>180</a>    <span class="kw">if </span>computed_crc != stored_crc {
+<a href=#181 id=181 data-nosnippet>181</a>        <span class="kw">return </span><span class="prelude-val">Err</span>(<span class="macro">invalid_data!</span>(
+<a href=#182 id=182 data-nosnippet>182</a>            <span class="string">"deletion-vector-v1 CRC mismatch: computed {computed_crc:#010x}, stored {stored_crc:#010x}"
+<a href=#183 id=183 data-nosnippet>183</a>        </span>));
+<a href=#184 id=184 data-nosnippet>184</a>    }
+<a href=#185 id=185 data-nosnippet>185</a>    <span class="prelude-val">Ok</span>(())
+<a href=#186 id=186 data-nosnippet>186</a>}
+<a href=#187 id=187 data-nosnippet>187</a>
+<a href=#188 id=188 data-nosnippet>188</a><span class="kw">fn </span>verify_magic(magic: <span class="kw-2">&amp;</span>[u8]) -&gt; <span class="prelude-ty">Result</span>&lt;()&gt; {
+<a href=#189 id=189 data-nosnippet>189</a>    <span class="kw">if </span>magic != DV_MAGIC {
+<a href=#190 id=190 data-nosnippet>190</a>        <span class="kw">return </span><span class="prelude-val">Err</span>(<span class="macro">invalid_data!</span>(
+<a href=#191 id=191 data-nosnippet>191</a>            <span class="string">"deletion-vector-v1 magic mismatch: {magic:02x?}, expected {DV_MAGIC:02x?}"
+<a href=#192 id=192 data-nosnippet>192</a>        </span>));
+<a href=#193 id=193 data-nosnippet>193</a>    }
+<a href=#194 id=194 data-nosnippet>194</a>    <span class="prelude-val">Ok</span>(())
+<a href=#195 id=195 data-nosnippet>195</a>}
+<a href=#196 id=196 data-nosnippet>196</a>
+<a href=#197 id=197 data-nosnippet>197</a><span class="comment">// The Puffin spec defines the roaring directory as the bitmaps "ordered by unsigned comparison
+<a href=#198 id=198 data-nosnippet>198</a>// of the 32-bit keys", with one bitmap per key. Walk it ourselves (rather than
+<a href=#199 id=199 data-nosnippet>199</a>// `RoaringTreemap::deserialize_from`, which stores keys in a `BTreeMap` via a plain insert and
+<a href=#200 id=200 data-nosnippet>200</a>// would silently accept a stream with duplicate or out-of-order keys, discarding the earlier
+<a href=#201 id=201 data-nosnippet>201</a>// bitmap on a duplicate) so a non-conformant blob is rejected instead of decoded into a value
+<a href=#202 id=202 data-nosnippet>202</a>// that doesn't match what was actually written.
+<a href=#203 id=203 data-nosnippet>203</a></span><span class="kw">fn </span>decode_roaring_directory(<span class="kw-2">mut </span>reader: <span class="kw-2">&amp;</span>[u8]) -&gt; <span class="prelude-ty">Result</span>&lt;RoaringTreemap&gt; {
+<a href=#204 id=204 data-nosnippet>204</a>    <span class="kw">let </span>bitmap_count = reader.try_get_u64_le().map_err(|e| {
+<a href=#205 id=205 data-nosnippet>205</a>        <span class="macro">invalid_data!</span>(<span class="string">"failed to decode deletion-vector-v1 roaring payload"</span>).with_source(e)
+<a href=#206 id=206 data-nosnippet>206</a>    })<span class="question-mark">?</span>;
+<a href=#207 id=207 data-nosnippet>207</a>    <span class="comment">// The roaring portable format restricts the bitmap count to [0, 2^32 - 1] (it is stored as a
+<a href=#208 id=208 data-nosnippet>208</a>    // u64 with the upper 32 bits reserved as zero padding).
+<a href=#209 id=209 data-nosnippet>209</a>    </span><span class="kw">if </span>bitmap_count &gt; u32::MAX <span class="kw">as </span>u64 {
+<a href=#210 id=210 data-nosnippet>210</a>        <span class="kw">return </span><span class="prelude-val">Err</span>(<span class="macro">invalid_data!</span>(
+<a href=#211 id=211 data-nosnippet>211</a>            <span class="string">"deletion-vector-v1 roaring bitmap count {bitmap_count} exceeds the {}-key maximum"</span>,
+<a href=#212 id=212 data-nosnippet>212</a>            u32::MAX
+<a href=#213 id=213 data-nosnippet>213</a>        ));
+<a href=#214 id=214 data-nosnippet>214</a>    }
+<a href=#215 id=215 data-nosnippet>215</a>
+<a href=#216 id=216 data-nosnippet>216</a>    <span class="kw">let </span><span class="kw-2">mut </span>bitmaps = Vec::new();
+<a href=#217 id=217 data-nosnippet>217</a>    <span class="kw">let </span><span class="kw-2">mut </span>last_key: <span class="prelude-ty">Option</span>&lt;u32&gt; = <span class="prelude-val">None</span>;
+<a href=#218 id=218 data-nosnippet>218</a>    <span class="kw">for _ in </span><span class="number">0</span>..bitmap_count {
+<a href=#219 id=219 data-nosnippet>219</a>        <span class="kw">let </span>key = reader.try_get_u32_le().map_err(|e| {
+<a href=#220 id=220 data-nosnippet>220</a>            <span class="macro">invalid_data!</span>(<span class="string">"failed to decode deletion-vector-v1 roaring payload"</span>).with_source(e)
+<a href=#221 id=221 data-nosnippet>221</a>        })<span class="question-mark">?</span>;
+<a href=#222 id=222 data-nosnippet>222</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(last) = last_key
+<a href=#223 id=223 data-nosnippet>223</a>            &amp;&amp; key &lt;= last
+<a href=#224 id=224 data-nosnippet>224</a>        {
+<a href=#225 id=225 data-nosnippet>225</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(<span class="macro">invalid_data!</span>(
+<a href=#226 id=226 data-nosnippet>226</a>                <span class="string">"deletion-vector-v1 roaring keys must be ordered by unsigned comparison, got key {key} after {last}"
+<a href=#227 id=227 data-nosnippet>227</a>            </span>));
+<a href=#228 id=228 data-nosnippet>228</a>        }
+<a href=#229 id=229 data-nosnippet>229</a>        last_key = <span class="prelude-val">Some</span>(key);
+<a href=#230 id=230 data-nosnippet>230</a>
+<a href=#231 id=231 data-nosnippet>231</a>        <span class="kw">let </span>bitmap = RoaringBitmap::deserialize_from(<span class="kw-2">&amp;mut </span>reader).map_err(|e| {
+<a href=#232 id=232 data-nosnippet>232</a>            <span class="macro">invalid_data!</span>(<span class="string">"failed to decode deletion-vector-v1 roaring payload"</span>).with_source(e)
+<a href=#233 id=233 data-nosnippet>233</a>        })<span class="question-mark">?</span>;
+<a href=#234 id=234 data-nosnippet>234</a>        bitmaps.push((key, bitmap));
+<a href=#235 id=235 data-nosnippet>235</a>    }
+<a href=#236 id=236 data-nosnippet>236</a>
+<a href=#237 id=237 data-nosnippet>237</a>    <span class="comment">// `bitmaps` is already sorted by key, but `roaring` has no constructor that accepts
+<a href=#238 id=238 data-nosnippet>238</a>    // pre-sorted pairs without re-sorting them; revisit if that changes upstream.
+<a href=#239 id=239 data-nosnippet>239</a>    </span><span class="prelude-val">Ok</span>(RoaringTreemap::from_bitmaps(bitmaps))
+<a href=#240 id=240 data-nosnippet>240</a>}
+<a href=#241 id=241 data-nosnippet>241</a>
+<a href=#242 id=242 data-nosnippet>242</a><span class="doccomment">/// Iterates the deleted positions of a [`DeleteVector`] in ascending order.
+<a href=#243 id=243 data-nosnippet>243</a></span><span class="kw">pub struct </span>DeleteVectorIterator&lt;<span class="lifetime">'a</span>&gt; {
+<a href=#244 id=244 data-nosnippet>244</a>    outer: BitmapIter&lt;<span class="lifetime">'a</span>&gt;,
+<a href=#245 id=245 data-nosnippet>245</a>    inner: <span class="prelude-ty">Option</span>&lt;DeleteVectorIteratorInner&lt;<span class="lifetime">'a</span>&gt;&gt;,
 <a href=#246 id=246 data-nosnippet>246</a>}
 <a href=#247 id=247 data-nosnippet>247</a>
-<a href=#248 id=248 data-nosnippet>248</a><span class="kw">impl </span>Iterator <span class="kw">for </span>DeleteVectorIterator&lt;<span class="lifetime">'_</span>&gt; {
-<a href=#249 id=249 data-nosnippet>249</a>    <span class="kw">type </span>Item = u64;
-<a href=#250 id=250 data-nosnippet>250</a>
-<a href=#251 id=251 data-nosnippet>251</a>    <span class="kw">fn </span>next(<span class="kw-2">&amp;mut </span><span class="self">self</span>) -&gt; <span class="prelude-ty">Option</span>&lt;<span class="self">Self</span>::Item&gt; {
-<a href=#252 id=252 data-nosnippet>252</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(inner) = <span class="kw-2">&amp;mut </span><span class="self">self</span>.inner
-<a href=#253 id=253 data-nosnippet>253</a>            &amp;&amp; <span class="kw">let </span><span class="prelude-val">Some</span>(inner_next) = inner.bitmap_iter.next()
-<a href=#254 id=254 data-nosnippet>254</a>        {
-<a href=#255 id=255 data-nosnippet>255</a>            <span class="kw">return </span><span class="prelude-val">Some</span>(u64::from(inner.high_bits) &lt;&lt; <span class="number">32 </span>| u64::from(inner_next));
-<a href=#256 id=256 data-nosnippet>256</a>        }
-<a href=#257 id=257 data-nosnippet>257</a>
-<a href=#258 id=258 data-nosnippet>258</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>((high_bits, next_bitmap)) = <span class="self">self</span>.outer.next() {
-<a href=#259 id=259 data-nosnippet>259</a>            <span class="self">self</span>.inner = <span class="prelude-val">Some</span>(DeleteVectorIteratorInner {
-<a href=#260 id=260 data-nosnippet>260</a>                high_bits,
-<a href=#261 id=261 data-nosnippet>261</a>                bitmap_iter: next_bitmap.iter(),
-<a href=#262 id=262 data-nosnippet>262</a>            })
-<a href=#263 id=263 data-nosnippet>263</a>        } <span class="kw">else </span>{
-<a href=#264 id=264 data-nosnippet>264</a>            <span class="kw">return </span><span class="prelude-val">None</span>;
-<a href=#265 id=265 data-nosnippet>265</a>        }
-<a href=#266 id=266 data-nosnippet>266</a>
-<a href=#267 id=267 data-nosnippet>267</a>        <span class="self">self</span>.next()
-<a href=#268 id=268 data-nosnippet>268</a>    }
-<a href=#269 id=269 data-nosnippet>269</a>}
-<a href=#270 id=270 data-nosnippet>270</a>
-<a href=#271 id=271 data-nosnippet>271</a><span class="kw">impl </span>DeleteVectorIterator&lt;<span class="lifetime">'_</span>&gt; {
-<a href=#272 id=272 data-nosnippet>272</a>    <span class="doccomment">/// Skips the positions below `pos`, so the next one yielded is the first
-<a href=#273 id=273 data-nosnippet>273</a>    /// at or after it. Does nothing before the first position is yielded.
-<a href=#274 id=274 data-nosnippet>274</a>    </span><span class="kw">pub fn </span>advance_to(<span class="kw-2">&amp;mut </span><span class="self">self</span>, pos: u64) {
-<a href=#275 id=275 data-nosnippet>275</a>        <span class="kw">let </span>hi = (pos &gt;&gt; <span class="number">32</span>) <span class="kw">as </span>u32;
-<a href=#276 id=276 data-nosnippet>276</a>        <span class="kw">let </span>lo = pos <span class="kw">as </span>u32;
-<a href=#277 id=277 data-nosnippet>277</a>
-<a href=#278 id=278 data-nosnippet>278</a>        <span class="kw">let </span><span class="prelude-val">Some</span>(<span class="kw-2">ref mut </span>inner) = <span class="self">self</span>.inner <span class="kw">else </span>{
-<a href=#279 id=279 data-nosnippet>279</a>            <span class="kw">return</span>;
-<a href=#280 id=280 data-nosnippet>280</a>        };
-<a href=#281 id=281 data-nosnippet>281</a>
-<a href=#282 id=282 data-nosnippet>282</a>        <span class="kw">while </span>inner.high_bits &lt; hi {
-<a href=#283 id=283 data-nosnippet>283</a>            <span class="kw">let </span><span class="prelude-val">Some</span>((next_hi, next_bitmap)) = <span class="self">self</span>.outer.next() <span class="kw">else </span>{
-<a href=#284 id=284 data-nosnippet>284</a>                <span class="kw">return</span>;
-<a href=#285 id=285 data-nosnippet>285</a>            };
-<a href=#286 id=286 data-nosnippet>286</a>
-<a href=#287 id=287 data-nosnippet>287</a>            <span class="kw-2">*</span>inner = DeleteVectorIteratorInner {
-<a href=#288 id=288 data-nosnippet>288</a>                high_bits: next_hi,
-<a href=#289 id=289 data-nosnippet>289</a>                bitmap_iter: next_bitmap.iter(),
+<a href=#248 id=248 data-nosnippet>248</a><span class="kw">struct </span>DeleteVectorIteratorInner&lt;<span class="lifetime">'a</span>&gt; {
+<a href=#249 id=249 data-nosnippet>249</a>    high_bits: u32,
+<a href=#250 id=250 data-nosnippet>250</a>    bitmap_iter: Iter&lt;<span class="lifetime">'a</span>&gt;,
+<a href=#251 id=251 data-nosnippet>251</a>}
+<a href=#252 id=252 data-nosnippet>252</a>
+<a href=#253 id=253 data-nosnippet>253</a><span class="kw">impl </span>Iterator <span class="kw">for </span>DeleteVectorIterator&lt;<span class="lifetime">'_</span>&gt; {
+<a href=#254 id=254 data-nosnippet>254</a>    <span class="kw">type </span>Item = u64;
+<a href=#255 id=255 data-nosnippet>255</a>
+<a href=#256 id=256 data-nosnippet>256</a>    <span class="kw">fn </span>next(<span class="kw-2">&amp;mut </span><span class="self">self</span>) -&gt; <span class="prelude-ty">Option</span>&lt;<span class="self">Self</span>::Item&gt; {
+<a href=#257 id=257 data-nosnippet>257</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(inner) = <span class="kw-2">&amp;mut </span><span class="self">self</span>.inner
+<a href=#258 id=258 data-nosnippet>258</a>            &amp;&amp; <span class="kw">let </span><span class="prelude-val">Some</span>(inner_next) = inner.bitmap_iter.next()
+<a href=#259 id=259 data-nosnippet>259</a>        {
+<a href=#260 id=260 data-nosnippet>260</a>            <span class="kw">return </span><span class="prelude-val">Some</span>(u64::from(inner.high_bits) &lt;&lt; <span class="number">32 </span>| u64::from(inner_next));
+<a href=#261 id=261 data-nosnippet>261</a>        }
+<a href=#262 id=262 data-nosnippet>262</a>
+<a href=#263 id=263 data-nosnippet>263</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>((high_bits, next_bitmap)) = <span class="self">self</span>.outer.next() {
+<a href=#264 id=264 data-nosnippet>264</a>            <span class="self">self</span>.inner = <span class="prelude-val">Some</span>(DeleteVectorIteratorInner {
+<a href=#265 id=265 data-nosnippet>265</a>                high_bits,
+<a href=#266 id=266 data-nosnippet>266</a>                bitmap_iter: next_bitmap.iter(),
+<a href=#267 id=267 data-nosnippet>267</a>            })
+<a href=#268 id=268 data-nosnippet>268</a>        } <span class="kw">else </span>{
+<a href=#269 id=269 data-nosnippet>269</a>            <span class="kw">return </span><span class="prelude-val">None</span>;
+<a href=#270 id=270 data-nosnippet>270</a>        }
+<a href=#271 id=271 data-nosnippet>271</a>
+<a href=#272 id=272 data-nosnippet>272</a>        <span class="self">self</span>.next()
+<a href=#273 id=273 data-nosnippet>273</a>    }
+<a href=#274 id=274 data-nosnippet>274</a>}
+<a href=#275 id=275 data-nosnippet>275</a>
+<a href=#276 id=276 data-nosnippet>276</a><span class="kw">impl </span>DeleteVectorIterator&lt;<span class="lifetime">'_</span>&gt; {
+<a href=#277 id=277 data-nosnippet>277</a>    <span class="doccomment">/// Skips the positions below `pos`, so the next one yielded is the first
+<a href=#278 id=278 data-nosnippet>278</a>    /// at or after it. A `pos` below the next position does nothing.
+<a href=#279 id=279 data-nosnippet>279</a>    </span><span class="kw">pub fn </span>advance_to(<span class="kw-2">&amp;mut </span><span class="self">self</span>, pos: u64) {
+<a href=#280 id=280 data-nosnippet>280</a>        <span class="kw">let </span>hi = (pos &gt;&gt; <span class="number">32</span>) <span class="kw">as </span>u32;
+<a href=#281 id=281 data-nosnippet>281</a>        <span class="kw">let </span>lo = pos <span class="kw">as </span>u32;
+<a href=#282 id=282 data-nosnippet>282</a>
+<a href=#283 id=283 data-nosnippet>283</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(inner) = <span class="kw-2">&amp;mut </span><span class="self">self</span>.inner {
+<a href=#284 id=284 data-nosnippet>284</a>            <span class="kw">if </span>inner.high_bits &gt; hi {
+<a href=#285 id=285 data-nosnippet>285</a>                <span class="kw">return</span>;
+<a href=#286 id=286 data-nosnippet>286</a>            }
+<a href=#287 id=287 data-nosnippet>287</a>            <span class="kw">if </span>inner.high_bits == hi {
+<a href=#288 id=288 data-nosnippet>288</a>                inner.bitmap_iter.advance_to(lo);
+<a href=#289 id=289 data-nosnippet>289</a>                <span class="kw">return</span>;
 <a href=#290 id=290 data-nosnippet>290</a>            }
 <a href=#291 id=291 data-nosnippet>291</a>        }
 <a href=#292 id=292 data-nosnippet>292</a>
-<a href=#293 id=293 data-nosnippet>293</a>        inner.bitmap_iter.advance_to(lo);
-<a href=#294 id=294 data-nosnippet>294</a>    }
-<a href=#295 id=295 data-nosnippet>295</a>}
-<a href=#296 id=296 data-nosnippet>296</a>
-<a href=#297 id=297 data-nosnippet>297</a><span class="kw">impl </span>BitOrAssign <span class="kw">for </span>DeleteVector {
-<a href=#298 id=298 data-nosnippet>298</a>    <span class="kw">fn </span>bitor_assign(<span class="kw-2">&amp;mut </span><span class="self">self</span>, other: <span class="self">Self</span>) {
-<a href=#299 id=299 data-nosnippet>299</a>        <span class="self">self</span>.inner.bitor_assign(<span class="kw-2">&amp;</span>other.inner);
-<a href=#300 id=300 data-nosnippet>300</a>    }
-<a href=#301 id=301 data-nosnippet>301</a>}
-<a href=#302 id=302 data-nosnippet>302</a>
-<a href=#303 id=303 data-nosnippet>303</a><span class="comment">// Reproduces Iceberg-Java's `deletion-vector-v1` framing so tests can round-trip through
-<a href=#304 id=304 data-nosnippet>304</a>// `deserialize` without a Java writer, and so other test modules can build blob fixtures.
-<a href=#305 id=305 data-nosnippet>305</a>// Cross-implementation golden fixtures produced by Iceberg-Java are tracked separately; this
-<a href=#306 id=306 data-nosnippet>306</a>// only checks that our decode matches our encode.
-<a href=#307 id=307 data-nosnippet>307</a></span><span class="attr">#[cfg(test)]
-<a href=#308 id=308 data-nosnippet>308</a></span><span class="kw">pub</span>(<span class="kw">crate</span>) <span class="kw">fn </span>frame_dv_blob(vector: <span class="kw-2">&amp;</span>[u8]) -&gt; Vec&lt;u8&gt; {
-<a href=#309 id=309 data-nosnippet>309</a>    <span class="kw">let </span>body_len = DV_MAGIC_BYTES + vector.len();
-<a href=#310 id=310 data-nosnippet>310</a>    <span class="kw">let </span><span class="kw-2">mut </span>blob = Vec::with_capacity(DV_LENGTH_PREFIX_BYTES + body_len + DV_CRC_BYTES);
-<a href=#311 id=311 data-nosnippet>311</a>    blob.extend_from_slice(<span class="kw-2">&amp;</span>(body_len <span class="kw">as </span>u32).to_be_bytes());
-<a href=#312 id=312 data-nosnippet>312</a>    blob.extend_from_slice(<span class="kw-2">&amp;</span>DV_MAGIC);
-<a href=#313 id=313 data-nosnippet>313</a>    blob.extend_from_slice(vector);
-<a href=#314 id=314 data-nosnippet>314</a>    <span class="kw">let </span>crc = crc32fast::hash(<span class="kw-2">&amp;</span>blob[DV_LENGTH_PREFIX_BYTES..]);
-<a href=#315 id=315 data-nosnippet>315</a>    blob.extend_from_slice(<span class="kw-2">&amp;</span>crc.to_be_bytes());
-<a href=#316 id=316 data-nosnippet>316</a>    blob
-<a href=#317 id=317 data-nosnippet>317</a>}
-<a href=#318 id=318 data-nosnippet>318</a>
-<a href=#319 id=319 data-nosnippet>319</a><span class="attr">#[cfg(test)]
-<a href=#320 id=320 data-nosnippet>320</a></span><span class="kw">mod </span>tests {
-<a href=#321 id=321 data-nosnippet>321</a>    <span class="kw">use </span>super::<span class="kw-2">*</span>;
-<a href=#322 id=322 data-nosnippet>322</a>
-<a href=#323 id=323 data-nosnippet>323</a>    <span class="attr">#[test]
-<a href=#324 id=324 data-nosnippet>324</a>    </span><span class="kw">fn </span>test_insertion_and_iteration() {
-<a href=#325 id=325 data-nosnippet>325</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
-<a href=#326 id=326 data-nosnippet>326</a>        <span class="macro">assert!</span>(dv.insert(<span class="number">42</span>));
-<a href=#327 id=327 data-nosnippet>327</a>        <span class="macro">assert!</span>(dv.insert(<span class="number">100</span>));
-<a href=#328 id=328 data-nosnippet>328</a>        <span class="macro">assert!</span>(!dv.insert(<span class="number">42</span>));
+<a href=#293 id=293 data-nosnippet>293</a>        <span class="comment">// `pos` is in a later bitmap, or nothing has been yielded yet.
+<a href=#294 id=294 data-nosnippet>294</a>        </span><span class="self">self</span>.inner = <span class="prelude-val">None</span>;
+<a href=#295 id=295 data-nosnippet>295</a>        <span class="kw">for </span>(high_bits, bitmap) <span class="kw">in </span><span class="self">self</span>.outer.by_ref() {
+<a href=#296 id=296 data-nosnippet>296</a>            <span class="kw">if </span>high_bits &lt; hi {
+<a href=#297 id=297 data-nosnippet>297</a>                <span class="kw">continue</span>;
+<a href=#298 id=298 data-nosnippet>298</a>            }
+<a href=#299 id=299 data-nosnippet>299</a>            <span class="kw">let </span><span class="kw-2">mut </span>bitmap_iter = bitmap.iter();
+<a href=#300 id=300 data-nosnippet>300</a>            <span class="kw">if </span>high_bits == hi {
+<a href=#301 id=301 data-nosnippet>301</a>                bitmap_iter.advance_to(lo);
+<a href=#302 id=302 data-nosnippet>302</a>            }
+<a href=#303 id=303 data-nosnippet>303</a>            <span class="self">self</span>.inner = <span class="prelude-val">Some</span>(DeleteVectorIteratorInner {
+<a href=#304 id=304 data-nosnippet>304</a>                high_bits,
+<a href=#305 id=305 data-nosnippet>305</a>                bitmap_iter,
+<a href=#306 id=306 data-nosnippet>306</a>            });
+<a href=#307 id=307 data-nosnippet>307</a>            <span class="kw">return</span>;
+<a href=#308 id=308 data-nosnippet>308</a>        }
+<a href=#309 id=309 data-nosnippet>309</a>    }
+<a href=#310 id=310 data-nosnippet>310</a>}
+<a href=#311 id=311 data-nosnippet>311</a>
+<a href=#312 id=312 data-nosnippet>312</a><span class="doccomment">/// Number of consecutive positions [`DeleteVectorRanges`] walks one by one before it asks the
+<a href=#313 id=313 data-nosnippet>313</a>/// bitmap for the rest of the run.
+<a href=#314 id=314 data-nosnippet>314</a>///
+<a href=#315 id=315 data-nosnippet>315</a>/// `roaring`'s `next_range` binary-searches the rest of an array container on every call, which
+<a href=#316 id=316 data-nosnippet>316</a>/// costs more than `next` when most runs are a single position, so it only pays off for long
+<a href=#317 id=317 data-nosnippet>317</a>/// runs. Tuned with `roaring` 0.11.4 by reading a 20M-row file with deletes in runs of 20, 100
+<a href=#318 id=318 data-nosnippet>318</a>/// and longer, and as scattered single rows and pairs: 4 was best on runs, compared with 8, 16
+<a href=#319 id=319 data-nosnippet>319</a>/// and 32, and no slower on scattered deletes.
+<a href=#320 id=320 data-nosnippet>320</a></span><span class="kw">const </span>RUN_PROBE_LEN: u32 = <span class="number">4</span>;
+<a href=#321 id=321 data-nosnippet>321</a>
+<a href=#322 id=322 data-nosnippet>322</a><span class="doccomment">/// What [`DeleteVectorRanges`] has taken from its bitmap iterator but not returned yet.
+<a href=#323 id=323 data-nosnippet>323</a></span><span class="kw">enum </span>Buffered {
+<a href=#324 id=324 data-nosnippet>324</a>    <span class="doccomment">/// Start of the next range.
+<a href=#325 id=325 data-nosnippet>325</a>    </span>Pos(u32),
+<a href=#326 id=326 data-nosnippet>326</a>    <span class="doccomment">/// A complete range, inclusive end.
+<a href=#327 id=327 data-nosnippet>327</a>    </span>Range(u32, u32),
+<a href=#328 id=328 data-nosnippet>328</a>}
 <a href=#329 id=329 data-nosnippet>329</a>
-<a href=#330 id=330 data-nosnippet>330</a>        <span class="kw">let </span><span class="kw-2">mut </span>items: Vec&lt;u64&gt; = dv.iter().collect();
-<a href=#331 id=331 data-nosnippet>331</a>        items.sort();
-<a href=#332 id=332 data-nosnippet>332</a>        <span class="macro">assert_eq!</span>(items, <span class="macro">vec!</span>[<span class="number">42</span>, <span class="number">100</span>]);
-<a href=#333 id=333 data-nosnippet>333</a>        <span class="macro">assert_eq!</span>(dv.len(), <span class="number">2</span>);
-<a href=#334 id=334 data-nosnippet>334</a>    }
-<a href=#335 id=335 data-nosnippet>335</a>
-<a href=#336 id=336 data-nosnippet>336</a>    <span class="attr">#[test]
-<a href=#337 id=337 data-nosnippet>337</a>    </span><span class="kw">fn </span>test_successful_insert_positions() {
-<a href=#338 id=338 data-nosnippet>338</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
-<a href=#339 id=339 data-nosnippet>339</a>        <span class="kw">let </span>positions = <span class="macro">vec!</span>[<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">1000</span>, <span class="number">1 </span>&lt;&lt; <span class="number">33</span>];
-<a href=#340 id=340 data-nosnippet>340</a>        <span class="macro">assert_eq!</span>(dv.insert_positions(<span class="kw-2">&amp;</span>positions).unwrap(), <span class="number">5</span>);
-<a href=#341 id=341 data-nosnippet>341</a>
-<a href=#342 id=342 data-nosnippet>342</a>        <span class="kw">let </span><span class="kw-2">mut </span>collected: Vec&lt;u64&gt; = dv.iter().collect();
-<a href=#343 id=343 data-nosnippet>343</a>        collected.sort();
-<a href=#344 id=344 data-nosnippet>344</a>        <span class="macro">assert_eq!</span>(collected, positions);
-<a href=#345 id=345 data-nosnippet>345</a>    }
-<a href=#346 id=346 data-nosnippet>346</a>
-<a href=#347 id=347 data-nosnippet>347</a>    <span class="doccomment">/// Testing scenario: bulk insertion fails because input positions are not strictly increasing.
-<a href=#348 id=348 data-nosnippet>348</a>    </span><span class="attr">#[test]
-<a href=#349 id=349 data-nosnippet>349</a>    </span><span class="kw">fn </span>test_failed_insertion_unsorted_elements() {
-<a href=#350 id=350 data-nosnippet>350</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
-<a href=#351 id=351 data-nosnippet>351</a>        <span class="kw">let </span>positions = <span class="macro">vec!</span>[<span class="number">1</span>, <span class="number">3</span>, <span class="number">5</span>, <span class="number">4</span>];
-<a href=#352 id=352 data-nosnippet>352</a>        <span class="kw">let </span>res = dv.insert_positions(<span class="kw-2">&amp;</span>positions);
-<a href=#353 id=353 data-nosnippet>353</a>        <span class="macro">assert!</span>(res.is_err());
-<a href=#354 id=354 data-nosnippet>354</a>    }
+<a href=#330 id=330 data-nosnippet>330</a><span class="doccomment">/// Iterator over a [`DeleteVector`]'s deleted positions as half-open ranges, see
+<a href=#331 id=331 data-nosnippet>331</a>/// [`DeleteVector::ranges`].
+<a href=#332 id=332 data-nosnippet>332</a></span><span class="kw">pub</span>(<span class="kw">crate</span>) <span class="kw">struct </span>DeleteVectorRanges&lt;<span class="lifetime">'a</span>&gt; {
+<a href=#333 id=333 data-nosnippet>333</a>    bitmaps: BitmapIter&lt;<span class="lifetime">'a</span>&gt;,
+<a href=#334 id=334 data-nosnippet>334</a>    <span class="doccomment">/// High 32 bits of the positions in `bitmap_iter` and `buffered`.
+<a href=#335 id=335 data-nosnippet>335</a>    </span>high_bits: u32,
+<a href=#336 id=336 data-nosnippet>336</a>    bitmap_iter: <span class="prelude-ty">Option</span>&lt;Iter&lt;<span class="lifetime">'a</span>&gt;&gt;,
+<a href=#337 id=337 data-nosnippet>337</a>    buffered: <span class="prelude-ty">Option</span>&lt;Buffered&gt;,
+<a href=#338 id=338 data-nosnippet>338</a>}
+<a href=#339 id=339 data-nosnippet>339</a>
+<a href=#340 id=340 data-nosnippet>340</a><span class="kw">impl </span>DeleteVectorRanges&lt;<span class="lifetime">'_</span>&gt; {
+<a href=#341 id=341 data-nosnippet>341</a>    <span class="doccomment">/// Returns `None` only for a range holding nothing but `u64::MAX`, see [`DeleteVector::ranges`].
+<a href=#342 id=342 data-nosnippet>342</a>    </span><span class="kw">fn </span>full_range(<span class="kw-2">&amp;</span><span class="self">self</span>, start: u32, end_inclusive: u32) -&gt; <span class="prelude-ty">Option</span>&lt;(u64, u64)&gt; {
+<a href=#343 id=343 data-nosnippet>343</a>        <span class="kw">let </span>base = u64::from(<span class="self">self</span>.high_bits) &lt;&lt; <span class="number">32</span>;
+<a href=#344 id=344 data-nosnippet>344</a>        <span class="kw">let </span>start = base | u64::from(start);
+<a href=#345 id=345 data-nosnippet>345</a>        <span class="kw">let </span>end = (base | u64::from(end_inclusive)).saturating_add(<span class="number">1</span>);
+<a href=#346 id=346 data-nosnippet>346</a>        (start &lt; end).then_some((start, end))
+<a href=#347 id=347 data-nosnippet>347</a>    }
+<a href=#348 id=348 data-nosnippet>348</a>
+<a href=#349 id=349 data-nosnippet>349</a>    <span class="doccomment">/// Skips every deleted position below `pos`. A range still buffered here that starts below
+<a href=#350 id=350 data-nosnippet>350</a>    /// `pos` and ends after it is cut to start at `pos`; a range already returned by `next` is
+<a href=#351 id=351 data-nosnippet>351</a>    /// the caller's to cut.
+<a href=#352 id=352 data-nosnippet>352</a>    </span><span class="kw">pub</span>(<span class="kw">crate</span>) <span class="kw">fn </span>skip_to(<span class="kw-2">&amp;mut </span><span class="self">self</span>, pos: u64) {
+<a href=#353 id=353 data-nosnippet>353</a>        <span class="kw">let </span>hi = (pos &gt;&gt; <span class="number">32</span>) <span class="kw">as </span>u32;
+<a href=#354 id=354 data-nosnippet>354</a>        <span class="kw">let </span>lo = pos <span class="kw">as </span>u32;
 <a href=#355 id=355 data-nosnippet>355</a>
-<a href=#356 id=356 data-nosnippet>356</a>    <span class="doccomment">/// Testing scenario: bulk insertion fails because input positions have intersection with existing ones.
-<a href=#357 id=357 data-nosnippet>357</a>    </span><span class="attr">#[test]
-<a href=#358 id=358 data-nosnippet>358</a>    </span><span class="kw">fn </span>test_failed_insertion_with_intersection() {
-<a href=#359 id=359 data-nosnippet>359</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
-<a href=#360 id=360 data-nosnippet>360</a>        <span class="kw">let </span>positions = <span class="macro">vec!</span>[<span class="number">1</span>, <span class="number">3</span>, <span class="number">5</span>];
-<a href=#361 id=361 data-nosnippet>361</a>        <span class="macro">assert_eq!</span>(dv.insert_positions(<span class="kw-2">&amp;</span>positions).unwrap(), <span class="number">3</span>);
-<a href=#362 id=362 data-nosnippet>362</a>
-<a href=#363 id=363 data-nosnippet>363</a>        <span class="kw">let </span>res = dv.insert_positions(<span class="kw-2">&amp;</span>[<span class="number">2</span>, <span class="number">4</span>]);
-<a href=#364 id=364 data-nosnippet>364</a>        <span class="macro">assert!</span>(res.is_err());
-<a href=#365 id=365 data-nosnippet>365</a>    }
-<a href=#366 id=366 data-nosnippet>366</a>
-<a href=#367 id=367 data-nosnippet>367</a>    <span class="doccomment">/// Testing scenario: bulk insertion fails because input positions have duplicates.
-<a href=#368 id=368 data-nosnippet>368</a>    </span><span class="attr">#[test]
-<a href=#369 id=369 data-nosnippet>369</a>    </span><span class="kw">fn </span>test_failed_insertion_duplicate_elements() {
-<a href=#370 id=370 data-nosnippet>370</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
-<a href=#371 id=371 data-nosnippet>371</a>        <span class="kw">let </span>positions = <span class="macro">vec!</span>[<span class="number">1</span>, <span class="number">3</span>, <span class="number">5</span>, <span class="number">5</span>];
-<a href=#372 id=372 data-nosnippet>372</a>        <span class="kw">let </span>res = dv.insert_positions(<span class="kw-2">&amp;</span>positions);
-<a href=#373 id=373 data-nosnippet>373</a>        <span class="macro">assert!</span>(res.is_err());
-<a href=#374 id=374 data-nosnippet>374</a>    }
-<a href=#375 id=375 data-nosnippet>375</a>
-<a href=#376 id=376 data-nosnippet>376</a>    <span class="kw">fn </span>encode_dv_blob(dv: <span class="kw-2">&amp;</span>DeleteVector) -&gt; Vec&lt;u8&gt; {
-<a href=#377 id=377 data-nosnippet>377</a>        <span class="kw">let </span><span class="kw-2">mut </span>vector = Vec::with_capacity(dv.inner.serialized_size());
-<a href=#378 id=378 data-nosnippet>378</a>        dv.inner.serialize_into(<span class="kw-2">&amp;mut </span>vector).unwrap();
-<a href=#379 id=379 data-nosnippet>379</a>        frame_dv_blob(<span class="kw-2">&amp;</span>vector)
-<a href=#380 id=380 data-nosnippet>380</a>    }
-<a href=#381 id=381 data-nosnippet>381</a>
-<a href=#382 id=382 data-nosnippet>382</a>    <span class="kw">fn </span>dv_of(positions: <span class="kw">impl </span>IntoIterator&lt;Item = u64&gt;) -&gt; DeleteVector {
-<a href=#383 id=383 data-nosnippet>383</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
-<a href=#384 id=384 data-nosnippet>384</a>        <span class="kw">for </span>pos <span class="kw">in </span>positions {
-<a href=#385 id=385 data-nosnippet>385</a>            dv.insert(pos);
+<a href=#356 id=356 data-nosnippet>356</a>        <span class="kw">if </span><span class="self">self</span>.bitmap_iter.is_some() &amp;&amp; <span class="self">self</span>.high_bits &gt; hi {
+<a href=#357 id=357 data-nosnippet>357</a>            <span class="kw">return</span>;
+<a href=#358 id=358 data-nosnippet>358</a>        }
+<a href=#359 id=359 data-nosnippet>359</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(iter) = <span class="self">self</span>.bitmap_iter.as_mut()
+<a href=#360 id=360 data-nosnippet>360</a>            &amp;&amp; <span class="self">self</span>.high_bits == hi
+<a href=#361 id=361 data-nosnippet>361</a>        {
+<a href=#362 id=362 data-nosnippet>362</a>            <span class="self">self</span>.buffered = <span class="kw">match </span><span class="self">self</span>.buffered.take() {
+<a href=#363 id=363 data-nosnippet>363</a>                <span class="prelude-val">Some</span>(Buffered::Pos(start)) <span class="kw">if </span>start &lt; lo =&gt; <span class="prelude-val">None</span>,
+<a href=#364 id=364 data-nosnippet>364</a>                <span class="prelude-val">Some</span>(Buffered::Range(<span class="kw">_</span>, end)) <span class="kw">if </span>end &lt; lo =&gt; <span class="prelude-val">None</span>,
+<a href=#365 id=365 data-nosnippet>365</a>                <span class="prelude-val">Some</span>(Buffered::Range(start, end)) <span class="kw">if </span>start &lt; lo =&gt; <span class="prelude-val">Some</span>(Buffered::Range(lo, end)),
+<a href=#366 id=366 data-nosnippet>366</a>                buffered =&gt; buffered,
+<a href=#367 id=367 data-nosnippet>367</a>            };
+<a href=#368 id=368 data-nosnippet>368</a>            iter.advance_to(lo);
+<a href=#369 id=369 data-nosnippet>369</a>            <span class="kw">return</span>;
+<a href=#370 id=370 data-nosnippet>370</a>        }
+<a href=#371 id=371 data-nosnippet>371</a>
+<a href=#372 id=372 data-nosnippet>372</a>        <span class="comment">// `pos` is in a later bitmap: drop everything buffered from the current one.
+<a href=#373 id=373 data-nosnippet>373</a>        </span><span class="self">self</span>.bitmap_iter = <span class="prelude-val">None</span>;
+<a href=#374 id=374 data-nosnippet>374</a>        <span class="self">self</span>.buffered = <span class="prelude-val">None</span>;
+<a href=#375 id=375 data-nosnippet>375</a>        <span class="kw">for </span>(high_bits, bitmap) <span class="kw">in </span><span class="self">self</span>.bitmaps.by_ref() {
+<a href=#376 id=376 data-nosnippet>376</a>            <span class="kw">if </span>high_bits &lt; hi {
+<a href=#377 id=377 data-nosnippet>377</a>                <span class="kw">continue</span>;
+<a href=#378 id=378 data-nosnippet>378</a>            }
+<a href=#379 id=379 data-nosnippet>379</a>            <span class="kw">let </span><span class="kw-2">mut </span>iter = bitmap.iter();
+<a href=#380 id=380 data-nosnippet>380</a>            <span class="kw">if </span>high_bits == hi {
+<a href=#381 id=381 data-nosnippet>381</a>                iter.advance_to(lo);
+<a href=#382 id=382 data-nosnippet>382</a>            }
+<a href=#383 id=383 data-nosnippet>383</a>            <span class="self">self</span>.high_bits = high_bits;
+<a href=#384 id=384 data-nosnippet>384</a>            <span class="self">self</span>.bitmap_iter = <span class="prelude-val">Some</span>(iter);
+<a href=#385 id=385 data-nosnippet>385</a>            <span class="kw">return</span>;
 <a href=#386 id=386 data-nosnippet>386</a>        }
-<a href=#387 id=387 data-nosnippet>387</a>        dv
-<a href=#388 id=388 data-nosnippet>388</a>    }
+<a href=#387 id=387 data-nosnippet>387</a>    }
+<a href=#388 id=388 data-nosnippet>388</a>}
 <a href=#389 id=389 data-nosnippet>389</a>
-<a href=#390 id=390 data-nosnippet>390</a>    <span class="kw">fn </span>sorted(dv: <span class="kw-2">&amp;</span>DeleteVector) -&gt; Vec&lt;u64&gt; {
-<a href=#391 id=391 data-nosnippet>391</a>        <span class="kw">let </span><span class="kw-2">mut </span>positions: Vec&lt;u64&gt; = dv.iter().collect();
-<a href=#392 id=392 data-nosnippet>392</a>        positions.sort_unstable();
-<a href=#393 id=393 data-nosnippet>393</a>        positions
-<a href=#394 id=394 data-nosnippet>394</a>    }
-<a href=#395 id=395 data-nosnippet>395</a>
-<a href=#396 id=396 data-nosnippet>396</a>    <span class="attr">#[test]
-<a href=#397 id=397 data-nosnippet>397</a>    </span><span class="kw">fn </span>test_deserialize_roundtrip_empty() {
-<a href=#398 id=398 data-nosnippet>398</a>        <span class="kw">let </span>blob = encode_dv_blob(<span class="kw-2">&amp;</span>DeleteVector::default());
-<a href=#399 id=399 data-nosnippet>399</a>        <span class="macro">assert_eq!</span>(DeleteVector::deserialize(<span class="kw-2">&amp;</span>blob).unwrap().len(), <span class="number">0</span>);
-<a href=#400 id=400 data-nosnippet>400</a>    }
-<a href=#401 id=401 data-nosnippet>401</a>
-<a href=#402 id=402 data-nosnippet>402</a>    <span class="attr">#[test]
-<a href=#403 id=403 data-nosnippet>403</a>    </span><span class="kw">fn </span>test_deserialize_roundtrip_small() {
-<a href=#404 id=404 data-nosnippet>404</a>        <span class="kw">let </span>positions = [<span class="number">0u64</span>, <span class="number">5</span>, <span class="number">100</span>, <span class="number">1000</span>];
-<a href=#405 id=405 data-nosnippet>405</a>        <span class="kw">let </span>dv = DeleteVector::deserialize(<span class="kw-2">&amp;</span>encode_dv_blob(<span class="kw-2">&amp;</span>dv_of(positions))).unwrap();
-<a href=#406 id=406 data-nosnippet>406</a>        <span class="macro">assert_eq!</span>(sorted(<span class="kw-2">&amp;</span>dv), positions);
-<a href=#407 id=407 data-nosnippet>407</a>    }
-<a href=#408 id=408 data-nosnippet>408</a>
-<a href=#409 id=409 data-nosnippet>409</a>    <span class="attr">#[test]
-<a href=#410 id=410 data-nosnippet>410</a>    </span><span class="kw">fn </span>test_deserialize_roundtrip_spanning_64bit_keys() {
-<a href=#411 id=411 data-nosnippet>411</a>        <span class="kw">let </span>positions = [<span class="number">1u64</span>, <span class="number">1 </span>&lt;&lt; <span class="number">33</span>, (<span class="number">1 </span>&lt;&lt; <span class="number">33</span>) + <span class="number">5</span>, <span class="number">1 </span>&lt;&lt; <span class="number">34</span>];
-<a href=#412 id=412 data-nosnippet>412</a>        <span class="kw">let </span>dv = DeleteVector::deserialize(<span class="kw-2">&amp;</span>encode_dv_blob(<span class="kw-2">&amp;</span>dv_of(positions))).unwrap();
-<a href=#413 id=413 data-nosnippet>413</a>        <span class="macro">assert_eq!</span>(sorted(<span class="kw-2">&amp;</span>dv), positions);
-<a href=#414 id=414 data-nosnippet>414</a>    }
-<a href=#415 id=415 data-nosnippet>415</a>
-<a href=#416 id=416 data-nosnippet>416</a>    <span class="comment">// Java run-optimizes every deletion vector before writing, so real blobs carry RUN
-<a href=#417 id=417 data-nosnippet>417</a>    // containers, which use the SERIAL_COOKIE roaring layout. Force that layout so decode
-<a href=#418 id=418 data-nosnippet>418</a>    // exercises the run-container path rather than only array and bitmap containers.
-<a href=#419 id=419 data-nosnippet>419</a>    </span><span class="attr">#[test]
-<a href=#420 id=420 data-nosnippet>420</a>    </span><span class="kw">fn </span>test_deserialize_roundtrip_run_optimized() {
-<a href=#421 id=421 data-nosnippet>421</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = dv_of(<span class="number">0</span>..<span class="number">10_000</span>);
-<a href=#422 id=422 data-nosnippet>422</a>        <span class="macro">assert!</span>(
-<a href=#423 id=423 data-nosnippet>423</a>            dv.inner.optimize(),
-<a href=#424 id=424 data-nosnippet>424</a>            <span class="string">"expected a dense range to run-length encode"
-<a href=#425 id=425 data-nosnippet>425</a>        </span>);
-<a href=#426 id=426 data-nosnippet>426</a>        <span class="kw">let </span>decoded = DeleteVector::deserialize(<span class="kw-2">&amp;</span>encode_dv_blob(<span class="kw-2">&amp;</span>dv)).unwrap();
-<a href=#427 id=427 data-nosnippet>427</a>        <span class="macro">assert_eq!</span>(decoded.len(), <span class="number">10_000</span>);
-<a href=#428 id=428 data-nosnippet>428</a>        <span class="kw">let </span>positions = sorted(<span class="kw-2">&amp;</span>decoded);
-<a href=#429 id=429 data-nosnippet>429</a>        <span class="macro">assert_eq!</span>(positions.first(), <span class="prelude-val">Some</span>(<span class="kw-2">&amp;</span><span class="number">0</span>));
-<a href=#430 id=430 data-nosnippet>430</a>        <span class="macro">assert_eq!</span>(positions.last(), <span class="prelude-val">Some</span>(<span class="kw-2">&amp;</span><span class="number">9_999</span>));
-<a href=#431 id=431 data-nosnippet>431</a>    }
-<a href=#432 id=432 data-nosnippet>432</a>
-<a href=#433 id=433 data-nosnippet>433</a>    <span class="attr">#[test]
-<a href=#434 id=434 data-nosnippet>434</a>    </span><span class="kw">fn </span>test_deserialize_rejects_short_blob() {
-<a href=#435 id=435 data-nosnippet>435</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>[<span class="number">0u8</span>; DV_MIN_BLOB_BYTES - <span class="number">1</span>]).unwrap_err();
-<a href=#436 id=436 data-nosnippet>436</a>        <span class="macro">assert_eq!</span>(err.kind(), ErrorKind::DataInvalid);
-<a href=#437 id=437 data-nosnippet>437</a>    }
-<a href=#438 id=438 data-nosnippet>438</a>
-<a href=#439 id=439 data-nosnippet>439</a>    <span class="attr">#[test]
-<a href=#440 id=440 data-nosnippet>440</a>    </span><span class="kw">fn </span>test_deserialize_rejects_bad_magic() {
-<a href=#441 id=441 data-nosnippet>441</a>        <span class="kw">let </span><span class="kw-2">mut </span>blob = encode_dv_blob(<span class="kw-2">&amp;</span>dv_of([<span class="number">1</span>]));
-<a href=#442 id=442 data-nosnippet>442</a>        blob[DV_LENGTH_PREFIX_BYTES] ^= <span class="number">0xFF</span>;
-<a href=#443 id=443 data-nosnippet>443</a>        <span class="comment">// Recompute the CRC so the magic check, not the CRC check, is what fails.
-<a href=#444 id=444 data-nosnippet>444</a>        </span><span class="kw">let </span>end = blob.len() - DV_CRC_BYTES;
-<a href=#445 id=445 data-nosnippet>445</a>        <span class="kw">let </span>crc = crc32fast::hash(<span class="kw-2">&amp;</span>blob[DV_LENGTH_PREFIX_BYTES..end]);
-<a href=#446 id=446 data-nosnippet>446</a>        blob[end..].copy_from_slice(<span class="kw-2">&amp;</span>crc.to_be_bytes());
-<a href=#447 id=447 data-nosnippet>447</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>blob).unwrap_err();
-<a href=#448 id=448 data-nosnippet>448</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"magic mismatch"</span>), <span class="string">"got: {err}"</span>);
-<a href=#449 id=449 data-nosnippet>449</a>    }
-<a href=#450 id=450 data-nosnippet>450</a>
-<a href=#451 id=451 data-nosnippet>451</a>    <span class="attr">#[test]
-<a href=#452 id=452 data-nosnippet>452</a>    </span><span class="kw">fn </span>test_deserialize_rejects_bad_crc() {
-<a href=#453 id=453 data-nosnippet>453</a>        <span class="kw">let </span><span class="kw-2">mut </span>blob = encode_dv_blob(<span class="kw-2">&amp;</span>dv_of([<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>]));
-<a href=#454 id=454 data-nosnippet>454</a>        <span class="kw">let </span>end = blob.len() - DV_CRC_BYTES;
-<a href=#455 id=455 data-nosnippet>455</a>        blob[end] ^= <span class="number">0xFF</span>;
-<a href=#456 id=456 data-nosnippet>456</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>blob).unwrap_err();
-<a href=#457 id=457 data-nosnippet>457</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"CRC mismatch"</span>), <span class="string">"got: {err}"</span>);
-<a href=#458 id=458 data-nosnippet>458</a>    }
-<a href=#459 id=459 data-nosnippet>459</a>
-<a href=#460 id=460 data-nosnippet>460</a>    <span class="attr">#[test]
-<a href=#461 id=461 data-nosnippet>461</a>    </span><span class="kw">fn </span>test_deserialize_rejects_length_prefix_mismatch() {
-<a href=#462 id=462 data-nosnippet>462</a>        <span class="kw">let </span><span class="kw-2">mut </span>blob = encode_dv_blob(<span class="kw-2">&amp;</span>dv_of([<span class="number">1</span>]));
-<a href=#463 id=463 data-nosnippet>463</a>        <span class="kw">let </span>declared = u32::from_be_bytes(blob[..DV_LENGTH_PREFIX_BYTES].try_into().unwrap());
-<a href=#464 id=464 data-nosnippet>464</a>        blob[..DV_LENGTH_PREFIX_BYTES].copy_from_slice(<span class="kw-2">&amp;</span>(declared + <span class="number">1</span>).to_be_bytes());
-<a href=#465 id=465 data-nosnippet>465</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>blob).unwrap_err();
-<a href=#466 id=466 data-nosnippet>466</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"length prefix"</span>), <span class="string">"got: {err}"</span>);
-<a href=#467 id=467 data-nosnippet>467</a>    }
-<a href=#468 id=468 data-nosnippet>468</a>
-<a href=#469 id=469 data-nosnippet>469</a>    <span class="comment">// Crafts a raw roaring treemap directory (bitmap count header + key/bitmap entries) so tests
-<a href=#470 id=470 data-nosnippet>470</a>    // can exercise key-ordering violations that `dv_of`/`encode_dv_blob` can never produce, since
-<a href=#471 id=471 data-nosnippet>471</a>    // `DeleteVector::insert` always keeps keys unique and ascending.
-<a href=#472 id=472 data-nosnippet>472</a>    </span><span class="kw">fn </span>raw_roaring_vector(entries: <span class="kw-2">&amp;</span>[(u32, <span class="kw-2">&amp;</span>[u32])]) -&gt; Vec&lt;u8&gt; {
-<a href=#473 id=473 data-nosnippet>473</a>        <span class="kw">let </span><span class="kw-2">mut </span>vector = Vec::new();
-<a href=#474 id=474 data-nosnippet>474</a>        vector.extend_from_slice(<span class="kw-2">&amp;</span>(entries.len() <span class="kw">as </span>u64).to_le_bytes());
-<a href=#475 id=475 data-nosnippet>475</a>        <span class="kw">for </span>(key, positions) <span class="kw">in </span>entries {
-<a href=#476 id=476 data-nosnippet>476</a>            <span class="kw">let </span><span class="kw-2">mut </span>bitmap = RoaringBitmap::new();
-<a href=#477 id=477 data-nosnippet>477</a>            <span class="kw">for </span><span class="kw-2">&amp;</span>pos <span class="kw">in </span><span class="kw-2">*</span>positions {
-<a href=#478 id=478 data-nosnippet>478</a>                bitmap.insert(pos);
-<a href=#479 id=479 data-nosnippet>479</a>            }
-<a href=#480 id=480 data-nosnippet>480</a>            vector.extend_from_slice(<span class="kw-2">&amp;</span>key.to_le_bytes());
-<a href=#481 id=481 data-nosnippet>481</a>            bitmap.serialize_into(<span class="kw-2">&amp;mut </span>vector).unwrap();
-<a href=#482 id=482 data-nosnippet>482</a>        }
-<a href=#483 id=483 data-nosnippet>483</a>        vector
-<a href=#484 id=484 data-nosnippet>484</a>    }
-<a href=#485 id=485 data-nosnippet>485</a>
-<a href=#486 id=486 data-nosnippet>486</a>    <span class="comment">// `RoaringTreemap::deserialize_from` stores keys in a `BTreeMap` via a plain insert, so
-<a href=#487 id=487 data-nosnippet>487</a>    // without our own ordering check, decoding this would silently keep only the second bitmap
-<a href=#488 id=488 data-nosnippet>488</a>    // for key 5 (position 2) and drop the first (position 1).
-<a href=#489 id=489 data-nosnippet>489</a>    </span><span class="attr">#[test]
-<a href=#490 id=490 data-nosnippet>490</a>    </span><span class="kw">fn </span>test_deserialize_rejects_duplicate_keys() {
-<a href=#491 id=491 data-nosnippet>491</a>        <span class="kw">let </span>vector = raw_roaring_vector(<span class="kw-2">&amp;</span>[(<span class="number">5</span>, <span class="kw-2">&amp;</span>[<span class="number">1</span>]), (<span class="number">5</span>, <span class="kw-2">&amp;</span>[<span class="number">2</span>])]);
-<a href=#492 id=492 data-nosnippet>492</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>frame_dv_blob(<span class="kw-2">&amp;</span>vector)).unwrap_err();
-<a href=#493 id=493 data-nosnippet>493</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"unsigned comparison"</span>), <span class="string">"got: {err}"</span>);
-<a href=#494 id=494 data-nosnippet>494</a>    }
-<a href=#495 id=495 data-nosnippet>495</a>
-<a href=#496 id=496 data-nosnippet>496</a>    <span class="comment">// The Puffin spec requires the roaring directory's keys to be "ordered by unsigned
-<a href=#497 id=497 data-nosnippet>497</a>    // comparison"; a stream with unique but out-of-order keys is not a conformant blob even
-<a href=#498 id=498 data-nosnippet>498</a>    // though `BTreeMap` would happily reorder it into a correct-looking result.
-<a href=#499 id=499 data-nosnippet>499</a>    </span><span class="attr">#[test]
-<a href=#500 id=500 data-nosnippet>500</a>    </span><span class="kw">fn </span>test_deserialize_rejects_out_of_order_keys() {
-<a href=#501 id=501 data-nosnippet>501</a>        <span class="kw">let </span>vector = raw_roaring_vector(<span class="kw-2">&amp;</span>[(<span class="number">5</span>, <span class="kw-2">&amp;</span>[<span class="number">1</span>]), (<span class="number">3</span>, <span class="kw-2">&amp;</span>[<span class="number">2</span>])]);
-<a href=#502 id=502 data-nosnippet>502</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>frame_dv_blob(<span class="kw-2">&amp;</span>vector)).unwrap_err();
-<a href=#503 id=503 data-nosnippet>503</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"unsigned comparison"</span>), <span class="string">"got: {err}"</span>);
-<a href=#504 id=504 data-nosnippet>504</a>    }
+<a href=#390 id=390 data-nosnippet>390</a><span class="kw">impl </span>Iterator <span class="kw">for </span>DeleteVectorRanges&lt;<span class="lifetime">'_</span>&gt; {
+<a href=#391 id=391 data-nosnippet>391</a>    <span class="kw">type </span>Item = (u64, u64);
+<a href=#392 id=392 data-nosnippet>392</a>
+<a href=#393 id=393 data-nosnippet>393</a>    <span class="kw">fn </span>next(<span class="kw-2">&amp;mut </span><span class="self">self</span>) -&gt; <span class="prelude-ty">Option</span>&lt;<span class="self">Self</span>::Item&gt; {
+<a href=#394 id=394 data-nosnippet>394</a>        <span class="kw">loop </span>{
+<a href=#395 id=395 data-nosnippet>395</a>            <span class="kw">let </span>pending = <span class="kw">match </span><span class="self">self</span>.buffered.take() {
+<a href=#396 id=396 data-nosnippet>396</a>                <span class="prelude-val">Some</span>(Buffered::Range(start, end)) =&gt; <span class="kw">return </span><span class="self">self</span>.full_range(start, end),
+<a href=#397 id=397 data-nosnippet>397</a>                <span class="prelude-val">Some</span>(Buffered::Pos(start)) =&gt; <span class="prelude-val">Some</span>(start),
+<a href=#398 id=398 data-nosnippet>398</a>                <span class="prelude-val">None </span>=&gt; <span class="prelude-val">None</span>,
+<a href=#399 id=399 data-nosnippet>399</a>            };
+<a href=#400 id=400 data-nosnippet>400</a>
+<a href=#401 id=401 data-nosnippet>401</a>            <span class="kw">if let </span><span class="prelude-val">Some</span>(iter) = <span class="self">self</span>.bitmap_iter.as_mut()
+<a href=#402 id=402 data-nosnippet>402</a>                &amp;&amp; <span class="kw">let </span><span class="prelude-val">Some</span>(start) = pending.or_else(|| iter.next())
+<a href=#403 id=403 data-nosnippet>403</a>            {
+<a href=#404 id=404 data-nosnippet>404</a>                <span class="comment">// Walk short runs with `next`, and ask for the rest of a long run at once.
+<a href=#405 id=405 data-nosnippet>405</a>                </span><span class="kw">let </span><span class="kw-2">mut </span>end = start;
+<a href=#406 id=406 data-nosnippet>406</a>                <span class="kw">let </span><span class="kw-2">mut </span>walked = <span class="number">0</span>;
+<a href=#407 id=407 data-nosnippet>407</a>                <span class="kw">loop </span>{
+<a href=#408 id=408 data-nosnippet>408</a>                    <span class="kw">if </span>walked == RUN_PROBE_LEN {
+<a href=#409 id=409 data-nosnippet>409</a>                        <span class="kw">match </span>iter.next_range() {
+<a href=#410 id=410 data-nosnippet>410</a>                            <span class="prelude-val">Some</span>(run) <span class="kw">if </span><span class="prelude-val">Some</span>(<span class="kw-2">*</span>run.start()) == end.checked_add(<span class="number">1</span>) =&gt; {
+<a href=#411 id=411 data-nosnippet>411</a>                                end = <span class="kw-2">*</span>run.end();
+<a href=#412 id=412 data-nosnippet>412</a>                            }
+<a href=#413 id=413 data-nosnippet>413</a>                            <span class="prelude-val">Some</span>(run) =&gt; {
+<a href=#414 id=414 data-nosnippet>414</a>                                <span class="self">self</span>.buffered = <span class="prelude-val">Some</span>(Buffered::Range(<span class="kw-2">*</span>run.start(), <span class="kw-2">*</span>run.end()))
+<a href=#415 id=415 data-nosnippet>415</a>                            }
+<a href=#416 id=416 data-nosnippet>416</a>                            <span class="prelude-val">None </span>=&gt; {}
+<a href=#417 id=417 data-nosnippet>417</a>                        }
+<a href=#418 id=418 data-nosnippet>418</a>                        <span class="kw">break</span>;
+<a href=#419 id=419 data-nosnippet>419</a>                    }
+<a href=#420 id=420 data-nosnippet>420</a>                    <span class="kw">match </span>iter.next() {
+<a href=#421 id=421 data-nosnippet>421</a>                        <span class="prelude-val">Some</span>(next) <span class="kw">if </span><span class="prelude-val">Some</span>(next) == end.checked_add(<span class="number">1</span>) =&gt; {
+<a href=#422 id=422 data-nosnippet>422</a>                            end = next;
+<a href=#423 id=423 data-nosnippet>423</a>                            walked += <span class="number">1</span>;
+<a href=#424 id=424 data-nosnippet>424</a>                        }
+<a href=#425 id=425 data-nosnippet>425</a>                        next =&gt; {
+<a href=#426 id=426 data-nosnippet>426</a>                            <span class="self">self</span>.buffered = next.map(Buffered::Pos);
+<a href=#427 id=427 data-nosnippet>427</a>                            <span class="kw">break</span>;
+<a href=#428 id=428 data-nosnippet>428</a>                        }
+<a href=#429 id=429 data-nosnippet>429</a>                    }
+<a href=#430 id=430 data-nosnippet>430</a>                }
+<a href=#431 id=431 data-nosnippet>431</a>                <span class="kw">return </span><span class="self">self</span>.full_range(start, end);
+<a href=#432 id=432 data-nosnippet>432</a>            }
+<a href=#433 id=433 data-nosnippet>433</a>
+<a href=#434 id=434 data-nosnippet>434</a>            <span class="kw">let </span>(high_bits, bitmap) = <span class="self">self</span>.bitmaps.next()<span class="question-mark">?</span>;
+<a href=#435 id=435 data-nosnippet>435</a>            <span class="self">self</span>.high_bits = high_bits;
+<a href=#436 id=436 data-nosnippet>436</a>            <span class="self">self</span>.bitmap_iter = <span class="prelude-val">Some</span>(bitmap.iter());
+<a href=#437 id=437 data-nosnippet>437</a>        }
+<a href=#438 id=438 data-nosnippet>438</a>    }
+<a href=#439 id=439 data-nosnippet>439</a>}
+<a href=#440 id=440 data-nosnippet>440</a>
+<a href=#441 id=441 data-nosnippet>441</a><span class="kw">impl </span>BitOrAssign <span class="kw">for </span>DeleteVector {
+<a href=#442 id=442 data-nosnippet>442</a>    <span class="kw">fn </span>bitor_assign(<span class="kw-2">&amp;mut </span><span class="self">self</span>, other: <span class="self">Self</span>) {
+<a href=#443 id=443 data-nosnippet>443</a>        <span class="self">self</span>.inner.bitor_assign(<span class="kw-2">&amp;</span>other.inner);
+<a href=#444 id=444 data-nosnippet>444</a>    }
+<a href=#445 id=445 data-nosnippet>445</a>}
+<a href=#446 id=446 data-nosnippet>446</a>
+<a href=#447 id=447 data-nosnippet>447</a><span class="comment">// Reproduces Iceberg-Java's `deletion-vector-v1` framing so tests can round-trip through
+<a href=#448 id=448 data-nosnippet>448</a>// `deserialize` without a Java writer, and so other test modules can build blob fixtures.
+<a href=#449 id=449 data-nosnippet>449</a>// Cross-implementation golden fixtures produced by Iceberg-Java are tracked separately; this
+<a href=#450 id=450 data-nosnippet>450</a>// only checks that our decode matches our encode.
+<a href=#451 id=451 data-nosnippet>451</a></span><span class="attr">#[cfg(test)]
+<a href=#452 id=452 data-nosnippet>452</a></span><span class="kw">pub</span>(<span class="kw">crate</span>) <span class="kw">fn </span>frame_dv_blob(vector: <span class="kw-2">&amp;</span>[u8]) -&gt; Vec&lt;u8&gt; {
+<a href=#453 id=453 data-nosnippet>453</a>    <span class="kw">let </span>body_len = DV_MAGIC_BYTES + vector.len();
+<a href=#454 id=454 data-nosnippet>454</a>    <span class="kw">let </span><span class="kw-2">mut </span>blob = Vec::with_capacity(DV_LENGTH_PREFIX_BYTES + body_len + DV_CRC_BYTES);
+<a href=#455 id=455 data-nosnippet>455</a>    blob.extend_from_slice(<span class="kw-2">&amp;</span>(body_len <span class="kw">as </span>u32).to_be_bytes());
+<a href=#456 id=456 data-nosnippet>456</a>    blob.extend_from_slice(<span class="kw-2">&amp;</span>DV_MAGIC);
+<a href=#457 id=457 data-nosnippet>457</a>    blob.extend_from_slice(vector);
+<a href=#458 id=458 data-nosnippet>458</a>    <span class="kw">let </span>crc = crc32fast::hash(<span class="kw-2">&amp;</span>blob[DV_LENGTH_PREFIX_BYTES..]);
+<a href=#459 id=459 data-nosnippet>459</a>    blob.extend_from_slice(<span class="kw-2">&amp;</span>crc.to_be_bytes());
+<a href=#460 id=460 data-nosnippet>460</a>    blob
+<a href=#461 id=461 data-nosnippet>461</a>}
+<a href=#462 id=462 data-nosnippet>462</a>
+<a href=#463 id=463 data-nosnippet>463</a><span class="doccomment">/// Deterministic pseudo-random sets mixing single positions, short and long runs, runs across
+<a href=#464 id=464 data-nosnippet>464</a>/// 2^16 and 2^32 boundaries, and empty bitmaps between used ones.
+<a href=#465 id=465 data-nosnippet>465</a></span><span class="attr">#[cfg(test)]
+<a href=#466 id=466 data-nosnippet>466</a></span><span class="kw">pub</span>(<span class="kw">crate</span>) <span class="kw">fn </span>mixed_delete_vectors() -&gt; Vec&lt;DeleteVector&gt; {
+<a href=#467 id=467 data-nosnippet>467</a>    <span class="kw">use </span>rand::rngs::StdRng;
+<a href=#468 id=468 data-nosnippet>468</a>    <span class="kw">use </span>rand::{Rng, SeedableRng};
+<a href=#469 id=469 data-nosnippet>469</a>
+<a href=#470 id=470 data-nosnippet>470</a>    <span class="kw">let </span><span class="kw-2">mut </span>rng = StdRng::seed_from_u64(<span class="number">7</span>);
+<a href=#471 id=471 data-nosnippet>471</a>    <span class="kw">let </span><span class="kw-2">mut </span>vectors = <span class="macro">vec!</span>[DeleteVector::default()];
+<a href=#472 id=472 data-nosnippet>472</a>    <span class="kw">for </span>case <span class="kw">in </span><span class="number">0</span>..<span class="number">40u64 </span>{
+<a href=#473 id=473 data-nosnippet>473</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
+<a href=#474 id=474 data-nosnippet>474</a>        <span class="kw">let </span>base = <span class="kw">if </span>case % <span class="number">4 </span>== <span class="number">0 </span>{ (<span class="number">1u64 </span>&lt;&lt; <span class="number">32</span>) - <span class="number">40 </span>} <span class="kw">else </span>{ <span class="number">0 </span>};
+<a href=#475 id=475 data-nosnippet>475</a>        <span class="kw">let </span><span class="kw-2">mut </span>pos = base + rng.random_range(<span class="number">0</span>..<span class="number">50</span>);
+<a href=#476 id=476 data-nosnippet>476</a>        <span class="kw">for _ in </span><span class="number">0</span>..rng.random_range(<span class="number">0</span>..<span class="number">60</span>) {
+<a href=#477 id=477 data-nosnippet>477</a>            <span class="kw">let </span>run = <span class="kw">match </span>rng.random_range(<span class="number">0</span>..<span class="number">5</span>) {
+<a href=#478 id=478 data-nosnippet>478</a>                <span class="number">0 </span>=&gt; <span class="number">1</span>,
+<a href=#479 id=479 data-nosnippet>479</a>                <span class="number">1 </span>=&gt; <span class="number">2</span>,
+<a href=#480 id=480 data-nosnippet>480</a>                <span class="comment">// one past the probe length: the rest comes from `next_range`
+<a href=#481 id=481 data-nosnippet>481</a>                </span><span class="number">2 </span>=&gt; u64::from(RUN_PROBE_LEN) + <span class="number">1</span>,
+<a href=#482 id=482 data-nosnippet>482</a>                <span class="number">3 </span>=&gt; rng.random_range(<span class="number">1</span>..=<span class="number">40</span>),
+<a href=#483 id=483 data-nosnippet>483</a>                <span class="kw">_ </span>=&gt; rng.random_range(<span class="number">1</span>..=<span class="number">70_000</span>),
+<a href=#484 id=484 data-nosnippet>484</a>            };
+<a href=#485 id=485 data-nosnippet>485</a>            dv.inner.insert_range(pos..pos + run);
+<a href=#486 id=486 data-nosnippet>486</a>            pos += run + rng.random_range(<span class="number">1</span>..=<span class="number">30</span>);
+<a href=#487 id=487 data-nosnippet>487</a>        }
+<a href=#488 id=488 data-nosnippet>488</a>        vectors.push(dv);
+<a href=#489 id=489 data-nosnippet>489</a>    }
+<a href=#490 id=490 data-nosnippet>490</a>    <span class="kw">let </span><span class="kw-2">mut </span>boundaries = DeleteVector::default();
+<a href=#491 id=491 data-nosnippet>491</a>    boundaries.inner.insert_range((<span class="number">1 </span>&lt;&lt; <span class="number">16</span>) - <span class="number">3</span>..(<span class="number">1 </span>&lt;&lt; <span class="number">16</span>) + <span class="number">3</span>);
+<a href=#492 id=492 data-nosnippet>492</a>    boundaries
+<a href=#493 id=493 data-nosnippet>493</a>        .inner
+<a href=#494 id=494 data-nosnippet>494</a>        .insert_range((<span class="number">1 </span>&lt;&lt; <span class="number">32</span>) - <span class="number">20</span>..(<span class="number">1 </span>&lt;&lt; <span class="number">32</span>) + <span class="number">20</span>);
+<a href=#495 id=495 data-nosnippet>495</a>    boundaries.inner.insert((<span class="number">2 </span>&lt;&lt; <span class="number">32</span>) + <span class="number">7</span>);
+<a href=#496 id=496 data-nosnippet>496</a>    vectors.push(boundaries);
+<a href=#497 id=497 data-nosnippet>497</a>    <span class="comment">// High bits 0, 3 and 7, so skipping walks past empty bitmaps.
+<a href=#498 id=498 data-nosnippet>498</a>    </span><span class="kw">let </span><span class="kw-2">mut </span>gaps = DeleteVector::default();
+<a href=#499 id=499 data-nosnippet>499</a>    gaps.inner.insert_range(<span class="number">2</span>..<span class="number">12</span>);
+<a href=#500 id=500 data-nosnippet>500</a>    gaps.inner.insert_range((<span class="number">3 </span>&lt;&lt; <span class="number">32</span>) + <span class="number">5</span>..(<span class="number">3 </span>&lt;&lt; <span class="number">32</span>) + <span class="number">70_000</span>);
+<a href=#501 id=501 data-nosnippet>501</a>    gaps.inner.insert((<span class="number">7 </span>&lt;&lt; <span class="number">32</span>) + <span class="number">1</span>);
+<a href=#502 id=502 data-nosnippet>502</a>    vectors.push(gaps);
+<a href=#503 id=503 data-nosnippet>503</a>    vectors
+<a href=#504 id=504 data-nosnippet>504</a>}
 <a href=#505 id=505 data-nosnippet>505</a>
-<a href=#506 id=506 data-nosnippet>506</a>    <span class="comment">// The roaring portable format stores the bitmap count as a u64 with the upper 32 bits
-<a href=#507 id=507 data-nosnippet>507</a>    // reserved as zero padding, restricting it to [0, 2^32 - 1]; a value above that is not a
-<a href=#508 id=508 data-nosnippet>508</a>    // conformant blob, regardless of whether any key/bitmap entries follow.
-<a href=#509 id=509 data-nosnippet>509</a>    </span><span class="attr">#[test]
-<a href=#510 id=510 data-nosnippet>510</a>    </span><span class="kw">fn </span>test_deserialize_rejects_bitmap_count_overflow() {
-<a href=#511 id=511 data-nosnippet>511</a>        <span class="kw">let </span>vector = (u32::MAX <span class="kw">as </span>u64 + <span class="number">1</span>).to_le_bytes().to_vec();
-<a href=#512 id=512 data-nosnippet>512</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>frame_dv_blob(<span class="kw-2">&amp;</span>vector)).unwrap_err();
-<a href=#513 id=513 data-nosnippet>513</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"exceeds the"</span>), <span class="string">"got: {err}"</span>);
-<a href=#514 id=514 data-nosnippet>514</a>    }
-<a href=#515 id=515 data-nosnippet>515</a>}
+<a href=#506 id=506 data-nosnippet>506</a><span class="attr">#[cfg(test)]
+<a href=#507 id=507 data-nosnippet>507</a></span><span class="kw">mod </span>tests {
+<a href=#508 id=508 data-nosnippet>508</a>    <span class="kw">use </span>rand::rngs::StdRng;
+<a href=#509 id=509 data-nosnippet>509</a>    <span class="kw">use </span>rand::{Rng, SeedableRng};
+<a href=#510 id=510 data-nosnippet>510</a>
+<a href=#511 id=511 data-nosnippet>511</a>    <span class="kw">use </span>super::<span class="kw-2">*</span>;
+<a href=#512 id=512 data-nosnippet>512</a>
+<a href=#513 id=513 data-nosnippet>513</a>    <span class="attr">#[test]
+<a href=#514 id=514 data-nosnippet>514</a>    </span><span class="kw">fn </span>test_ranges_cover_exactly_the_deleted_positions() {
+<a href=#515 id=515 data-nosnippet>515</a>        <span class="kw">for </span>dv <span class="kw">in </span>mixed_delete_vectors() {
+<a href=#516 id=516 data-nosnippet>516</a>            <span class="kw">let </span>positions: Vec&lt;u64&gt; = dv.iter().collect();
+<a href=#517 id=517 data-nosnippet>517</a>            <span class="kw">let </span><span class="kw-2">mut </span>expanded = Vec::new();
+<a href=#518 id=518 data-nosnippet>518</a>            <span class="kw">let </span><span class="kw-2">mut </span>last_end = <span class="prelude-val">None</span>;
+<a href=#519 id=519 data-nosnippet>519</a>            <span class="kw">for </span>(start, end) <span class="kw">in </span>dv.ranges() {
+<a href=#520 id=520 data-nosnippet>520</a>                <span class="macro">assert!</span>(start &lt; end, <span class="string">"empty range {start}..{end}"</span>);
+<a href=#521 id=521 data-nosnippet>521</a>                <span class="macro">assert!</span>(
+<a href=#522 id=522 data-nosnippet>522</a>                    last_end.is_none_or(|last| last &lt;= start),
+<a href=#523 id=523 data-nosnippet>523</a>                    <span class="string">"ranges out of order"
+<a href=#524 id=524 data-nosnippet>524</a>                </span>);
+<a href=#525 id=525 data-nosnippet>525</a>                last_end = <span class="prelude-val">Some</span>(end);
+<a href=#526 id=526 data-nosnippet>526</a>                expanded.extend(start..end);
+<a href=#527 id=527 data-nosnippet>527</a>            }
+<a href=#528 id=528 data-nosnippet>528</a>            <span class="macro">assert_eq!</span>(expanded, positions);
+<a href=#529 id=529 data-nosnippet>529</a>        }
+<a href=#530 id=530 data-nosnippet>530</a>    }
+<a href=#531 id=531 data-nosnippet>531</a>
+<a href=#532 id=532 data-nosnippet>532</a>    <span class="attr">#[test]
+<a href=#533 id=533 data-nosnippet>533</a>    </span><span class="kw">fn </span>test_ranges_skip_to_matches_model() {
+<a href=#534 id=534 data-nosnippet>534</a>        <span class="kw">let </span><span class="kw-2">mut </span>rng = StdRng::seed_from_u64(<span class="number">7</span>);
+<a href=#535 id=535 data-nosnippet>535</a>
+<a href=#536 id=536 data-nosnippet>536</a>        <span class="kw">for </span>dv <span class="kw">in </span>mixed_delete_vectors() {
+<a href=#537 id=537 data-nosnippet>537</a>            <span class="kw">let </span>deleted: Vec&lt;u64&gt; = dv.iter().collect();
+<a href=#538 id=538 data-nosnippet>538</a>            <span class="kw">let </span>max = deleted.last().copied().unwrap_or(<span class="number">0</span>) + <span class="number">10</span>;
+<a href=#539 id=539 data-nosnippet>539</a>            <span class="comment">// `deleted[next..]` has been neither returned nor skipped, and every position below
+<a href=#540 id=540 data-nosnippet>540</a>            // `done` has.
+<a href=#541 id=541 data-nosnippet>541</a>            </span><span class="kw">let </span><span class="kw-2">mut </span>next = <span class="number">0</span>;
+<a href=#542 id=542 data-nosnippet>542</a>            <span class="kw">let </span><span class="kw-2">mut </span>done = <span class="number">0u64</span>;
+<a href=#543 id=543 data-nosnippet>543</a>            <span class="kw">let </span><span class="kw-2">mut </span>ranges = dv.ranges();
+<a href=#544 id=544 data-nosnippet>544</a>            <span class="kw">loop </span>{
+<a href=#545 id=545 data-nosnippet>545</a>                <span class="kw">match </span>rng.random_range(<span class="number">0</span>..<span class="number">6</span>) {
+<a href=#546 id=546 data-nosnippet>546</a>                    <span class="number">0 </span>| <span class="number">1 </span>=&gt; {
+<a href=#547 id=547 data-nosnippet>547</a>                        <span class="kw">let </span>target = done.max(deleted.get(next).copied().unwrap_or(max))
+<a href=#548 id=548 data-nosnippet>548</a>                            + rng.random_range(<span class="number">0</span>..<span class="number">200</span>);
+<a href=#549 id=549 data-nosnippet>549</a>                        ranges.skip_to(target);
+<a href=#550 id=550 data-nosnippet>550</a>                        next = deleted.partition_point(|<span class="kw-2">&amp;</span>p| p &lt; target);
+<a href=#551 id=551 data-nosnippet>551</a>                        done = target;
+<a href=#552 id=552 data-nosnippet>552</a>                        <span class="kw">continue</span>;
+<a href=#553 id=553 data-nosnippet>553</a>                    }
+<a href=#554 id=554 data-nosnippet>554</a>                    <span class="comment">// A target at or below `done`, near it or anywhere below: does nothing.
+<a href=#555 id=555 data-nosnippet>555</a>                    </span><span class="number">2 </span>=&gt; {
+<a href=#556 id=556 data-nosnippet>556</a>                        <span class="kw">let </span>back = <span class="kw">if </span>rng.random_bool(<span class="number">0.5</span>) {
+<a href=#557 id=557 data-nosnippet>557</a>                            done.saturating_sub(rng.random_range(<span class="number">0</span>..<span class="number">200</span>))
+<a href=#558 id=558 data-nosnippet>558</a>                        } <span class="kw">else </span>{
+<a href=#559 id=559 data-nosnippet>559</a>                            rng.random_range(<span class="number">0</span>..=done)
+<a href=#560 id=560 data-nosnippet>560</a>                        };
+<a href=#561 id=561 data-nosnippet>561</a>                        ranges.skip_to(back);
+<a href=#562 id=562 data-nosnippet>562</a>                        <span class="kw">continue</span>;
+<a href=#563 id=563 data-nosnippet>563</a>                    }
+<a href=#564 id=564 data-nosnippet>564</a>                    <span class="kw">_ </span>=&gt; {}
+<a href=#565 id=565 data-nosnippet>565</a>                }
+<a href=#566 id=566 data-nosnippet>566</a>                <span class="kw">match </span>ranges.next() {
+<a href=#567 id=567 data-nosnippet>567</a>                    <span class="prelude-val">Some</span>((start, end)) =&gt; {
+<a href=#568 id=568 data-nosnippet>568</a>                        <span class="macro">assert!</span>(start &lt; end, <span class="string">"empty range {start}..{end}"</span>);
+<a href=#569 id=569 data-nosnippet>569</a>                        <span class="kw">let </span>len = (end - start) <span class="kw">as </span>usize;
+<a href=#570 id=570 data-nosnippet>570</a>                        <span class="macro">assert_eq!</span>(
+<a href=#571 id=571 data-nosnippet>571</a>                            deleted
+<a href=#572 id=572 data-nosnippet>572</a>                                .get(next..next + len)
+<a href=#573 id=573 data-nosnippet>573</a>                                .map(|run| (run[<span class="number">0</span>], run[len - <span class="number">1</span>])),
+<a href=#574 id=574 data-nosnippet>574</a>                            <span class="prelude-val">Some</span>((start, end - <span class="number">1</span>)),
+<a href=#575 id=575 data-nosnippet>575</a>                            <span class="string">"returned {start}..{end}"
+<a href=#576 id=576 data-nosnippet>576</a>                        </span>);
+<a href=#577 id=577 data-nosnippet>577</a>                        next += len;
+<a href=#578 id=578 data-nosnippet>578</a>                        done = end;
+<a href=#579 id=579 data-nosnippet>579</a>                    }
+<a href=#580 id=580 data-nosnippet>580</a>                    <span class="prelude-val">None </span>=&gt; {
+<a href=#581 id=581 data-nosnippet>581</a>                        <span class="macro">assert_eq!</span>(next, deleted.len(), <span class="string">"missed {:?}"</span>, <span class="kw-2">&amp;</span>deleted[next..]);
+<a href=#582 id=582 data-nosnippet>582</a>                        <span class="kw">break</span>;
+<a href=#583 id=583 data-nosnippet>583</a>                    }
+<a href=#584 id=584 data-nosnippet>584</a>                }
+<a href=#585 id=585 data-nosnippet>585</a>            }
+<a href=#586 id=586 data-nosnippet>586</a>        }
+<a href=#587 id=587 data-nosnippet>587</a>    }
+<a href=#588 id=588 data-nosnippet>588</a>
+<a href=#589 id=589 data-nosnippet>589</a>    <span class="attr">#[test]
+<a href=#590 id=590 data-nosnippet>590</a>    </span><span class="kw">fn </span>test_advance_to_matches_model() {
+<a href=#591 id=591 data-nosnippet>591</a>        <span class="kw">let </span><span class="kw-2">mut </span>rng = StdRng::seed_from_u64(<span class="number">11</span>);
+<a href=#592 id=592 data-nosnippet>592</a>
+<a href=#593 id=593 data-nosnippet>593</a>        <span class="kw">for </span>dv <span class="kw">in </span>mixed_delete_vectors() {
+<a href=#594 id=594 data-nosnippet>594</a>            <span class="kw">let </span>deleted: Vec&lt;u64&gt; = dv.iter().collect();
+<a href=#595 id=595 data-nosnippet>595</a>            <span class="kw">let </span>max = deleted.last().copied().unwrap_or(<span class="number">0</span>) + <span class="number">10</span>;
+<a href=#596 id=596 data-nosnippet>596</a>            <span class="kw">let </span><span class="kw-2">mut </span>next = <span class="number">0</span>;
+<a href=#597 id=597 data-nosnippet>597</a>            <span class="kw">let </span><span class="kw-2">mut </span>done = <span class="number">0u64</span>;
+<a href=#598 id=598 data-nosnippet>598</a>            <span class="kw">let </span><span class="kw-2">mut </span>iter = dv.iter();
+<a href=#599 id=599 data-nosnippet>599</a>            <span class="kw">loop </span>{
+<a href=#600 id=600 data-nosnippet>600</a>                <span class="kw">match </span>rng.random_range(<span class="number">0</span>..<span class="number">6</span>) {
+<a href=#601 id=601 data-nosnippet>601</a>                    <span class="number">0 </span>=&gt; {
+<a href=#602 id=602 data-nosnippet>602</a>                        <span class="kw">let </span>target = done.max(deleted.get(next).copied().unwrap_or(max))
+<a href=#603 id=603 data-nosnippet>603</a>                            + rng.random_range(<span class="number">0</span>..<span class="number">200</span>);
+<a href=#604 id=604 data-nosnippet>604</a>                        iter.advance_to(target);
+<a href=#605 id=605 data-nosnippet>605</a>                        next = deleted.partition_point(|<span class="kw-2">&amp;</span>p| p &lt; target);
+<a href=#606 id=606 data-nosnippet>606</a>                        done = target;
+<a href=#607 id=607 data-nosnippet>607</a>                        <span class="kw">continue</span>;
+<a href=#608 id=608 data-nosnippet>608</a>                    }
+<a href=#609 id=609 data-nosnippet>609</a>                    <span class="number">1 </span>=&gt; {
+<a href=#610 id=610 data-nosnippet>610</a>                        <span class="kw">let </span>back = <span class="kw">if </span>rng.random_bool(<span class="number">0.5</span>) {
+<a href=#611 id=611 data-nosnippet>611</a>                            done.saturating_sub(rng.random_range(<span class="number">0</span>..<span class="number">200</span>))
+<a href=#612 id=612 data-nosnippet>612</a>                        } <span class="kw">else </span>{
+<a href=#613 id=613 data-nosnippet>613</a>                            rng.random_range(<span class="number">0</span>..=done)
+<a href=#614 id=614 data-nosnippet>614</a>                        };
+<a href=#615 id=615 data-nosnippet>615</a>                        iter.advance_to(back);
+<a href=#616 id=616 data-nosnippet>616</a>                        <span class="kw">continue</span>;
+<a href=#617 id=617 data-nosnippet>617</a>                    }
+<a href=#618 id=618 data-nosnippet>618</a>                    <span class="kw">_ </span>=&gt; {}
+<a href=#619 id=619 data-nosnippet>619</a>                }
+<a href=#620 id=620 data-nosnippet>620</a>                <span class="kw">let </span>pos = iter.next();
+<a href=#621 id=621 data-nosnippet>621</a>                <span class="macro">assert_eq!</span>(pos, deleted.get(next).copied());
+<a href=#622 id=622 data-nosnippet>622</a>                <span class="kw">match </span>pos {
+<a href=#623 id=623 data-nosnippet>623</a>                    <span class="prelude-val">Some</span>(pos) =&gt; {
+<a href=#624 id=624 data-nosnippet>624</a>                        next += <span class="number">1</span>;
+<a href=#625 id=625 data-nosnippet>625</a>                        done = pos + <span class="number">1</span>;
+<a href=#626 id=626 data-nosnippet>626</a>                    }
+<a href=#627 id=627 data-nosnippet>627</a>                    <span class="prelude-val">None </span>=&gt; <span class="kw">break</span>,
+<a href=#628 id=628 data-nosnippet>628</a>                }
+<a href=#629 id=629 data-nosnippet>629</a>            }
+<a href=#630 id=630 data-nosnippet>630</a>        }
+<a href=#631 id=631 data-nosnippet>631</a>    }
+<a href=#632 id=632 data-nosnippet>632</a>
+<a href=#633 id=633 data-nosnippet>633</a>    <span class="attr">#[test]
+<a href=#634 id=634 data-nosnippet>634</a>    </span><span class="kw">fn </span>test_ranges_skip_to_inside_buffered_run() {
+<a href=#635 id=635 data-nosnippet>635</a>        <span class="comment">// After a run of exactly RUN_PROBE_LEN + 1, the following run is already buffered.
+<a href=#636 id=636 data-nosnippet>636</a>        </span><span class="kw">let </span>probe = u64::from(RUN_PROBE_LEN);
+<a href=#637 id=637 data-nosnippet>637</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
+<a href=#638 id=638 data-nosnippet>638</a>        dv.inner.insert_range(<span class="number">0</span>..probe + <span class="number">1</span>);
+<a href=#639 id=639 data-nosnippet>639</a>        dv.inner.insert_range(probe + <span class="number">3</span>..probe + <span class="number">13</span>);
+<a href=#640 id=640 data-nosnippet>640</a>        <span class="kw">let </span><span class="kw-2">mut </span>ranges = dv.ranges();
+<a href=#641 id=641 data-nosnippet>641</a>        <span class="macro">assert_eq!</span>(ranges.next(), <span class="prelude-val">Some</span>((<span class="number">0</span>, probe + <span class="number">1</span>)));
+<a href=#642 id=642 data-nosnippet>642</a>        ranges.skip_to(probe + <span class="number">8</span>);
+<a href=#643 id=643 data-nosnippet>643</a>        <span class="macro">assert_eq!</span>(ranges.next(), <span class="prelude-val">Some</span>((probe + <span class="number">8</span>, probe + <span class="number">13</span>)));
+<a href=#644 id=644 data-nosnippet>644</a>        <span class="macro">assert_eq!</span>(ranges.next(), <span class="prelude-val">None</span>);
+<a href=#645 id=645 data-nosnippet>645</a>    }
+<a href=#646 id=646 data-nosnippet>646</a>
+<a href=#647 id=647 data-nosnippet>647</a>    <span class="attr">#[test]
+<a href=#648 id=648 data-nosnippet>648</a>    </span><span class="kw">fn </span>test_ranges_join_runs_across_2_16() {
+<a href=#649 id=649 data-nosnippet>649</a>        <span class="comment">// `roaring`'s `next_range` joins a run across containers, so these come back whole.
+<a href=#650 id=650 data-nosnippet>650</a>        // Starting near the boundary also covers the positions walked before `next_range`.
+<a href=#651 id=651 data-nosnippet>651</a>        </span><span class="kw">for </span>start <span class="kw">in </span>[(<span class="number">1 </span>&lt;&lt; <span class="number">16</span>) - <span class="number">100</span>, (<span class="number">1 </span>&lt;&lt; <span class="number">16</span>) - <span class="number">2</span>] {
+<a href=#652 id=652 data-nosnippet>652</a>            <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
+<a href=#653 id=653 data-nosnippet>653</a>            dv.inner.insert_range(start..(<span class="number">3 </span>&lt;&lt; <span class="number">16</span>) + <span class="number">5</span>);
+<a href=#654 id=654 data-nosnippet>654</a>            <span class="macro">assert_eq!</span>(dv.ranges().collect::&lt;Vec&lt;<span class="kw">_</span>&gt;&gt;(), <span class="macro">vec!</span>[(
+<a href=#655 id=655 data-nosnippet>655</a>                start,
+<a href=#656 id=656 data-nosnippet>656</a>                (<span class="number">3 </span>&lt;&lt; <span class="number">16</span>) + <span class="number">5
+<a href=#657 id=657 data-nosnippet>657</a>            </span>)]);
+<a href=#658 id=658 data-nosnippet>658</a>        }
+<a href=#659 id=659 data-nosnippet>659</a>    }
+<a href=#660 id=660 data-nosnippet>660</a>
+<a href=#661 id=661 data-nosnippet>661</a>    <span class="attr">#[test]
+<a href=#662 id=662 data-nosnippet>662</a>    </span><span class="kw">fn </span>test_ranges_end_at_u64_max() {
+<a href=#663 id=663 data-nosnippet>663</a>        <span class="kw">let </span>dv = DeleteVector::new(RoaringTreemap::from_iter([<span class="number">5</span>, u64::MAX - <span class="number">2</span>, u64::MAX - <span class="number">1</span>]));
+<a href=#664 id=664 data-nosnippet>664</a>        <span class="macro">assert_eq!</span>(dv.ranges().collect::&lt;Vec&lt;<span class="kw">_</span>&gt;&gt;(), <span class="macro">vec!</span>[
+<a href=#665 id=665 data-nosnippet>665</a>            (<span class="number">5</span>, <span class="number">6</span>),
+<a href=#666 id=666 data-nosnippet>666</a>            (u64::MAX - <span class="number">2</span>, u64::MAX)
+<a href=#667 id=667 data-nosnippet>667</a>        ]);
+<a href=#668 id=668 data-nosnippet>668</a>
+<a href=#669 id=669 data-nosnippet>669</a>        <span class="comment">// `u64::MAX` itself can't end a half-open range, and no row can be there.
+<a href=#670 id=670 data-nosnippet>670</a>        </span><span class="kw">let </span>dv = DeleteVector::new(RoaringTreemap::from_iter([<span class="number">5</span>, u64::MAX - <span class="number">1</span>, u64::MAX]));
+<a href=#671 id=671 data-nosnippet>671</a>        <span class="macro">assert_eq!</span>(dv.ranges().collect::&lt;Vec&lt;<span class="kw">_</span>&gt;&gt;(), <span class="macro">vec!</span>[
+<a href=#672 id=672 data-nosnippet>672</a>            (<span class="number">5</span>, <span class="number">6</span>),
+<a href=#673 id=673 data-nosnippet>673</a>            (u64::MAX - <span class="number">1</span>, u64::MAX)
+<a href=#674 id=674 data-nosnippet>674</a>        ]);
+<a href=#675 id=675 data-nosnippet>675</a>        <span class="kw">let </span>dv = DeleteVector::new(RoaringTreemap::from_iter([u64::MAX]));
+<a href=#676 id=676 data-nosnippet>676</a>        <span class="macro">assert_eq!</span>(dv.ranges().next(), <span class="prelude-val">None</span>);
+<a href=#677 id=677 data-nosnippet>677</a>    }
+<a href=#678 id=678 data-nosnippet>678</a>
+<a href=#679 id=679 data-nosnippet>679</a>    <span class="comment">// The cases from https://github.com/apache/iceberg-rust/issues/3400, for `skip_to` and for
+<a href=#680 id=680 data-nosnippet>680</a>    // `advance_to`.
+<a href=#681 id=681 data-nosnippet>681</a>    </span><span class="attr">#[test]
+<a href=#682 id=682 data-nosnippet>682</a>    </span><span class="kw">fn </span>test_ranges_skip_to_missing_later_bitmap() {
+<a href=#683 id=683 data-nosnippet>683</a>        <span class="kw">let </span>dv = DeleteVector::new(RoaringTreemap::from_iter([<span class="number">1u64</span>, <span class="number">5</span>, <span class="number">9</span>]));
+<a href=#684 id=684 data-nosnippet>684</a>        <span class="kw">let </span><span class="kw-2">mut </span>ranges = dv.ranges();
+<a href=#685 id=685 data-nosnippet>685</a>        <span class="macro">assert_eq!</span>(ranges.next(), <span class="prelude-val">Some</span>((<span class="number">1</span>, <span class="number">2</span>)));
+<a href=#686 id=686 data-nosnippet>686</a>        ranges.skip_to((<span class="number">1 </span>&lt;&lt; <span class="number">32</span>) + <span class="number">5</span>);
+<a href=#687 id=687 data-nosnippet>687</a>        <span class="macro">assert_eq!</span>(ranges.next(), <span class="prelude-val">None</span>);
+<a href=#688 id=688 data-nosnippet>688</a>    }
+<a href=#689 id=689 data-nosnippet>689</a>
+<a href=#690 id=690 data-nosnippet>690</a>    <span class="attr">#[test]
+<a href=#691 id=691 data-nosnippet>691</a>    </span><span class="kw">fn </span>test_ranges_skip_to_below_current_bitmap() {
+<a href=#692 id=692 data-nosnippet>692</a>        <span class="kw">let </span>high = <span class="number">1u64 </span>&lt;&lt; <span class="number">32</span>;
+<a href=#693 id=693 data-nosnippet>693</a>        <span class="kw">let </span>dv = DeleteVector::new(RoaringTreemap::from_iter([high + <span class="number">1</span>, high + <span class="number">2</span>, high + <span class="number">9</span>]));
+<a href=#694 id=694 data-nosnippet>694</a>        <span class="kw">let </span><span class="kw-2">mut </span>ranges = dv.ranges();
+<a href=#695 id=695 data-nosnippet>695</a>        <span class="macro">assert_eq!</span>(ranges.next(), <span class="prelude-val">Some</span>((high + <span class="number">1</span>, high + <span class="number">3</span>)));
+<a href=#696 id=696 data-nosnippet>696</a>        ranges.skip_to(<span class="number">10</span>);
+<a href=#697 id=697 data-nosnippet>697</a>        <span class="macro">assert_eq!</span>(ranges.next(), <span class="prelude-val">Some</span>((high + <span class="number">9</span>, high + <span class="number">10</span>)));
+<a href=#698 id=698 data-nosnippet>698</a>        <span class="macro">assert_eq!</span>(ranges.next(), <span class="prelude-val">None</span>);
+<a href=#699 id=699 data-nosnippet>699</a>    }
+<a href=#700 id=700 data-nosnippet>700</a>
+<a href=#701 id=701 data-nosnippet>701</a>    <span class="attr">#[test]
+<a href=#702 id=702 data-nosnippet>702</a>    </span><span class="kw">fn </span>test_advance_to_missing_later_bitmap() {
+<a href=#703 id=703 data-nosnippet>703</a>        <span class="kw">let </span>dv = DeleteVector::new(RoaringTreemap::from_iter([<span class="number">5u64</span>, <span class="number">7</span>]));
+<a href=#704 id=704 data-nosnippet>704</a>        <span class="kw">let </span><span class="kw-2">mut </span>iter = dv.iter();
+<a href=#705 id=705 data-nosnippet>705</a>        <span class="macro">assert_eq!</span>(iter.next(), <span class="prelude-val">Some</span>(<span class="number">5</span>));
+<a href=#706 id=706 data-nosnippet>706</a>        iter.advance_to((<span class="number">1 </span>&lt;&lt; <span class="number">32</span>) + <span class="number">10</span>);
+<a href=#707 id=707 data-nosnippet>707</a>        <span class="macro">assert_eq!</span>(iter.next(), <span class="prelude-val">None</span>);
+<a href=#708 id=708 data-nosnippet>708</a>    }
+<a href=#709 id=709 data-nosnippet>709</a>
+<a href=#710 id=710 data-nosnippet>710</a>    <span class="attr">#[test]
+<a href=#711 id=711 data-nosnippet>711</a>    </span><span class="kw">fn </span>test_advance_to_below_current_bitmap() {
+<a href=#712 id=712 data-nosnippet>712</a>        <span class="kw">let </span>high = <span class="number">1u64 </span>&lt;&lt; <span class="number">32</span>;
+<a href=#713 id=713 data-nosnippet>713</a>        <span class="kw">let </span>dv = DeleteVector::new(RoaringTreemap::from_iter([high + <span class="number">10</span>, high + <span class="number">20</span>]));
+<a href=#714 id=714 data-nosnippet>714</a>        <span class="kw">let </span><span class="kw-2">mut </span>iter = dv.iter();
+<a href=#715 id=715 data-nosnippet>715</a>        <span class="macro">assert_eq!</span>(iter.next(), <span class="prelude-val">Some</span>(high + <span class="number">10</span>));
+<a href=#716 id=716 data-nosnippet>716</a>        iter.advance_to(<span class="number">50</span>);
+<a href=#717 id=717 data-nosnippet>717</a>        <span class="macro">assert_eq!</span>(iter.collect::&lt;Vec&lt;<span class="kw">_</span>&gt;&gt;(), <span class="macro">vec!</span>[high + <span class="number">20</span>]);
+<a href=#718 id=718 data-nosnippet>718</a>    }
+<a href=#719 id=719 data-nosnippet>719</a>
+<a href=#720 id=720 data-nosnippet>720</a>    <span class="attr">#[test]
+<a href=#721 id=721 data-nosnippet>721</a>    </span><span class="kw">fn </span>test_advance_to_before_first_next() {
+<a href=#722 id=722 data-nosnippet>722</a>        <span class="kw">let </span>dv = DeleteVector::new(RoaringTreemap::from_iter([<span class="number">5u64</span>, <span class="number">200</span>]));
+<a href=#723 id=723 data-nosnippet>723</a>        <span class="kw">let </span><span class="kw-2">mut </span>iter = dv.iter();
+<a href=#724 id=724 data-nosnippet>724</a>        iter.advance_to(<span class="number">100</span>);
+<a href=#725 id=725 data-nosnippet>725</a>        <span class="macro">assert_eq!</span>(iter.collect::&lt;Vec&lt;<span class="kw">_</span>&gt;&gt;(), <span class="macro">vec!</span>[<span class="number">200</span>]);
+<a href=#726 id=726 data-nosnippet>726</a>    }
+<a href=#727 id=727 data-nosnippet>727</a>
+<a href=#728 id=728 data-nosnippet>728</a>    <span class="attr">#[test]
+<a href=#729 id=729 data-nosnippet>729</a>    </span><span class="kw">fn </span>test_insertion_and_iteration() {
+<a href=#730 id=730 data-nosnippet>730</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
+<a href=#731 id=731 data-nosnippet>731</a>        <span class="macro">assert!</span>(dv.insert(<span class="number">42</span>));
+<a href=#732 id=732 data-nosnippet>732</a>        <span class="macro">assert!</span>(dv.insert(<span class="number">100</span>));
+<a href=#733 id=733 data-nosnippet>733</a>        <span class="macro">assert!</span>(!dv.insert(<span class="number">42</span>));
+<a href=#734 id=734 data-nosnippet>734</a>
+<a href=#735 id=735 data-nosnippet>735</a>        <span class="kw">let </span><span class="kw-2">mut </span>items: Vec&lt;u64&gt; = dv.iter().collect();
+<a href=#736 id=736 data-nosnippet>736</a>        items.sort();
+<a href=#737 id=737 data-nosnippet>737</a>        <span class="macro">assert_eq!</span>(items, <span class="macro">vec!</span>[<span class="number">42</span>, <span class="number">100</span>]);
+<a href=#738 id=738 data-nosnippet>738</a>        <span class="macro">assert_eq!</span>(dv.len(), <span class="number">2</span>);
+<a href=#739 id=739 data-nosnippet>739</a>    }
+<a href=#740 id=740 data-nosnippet>740</a>
+<a href=#741 id=741 data-nosnippet>741</a>    <span class="attr">#[test]
+<a href=#742 id=742 data-nosnippet>742</a>    </span><span class="kw">fn </span>test_successful_insert_positions() {
+<a href=#743 id=743 data-nosnippet>743</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
+<a href=#744 id=744 data-nosnippet>744</a>        <span class="kw">let </span>positions = <span class="macro">vec!</span>[<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">1000</span>, <span class="number">1 </span>&lt;&lt; <span class="number">33</span>];
+<a href=#745 id=745 data-nosnippet>745</a>        <span class="macro">assert_eq!</span>(dv.insert_positions(<span class="kw-2">&amp;</span>positions).unwrap(), <span class="number">5</span>);
+<a href=#746 id=746 data-nosnippet>746</a>
+<a href=#747 id=747 data-nosnippet>747</a>        <span class="kw">let </span><span class="kw-2">mut </span>collected: Vec&lt;u64&gt; = dv.iter().collect();
+<a href=#748 id=748 data-nosnippet>748</a>        collected.sort();
+<a href=#749 id=749 data-nosnippet>749</a>        <span class="macro">assert_eq!</span>(collected, positions);
+<a href=#750 id=750 data-nosnippet>750</a>    }
+<a href=#751 id=751 data-nosnippet>751</a>
+<a href=#752 id=752 data-nosnippet>752</a>    <span class="doccomment">/// Testing scenario: bulk insertion fails because input positions are not strictly increasing.
+<a href=#753 id=753 data-nosnippet>753</a>    </span><span class="attr">#[test]
+<a href=#754 id=754 data-nosnippet>754</a>    </span><span class="kw">fn </span>test_failed_insertion_unsorted_elements() {
+<a href=#755 id=755 data-nosnippet>755</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
+<a href=#756 id=756 data-nosnippet>756</a>        <span class="kw">let </span>positions = <span class="macro">vec!</span>[<span class="number">1</span>, <span class="number">3</span>, <span class="number">5</span>, <span class="number">4</span>];
+<a href=#757 id=757 data-nosnippet>757</a>        <span class="kw">let </span>res = dv.insert_positions(<span class="kw-2">&amp;</span>positions);
+<a href=#758 id=758 data-nosnippet>758</a>        <span class="macro">assert!</span>(res.is_err());
+<a href=#759 id=759 data-nosnippet>759</a>    }
+<a href=#760 id=760 data-nosnippet>760</a>
+<a href=#761 id=761 data-nosnippet>761</a>    <span class="doccomment">/// Testing scenario: bulk insertion fails because input positions have intersection with existing ones.
+<a href=#762 id=762 data-nosnippet>762</a>    </span><span class="attr">#[test]
+<a href=#763 id=763 data-nosnippet>763</a>    </span><span class="kw">fn </span>test_failed_insertion_with_intersection() {
+<a href=#764 id=764 data-nosnippet>764</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
+<a href=#765 id=765 data-nosnippet>765</a>        <span class="kw">let </span>positions = <span class="macro">vec!</span>[<span class="number">1</span>, <span class="number">3</span>, <span class="number">5</span>];
+<a href=#766 id=766 data-nosnippet>766</a>        <span class="macro">assert_eq!</span>(dv.insert_positions(<span class="kw-2">&amp;</span>positions).unwrap(), <span class="number">3</span>);
+<a href=#767 id=767 data-nosnippet>767</a>
+<a href=#768 id=768 data-nosnippet>768</a>        <span class="kw">let </span>res = dv.insert_positions(<span class="kw-2">&amp;</span>[<span class="number">2</span>, <span class="number">4</span>]);
+<a href=#769 id=769 data-nosnippet>769</a>        <span class="macro">assert!</span>(res.is_err());
+<a href=#770 id=770 data-nosnippet>770</a>    }
+<a href=#771 id=771 data-nosnippet>771</a>
+<a href=#772 id=772 data-nosnippet>772</a>    <span class="doccomment">/// Testing scenario: bulk insertion fails because input positions have duplicates.
+<a href=#773 id=773 data-nosnippet>773</a>    </span><span class="attr">#[test]
+<a href=#774 id=774 data-nosnippet>774</a>    </span><span class="kw">fn </span>test_failed_insertion_duplicate_elements() {
+<a href=#775 id=775 data-nosnippet>775</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
+<a href=#776 id=776 data-nosnippet>776</a>        <span class="kw">let </span>positions = <span class="macro">vec!</span>[<span class="number">1</span>, <span class="number">3</span>, <span class="number">5</span>, <span class="number">5</span>];
+<a href=#777 id=777 data-nosnippet>777</a>        <span class="kw">let </span>res = dv.insert_positions(<span class="kw-2">&amp;</span>positions);
+<a href=#778 id=778 data-nosnippet>778</a>        <span class="macro">assert!</span>(res.is_err());
+<a href=#779 id=779 data-nosnippet>779</a>    }
+<a href=#780 id=780 data-nosnippet>780</a>
+<a href=#781 id=781 data-nosnippet>781</a>    <span class="kw">fn </span>encode_dv_blob(dv: <span class="kw-2">&amp;</span>DeleteVector) -&gt; Vec&lt;u8&gt; {
+<a href=#782 id=782 data-nosnippet>782</a>        <span class="kw">let </span><span class="kw-2">mut </span>vector = Vec::with_capacity(dv.inner.serialized_size());
+<a href=#783 id=783 data-nosnippet>783</a>        dv.inner.serialize_into(<span class="kw-2">&amp;mut </span>vector).unwrap();
+<a href=#784 id=784 data-nosnippet>784</a>        frame_dv_blob(<span class="kw-2">&amp;</span>vector)
+<a href=#785 id=785 data-nosnippet>785</a>    }
+<a href=#786 id=786 data-nosnippet>786</a>
+<a href=#787 id=787 data-nosnippet>787</a>    <span class="kw">fn </span>dv_of(positions: <span class="kw">impl </span>IntoIterator&lt;Item = u64&gt;) -&gt; DeleteVector {
+<a href=#788 id=788 data-nosnippet>788</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = DeleteVector::default();
+<a href=#789 id=789 data-nosnippet>789</a>        <span class="kw">for </span>pos <span class="kw">in </span>positions {
+<a href=#790 id=790 data-nosnippet>790</a>            dv.insert(pos);
+<a href=#791 id=791 data-nosnippet>791</a>        }
+<a href=#792 id=792 data-nosnippet>792</a>        dv
+<a href=#793 id=793 data-nosnippet>793</a>    }
+<a href=#794 id=794 data-nosnippet>794</a>
+<a href=#795 id=795 data-nosnippet>795</a>    <span class="kw">fn </span>sorted(dv: <span class="kw-2">&amp;</span>DeleteVector) -&gt; Vec&lt;u64&gt; {
+<a href=#796 id=796 data-nosnippet>796</a>        <span class="kw">let </span><span class="kw-2">mut </span>positions: Vec&lt;u64&gt; = dv.iter().collect();
+<a href=#797 id=797 data-nosnippet>797</a>        positions.sort_unstable();
+<a href=#798 id=798 data-nosnippet>798</a>        positions
+<a href=#799 id=799 data-nosnippet>799</a>    }
+<a href=#800 id=800 data-nosnippet>800</a>
+<a href=#801 id=801 data-nosnippet>801</a>    <span class="attr">#[test]
+<a href=#802 id=802 data-nosnippet>802</a>    </span><span class="kw">fn </span>test_deserialize_roundtrip_empty() {
+<a href=#803 id=803 data-nosnippet>803</a>        <span class="kw">let </span>blob = encode_dv_blob(<span class="kw-2">&amp;</span>DeleteVector::default());
+<a href=#804 id=804 data-nosnippet>804</a>        <span class="macro">assert_eq!</span>(DeleteVector::deserialize(<span class="kw-2">&amp;</span>blob).unwrap().len(), <span class="number">0</span>);
+<a href=#805 id=805 data-nosnippet>805</a>    }
+<a href=#806 id=806 data-nosnippet>806</a>
+<a href=#807 id=807 data-nosnippet>807</a>    <span class="attr">#[test]
+<a href=#808 id=808 data-nosnippet>808</a>    </span><span class="kw">fn </span>test_deserialize_roundtrip_small() {
+<a href=#809 id=809 data-nosnippet>809</a>        <span class="kw">let </span>positions = [<span class="number">0u64</span>, <span class="number">5</span>, <span class="number">100</span>, <span class="number">1000</span>];
+<a href=#810 id=810 data-nosnippet>810</a>        <span class="kw">let </span>dv = DeleteVector::deserialize(<span class="kw-2">&amp;</span>encode_dv_blob(<span class="kw-2">&amp;</span>dv_of(positions))).unwrap();
+<a href=#811 id=811 data-nosnippet>811</a>        <span class="macro">assert_eq!</span>(sorted(<span class="kw-2">&amp;</span>dv), positions);
+<a href=#812 id=812 data-nosnippet>812</a>    }
+<a href=#813 id=813 data-nosnippet>813</a>
+<a href=#814 id=814 data-nosnippet>814</a>    <span class="attr">#[test]
+<a href=#815 id=815 data-nosnippet>815</a>    </span><span class="kw">fn </span>test_deserialize_roundtrip_spanning_64bit_keys() {
+<a href=#816 id=816 data-nosnippet>816</a>        <span class="kw">let </span>positions = [<span class="number">1u64</span>, <span class="number">1 </span>&lt;&lt; <span class="number">33</span>, (<span class="number">1 </span>&lt;&lt; <span class="number">33</span>) + <span class="number">5</span>, <span class="number">1 </span>&lt;&lt; <span class="number">34</span>];
+<a href=#817 id=817 data-nosnippet>817</a>        <span class="kw">let </span>dv = DeleteVector::deserialize(<span class="kw-2">&amp;</span>encode_dv_blob(<span class="kw-2">&amp;</span>dv_of(positions))).unwrap();
+<a href=#818 id=818 data-nosnippet>818</a>        <span class="macro">assert_eq!</span>(sorted(<span class="kw-2">&amp;</span>dv), positions);
+<a href=#819 id=819 data-nosnippet>819</a>    }
+<a href=#820 id=820 data-nosnippet>820</a>
+<a href=#821 id=821 data-nosnippet>821</a>    <span class="comment">// Java run-optimizes every deletion vector before writing, so real blobs carry RUN
+<a href=#822 id=822 data-nosnippet>822</a>    // containers, which use the SERIAL_COOKIE roaring layout. Force that layout so decode
+<a href=#823 id=823 data-nosnippet>823</a>    // exercises the run-container path rather than only array and bitmap containers.
+<a href=#824 id=824 data-nosnippet>824</a>    </span><span class="attr">#[test]
+<a href=#825 id=825 data-nosnippet>825</a>    </span><span class="kw">fn </span>test_deserialize_roundtrip_run_optimized() {
+<a href=#826 id=826 data-nosnippet>826</a>        <span class="kw">let </span><span class="kw-2">mut </span>dv = dv_of(<span class="number">0</span>..<span class="number">10_000</span>);
+<a href=#827 id=827 data-nosnippet>827</a>        <span class="macro">assert!</span>(
+<a href=#828 id=828 data-nosnippet>828</a>            dv.inner.optimize(),
+<a href=#829 id=829 data-nosnippet>829</a>            <span class="string">"expected a dense range to run-length encode"
+<a href=#830 id=830 data-nosnippet>830</a>        </span>);
+<a href=#831 id=831 data-nosnippet>831</a>        <span class="kw">let </span>decoded = DeleteVector::deserialize(<span class="kw-2">&amp;</span>encode_dv_blob(<span class="kw-2">&amp;</span>dv)).unwrap();
+<a href=#832 id=832 data-nosnippet>832</a>        <span class="macro">assert_eq!</span>(decoded.len(), <span class="number">10_000</span>);
+<a href=#833 id=833 data-nosnippet>833</a>        <span class="kw">let </span>positions = sorted(<span class="kw-2">&amp;</span>decoded);
+<a href=#834 id=834 data-nosnippet>834</a>        <span class="macro">assert_eq!</span>(positions.first(), <span class="prelude-val">Some</span>(<span class="kw-2">&amp;</span><span class="number">0</span>));
+<a href=#835 id=835 data-nosnippet>835</a>        <span class="macro">assert_eq!</span>(positions.last(), <span class="prelude-val">Some</span>(<span class="kw-2">&amp;</span><span class="number">9_999</span>));
+<a href=#836 id=836 data-nosnippet>836</a>    }
+<a href=#837 id=837 data-nosnippet>837</a>
+<a href=#838 id=838 data-nosnippet>838</a>    <span class="attr">#[test]
+<a href=#839 id=839 data-nosnippet>839</a>    </span><span class="kw">fn </span>test_deserialize_rejects_short_blob() {
+<a href=#840 id=840 data-nosnippet>840</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>[<span class="number">0u8</span>; DV_MIN_BLOB_BYTES - <span class="number">1</span>]).unwrap_err();
+<a href=#841 id=841 data-nosnippet>841</a>        <span class="macro">assert_eq!</span>(err.kind(), ErrorKind::DataInvalid);
+<a href=#842 id=842 data-nosnippet>842</a>    }
+<a href=#843 id=843 data-nosnippet>843</a>
+<a href=#844 id=844 data-nosnippet>844</a>    <span class="attr">#[test]
+<a href=#845 id=845 data-nosnippet>845</a>    </span><span class="kw">fn </span>test_deserialize_rejects_bad_magic() {
+<a href=#846 id=846 data-nosnippet>846</a>        <span class="kw">let </span><span class="kw-2">mut </span>blob = encode_dv_blob(<span class="kw-2">&amp;</span>dv_of([<span class="number">1</span>]));
+<a href=#847 id=847 data-nosnippet>847</a>        blob[DV_LENGTH_PREFIX_BYTES] ^= <span class="number">0xFF</span>;
+<a href=#848 id=848 data-nosnippet>848</a>        <span class="comment">// Recompute the CRC so the magic check, not the CRC check, is what fails.
+<a href=#849 id=849 data-nosnippet>849</a>        </span><span class="kw">let </span>end = blob.len() - DV_CRC_BYTES;
+<a href=#850 id=850 data-nosnippet>850</a>        <span class="kw">let </span>crc = crc32fast::hash(<span class="kw-2">&amp;</span>blob[DV_LENGTH_PREFIX_BYTES..end]);
+<a href=#851 id=851 data-nosnippet>851</a>        blob[end..].copy_from_slice(<span class="kw-2">&amp;</span>crc.to_be_bytes());
+<a href=#852 id=852 data-nosnippet>852</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>blob).unwrap_err();
+<a href=#853 id=853 data-nosnippet>853</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"magic mismatch"</span>), <span class="string">"got: {err}"</span>);
+<a href=#854 id=854 data-nosnippet>854</a>    }
+<a href=#855 id=855 data-nosnippet>855</a>
+<a href=#856 id=856 data-nosnippet>856</a>    <span class="attr">#[test]
+<a href=#857 id=857 data-nosnippet>857</a>    </span><span class="kw">fn </span>test_deserialize_rejects_bad_crc() {
+<a href=#858 id=858 data-nosnippet>858</a>        <span class="kw">let </span><span class="kw-2">mut </span>blob = encode_dv_blob(<span class="kw-2">&amp;</span>dv_of([<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>]));
+<a href=#859 id=859 data-nosnippet>859</a>        <span class="kw">let </span>end = blob.len() - DV_CRC_BYTES;
+<a href=#860 id=860 data-nosnippet>860</a>        blob[end] ^= <span class="number">0xFF</span>;
+<a href=#861 id=861 data-nosnippet>861</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>blob).unwrap_err();
+<a href=#862 id=862 data-nosnippet>862</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"CRC mismatch"</span>), <span class="string">"got: {err}"</span>);
+<a href=#863 id=863 data-nosnippet>863</a>    }
+<a href=#864 id=864 data-nosnippet>864</a>
+<a href=#865 id=865 data-nosnippet>865</a>    <span class="attr">#[test]
+<a href=#866 id=866 data-nosnippet>866</a>    </span><span class="kw">fn </span>test_deserialize_rejects_length_prefix_mismatch() {
+<a href=#867 id=867 data-nosnippet>867</a>        <span class="kw">let </span><span class="kw-2">mut </span>blob = encode_dv_blob(<span class="kw-2">&amp;</span>dv_of([<span class="number">1</span>]));
+<a href=#868 id=868 data-nosnippet>868</a>        <span class="kw">let </span>declared = u32::from_be_bytes(blob[..DV_LENGTH_PREFIX_BYTES].try_into().unwrap());
+<a href=#869 id=869 data-nosnippet>869</a>        blob[..DV_LENGTH_PREFIX_BYTES].copy_from_slice(<span class="kw-2">&amp;</span>(declared + <span class="number">1</span>).to_be_bytes());
+<a href=#870 id=870 data-nosnippet>870</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>blob).unwrap_err();
+<a href=#871 id=871 data-nosnippet>871</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"length prefix"</span>), <span class="string">"got: {err}"</span>);
+<a href=#872 id=872 data-nosnippet>872</a>    }
+<a href=#873 id=873 data-nosnippet>873</a>
+<a href=#874 id=874 data-nosnippet>874</a>    <span class="comment">// Crafts a raw roaring treemap directory (bitmap count header + key/bitmap entries) so tests
+<a href=#875 id=875 data-nosnippet>875</a>    // can exercise key-ordering violations that `dv_of`/`encode_dv_blob` can never produce, since
+<a href=#876 id=876 data-nosnippet>876</a>    // `DeleteVector::insert` always keeps keys unique and ascending.
+<a href=#877 id=877 data-nosnippet>877</a>    </span><span class="kw">fn </span>raw_roaring_vector(entries: <span class="kw-2">&amp;</span>[(u32, <span class="kw-2">&amp;</span>[u32])]) -&gt; Vec&lt;u8&gt; {
+<a href=#878 id=878 data-nosnippet>878</a>        <span class="kw">let </span><span class="kw-2">mut </span>vector = Vec::new();
+<a href=#879 id=879 data-nosnippet>879</a>        vector.extend_from_slice(<span class="kw-2">&amp;</span>(entries.len() <span class="kw">as </span>u64).to_le_bytes());
+<a href=#880 id=880 data-nosnippet>880</a>        <span class="kw">for </span>(key, positions) <span class="kw">in </span>entries {
+<a href=#881 id=881 data-nosnippet>881</a>            <span class="kw">let </span><span class="kw-2">mut </span>bitmap = RoaringBitmap::new();
+<a href=#882 id=882 data-nosnippet>882</a>            <span class="kw">for </span><span class="kw-2">&amp;</span>pos <span class="kw">in </span><span class="kw-2">*</span>positions {
+<a href=#883 id=883 data-nosnippet>883</a>                bitmap.insert(pos);
+<a href=#884 id=884 data-nosnippet>884</a>            }
+<a href=#885 id=885 data-nosnippet>885</a>            vector.extend_from_slice(<span class="kw-2">&amp;</span>key.to_le_bytes());
+<a href=#886 id=886 data-nosnippet>886</a>            bitmap.serialize_into(<span class="kw-2">&amp;mut </span>vector).unwrap();
+<a href=#887 id=887 data-nosnippet>887</a>        }
+<a href=#888 id=888 data-nosnippet>888</a>        vector
+<a href=#889 id=889 data-nosnippet>889</a>    }
+<a href=#890 id=890 data-nosnippet>890</a>
+<a href=#891 id=891 data-nosnippet>891</a>    <span class="comment">// `RoaringTreemap::deserialize_from` stores keys in a `BTreeMap` via a plain insert, so
+<a href=#892 id=892 data-nosnippet>892</a>    // without our own ordering check, decoding this would silently keep only the second bitmap
+<a href=#893 id=893 data-nosnippet>893</a>    // for key 5 (position 2) and drop the first (position 1).
+<a href=#894 id=894 data-nosnippet>894</a>    </span><span class="attr">#[test]
+<a href=#895 id=895 data-nosnippet>895</a>    </span><span class="kw">fn </span>test_deserialize_rejects_duplicate_keys() {
+<a href=#896 id=896 data-nosnippet>896</a>        <span class="kw">let </span>vector = raw_roaring_vector(<span class="kw-2">&amp;</span>[(<span class="number">5</span>, <span class="kw-2">&amp;</span>[<span class="number">1</span>]), (<span class="number">5</span>, <span class="kw-2">&amp;</span>[<span class="number">2</span>])]);
+<a href=#897 id=897 data-nosnippet>897</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>frame_dv_blob(<span class="kw-2">&amp;</span>vector)).unwrap_err();
+<a href=#898 id=898 data-nosnippet>898</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"unsigned comparison"</span>), <span class="string">"got: {err}"</span>);
+<a href=#899 id=899 data-nosnippet>899</a>    }
+<a href=#900 id=900 data-nosnippet>900</a>
+<a href=#901 id=901 data-nosnippet>901</a>    <span class="comment">// The Puffin spec requires the roaring directory's keys to be "ordered by unsigned
+<a href=#902 id=902 data-nosnippet>902</a>    // comparison"; a stream with unique but out-of-order keys is not a conformant blob even
+<a href=#903 id=903 data-nosnippet>903</a>    // though `BTreeMap` would happily reorder it into a correct-looking result.
+<a href=#904 id=904 data-nosnippet>904</a>    </span><span class="attr">#[test]
+<a href=#905 id=905 data-nosnippet>905</a>    </span><span class="kw">fn </span>test_deserialize_rejects_out_of_order_keys() {
+<a href=#906 id=906 data-nosnippet>906</a>        <span class="kw">let </span>vector = raw_roaring_vector(<span class="kw-2">&amp;</span>[(<span class="number">5</span>, <span class="kw-2">&amp;</span>[<span class="number">1</span>]), (<span class="number">3</span>, <span class="kw-2">&amp;</span>[<span class="number">2</span>])]);
+<a href=#907 id=907 data-nosnippet>907</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>frame_dv_blob(<span class="kw-2">&amp;</span>vector)).unwrap_err();
+<a href=#908 id=908 data-nosnippet>908</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"unsigned comparison"</span>), <span class="string">"got: {err}"</span>);
+<a href=#909 id=909 data-nosnippet>909</a>    }
+<a href=#910 id=910 data-nosnippet>910</a>
+<a href=#911 id=911 data-nosnippet>911</a>    <span class="comment">// The roaring portable format stores the bitmap count as a u64 with the upper 32 bits
+<a href=#912 id=912 data-nosnippet>912</a>    // reserved as zero padding, restricting it to [0, 2^32 - 1]; a value above that is not a
+<a href=#913 id=913 data-nosnippet>913</a>    // conformant blob, regardless of whether any key/bitmap entries follow.
+<a href=#914 id=914 data-nosnippet>914</a>    </span><span class="attr">#[test]
+<a href=#915 id=915 data-nosnippet>915</a>    </span><span class="kw">fn </span>test_deserialize_rejects_bitmap_count_overflow() {
+<a href=#916 id=916 data-nosnippet>916</a>        <span class="kw">let </span>vector = (u32::MAX <span class="kw">as </span>u64 + <span class="number">1</span>).to_le_bytes().to_vec();
+<a href=#917 id=917 data-nosnippet>917</a>        <span class="kw">let </span>err = DeleteVector::deserialize(<span class="kw-2">&amp;</span>frame_dv_blob(<span class="kw-2">&amp;</span>vector)).unwrap_err();
+<a href=#918 id=918 data-nosnippet>918</a>        <span class="macro">assert!</span>(err.message().contains(<span class="string">"exceeds the"</span>), <span class="string">"got: {err}"</span>);
+<a href=#919 id=919 data-nosnippet>919</a>    }
+<a href=#920 id=920 data-nosnippet>920</a>}
 </code></pre></div></section></main></body></html>
\ No newline at end of file