1#[cfg(feature = "native")]
12use std::collections::HashSet;
13
14use super::staged_row::StagedRow;
15use crate::dsl::{Document, FieldValue, Schema};
16use byteorder::{LittleEndian, WriteBytesExt};
17use rustc_hash::{FxHashMap, FxHashSet};
18use std::sync::Arc;
19
20use crate::dsl::Field;
21use crate::error::{Error, Result};
22#[cfg(feature = "native")]
23use crate::segment::SegmentSnapshot;
24use crate::structures::BloomFilter;
25
26const BLOOM_BITS_PER_KEY: usize = 10;
28
29const BLOOM_HEADROOM: usize = 100_000;
31
32#[cfg(feature = "native")]
34pub const PK_BLOOM_FILE: &str = "pk_bloom.bin";
35
36const PK_BLOOM_MAGIC: u32 = 0x504B424C; pub(super) fn document_key(doc: &crate::dsl::Document, field: crate::Field) -> Result<&str> {
42 let mut values = doc.get_all(field);
43 let key = values
44 .next()
45 .ok_or_else(|| Error::Document("Missing primary key field".into()))?
46 .as_text()
47 .ok_or_else(|| Error::Document("Primary key must be text".into()))?;
48 if values.next().is_some() {
49 return Err(Error::Document(
50 "primary key requires exactly one text value".into(),
51 ));
52 }
53 if key.is_empty() {
54 return Err(Error::Document("Primary key must not be empty".into()));
55 }
56 if key.len() > 64 * 1024 {
57 return Err(Error::Document(
58 "primary key must contain 1..=65536 bytes".into(),
59 ));
60 }
61 Ok(key)
62}
63
64pub struct PkSegmentData {
69 pub segment_id: String,
70 pub(super) content_hash: Option<super::content_hash::ContentHashLookup>,
71 pub deletion_meta: Option<crate::segment::DeletionMeta>,
72 pub alive_docs: Option<std::sync::Arc<crate::query::DocBitset>>,
73 pub live_key_ordinals: Option<crate::query::DocBitset>,
74 pub fast_fields: FxHashMap<u32, crate::structures::fast_field::FastFieldReader>,
75}
76
77impl PkSegmentData {
78 pub(crate) fn prepare_live_keys(&mut self, field: Field) {
81 if self.live_key_ordinals.is_some() {
82 return;
83 }
84 let Some(alive) = &self.alive_docs else {
85 return;
86 };
87 let Some(ff) = self.fast_fields.get(&field.0) else {
88 return;
89 };
90 let Some(dict) = ff.text_dict() else {
91 return;
92 };
93 let mut keys = crate::query::DocBitset::new(dict.len());
94 ff.scan_single_values(|doc, ordinal| {
95 if alive.contains(doc) && ordinal < u64::from(dict.len()) {
96 keys.set(ordinal as u32);
97 }
98 });
99 self.live_key_ordinals = Some(keys);
100 }
101}
102
103pub struct PrimaryKeyIndex {
112 field: Field,
113 state: parking_lot::Mutex<PrimaryKeyState>,
114 committed_data: Vec<PkSegmentData>,
117 #[cfg(feature = "native")]
119 _snapshot: Option<std::sync::Arc<SegmentSnapshot>>,
120}
121
122struct PrimaryKeyState {
123 bloom: BloomFilter,
124 uncommitted: FxHashMap<Vec<u8>, PendingKey>,
125 pending_bytes: usize,
126 cancelled_bytes: usize,
127 deletes: FxHashSet<String>,
128 delete_bytes: usize,
129}
130
131const MAX_PENDING_KEY_BYTES: usize = 64 * 1024 * 1024;
132const CANCELLED_ROW_BYTES: usize = 4 * std::mem::size_of::<u32>();
134
135struct PendingKey {
136 row: Arc<StagedRow>,
137 hash: Option<FieldValue>,
138 bytes: usize,
139}
140
141const KEY_SLOT_BYTES: usize =
143 2 * (std::mem::size_of::<PendingKey>() + std::mem::size_of::<Vec<u8>>());
144
145fn pending_bytes(key: &str, hash: Option<&FieldValue>) -> usize {
146 KEY_SLOT_BYTES
147 + std::mem::size_of::<StagedRow>()
148 + 2 * std::mem::size_of::<usize>()
149 + key.len()
150 + match hash {
151 Some(FieldValue::Text(value)) => value.len(),
152 Some(FieldValue::Bytes(value)) => value.len(),
153 _ => 0,
154 }
155}
156
157fn check_pending_budget(
158 state: &PrimaryKeyState,
159 old: usize,
160 new: usize,
161 cancelled: usize,
162) -> Result<()> {
163 let next_len = state.uncommitted.len() + usize::from(new > 0) - usize::from(old > 0);
164 let capacity = if next_len > state.uncommitted.capacity() {
167 (next_len * 2).max(3)
168 } else {
169 state.uncommitted.capacity()
170 };
171 let payload = state.pending_bytes - old + new - next_len * KEY_SLOT_BYTES;
172 let used = payload + capacity * KEY_SLOT_BYTES + state.cancelled_bytes + cancelled;
173 if used > MAX_PENDING_KEY_BYTES {
174 return Err(Error::Document(
175 "pending primary-key metadata exceeds 64 MiB; commit before continuing".into(),
176 ));
177 }
178 Ok(())
179}
180
181fn validate_mutation_key(key: &str) -> Result<()> {
182 if key.is_empty() || key.len() > 64 * 1024 {
183 return Err(Error::Document(
184 "deletion key must contain 1..=65536 bytes".into(),
185 ));
186 }
187 Ok(())
188}
189
190fn check_delete_budget(state: &PrimaryKeyState, key: &str) -> Result<()> {
191 if !state.deletes.contains(key)
192 && (state.deletes.len() >= 100_000 || state.delete_bytes + key.len() > 8 * 1024 * 1024)
193 {
194 return Err(Error::Document(
195 "pending deletions exceed 100000 keys or 8 MiB; commit before continuing".into(),
196 ));
197 }
198 Ok(())
199}
200
201fn stage_delete(state: &mut PrimaryKeyState, key: &str) -> bool {
202 if state.deletes.contains(key) {
203 return false;
204 }
205 state.delete_bytes += key.len();
206 state.deletes.insert(key.to_owned());
207 true
208}
209
210impl PrimaryKeyIndex {
211 #[cfg(feature = "native")]
219 pub fn new(field: Field, pk_data: Vec<PkSegmentData>, snapshot: SegmentSnapshot) -> Self {
220 let mut index = Self::build(field, pk_data);
221 index._snapshot = Some(std::sync::Arc::new(snapshot));
222 index
223 }
224
225 pub(crate) fn build(field: Field, mut pk_data: Vec<PkSegmentData>) -> Self {
226 for data in &mut pk_data {
227 data.prepare_live_keys(field);
228 }
229 let mut total_keys: usize = 0;
231 for data in &pk_data {
232 if let Some(ff) = data.fast_fields.get(&field.0)
233 && let Some(dict) = ff.text_dict()
234 {
235 total_keys += dict.len() as usize;
236 }
237 }
238
239 let mut bloom = BloomFilter::new(total_keys + BLOOM_HEADROOM, BLOOM_BITS_PER_KEY);
240
241 for data in &pk_data {
243 if let Some(ff) = data.fast_fields.get(&field.0)
244 && let Some(dict) = ff.text_dict()
245 {
246 for key in dict.iter() {
247 bloom.insert(key.as_bytes());
248 }
249 }
250 }
251
252 let bloom_bytes = bloom.size_bytes();
253 log::info!(
254 "[primary_key] bloom filter: {} keys, {}",
255 total_keys,
256 crate::format_bytes(bloom_bytes as u64),
257 );
258
259 Self {
260 field,
261 state: parking_lot::Mutex::new(PrimaryKeyState {
262 bloom,
263 uncommitted: FxHashMap::default(),
264 pending_bytes: 0,
265 cancelled_bytes: 0,
266 deletes: FxHashSet::default(),
267 delete_bytes: 0,
268 }),
269 committed_data: pk_data,
270 #[cfg(feature = "native")]
271 _snapshot: None,
272 }
273 }
274
275 #[cfg(feature = "native")]
281 pub fn from_persisted(
282 field: Field,
283 bloom: BloomFilter,
284 mut pk_data: Vec<PkSegmentData>,
285 snapshot: SegmentSnapshot,
286 ) -> Self {
287 for data in &mut pk_data {
288 data.prepare_live_keys(field);
289 }
290 log::info!(
291 "[primary_key] bloom filter loaded from cache: {}",
292 crate::format_bytes(bloom.size_bytes() as u64),
293 );
294
295 Self {
296 field,
297 state: parking_lot::Mutex::new(PrimaryKeyState {
298 bloom,
299 uncommitted: FxHashMap::default(),
300 pending_bytes: 0,
301 cancelled_bytes: 0,
302 deletes: FxHashSet::default(),
303 delete_bytes: 0,
304 }),
305 committed_data: pk_data,
306 _snapshot: Some(std::sync::Arc::new(snapshot)),
307 }
308 }
309
310 pub fn write_bloom_cache(
313 &self,
314 segment_ids: &[String],
315 writer: &mut (impl std::io::Write + ?Sized),
316 ) -> std::io::Result<()> {
317 let state = self.state.lock();
318 write_pk_bloom(writer, segment_ids, &state.bloom)
319 }
320
321 pub fn memory_bytes(&self) -> usize {
323 let state = self.state.lock();
324 state.bloom.size_bytes()
325 + state.pending_bytes
326 + (state.uncommitted.capacity() - state.uncommitted.len()) * KEY_SLOT_BYTES
327 + state.cancelled_bytes
328 + state.delete_bytes
329 + state.deletes.capacity() * std::mem::size_of::<String>()
330 + self
331 .committed_data
332 .iter()
333 .map(|data| {
334 data.content_hash
335 .as_ref()
336 .map_or(0, |lookup| lookup.memory_bytes())
337 + data
338 .alive_docs
339 .as_ref()
340 .map_or(0, |bits| bits.bits.len() * 8)
341 + data
342 .live_key_ordinals
343 .as_ref()
344 .map_or(0, |bits| bits.bits.len() * 8)
345 })
346 .sum::<usize>()
347 }
348
349 pub(super) fn content_hash_target(
351 &self,
352 key: &str,
353 ) -> Result<Option<super::content_hash::ContentHashTarget>> {
354 let state = self.state.lock();
355 if !state.bloom.may_contain(key.as_bytes()) {
356 return Ok(None);
357 }
358 if state.uncommitted.contains_key(key.as_bytes()) {
359 return Ok(None);
360 }
361 if state.deletes.contains(key) {
362 return Ok(None);
363 }
364 drop(state);
365 let mut target = None;
366 for data in &self.committed_data {
367 let column = data
368 .fast_fields
369 .get(&self.field.0)
370 .ok_or_else(|| Error::Corruption("primary-key column is missing".into()))?;
371 if let Some(ordinal) = column.text_ordinal(key)
372 && data
373 .live_key_ordinals
374 .as_ref()
375 .is_none_or(|keys| keys.contains(ordinal as u32))
376 {
377 if target.is_some() {
378 return Err(Error::Corruption(
379 "multiple live segments share a primary key".into(),
380 ));
381 }
382 let lookup = data.content_hash.as_ref().ok_or_else(|| {
383 Error::Internal("content hash lookup was not initialized".into())
384 })?;
385 target = Some(super::content_hash::ContentHashTarget {
386 store: std::sync::Arc::clone(&lookup.store),
387 row: lookup.row(ordinal, column, data)?,
388 #[cfg(feature = "native")]
389 snapshot: self._snapshot.clone(),
390 });
391 }
392 }
393 Ok(target)
394 }
395
396 pub(crate) fn delete(&self, key: &str) -> Result<bool> {
398 validate_mutation_key(key)?;
399 let mut state = self.state.lock();
400 check_delete_budget(&state, key)?;
401 if let Some(old) = state.uncommitted.get(key.as_bytes()) {
402 check_pending_budget(&state, old.bytes, 0, CANCELLED_ROW_BYTES)?;
403 }
404 let staged = stage_delete(&mut state, key);
405 if let Some(old) = state.uncommitted.remove(key.as_bytes()) {
406 state.pending_bytes -= old.bytes;
407 state.cancelled_bytes += CANCELLED_ROW_BYTES;
408 old.row.cancel();
409 }
410 Ok(staged)
411 }
412
413 pub(super) fn staged_hash_matches(&self, key: &str, hash: &FieldValue) -> Option<bool> {
416 self.state
417 .lock()
418 .uncommitted
419 .get(key.as_bytes())
420 .map(|pending| pending.hash.as_ref() == Some(hash))
421 }
422
423 pub(super) fn admit_document(
424 &self,
425 doc: Document,
426 schema: &Schema,
427 replace: bool,
428 accept: impl FnOnce(Document, Arc<StagedRow>) -> Result<()>,
429 ) -> Result<()> {
430 let key = document_key(&doc, self.field)?.to_owned();
431 let hash = super::content_hash::document_hash(&doc, schema)?;
432 let bytes = pending_bytes(&key, hash);
434 let mut state = self.state.lock();
435 self.check_admission(&state, &key, replace, bytes)?;
436 let hash = hash.cloned();
437 let row = Arc::new(StagedRow::default());
438 accept(doc, Arc::clone(&row))?;
439 self.finish_admission(&mut state, key, PendingKey { row, hash, bytes }, replace);
440 Ok(())
441 }
442
443 fn check_admission(
444 &self,
445 state: &PrimaryKeyState,
446 key: &str,
447 replace: bool,
448 bytes: usize,
449 ) -> Result<()> {
450 if replace {
451 check_delete_budget(state, key)?;
452 } else if state.uncommitted.contains_key(key.as_bytes()) {
453 return Err(Error::DuplicatePrimaryKey(key.to_owned()));
454 } else if !state.deletes.contains(key) && state.bloom.may_contain(key.as_bytes()) {
455 for data in &self.committed_data {
456 if let Some(ff) = data.fast_fields.get(&self.field.0)
457 && let Some(ordinal) = ff.text_ordinal(key)
458 && data
459 .live_key_ordinals
460 .as_ref()
461 .is_none_or(|keys| keys.contains(ordinal as u32))
462 {
463 return Err(Error::DuplicatePrimaryKey(key.to_owned()));
464 }
465 }
466 }
467 let old_bytes = state
468 .uncommitted
469 .get(key.as_bytes())
470 .map_or(0, |old| old.bytes);
471 check_pending_budget(
472 state,
473 old_bytes,
474 bytes,
475 if old_bytes > 0 {
476 CANCELLED_ROW_BYTES
477 } else {
478 0
479 },
480 )?;
481 Ok(())
482 }
483
484 fn finish_admission(
485 &self,
486 state: &mut PrimaryKeyState,
487 key: String,
488 pending: PendingKey,
489 replace: bool,
490 ) {
491 if replace {
492 stage_delete(state, &key);
493 }
494 state.bloom.insert(key.as_bytes());
495 state.pending_bytes += pending.bytes;
496 if let Some(old) = state.uncommitted.insert(key.into_bytes(), pending) {
497 state.pending_bytes -= old.bytes;
498 state.cancelled_bytes += CANCELLED_ROW_BYTES;
499 old.row.cancel();
500 }
501 }
502
503 pub(crate) fn mark_deletes_published(&self) {
506 let mut state = self.state.lock();
507 state.deletes.clear();
508 state.delete_bytes = 0;
509 }
510
511 pub(crate) fn pending_deletes(&self) -> Vec<String> {
512 self.state.lock().deletes.iter().cloned().collect()
513 }
514
515 pub fn check_and_insert(&self, doc: &Document) -> Result<()> {
521 let key = document_key(doc, self.field)?;
522 let bytes = pending_bytes(key, None);
523 let mut state = self.state.lock();
524 self.check_admission(&state, key, false, bytes)?;
525 self.finish_admission(
526 &mut state,
527 key.to_owned(),
528 PendingKey {
529 row: Arc::new(StagedRow::default()),
530 hash: None,
531 bytes,
532 },
533 false,
534 );
535 Ok(())
536 }
537
538 #[cfg(feature = "native")]
545 pub fn refresh_incremental(&mut self, new_data: Vec<PkSegmentData>, snapshot: SegmentSnapshot) {
546 self.refresh_data(new_data, snapshot.segment_ids());
547 self._snapshot = Some(std::sync::Arc::new(snapshot));
548 }
549
550 pub(crate) fn refresh_data(&mut self, new_data: Vec<PkSegmentData>, segment_ids: &[String]) {
551 let state = self.state.get_mut();
554 for data in &new_data {
555 if let Some(ff) = data.fast_fields.get(&self.field.0)
556 && let Some(dict) = ff.text_dict()
557 {
558 for key in dict.iter() {
559 state.bloom.insert(key.as_bytes());
560 }
561 }
562 }
563 state.uncommitted.clear();
564 state.pending_bytes = 0;
565 state.cancelled_bytes = 0;
566 state.deletes.clear();
567 state.delete_bytes = 0;
568 self.replace_committed_data(new_data, segment_ids);
569 }
570
571 #[cfg(feature = "native")]
578 pub fn refresh_replacement(&mut self, new_data: Vec<PkSegmentData>, snapshot: SegmentSnapshot) {
579 self.replace_committed_data(new_data, snapshot.segment_ids());
580 self._snapshot = Some(std::sync::Arc::new(snapshot));
581 }
582
583 fn replace_committed_data(&mut self, new_data: Vec<PkSegmentData>, segment_ids: &[String]) {
584 let new_seg_ids: FxHashSet<&str> = segment_ids.iter().map(|s| s.as_str()).collect();
585 let replaced: FxHashSet<&str> = new_data
586 .iter()
587 .map(|data| data.segment_id.as_str())
588 .collect();
589 let mut kept: Vec<PkSegmentData> = self
590 .committed_data
591 .drain(..)
592 .filter(|d| {
593 new_seg_ids.contains(d.segment_id.as_str())
594 && !replaced.contains(d.segment_id.as_str())
595 })
596 .collect();
597 kept.extend(new_data);
598 self.committed_data = kept;
599 }
600
601 #[cfg(all(feature = "wasm", not(feature = "native")))]
602 pub(crate) fn segment_data(&self) -> &[PkSegmentData] {
603 &self.committed_data
604 }
605
606 pub fn committed_segment_ids(&self) -> impl Iterator<Item = &str> {
608 self.committed_data.iter().map(|d| d.segment_id.as_str())
609 }
610
611 pub(crate) fn committed_visibility(
612 &self,
613 ) -> impl Iterator<Item = (&str, Option<&crate::segment::DeletionMeta>)> {
614 self.committed_data
615 .iter()
616 .map(|data| (data.segment_id.as_str(), data.deletion_meta.as_ref()))
617 }
618
619 pub fn rollback_uncommitted_key(&self, doc: &crate::dsl::Document) {
623 if let Some(value) = doc.get_first(self.field)
624 && let Some(key) = value.as_text()
625 {
626 let mut state = self.state.lock();
627 if let Some(old) = state.uncommitted.remove(key.as_bytes()) {
628 state.pending_bytes -= old.bytes;
629 state.cancelled_bytes += CANCELLED_ROW_BYTES;
630 old.row.cancel();
631 }
632 }
633 }
634
635 pub fn clear_uncommitted(&mut self) {
639 let state = self.state.get_mut();
640 state.uncommitted.clear();
641 state.pending_bytes = 0;
642 state.cancelled_bytes = 0;
643 state.deletes.clear();
644 state.delete_bytes = 0;
645 }
646}
647
648fn write_pk_bloom(
652 writer: &mut (impl std::io::Write + ?Sized),
653 segment_ids: &[String],
654 bloom: &BloomFilter,
655) -> std::io::Result<()> {
656 writer.write_u32::<LittleEndian>(PK_BLOOM_MAGIC)?;
657 writer.write_u32::<LittleEndian>(u32::try_from(segment_ids.len()).map_err(|_| {
658 std::io::Error::new(
659 std::io::ErrorKind::InvalidInput,
660 "primary-key bloom segment count exceeds u32::MAX",
661 )
662 })?)?;
663 for seg_id in segment_ids {
664 let bytes = seg_id.as_bytes();
665 if bytes.len() > 32 {
666 return Err(std::io::Error::new(
667 std::io::ErrorKind::InvalidInput,
668 "primary-key bloom segment ID exceeds 32 bytes",
669 ));
670 }
671 writer.write_all(bytes)?;
672 writer.write_all(&[0u8; 32][..32 - bytes.len()])?;
674 }
675 bloom.write_to(writer)
676}
677
678#[cfg(feature = "native")]
681pub fn deserialize_pk_bloom(data: &[u8]) -> Option<(HashSet<String>, BloomFilter)> {
682 if data.len() < 8 {
683 return None;
684 }
685 let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
686 if magic != PK_BLOOM_MAGIC {
687 return None;
688 }
689 let num_segments = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
690 let header_end = 8 + num_segments * 32;
691 if data.len() < header_end + BloomFilter::SERIALIZED_HEADER_SIZE {
692 return None;
693 }
694 let mut segment_ids = HashSet::with_capacity(num_segments);
695 for i in 0..num_segments {
696 let start = 8 + i * 32;
697 let raw = &data[start..start + 32];
698 let end = raw.iter().position(|&b| b == 0).unwrap_or(32);
699 let hex = std::str::from_utf8(&raw[..end]).ok()?;
700 segment_ids.insert(hex.to_string());
701 }
702 let bloom = BloomFilter::from_bytes_mutable(&data[header_end..]).ok()?;
703 Some((segment_ids, bloom))
704}
705
706#[cfg(all(test, feature = "native"))]
707mod tests {
708 use std::sync::Arc;
709
710 use super::*;
711 use crate::dsl::{Document, Field};
712 use crate::segment::SegmentTracker;
713
714 fn make_doc(field: Field, key: &str) -> Document {
715 let mut doc = Document::new();
716 doc.add_text(field, key);
717 doc
718 }
719
720 fn empty_snapshot() -> SegmentSnapshot {
721 SegmentSnapshot::new(Arc::new(SegmentTracker::new()), vec![])
722 }
723
724 #[test]
725 fn test_new_empty_readers() {
726 let field = Field(0);
727 let pk = PrimaryKeyIndex::new(field, vec![], empty_snapshot());
728 let doc = make_doc(field, "key1");
730 assert!(pk.check_and_insert(&doc).is_ok());
731 }
732
733 #[test]
734 fn test_unique_keys_accepted() {
735 let field = Field(0);
736 let pk = PrimaryKeyIndex::new(field, vec![], empty_snapshot());
737
738 assert!(pk.check_and_insert(&make_doc(field, "a")).is_ok());
739 assert!(pk.check_and_insert(&make_doc(field, "b")).is_ok());
740 assert!(pk.check_and_insert(&make_doc(field, "c")).is_ok());
741 }
742
743 #[test]
744 fn test_duplicate_uncommitted_rejected() {
745 let field = Field(0);
746 let pk = PrimaryKeyIndex::new(field, vec![], empty_snapshot());
747
748 assert!(pk.check_and_insert(&make_doc(field, "key1")).is_ok());
749 let result = pk.check_and_insert(&make_doc(field, "key1"));
750 assert!(result.is_err());
751 match result.unwrap_err() {
752 Error::DuplicatePrimaryKey(k) => assert_eq!(k, "key1"),
753 other => panic!("Expected DuplicatePrimaryKey, got {:?}", other),
754 }
755 }
756
757 #[test]
758 fn test_missing_field_rejected() {
759 let field = Field(0);
760 let other_field = Field(1);
761 let pk = PrimaryKeyIndex::new(field, vec![], empty_snapshot());
762
763 let doc = make_doc(other_field, "value");
765 let result = pk.check_and_insert(&doc);
766 assert!(result.is_err());
767 match result.unwrap_err() {
768 Error::Document(msg) => assert!(msg.contains("Missing"), "{}", msg),
769 other => panic!("Expected Document error, got {:?}", other),
770 }
771 }
772
773 #[test]
774 fn test_empty_key_rejected() {
775 let field = Field(0);
776 let pk = PrimaryKeyIndex::new(field, vec![], empty_snapshot());
777
778 let result = pk.check_and_insert(&make_doc(field, ""));
779 assert!(result.is_err());
780 match result.unwrap_err() {
781 Error::Document(msg) => assert!(msg.contains("empty"), "{}", msg),
782 other => panic!("Expected Document error, got {:?}", other),
783 }
784 }
785
786 #[test]
787 fn test_clear_uncommitted() {
788 let field = Field(0);
789 let mut pk = PrimaryKeyIndex::new(field, vec![], empty_snapshot());
790
791 assert!(pk.check_and_insert(&make_doc(field, "key1")).is_ok());
793 assert!(pk.check_and_insert(&make_doc(field, "key1")).is_err());
795
796 pk.clear_uncommitted();
798
799 assert!(pk.check_and_insert(&make_doc(field, "key1")).is_ok());
803 }
804
805 #[test]
806 fn test_many_unique_keys() {
807 let field = Field(0);
808 let pk = PrimaryKeyIndex::new(field, vec![], empty_snapshot());
809
810 for i in 0..1000 {
811 let key = format!("key_{}", i);
812 assert!(pk.check_and_insert(&make_doc(field, &key)).is_ok());
813 }
814
815 for i in 0..1000 {
817 let key = format!("key_{}", i);
818 assert!(pk.check_and_insert(&make_doc(field, &key)).is_err());
819 }
820 }
821
822 #[test]
823 fn test_refresh_clears_uncommitted() {
824 let field = Field(0);
825 let mut pk = PrimaryKeyIndex::new(field, vec![], empty_snapshot());
826
827 assert!(pk.check_and_insert(&make_doc(field, "key1")).is_ok());
828 assert!(pk.check_and_insert(&make_doc(field, "key1")).is_err());
829
830 pk.refresh_incremental(vec![], empty_snapshot());
833
834 assert!(pk.check_and_insert(&make_doc(field, "key1")).is_ok());
837 }
838
839 #[test]
840 fn replacement_refresh_preserves_uncommitted_reservations() {
841 let field = Field(0);
842 let mut pk = PrimaryKeyIndex::new(field, vec![], empty_snapshot());
843
844 assert!(pk.check_and_insert(&make_doc(field, "queued")).is_ok());
845 pk.refresh_replacement(vec![], empty_snapshot());
846
847 assert!(
848 pk.check_and_insert(&make_doc(field, "queued")).is_err(),
849 "topology-only BP refresh must not erase a queued key reservation"
850 );
851 }
852
853 #[test]
854 fn test_pk_bloom_serialize_roundtrip() {
855 let field = Field(0);
856 let pk = PrimaryKeyIndex::new(field, vec![], empty_snapshot());
857 for i in 0..100 {
858 pk.check_and_insert(&make_doc(field, &format!("key_{}", i)))
859 .unwrap();
860 }
861
862 let seg_ids = vec![
863 "00000000000000000000000000000001".to_string(),
864 "00000000000000000000000000000002".to_string(),
865 ];
866 let mut data = Vec::new();
867 pk.write_bloom_cache(&seg_ids, &mut data).unwrap();
868 let (got_ids, got_bloom) = deserialize_pk_bloom(&data).expect("deserialize failed");
869
870 assert_eq!(got_ids.len(), 2);
871 assert!(got_ids.contains(&seg_ids[0]));
872 assert!(got_ids.contains(&seg_ids[1]));
873
874 for i in 0..100 {
876 let key = format!("key_{}", i);
877 assert!(
878 got_bloom.may_contain(key.as_bytes()),
879 "bloom miss for {}",
880 key
881 );
882 }
883 }
884
885 #[test]
886 fn test_pk_bloom_deserialize_bad_data() {
887 assert!(deserialize_pk_bloom(&[]).is_none());
888 assert!(deserialize_pk_bloom(&[0; 7]).is_none());
889 assert!(deserialize_pk_bloom(&[0; 8]).is_none()); }
891
892 #[test]
893 fn test_concurrent_access() {
894 use std::sync::Arc;
895
896 let field = Field(0);
897 let pk = Arc::new(PrimaryKeyIndex::new(field, vec![], empty_snapshot()));
898
899 let mut handles = vec![];
901 for _ in 0..10 {
902 let pk = Arc::clone(&pk);
903 handles.push(std::thread::spawn(move || {
904 pk.check_and_insert(&make_doc(field, "contested_key"))
905 }));
906 }
907
908 let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
909 let successes = results.iter().filter(|r| r.is_ok()).count();
910 let failures = results.iter().filter(|r| r.is_err()).count();
911
912 assert_eq!(successes, 1, "Exactly one insert should succeed");
914 assert_eq!(failures, 9, "Rest should fail with duplicate");
915 }
916}
917
918pub(crate) async fn load_pk_segment_data<D: crate::directories::Directory>(
920 dir: &D,
921 seg_id_str: &str,
922 schema: &crate::dsl::Schema,
923 deletion: Option<(u32, crate::segment::DeletionMeta)>,
924) -> Result<PkSegmentData> {
925 let seg_id = crate::segment::SegmentId::from_hex(seg_id_str)
926 .ok_or_else(|| Error::Internal(format!("Invalid segment id: {}", seg_id_str)))?;
927 let files = crate::segment::SegmentFiles::new(seg_id.0);
928 let fast_fields =
929 crate::segment::reader::loader::load_fast_fields_file(dir, &files, schema).await?;
930 let (deletion_meta, alive_docs) = match deletion {
931 Some((num_docs, meta)) => {
932 let alive = meta.load(dir, num_docs).await?;
933 (Some(meta), Some(alive))
934 }
935 None => (None, None),
936 };
937 let data = PkSegmentData {
938 deletion_meta,
939 alive_docs,
940 live_key_ordinals: None,
941 segment_id: seg_id_str.to_string(),
942 content_hash: None,
943 fast_fields,
944 };
945 if schema.content_hash_field().is_none() {
946 return Ok(data);
947 }
948 #[cfg(feature = "native")]
949 let cache = super::shared_store_cache(32 * 1024 * 1024);
950 #[cfg(not(feature = "native"))]
951 let cache = std::sync::Arc::new(crate::segment::SharedStoreCache::new(0));
952 let store = std::sync::Arc::new(
953 crate::segment::AsyncStoreReader::open(
954 dir.open_lazy(&files.store).await?,
955 dir as *const D as usize,
956 seg_id.0,
957 cache,
958 )
959 .await?,
960 );
961 let field = schema
962 .primary_field()
963 .ok_or_else(|| Error::Schema("content_hash requires a primary key".into()))?;
964 let prepare = move || {
965 let mut data = data;
966 let column = data
967 .fast_fields
968 .get(&field.0)
969 .ok_or_else(|| Error::Corruption("primary-key column is missing".into()))?;
970 let lookup = super::content_hash::ContentHashLookup::new(store, column, &data)?;
971 data.content_hash = Some(lookup);
972 Ok(data)
973 };
974 #[cfg(feature = "native")]
975 {
976 tokio::task::spawn_blocking(prepare)
977 .await
978 .map_err(|error| {
979 Error::Internal(format!("content hash lookup preparation failed: {error}"))
980 })?
981 }
982 #[cfg(not(feature = "native"))]
983 {
984 prepare()
985 }
986}