1use 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
23fn 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
39pub struct Q42RangeVolume<S: Q42RangeSource> {
42 source: S,
43 header: Q42VolumeHeader,
44 source_length: u64,
45}
46
47#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
50pub struct Q42ObjectSearchCursor {
51 pub block_offset: usize,
53 pub quin_offset: usize,
55}
56
57#[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#[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#[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 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 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 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 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 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 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 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 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 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}