Skip to main content

qualia_core_db/q42/volume/
range_volume.rs

1//! Range-backed Q42 segment reader for local, HTTP, and IPFS sources.
2
3use std::io;
4
5use super::super::{
6    header_from_bytes, BlockDirectoryEntry, Q42VolumeHeader, BIDX_MAGIC, FIELD_RANGE_INDEX_MAGIC,
7    FLAG_BLOCKS_LZ4, HEADER_SIZE, MAX_COMPRESSED_SUPERBLOCK_SIZE, Q42_VERSION_V3, QUINS_PER_BLOCK,
8    QUIN_SIZE, SUPERBLOCK_HEADER, SUPERBLOCK_SIZE,
9};
10use super::index::{BidxBlockRange, BidxMatchPage};
11use super::range::{Q42ByteRange, Q42RangeSource};
12use crate::NQuin;
13
14const BIDX_HEADER_BYTES: usize = 16;
15const BIDX_ENTRY_BYTES: usize = 16;
16const FIELD_RANGE_INDEX_HEADER_BYTES: usize = 16;
17const FIELD_RANGE_INDEX_ENTRY_BYTES: usize = 48;
18
19fn invalid(message: impl Into<String>) -> io::Error {
20    io::Error::new(io::ErrorKind::InvalidData, message.into())
21}
22
23/// Fail closed on a decoded SuperBlock before any Quin is copied out.
24fn verify_decoded_superblock(decoded: &[u8]) -> io::Result<()> {
25    if decoded.len() != SUPERBLOCK_SIZE {
26        return Err(invalid("decoded Q42 SuperBlock has the wrong length"));
27    }
28    let count = u64::from_le_bytes(decoded[16..24].try_into().unwrap()) as usize;
29    if count > QUINS_PER_BLOCK {
30        return Err(invalid("decoded Q42 SuperBlock Quin count exceeds capacity"));
31    }
32    let occupied = SUPERBLOCK_HEADER + count * QUIN_SIZE;
33    if occupied > SUPERBLOCK_SIZE {
34        return Err(invalid("decoded Q42 SuperBlock Quin payload overruns the block"));
35    }
36    Ok(())
37}
38
39/// A Q42 reader that fetches exactly the bytes needed from a random-access
40/// source. All variable-size buffers remain caller-owned.
41pub struct Q42RangeVolume<S: Q42RangeSource> {
42    source: S,
43    header: Q42VolumeHeader,
44    source_length: u64,
45}
46
47/// Resume state for a caller-buffered object search. It is tied to the object
48/// hash passed to [`Q42RangeVolume::find_object_into`].
49#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
50pub struct Q42ObjectSearchCursor {
51    /// Offset into the matching BIDX block interval.
52    pub block_offset: usize,
53    /// Quin offset in the current decoded block.
54    pub quin_offset: usize,
55}
56
57/// One page of exact object matches written by [`Q42RangeVolume::find_object_into`].
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub struct Q42ObjectMatchPage {
60    pub block_range: BidxBlockRange,
61    pub returned: usize,
62    pub next_cursor: Option<Q42ObjectSearchCursor>,
63}
64
65/// A simple physical pattern for range-backed Q42 scans. `None` is an
66/// unbound SPARQL position.  The planner selects the BIDX object index when
67/// `object` is bound and otherwise performs a bounded sequential block scan.
68#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
69pub struct Q42RangeQueryPattern {
70    pub subject: Option<u64>,
71    pub predicate: Option<u64>,
72    pub object: Option<u64>,
73    pub context: Option<u64>,
74}
75
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
77pub enum Q42RangeQueryStrategy {
78    ObjectBidx,
79    FieldRanges,
80    Sequential,
81}
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub struct Q42RangeQueryPlan {
85    pub pattern: Q42RangeQueryPattern,
86    pub strategy: Q42RangeQueryStrategy,
87}
88
89impl Q42RangeQueryPlan {
90    pub fn for_pattern(pattern: Q42RangeQueryPattern) -> Self {
91        Self {
92            strategy: if pattern.object.is_some() {
93                Q42RangeQueryStrategy::ObjectBidx
94            } else if pattern.subject.is_some()
95                || pattern.predicate.is_some()
96                || pattern.context.is_some()
97            {
98                Q42RangeQueryStrategy::FieldRanges
99            } else {
100                Q42RangeQueryStrategy::Sequential
101            },
102            pattern,
103        }
104    }
105}
106
107/// Resume state for a bounded range-query page.
108#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
109pub struct Q42RangeQueryCursor {
110    pub block_index: usize,
111    pub quin_offset: usize,
112}
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
115pub struct Q42RangeQueryPage {
116    pub returned: usize,
117    pub next_cursor: Option<Q42RangeQueryCursor>,
118}
119
120impl<S: Q42RangeSource> Q42RangeVolume<S> {
121    pub fn open(source: S) -> io::Result<Self> {
122        let source_length = source.length()?;
123        if source_length < HEADER_SIZE as u64 {
124            return Err(invalid("Q42 range source is shorter than its header"));
125        }
126        let mut bytes = [0u8; HEADER_SIZE];
127        source.read_range_into(
128            Q42ByteRange {
129                offset: 0,
130                length: HEADER_SIZE,
131            },
132            &mut bytes,
133        )?;
134        let header = header_from_bytes(&bytes)?;
135        let version = header.version;
136        let flags = header.flags;
137        let block_size = header.block_size;
138        let quins_per_block = header.quins_per_block;
139        if version != Q42_VERSION_V3
140            || flags & FLAG_BLOCKS_LZ4 == 0
141            || block_size != SUPERBLOCK_SIZE as u32
142            || quins_per_block != QUINS_PER_BLOCK as u32
143        {
144            return Err(invalid("unsupported Q42 range-volume header"));
145        }
146        for (name, offset, length) in [
147            ("lexicon", header.lex_offset, header.lex_length),
148            ("BIDX", header.bidx_offset, header.bidx_length),
149            match header.field_range_index_range() {
150                Some((offset, length)) => ("field-range index", offset, length),
151                None => ("field-range index", 0, 0),
152            },
153            (
154                "block directory",
155                header.block_dir_offset,
156                header.block_dir_length,
157            ),
158            ("block data", header.data_offset, header.data_length),
159        ] {
160            if length != 0
161                && (offset < HEADER_SIZE as u64
162                    || offset
163                        .checked_add(length)
164                        .is_none_or(|end| end > source_length))
165            {
166                return Err(invalid(format!(
167                    "Q42 {name} section lies outside the range source"
168                )));
169            }
170        }
171        let expected_directory = header
172            .block_count
173            .checked_mul(BlockDirectoryEntry::SIZE as u64)
174            .ok_or_else(|| invalid("Q42 directory length overflow"))?;
175        if header.block_dir_length != expected_directory {
176            return Err(invalid("Q42 directory does not match its block count"));
177        }
178        let volume = Self {
179            source,
180            header,
181            source_length,
182        };
183        volume.bidx_block_count()?;
184        volume.validate_field_range_index()?;
185        volume.validate_field_postings()?;
186        Ok(volume)
187    }
188
189    pub fn header(&self) -> &Q42VolumeHeader {
190        &self.header
191    }
192    pub fn source_length(&self) -> u64 {
193        self.source_length
194    }
195    pub fn block_count(&self) -> u64 {
196        self.header.block_count
197    }
198
199    /// Execute one caller-buffered page of a physical Q42 scan.  A bound
200    /// object uses BIDX to avoid unrelated SuperBlocks; all other patterns
201    /// stream one block at a time.  This is the reusable low-level path that
202    /// a SPARQL planner can drive without materialising a graph snapshot.
203    pub fn execute_query_page_into(
204        &self,
205        plan: Q42RangeQueryPlan,
206        cursor: Q42RangeQueryCursor,
207        compressed: &mut [u8],
208        decoded: &mut [u8],
209        out: &mut [NQuin],
210    ) -> io::Result<Q42RangeQueryPage> {
211        if out.is_empty() {
212            return Err(io::Error::new(
213                io::ErrorKind::InvalidInput,
214                "Q42 query output buffer is empty",
215            ));
216        }
217        let (start, end) = match plan.strategy {
218            Q42RangeQueryStrategy::ObjectBidx => match self.bidx_block_range_for_hash(
219                plan.pattern
220                    .object
221                    .expect("object strategy requires object"),
222            )? {
223                Some(range) => (range.start, range.end),
224                None => {
225                    return Ok(Q42RangeQueryPage {
226                        returned: 0,
227                        next_cursor: None,
228                    })
229                }
230            },
231            Q42RangeQueryStrategy::FieldRanges | Q42RangeQueryStrategy::Sequential => {
232                (0, self.header.block_count as usize)
233            }
234        };
235        let mut block_index = cursor.block_index.max(start);
236        let mut quin_offset = if block_index == cursor.block_index {
237            cursor.quin_offset
238        } else {
239            0
240        };
241        let mut returned = 0usize;
242        while block_index < end {
243            if plan.strategy == Q42RangeQueryStrategy::FieldRanges
244                && !self.field_membership_may_match(block_index, plan.pattern)?
245            {
246                block_index += 1;
247                quin_offset = 0;
248                continue;
249            }
250            self.read_superblock_into(block_index, compressed, decoded)?;
251            let count = u64::from_le_bytes(decoded[16..24].try_into().unwrap()) as usize;
252            if count > QUINS_PER_BLOCK {
253                return Err(invalid("Q42 decoded SuperBlock has invalid Quin count"));
254            }
255            while quin_offset < count {
256                let offset = SUPERBLOCK_HEADER + quin_offset * QUIN_SIZE;
257                let quin = NQuin {
258                    subject: u64::from_le_bytes(decoded[offset..offset + 8].try_into().unwrap()),
259                    predicate: u64::from_le_bytes(
260                        decoded[offset + 8..offset + 16].try_into().unwrap(),
261                    ),
262                    object: u64::from_le_bytes(
263                        decoded[offset + 16..offset + 24].try_into().unwrap(),
264                    ),
265                    context: u64::from_le_bytes(
266                        decoded[offset + 24..offset + 32].try_into().unwrap(),
267                    ),
268                    metadata: u64::from_le_bytes(
269                        decoded[offset + 32..offset + 40].try_into().unwrap(),
270                    ),
271                    parity: u64::from_le_bytes(
272                        decoded[offset + 40..offset + 48].try_into().unwrap(),
273                    ),
274                };
275                quin_offset += 1;
276                let pattern = plan.pattern;
277                if pattern.subject.is_some_and(|value| value != quin.subject)
278                    || pattern
279                        .predicate
280                        .is_some_and(|value| value != quin.predicate)
281                    || pattern.object.is_some_and(|value| value != quin.object)
282                    || pattern.context.is_some_and(|value| value != quin.context)
283                {
284                    continue;
285                }
286                out[returned] = quin;
287                returned += 1;
288                if returned == out.len() {
289                    let next = if quin_offset < count {
290                        Some(Q42RangeQueryCursor {
291                            block_index,
292                            quin_offset,
293                        })
294                    } else if block_index + 1 < end {
295                        Some(Q42RangeQueryCursor {
296                            block_index: block_index + 1,
297                            quin_offset: 0,
298                        })
299                    } else {
300                        None
301                    };
302                    return Ok(Q42RangeQueryPage {
303                        returned,
304                        next_cursor: next,
305                    });
306                }
307            }
308            block_index += 1;
309            quin_offset = 0;
310        }
311        Ok(Q42RangeQueryPage {
312            returned,
313            next_cursor: None,
314        })
315    }
316    pub fn source(&self) -> &S {
317        &self.source
318    }
319
320    pub fn read_lexicon_into(&self, out: &mut [u8]) -> io::Result<()> {
321        self.read_section(self.header.lex_offset, self.header.lex_length, out)
322    }
323
324    /// Read the fixed Q42LEX header only.  This is used to validate a routed
325    /// lexicon shard without transferring its dictionary pages.
326    pub fn read_lexicon_prefix_into(
327        &self,
328        out: &mut [u8; crate::q42_lex::LEX_HEADER_SIZE],
329    ) -> io::Result<()> {
330        if self.header.lex_length < crate::q42_lex::LEX_HEADER_SIZE as u64 {
331            return Err(invalid("Q42 lexicon is shorter than its fixed header"));
332        }
333        self.source.read_range_into(
334            Q42ByteRange {
335                offset: self.header.lex_offset,
336                length: crate::q42_lex::LEX_HEADER_SIZE,
337            },
338            out,
339        )
340    }
341
342    /// Resolve a string from a paged Q42LEX dictionary using exact range reads.
343    /// `page_scratch` and `out` are caller-owned; neither a full lexicon nor a
344    /// decoded page is retained by the reader.  Returns the UTF-8 byte length
345    /// written into `out`, or `None` when the hash is absent.
346    pub fn lookup_lexicon_hash_into(
347        &self,
348        hash: u64,
349        page_scratch: &mut [u8],
350        out: &mut [u8],
351    ) -> io::Result<Option<usize>> {
352        use crate::q42_lex::{
353            LEX_HEADER_SIZE, LEX_MAGIC, LEX_VERSION_PAGED, PAGED_DIRECTORY_ENTRY_SIZE,
354            PAGED_DIRECTORY_HEADER_SIZE, PAGED_PAGE_HEADER_SIZE,
355        };
356        if self.header.lex_length < (LEX_HEADER_SIZE + PAGED_DIRECTORY_HEADER_SIZE) as u64 {
357            return Err(invalid("Q42 lexicon is too short for paged lookup"));
358        }
359        let mut header = [0u8; LEX_HEADER_SIZE];
360        self.source.read_range_into(
361            Q42ByteRange {
362                offset: self.header.lex_offset,
363                length: LEX_HEADER_SIZE,
364            },
365            &mut header,
366        )?;
367        if header[0..8] != LEX_MAGIC
368            || u64::from_le_bytes(header[24..32].try_into().unwrap()) != LEX_VERSION_PAGED
369        {
370            return Err(io::Error::new(
371                io::ErrorKind::Unsupported,
372                "Q42 range lexicon is not paged Q42LEX v2",
373            ));
374        }
375        let directory_offset = u64::from_le_bytes(header[16..24].try_into().unwrap());
376        if directory_offset != LEX_HEADER_SIZE as u64 {
377            return Err(invalid("Q42 paged lexicon has an invalid directory offset"));
378        }
379        let mut page_count_bytes = [0u8; PAGED_DIRECTORY_HEADER_SIZE];
380        self.source.read_range_into(
381            Q42ByteRange {
382                offset: self.header.lex_offset + directory_offset,
383                length: PAGED_DIRECTORY_HEADER_SIZE,
384            },
385            &mut page_count_bytes,
386        )?;
387        let page_count = usize::try_from(u64::from_le_bytes(page_count_bytes))
388            .map_err(|_| invalid("Q42 paged lexicon page count exceeds platform"))?;
389        let mut lo = 0usize;
390        let mut hi = page_count;
391        let mut entry = [0u8; PAGED_DIRECTORY_ENTRY_SIZE];
392        while lo < hi {
393            let mid = lo + (hi - lo) / 2;
394            let offset = self.header.lex_offset
395                + directory_offset
396                + PAGED_DIRECTORY_HEADER_SIZE as u64
397                + (mid * PAGED_DIRECTORY_ENTRY_SIZE) as u64;
398            self.source.read_range_into(
399                Q42ByteRange {
400                    offset,
401                    length: PAGED_DIRECTORY_ENTRY_SIZE,
402                },
403                &mut entry,
404            )?;
405            if u64::from_le_bytes(entry[0..8].try_into().unwrap()) <= hash {
406                lo = mid + 1;
407            } else {
408                hi = mid;
409            }
410        }
411        let Some(page_index) = lo.checked_sub(1) else {
412            return Ok(None);
413        };
414        let offset = self.header.lex_offset
415            + directory_offset
416            + PAGED_DIRECTORY_HEADER_SIZE as u64
417            + (page_index * PAGED_DIRECTORY_ENTRY_SIZE) as u64;
418        self.source.read_range_into(
419            Q42ByteRange {
420                offset,
421                length: PAGED_DIRECTORY_ENTRY_SIZE,
422            },
423            &mut entry,
424        )?;
425        let page_offset = u64::from_le_bytes(entry[8..16].try_into().unwrap());
426        let page_length = usize::try_from(u64::from_le_bytes(entry[16..24].try_into().unwrap()))
427            .map_err(|_| invalid("Q42 lexicon page exceeds platform"))?;
428        let count = u32::from_le_bytes(entry[24..28].try_into().unwrap()) as usize;
429        if page_length > page_scratch.len()
430            || page_length < PAGED_PAGE_HEADER_SIZE
431            || page_offset
432                .checked_add(page_length as u64)
433                .is_none_or(|end| end > self.header.lex_length)
434        {
435            return Err(invalid(
436                "Q42 lexicon page is out of bounds or exceeds scratch",
437            ));
438        }
439        let page = &mut page_scratch[..page_length];
440        self.source.read_range_into(
441            Q42ByteRange {
442                offset: self.header.lex_offset + page_offset,
443                length: page_length,
444            },
445            page,
446        )?;
447        let declared_count = u32::from_le_bytes(page[0..4].try_into().unwrap()) as usize;
448        let blob_offset = usize::try_from(u64::from_le_bytes(page[8..16].try_into().unwrap()))
449            .map_err(|_| invalid("Q42 lexicon blob offset exceeds platform"))?;
450        if declared_count != count
451            || blob_offset != PAGED_PAGE_HEADER_SIZE + count * 16
452            || blob_offset > page.len()
453        {
454            return Err(invalid("Q42 lexicon page is malformed"));
455        }
456        let mut left = 0usize;
457        let mut right = count;
458        while left < right {
459            let mid = left + (right - left) / 2;
460            let index = PAGED_PAGE_HEADER_SIZE + mid * 16;
461            let entry_hash = u64::from_le_bytes(page[index..index + 8].try_into().unwrap());
462            if entry_hash < hash {
463                left = mid + 1;
464                continue;
465            }
466            if entry_hash > hash {
467                right = mid;
468                continue;
469            }
470            let relative = usize::try_from(u64::from_le_bytes(
471                page[index + 8..index + 16].try_into().unwrap(),
472            ))
473            .map_err(|_| invalid("Q42 lexicon string offset exceeds platform"))?;
474            let start = blob_offset
475                .checked_add(relative)
476                .ok_or_else(|| invalid("Q42 lexicon string offset overflow"))?;
477            if start + 3 > page.len() || page[start] != 1 {
478                return Err(invalid("Q42 lexicon string entry is malformed"));
479            }
480            let length =
481                u16::from_le_bytes(page[start + 1..start + 3].try_into().unwrap()) as usize;
482            let end = start
483                .checked_add(3 + length)
484                .ok_or_else(|| invalid("Q42 lexicon string length overflow"))?;
485            if end > page.len() {
486                return Err(invalid("Q42 lexicon string extends beyond page"));
487            }
488            if length > out.len() {
489                return Err(io::Error::new(
490                    io::ErrorKind::WriteZero,
491                    "Q42 lexicon output buffer is too small",
492                ));
493            }
494            std::str::from_utf8(&page[start + 3..end])
495                .map_err(|_| invalid("Q42 lexicon string is not UTF-8"))?;
496            out[..length].copy_from_slice(&page[start + 3..end]);
497            return Ok(Some(length));
498        }
499        Ok(None)
500    }
501    pub fn read_bidx_into(&self, out: &mut [u8]) -> io::Result<()> {
502        self.read_section(self.header.bidx_offset, self.header.bidx_length, out)
503    }
504
505    /// Return the size of the front-embedded logical-volume manifest, if this
506    /// segment is a root. The caller can use [`Self::read_volume_manifest_into`]
507    /// to retrieve exactly those bytes.
508    pub fn volume_manifest_length(&self) -> io::Result<Option<usize>> {
509        let Some((offset, length)) = self.header.volume_manifest_range() else {
510            return Ok(None);
511        };
512        let length =
513            usize::try_from(length).map_err(|_| invalid("Q42 manifest exceeds platform"))?;
514        if length == 0 || length > super::manifest::MAX_VOLUME_MANIFEST_BYTES {
515            return Err(invalid(
516                "Q42 root has an invalid embedded volume manifest length",
517            ));
518        }
519        Q42ByteRange { offset, length }.validate_for(self.source_length)?;
520        Ok(Some(length))
521    }
522
523    pub fn read_volume_manifest_into(&self, out: &mut [u8]) -> io::Result<bool> {
524        let Some(length) = self.volume_manifest_length()? else {
525            return Ok(false);
526        };
527        if out.len() != length {
528            return Err(io::Error::new(
529                io::ErrorKind::InvalidInput,
530                "Q42 manifest output buffer has wrong length",
531            ));
532        }
533        let (offset, _) = self
534            .header
535            .volume_manifest_range()
536            .expect("manifest length was present");
537        self.source
538            .read_range_into(Q42ByteRange { offset, length }, out)?;
539        Ok(true)
540    }
541    fn read_section(&self, offset: u64, length: u64, out: &mut [u8]) -> io::Result<()> {
542        let length =
543            usize::try_from(length).map_err(|_| invalid("Q42 section exceeds platform"))?;
544        if out.len() != length {
545            return Err(io::Error::new(
546                io::ErrorKind::InvalidInput,
547                "Q42 section output buffer has wrong length",
548            ));
549        }
550        self.source
551            .read_range_into(Q42ByteRange { offset, length }, out)
552    }
553
554    pub fn block_directory_entry(&self, index: usize) -> io::Result<BlockDirectoryEntry> {
555        if index >= self.header.block_count as usize {
556            return Err(io::Error::new(
557                io::ErrorKind::InvalidInput,
558                "Q42 block index out of range",
559            ));
560        }
561        let offset = self
562            .header
563            .block_dir_offset
564            .checked_add((index * BlockDirectoryEntry::SIZE) as u64)
565            .ok_or_else(|| invalid("Q42 directory offset overflow"))?;
566        let mut bytes = [0u8; BlockDirectoryEntry::SIZE];
567        self.source.read_range_into(
568            Q42ByteRange {
569                offset,
570                length: BlockDirectoryEntry::SIZE,
571            },
572            &mut bytes,
573        )?;
574        Ok(BlockDirectoryEntry::from_bytes(&bytes))
575    }
576
577    /// Object-hash bounds read from the first and last BIDX entry. This avoids
578    /// materialising the index merely to validate a manifest segment.
579    pub fn object_hash_bounds(&self) -> io::Result<Option<(u64, u64)>> {
580        let count = self.bidx_block_count()?;
581        if count == 0 {
582            return Ok(None);
583        }
584        let first = self.bidx_entry(0)?;
585        let last = self.bidx_entry(count - 1)?;
586        if first.0 > first.1 || last.0 > last.1 || first.0 > last.1 {
587            return Err(invalid("Q42 BIDX object bounds are invalid"));
588        }
589        Ok(Some((first.0, last.1)))
590    }
591
592    /// Return the complete BIDX interval which can contain an object hash.
593    /// Each comparison fetches one fixed 16-byte BIDX entry; there is no index
594    /// allocation or whole-index transfer.
595    pub fn bidx_block_range_for_hash(
596        &self,
597        object_hash: u64,
598    ) -> io::Result<Option<BidxBlockRange>> {
599        let block_count = self.bidx_block_count()?;
600        if block_count == 0 {
601            return Ok(None);
602        }
603        let mut lo = 0usize;
604        let mut hi = block_count;
605        while lo < hi {
606            let mid = lo + (hi - lo) / 2;
607            if self.bidx_entry(mid)?.1 < object_hash {
608                lo = mid + 1;
609            } else {
610                hi = mid;
611            }
612        }
613        let start = lo;
614        if start == block_count || self.bidx_entry(start)?.0 > object_hash {
615            return Ok(None);
616        }
617        lo = start;
618        hi = block_count;
619        while lo < hi {
620            let mid = lo + (hi - lo) / 2;
621            if self.bidx_entry(mid)?.0 <= object_hash {
622                lo = mid + 1;
623            } else {
624                hi = mid;
625            }
626        }
627        Ok(Some(BidxBlockRange { start, end: lo }))
628    }
629
630    /// Fill one bounded page of BIDX block indices. This retains complete
631    /// heavy-hitter semantics while forcing callers to provide the cap.
632    pub fn bidx_blocks_for_hash_into(
633        &self,
634        object_hash: u64,
635        cursor: usize,
636        out: &mut [usize],
637    ) -> io::Result<Option<BidxMatchPage>> {
638        let Some(range) = self.bidx_block_range_for_hash(object_hash)? else {
639            return Ok(None);
640        };
641        if cursor > range.len() {
642            return Err(io::Error::new(
643                io::ErrorKind::InvalidInput,
644                "Q42 BIDX cursor is beyond the matching interval",
645            ));
646        }
647        if out.is_empty() && cursor < range.len() {
648            return Err(io::Error::new(
649                io::ErrorKind::InvalidInput,
650                "Q42 BIDX page buffer must contain at least one block index",
651            ));
652        }
653        let returned = (range.len() - cursor).min(out.len());
654        for (offset, slot) in out.iter_mut().take(returned).enumerate() {
655            *slot = range.start + cursor + offset;
656        }
657        let next = cursor + returned;
658        Ok(Some(BidxMatchPage {
659            range,
660            returned,
661            next_cursor: (next < range.len()).then_some(next),
662        }))
663    }
664
665    /// Find Quins whose object equals `object_hash`, using the BIDX to fetch
666    /// only candidate SuperBlocks. `compressed`, `decoded`, and `out` are all
667    /// caller-owned. Reuse `cursor` from the returned page until it is `None`.
668    pub fn find_object_into(
669        &self,
670        object_hash: u64,
671        cursor: Q42ObjectSearchCursor,
672        compressed: &mut [u8],
673        decoded: &mut [u8],
674        out: &mut [NQuin],
675    ) -> io::Result<Option<Q42ObjectMatchPage>> {
676        let Some(block_range) = self.bidx_block_range_for_hash(object_hash)? else {
677            return Ok(None);
678        };
679        if cursor.block_offset > block_range.len() {
680            return Err(io::Error::new(
681                io::ErrorKind::InvalidInput,
682                "Q42 object search cursor is beyond the matching block interval",
683            ));
684        }
685        if out.is_empty() && cursor.block_offset < block_range.len() {
686            return Err(io::Error::new(
687                io::ErrorKind::InvalidInput,
688                "Q42 object search output buffer must contain at least one Quin",
689            ));
690        }
691
692        let mut written = 0usize;
693        let mut block_offset = cursor.block_offset;
694        let mut quin_offset = cursor.quin_offset;
695        while block_offset < block_range.len() && written < out.len() {
696            self.read_superblock_into(block_range.start + block_offset, compressed, decoded)?;
697            let live = u64::from_le_bytes(decoded[16..24].try_into().unwrap()) as usize;
698            if live > QUINS_PER_BLOCK || quin_offset > live {
699                return Err(invalid(
700                    "Q42 object search encountered an invalid SuperBlock",
701                ));
702            }
703            while quin_offset < live && written < out.len() {
704                let offset = SUPERBLOCK_HEADER + quin_offset * QUIN_SIZE;
705                let quin =
706                    bytemuck::pod_read_unaligned::<NQuin>(&decoded[offset..offset + QUIN_SIZE]);
707                if quin.object < object_hash {
708                    quin_offset += 1;
709                    continue;
710                }
711                if quin.object > object_hash {
712                    quin_offset = live;
713                    break;
714                }
715                out[written] = quin;
716                written += 1;
717                quin_offset += 1;
718            }
719            if quin_offset == live {
720                block_offset += 1;
721                quin_offset = 0;
722            }
723        }
724        let next_cursor = (block_offset < block_range.len()).then_some(Q42ObjectSearchCursor {
725            block_offset,
726            quin_offset,
727        });
728        Ok(Some(Q42ObjectMatchPage {
729            block_range,
730            returned: written,
731            next_cursor,
732        }))
733    }
734
735    /// Fetch and decode one block. `compressed` must fit the directory entry;
736    /// `out` must be at least one full decoded SuperBlock.
737    pub fn read_superblock_into(
738        &self,
739        index: usize,
740        compressed: &mut [u8],
741        out: &mut [u8],
742    ) -> io::Result<usize> {
743        if out.len() < SUPERBLOCK_SIZE {
744            return Err(io::Error::new(
745                io::ErrorKind::InvalidInput,
746                "Q42 decoded output buffer is too small",
747            ));
748        }
749        let entry = self.block_directory_entry(index)?;
750        let compressed_len = entry.comp_len as usize;
751        if compressed_len < 4
752            || compressed_len > MAX_COMPRESSED_SUPERBLOCK_SIZE
753            || compressed.len() < compressed_len
754            || entry.uncomp_len != SUPERBLOCK_SIZE as u32
755        {
756            return Err(invalid("invalid Q42 compressed block directory entry"));
757        }
758        let offset = self
759            .header
760            .data_offset
761            .checked_add(entry.rel_offset)
762            .ok_or_else(|| invalid("Q42 compressed block offset overflow"))?;
763        self.source.read_range_into(
764            Q42ByteRange {
765                offset,
766                length: compressed_len,
767            },
768            &mut compressed[..compressed_len],
769        )?;
770        let declared = u32::from_le_bytes(compressed[0..4].try_into().unwrap()) as usize;
771        if declared != SUPERBLOCK_SIZE {
772            return Err(invalid("Q42 LZ4 prefix does not declare one SuperBlock"));
773        }
774        let decoded =
775            lz4_flex::decompress_into(&compressed[4..compressed_len], &mut out[..declared])
776                .map_err(|error| invalid(format!("decode Q42 range block: {error}")))?;
777        if decoded != declared {
778            return Err(invalid("Q42 range block decoded to an unexpected length"));
779        }
780        verify_decoded_superblock(&out[..decoded])?;
781        Ok(decoded)
782    }
783
784    pub fn into_source(self) -> S {
785        self.source
786    }
787
788    fn bidx_block_count(&self) -> io::Result<usize> {
789        let bidx_length = self.header.bidx_length;
790        if bidx_length < BIDX_HEADER_BYTES as u64 {
791            return Err(invalid("Q42 BIDX is shorter than its header"));
792        }
793        let mut bytes = [0u8; BIDX_HEADER_BYTES];
794        let offset = self.header.bidx_offset;
795        self.source.read_range_into(
796            Q42ByteRange {
797                offset,
798                length: BIDX_HEADER_BYTES,
799            },
800            &mut bytes,
801        )?;
802        if bytes[0..4] != BIDX_MAGIC || u32::from_le_bytes(bytes[4..8].try_into().unwrap()) != 1 {
803            return Err(invalid("unsupported Q42 BIDX header"));
804        }
805        let count = u32::from_le_bytes(bytes[8..12].try_into().unwrap()) as usize;
806        if count != self.header.block_count as usize {
807            return Err(invalid("Q42 BIDX count does not match the block directory"));
808        }
809        let expected = BIDX_HEADER_BYTES
810            .checked_add(
811                count
812                    .checked_mul(BIDX_ENTRY_BYTES)
813                    .ok_or_else(|| invalid("Q42 BIDX entry count overflow"))?,
814            )
815            .ok_or_else(|| invalid("Q42 BIDX length overflow"))?;
816        if bidx_length != expected as u64 {
817            return Err(invalid("Q42 BIDX length does not match its entry count"));
818        }
819        Ok(count)
820    }
821
822    fn bidx_entry(&self, index: usize) -> io::Result<(u64, u64)> {
823        let block_count = usize::try_from(self.header.block_count)
824            .map_err(|_| invalid("Q42 block count exceeds platform"))?;
825        if index >= block_count {
826            return Err(io::Error::new(
827                io::ErrorKind::InvalidInput,
828                "Q42 BIDX entry index out of range",
829            ));
830        }
831        let offset = self
832            .header
833            .bidx_offset
834            .checked_add(BIDX_HEADER_BYTES as u64)
835            .and_then(|value| value.checked_add((index * BIDX_ENTRY_BYTES) as u64))
836            .ok_or_else(|| invalid("Q42 BIDX entry offset overflow"))?;
837        let mut bytes = [0u8; BIDX_ENTRY_BYTES];
838        self.source.read_range_into(
839            Q42ByteRange {
840                offset,
841                length: BIDX_ENTRY_BYTES,
842            },
843            &mut bytes,
844        )?;
845        let min = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
846        let max = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
847        if min > max {
848            return Err(invalid("Q42 BIDX entry has min > max"));
849        }
850        Ok((min, max))
851    }
852
853    fn validate_field_range_index(&self) -> io::Result<()> {
854        let Some((offset, length)) = self.header.field_range_index_range() else {
855            return Ok(());
856        };
857        let expected = FIELD_RANGE_INDEX_HEADER_BYTES
858            .checked_add(
859                (self.header.block_count as usize)
860                    .checked_mul(FIELD_RANGE_INDEX_ENTRY_BYTES)
861                    .ok_or_else(|| invalid("Q42 field-range index length overflows"))?,
862            )
863            .ok_or_else(|| invalid("Q42 field-range index length overflows"))?;
864        if length != expected as u64 {
865            return Err(invalid(
866                "Q42 field-range index length does not match block count",
867            ));
868        }
869        let mut header = [0u8; FIELD_RANGE_INDEX_HEADER_BYTES];
870        self.read_section(offset, FIELD_RANGE_INDEX_HEADER_BYTES as u64, &mut header)?;
871        if header[0..4] != FIELD_RANGE_INDEX_MAGIC
872            || u32::from_le_bytes(header[4..8].try_into().unwrap()) != 1
873            || u32::from_le_bytes(header[8..12].try_into().unwrap()) as u64
874                != self.header.block_count
875        {
876            return Err(invalid("Q42 field-range index header is invalid"));
877        }
878        Ok(())
879    }
880
881    fn validate_field_postings(&self) -> io::Result<()> {
882        let Some((offset, length)) = self.header.field_postings_range() else {
883            return Ok(());
884        };
885        if length < super::postings::FIELD_POSTINGS_HEADER_BYTES as u64 {
886            return Err(invalid("Q42 field postings shorter than header"));
887        }
888        let mut header = [0u8; super::postings::FIELD_POSTINGS_HEADER_BYTES];
889        self.read_section(
890            offset,
891            super::postings::FIELD_POSTINGS_HEADER_BYTES as u64,
892            &mut header,
893        )?;
894        if header[0..4] != super::postings::FIELD_POSTINGS_MAGIC
895            || u32::from_le_bytes(header[4..8].try_into().unwrap()) != 1
896            || u32::from_le_bytes(header[8..12].try_into().unwrap()) as u64
897                != self.header.block_count
898        {
899            return Err(invalid("Q42 field postings header is invalid"));
900        }
901        Ok(())
902    }
903
904    fn field_membership_may_match(
905        &self,
906        block_index: usize,
907        pattern: Q42RangeQueryPattern,
908    ) -> io::Result<bool> {
909        if let Some(matches) = self.field_postings_may_match(block_index, pattern)? {
910            return Ok(matches);
911        }
912        self.field_range_may_match(block_index, pattern)
913    }
914
915    fn field_postings_may_match(
916        &self,
917        block_index: usize,
918        pattern: Q42RangeQueryPattern,
919    ) -> io::Result<Option<bool>> {
920        let Some((offset, length)) = self.header.field_postings_range() else {
921            return Ok(None);
922        };
923        if block_index >= self.header.block_count as usize {
924            return Err(invalid("Q42 field postings block index is out of bounds"));
925        }
926        let table = offset
927            + super::postings::FIELD_POSTINGS_HEADER_BYTES as u64
928            + (block_index as u64) * 4;
929        let mut ends = [0u8; 8];
930        self.read_section(table, 8, &mut ends)?;
931        let start = u32::from_le_bytes(ends[0..4].try_into().unwrap());
932        let end = u32::from_le_bytes(ends[4..8].try_into().unwrap());
933        let (rel_from, rel_to) = super::postings::block_payload_interval(
934            self.header.block_count as usize,
935            block_index,
936            start,
937            end,
938        )?;
939        let payload_offset = offset + rel_from as u64;
940        let payload_len = rel_to - rel_from;
941        if payload_offset + payload_len as u64 > offset + length {
942            return Err(invalid("Q42 field postings payload leaves its section"));
943        }
944        let mut payload = vec![0u8; payload_len];
945        self.read_section(payload_offset, payload_len as u64, &mut payload)?;
946        let check = |field: usize, value: Option<u64>| -> io::Result<bool> {
947            match value {
948                None => Ok(true),
949                Some(hash) => super::postings::field_may_contain(&payload, field, hash),
950            }
951        };
952        Ok(Some(
953            check(0, pattern.subject)?
954                && check(1, pattern.predicate)?
955                && check(2, pattern.context)?,
956        ))
957    }
958
959    fn field_range_may_match(
960        &self,
961        block_index: usize,
962        pattern: Q42RangeQueryPattern,
963    ) -> io::Result<bool> {
964        let Some((offset, _)) = self.header.field_range_index_range() else {
965            return Ok(true);
966        };
967        if block_index >= self.header.block_count as usize {
968            return Err(invalid("Q42 field-range block index is out of bounds"));
969        }
970        let entry_offset = offset
971            .checked_add(FIELD_RANGE_INDEX_HEADER_BYTES as u64)
972            .and_then(|value| {
973                value.checked_add((block_index * FIELD_RANGE_INDEX_ENTRY_BYTES) as u64)
974            })
975            .ok_or_else(|| invalid("Q42 field-range entry offset overflows"))?;
976        let mut entry = [0u8; FIELD_RANGE_INDEX_ENTRY_BYTES];
977        self.read_section(
978            entry_offset,
979            FIELD_RANGE_INDEX_ENTRY_BYTES as u64,
980            &mut entry,
981        )?;
982        let includes = |field: usize, value: Option<u64>| {
983            let start = field * 16;
984            let min = u64::from_le_bytes(entry[start..start + 8].try_into().unwrap());
985            let max = u64::from_le_bytes(entry[start + 8..start + 16].try_into().unwrap());
986            min <= max && value.is_none_or(|value| min <= value && value <= max)
987        };
988        Ok(includes(0, pattern.subject)
989            && includes(1, pattern.predicate)
990            && includes(2, pattern.context))
991    }
992}
993
994#[cfg(test)]
995mod tests {
996    use super::super::super::{
997        write_unified_volume, write_volume_root, Q42RangeVolumeSet, Q42VolumeManifest,
998    };
999    use super::*;
1000    use crate::mini_parser::hash_token;
1001    use crate::specialized_libs::computational_geometry::allocation_counter::assert_zero_alloc;
1002    use crate::NQuin;
1003    use std::collections::HashMap;
1004    use tempfile::NamedTempFile;
1005
1006    fn sample_volume() -> (NamedTempFile, NQuin) {
1007        let subject = hash_token("urn:q42:range-subject");
1008        let predicate = hash_token("urn:q42:range-predicate");
1009        let object = hash_token("urn:q42:range-object");
1010        let quin = NQuin {
1011            subject,
1012            predicate,
1013            object,
1014            context: 0,
1015            metadata: 0,
1016            parity: 0,
1017        };
1018        let mut lex = HashMap::new();
1019        lex.insert(subject, "urn:q42:range-subject".to_string());
1020        lex.insert(predicate, "urn:q42:range-predicate".to_string());
1021        lex.insert(object, "urn:q42:range-object".to_string());
1022        let file = NamedTempFile::new().unwrap();
1023        write_unified_volume(file.path(), &lex, &[(object, object)], &[vec![quin]]).unwrap();
1024        (file, quin)
1025    }
1026
1027    #[test]
1028    fn range_volume_reads_only_the_directory_entry_and_block() {
1029        let (file, quin) = sample_volume();
1030        let source = super::super::range::LocalFileRangeSource::open(file.path()).unwrap();
1031        let volume = Q42RangeVolume::open(source).unwrap();
1032        assert_eq!(volume.block_count(), 1);
1033        let entry = volume.block_directory_entry(0).unwrap();
1034        assert!(entry.comp_len as usize <= MAX_COMPRESSED_SUPERBLOCK_SIZE);
1035
1036        let mut compressed = [0u8; MAX_COMPRESSED_SUPERBLOCK_SIZE];
1037        let mut decoded = [0u8; SUPERBLOCK_SIZE];
1038        assert_eq!(
1039            volume
1040                .read_superblock_into(0, &mut compressed, &mut decoded)
1041                .unwrap(),
1042            SUPERBLOCK_SIZE
1043        );
1044        assert_eq!(u64::from_le_bytes(decoded[16..24].try_into().unwrap()), 1);
1045        assert_eq!(
1046            u64::from_le_bytes(decoded[160..168].try_into().unwrap()),
1047            quin.subject
1048        );
1049    }
1050
1051    #[test]
1052    fn range_volume_block_read_is_zero_heap() {
1053        let (file, _) = sample_volume();
1054        let source = super::super::range::LocalFileRangeSource::open(file.path()).unwrap();
1055        let volume = Q42RangeVolume::open(source).unwrap();
1056        let mut compressed = [0u8; MAX_COMPRESSED_SUPERBLOCK_SIZE];
1057        let mut decoded = [0u8; SUPERBLOCK_SIZE];
1058        assert_zero_alloc("q42_range_volume_block_read", || {
1059            volume
1060                .read_superblock_into(0, &mut compressed, &mut decoded)
1061                .unwrap();
1062        });
1063    }
1064
1065    #[test]
1066    fn range_volume_resolves_one_paged_lexicon_page_without_heap() {
1067        let (file, quin) = sample_volume();
1068        let source = super::super::range::LocalFileRangeSource::open(file.path()).unwrap();
1069        let volume = Q42RangeVolume::open(source).unwrap();
1070        let mut page = [0u8; 4_096];
1071        let mut text = [0u8; 128];
1072        let length = volume
1073            .lookup_lexicon_hash_into(quin.object, &mut page, &mut text)
1074            .unwrap()
1075            .unwrap();
1076        assert_eq!(&text[..length], b"urn:q42:range-object");
1077        assert_eq!(
1078            volume
1079                .lookup_lexicon_hash_into(7, &mut page, &mut text)
1080                .unwrap(),
1081            None
1082        );
1083        assert_zero_alloc("q42_range_volume_paged_lex_lookup", || {
1084            volume
1085                .lookup_lexicon_hash_into(quin.object, &mut page, &mut text)
1086                .unwrap();
1087        });
1088    }
1089
1090    #[test]
1091    fn range_volume_bidx_pages_complete_heavy_hitters() {
1092        let (file, quin) = sample_volume();
1093        let mut lex = HashMap::new();
1094        lex.insert(quin.subject, "urn:q42:range-subject".to_string());
1095        lex.insert(quin.predicate, "urn:q42:range-predicate".to_string());
1096        lex.insert(quin.object, "urn:q42:range-object".to_string());
1097        write_unified_volume(
1098            file.path(),
1099            &lex,
1100            &[(quin.object, quin.object); 5],
1101            &[vec![quin], vec![quin], vec![quin], vec![quin], vec![quin]],
1102        )
1103        .unwrap();
1104        let source = super::super::range::LocalFileRangeSource::open(file.path()).unwrap();
1105        let volume = Q42RangeVolume::open(source).unwrap();
1106        let mut page = [usize::MAX; 2];
1107        let first = volume
1108            .bidx_blocks_for_hash_into(quin.object, 0, &mut page)
1109            .unwrap()
1110            .unwrap();
1111        assert_eq!(first.range, BidxBlockRange { start: 0, end: 5 });
1112        assert_eq!(&page, &[0, 1]);
1113        let last = volume
1114            .bidx_blocks_for_hash_into(quin.object, first.next_cursor.unwrap(), &mut page)
1115            .unwrap()
1116            .unwrap();
1117        assert_eq!(&page, &[2, 3]);
1118        assert_eq!(last.next_cursor, Some(4));
1119    }
1120
1121    #[test]
1122    fn range_volume_object_search_is_paged_and_zero_heap() {
1123        let (file, quin) = sample_volume();
1124        let mut lex = HashMap::new();
1125        lex.insert(quin.subject, "urn:q42:range-subject".to_string());
1126        lex.insert(quin.predicate, "urn:q42:range-predicate".to_string());
1127        lex.insert(quin.object, "urn:q42:range-object".to_string());
1128        write_unified_volume(
1129            file.path(),
1130            &lex,
1131            &[(quin.object, quin.object); 3],
1132            &[vec![quin], vec![quin], vec![quin]],
1133        )
1134        .unwrap();
1135        let source = super::super::range::LocalFileRangeSource::open(file.path()).unwrap();
1136        let volume = Q42RangeVolume::open(source).unwrap();
1137        let mut compressed = [0u8; MAX_COMPRESSED_SUPERBLOCK_SIZE];
1138        let mut decoded = [0u8; SUPERBLOCK_SIZE];
1139        let mut out = [NQuin::default(); 2];
1140        let first = volume
1141            .find_object_into(
1142                quin.object,
1143                Q42ObjectSearchCursor::default(),
1144                &mut compressed,
1145                &mut decoded,
1146                &mut out,
1147            )
1148            .unwrap()
1149            .unwrap();
1150        assert_eq!(first.returned, 2);
1151        assert_eq!(out, [quin, quin]);
1152        let second = volume
1153            .find_object_into(
1154                quin.object,
1155                first.next_cursor.unwrap(),
1156                &mut compressed,
1157                &mut decoded,
1158                &mut out,
1159            )
1160            .unwrap()
1161            .unwrap();
1162        assert_eq!(second.returned, 1);
1163        assert_eq!(out[0], quin);
1164        assert_eq!(second.next_cursor, None);
1165
1166        assert_zero_alloc("q42_range_volume_object_search", || {
1167            volume
1168                .find_object_into(
1169                    quin.object,
1170                    Q42ObjectSearchCursor::default(),
1171                    &mut compressed,
1172                    &mut decoded,
1173                    &mut out,
1174                )
1175                .unwrap();
1176        });
1177    }
1178
1179    #[test]
1180    fn range_query_planner_uses_bidx_and_pages_matching_quins() {
1181        let (file, quin) = sample_volume();
1182        let mut lex = HashMap::new();
1183        lex.insert(quin.subject, "urn:q42:range-subject".to_string());
1184        lex.insert(quin.predicate, "urn:q42:range-predicate".to_string());
1185        lex.insert(quin.object, "urn:q42:range-object".to_string());
1186        write_unified_volume(
1187            file.path(),
1188            &lex,
1189            &[(quin.object, quin.object); 3],
1190            &[vec![quin], vec![quin], vec![quin]],
1191        )
1192        .unwrap();
1193        let source = super::super::range::LocalFileRangeSource::open(file.path()).unwrap();
1194        let volume = Q42RangeVolume::open(source).unwrap();
1195        let plan = Q42RangeQueryPlan::for_pattern(Q42RangeQueryPattern {
1196            object: Some(quin.object),
1197            predicate: Some(quin.predicate),
1198            ..Default::default()
1199        });
1200        assert_eq!(plan.strategy, Q42RangeQueryStrategy::ObjectBidx);
1201        let mut compressed = [0u8; MAX_COMPRESSED_SUPERBLOCK_SIZE];
1202        let mut decoded = [0u8; SUPERBLOCK_SIZE];
1203        let mut out = [NQuin::default(); 2];
1204        let first = volume
1205            .execute_query_page_into(
1206                plan,
1207                Q42RangeQueryCursor::default(),
1208                &mut compressed,
1209                &mut decoded,
1210                &mut out,
1211            )
1212            .unwrap();
1213        assert_eq!(first.returned, 2);
1214        assert_eq!(out, [quin, quin]);
1215        let second = volume
1216            .execute_query_page_into(
1217                plan,
1218                first.next_cursor.unwrap(),
1219                &mut compressed,
1220                &mut decoded,
1221                &mut out,
1222            )
1223            .unwrap();
1224        assert_eq!(second.returned, 1);
1225        assert_eq!(second.next_cursor, None);
1226        assert_zero_alloc("q42_range_query_bidx_page", || {
1227            volume
1228                .execute_query_page_into(
1229                    plan,
1230                    Q42RangeQueryCursor::default(),
1231                    &mut compressed,
1232                    &mut decoded,
1233                    &mut out,
1234                )
1235                .unwrap();
1236        });
1237    }
1238
1239    #[test]
1240    fn range_volume_set_opens_front_embedded_root_and_verifies_segments() {
1241        let dir = tempfile::TempDir::new().unwrap();
1242        let root_path = dir.path().join("root.q42");
1243        let first_path = dir.path().join("segment-000.q42");
1244        let second_path = dir.path().join("segment-001.q42");
1245        let lex_path = dir.path().join("lex-000.q42");
1246        let (first_quin, first_lex) = sample_quin("urn:q42:one");
1247        let (second_quin, second_lex) = sample_quin("urn:q42:two");
1248        let mut entries = [(first_quin, first_lex), (second_quin, second_lex)];
1249        entries.sort_unstable_by_key(|(quin, _)| quin.object);
1250        write_unified_volume(
1251            &first_path,
1252            &entries[0].1,
1253            &[(entries[0].0.object, entries[0].0.object)],
1254            &[vec![entries[0].0]],
1255        )
1256        .unwrap();
1257        write_unified_volume(
1258            &second_path,
1259            &entries[1].1,
1260            &[(entries[1].0.object, entries[1].0.object)],
1261            &[vec![entries[1].0]],
1262        )
1263        .unwrap();
1264        write_unified_volume(&lex_path, &entries[0].1, &[], &[]).unwrap();
1265        let manifest = Q42VolumeManifest {
1266            generation: 1,
1267            segments: vec![
1268                Q42VolumeManifest::segment_from_file(&first_path, "segment-000.q42".into())
1269                    .unwrap(),
1270                Q42VolumeManifest::segment_from_file(&second_path, "segment-001.q42".into())
1271                    .unwrap(),
1272            ],
1273            lexicon_segments: vec![Q42VolumeManifest::lexicon_segment_from_file(
1274                &lex_path,
1275                "lex-000.q42".into(),
1276            )
1277            .unwrap()],
1278        };
1279        write_volume_root(&root_path, &manifest).unwrap();
1280
1281        let root_source = super::super::range::LocalFileRangeSource::open(&root_path).unwrap();
1282        let root = Q42RangeVolume::open(root_source).unwrap();
1283        let factory = |entry: &super::super::manifest::Q42VolumeSegment| {
1284            super::super::range::LocalFileRangeSource::open(&dir.path().join(&entry.locator))
1285        };
1286        let mut set = Q42RangeVolumeSet::open_root(&root, &factory).unwrap();
1287        set.attach_lexicon_segments(&|entry: &super::super::manifest::Q42LexiconSegment| {
1288            super::super::range::LocalFileRangeSource::open(&dir.path().join(&entry.locator))
1289        })
1290        .unwrap();
1291        let mut lex_page = [0u8; 4096];
1292        let mut lex_text = [0u8; 128];
1293        let lex_len = set
1294            .lookup_lexicon_hash_into(entries[0].0.object, &mut lex_page, &mut lex_text)
1295            .unwrap()
1296            .unwrap();
1297        assert_eq!(
1298            &lex_text[..lex_len],
1299            entries[0].1[&entries[0].0.object].as_bytes()
1300        );
1301        assert_eq!(set.segment_index_for_object(entries[0].0.object), Some(0));
1302        assert_eq!(set.segment_index_for_object(entries[1].0.object), Some(1));
1303        let mut digest_scratch = [0u8; 1024];
1304        set.verify_segment_hashes(&mut digest_scratch).unwrap();
1305        let mut compressed = [0u8; MAX_COMPRESSED_SUPERBLOCK_SIZE];
1306        let mut decoded = [0u8; SUPERBLOCK_SIZE];
1307        set.verify_segment_quin_counts(&mut compressed, &mut decoded)
1308            .unwrap();
1309        let plan = Q42RangeQueryPlan::for_pattern(Q42RangeQueryPattern {
1310            predicate: Some(entries[0].0.predicate),
1311            ..Default::default()
1312        });
1313        let mut out = [NQuin::default(); 2];
1314        let page = set
1315            .execute_query_page_into(
1316                plan,
1317                super::super::manifest::Q42VolumeSetQueryCursor::default(),
1318                &mut compressed,
1319                &mut decoded,
1320                &mut out,
1321            )
1322            .unwrap();
1323        assert_eq!(page.returned, 2);
1324        assert_eq!(page.next_cursor, None);
1325    }
1326
1327    fn sample_quin(object_text: &str) -> (NQuin, HashMap<u64, String>) {
1328        let subject = hash_token("urn:q42:range-subject");
1329        let predicate = hash_token("urn:q42:range-predicate");
1330        let object = hash_token(object_text);
1331        let quin = NQuin {
1332            subject,
1333            predicate,
1334            object,
1335            context: 0,
1336            metadata: 0,
1337            parity: 0,
1338        };
1339        let mut lex = HashMap::new();
1340        lex.insert(subject, "urn:q42:range-subject".to_string());
1341        lex.insert(predicate, "urn:q42:range-predicate".to_string());
1342        lex.insert(object, object_text.to_string());
1343        (quin, lex)
1344    }
1345}
1346#[test]
1347fn field_ranges_prune_non_object_constant_scans() {
1348    let file = tempfile::NamedTempFile::new().unwrap();
1349    let first = NQuin {
1350        subject: 10,
1351        predicate: 20,
1352        object: 1,
1353        context: 30,
1354        metadata: 0,
1355        parity: 0,
1356    };
1357    let second = NQuin {
1358        subject: 40,
1359        predicate: 50,
1360        object: 2,
1361        context: 60,
1362        metadata: 0,
1363        parity: 0,
1364    };
1365    crate::q42_volume::write_unified_volume(
1366        file.path(),
1367        &std::collections::HashMap::new(),
1368        &[(1, 1), (2, 2)],
1369        &[vec![first], vec![second]],
1370    )
1371    .unwrap();
1372    let source = crate::q42_volume::LocalFileRangeSource::open(file.path()).unwrap();
1373    let volume = Q42RangeVolume::open(source).unwrap();
1374    let pattern = Q42RangeQueryPattern {
1375        predicate: Some(second.predicate),
1376        ..Q42RangeQueryPattern::default()
1377    };
1378    assert_eq!(
1379        Q42RangeQueryPlan::for_pattern(pattern).strategy,
1380        Q42RangeQueryStrategy::FieldRanges
1381    );
1382    let mut compressed = [0u8; MAX_COMPRESSED_SUPERBLOCK_SIZE];
1383    let mut decoded = [0u8; SUPERBLOCK_SIZE];
1384    let mut out = [NQuin::default(); 1];
1385    let page = volume
1386        .execute_query_page_into(
1387            Q42RangeQueryPlan::for_pattern(pattern),
1388            Q42RangeQueryCursor::default(),
1389            &mut compressed,
1390            &mut decoded,
1391            &mut out,
1392        )
1393        .unwrap();
1394    assert_eq!(page.returned, 1);
1395    assert_eq!(out[0], second);
1396    assert!(page.next_cursor.is_none());
1397}