Skip to main content

zeppelin_embed/lifecycle/
mod.rs

1//! Store lifecycle and memory accounting.
2
3mod budget;
4mod cancel;
5mod cascade;
6mod clock;
7mod close;
8pub mod durability;
9mod expansion;
10pub(crate) mod graph_cache;
11mod group_count;
12pub(crate) mod hash_table;
13mod hybrid;
14pub mod lock;
15pub(crate) mod materialize;
16pub(crate) mod namespace_batch;
17pub use cascade::CascadeRule;
18#[cfg(any(test, feature = "test-seams"))]
19pub use namespace_batch::namespace_delete_cascade_with_steps;
20pub use namespace_batch::{namespace_declare_cascade, namespace_delete_cascade};
21#[cfg(feature = "graph-cypher")]
22mod enable_graph;
23#[cfg(feature = "graph-cypher")]
24pub(crate) mod native_graph;
25#[cfg(feature = "graph-cypher")]
26mod pinned_search;
27mod pool;
28pub(crate) mod prepared;
29mod prepared_lexical;
30mod reindex;
31pub(crate) mod rescored_scan;
32mod snapshot;
33mod snapshot_copy;
34mod snapshot_view;
35pub use namespace_batch::{
36    LiveNamespaceMutation, NamespaceMutation, NamespaceRootId, namespace_batch,
37    namespace_batch_live, namespace_relocate,
38};
39#[cfg(any(test, feature = "test-seams"))]
40#[doc(hidden)]
41pub use namespace_batch::{
42    namespace_batch_live_on_vfs, namespace_batch_live_with_steps, namespace_batch_with_steps,
43    namespace_reclaim, namespace_reclaim_on_vfs, namespace_relocate_with_steps,
44};
45pub(crate) mod stats;
46
47use crate::diag::{timing_elapsed, timing_start};
48pub use cancel::{
49    CancelToken, Deadline, DeadlineError, QueryCancellation, QueryControl, QueryError,
50};
51#[cfg(any(test, feature = "test-seams"))]
52pub use clock::ManualMonotonicClock;
53pub use clock::{MonotonicClock, SystemMonotonicClock};
54#[cfg(any(test, feature = "test-seams"))]
55pub use hybrid::{
56    HybridScoreTestObservations, begin_hybrid_fresh_round_test_observations,
57    begin_hybrid_score_test_observations, take_hybrid_score_test_observations,
58};
59#[cfg(any(test, feature = "test-seams"))]
60pub use materialize::MaterializationTestCounters;
61pub use materialize::{
62    HybridPreparationError, MaterializationError, MaterializedRow, QueryMaterializer,
63};
64
65pub use group_count::{
66    DocumentGroup, DocumentGroupCounts, DocumentGroupValue, MAX_DOCUMENT_GROUP_LIMIT,
67};
68pub use rescored_scan::ScanRescoreOptions;
69pub use snapshot::{
70    InMemorySegment, InMemorySegmentFactors, PreparedSegment, PublishedSnapshot, SnapshotLease,
71};
72pub use snapshot_copy::SnapshotTargetReason;
73pub use stats::Stats;
74
75use std::collections::HashSet;
76#[cfg(any(test, feature = "test-seams"))]
77use std::io::IoSlice;
78use std::path::{Path, PathBuf};
79use std::sync::atomic::{AtomicU64, Ordering};
80use std::sync::{Arc, Condvar, Mutex, RwLock};
81use std::time::Duration;
82
83use durability::{
84    CommitTier, DurabilityMode, DurabilityPolicy, DurabilityPolicyError, SyncRequirement,
85};
86use lock::{StoreLock, StoreLockError};
87
88use self::close::BackgroundThread;
89
90/// Store-owned infrastructure injected only by deterministic tests.
91#[cfg(any(test, feature = "test-seams"))]
92#[derive(Clone)]
93pub struct StoreTestDependencies {
94    pub(crate) vfs: Arc<dyn crate::vfs::Vfs>,
95    pub(crate) clock: Arc<dyn MonotonicClock>,
96    hybrid_leg_fault: Option<HybridLegTestFault>,
97    storage_fault_controller: Option<StorageFaultController>,
98    ingest_retention_fault_controller: Option<crate::ingest::IngestRetentionFaultController>,
99    metadata_test_controller: Option<Arc<crate::planner::MetadataTestController>>,
100    pub(crate) vector_fault_controller: Option<crate::scan::vector_fault::VectorFaultController>,
101    pub(crate) kernel_fault_controller: Option<crate::kernels::vector_fault::KernelFaultController>,
102    vector_seal_scheme: Option<crate::quant::QuantScheme>,
103}
104
105#[cfg(any(test, feature = "test-seams"))]
106impl StoreTestDependencies {
107    /// Binds one filesystem and monotonic clock to every operation performed
108    /// by a test store handle.
109    #[must_use]
110    pub fn new(vfs: Arc<dyn crate::vfs::Vfs>, clock: Arc<dyn MonotonicClock>) -> Self {
111        Self {
112            vfs,
113            clock,
114            hybrid_leg_fault: None,
115            storage_fault_controller: None,
116            ingest_retention_fault_controller: None,
117            metadata_test_controller: None,
118            vector_fault_controller: None,
119            kernel_fault_controller: None,
120            vector_seal_scheme: None,
121        }
122    }
123
124    /// Arms one Store-owned hybrid fault; it is consumed exactly once.
125    #[must_use]
126    pub const fn with_hybrid_leg_fault(mut self, fault: HybridLegTestFault) -> Self {
127        self.hybrid_leg_fault = Some(fault);
128        self
129    }
130
131    /// Arms one storage-family fault controller at Store-owned production seams.
132    #[must_use]
133    pub fn with_storage_fault_controller(mut self, controller: StorageFaultController) -> Self {
134        self.storage_fault_controller = Some(controller);
135        self
136    }
137
138    /// Arms one ingest-retention fault at a Store-owned mutation checkpoint.
139    #[must_use]
140    pub fn with_ingest_retention_fault_controller(
141        mut self,
142        controller: crate::ingest::IngestRetentionFaultController,
143    ) -> Self {
144        self.ingest_retention_fault_controller = Some(controller);
145        self
146    }
147
148    /// Binds one metadata/planner observation and fault controller to the
149    /// public Store query path.
150    #[must_use]
151    pub fn with_metadata_test_controller(
152        mut self,
153        controller: Arc<crate::planner::MetadataTestController>,
154    ) -> Self {
155        self.metadata_test_controller = Some(controller);
156        self
157    }
158
159    /// Arms one vector execution fault at the Store-owned query path.
160    #[must_use]
161    pub fn with_vector_fault_controller(
162        mut self,
163        controller: crate::scan::vector_fault::VectorFaultController,
164    ) -> Self {
165        self.vector_fault_controller = Some(controller);
166        self
167    }
168
169    /// Forces and observes one concrete kernel backend through Store search.
170    #[must_use]
171    pub fn with_kernel_fault_controller(
172        mut self,
173        controller: crate::kernels::vector_fault::KernelFaultController,
174    ) -> Self {
175        self.kernel_fault_controller = Some(controller);
176        self
177    }
178
179    /// Selects the immutable vector codec used by the next public Store seal.
180    ///
181    /// This test-support-only seam leaves public ingest and shipping defaults
182    /// unchanged while exercising document-preserving Int8 publication.
183    #[must_use]
184    pub const fn with_vector_seal_scheme(mut self, scheme: crate::quant::QuantScheme) -> Self {
185        self.vector_seal_scheme = Some(scheme);
186        self
187    }
188}
189
190/// Narrow storage-family faults available only through hidden test dependencies.
191#[cfg(any(test, feature = "test-seams"))]
192#[derive(Clone, Debug, Eq, PartialEq)]
193#[doc(hidden)]
194pub enum StorageTestFault {
195    TornWalHeader,
196    TornWalBody,
197    TornWalChecksum,
198    PostCommitError,
199    ManifestPreRename,
200    ManifestPostRename,
201    CorruptSegmentRegion,
202    WrongManifestObject,
203    WrongSegmentObject,
204    ListOmission { file_name: String },
205    DeleteOmission { file_name: String },
206}
207
208#[cfg(any(test, feature = "test-seams"))]
209impl StorageTestFault {
210    const fn operation(&self) -> &'static str {
211        match self {
212            Self::TornWalHeader | Self::TornWalBody | Self::TornWalChecksum => "wal-prefix",
213            Self::PostCommitError => "retry",
214            Self::ManifestPreRename | Self::ManifestPostRename => "publication",
215            Self::CorruptSegmentRegion | Self::WrongManifestObject | Self::WrongSegmentObject => {
216                "format-check"
217            }
218            Self::ListOmission { .. } | Self::DeleteOmission { .. } => "orphan-cleanup",
219        }
220    }
221
222    const fn key(&self) -> &'static str {
223        match self {
224            Self::TornWalHeader => "torn-wal-header",
225            Self::TornWalBody => "torn-wal-body",
226            Self::TornWalChecksum => "torn-wal-checksum",
227            Self::PostCommitError => "post-commit-error",
228            Self::ManifestPreRename => "manifest-pre-rename-crash",
229            Self::ManifestPostRename => "manifest-post-rename-crash",
230            Self::CorruptSegmentRegion => "corrupt-segment-region",
231            Self::WrongManifestObject => "wrong-manifest-object",
232            Self::WrongSegmentObject => "wrong-segment-object",
233            Self::ListOmission { .. } | Self::DeleteOmission { .. } => "list-delete-omission",
234        }
235    }
236}
237
238/// Exact seed-derived target facts supplied before one storage operation runs.
239#[cfg(any(test, feature = "test-seams"))]
240#[derive(Clone, Debug, Eq, PartialEq)]
241#[doc(hidden)]
242pub struct StorageFaultPlan {
243    op_index: u32,
244    artifact: String,
245    offset: Option<u64>,
246    segment: Option<crate::segment::SegmentId>,
247    region_kind: Option<u16>,
248    chunk: Option<u32>,
249}
250
251#[cfg(any(test, feature = "test-seams"))]
252impl StorageFaultPlan {
253    /// Creates an exact artifact plan for one operation index.
254    #[must_use]
255    pub fn new(op_index: u32, artifact: impl Into<String>) -> Self {
256        Self {
257            op_index,
258            artifact: artifact.into(),
259            offset: None,
260            segment: None,
261            region_kind: None,
262            chunk: None,
263        }
264    }
265
266    /// Pins the exact byte offset selected by the primitive fixture.
267    #[must_use]
268    pub const fn with_offset(mut self, offset: u64) -> Self {
269        self.offset = Some(offset);
270        self
271    }
272
273    /// Pins the exact segment, region kind, and checksum chunk.
274    #[must_use]
275    pub const fn with_segment_region(
276        mut self,
277        segment: crate::segment::SegmentId,
278        region_kind: u16,
279        chunk: u32,
280    ) -> Self {
281        self.segment = Some(segment);
282        self.region_kind = Some(region_kind);
283        self.chunk = Some(chunk);
284        self
285    }
286
287    /// Pins the exact newly published segment for a manifest checkpoint.
288    #[must_use]
289    pub const fn with_segment(mut self, segment: crate::segment::SegmentId) -> Self {
290        self.segment = Some(segment);
291        self
292    }
293
294    /// Exact operation index in the generated program.
295    #[must_use]
296    pub const fn op_index(&self) -> u32 {
297        self.op_index
298    }
299
300    /// Normalized planned artifact.
301    #[must_use]
302    pub fn artifact(&self) -> &str {
303        &self.artifact
304    }
305
306    /// Planned mutation/checkpoint byte offset when applicable.
307    #[must_use]
308    pub const fn offset(&self) -> Option<u64> {
309        self.offset
310    }
311
312    /// Planned segment identity when applicable.
313    #[must_use]
314    pub const fn segment(&self) -> Option<crate::segment::SegmentId> {
315        self.segment
316    }
317
318    /// Planned region kind when applicable.
319    #[must_use]
320    pub const fn region_kind(&self) -> Option<u16> {
321        self.region_kind
322    }
323
324    /// Planned checksum chunk when applicable.
325    #[must_use]
326    pub const fn chunk(&self) -> Option<u32> {
327        self.chunk
328    }
329}
330
331/// Typed production checkpoint that consumed a storage fault.
332#[cfg(any(test, feature = "test-seams"))]
333#[derive(Clone, Copy, Debug, Eq, PartialEq)]
334#[doc(hidden)]
335pub enum StorageReceiptSite {
336    WalOpenHeaderValidation,
337    WalOpenRecordValidation,
338    WalOpenRecordChecksum,
339    WalCommitAppendAfterInnerSuccess,
340    ManifestCommitBeforeRename,
341    ManifestCommitAfterRename,
342    SegmentReadRegionChecksum,
343    ManifestOpenFamilyValidation,
344    SegmentOpenFamilyValidation,
345    SegmentOpenObjectIdentity,
346    OrphanCleanupList,
347    OrphanCleanupDelete,
348}
349
350#[cfg(any(test, feature = "test-seams"))]
351impl StorageReceiptSite {
352    const fn key(self) -> &'static str {
353        match self {
354            Self::WalOpenHeaderValidation => "WalOpen.HeaderValidation",
355            Self::WalOpenRecordValidation => "WalOpen.RecordValidation",
356            Self::WalOpenRecordChecksum => "WalOpen.RecordChecksum",
357            Self::WalCommitAppendAfterInnerSuccess => "WalCommit.AppendAfterInnerSuccess",
358            Self::ManifestCommitBeforeRename => "ManifestCommit.BeforeRename",
359            Self::ManifestCommitAfterRename => "ManifestCommit.AfterRename",
360            Self::SegmentReadRegionChecksum => "SegmentRead.RegionChecksum",
361            Self::ManifestOpenFamilyValidation => "ManifestOpen.FamilyValidation",
362            Self::SegmentOpenFamilyValidation => "SegmentOpen.FamilyValidation",
363            Self::SegmentOpenObjectIdentity => "SegmentOpen.ObjectIdentity",
364            Self::OrphanCleanupList => "OrphanCleanup.List",
365            Self::OrphanCleanupDelete => "OrphanCleanup.Delete",
366        }
367    }
368}
369
370/// Exact facts observed at the production checkpoint.
371#[cfg(any(test, feature = "test-seams"))]
372#[derive(Clone, Debug, Eq, PartialEq)]
373#[doc(hidden)]
374pub enum StorageReceiptObserved {
375    WalHeader {
376        artifact: String,
377        reason: crate::wal::header::WalHeaderError,
378    },
379    WalRecord {
380        artifact: String,
381        offset: u64,
382        reason: crate::wal::replay::CorruptionReason,
383    },
384    WalAppend {
385        artifact: String,
386        encoded_len: u64,
387        first_seq: u64,
388        last_seq: u64,
389        inner_append_completed: bool,
390        caller_saw_error: bool,
391    },
392    ManifestRename {
393        temporary: String,
394        committed: String,
395        rename_performed: bool,
396        new_segment_final: bool,
397        directory_sync_returned: bool,
398    },
399    SegmentChecksum {
400        artifact: String,
401        segment: crate::segment::SegmentId,
402        region_kind: u16,
403        chunk: u32,
404        expected_checksum: u64,
405        actual_checksum: u64,
406    },
407    Format {
408        artifact: String,
409        check: crate::format::frame::FormatCheck,
410        expected_family: Option<u16>,
411        actual_family: Option<u16>,
412        expected_id: Option<crate::segment::SegmentId>,
413        actual_id: Option<crate::segment::SegmentId>,
414    },
415    Omission {
416        artifact: String,
417        deletion_observed: bool,
418    },
419}
420
421/// One fact-only typed storage receipt emitted by product code.
422#[cfg(any(test, feature = "test-seams"))]
423#[derive(Clone, Debug, Eq, PartialEq)]
424#[doc(hidden)]
425pub struct StorageFaultReceipt {
426    operation: &'static str,
427    fault: &'static str,
428    site: StorageReceiptSite,
429    plan: StorageFaultPlan,
430    observed: StorageReceiptObserved,
431    cardinality: u32,
432}
433
434#[cfg(any(test, feature = "test-seams"))]
435impl StorageFaultReceipt {
436    #[must_use]
437    pub const fn campaign(&self) -> &'static str {
438        "storage-durability"
439    }
440
441    #[must_use]
442    pub const fn operation(&self) -> &'static str {
443        self.operation
444    }
445
446    #[must_use]
447    pub const fn fault(&self) -> &'static str {
448        self.fault
449    }
450
451    #[must_use]
452    pub const fn site(&self) -> &'static str {
453        self.site.key()
454    }
455
456    #[must_use]
457    pub const fn typed_site(&self) -> StorageReceiptSite {
458        self.site
459    }
460
461    #[must_use]
462    pub const fn cardinality(&self) -> u32 {
463        self.cardinality
464    }
465
466    #[must_use]
467    pub const fn plan(&self) -> &StorageFaultPlan {
468        &self.plan
469    }
470
471    #[must_use]
472    pub const fn observed(&self) -> &StorageReceiptObserved {
473        &self.observed
474    }
475
476    /// Decodes the exact receipt acknowledged by an aborting publication child.
477    ///
478    /// The child writes this only after `emit` has stored the typed production
479    /// receipt. Parsing remains in product test support so an external harness
480    /// cannot manufacture a generic receipt from partial fields.
481    pub fn from_manifest_abort_acknowledgment(acknowledgment: &str) -> Result<Self, String> {
482        let body = acknowledgment
483            .strip_suffix('\n')
484            .filter(|body| !body.contains('\n'))
485            .ok_or_else(|| {
486                "manifest abort acknowledgment must end in exactly one newline".to_owned()
487            })?;
488        let fields = body
489            .split('|')
490            .map(|field| {
491                field.split_once('=').ok_or_else(|| {
492                    format!("manifest abort acknowledgment field lacks '=': {field}")
493                })
494            })
495            .collect::<Result<Vec<_>, _>>()?;
496        let expected_keys = [
497            "campaign",
498            "operation",
499            "fault",
500            "site",
501            "op_index",
502            "cardinality",
503            "artifact",
504            "temporary",
505            "committed",
506            "rename_performed",
507            "new_segment_final",
508            "directory_sync_returned",
509        ];
510        if fields.len() != expected_keys.len()
511            || fields
512                .iter()
513                .map(|(key, _)| *key)
514                .ne(expected_keys.iter().copied())
515        {
516            return Err("manifest abort acknowledgment field inventory differs".to_owned());
517        }
518        let mut values = fields.into_iter().map(|(_, value)| value);
519        let campaign = values
520            .next()
521            .ok_or_else(|| "manifest abort acknowledgment lacks campaign".to_owned())?;
522        let operation = values
523            .next()
524            .ok_or_else(|| "manifest abort acknowledgment lacks operation".to_owned())?;
525        let fault = values
526            .next()
527            .ok_or_else(|| "manifest abort acknowledgment lacks fault".to_owned())?;
528        let site = values
529            .next()
530            .ok_or_else(|| "manifest abort acknowledgment lacks site".to_owned())?;
531        let op_index = values
532            .next()
533            .ok_or_else(|| "manifest abort acknowledgment lacks op index".to_owned())?
534            .parse::<u32>()
535            .map_err(|error| format!("manifest abort op index is invalid: {error}"))?;
536        let cardinality = values
537            .next()
538            .ok_or_else(|| "manifest abort acknowledgment lacks cardinality".to_owned())?
539            .parse::<u32>()
540            .map_err(|error| format!("manifest abort cardinality is invalid: {error}"))?;
541        let artifact = values
542            .next()
543            .ok_or_else(|| "manifest abort acknowledgment lacks artifact".to_owned())?;
544        let temporary = values
545            .next()
546            .ok_or_else(|| "manifest abort acknowledgment lacks temporary path".to_owned())?;
547        let committed = values
548            .next()
549            .ok_or_else(|| "manifest abort acknowledgment lacks committed path".to_owned())?;
550        let parse_bool = |field: &str, value: &str| match value {
551            "true" => Ok(true),
552            "false" => Ok(false),
553            _ => Err(format!("manifest abort {field} is not a bool: {value}")),
554        };
555        let rename_performed = parse_bool(
556            "rename_performed",
557            values
558                .next()
559                .ok_or_else(|| "manifest abort acknowledgment lacks rename result".to_owned())?,
560        )?;
561        let new_segment_final = parse_bool(
562            "new_segment_final",
563            values.next().ok_or_else(|| {
564                "manifest abort acknowledgment lacks final-segment result".to_owned()
565            })?,
566        )?;
567        let directory_sync_returned = parse_bool(
568            "directory_sync_returned",
569            values.next().ok_or_else(|| {
570                "manifest abort acknowledgment lacks directory-sync result".to_owned()
571            })?,
572        )?;
573        if campaign != "storage-durability"
574            || operation != "publication"
575            || cardinality != 1
576            || artifact != crate::manifest::io::MANIFEST_FILE
577            || temporary != crate::manifest::io::MANIFEST_TEMP_FILE
578            || committed != crate::manifest::io::MANIFEST_FILE
579            || !new_segment_final
580            || directory_sync_returned
581        {
582            return Err("manifest abort acknowledgment fixed facts differ".to_owned());
583        }
584        let (fault, site, expected_rename) = match (fault, site) {
585            ("manifest-pre-rename-crash", "ManifestCommit.BeforeRename") => (
586                "manifest-pre-rename-crash",
587                StorageReceiptSite::ManifestCommitBeforeRename,
588                false,
589            ),
590            ("manifest-post-rename-crash", "ManifestCommit.AfterRename") => (
591                "manifest-post-rename-crash",
592                StorageReceiptSite::ManifestCommitAfterRename,
593                true,
594            ),
595            _ => return Err("manifest abort fault and production site differ".to_owned()),
596        };
597        if rename_performed != expected_rename {
598            return Err("manifest abort rename result differs from production site".to_owned());
599        }
600        Ok(Self {
601            operation: "publication",
602            fault,
603            site,
604            plan: StorageFaultPlan::new(op_index, artifact),
605            observed: StorageReceiptObserved::ManifestRename {
606                temporary: temporary.to_owned(),
607                committed: committed.to_owned(),
608                rename_performed,
609                new_segment_final,
610                directory_sync_returned,
611            },
612            cardinality,
613        })
614    }
615}
616
617#[cfg(any(test, feature = "test-seams"))]
618#[derive(Debug)]
619struct StorageFaultState {
620    fault: StorageTestFault,
621    plan: StorageFaultPlan,
622    consumed: bool,
623    receipt: Option<StorageFaultReceipt>,
624    cleanup_report: Option<StorageCleanupReport>,
625    #[cfg(unix)]
626    child_abort_ack: Option<std::os::unix::net::UnixStream>,
627}
628
629/// Truthful internal result of Store-owned orphan cleanup.
630#[cfg(any(test, feature = "test-seams"))]
631#[derive(Clone, Debug, Eq, PartialEq)]
632#[doc(hidden)]
633pub struct StorageCleanupReport {
634    reclaimed_bytes: u64,
635    deleted_paths: Vec<String>,
636    retained_eligible_paths: Vec<String>,
637    directory_synced: bool,
638}
639
640#[cfg(any(test, feature = "test-seams"))]
641impl StorageCleanupReport {
642    /// Exact bytes removed from the filesystem.
643    #[must_use]
644    pub const fn reclaimed_bytes(&self) -> u64 {
645        self.reclaimed_bytes
646    }
647
648    /// Normalized relative paths confirmed absent after deletion.
649    #[must_use]
650    pub fn deleted_paths(&self) -> &[String] {
651        &self.deleted_paths
652    }
653
654    /// Eligible paths whose deletion was omitted by an armed test fault.
655    #[must_use]
656    pub fn retained_eligible_paths(&self) -> &[String] {
657        &self.retained_eligible_paths
658    }
659
660    /// Whether the cleanup issued its required directory synchronization.
661    #[must_use]
662    pub const fn directory_synced(&self) -> bool {
663        self.directory_synced
664    }
665}
666
667/// Shared handle that arms one storage fault and receives its production receipt.
668#[cfg(any(test, feature = "test-seams"))]
669#[derive(Clone, Debug)]
670#[doc(hidden)]
671pub struct StorageFaultController {
672    state: Arc<Mutex<StorageFaultState>>,
673}
674
675#[cfg(any(test, feature = "test-seams"))]
676impl StorageFaultController {
677    /// Arms one controller. A clone observes the same one-shot receipt.
678    #[must_use]
679    pub fn new(fault: StorageTestFault, plan: StorageFaultPlan) -> Self {
680        Self {
681            state: Arc::new(Mutex::new(StorageFaultState {
682                fault,
683                plan,
684                consumed: false,
685                receipt: None,
686                cleanup_report: None,
687                #[cfg(unix)]
688                child_abort_ack: None,
689            })),
690        }
691    }
692
693    /// Supplies the inherited descriptor used to acknowledge a child abort.
694    #[cfg(unix)]
695    #[must_use]
696    pub fn with_child_abort_ack(self, acknowledgment: std::os::unix::net::UnixStream) -> Self {
697        if let Ok(mut state) = self.state.lock() {
698            state.child_abort_ack = Some(acknowledgment);
699        }
700        self
701    }
702
703    /// Takes the single receipt emitted by the intended production operation.
704    pub fn take_receipt(&self) -> Option<StorageFaultReceipt> {
705        self.state
706            .lock()
707            .ok()
708            .and_then(|mut state| state.receipt.take())
709    }
710
711    /// Takes the exact result of the most recent public-open cleanup pass.
712    pub fn take_cleanup_report(&self) -> Option<StorageCleanupReport> {
713        self.state
714            .lock()
715            .ok()
716            .and_then(|mut state| state.cleanup_report.take())
717    }
718
719    fn record_cleanup_report(&self, report: crate::manifest::io::OrphanCleanupReport) {
720        let normalize = |paths: Vec<PathBuf>| {
721            paths
722                .into_iter()
723                .filter_map(|path| {
724                    path.file_name()
725                        .and_then(|name| name.to_str())
726                        .map(str::to_owned)
727                })
728                .collect::<Vec<_>>()
729        };
730        if let Ok(mut state) = self.state.lock() {
731            state.cleanup_report = Some(StorageCleanupReport {
732                reclaimed_bytes: report.reclaimed_bytes,
733                deleted_paths: normalize(report.deleted_paths),
734                retained_eligible_paths: normalize(report.retained_eligible_paths),
735                directory_synced: report.directory_synced,
736            });
737        }
738    }
739
740    fn armed_fault(&self) -> Option<StorageTestFault> {
741        self.state
742            .lock()
743            .ok()
744            .and_then(|state| (!state.consumed).then(|| state.fault.clone()))
745    }
746
747    fn planned_segment(&self) -> Option<crate::segment::SegmentId> {
748        self.state
749            .lock()
750            .ok()
751            .and_then(|state| state.plan.segment())
752    }
753
754    fn emit(&self, site: StorageReceiptSite, observed: StorageReceiptObserved) -> bool {
755        let Ok(mut state) = self.state.lock() else {
756            return false;
757        };
758        if state.consumed {
759            return false;
760        }
761        state.consumed = true;
762        state.receipt = Some(StorageFaultReceipt {
763            operation: state.fault.operation(),
764            fault: state.fault.key(),
765            site,
766            plan: state.plan.clone(),
767            observed,
768            cardinality: 1,
769        });
770        true
771    }
772
773    fn abort_after_manifest_receipt(&self) -> ! {
774        #[cfg(unix)]
775        if let Ok(mut state) = self.state.lock() {
776            let wire_receipt = state
777                .receipt
778                .as_ref()
779                .and_then(storage_manifest_abort_wire_receipt);
780            if let (Some(wire_receipt), Some(acknowledgment)) =
781                (wire_receipt, state.child_abort_ack.as_mut())
782            {
783                use std::io::Write as _;
784                let _ = acknowledgment.write_all(wire_receipt.as_bytes());
785                let _ = acknowledgment.flush();
786            }
787        }
788        std::process::abort()
789    }
790}
791
792#[cfg(any(test, feature = "test-seams"))]
793thread_local! {
794    static STORAGE_OPEN_CONTROLLER: std::cell::RefCell<
795        Option<std::sync::Weak<Mutex<StorageFaultState>>>
796    > = const { std::cell::RefCell::new(None) };
797}
798
799#[cfg(any(test, feature = "test-seams"))]
800fn with_storage_open_controller<R>(
801    controller: Option<&StorageFaultController>,
802    operation: impl FnOnce() -> R,
803) -> R {
804    STORAGE_OPEN_CONTROLLER.with(|slot| {
805        let previous = slot.replace(controller.map(|controller| Arc::downgrade(&controller.state)));
806        let result = operation();
807        let _ = slot.replace(previous);
808        result
809    })
810}
811
812#[cfg(any(test, feature = "test-seams"))]
813fn storage_open_controller() -> Option<StorageFaultController> {
814    STORAGE_OPEN_CONTROLLER.with(|slot| {
815        slot.borrow()
816            .as_ref()
817            .and_then(std::sync::Weak::upgrade)
818            .map(|state| StorageFaultController { state })
819    })
820}
821
822#[cfg(any(test, feature = "test-seams"))]
823pub(crate) fn record_storage_wal_recovery_fault(error: &crate::wal::WalRecoveryError) {
824    let Some(controller) = storage_open_controller() else {
825        return;
826    };
827    match (controller.armed_fault(), error) {
828        (
829            Some(StorageTestFault::TornWalHeader),
830            crate::wal::WalRecoveryError::InvalidHeader(reason),
831        ) => {
832            let _ = controller.emit(
833                StorageReceiptSite::WalOpenHeaderValidation,
834                StorageReceiptObserved::WalHeader {
835                    artifact: "wal.ze".to_owned(),
836                    reason: *reason,
837                },
838            );
839        }
840        (
841            Some(StorageTestFault::TornWalBody),
842            crate::wal::WalRecoveryError::CorruptAt {
843                offset,
844                reason:
845                    reason @ crate::wal::replay::CorruptionReason::Record {
846                        error:
847                            crate::wal::record::RecordError::HeaderTruncated { .. }
848                            | crate::wal::record::RecordError::BodyTruncated { .. }
849                            | crate::wal::record::RecordError::LengthOverflow { .. },
850                        ..
851                    },
852            },
853        ) => record_storage_wal_record_fault(
854            &controller,
855            StorageReceiptSite::WalOpenRecordValidation,
856            *offset,
857            *reason,
858        ),
859        (
860            Some(StorageTestFault::TornWalChecksum),
861            crate::wal::WalRecoveryError::CorruptAt {
862                offset,
863                reason:
864                    reason @ crate::wal::replay::CorruptionReason::Record {
865                        error: crate::wal::record::RecordError::ChecksumMismatch { .. },
866                        ..
867                    },
868            },
869        ) => record_storage_wal_record_fault(
870            &controller,
871            StorageReceiptSite::WalOpenRecordChecksum,
872            *offset,
873            *reason,
874        ),
875        _ => {}
876    }
877}
878
879#[cfg(any(test, feature = "test-seams"))]
880fn record_storage_wal_record_fault(
881    controller: &StorageFaultController,
882    site: StorageReceiptSite,
883    offset: usize,
884    reason: crate::wal::replay::CorruptionReason,
885) {
886    let Ok(offset) = u64::try_from(offset) else {
887        return;
888    };
889    let _ = controller.emit(
890        site,
891        StorageReceiptObserved::WalRecord {
892            artifact: "wal.ze".to_owned(),
893            offset,
894            reason,
895        },
896    );
897}
898
899#[cfg(any(test, feature = "test-seams"))]
900pub(crate) fn record_storage_manifest_format_fault(
901    error: &crate::format::frame::FormatError,
902    actual_family: Option<u16>,
903) {
904    let Some(controller) = storage_open_controller() else {
905        return;
906    };
907    if matches!(
908        controller.armed_fault(),
909        Some(StorageTestFault::WrongManifestObject)
910    ) && error.check() == crate::format::frame::FormatCheck::Family
911    {
912        let _ = controller.emit(
913            StorageReceiptSite::ManifestOpenFamilyValidation,
914            StorageReceiptObserved::Format {
915                artifact: storage_artifact_name(error.artifact()).to_owned(),
916                check: error.check(),
917                expected_family: Some(crate::format::FormatFamily::Manifest.id()),
918                actual_family,
919                expected_id: None,
920                actual_id: None,
921            },
922        );
923    }
924}
925
926#[cfg(any(test, feature = "test-seams"))]
927pub(crate) fn record_storage_segment_format_fault(
928    error: &crate::format::frame::FormatError,
929    actual_family: Option<u16>,
930    expected_id: crate::segment::SegmentId,
931) {
932    let Some(controller) = storage_open_controller() else {
933        return;
934    };
935    if matches!(
936        controller.armed_fault(),
937        Some(StorageTestFault::WrongSegmentObject)
938    ) && error.check() == crate::format::frame::FormatCheck::Family
939    {
940        let _ = controller.emit(
941            StorageReceiptSite::SegmentOpenFamilyValidation,
942            StorageReceiptObserved::Format {
943                artifact: storage_artifact_name(error.artifact()).to_owned(),
944                check: error.check(),
945                expected_family: Some(crate::format::FormatFamily::Segment.id()),
946                actual_family,
947                expected_id: Some(expected_id),
948                actual_id: None,
949            },
950        );
951    }
952}
953
954#[cfg(any(test, feature = "test-seams"))]
955pub(crate) fn record_storage_segment_identity_fault(
956    artifact: &str,
957    expected: crate::segment::SegmentId,
958    actual: crate::segment::SegmentId,
959) {
960    let Some(controller) = storage_open_controller() else {
961        return;
962    };
963    if matches!(
964        controller.armed_fault(),
965        Some(StorageTestFault::WrongSegmentObject)
966    ) {
967        let _ = controller.emit(
968            StorageReceiptSite::SegmentOpenObjectIdentity,
969            StorageReceiptObserved::Format {
970                artifact: storage_artifact_name(artifact).to_owned(),
971                check: crate::format::frame::FormatCheck::ObjectIdentity,
972                expected_family: None,
973                actual_family: None,
974                expected_id: Some(expected),
975                actual_id: Some(actual),
976            },
977        );
978    }
979}
980
981/// Renders a manifest-abort receipt for the child-abort acknowledgement wire.
982///
983/// Gated on `unix` because its only caller is the `#[cfg(unix)]` arm of
984/// [`StorageFaultController::abort_after_manifest_receipt`], which writes the
985/// receipt down an inherited `UnixStream`. Without the gate this is dead code
986/// on every other platform. W11 adds the Windows child-abort channel; the gate
987/// widens with it.
988#[cfg(unix)]
989#[cfg(any(test, feature = "test-seams"))]
990fn storage_manifest_abort_wire_receipt(receipt: &StorageFaultReceipt) -> Option<String> {
991    let StorageReceiptObserved::ManifestRename {
992        temporary,
993        committed,
994        rename_performed,
995        new_segment_final,
996        directory_sync_returned,
997    } = &receipt.observed
998    else {
999        return None;
1000    };
1001    Some(format!(
1002        "campaign={}|operation={}|fault={}|site={}|op_index={}|cardinality={}|artifact={}|temporary={temporary}|committed={committed}|rename_performed={rename_performed}|new_segment_final={new_segment_final}|directory_sync_returned={directory_sync_returned}\n",
1003        receipt.campaign(),
1004        receipt.operation,
1005        receipt.fault,
1006        receipt.site.key(),
1007        receipt.plan.op_index,
1008        receipt.cardinality,
1009        receipt.plan.artifact,
1010    ))
1011}
1012
1013#[cfg(any(test, feature = "test-seams"))]
1014fn storage_artifact_name(artifact: &str) -> &str {
1015    Path::new(artifact)
1016        .file_name()
1017        .and_then(|name| name.to_str())
1018        .unwrap_or(artifact)
1019}
1020
1021#[cfg(any(test, feature = "test-seams"))]
1022type StorageSegmentControllerRegistry = Mutex<
1023    Vec<(
1024        PathBuf,
1025        crate::segment::SegmentId,
1026        std::sync::Weak<Mutex<StorageFaultState>>,
1027    )>,
1028>;
1029
1030#[cfg(any(test, feature = "test-seams"))]
1031static STORAGE_SEGMENT_CONTROLLERS: std::sync::OnceLock<StorageSegmentControllerRegistry> =
1032    std::sync::OnceLock::new();
1033
1034#[cfg(any(test, feature = "test-seams"))]
1035fn register_storage_segment_controller(
1036    store_directory: &Path,
1037    segment: crate::segment::SegmentId,
1038    controller: &StorageFaultController,
1039) {
1040    let registry = STORAGE_SEGMENT_CONTROLLERS.get_or_init(|| Mutex::new(Vec::new()));
1041    if let Ok(mut entries) = registry.lock() {
1042        entries.retain(|(_, _, state)| state.strong_count() != 0);
1043        entries.push((
1044            store_directory.to_path_buf(),
1045            segment,
1046            Arc::downgrade(&controller.state),
1047        ));
1048    }
1049}
1050
1051#[cfg(any(test, feature = "test-seams"))]
1052pub(crate) fn record_storage_segment_checksum_fault(
1053    store_directory: &Path,
1054    segment: crate::segment::SegmentId,
1055    kind: crate::segment::layout::RegionKind,
1056    chunk: u32,
1057    expected: u64,
1058    actual: u64,
1059) {
1060    let Some(registry) = STORAGE_SEGMENT_CONTROLLERS.get() else {
1061        return;
1062    };
1063    let states = match registry.lock() {
1064        Ok(mut entries) => {
1065            entries.retain(|(_, _, state)| state.strong_count() != 0);
1066            entries
1067                .iter()
1068                .filter(|(directory, registered, _)| {
1069                    directory == store_directory && *registered == segment
1070                })
1071                .filter_map(|(_, _, state)| state.upgrade())
1072                .collect::<Vec<_>>()
1073        }
1074        Err(_) => return,
1075    };
1076    for state in states {
1077        let controller = StorageFaultController { state };
1078        if matches!(
1079            controller.armed_fault(),
1080            Some(StorageTestFault::CorruptSegmentRegion)
1081        ) {
1082            let _ = controller.emit(
1083                StorageReceiptSite::SegmentReadRegionChecksum,
1084                StorageReceiptObserved::SegmentChecksum {
1085                    artifact: segment.file_name(),
1086                    segment,
1087                    region_kind: kind.id(),
1088                    chunk,
1089                    expected_checksum: expected,
1090                    actual_checksum: actual,
1091                },
1092            );
1093        }
1094    }
1095}
1096
1097#[cfg(any(test, feature = "test-seams"))]
1098struct StorageFaultVfs {
1099    inner: Arc<dyn crate::vfs::Vfs>,
1100    controller: StorageFaultController,
1101}
1102
1103#[cfg(any(test, feature = "test-seams"))]
1104struct StorageFaultFile {
1105    inner: Box<dyn crate::vfs::VfsFile>,
1106    path: PathBuf,
1107    controller: StorageFaultController,
1108}
1109
1110#[cfg(any(test, feature = "test-seams"))]
1111impl StorageFaultFile {
1112    fn after_append(&self, encoded_len: u64, first_seq: u64, last_seq: u64) -> std::io::Result<()> {
1113        if matches!(
1114            self.controller.armed_fault(),
1115            Some(StorageTestFault::PostCommitError)
1116        ) && self.path.file_name().is_some_and(|name| name == "wal.ze")
1117            && self.controller.emit(
1118                StorageReceiptSite::WalCommitAppendAfterInnerSuccess,
1119                StorageReceiptObserved::WalAppend {
1120                    artifact: "wal.ze".to_owned(),
1121                    encoded_len,
1122                    first_seq,
1123                    last_seq,
1124                    inner_append_completed: true,
1125                    caller_saw_error: true,
1126                },
1127            )
1128        {
1129            return Err(std::io::Error::other(
1130                "scheduled post-commit error after inner append",
1131            ));
1132        }
1133        Ok(())
1134    }
1135}
1136
1137#[cfg(any(test, feature = "test-seams"))]
1138impl crate::vfs::VfsFile for StorageFaultFile {
1139    fn append(&mut self, bytes: &[u8]) -> std::io::Result<()> {
1140        let record_facts = storage_wal_append_facts(bytes)?;
1141        self.inner.append(bytes)?;
1142        record_facts.map_or(Ok(()), |(encoded_len, first_seq, last_seq)| {
1143            self.after_append(encoded_len, first_seq, last_seq)
1144        })
1145    }
1146
1147    fn append_vectored(&mut self, buffers: &mut [IoSlice<'_>]) -> std::io::Result<()> {
1148        let mut aggregate = None::<(u64, u64, u64)>;
1149        for bytes in buffers.iter() {
1150            if let Some((encoded_len, first_seq, last_seq)) = storage_wal_append_facts(bytes)? {
1151                aggregate = Some(match aggregate {
1152                    None => (encoded_len, first_seq, last_seq),
1153                    Some((total, first, _)) => (
1154                        total.checked_add(encoded_len).ok_or_else(|| {
1155                            std::io::Error::other("WAL record append length overflow")
1156                        })?,
1157                        first,
1158                        last_seq,
1159                    ),
1160                });
1161            }
1162        }
1163        self.inner.append_vectored(buffers)?;
1164        aggregate.map_or(Ok(()), |(encoded_len, first_seq, last_seq)| {
1165            self.after_append(encoded_len, first_seq, last_seq)
1166        })
1167    }
1168
1169    fn sync(&self, kind: crate::vfs::SyncKind) -> std::io::Result<()> {
1170        self.inner.sync(kind)
1171    }
1172}
1173
1174#[cfg(any(test, feature = "test-seams"))]
1175impl crate::vfs::Vfs for StorageFaultVfs {
1176    fn segment_data_read_counter(&self) -> Option<Arc<AtomicU64>> {
1177        self.inner.segment_data_read_counter()
1178    }
1179
1180    fn ensure_directory(&self, path: &Path, create: bool) -> std::io::Result<bool> {
1181        self.inner.ensure_directory(path, create)
1182    }
1183
1184    fn open(&self, path: &Path) -> std::io::Result<u64> {
1185        self.inner.open(path)
1186    }
1187
1188    fn open_for_map(&self, path: &Path) -> std::io::Result<std::fs::File> {
1189        self.inner.open_for_map(path)
1190    }
1191
1192    fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
1193        self.inner.read(path)
1194    }
1195
1196    fn read_range(&self, path: &Path, offset: u64, length: usize) -> std::io::Result<Vec<u8>> {
1197        self.inner.read_range(path, offset, length)
1198    }
1199
1200    fn write(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()> {
1201        self.inner.write(path, bytes)
1202    }
1203
1204    fn open_append(&self, path: &Path) -> std::io::Result<Box<dyn crate::vfs::VfsFile>> {
1205        Ok(Box::new(StorageFaultFile {
1206            inner: self.inner.open_append(path)?,
1207            path: path.to_path_buf(),
1208            controller: self.controller.clone(),
1209        }))
1210    }
1211
1212    fn rename(&self, from: &Path, to: &Path) -> std::io::Result<()> {
1213        let is_manifest = from
1214            .file_name()
1215            .is_some_and(|name| name == crate::manifest::io::MANIFEST_TEMP_FILE)
1216            && to
1217                .file_name()
1218                .is_some_and(|name| name == crate::manifest::io::MANIFEST_FILE);
1219        if is_manifest
1220            && matches!(
1221                self.controller.armed_fault(),
1222                Some(StorageTestFault::ManifestPreRename)
1223            )
1224            && self.controller.emit(
1225                StorageReceiptSite::ManifestCommitBeforeRename,
1226                StorageReceiptObserved::ManifestRename {
1227                    temporary: crate::manifest::io::MANIFEST_TEMP_FILE.to_owned(),
1228                    committed: crate::manifest::io::MANIFEST_FILE.to_owned(),
1229                    rename_performed: false,
1230                    new_segment_final: storage_planned_segment_is_final(
1231                        &*self.inner,
1232                        from,
1233                        self.controller.planned_segment(),
1234                    ),
1235                    directory_sync_returned: false,
1236                },
1237            )
1238        {
1239            self.controller.abort_after_manifest_receipt();
1240        }
1241        self.inner.rename(from, to)?;
1242        if is_manifest
1243            && matches!(
1244                self.controller.armed_fault(),
1245                Some(StorageTestFault::ManifestPostRename)
1246            )
1247            && self.controller.emit(
1248                StorageReceiptSite::ManifestCommitAfterRename,
1249                StorageReceiptObserved::ManifestRename {
1250                    temporary: crate::manifest::io::MANIFEST_TEMP_FILE.to_owned(),
1251                    committed: crate::manifest::io::MANIFEST_FILE.to_owned(),
1252                    rename_performed: true,
1253                    new_segment_final: storage_planned_segment_is_final(
1254                        &*self.inner,
1255                        to,
1256                        self.controller.planned_segment(),
1257                    ),
1258                    directory_sync_returned: false,
1259                },
1260            )
1261        {
1262            self.controller.abort_after_manifest_receipt();
1263        }
1264        Ok(())
1265    }
1266
1267    fn sync(&self, path: &Path, kind: crate::vfs::SyncKind) -> std::io::Result<()> {
1268        self.inner.sync(path, kind)
1269    }
1270
1271    fn list(&self, directory: &Path) -> std::io::Result<Vec<PathBuf>> {
1272        let mut paths = self.inner.list(directory)?;
1273        if let Some(StorageTestFault::ListOmission { file_name }) = self.controller.armed_fault()
1274            && let Some(position) = paths.iter().position(|path| {
1275                path.file_name()
1276                    .is_some_and(|name| name == file_name.as_str())
1277            })
1278        {
1279            let _ = paths.remove(position);
1280            let _ = self.controller.emit(
1281                StorageReceiptSite::OrphanCleanupList,
1282                StorageReceiptObserved::Omission {
1283                    artifact: file_name,
1284                    deletion_observed: false,
1285                },
1286            );
1287        }
1288        Ok(paths)
1289    }
1290
1291    fn for_each_direct_child(
1292        &self,
1293        directory: &Path,
1294        visitor: &mut dyn FnMut(&Path) -> std::io::Result<()>,
1295    ) -> std::io::Result<()> {
1296        // The omission belongs to OrphanCleanup.List, not the earlier native
1297        // format admission scan. Only list() consumes that one-shot fault.
1298        self.inner.for_each_direct_child(directory, visitor)
1299    }
1300
1301    fn delete(&self, path: &Path) -> std::io::Result<()> {
1302        if let Some(StorageTestFault::DeleteOmission { file_name }) = self.controller.armed_fault()
1303            && path
1304                .file_name()
1305                .is_some_and(|name| name == file_name.as_str())
1306            && self.controller.emit(
1307                StorageReceiptSite::OrphanCleanupDelete,
1308                StorageReceiptObserved::Omission {
1309                    artifact: file_name,
1310                    deletion_observed: false,
1311                },
1312            )
1313        {
1314            return Ok(());
1315        }
1316        self.inner.delete(path)
1317    }
1318}
1319
1320#[cfg(any(test, feature = "test-seams"))]
1321fn storage_wal_append_facts(bytes: &[u8]) -> std::io::Result<Option<(u64, u64, u64)>> {
1322    const RECORD_HEADER_LEN: usize = 14;
1323    const RECORD_CHECKSUM_LEN: usize = 8;
1324    let has_wal_header = bytes
1325        .get(..8)
1326        .is_some_and(|magic| magic == crate::format::frame::FILE_MAGIC)
1327        && bytes
1328            .get(8..10)
1329            .is_some_and(|family| family == crate::format::FormatFamily::Wal.id().to_le_bytes());
1330    let mut offset = if has_wal_header {
1331        crate::wal::header::WAL_HEADER_LEN
1332    } else {
1333        0
1334    };
1335    if offset == bytes.len() {
1336        return Ok(None);
1337    }
1338    let mut encoded_len = 0_u64;
1339    let mut first_seq = None;
1340    let mut last_seq = None;
1341    while offset < bytes.len() {
1342        let header_end = offset
1343            .checked_add(RECORD_HEADER_LEN)
1344            .ok_or_else(|| std::io::Error::other("WAL record header offset overflow"))?;
1345        let header = bytes
1346            .get(offset..header_end)
1347            .ok_or_else(|| std::io::Error::other("WAL append has a torn record header"))?;
1348        let payload_len = usize::try_from(u32::from_le_bytes(
1349            header
1350                .get(..4)
1351                .ok_or_else(|| std::io::Error::other("WAL payload length is absent"))?
1352                .try_into()
1353                .map_err(|_| std::io::Error::other("WAL payload length width changed"))?,
1354        ))
1355        .map_err(|_| std::io::Error::other("WAL payload length does not fit usize"))?;
1356        let seq = u64::from_le_bytes(
1357            header
1358                .get(4..12)
1359                .ok_or_else(|| std::io::Error::other("WAL sequence is absent"))?
1360                .try_into()
1361                .map_err(|_| std::io::Error::other("WAL sequence width changed"))?,
1362        );
1363        let record_len = RECORD_HEADER_LEN
1364            .checked_add(payload_len)
1365            .and_then(|length| length.checked_add(RECORD_CHECKSUM_LEN))
1366            .ok_or_else(|| std::io::Error::other("WAL record length overflow"))?;
1367        offset = offset
1368            .checked_add(record_len)
1369            .ok_or_else(|| std::io::Error::other("WAL record end overflow"))?;
1370        if offset > bytes.len() {
1371            return Err(std::io::Error::other("WAL append has a torn record body"));
1372        }
1373        encoded_len = encoded_len
1374            .checked_add(
1375                u64::try_from(record_len)
1376                    .map_err(|_| std::io::Error::other("WAL record length does not fit u64"))?,
1377            )
1378            .ok_or_else(|| std::io::Error::other("WAL record append length overflow"))?;
1379        first_seq.get_or_insert(seq);
1380        last_seq = Some(seq);
1381    }
1382    match (first_seq, last_seq) {
1383        (Some(first), Some(last)) => Ok(Some((encoded_len, first, last))),
1384        _ => Ok(None),
1385    }
1386}
1387
1388#[cfg(any(test, feature = "test-seams"))]
1389fn storage_planned_segment_is_final(
1390    vfs: &dyn crate::vfs::Vfs,
1391    artifact: &Path,
1392    planned: Option<crate::segment::SegmentId>,
1393) -> bool {
1394    let Some(file_name) = planned.map(crate::segment::SegmentId::file_name) else {
1395        return false;
1396    };
1397    artifact.parent().is_some_and(|directory| {
1398        vfs.list(directory).is_ok_and(|paths| {
1399            paths.iter().any(|path| {
1400                path.file_name()
1401                    .and_then(|name| name.to_str())
1402                    .is_some_and(|name| name == file_name)
1403            })
1404        })
1405    })
1406}
1407
1408/// Narrow test-only fault at a Store-owned hybrid leg seam.
1409#[cfg(any(test, feature = "test-seams"))]
1410#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1411#[doc(hidden)]
1412pub enum HybridLegTestFault {
1413    /// Panics after the named leg starts and before it can publish output.
1414    Panic(crate::fusion::FusionLeg),
1415}
1416
1417#[allow(clippy::panic)]
1418fn maybe_trigger_hybrid_leg_panic(armed: bool, detail: &'static str) {
1419    #[cfg(any(test, feature = "test-seams"))]
1420    if armed {
1421        std::panic::panic_any(detail);
1422    }
1423    #[cfg(not(any(test, feature = "test-seams")))]
1424    let _ = (armed, detail);
1425}
1426
1427/// Facts emitted by one Store-owned parallel hybrid execution.
1428#[cfg(any(test, feature = "test-seams"))]
1429#[derive(Clone, Debug, Eq, PartialEq)]
1430#[doc(hidden)]
1431pub struct HybridExecutionReceipt {
1432    /// Thread that executed vector work.
1433    pub vector_thread: std::thread::ThreadId,
1434    /// Name of the scoped lexical worker.
1435    pub lexical_thread_name: Option<String>,
1436    /// Vector leg reached a terminal result before return.
1437    pub vector_completed: bool,
1438    /// Lexical leg reached a terminal result before return.
1439    pub lexical_completed: bool,
1440}
1441
1442/// Default grace period given to admitted readers before close cancellation.
1443pub const DEFAULT_READER_DRAIN_TIMEOUT: Duration = Duration::from_millis(250);
1444
1445/// Explicit lifecycle state for one store handle.
1446#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1447pub enum StoreState {
1448    /// New calls may be admitted.
1449    Open,
1450    /// New calls are rejected while admitted work drains.
1451    Closing,
1452    /// Teardown completed and only idempotent close/state calls remain valid.
1453    Closed,
1454}
1455
1456/// Store-open configuration.
1457#[derive(Clone, Debug, Eq, PartialEq)]
1458pub struct OpenOptions {
1459    inspection: bool,
1460    access_mode: AccessMode,
1461    durability_mode: DurabilityMode,
1462    commit_tier: CommitTier,
1463    reader_drain_timeout: Duration,
1464    max_resident_bytes: u64,
1465    max_temp_bytes: u64,
1466    epoch: Option<crate::epoch::StoreEpoch>,
1467    tokenizer: Option<crate::fts::tokenizer::TokenizerConfig>,
1468    schema: Option<crate::meta::Schema>,
1469    #[cfg(feature = "graph-cypher")]
1470    graph_document: Option<crate::epoch::EmbeddingTower>,
1471}
1472
1473/// Filesystem authority requested for one store handle.
1474#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1475pub enum AccessMode {
1476    /// Own the store's one kernel-enforced writer slot.
1477    ReadWrite,
1478    /// Load only the last committed snapshot without filesystem mutation.
1479    ReadOnly,
1480}
1481
1482impl OpenOptions {
1483    /// Returns the default read-write, derived-durability configuration.
1484    #[must_use]
1485    pub const fn new() -> Self {
1486        Self {
1487            inspection: false,
1488            access_mode: AccessMode::ReadWrite,
1489            durability_mode: DurabilityMode::Derived,
1490            commit_tier: CommitTier::Ordered,
1491            reader_drain_timeout: DEFAULT_READER_DRAIN_TIMEOUT,
1492            max_resident_bytes: u64::MAX,
1493            max_temp_bytes: u64::MAX,
1494            epoch: None,
1495            tokenizer: None,
1496            schema: None,
1497            #[cfg(feature = "graph-cypher")]
1498            graph_document: None,
1499        }
1500    }
1501
1502    /// Returns a pure-read configuration that takes no writer lock.
1503    #[must_use]
1504    pub const fn read_only() -> Self {
1505        Self {
1506            inspection: false,
1507            access_mode: AccessMode::ReadOnly,
1508            durability_mode: DurabilityMode::Derived,
1509            commit_tier: CommitTier::Ordered,
1510            reader_drain_timeout: DEFAULT_READER_DRAIN_TIMEOUT,
1511            max_resident_bytes: u64::MAX,
1512            max_temp_bytes: u64::MAX,
1513            epoch: None,
1514            tokenizer: None,
1515            schema: None,
1516            #[cfg(feature = "graph-cypher")]
1517            graph_document: None,
1518        }
1519    }
1520
1521    /// Selects the existing durability-policy mode and per-commit tier.
1522    #[must_use]
1523    pub const fn with_durability(mut self, mode: DurabilityMode, tier: CommitTier) -> Self {
1524        self.durability_mode = mode;
1525        self.commit_tier = tier;
1526        self
1527    }
1528
1529    /// Replaces the requested filesystem authority. The graph store facade
1530    /// owns its access mode, so this stays crate-private.
1531    #[cfg(feature = "graph-cypher")]
1532    #[must_use]
1533    pub(crate) const fn with_access_mode(mut self, access_mode: AccessMode) -> Self {
1534        self.access_mode = access_mode;
1535        self
1536    }
1537
1538    /// Sets the grace period close gives admitted readers before cancellation.
1539    #[must_use]
1540    pub const fn with_reader_drain_timeout(mut self, timeout: Duration) -> Self {
1541        self.reader_drain_timeout = timeout;
1542        self
1543    }
1544
1545    /// Sets the exact ceiling for live engine-owned anonymous bytes.
1546    #[must_use]
1547    pub const fn with_max_resident_bytes(mut self, bytes: u64) -> Self {
1548        self.max_resident_bytes = bytes;
1549        self
1550    }
1551
1552    /// Declares the typed user-column schema when creating a store.
1553    ///
1554    /// On reopen, the declaration must be an additive evolution of the
1555    /// persisted schema (see [`crate::meta::Schema::additive_evolution`]).
1556    /// A read-write open commits added nullable columns as one generation;
1557    /// rows written earlier read them as null. A read-only open, or any
1558    /// other difference, is [`StoreError::SchemaMismatch`].
1559    #[must_use]
1560    pub fn with_schema(mut self, schema: crate::meta::Schema) -> Self {
1561        self.schema = Some(schema);
1562        self
1563    }
1564
1565    /// Sets the exact ceiling for live temporary anonymous bytes.
1566    #[must_use]
1567    pub const fn with_max_temp_bytes(mut self, bytes: u64) -> Self {
1568        self.max_temp_bytes = bytes;
1569        self
1570    }
1571
1572    /// Declares the embedding and tokenizer identity used by this handle.
1573    ///
1574    /// The tokenizer configuration defaults to
1575    /// [`crate::fts::tokenizer::TokenizerConfig::text_default`].
1576    /// A non-default tokenizer epoch must be paired with [`Self::with_tokenizer`].
1577    #[must_use]
1578    pub fn with_epoch(mut self, epoch: crate::epoch::StoreEpoch) -> Self {
1579        self.epoch = Some(epoch);
1580        self
1581    }
1582
1583    /// Selects the tokenizer configuration used for indexing and queries.
1584    ///
1585    /// Open rejects this configuration when its epoch differs from the
1586    /// declared or persisted store identity.
1587    #[must_use]
1588    pub fn with_tokenizer(mut self, tokenizer: crate::fts::tokenizer::TokenizerConfig) -> Self {
1589        self.tokenizer = Some(tokenizer);
1590        self
1591    }
1592}
1593
1594impl Default for OpenOptions {
1595    fn default() -> Self {
1596        Self::new()
1597    }
1598}
1599
1600/// Traversal controls carried only when a caller explicitly selects the graph tier.
1601#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1602pub struct GraphSearchOptions {
1603    profile: crate::graph::search::GraphSearchProfile,
1604    ef: Option<usize>,
1605    seed: u64,
1606    #[cfg(any(test, feature = "test-seams"))]
1607    cancel_after_hops: Option<usize>,
1608}
1609
1610impl GraphSearchOptions {
1611    /// Creates an adaptive-width graph request for one dataset-shape profile.
1612    #[must_use]
1613    pub const fn new(profile: crate::graph::search::GraphSearchProfile) -> Self {
1614        Self {
1615            profile,
1616            ef: None,
1617            seed: 0,
1618            #[cfg(any(test, feature = "test-seams"))]
1619            cancel_after_hops: None,
1620        }
1621    }
1622
1623    /// Overrides the profile's adaptive traversal width.
1624    #[must_use]
1625    pub const fn with_ef(mut self, ef: usize) -> Self {
1626        self.ef = Some(ef);
1627        self
1628    }
1629
1630    /// Selects the deterministic Bit4 query-preparation seed.
1631    #[must_use]
1632    pub const fn with_seed(mut self, seed: u64) -> Self {
1633        self.seed = seed;
1634        self
1635    }
1636
1637    /// Arms deterministic in-traversal cancellation for integration tests.
1638    #[cfg(any(test, feature = "test-seams"))]
1639    #[must_use]
1640    pub const fn with_cancel_after_hops(mut self, hops: usize) -> Self {
1641        self.cancel_after_hops = Some(hops);
1642        self
1643    }
1644
1645    pub(crate) const fn profile(self) -> crate::graph::search::GraphSearchProfile {
1646        self.profile
1647    }
1648
1649    pub(crate) const fn ef(self) -> Option<usize> {
1650        self.ef
1651    }
1652
1653    pub(crate) const fn seed(self) -> u64 {
1654        self.seed
1655    }
1656
1657    #[cfg(any(test, feature = "test-seams"))]
1658    pub(crate) const fn cancel_after_hops(self) -> Option<usize> {
1659        self.cancel_after_hops
1660    }
1661}
1662
1663impl Default for GraphSearchOptions {
1664    fn default() -> Self {
1665        Self::new(crate::graph::search::GraphSearchProfile::SiftClass)
1666    }
1667}
1668
1669pub(crate) fn auto_graph_search_options(
1670    snapshot: &PublishedSnapshot,
1671) -> Result<GraphSearchOptions, QueryError> {
1672    snapshot
1673        .graph_profile()
1674        .map(|profile| GraphSearchOptions::new(profile.search_profile()))
1675        .map_err(crate::graph::search::GraphSearchError::Profile)
1676        .map_err(QueryError::Graph)
1677}
1678
1679/// Per-query execution tier at the public store seam.
1680#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1681pub enum SearchTier {
1682    /// Select graph traversal per sealed segment when that graph is published.
1683    #[default]
1684    Auto,
1685    /// Exhaustively score full-precision vectors. Large scans share at most
1686    /// four pool workers across callers, bounded further by the scan worker
1687    /// budget; small scans execute serially.
1688    Exact,
1689    /// Preserve the existing exhaustive sealed-segment scan.
1690    Scan,
1691    /// Traverse every sealed segment's Vamana graph.
1692    Graph(GraphSearchOptions),
1693}
1694
1695/// Store-search controls, separating scan worker budget from tier selection.
1696#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1697pub struct SearchOptions {
1698    scan: crate::scan::ScanOptions,
1699    tier: Option<SearchTier>,
1700    scan_rescore: Option<ScanRescoreOptions>,
1701}
1702
1703#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1704enum GraphBoundMode {
1705    Shared,
1706    #[cfg(test)]
1707    Independent,
1708}
1709
1710impl SearchOptions {
1711    /// Creates options with no tier preference and the supplied scan worker budget.
1712    #[must_use]
1713    pub const fn new(scan: crate::scan::ScanOptions) -> Self {
1714        Self {
1715            scan,
1716            tier: None,
1717            scan_rescore: None,
1718        }
1719    }
1720
1721    /// Explicitly selects one store-search tier.
1722    #[must_use]
1723    pub const fn with_tier(mut self, tier: SearchTier) -> Self {
1724        self.tier = Some(tier);
1725        self.scan_rescore = None;
1726        self
1727    }
1728
1729    /// Selects quantized Scan candidates followed by exact full-precision scores.
1730    /// Candidate membership remains approximate unless every eligible row is
1731    /// rescored. A subsequent `with_tier` call clears these controls.
1732    #[must_use]
1733    pub const fn with_scan_rescore(mut self, options: ScanRescoreOptions) -> Self {
1734        self.tier = Some(SearchTier::Scan);
1735        self.scan_rescore = Some(options);
1736        self
1737    }
1738
1739    /// Explicit selected-row rescoring controls, absent for ordinary tiers.
1740    #[must_use]
1741    pub const fn scan_rescore(self) -> Option<ScanRescoreOptions> {
1742        self.scan_rescore
1743    }
1744
1745    /// Returns the scan worker controls used by scan-tier work.
1746    #[must_use]
1747    pub const fn scan(self) -> crate::scan::ScanOptions {
1748        self.scan
1749    }
1750
1751    /// Returns the effective execution tier, using automatic selection when
1752    /// the caller expressed no preference.
1753    #[must_use]
1754    pub const fn tier(self) -> SearchTier {
1755        match self.tier {
1756            Some(tier) => tier,
1757            None => SearchTier::Auto,
1758        }
1759    }
1760
1761    pub(crate) const fn explicit_tier(self) -> Option<SearchTier> {
1762        self.tier
1763    }
1764}
1765
1766impl From<crate::scan::ScanOptions> for SearchOptions {
1767    fn from(scan: crate::scan::ScanOptions) -> Self {
1768        Self::new(scan)
1769    }
1770}
1771
1772/// Bit-set selecting the optional fields copied by [`Store::get_documents`].
1773#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1774pub struct DocumentFields(u8);
1775
1776impl DocumentFields {
1777    /// Selects no optional fields.
1778    pub const NONE: Self = Self(0);
1779    /// Selects the full-precision vector.
1780    pub const VECTOR: Self = Self(1 << 0);
1781    /// Selects the stored UTF-8 text.
1782    pub const TEXT: Self = Self(1 << 1);
1783    /// Selects the opaque metadata bytes.
1784    pub const METADATA: Self = Self(1 << 2);
1785    /// Selects the typed schema attributes.
1786    pub const ATTRIBUTES: Self = Self(1 << 3);
1787    /// Selects every optional field.
1788    pub const ALL: Self =
1789        Self(Self::VECTOR.0 | Self::TEXT.0 | Self::METADATA.0 | Self::ATTRIBUTES.0);
1790
1791    /// Returns whether every bit in `fields` is selected.
1792    #[must_use]
1793    pub const fn contains(self, fields: Self) -> bool {
1794        self.0 & fields.0 == fields.0
1795    }
1796}
1797
1798impl std::ops::BitOr for DocumentFields {
1799    type Output = Self;
1800
1801    fn bitor(self, rhs: Self) -> Self::Output {
1802        Self(self.0 | rhs.0)
1803    }
1804}
1805
1806/// One live document copied from a single pinned store generation.
1807#[derive(Clone, Debug, PartialEq)]
1808pub struct StoredDocument {
1809    /// Stable application document identifier.
1810    pub doc_id: crate::ingest::DocId,
1811    /// Live monotonic revision.
1812    pub revision: crate::ingest::Revision,
1813    /// Canonical clustering timestamp.
1814    pub timestamp: i64,
1815    /// Selected full-precision vector, or `None` when not requested.
1816    pub vector: Option<Vec<f32>>,
1817    /// Selected stored UTF-8 text, or `None` when absent or not requested.
1818    pub text: Option<String>,
1819    /// Selected opaque metadata, or `None` when absent or not requested.
1820    pub metadata: Option<Vec<u8>>,
1821    /// Selected non-null typed attributes, or `None` when not requested.
1822    pub attributes: Option<Vec<(crate::meta::ColumnId, crate::meta::PredicateValue)>>,
1823}
1824
1825/// Deterministic ordering used by [`Store::scan_documents`].
1826#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1827pub enum ScanOrder {
1828    /// Immutable segments in snapshot order and then the active segment, with
1829    /// ascending row numbers inside every source.
1830    #[default]
1831    Storage,
1832    /// Ascending timestamp with ascending document id as the tie breaker.
1833    TimestampAscending,
1834    /// Descending timestamp with ascending document id as the tie breaker.
1835    TimestampDescending,
1836    /// A declared `U64`, `I64` or `F64` user attribute in `direction`, with
1837    /// ascending document id as the tie breaker. `F64` compares numerically
1838    /// and `-0.0` equals `+0.0`. A row whose value is missing or NaN has no
1839    /// orderable value and sorts after every orderable row in both
1840    /// directions.
1841    Attribute {
1842        /// Declared user attribute that supplies the sort key.
1843        column: crate::meta::ColumnId,
1844        /// Direction applied to orderable values.
1845        direction: ScanDirection,
1846    },
1847}
1848
1849/// Direction of an attribute-ordered document scan.
1850#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1851pub enum ScanDirection {
1852    /// Smallest value first.
1853    Ascending,
1854    /// Largest value first.
1855    Descending,
1856}
1857
1858/// Source position named by a document-scan continuation.
1859#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1860pub enum DocumentScanSource {
1861    /// An immutable segment and its stable identifier.
1862    Sealed(crate::segment::SegmentId),
1863    /// The active in-memory segment.
1864    Active,
1865}
1866
1867/// Opaque-to-callers continuation for one published store generation.
1868#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1869pub struct DocumentScanCursor {
1870    /// Generation pinned by the page that produced this cursor.
1871    pub generation: u64,
1872    /// Order of the scan that issued this cursor; a request with any other
1873    /// order rejects it.
1874    pub order: ScanOrder,
1875    /// Source containing the next matching row.
1876    pub source: DocumentScanSource,
1877    /// Source-local row at which the next page starts.
1878    pub next_row: u32,
1879}
1880
1881/// One bounded document-scan request.
1882#[derive(Clone, Debug)]
1883pub struct DocumentScanRequest<'a> {
1884    limit: usize,
1885    fields: DocumentFields,
1886    order: ScanOrder,
1887    cursor: Option<DocumentScanCursor>,
1888    timestamp_range: Option<(i64, i64)>,
1889    predicate: Option<&'a crate::meta::Predicate>,
1890    control: QueryControl,
1891}
1892
1893impl<'a> DocumentScanRequest<'a> {
1894    /// Creates a storage-ordered scan without a filter or timestamp bound.
1895    #[must_use]
1896    pub fn new(limit: usize, fields: DocumentFields, control: QueryControl) -> Self {
1897        Self {
1898            limit,
1899            fields,
1900            order: ScanOrder::Storage,
1901            cursor: None,
1902            timestamp_range: None,
1903            predicate: None,
1904            control,
1905        }
1906    }
1907
1908    /// Selects the deterministic row ordering.
1909    #[must_use]
1910    pub const fn with_order(mut self, order: ScanOrder) -> Self {
1911        self.order = order;
1912        self
1913    }
1914
1915    /// Resumes at a continuation returned by an earlier page.
1916    #[must_use]
1917    pub const fn with_cursor(mut self, cursor: DocumentScanCursor) -> Self {
1918        self.cursor = Some(cursor);
1919        self
1920    }
1921
1922    /// Restricts timestamps to the half-open interval `[start, end)`.
1923    #[must_use]
1924    pub const fn with_timestamp_range(mut self, start: i64, end: i64) -> Self {
1925        self.timestamp_range = Some((start, end));
1926        self
1927    }
1928
1929    /// Applies one exact typed metadata predicate.
1930    #[must_use]
1931    pub const fn with_predicate(mut self, predicate: &'a crate::meta::Predicate) -> Self {
1932        self.predicate = Some(predicate);
1933        self
1934    }
1935}
1936
1937/// One bounded page returned by [`Store::scan_documents`].
1938#[derive(Clone, Debug, PartialEq)]
1939pub struct DocumentScanPage {
1940    /// Materialized live documents in the requested deterministic order.
1941    pub documents: Vec<StoredDocument>,
1942    /// Store generation pinned for the complete call.
1943    pub generation: u64,
1944    /// Position of the next matching row, or `None` on the last page.
1945    pub continuation: Option<DocumentScanCursor>,
1946}
1947
1948/// Exact live-row count from one pinned store generation.
1949#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1950pub struct DocumentCount {
1951    /// Number of live rows matching the requested bounds.
1952    pub count: u64,
1953    /// Store generation pinned for the complete count.
1954    pub generation: u64,
1955}
1956
1957/// Largest document page accepted by the core and C ABI.
1958pub const MAX_DOCUMENT_SCAN_LIMIT: usize = 1 << 20;
1959
1960/// An open, lifecycle, or close operation was rejected.
1961#[derive(Debug)]
1962pub enum StoreError {
1963    /// A graph rule or coordinated document mutation refused retention.
1964    #[cfg(feature = "graph-cypher")]
1965    DocumentMutation(Box<crate::ingest::IngestError>),
1966    /// A declaration would introduce a namespace ownership cycle.
1967    CascadeCycle {
1968        /// Closed path of namespace names, in traversal order.
1969        cycle: Vec<String>,
1970    },
1971    /// A filesystem operation failed for the named store path.
1972    Io {
1973        /// Store path being opened.
1974        path: PathBuf,
1975        /// Underlying filesystem failure.
1976        source: std::io::Error,
1977    },
1978    /// The supplied path exists but is not a directory.
1979    NotDirectory {
1980        /// Invalid store path.
1981        path: PathBuf,
1982    },
1983    /// Another writer owns the store's process or kernel lock.
1984    StoreBusy {
1985        /// Busy store directory.
1986        path: PathBuf,
1987    },
1988    /// The writer lock could not be opened or acquired.
1989    Lock(StoreLockError),
1990    /// The selected durability mode/tier is unsupported.
1991    Durability(DurabilityPolicyError),
1992    /// Runtime kernel selection or an explicit override was invalid.
1993    Kernel(crate::kernels::KernelInitError),
1994    /// The committed manifest could not be loaded or validated.
1995    Manifest(crate::manifest::ManifestError),
1996    /// The selected tokenizer configuration could not be compiled.
1997    Tokenizer(crate::fts::tokenizer::TokenizerError),
1998    /// The caller's declared interpretation differs from the persisted one.
1999    EpochMismatch(crate::epoch::EpochMismatch),
2000    /// Persisted bytes name an epoch but the caller did not declare one.
2001    EpochUndeclared,
2002    /// A pre-epoch manifest cannot be retroactively assigned an identity.
2003    EpochUnstamped,
2004    /// A schema declaration on reopen differed from the committed schema.
2005    SchemaMismatch {
2006        /// Schema already committed in the manifest.
2007        persisted: crate::meta::Schema,
2008        /// Schema supplied by the opening caller.
2009        declared: crate::meta::Schema,
2010    },
2011    /// A scan continuation names a different published generation.
2012    ScanStale {
2013        /// Generation encoded by the caller's continuation.
2014        cursor_generation: u64,
2015        /// Generation pinned by this scan call.
2016        current_generation: u64,
2017    },
2018    /// A document-scan request or predicate is invalid.
2019    InvalidScan {
2020        /// Precise validation failure.
2021        detail: String,
2022    },
2023    /// A grouped count found more distinct values than its group limit.
2024    GroupLimitExceeded {
2025        /// Group limit supplied by the caller.
2026        limit: usize,
2027    },
2028    /// A referenced immutable segment could not be mapped or validated.
2029    Segment(crate::segment::SegmentError),
2030    /// More than one live row carried the same stable document id.
2031    DuplicateLiveDocument {
2032        /// Stable id whose uniqueness invariant was violated.
2033        doc_id: crate::ingest::DocId,
2034    },
2035    /// The caller selected graph traversal for a sealed segment without a graph.
2036    GraphUnavailable {
2037        /// Immutable segment that cannot satisfy the explicit graph-tier request.
2038        segment_id: crate::segment::SegmentId,
2039    },
2040    /// The checked durable WAL prefix could not be opened.
2041    Wal(crate::wal::WalReadError),
2042    /// A non-empty WAL did not replay through a clean end boundary.
2043    WalRecovery(crate::wal::WalRecoveryError),
2044    /// A replayed record's retained encoded bytes could not be decoded.
2045    WalRecord {
2046        /// Sequence assigned to the invalid retained record.
2047        seq: crate::wal::LogSeq,
2048        /// Checked record-access failure.
2049        source: crate::wal::VisibleRecordError,
2050    },
2051    /// A checksummed WAL mutation payload violated its versioned contract.
2052    WalMutation {
2053        /// Sequence assigned to the invalid mutation.
2054        seq: crate::wal::LogSeq,
2055        /// Persisted operation identifier.
2056        op: u16,
2057        /// Typed mutation-payload failure.
2058        source: crate::ingest::wal_payload::PayloadError,
2059    },
2060    /// A recovered upsert would make one document's revision non-monotonic.
2061    WalRevisionOrder {
2062        /// Sequence assigned to the invalid upsert.
2063        seq: crate::wal::LogSeq,
2064        /// Document whose recovered history is invalid.
2065        doc_id: crate::ingest::DocId,
2066        /// Revision already rebuilt from the trusted prefix.
2067        current: crate::ingest::Revision,
2068        /// Revision carried by this record.
2069        attempted: crate::ingest::Revision,
2070    },
2071    /// A valid mutation kind has no active-segment application path yet.
2072    UnsupportedWalMutation {
2073        /// Sequence assigned to the unsupported mutation.
2074        seq: crate::wal::LogSeq,
2075        /// Persisted operation identifier.
2076        op: u16,
2077    },
2078    /// A recovered vector passed payload validation but failed quantization.
2079    WalVector {
2080        /// Sequence assigned to the invalid vector mutation.
2081        seq: crate::wal::LogSeq,
2082        /// Typed vector-contract failure.
2083        source: crate::quant::QuantError,
2084    },
2085    /// The store-owned WAL writer could not be created or advanced.
2086    WalWrite(crate::wal::WalWriteError),
2087    /// Retained WAL visibility could not be released through a committed seal.
2088    WalRetire(crate::wal::WalRetireError),
2089    /// A kernel probe needed for an exact statistics snapshot failed.
2090    Statistics {
2091        /// Counter or residency component that could not be read.
2092        component: &'static str,
2093        /// Underlying operating-system failure.
2094        source: std::io::Error,
2095    },
2096    /// An accounted allocation would exceed a configured ceiling.
2097    BudgetExceeded {
2098        /// Total live bytes that would be needed if the operation proceeded.
2099        needed: u64,
2100        /// Configured ceiling in bytes.
2101        budget: u64,
2102        /// Engine component requesting the allocation.
2103        component: &'static str,
2104    },
2105    /// Native recovery exhausted a checked work or scratch allowance.
2106    RecoveryLimit {
2107        /// Recovery resource whose allowance was exhausted.
2108        component: &'static str,
2109    },
2110    /// The allocator rejected a checked reservation without aborting.
2111    AllocationFailed {
2112        /// Exact requested bytes.
2113        needed: u64,
2114        /// Engine component requesting the allocation.
2115        component: &'static str,
2116    },
2117    /// An active vector did not match the collection's established dimension.
2118    DimensionMismatch {
2119        /// Established active dimension.
2120        expected: usize,
2121        /// Supplied vector dimension.
2122        actual: usize,
2123    },
2124    /// The active segment exceeded its dense u32 row address space.
2125    ActiveRowOverflow,
2126    /// An explicit seal was requested without any active rows.
2127    EmptyActiveSegment,
2128    /// Caller cancellation stopped a seal before its manifest commit point.
2129    SealCancelled,
2130    /// A write-only lifecycle operation was requested from a read-only handle.
2131    ReadOnly,
2132    /// WAL replay requires sealed tombstone repair that a read-only open cannot publish.
2133    SealedTombstoneRecoveryRequired,
2134    /// A prepared segment was accounted to a different store handle.
2135    ForeignPreparedSegment,
2136    /// The current snapshot generation cannot be incremented.
2137    GenerationOverflow,
2138    /// The active generation changed while a serialized writer prepared a commit.
2139    ConcurrentActiveMutation {
2140        /// Generation snapshotted before preparing the mutation.
2141        expected_generation: u64,
2142        /// Generation found when the writer re-acquired the active state.
2143        actual_generation: u64,
2144    },
2145    /// Exact reclaimed-byte reporting overflowed its u64 contract.
2146    PartitionBytesOverflow,
2147    /// A durable physical-purge intent could not be resumed during open.
2148    PurgeRecovery {
2149        /// Typed purge failure rendered without discarding its actionable values.
2150        detail: String,
2151    },
2152    /// A new operation raced with close after admissions stopped.
2153    Closing,
2154    /// The handle has completed teardown.
2155    Closed,
2156    /// Close cancelled an admitted read after its drain grace period elapsed.
2157    ReadCancelled,
2158    /// The lifecycle background thread could not be created.
2159    BackgroundStart {
2160        /// Operating-system thread creation failure.
2161        source: std::io::Error,
2162    },
2163    /// The lifecycle background thread exited before its startup handshake.
2164    BackgroundHandshake,
2165    /// The lifecycle background thread panicked before close joined it.
2166    BackgroundThreadPanicked,
2167    /// A persistent query worker thread could not be created.
2168    QueryPoolStart {
2169        /// Operating-system thread creation failure.
2170        source: std::io::Error,
2171    },
2172    /// A persistent query worker exited before its startup handshake.
2173    QueryPoolHandshake,
2174    /// A persistent query worker panicked before close joined it.
2175    QueryPoolThreadPanicked,
2176    /// The operating system could not report a usable query-worker count.
2177    QueryPoolCapacity {
2178        /// CPU-topology failure text.
2179        source: String,
2180    },
2181    /// A lifecycle synchronization primitive was poisoned.
2182    Synchronization {
2183        /// Synchronization component that rejected the operation.
2184        component: &'static str,
2185    },
2186    /// The directory holds a native graph store, which this legacy store
2187    /// cannot open. Nothing in the directory was changed.
2188    NativeGraphDirectory {
2189        /// Native graph store directory.
2190        path: PathBuf,
2191    },
2192    /// A snapshot target cannot receive a snapshot. Nothing was written.
2193    SnapshotTarget {
2194        /// Target path as the caller supplied it.
2195        path: PathBuf,
2196        /// Why the target was rejected.
2197        reason: SnapshotTargetReason,
2198    },
2199    /// The state pinned for a snapshot failed a consistency check. Nothing
2200    /// was published.
2201    SnapshotPin {
2202        /// The violated pin invariant.
2203        detail: &'static str,
2204    },
2205    /// A physical purge is pending; a snapshot would copy bytes it must
2206    /// remove. Await the purge, then snapshot. Nothing was written.
2207    SnapshotPurgePending,
2208    /// A physical removal must wait for open in-place snapshots to close.
2209    SnapshotViewsOpen,
2210}
2211
2212impl std::fmt::Display for StoreError {
2213    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2214        match self {
2215            #[cfg(feature = "graph-cypher")]
2216            Self::DocumentMutation(error) => error.fmt(formatter),
2217            Self::CascadeCycle { cycle } => {
2218                write!(formatter, "cascade cycle: {}", cycle.join(" -> "))
2219            }
2220            Self::Io { path, source } => {
2221                write!(formatter, "store I/O {}: {source}", path.display())
2222            }
2223            Self::NotDirectory { path } => {
2224                write!(
2225                    formatter,
2226                    "store path is not a directory: {}",
2227                    path.display()
2228                )
2229            }
2230            Self::SnapshotViewsOpen => {
2231                formatter.write_str("close open snapshots before physical removal")
2232            }
2233            Self::StoreBusy { path } => {
2234                write!(formatter, "store already has a writer: {}", path.display())
2235            }
2236            Self::Lock(error) => error.fmt(formatter),
2237            Self::Durability(error) => error.fmt(formatter),
2238            Self::Kernel(error) => error.fmt(formatter),
2239            Self::Manifest(error) => error.fmt(formatter),
2240            Self::Tokenizer(error) => write!(formatter, "store tokenizer: {error}"),
2241            Self::EpochMismatch(error) => error.fmt(formatter),
2242            Self::EpochUndeclared => {
2243                formatter.write_str("store epoch is persisted but the caller declared none")
2244            }
2245            Self::EpochUnstamped => formatter
2246                .write_str("store manifest predates epoch identity and cannot adopt a declaration"),
2247            Self::SchemaMismatch {
2248                persisted,
2249                declared,
2250            } => match persisted.additive_evolution(declared) {
2251                Err(conflict) => write!(
2252                    formatter,
2253                    "declared store schema does not match the persisted schema: {conflict}"
2254                ),
2255                // An additive declaration is refused only by a read-only
2256                // open, which cannot commit the evolved schema.
2257                Ok(Some(_)) => {
2258                    formatter.write_str(
2259                        "declared store schema adds attributes that a read-only open \
2260                         cannot persist:",
2261                    )?;
2262                    for added in declared
2263                        .columns()
2264                        .iter()
2265                        .filter(|column| persisted.column(column.id()).is_none())
2266                    {
2267                        write!(formatter, " '{}' (id {})", added.name(), added.id().get())?;
2268                    }
2269                    Ok(())
2270                }
2271                Ok(None) => write!(
2272                    formatter,
2273                    "declared store schema {declared:?} does not match persisted schema \
2274                     {persisted:?}"
2275                ),
2276            },
2277            Self::ScanStale {
2278                cursor_generation,
2279                current_generation,
2280            } => write!(
2281                formatter,
2282                "scan cursor generation {cursor_generation} differs from current generation {current_generation}"
2283            ),
2284            Self::InvalidScan { detail } => write!(formatter, "invalid document scan: {detail}"),
2285            Self::GroupLimitExceeded { limit } => write!(
2286                formatter,
2287                "grouped count found more than {limit} distinct values; raise the group limit"
2288            ),
2289            Self::Segment(error) => error.fmt(formatter),
2290            Self::DuplicateLiveDocument { doc_id } => {
2291                write!(
2292                    formatter,
2293                    "document {} has multiple live rows",
2294                    doc_id.get()
2295                )
2296            }
2297            Self::GraphUnavailable { segment_id } => {
2298                write!(formatter, "sealed segment {segment_id} has no graph region")
2299            }
2300            Self::Wal(error) => error.fmt(formatter),
2301            Self::WalRecovery(error) => error.fmt(formatter),
2302            Self::WalRecord { seq, source } => {
2303                write!(
2304                    formatter,
2305                    "WAL sequence {} retained record: {source}",
2306                    seq.get()
2307                )
2308            }
2309            Self::WalMutation { seq, op, source } => write!(
2310                formatter,
2311                "WAL sequence {} operation {op} payload: {source}",
2312                seq.get()
2313            ),
2314            Self::WalRevisionOrder {
2315                seq,
2316                doc_id,
2317                current,
2318                attempted,
2319            } => write!(
2320                formatter,
2321                "WAL sequence {} document {} revision {} does not follow {}",
2322                seq.get(),
2323                doc_id.get(),
2324                attempted.get(),
2325                current.get()
2326            ),
2327            Self::UnsupportedWalMutation { seq, op } => write!(
2328                formatter,
2329                "WAL sequence {} operation {op} has no active-segment recovery path",
2330                seq.get()
2331            ),
2332            Self::WalVector { seq, source } => {
2333                write!(formatter, "WAL sequence {} vector: {source}", seq.get())
2334            }
2335            Self::WalWrite(error) => error.fmt(formatter),
2336            Self::WalRetire(error) => error.fmt(formatter),
2337            Self::Statistics { component, source } => {
2338                write!(formatter, "store statistics {component}: {source}")
2339            }
2340            Self::RecoveryLimit { component } => {
2341                write!(formatter, "graph recovery {component} allowance exhausted")
2342            }
2343            Self::BudgetExceeded {
2344                needed,
2345                budget,
2346                component,
2347            } => write!(
2348                formatter,
2349                "store {component} allocation needs {needed} bytes, budget is {budget} bytes"
2350            ),
2351            Self::AllocationFailed { needed, component } => write!(
2352                formatter,
2353                "store {component} allocator rejected {needed} bytes"
2354            ),
2355            Self::DimensionMismatch { expected, actual } => write!(
2356                formatter,
2357                "active vector dimension {actual} does not match {expected}"
2358            ),
2359            Self::ActiveRowOverflow => {
2360                formatter.write_str("active segment row or byte geometry overflow")
2361            }
2362            Self::EmptyActiveSegment => formatter.write_str("active segment is empty"),
2363            Self::SealCancelled => formatter.write_str("seal was cancelled before commit"),
2364            Self::ReadOnly => formatter.write_str("store handle is read-only"),
2365            Self::SealedTombstoneRecoveryRequired => formatter.write_str(
2366                "store requires writable sealed-tombstone recovery before reads are safe",
2367            ),
2368            Self::ForeignPreparedSegment => {
2369                formatter.write_str("prepared segment belongs to another store")
2370            }
2371            Self::GenerationOverflow => formatter.write_str("store snapshot generation overflow"),
2372            Self::ConcurrentActiveMutation {
2373                expected_generation,
2374                actual_generation,
2375            } => write!(
2376                formatter,
2377                "active generation changed from {expected_generation} to {actual_generation} while preparing a commit"
2378            ),
2379            Self::PartitionBytesOverflow => {
2380                formatter.write_str("partition reclaimed-byte count overflow")
2381            }
2382            Self::PurgeRecovery { detail } => {
2383                write!(formatter, "physical purge recovery failed: {detail}")
2384            }
2385            Self::Closing => formatter.write_str("store is closing"),
2386            Self::Closed => formatter.write_str("store is closed"),
2387            Self::ReadCancelled => formatter.write_str("store close cancelled the admitted read"),
2388            Self::BackgroundStart { source } => {
2389                write!(
2390                    formatter,
2391                    "store lifecycle thread could not start: {source}"
2392                )
2393            }
2394            Self::BackgroundHandshake => {
2395                formatter.write_str("store lifecycle thread startup handshake failed")
2396            }
2397            Self::BackgroundThreadPanicked => {
2398                formatter.write_str("store lifecycle thread panicked")
2399            }
2400            Self::QueryPoolStart { source } => {
2401                write!(formatter, "store query worker could not start: {source}")
2402            }
2403            Self::QueryPoolHandshake => {
2404                formatter.write_str("store query worker startup handshake failed")
2405            }
2406            Self::QueryPoolThreadPanicked => formatter.write_str("store query worker panicked"),
2407            Self::QueryPoolCapacity { source } => {
2408                write!(formatter, "store query worker capacity failed: {source}")
2409            }
2410            Self::Synchronization { component } => {
2411                write!(
2412                    formatter,
2413                    "store lifecycle synchronization poisoned: {component}"
2414                )
2415            }
2416            Self::NativeGraphDirectory { path } => write!(
2417                formatter,
2418                "native graph directory cannot be opened as a legacy store: {}",
2419                path.display()
2420            ),
2421            Self::SnapshotTarget { path, reason } => {
2422                write!(formatter, "snapshot target {} {reason}", path.display())
2423            }
2424            Self::SnapshotPin { detail } => {
2425                write!(formatter, "snapshot pin is inconsistent: {detail}")
2426            }
2427            Self::SnapshotPurgePending => formatter
2428                .write_str("a physical purge is pending; await it before taking a snapshot"),
2429        }
2430    }
2431}
2432
2433/// Coarse, exhaustive classification of a [`StoreError`], stable for hosts
2434/// that map engine failures onto their own status codes. Every variant of
2435/// [`StoreError`] maps to exactly one kind; adding a variant is a compile
2436/// error here until it is classified.
2437#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
2438pub enum StoreErrorKind {
2439    /// An operating-system or filesystem operation failed.
2440    Io,
2441    /// A caller-supplied path or schema was invalid.
2442    InvalidArgument,
2443    /// Another process or handle owns the store's writer lock.
2444    StoreBusy,
2445    /// The requested mode or operation is unsupported.
2446    Unsupported,
2447    /// Persisted data failed validation.
2448    Corrupt,
2449    /// A configured memory, disk, or work budget was exceeded.
2450    BudgetExceeded,
2451    /// A checked allocation could not be reserved.
2452    OutOfMemory,
2453    /// Vector dimensions disagreed.
2454    DimensionMismatch,
2455    /// The declared epoch differs from the persisted identity.
2456    EpochMismatch,
2457    /// The store requires an epoch declaration and none was supplied.
2458    EpochUndeclared,
2459    /// An epoch was declared for a store with no stamped identity.
2460    EpochUnstamped,
2461    /// An internal invariant failed.
2462    Internal,
2463    /// A batch or active segment was empty.
2464    EmptyBatch,
2465    /// Cooperative cancellation stopped the operation.
2466    Cancelled,
2467    /// The store is read-only.
2468    ReadOnly,
2469    /// The store is closing.
2470    Closing,
2471    /// The store is closed.
2472    Closed,
2473    /// A background or query-pool thread panicked.
2474    Panic,
2475    /// A synchronization primitive was poisoned or unavailable.
2476    Synchronization,
2477}
2478
2479impl StoreError {
2480    /// Classifies this error; see [`StoreErrorKind`].
2481    #[must_use]
2482    pub fn kind(&self) -> StoreErrorKind {
2483        match self {
2484            Self::WalWrite(crate::wal::WalWriteError::GroupTooLarge { .. }) => {
2485                StoreErrorKind::BudgetExceeded
2486            }
2487            #[cfg(feature = "graph-cypher")]
2488            Self::DocumentMutation(error) => match error.as_ref() {
2489                crate::ingest::IngestError::Store(error) => error.kind(),
2490                _ => StoreErrorKind::InvalidArgument,
2491            },
2492            Self::Io { .. }
2493            // A failed manifest publication is I/O, not damaged persisted bytes.
2494            | Self::Manifest(crate::manifest::ManifestError::Io { .. })
2495            | Self::Lock(_)
2496            | Self::Statistics { .. }
2497            | Self::BackgroundStart { .. }
2498            | Self::QueryPoolStart { .. }
2499            | Self::WalWrite(_)
2500            | Self::WalRetire(_) => StoreErrorKind::Io,
2501            Self::CascadeCycle { .. }
2502            | Self::NotDirectory { .. }
2503            | Self::Tokenizer(_)
2504            | Self::SchemaMismatch { .. }
2505            | Self::ScanStale { .. }
2506            | Self::InvalidScan { .. }
2507            | Self::NativeGraphDirectory { .. }
2508            | Self::SnapshotTarget { .. } => StoreErrorKind::InvalidArgument,
2509            Self::StoreBusy { .. } | Self::SnapshotViewsOpen => StoreErrorKind::StoreBusy,
2510            Self::Manifest(crate::manifest::ManifestError::GraphUnsupportedBuild)
2511            | Self::Durability(_)
2512            | Self::Kernel(_)
2513            | Self::GraphUnavailable { .. }
2514            | Self::UnsupportedWalMutation { .. }
2515            | Self::SnapshotPurgePending => StoreErrorKind::Unsupported,
2516            Self::Manifest(_)
2517            | Self::Segment(_)
2518            | Self::DuplicateLiveDocument { .. }
2519            | Self::Wal(_)
2520            | Self::WalRecovery(_)
2521            | Self::WalRecord { .. }
2522            | Self::WalMutation { .. }
2523            | Self::WalRevisionOrder { .. }
2524            | Self::WalVector { .. }
2525            | Self::PurgeRecovery { .. } => StoreErrorKind::Corrupt,
2526            Self::BudgetExceeded { .. } | Self::GroupLimitExceeded { .. } | Self::RecoveryLimit { .. } => {
2527                StoreErrorKind::BudgetExceeded
2528            }
2529            Self::AllocationFailed { .. } => StoreErrorKind::OutOfMemory,
2530            Self::DimensionMismatch { .. } => StoreErrorKind::DimensionMismatch,
2531            Self::EpochMismatch(_) => StoreErrorKind::EpochMismatch,
2532            Self::EpochUndeclared => StoreErrorKind::EpochUndeclared,
2533            Self::EpochUnstamped => StoreErrorKind::EpochUnstamped,
2534            Self::ActiveRowOverflow
2535            | Self::GenerationOverflow
2536            | Self::ConcurrentActiveMutation { .. }
2537            | Self::PartitionBytesOverflow
2538            | Self::ForeignPreparedSegment
2539            | Self::BackgroundHandshake
2540            | Self::QueryPoolHandshake
2541            | Self::QueryPoolCapacity { .. }
2542            | Self::SnapshotPin { .. } => StoreErrorKind::Internal,
2543            Self::EmptyActiveSegment => StoreErrorKind::EmptyBatch,
2544            Self::SealCancelled | Self::ReadCancelled => StoreErrorKind::Cancelled,
2545            Self::ReadOnly | Self::SealedTombstoneRecoveryRequired => StoreErrorKind::ReadOnly,
2546            Self::Closing => StoreErrorKind::Closing,
2547            Self::Closed => StoreErrorKind::Closed,
2548            Self::BackgroundThreadPanicked | Self::QueryPoolThreadPanicked => StoreErrorKind::Panic,
2549            Self::Synchronization { .. } => StoreErrorKind::Synchronization,
2550        }
2551    }
2552}
2553
2554impl std::error::Error for StoreError {
2555    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2556        match self {
2557            #[cfg(feature = "graph-cypher")]
2558            Self::DocumentMutation(error) => Some(error.as_ref()),
2559            Self::Io { source, .. } => Some(source),
2560            Self::Lock(error) => Some(error),
2561            Self::Durability(error) => Some(error),
2562            Self::Kernel(error) => Some(error),
2563            Self::Manifest(error) => Some(error),
2564            Self::Tokenizer(error) => Some(error),
2565            Self::EpochMismatch(error) => Some(error),
2566            Self::Segment(error) => Some(error),
2567            Self::Wal(error) => Some(error),
2568            Self::WalRecovery(error) => Some(error),
2569            Self::WalRecord { source, .. } => Some(source),
2570            Self::WalMutation { source, .. } => Some(source),
2571            Self::WalVector { source, .. } => Some(source),
2572            Self::WalWrite(error) => Some(error),
2573            Self::WalRetire(error) => Some(error),
2574            Self::Statistics { source, .. } => Some(source),
2575            Self::BackgroundStart { source } => Some(source),
2576            Self::QueryPoolStart { source } => Some(source),
2577            Self::CascadeCycle { .. }
2578            | Self::NotDirectory { .. }
2579            | Self::StoreBusy { .. }
2580            | Self::EpochUndeclared
2581            | Self::EpochUnstamped
2582            | Self::SchemaMismatch { .. }
2583            | Self::ScanStale { .. }
2584            | Self::InvalidScan { .. }
2585            | Self::GraphUnavailable { .. }
2586            | Self::DuplicateLiveDocument { .. }
2587            | Self::WalRevisionOrder { .. }
2588            | Self::UnsupportedWalMutation { .. }
2589            | Self::BudgetExceeded { .. }
2590            | Self::RecoveryLimit { .. }
2591            | Self::GroupLimitExceeded { .. }
2592            | Self::AllocationFailed { .. }
2593            | Self::DimensionMismatch { .. }
2594            | Self::ActiveRowOverflow
2595            | Self::EmptyActiveSegment
2596            | Self::SealCancelled
2597            | Self::ReadOnly
2598            | Self::SealedTombstoneRecoveryRequired
2599            | Self::ForeignPreparedSegment
2600            | Self::GenerationOverflow
2601            | Self::ConcurrentActiveMutation { .. }
2602            | Self::PartitionBytesOverflow
2603            | Self::PurgeRecovery { .. }
2604            | Self::Closing
2605            | Self::Closed
2606            | Self::ReadCancelled
2607            | Self::BackgroundHandshake
2608            | Self::BackgroundThreadPanicked
2609            | Self::QueryPoolHandshake
2610            | Self::QueryPoolThreadPanicked
2611            | Self::QueryPoolCapacity { .. }
2612            | Self::Synchronization { .. }
2613            | Self::NativeGraphDirectory { .. }
2614            | Self::SnapshotTarget { .. }
2615            | Self::SnapshotPin { .. }
2616            | Self::SnapshotPurgePending
2617            | Self::SnapshotViewsOpen => None,
2618        }
2619    }
2620}
2621
2622/// Changes completed by this open. Format versions remain manifest v2 / WAL v1.
2623#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
2624pub struct OpenMigrations {
2625    /// A nullable schema extension was committed.
2626    pub schema_added: bool,
2627    /// An incomplete final WAL record was cut atomically.
2628    pub wal_tail_cut: bool,
2629    /// Generation visible when open completed.
2630    pub generation: u64,
2631}
2632
2633/// One explicitly closeable embedded-store handle.
2634pub struct Store {
2635    open_migrations: OpenMigrations,
2636    pub(crate) private_preparation: Option<namespace_batch::PrivatePreparation>,
2637    pub(crate) directory: PathBuf,
2638    pub(crate) vfs: Arc<dyn crate::vfs::Vfs>,
2639    pub(crate) clock: Arc<dyn MonotonicClock>,
2640    pub(crate) state: Mutex<StoreState>,
2641    pub(crate) state_changed: Condvar,
2642    pub(crate) background: Mutex<Option<BackgroundThread>>,
2643    pub(crate) query_pool: Mutex<Option<Arc<pool::QueryPool>>>,
2644    pub(crate) lexical_worker: Mutex<Option<Arc<pool::LexicalWorker>>>,
2645    #[cfg(feature = "graph-cypher")]
2646    pub(crate) native_graph: Arc<native_graph::NativeGraphPublication>,
2647    #[cfg(feature = "graph-cypher")]
2648    graph_enable_pending: std::sync::atomic::AtomicBool,
2649    pub(crate) snapshot: RwLock<Option<Arc<PublishedSnapshot>>>,
2650    // Drop order is deliberate: immutable mappings, active buffers, and the
2651    // WAL descriptor all release before the kernel writer lock.
2652    pub(crate) active: Mutex<Option<crate::ingest::ActiveState>>,
2653    pub(crate) wal_writer: Mutex<Option<crate::ingest::StoreWal>>,
2654    pub(crate) writer_lock: Mutex<Option<Arc<StoreLock>>>,
2655    #[cfg(feature = "graph-cypher")]
2656    reader_store_lock: Mutex<Option<StoreLock>>,
2657    snapshot_pins: Arc<AtomicU64>,
2658    snapshot_pin: Mutex<Option<snapshot_view::SnapshotPin>>,
2659    logical_writer_lock: Mutex<Option<StoreLock>>,
2660    reclamation_pin: Mutex<Option<Arc<StoreLock>>>,
2661    pub(crate) maintenance: Mutex<()>,
2662    pub(crate) health_state: Mutex<crate::diag::HealthState>,
2663    pub(crate) durability_policy: DurabilityPolicy,
2664    pub(crate) reader_drain_timeout: Duration,
2665    pub(crate) accounting: Arc<stats::Accounting>,
2666    pub(crate) active_queries: AtomicU64,
2667    #[cfg(any(test, feature = "test-seams"))]
2668    text_materialization_work: materialize::TestMaterializationWork,
2669    pub(crate) epoch: Option<crate::epoch::StoreEpoch>,
2670    pub(crate) epoch_alias: crate::epoch::EpochAliasCell,
2671    pub(crate) tokenizer: crate::fts::tokenizer::Analyzer,
2672    pub(crate) schema: crate::meta::Schema,
2673    // Holds only `Weak` handles to the snapshot and active segment it was
2674    // built from, so it never delays their release; see
2675    // `CachedLexicalAssembly`.
2676    lexical_index_cache: LexicalIndexCache,
2677    #[cfg(any(test, feature = "test-seams"))]
2678    pub(crate) ingest_retention_fault_controller:
2679        Option<crate::ingest::IngestRetentionFaultController>,
2680    #[cfg(any(test, feature = "test-seams"))]
2681    hybrid_leg_fault: Mutex<Option<HybridLegTestFault>>,
2682    #[cfg(any(test, feature = "test-seams"))]
2683    hybrid_execution_receipt: Mutex<Option<HybridExecutionReceipt>>,
2684    #[cfg(any(test, feature = "test-seams"))]
2685    pub(crate) metadata_test_controller: Option<Arc<crate::planner::MetadataTestController>>,
2686    #[cfg(any(test, feature = "test-seams"))]
2687    pub(crate) vector_fault_controller: Option<crate::scan::vector_fault::VectorFaultController>,
2688    #[cfg(any(test, feature = "test-seams"))]
2689    pub(crate) kernel_fault_controller: Option<crate::kernels::vector_fault::KernelFaultController>,
2690    #[cfg(any(test, feature = "test-seams"))]
2691    pub(crate) vector_seal_scheme: Option<crate::quant::QuantScheme>,
2692    #[cfg(test)]
2693    pub(crate) teardown_probe: Arc<close::TeardownProbe>,
2694}
2695
2696fn acquire_writer_lock(
2697    path: &Path,
2698    access_mode: AccessMode,
2699) -> Result<Option<StoreLock>, StoreError> {
2700    Ok(match access_mode {
2701        AccessMode::ReadWrite => Some(StoreLock::acquire(path).map_err(|error| match error {
2702            StoreLockError::Io { path, source }
2703                if source.kind() == std::io::ErrorKind::WouldBlock =>
2704            {
2705                StoreError::StoreBusy { path }
2706            }
2707            StoreLockError::Io { path, source } => {
2708                StoreError::Lock(StoreLockError::Io { path, source })
2709            }
2710        })?),
2711        AccessMode::ReadOnly => None,
2712    })
2713}
2714
2715fn refuse_native_graph_directory(vfs: &dyn crate::vfs::Vfs, path: &Path) -> Result<(), StoreError> {
2716    match vfs.ensure_directory(path, false) {
2717        Ok(true) => {}
2718        Ok(false) => return Ok(()),
2719        Err(source) if source.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2720        Err(source) => {
2721            return Err(StoreError::Io {
2722                path: path.to_path_buf(),
2723                source,
2724            });
2725        }
2726    }
2727    // A catalog protected before a v3 commit can be an orphan beside a v2
2728    // document store after a crash. Only selector/WAL files mark the legacy
2729    // graph format there; graph-only directories remain refused.
2730    let document_store = is_legacy_store_directory(vfs, path)?;
2731    let mut native = false;
2732    vfs.for_each_direct_child(path, &mut |child| {
2733        let Some(name) = child.file_name().and_then(|name| name.to_str()) else {
2734            return Ok(());
2735        };
2736        if name == "graph-root.ze"
2737            || (name.starts_with("graph-wal-") && name.ends_with(".ze"))
2738            || (!document_store && name.starts_with("graph-") && name.ends_with(".zgraph"))
2739            || (name.starts_with("graph-root-") && name.ends_with(".tmp"))
2740        {
2741            native = true;
2742        }
2743        Ok(())
2744    })
2745    .map_err(|source| StoreError::Io {
2746        path: path.to_path_buf(),
2747        source,
2748    })?;
2749    if native {
2750        return Err(StoreError::NativeGraphDirectory {
2751            path: path.to_path_buf(),
2752        });
2753    }
2754    Ok(())
2755}
2756
2757/// True when `path` is a directory holding a legacy store's manifest or WAL.
2758/// A missing path or a non-directory is not a legacy store; the graph open
2759/// path reports those itself. It only opens candidate files for reading.
2760pub(crate) fn is_legacy_store_directory(
2761    vfs: &dyn crate::vfs::Vfs,
2762    path: &Path,
2763) -> Result<bool, StoreError> {
2764    for name in [
2765        crate::manifest::io::MANIFEST_FILE,
2766        crate::manifest::io::MANIFEST_TEMP_FILE,
2767        "wal.ze",
2768    ] {
2769        let candidate = path.join(name);
2770        match vfs.open(&candidate) {
2771            Ok(_) => return Ok(true),
2772            Err(source)
2773                if matches!(
2774                    source.kind(),
2775                    std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
2776                ) => {}
2777            Err(source) => {
2778                return Err(StoreError::Io {
2779                    path: candidate,
2780                    source,
2781                });
2782            }
2783        }
2784    }
2785    Ok(false)
2786}
2787
2788/// Returns the schema the opened store serves and whether it is an additive
2789/// evolution of the committed schema that open must commit before publishing.
2790fn resolve_open_schema(
2791    manifest_exists: bool,
2792    snapshot: &PublishedSnapshot,
2793    declared: Option<&crate::meta::Schema>,
2794    access_mode: AccessMode,
2795) -> Result<(crate::meta::Schema, bool), StoreError> {
2796    if !manifest_exists {
2797        return Ok((
2798            declared
2799                .cloned()
2800                .unwrap_or_else(crate::meta::Schema::timestamp_only),
2801            false,
2802        ));
2803    }
2804    let persisted = snapshot.schema();
2805    let Some(declared) = declared else {
2806        return Ok((persisted.clone(), false));
2807    };
2808    let mismatch = || StoreError::SchemaMismatch {
2809        persisted: persisted.clone(),
2810        declared: declared.clone(),
2811    };
2812    match persisted.additive_evolution(declared) {
2813        Ok(None) => Ok((persisted.clone(), false)),
2814        Ok(Some(evolved)) if access_mode == AccessMode::ReadWrite => Ok((evolved, true)),
2815        Ok(Some(_)) | Err(_) => Err(mismatch()),
2816    }
2817}
2818
2819fn validate_persisted_epoch_before_open(
2820    vfs: &dyn crate::vfs::Vfs,
2821    path: &Path,
2822    options: &OpenOptions,
2823) -> Result<(), StoreError> {
2824    let manifest_path = path.join(crate::manifest::io::MANIFEST_FILE);
2825    let bytes = match vfs.read(&manifest_path) {
2826        Ok(bytes) => bytes,
2827        Err(source) if source.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2828        Err(source) => {
2829            return Err(StoreError::Io {
2830                path: manifest_path,
2831                source,
2832            });
2833        }
2834    };
2835    let manifest = crate::manifest::decode_manifest(&manifest_path.display().to_string(), &bytes)
2836        .map_err(StoreError::Manifest)?;
2837    let declared = if options.inspection {
2838        manifest.epoch_alias
2839    } else {
2840        options
2841            .epoch
2842            .as_ref()
2843            .map(crate::epoch::StoreEpoch::identity)
2844    };
2845    validate_epoch_identity(manifest.epoch_alias, declared, true, options.access_mode)
2846}
2847
2848fn validate_epoch_identity(
2849    persisted: Option<crate::epoch::EpochIdentity>,
2850    declared: Option<crate::epoch::EpochIdentity>,
2851    manifest_exists: bool,
2852    access_mode: AccessMode,
2853) -> Result<(), StoreError> {
2854    match (persisted, declared) {
2855        (Some(expected), Some(declared)) if expected != declared => {
2856            Err(StoreError::EpochMismatch(crate::epoch::EpochMismatch {
2857                expected,
2858                declared,
2859            }))
2860        }
2861        (Some(_), None) => Err(StoreError::EpochUndeclared),
2862        (None, Some(_)) if manifest_exists || access_mode == AccessMode::ReadOnly => {
2863            Err(StoreError::EpochUnstamped)
2864        }
2865        (Some(_), Some(_)) | (None, Some(_)) | (None, None) => Ok(()),
2866    }
2867}
2868
2869fn validate_tokenizer_epoch(
2870    expected: Option<crate::epoch::EpochIdentity>,
2871    tokenizer: &crate::fts::tokenizer::Analyzer,
2872) -> Result<(), StoreError> {
2873    if let Some(expected) = expected
2874        && tokenizer.epoch() != expected.tokenizer
2875    {
2876        return Err(StoreError::EpochMismatch(crate::epoch::EpochMismatch {
2877            expected,
2878            declared: crate::epoch::EpochIdentity {
2879                embedding: expected.embedding,
2880                tokenizer: tokenizer.epoch(),
2881            },
2882        }));
2883    }
2884    Ok(())
2885}
2886
2887fn cleanup_open_orphans(
2888    store: &Store,
2889) -> Result<crate::manifest::io::OrphanCleanupReport, StoreError> {
2890    let (reachable_segments, graph_enabled) = store
2891        .snapshot
2892        .read()
2893        .map_err(|_| StoreError::Synchronization {
2894            component: "published snapshot",
2895        })?
2896        .as_ref()
2897        .map(|snapshot| {
2898            let reachable = snapshot
2899                .all_segments()
2900                .iter()
2901                .map(|segment| store.directory.join(segment.meta().id.file_name()))
2902                .collect::<HashSet<_>>();
2903            #[cfg(feature = "graph-cypher")]
2904            let graph_enabled = snapshot.graph_enabled;
2905            #[cfg(not(feature = "graph-cypher"))]
2906            let graph_enabled = false;
2907            (reachable, graph_enabled)
2908        })
2909        .ok_or(StoreError::Closed)?;
2910    let mut publication = Some(
2911        store
2912            .wal_writer
2913            .lock()
2914            .map_err(|_| StoreError::Synchronization {
2915                component: "WAL writer",
2916            })?
2917            .as_ref()
2918            .ok_or(StoreError::ReadOnly)?
2919            .manifest_publication()?,
2920    );
2921    let report = crate::manifest::io::cleanup_store_orphans(
2922        store.vfs.as_ref(),
2923        &store.directory,
2924        &reachable_segments,
2925        graph_enabled,
2926        store.durability_policy,
2927        || {
2928            // Listing and opening a candidate write nothing. Arm only before
2929            // the first unlink; retain the fence through the directory sync.
2930            publication = publication
2931                .take()
2932                .map(crate::ingest::ManifestPublication::arm);
2933        },
2934    )
2935    .map_err(StoreError::Manifest)?;
2936    if let Some(publication) = publication {
2937        publication.complete();
2938    }
2939    Ok(report)
2940}
2941
2942impl Store {
2943    /// Opens an existing diagnostics copy read-only using its persisted identity.
2944    /// Tokenizer compatibility is still validated; this never repairs or writes.
2945    pub fn open_for_inspection(path: impl AsRef<Path>) -> Result<Self, StoreError> {
2946        let mut options = OpenOptions::read_only();
2947        options.inspection = true;
2948        Self::open(path, options)
2949    }
2950
2951    /// Opens a store directory with the requested access and durability policy.
2952    pub fn open(path: impl AsRef<Path>, options: OpenOptions) -> Result<Self, StoreError> {
2953        let path = path.as_ref();
2954        namespace_batch::validate_store_path(path)?;
2955        if options.access_mode == AccessMode::ReadWrite {
2956            namespace_batch::reclaim_for_open(path)?;
2957        }
2958        let _admission =
2959            namespace_batch::reader_admission(path, options.access_mode == AccessMode::ReadWrite)?;
2960        let selected = namespace_batch::resolve(path)?;
2961        let logical_lock = if selected != path {
2962            acquire_writer_lock(path, options.access_mode)?
2963        } else {
2964            None
2965        };
2966        // A coordinator could commit between resolution and writer admission.
2967        if logical_lock.is_some() && namespace_batch::resolve(path)? != selected {
2968            return Err(StoreError::StoreBusy {
2969                path: path.to_path_buf(),
2970            });
2971        }
2972        if logical_lock.is_some() {
2973            // Adopt a decision that may have survived an interrupted directory
2974            // sync before acknowledging any later ordinary namespace write.
2975            if let Some(root) = path.parent() {
2976                crate::vfs::Vfs::sync(&crate::vfs::StdVfs, root, crate::vfs::SyncKind::Full)
2977                    .map_err(|source| StoreError::Io {
2978                        path: root.to_path_buf(),
2979                        source,
2980                    })?;
2981            }
2982        }
2983        let store = Self::open_with_infrastructure(
2984            &selected,
2985            options,
2986            Arc::new(crate::vfs::StdVfs),
2987            Arc::new(SystemMonotonicClock),
2988            None,
2989            #[cfg(any(test, feature = "test-seams"))]
2990            None,
2991            #[cfg(any(test, feature = "test-seams"))]
2992            None,
2993            #[cfg(any(test, feature = "test-seams"))]
2994            None,
2995            #[cfg(any(test, feature = "test-seams"))]
2996            None,
2997            #[cfg(any(test, feature = "test-seams"))]
2998            None,
2999            #[cfg(any(test, feature = "test-seams"))]
3000            None,
3001            #[cfg(any(test, feature = "test-seams"))]
3002            None,
3003        )?;
3004        if store
3005            .writer_lock
3006            .lock()
3007            .map_err(|_| StoreError::Synchronization {
3008                component: "writer lock",
3009            })?
3010            .is_some()
3011            && namespace_batch::resolve(path)? != selected
3012        {
3013            return Err(StoreError::StoreBusy {
3014                path: path.to_path_buf(),
3015            });
3016        }
3017        *store
3018            .logical_writer_lock
3019            .lock()
3020            .map_err(|_| StoreError::Synchronization {
3021                component: "logical writer lock",
3022            })? = logical_lock;
3023        Ok(store)
3024    }
3025
3026    #[cfg(test)]
3027    fn open_private_preparation(
3028        path: &Path,
3029        options: OpenOptions,
3030        authority: namespace_batch::PrivatePreparation,
3031    ) -> Result<Self, StoreError> {
3032        Self::open_with_infrastructure(
3033            path,
3034            options,
3035            Arc::new(crate::vfs::StdVfs),
3036            Arc::new(SystemMonotonicClock),
3037            Some(authority),
3038            #[cfg(any(test, feature = "test-seams"))]
3039            None,
3040            #[cfg(any(test, feature = "test-seams"))]
3041            None,
3042            #[cfg(any(test, feature = "test-seams"))]
3043            None,
3044            #[cfg(any(test, feature = "test-seams"))]
3045            None,
3046            #[cfg(any(test, feature = "test-seams"))]
3047            None,
3048            #[cfg(any(test, feature = "test-seams"))]
3049            None,
3050            #[cfg(any(test, feature = "test-seams"))]
3051            None,
3052        )
3053    }
3054
3055    #[cfg(feature = "graph-cypher")]
3056    pub(crate) fn open_graph_with_infrastructure(
3057        path: &Path,
3058        mut options: OpenOptions,
3059        document: Option<crate::epoch::EmbeddingTower>,
3060        vfs: Arc<dyn crate::vfs::Vfs>,
3061        clock: Arc<dyn MonotonicClock>,
3062    ) -> Result<Self, StoreError> {
3063        options.graph_document = document;
3064        options.max_resident_bytes = options
3065            .max_resident_bytes
3066            .min(crate::property_graph::resources::MAX_GRAPH_RESIDENT_BYTES);
3067        Self::open_with_infrastructure(
3068            path,
3069            options,
3070            vfs,
3071            clock,
3072            None,
3073            #[cfg(any(test, feature = "test-seams"))]
3074            None,
3075            #[cfg(any(test, feature = "test-seams"))]
3076            None,
3077            #[cfg(any(test, feature = "test-seams"))]
3078            None,
3079            #[cfg(any(test, feature = "test-seams"))]
3080            None,
3081            #[cfg(any(test, feature = "test-seams"))]
3082            None,
3083            #[cfg(any(test, feature = "test-seams"))]
3084            None,
3085            #[cfg(any(test, feature = "test-seams"))]
3086            None,
3087        )
3088    }
3089
3090    #[allow(
3091        clippy::too_many_arguments,
3092        reason = "test-support controllers are explicit optional infrastructure dependencies"
3093    )]
3094    pub(crate) fn open_with_infrastructure(
3095        path: impl AsRef<Path>,
3096        options: OpenOptions,
3097        vfs: Arc<dyn crate::vfs::Vfs>,
3098        clock: Arc<dyn MonotonicClock>,
3099        private_preparation: Option<namespace_batch::PrivatePreparation>,
3100        #[cfg(any(test, feature = "test-seams"))] hybrid_leg_fault: Option<HybridLegTestFault>,
3101        #[cfg(any(test, feature = "test-seams"))] storage_fault_controller: Option<
3102            StorageFaultController,
3103        >,
3104        #[cfg(any(test, feature = "test-seams"))] ingest_retention_fault_controller: Option<
3105            crate::ingest::IngestRetentionFaultController,
3106        >,
3107        #[cfg(any(test, feature = "test-seams"))] metadata_test_controller: Option<
3108            Arc<crate::planner::MetadataTestController>,
3109        >,
3110        #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
3111            crate::scan::vector_fault::VectorFaultController,
3112        >,
3113        #[cfg(any(test, feature = "test-seams"))] kernel_fault_controller: Option<
3114            crate::kernels::vector_fault::KernelFaultController,
3115        >,
3116        #[cfg(any(test, feature = "test-seams"))] vector_seal_scheme: Option<
3117            crate::quant::QuantScheme,
3118        >,
3119    ) -> Result<Self, StoreError> {
3120        let path = path.as_ref();
3121        #[cfg(any(test, feature = "test-seams"))]
3122        match kernel_fault_controller.as_ref() {
3123            Some(controller) => {
3124                controller
3125                    .initialize_for_store()
3126                    .map_err(StoreError::Kernel)?;
3127            }
3128            None => {
3129                crate::kernels::initialize().map_err(StoreError::Kernel)?;
3130            }
3131        }
3132        #[cfg(not(any(test, feature = "test-seams")))]
3133        crate::kernels::initialize().map_err(StoreError::Kernel)?;
3134        #[cfg(any(test, feature = "test-seams"))]
3135        let vfs = match storage_fault_controller.as_ref() {
3136            Some(controller) => Arc::new(StorageFaultVfs {
3137                inner: vfs,
3138                controller: controller.clone(),
3139            }) as Arc<dyn crate::vfs::Vfs>,
3140            None => vfs,
3141        };
3142        let durability_policy = DurabilityPolicy::new(options.durability_mode, options.commit_tier)
3143            .map_err(StoreError::Durability)?;
3144        refuse_native_graph_directory(vfs.as_ref(), path)?;
3145        let accounting = Arc::new(stats::Accounting::new(
3146            options.max_resident_bytes,
3147            options.max_temp_bytes,
3148        ));
3149        let is_directory = vfs
3150            .ensure_directory(path, options.access_mode == AccessMode::ReadWrite)
3151            .map_err(|source| StoreError::Io {
3152                path: path.to_path_buf(),
3153                source,
3154            })?;
3155        if !is_directory {
3156            return Err(StoreError::NotDirectory {
3157                path: path.to_path_buf(),
3158            });
3159        }
3160        validate_persisted_epoch_before_open(vfs.as_ref(), path, &options)?;
3161        let _reclamation_admission = if private_preparation.is_some() {
3162            namespace_batch::validate_private_preparation(
3163                vfs.as_ref(),
3164                path,
3165                private_preparation.as_ref(),
3166            )?;
3167            None
3168        } else {
3169            namespace_batch::reader_admission(path, options.access_mode == AccessMode::ReadWrite)?
3170        };
3171        let reclamation_pin =
3172            namespace_batch::reader_lease(path, options.access_mode == AccessMode::ReadWrite)?
3173                .map(Arc::new);
3174        namespace_batch::refuse_retired(vfs.as_ref(), path)?;
3175        #[cfg(feature = "graph-cypher")]
3176        let reader_store_lock = if options.access_mode == AccessMode::ReadOnly
3177            && vfs
3178                .read_range(&path.join(crate::manifest::io::MANIFEST_FILE), 10, 2)
3179                .is_ok_and(|version| version.as_slice() == 3_u16.to_le_bytes())
3180        {
3181            Some(
3182                StoreLock::acquire_shared(path).map_err(|error| match error {
3183                    StoreLockError::Io { path, source }
3184                        if source.kind() == std::io::ErrorKind::WouldBlock =>
3185                    {
3186                        StoreError::StoreBusy { path }
3187                    }
3188                    error => StoreError::Lock(error),
3189                })?,
3190            )
3191        } else {
3192            None
3193        };
3194        let writer_lock = acquire_writer_lock(path, options.access_mode)?;
3195        if options.access_mode == AccessMode::ReadWrite && private_preparation.is_none() {
3196            namespace_batch::sync_portable_publication_for_open(vfs.as_ref(), path)?;
3197        }
3198        // Recheck after reclamation admission and the writer lock: the first
3199        // read is only a refusal probe and cannot authorize any mutation.
3200        validate_persisted_epoch_before_open(vfs.as_ref(), path, &options)?;
3201        let mut wal_tail_cut = false;
3202        if options.access_mode == AccessMode::ReadWrite {
3203            // The single writer owns the WAL tail: a crash mid-append is cut
3204            // off here so recovery sees whole batches only (ZE-216).
3205            wal_tail_cut =
3206                crate::ingest::cut_interrupted_append(vfs.as_ref(), path, durability_policy)?;
3207        }
3208        if options.access_mode == AccessMode::ReadWrite && private_preparation.is_none() {
3209            namespace_batch::adopt_for_open(vfs.as_ref(), path)?;
3210        }
3211        let manifest_path = path.join(crate::manifest::io::MANIFEST_FILE);
3212        let manifest_exists = match vfs.open(&manifest_path) {
3213            Ok(_) => true,
3214            Err(source) if source.kind() == std::io::ErrorKind::NotFound => false,
3215            Err(source) => {
3216                return Err(StoreError::Io {
3217                    path: manifest_path,
3218                    source,
3219                });
3220            }
3221        };
3222        // A persisted store gets one bounded header probe through the
3223        // Store-owned VFS before mmap becomes the query data plane. Internal
3224        // manifest-only remaps deliberately skip this probe so their exact
3225        // zero-segment-read accounting contracts remain intact.
3226        let mut snapshot = PublishedSnapshot::load_for_open_on_vfs(
3227            path,
3228            &accounting,
3229            vfs.as_ref(),
3230            private_preparation.as_ref(),
3231        )?;
3232        #[cfg(feature = "graph-cypher")]
3233        if options.access_mode == AccessMode::ReadOnly
3234            && snapshot.graph_enabled
3235            && reader_store_lock.is_none()
3236        {
3237            // Graph enable raced the unlocked v2 probe. Retry open under the
3238            // shared graph lock rather than admitting this unlocked snapshot.
3239            return Err(StoreError::StoreBusy {
3240                path: path.join(lock::STORE_LOCK_FILE),
3241            });
3242        }
3243        let (schema, schema_evolved) = resolve_open_schema(
3244            manifest_exists,
3245            &snapshot,
3246            options.schema.as_ref(),
3247            options.access_mode,
3248        )?;
3249        let persisted_epoch = snapshot.epoch_alias();
3250        let declared_epoch = if options.inspection {
3251            persisted_epoch
3252        } else {
3253            options
3254                .epoch
3255                .as_ref()
3256                .map(crate::epoch::StoreEpoch::identity)
3257        };
3258        validate_epoch_identity(
3259            persisted_epoch,
3260            declared_epoch,
3261            manifest_exists,
3262            options.access_mode,
3263        )?;
3264        let tokenizer = crate::fts::tokenizer::Analyzer::new(
3265            options
3266                .tokenizer
3267                .clone()
3268                .unwrap_or_else(crate::fts::tokenizer::TokenizerConfig::text_default),
3269        )
3270        .map_err(StoreError::Tokenizer)?;
3271        validate_tokenizer_epoch(persisted_epoch.or(declared_epoch), &tokenizer)?;
3272        let absorbed_through = snapshot.absorbed_through();
3273        let wal_path = path.join("wal.ze");
3274        let recovered = crate::ingest::ActiveState::recover(
3275            vfs.as_ref(),
3276            &wal_path,
3277            snapshot.generation(),
3278            absorbed_through,
3279            snapshot.graph_absorbed_through,
3280            #[cfg(feature = "graph-cypher")]
3281            snapshot.graph_manifest.as_ref().map(|graph| &***graph),
3282            #[cfg(feature = "graph-cypher")]
3283            snapshot.graph_enabled,
3284            &accounting,
3285            &schema,
3286            &tokenizer,
3287            private_preparation.as_ref(),
3288        )?;
3289        let mut active = recovered.active;
3290        let recovered_wal = recovered.wal;
3291        let sealed_tombstones = recovered.tombstones;
3292        #[cfg(feature = "graph-cypher")]
3293        let graph_replay = recovered.graph;
3294        let mut open_publication = None;
3295        #[cfg(feature = "graph-cypher")]
3296        let mut pending_graph_schema = None;
3297        if schema_evolved {
3298            let boundary = recovered_wal
3299                .as_ref()
3300                .map_or(absorbed_through, crate::wal::CleanWalReader::durable_end);
3301            let generation = active
3302                .generation
3303                .checked_add(1)
3304                .ok_or(StoreError::GenerationOverflow)?;
3305            let mut manifest =
3306                crate::manifest::io::load_manifest(vfs.as_ref(), &manifest_path, boundary)
3307                    .map_err(StoreError::Manifest)?;
3308            manifest.generation = generation;
3309            manifest.schema = schema.clone();
3310            #[cfg(feature = "graph-cypher")]
3311            manifest
3312                .record_generation_bump(boundary)
3313                .map_err(StoreError::Manifest)?;
3314            let mut publication = crate::ingest::ManifestPublication::before_writer_creation();
3315            #[cfg(feature = "graph-cypher")]
3316            let defer_commit = snapshot.graph_enabled;
3317            #[cfg(not(feature = "graph-cypher"))]
3318            let defer_commit = false;
3319            if !defer_commit {
3320                publication
3321                    .commit_manifest(vfs.as_ref(), path, &manifest, durability_policy)
3322                    .map_err(StoreError::Manifest)?;
3323            }
3324            snapshot =
3325                PublishedSnapshot::from_manifest(vfs.as_ref(), path, &manifest, &accounting)?;
3326            active.generation = generation;
3327            #[cfg(feature = "graph-cypher")]
3328            if defer_commit {
3329                pending_graph_schema = Some(manifest);
3330            }
3331            open_publication = Some(publication);
3332        }
3333        if options.access_mode == AccessMode::ReadWrite
3334            && !manifest_exists
3335            && (options.epoch.is_some() || options.schema.is_some())
3336        {
3337            let mut publication = crate::ingest::ManifestPublication::before_writer_creation();
3338            publication
3339                .commit_manifest(
3340                    vfs.as_ref(),
3341                    path,
3342                    &crate::manifest::Manifest {
3343                        #[cfg(feature = "graph-cypher")]
3344                        graph: None,
3345                        generation: active.generation,
3346                        log_seq: 0,
3347                        segments: Vec::new(),
3348                        epochs: options
3349                            .epoch
3350                            .as_ref()
3351                            .cloned()
3352                            .map(crate::manifest::EpochMeta::from)
3353                            .into_iter()
3354                            .collect(),
3355                        epoch_alias: options
3356                            .epoch
3357                            .as_ref()
3358                            .map(crate::epoch::StoreEpoch::identity),
3359                        schema: schema.clone(),
3360                    },
3361                    durability_policy,
3362                )
3363                .map_err(StoreError::Manifest)?;
3364            open_publication = Some(publication);
3365        }
3366        let wal_writer = match options.access_mode {
3367            AccessMode::ReadWrite => Some(match recovered_wal {
3368                Some(recovered) => crate::ingest::StoreWal::resume(
3369                    vfs.as_ref(),
3370                    &wal_path,
3371                    recovered,
3372                    durability_policy,
3373                    absorbed_through,
3374                    &accounting,
3375                )?,
3376                None => crate::ingest::StoreWal::create(
3377                    Arc::clone(&vfs),
3378                    path,
3379                    &wal_path,
3380                    durability_policy,
3381                    &accounting,
3382                )?,
3383            }),
3384            AccessMode::ReadOnly => {
3385                drop(recovered_wal);
3386                None
3387            }
3388        };
3389        if let Some(publication) = open_publication.as_mut() {
3390            publication.bind_writer(wal_writer.as_ref().ok_or(StoreError::ReadOnly)?);
3391        }
3392        let background = match options.access_mode {
3393            AccessMode::ReadWrite => Some(BackgroundThread::start()?),
3394            AccessMode::ReadOnly => None,
3395        };
3396        #[cfg(feature = "graph-cypher")]
3397        let native_graph =
3398            native_graph::NativeGraphPublication::new(&accounting, snapshot.graph_enabled)?;
3399        #[cfg(test)]
3400        let (snapshot, background, teardown_probe) = {
3401            let mut snapshot = snapshot;
3402            let mut background = background;
3403            let teardown_probe = Arc::new(close::TeardownProbe::new());
3404            snapshot.set_teardown_probe(Arc::clone(&teardown_probe));
3405            if let Some(background) = background.as_mut() {
3406                background.set_teardown_probe(Arc::clone(&teardown_probe));
3407            }
3408            (snapshot, background, teardown_probe)
3409        };
3410        let mut store = Self {
3411            private_preparation,
3412            open_migrations: OpenMigrations {
3413                schema_added: schema_evolved,
3414                wal_tail_cut,
3415                generation: active.generation,
3416            },
3417            directory: path.to_path_buf(),
3418            vfs,
3419            clock,
3420            state: Mutex::new(StoreState::Open),
3421            state_changed: Condvar::new(),
3422            background: Mutex::new(background),
3423            query_pool: Mutex::new(None),
3424            lexical_worker: Mutex::new(None),
3425            #[cfg(feature = "graph-cypher")]
3426            native_graph,
3427            #[cfg(feature = "graph-cypher")]
3428            graph_enable_pending: std::sync::atomic::AtomicBool::new(false),
3429            snapshot: RwLock::new(None),
3430            active: Mutex::new(Some(active)),
3431            wal_writer: Mutex::new(wal_writer),
3432            writer_lock: Mutex::new(writer_lock.map(Arc::new)),
3433            #[cfg(feature = "graph-cypher")]
3434            reader_store_lock: Mutex::new(reader_store_lock),
3435            snapshot_pins: Arc::new(AtomicU64::new(0)),
3436            snapshot_pin: Mutex::new(None),
3437            logical_writer_lock: Mutex::new(None),
3438            reclamation_pin: Mutex::new(reclamation_pin),
3439            maintenance: Mutex::new(()),
3440            health_state: Mutex::new(crate::diag::HealthState::default()),
3441            durability_policy,
3442            reader_drain_timeout: options.reader_drain_timeout,
3443            accounting,
3444            active_queries: AtomicU64::new(0),
3445            #[cfg(any(test, feature = "test-seams"))]
3446            text_materialization_work: materialize::TestMaterializationWork::default(),
3447            epoch_alias: crate::epoch::EpochAliasCell::new(persisted_epoch.or(declared_epoch)),
3448            epoch: options.epoch,
3449            tokenizer,
3450            schema,
3451            lexical_index_cache: LexicalIndexCache::new(),
3452            #[cfg(any(test, feature = "test-seams"))]
3453            ingest_retention_fault_controller,
3454            #[cfg(any(test, feature = "test-seams"))]
3455            hybrid_leg_fault: Mutex::new(hybrid_leg_fault),
3456            #[cfg(any(test, feature = "test-seams"))]
3457            hybrid_execution_receipt: Mutex::new(None),
3458            #[cfg(any(test, feature = "test-seams"))]
3459            metadata_test_controller,
3460            #[cfg(any(test, feature = "test-seams"))]
3461            vector_fault_controller,
3462            #[cfg(any(test, feature = "test-seams"))]
3463            kernel_fault_controller,
3464            #[cfg(any(test, feature = "test-seams"))]
3465            vector_seal_scheme,
3466            #[cfg(test)]
3467            teardown_probe,
3468        };
3469        #[cfg(feature = "graph-cypher")]
3470        if let Some(graph) = snapshot.graph_manifest.as_ref() {
3471            native_graph::recovery::install_unified(
3472                &store,
3473                graph,
3474                snapshot.generation(),
3475                &graph_replay,
3476                options.access_mode,
3477                options.graph_document.clone(),
3478            )?;
3479        }
3480        #[cfg(feature = "graph-cypher")]
3481        if let Some(manifest) = pending_graph_schema {
3482            // Graph admission must accept the prospective snapshot before its
3483            // schema and generation become durable. The bound guard arms at
3484            // the first manifest write and completes after snapshot publication.
3485            open_publication
3486                .as_mut()
3487                .ok_or(StoreError::Synchronization {
3488                    component: "schema publication",
3489                })?
3490                .commit_manifest(store.vfs.as_ref(), path, &manifest, durability_policy)
3491                .map_err(StoreError::Manifest)?;
3492        }
3493        if options.access_mode == AccessMode::ReadWrite && manifest_exists {
3494            // An adopted manifest may be the survivor of a commit interrupted
3495            // between rename and directory sync. Make its dirent durable before
3496            // any new generation is acknowledged on top of it.
3497            match durability_policy.directory_sync() {
3498                SyncRequirement::Skip => {}
3499                SyncRequirement::Sync(kind) => {
3500                    store
3501                        .vfs
3502                        .sync(path, kind)
3503                        .map_err(|source| StoreError::Io {
3504                            path: path.to_path_buf(),
3505                            source,
3506                        })?;
3507                }
3508            }
3509        }
3510        #[cfg(feature = "graph-cypher")]
3511        let repair_before_purge = snapshot.graph_enabled;
3512        #[cfg(not(feature = "graph-cypher"))]
3513        let repair_before_purge = false;
3514        store.publish_snapshot(snapshot)?;
3515        if let Some(publication) = open_publication {
3516            publication.complete();
3517        }
3518        if repair_before_purge {
3519            // Graph folds must not absorb unrepaired sealed deletes.
3520            store.recover_sealed_tombstones(&sealed_tombstones)?;
3521        }
3522        #[cfg(feature = "graph-cypher")]
3523        if options.access_mode == AccessMode::ReadWrite
3524            && store.native_graph.has_pending_reclaim()?
3525        {
3526            for _ in 0..2 {
3527                let admission = store
3528                    .admit_native_graph_maintenance()
3529                    .map_err(native_graph::recovery::store_error)?;
3530                match store.commit_native_graph_maintenance(
3531                    &admission,
3532                    &QueryControl::Cancel(CancelToken::new()),
3533                ) {
3534                    Ok(_) => break,
3535                    Err(native_graph::NativeGraphError::StalePreparation) => continue,
3536                    Err(error) => {
3537                        return Err(native_graph::recovery::store_error(error));
3538                    }
3539                }
3540            }
3541            if store.native_graph.has_pending_reclaim()? {
3542                return Err(StoreError::Manifest(
3543                    crate::manifest::ManifestError::Decode(
3544                        "pending reclaim recovery did not finish after its manifest fold"
3545                            .to_owned(),
3546                    ),
3547                ));
3548            }
3549        }
3550        store
3551            .recover_pending_physical_purge()
3552            .map_err(|error| StoreError::PurgeRecovery {
3553                detail: error.to_string(),
3554            })?;
3555        if !repair_before_purge {
3556            // Preserve main's exact graph-free generation, bytes and refusals.
3557            store.recover_sealed_tombstones(&sealed_tombstones)?;
3558        }
3559        #[cfg(feature = "graph-cypher")]
3560        if repair_before_purge && options.access_mode == AccessMode::ReadWrite {
3561            // Older writers could fold graph roots while document rows pinned
3562            // an oversized WAL. Checkpoint both participants after recovery,
3563            // before a new batch competes with that retained log for memory.
3564            store
3565                .checkpoint_native_graph_on_open()
3566                .map_err(native_graph::recovery::store_error)?;
3567        }
3568        if options.access_mode == AccessMode::ReadWrite {
3569            let cleanup_report = cleanup_open_orphans(&store)?;
3570            #[cfg(any(test, feature = "test-seams"))]
3571            if let Some(controller) = storage_fault_controller.as_ref() {
3572                controller.record_cleanup_report(cleanup_report);
3573            }
3574            #[cfg(not(any(test, feature = "test-seams")))]
3575            let _ = cleanup_report;
3576        }
3577        #[cfg(any(test, feature = "test-seams"))]
3578        if let Some(controller) = storage_fault_controller.as_ref() {
3579            let published = store
3580                .snapshot
3581                .read()
3582                .map_err(|_| StoreError::Synchronization {
3583                    component: "published snapshot",
3584                })?;
3585            let snapshot = published.as_ref().ok_or(StoreError::Closed)?;
3586            for segment in snapshot.all_segments() {
3587                register_storage_segment_controller(
3588                    &store.directory,
3589                    segment.meta().id,
3590                    controller,
3591                );
3592            }
3593        }
3594        store.open_migrations.generation = store
3595            .active
3596            .lock()
3597            .map_err(|_| StoreError::Synchronization {
3598                component: "active segment",
3599            })?
3600            .as_ref()
3601            .ok_or(StoreError::Closed)?
3602            .generation;
3603        Ok(store)
3604    }
3605
3606    /// Opens a store with deterministic infrastructure for adversarial tests.
3607    #[cfg(any(test, feature = "test-seams"))]
3608    #[doc(hidden)]
3609    pub fn open_with_test_dependencies(
3610        path: impl AsRef<Path>,
3611        options: OpenOptions,
3612        dependencies: StoreTestDependencies,
3613    ) -> Result<Self, StoreError> {
3614        let controller = dependencies.storage_fault_controller.clone();
3615        with_storage_open_controller(controller.as_ref(), || {
3616            Self::open_with_infrastructure(
3617                path,
3618                options,
3619                dependencies.vfs,
3620                dependencies.clock,
3621                None,
3622                dependencies.hybrid_leg_fault,
3623                dependencies.storage_fault_controller,
3624                dependencies.ingest_retention_fault_controller,
3625                dependencies.metadata_test_controller,
3626                dependencies.vector_fault_controller,
3627                dependencies.kernel_fault_controller,
3628                dependencies.vector_seal_scheme,
3629            )
3630        })
3631    }
3632
3633    /// Takes the most recent Store-owned hybrid execution receipt.
3634    #[cfg(any(test, feature = "test-seams"))]
3635    #[doc(hidden)]
3636    pub fn take_hybrid_execution_receipt(&self) -> Option<HybridExecutionReceipt> {
3637        self.hybrid_execution_receipt
3638            .lock()
3639            .ok()
3640            .and_then(|mut receipt| receipt.take())
3641    }
3642
3643    /// Carries the committed epoch registry forward unchanged.
3644    ///
3645    /// A declared epoch is stamped once, during open, before any write is
3646    /// admitted, so every later commit only propagates what is already
3647    /// committed. Replacement-snapshot paths may not have a prior manifest
3648    /// value in hand, so a declared store reconstructs its already-committed
3649    /// singleton entry; an unstamped store still returns an empty registry.
3650    pub(crate) fn epoch_registry(
3651        &self,
3652        prior: &[crate::manifest::EpochMeta],
3653    ) -> Vec<crate::manifest::EpochMeta> {
3654        if prior.is_empty() {
3655            self.epoch
3656                .as_ref()
3657                .map(crate::manifest::EpochMeta::from)
3658                .into_iter()
3659                .collect()
3660        } else {
3661            prior.to_vec()
3662        }
3663    }
3664
3665    /// Returns the immutable report of changes completed by this open.
3666    pub fn open_migrations(&self) -> OpenMigrations {
3667        self.open_migrations
3668    }
3669
3670    /// Returns the published embedding and tokenizer identity, or `None`
3671    /// for a store that carries no stamped epoch.
3672    #[must_use]
3673    pub fn epoch_identity(&self) -> Option<crate::epoch::EpochIdentity> {
3674        self.epoch_alias.load()
3675    }
3676
3677    /// Returns the immutable schema declared for this store.
3678    #[must_use]
3679    pub const fn schema(&self) -> &crate::meta::Schema {
3680        &self.schema
3681    }
3682
3683    /// Returns the current explicit lifecycle state.
3684    pub fn state(&self) -> Result<StoreState, StoreError> {
3685        self.state
3686            .lock()
3687            .map(|state| *state)
3688            .map_err(|_| StoreError::Synchronization { component: "state" })
3689    }
3690
3691    /// Acquires the complete immutable snapshot admitted for one read.
3692    pub fn snapshot(&self) -> Result<SnapshotLease, StoreError> {
3693        let state = self
3694            .state
3695            .lock()
3696            .map_err(|_| StoreError::Synchronization { component: "state" })?;
3697        match *state {
3698            StoreState::Open => {}
3699            StoreState::Closing => return Err(StoreError::Closing),
3700            StoreState::Closed => return Err(StoreError::Closed),
3701        }
3702        let active = self
3703            .active
3704            .lock()
3705            .map_err(|_| StoreError::Synchronization {
3706                component: "active segment",
3707            })?;
3708        let generation = active.as_ref().ok_or(StoreError::Closed)?.generation;
3709        let snapshot = self
3710            .snapshot
3711            .read()
3712            .map_err(|_| StoreError::Synchronization {
3713                component: "published snapshot",
3714            })?
3715            .as_ref()
3716            .cloned()
3717            .ok_or(StoreError::Closed)?;
3718        drop(active);
3719        drop(state);
3720        Ok(SnapshotLease::new_at(snapshot, generation))
3721    }
3722
3723    /// Reads one public result row's typed metadata through the owning Store boundary.
3724    ///
3725    /// This hidden test-support adapter lets independent persistence tests pair
3726    /// row identities returned by a public query with the values the reopened
3727    /// Store actually exposes. It is not compiled into the shipping API.
3728    #[cfg(any(test, feature = "test-seams"))]
3729    #[doc(hidden)]
3730    pub fn test_metadata_row_values(
3731        &self,
3732        row_id: crate::ingest::GlobalRowId,
3733    ) -> Result<Vec<(crate::meta::ColumnId, Option<crate::meta::PredicateValue>)>, StoreError> {
3734        match row_id.source() {
3735            crate::ingest::RowSource::Active => {
3736                let row = usize::try_from(row_id.local_row())
3737                    .map_err(|_| StoreError::ActiveRowOverflow)?;
3738                let active = self
3739                    .active
3740                    .lock()
3741                    .map_err(|_| StoreError::Synchronization {
3742                        component: "active segment",
3743                    })?;
3744                let segment = &active.as_ref().ok_or(StoreError::Closed)?.segment;
3745                let timestamp = segment
3746                    .timestamps()
3747                    .get(row)
3748                    .copied()
3749                    .ok_or(StoreError::ActiveRowOverflow)?;
3750                let mut values = segment.column_values(row)?;
3751                values.push((
3752                    crate::meta::TIMESTAMP_COLUMN,
3753                    crate::meta::PredicateValue::I64(timestamp),
3754                ));
3755                self.schema
3756                    .columns()
3757                    .iter()
3758                    .map(|definition| {
3759                        let value = values
3760                            .iter()
3761                            .find(|(column, _)| *column == definition.id())
3762                            .map(|(_, value)| value.clone());
3763                        Ok((definition.id(), value))
3764                    })
3765                    .collect()
3766            }
3767            crate::ingest::RowSource::Sealed(segment_id) => {
3768                let snapshot = self.snapshot()?;
3769                let segment = snapshot
3770                    .segments()
3771                    .iter()
3772                    .find(|segment| segment.meta().id == segment_id)
3773                    .ok_or_else(|| {
3774                        StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
3775                            "public metadata row names absent segment {segment_id}"
3776                        )))
3777                    })?;
3778                let columns = segment.columns().map_err(StoreError::Segment)?;
3779                if row_id.local_row() >= columns.row_count() {
3780                    return Err(StoreError::Segment(crate::segment::SegmentError::Geometry(
3781                        format!(
3782                            "public metadata row {} is outside {} rows",
3783                            row_id.local_row(),
3784                            columns.row_count()
3785                        ),
3786                    )));
3787                }
3788                columns
3789                    .schema()
3790                    .columns()
3791                    .iter()
3792                    .map(|definition| {
3793                        let column = columns.column(definition.id()).ok_or_else(|| {
3794                            StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
3795                                "public metadata row is missing column {}",
3796                                definition.id().get()
3797                            )))
3798                        })?;
3799                        let value = match column {
3800                            crate::meta::Column::Id128(column) => column
3801                                .get(row_id.local_row())
3802                                .map(crate::meta::PredicateValue::Id128),
3803                            crate::meta::Column::U64(column) => column
3804                                .get(row_id.local_row())
3805                                .map(crate::meta::PredicateValue::U64),
3806                            crate::meta::Column::I64(column) => column
3807                                .get(row_id.local_row())
3808                                .map(crate::meta::PredicateValue::I64),
3809                            crate::meta::Column::F64(column) => column
3810                                .get(row_id.local_row())
3811                                .map(crate::meta::PredicateValue::F64),
3812                            crate::meta::Column::Bool(column) => column
3813                                .get(row_id.local_row())
3814                                .map(crate::meta::PredicateValue::Bool),
3815                            crate::meta::Column::DictionaryString(column) => column
3816                                .get(row_id.local_row())
3817                                .map(|value| crate::meta::PredicateValue::String(value.to_owned())),
3818                            crate::meta::Column::RawString(column) => column
3819                                .get(row_id.local_row())
3820                                .map(|value| crate::meta::PredicateValue::String(value.to_owned())),
3821                        };
3822                        Ok((definition.id(), value))
3823                    })
3824                    .collect()
3825            }
3826        }
3827    }
3828
3829    /// Evaluates one source's actual typed metadata state for independent tests.
3830    #[cfg(any(test, feature = "test-seams"))]
3831    #[doc(hidden)]
3832    pub fn test_metadata_evaluate_source(
3833        &self,
3834        source: crate::ingest::RowSource,
3835        predicate: &crate::meta::Predicate,
3836    ) -> Result<(u32, Vec<u32>, Vec<u32>), StoreError> {
3837        let evaluate = |columns: &crate::meta::ColumnStore,
3838                        alive: &crate::meta::AliveSet|
3839         -> Result<(u32, Vec<u32>, Vec<u32>), StoreError> {
3840            let matches = crate::meta::evaluate(predicate, columns, alive).map_err(|error| {
3841                StoreError::Segment(crate::segment::SegmentError::Columns(format!(
3842                    "test metadata source evaluation: {error}"
3843                )))
3844            })?;
3845            Ok((
3846                columns.row_count(),
3847                alive.iter_alive().collect(),
3848                matches.iter().collect(),
3849            ))
3850        };
3851        match source {
3852            crate::ingest::RowSource::Active => {
3853                let active = self
3854                    .active
3855                    .lock()
3856                    .map_err(|_| StoreError::Synchronization {
3857                        component: "active segment",
3858                    })?;
3859                let segment = &active.as_ref().ok_or(StoreError::Closed)?.segment;
3860                let mut builder = crate::meta::ColumnStoreBuilder::new(self.schema.clone());
3861                for (row, timestamp) in segment.timestamps().iter().copied().enumerate() {
3862                    let values = segment.column_values(row)?;
3863                    builder
3864                        .push_row(timestamp, &crate::ingest::column_inputs(&values))
3865                        .map_err(|error| {
3866                            StoreError::Segment(crate::segment::SegmentError::Columns(
3867                                error.to_string(),
3868                            ))
3869                        })?;
3870                }
3871                let columns = builder.finish().map_err(|error| {
3872                    StoreError::Segment(crate::segment::SegmentError::Columns(error.to_string()))
3873                })?;
3874                let alive = segment.alive()?;
3875                evaluate(&columns, &alive)
3876            }
3877            crate::ingest::RowSource::Sealed(segment_id) => {
3878                let snapshot = self.snapshot()?;
3879                let segment = snapshot
3880                    .segments()
3881                    .iter()
3882                    .find(|segment| segment.meta().id == segment_id)
3883                    .ok_or_else(|| {
3884                        StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
3885                            "metadata source evaluation names absent segment {segment_id}"
3886                        )))
3887                    })?;
3888                let columns = segment.columns().map_err(StoreError::Segment)?;
3889                let alive = segment.alive().map_err(StoreError::Segment)?;
3890                evaluate(&columns, &alive)
3891            }
3892        }
3893    }
3894
3895    pub(crate) fn publish_snapshot(&self, snapshot: PublishedSnapshot) -> Result<(), StoreError> {
3896        let state = self
3897            .state
3898            .lock()
3899            .map_err(|_| StoreError::Synchronization { component: "state" })?;
3900        match *state {
3901            StoreState::Open => {}
3902            StoreState::Closing => return Err(StoreError::Closing),
3903            StoreState::Closed => return Err(StoreError::Closed),
3904        }
3905        let generation = snapshot.generation();
3906        let mut active = self
3907            .active
3908            .lock()
3909            .map_err(|_| StoreError::Synchronization {
3910                component: "active segment",
3911            })?;
3912        let active_generation = &mut active.as_mut().ok_or(StoreError::Closed)?.generation;
3913        *active_generation = (*active_generation).max(generation);
3914        let mut published = self
3915            .snapshot
3916            .write()
3917            .map_err(|_| StoreError::Synchronization {
3918                component: "published snapshot",
3919            })?;
3920        *published = Some(Arc::new(snapshot));
3921        drop(active);
3922        drop(state);
3923        Ok(())
3924    }
3925
3926    /// Runs one exact parallel scan admitted against this store's snapshot.
3927    ///
3928    /// The query holds one snapshot lease for its complete execution and uses
3929    /// the store's lazily started persistent worker pool.
3930    pub fn top_k_with_options(
3931        &self,
3932        request: crate::scan::ScanRequest<'_>,
3933        k: usize,
3934        options: crate::scan::ScanOptions,
3935        control: QueryControl,
3936    ) -> Result<crate::scan::ScanOutcome, QueryError> {
3937        let control = control.with_clock(Arc::clone(&self.clock));
3938        let state = self
3939            .state
3940            .lock()
3941            .map_err(|_| QueryError::Store(StoreError::Synchronization { component: "state" }))?;
3942        match *state {
3943            StoreState::Open => {}
3944            StoreState::Closing => return Err(QueryError::Store(StoreError::Closing)),
3945            StoreState::Closed => return Err(QueryError::Store(StoreError::Closed)),
3946        }
3947        let snapshot = self
3948            .snapshot
3949            .read()
3950            .map_err(|_| {
3951                QueryError::Store(StoreError::Synchronization {
3952                    component: "published snapshot",
3953                })
3954            })?
3955            .as_ref()
3956            .cloned()
3957            .ok_or(QueryError::Store(StoreError::Closed))?;
3958        let mut pool_slot = self.query_pool.lock().map_err(|_| {
3959            QueryError::Store(StoreError::Synchronization {
3960                component: "query pool",
3961            })
3962        })?;
3963        if pool_slot.is_none() {
3964            let capacity = crate::scan::physical_thread_capacity().map_err(|error| {
3965                QueryError::Store(StoreError::QueryPoolCapacity {
3966                    source: error.to_string(),
3967                })
3968            })?;
3969            let pool =
3970                pool::QueryPool::start(capacity, &self.accounting).map_err(QueryError::Store)?;
3971            *pool_slot = Some(Arc::new(pool));
3972        }
3973        let pool = pool_slot.as_ref().cloned().ok_or({
3974            QueryError::Store(StoreError::Synchronization {
3975                component: "query pool initialization",
3976            })
3977        })?;
3978        self.active_queries.fetch_add(1, Ordering::Relaxed);
3979        let active = ActiveQuery {
3980            count: &self.active_queries,
3981        };
3982        drop(pool_slot);
3983        drop(state);
3984
3985        let result = pool
3986            .execute(request, k, options, control, SnapshotLease::new(snapshot))
3987            .map(|mut outcome| {
3988                outcome.candidates.truncate(k);
3989                outcome
3990            });
3991        drop(active);
3992        result
3993    }
3994
3995    /// Injects one persistent query-worker panic and joins the pool.
3996    ///
3997    /// This is test-support-only and exists so the lifecycle campaign reaches
3998    /// the real worker teardown/error path.
3999    #[cfg(any(test, feature = "test-seams"))]
4000    pub fn panic_query_worker_for_test(&self) -> Result<(), StoreError> {
4001        let pool = self
4002            .query_pool
4003            .lock()
4004            .map_err(|_| StoreError::Synchronization {
4005                component: "query pool",
4006            })?
4007            .as_ref()
4008            .cloned()
4009            .ok_or(StoreError::Synchronization {
4010                component: "query pool initialization",
4011            })?;
4012        pool.panic_one_and_join()
4013    }
4014
4015    /// Searches the active segment plus every immutable segment in one pinned
4016    /// store generation, then merges one global top-k.
4017    ///
4018    /// Scan-tier segments use the persistent query pool. Explicit graph-tier
4019    /// segments traverse serially on the calling thread, while the active
4020    /// segment is still scanned. Both tiers share one cancellation state and
4021    /// `(RowSource, local_row)` addressing, so immutable row zero never collides
4022    /// across segments and manifest reordering cannot rename a sealed row.
4023    pub fn search(
4024        &self,
4025        request: crate::ingest::SearchRequest<'_>,
4026        k: usize,
4027        options: impl Into<SearchOptions>,
4028        control: QueryControl,
4029    ) -> Result<crate::ingest::SearchOutcome, QueryError> {
4030        self.search_with_graph_bound_mode(
4031            request,
4032            k,
4033            options.into(),
4034            control,
4035            GraphBoundMode::Shared,
4036        )
4037    }
4038
4039    fn admit_lexical_query(&self) -> Result<AdmittedLexicalQuery<'_>, QueryError> {
4040        #[cfg(any(test, feature = "test-seams"))]
4041        self.text_materialization_work
4042            .admissions
4043            .fetch_add(1, Ordering::Relaxed);
4044        #[cfg(all(feature = "query-timing", any(test, feature = "test-seams")))]
4045        let lock_started = std::time::Instant::now();
4046        let state = self
4047            .state
4048            .lock()
4049            .map_err(|_| QueryError::Store(StoreError::Synchronization { component: "state" }))?;
4050        #[cfg(all(feature = "query-timing", any(test, feature = "test-seams")))]
4051        self.text_materialization_work
4052            .observe_lexical_lock(lock_started);
4053        match *state {
4054            StoreState::Open => {}
4055            StoreState::Closing => return Err(QueryError::Store(StoreError::Closing)),
4056            StoreState::Closed => return Err(QueryError::Store(StoreError::Closed)),
4057        }
4058        #[cfg(all(feature = "query-timing", any(test, feature = "test-seams")))]
4059        let lock_started = std::time::Instant::now();
4060        let active_guard = self.active.lock().map_err(|_| {
4061            QueryError::Store(StoreError::Synchronization {
4062                component: "active segment",
4063            })
4064        })?;
4065        #[cfg(all(feature = "query-timing", any(test, feature = "test-seams")))]
4066        self.text_materialization_work
4067            .observe_lexical_lock(lock_started);
4068        let active_state = active_guard
4069            .as_ref()
4070            .ok_or(QueryError::Store(StoreError::Closed))?;
4071        let generation = active_state.generation;
4072        let active = Arc::clone(&active_state.segment);
4073        #[cfg(all(feature = "query-timing", any(test, feature = "test-seams")))]
4074        let lock_started = std::time::Instant::now();
4075        let snapshot = self
4076            .snapshot
4077            .read()
4078            .map_err(|_| {
4079                QueryError::Store(StoreError::Synchronization {
4080                    component: "published snapshot",
4081                })
4082            })
4083            .inspect(|_| {
4084                #[cfg(all(feature = "query-timing", any(test, feature = "test-seams")))]
4085                self.text_materialization_work
4086                    .observe_lexical_lock(lock_started);
4087            })?
4088            .as_ref()
4089            .cloned()
4090            .ok_or(QueryError::Store(StoreError::Closed))?;
4091        self.active_queries.fetch_add(1, Ordering::Relaxed);
4092        let active_query = ActiveQuery {
4093            count: &self.active_queries,
4094        };
4095        drop(active_guard);
4096        drop(state);
4097        Ok(AdmittedLexicalQuery {
4098            generation,
4099            active,
4100            snapshot: snapshot::ReadSnapshot::new(snapshot),
4101            active_query,
4102        })
4103    }
4104
4105    fn admit_document_read(&self) -> Result<AdmittedDocumentRead<'_>, StoreError> {
4106        let state = self
4107            .state
4108            .lock()
4109            .map_err(|_| StoreError::Synchronization { component: "state" })?;
4110        match *state {
4111            StoreState::Open => {}
4112            StoreState::Closing => return Err(StoreError::Closing),
4113            StoreState::Closed => return Err(StoreError::Closed),
4114        }
4115        let active_guard = self
4116            .active
4117            .lock()
4118            .map_err(|_| StoreError::Synchronization {
4119                component: "active segment",
4120            })?;
4121        let active_state = active_guard.as_ref().ok_or(StoreError::Closed)?;
4122        let generation = active_state.generation;
4123        let active = Arc::clone(&active_state.segment);
4124        let snapshot = self
4125            .snapshot
4126            .read()
4127            .map_err(|_| StoreError::Synchronization {
4128                component: "published snapshot",
4129            })?
4130            .as_ref()
4131            .cloned()
4132            .ok_or(StoreError::Closed)?;
4133        self.active_queries.fetch_add(1, Ordering::Relaxed);
4134        let active_query = ActiveQuery {
4135            count: &self.active_queries,
4136        };
4137        drop(active_guard);
4138        drop(state);
4139        Ok(AdmittedDocumentRead {
4140            generation,
4141            active,
4142            snapshot: snapshot::ReadSnapshot::new(snapshot),
4143            active_query,
4144        })
4145    }
4146
4147    /// Prepares the current lexical assembly and prefix vocabulary without ranking.
4148    ///
4149    /// Call after open to move lazy assembly preparation out of the first query.
4150    /// Mutations can invalidate both caches. Cancellation may retain a complete
4151    /// assembly, but never publishes a partial vocabulary.
4152    pub fn warm_lexical(
4153        &self,
4154        control: QueryControl,
4155    ) -> Result<(), crate::ingest::StoreLexicalError> {
4156        let control = control.with_clock(Arc::clone(&self.clock));
4157        let admission = self.admit_lexical_query()?;
4158        let lease = SnapshotLease::new_at(Arc::clone(&admission.snapshot), admission.generation);
4159        let cancellation = QueryCancellation::new(&control, &lease);
4160        cancellation.check_graph().map_err(QueryError::Scan)?;
4161        let receipt = assemble_unfiltered_lexical_index(
4162            LexicalInputs {
4163                filter: None,
4164                generation: admission.generation,
4165                cache: &self.lexical_index_cache,
4166                snapshot: &admission.snapshot,
4167                active: &admission.active,
4168                accounting: &self.accounting,
4169            },
4170            true,
4171            Some(&cancellation),
4172        )
4173        .map_err(map_store_lexical_assembly_error)?;
4174        receipt.vocabulary(&self.accounting, &cancellation)?;
4175        Ok(())
4176    }
4177
4178    /// Runs an exact structured lexical query over active and sealed text.
4179    pub fn search_lexical(
4180        &self,
4181        query: &crate::fts::search::TermQuery,
4182        k: usize,
4183        control: QueryControl,
4184    ) -> Result<crate::ingest::StoreLexicalSearchOutcome, crate::ingest::StoreLexicalError> {
4185        self.search_lexical_then(query, k, control, None, |outcome, _, _, _, _, _| {
4186            Ok(outcome)
4187        })
4188    }
4189
4190    /// Runs lexical ranking with scan-compatible eligibility constraints.
4191    pub fn search_lexical_filtered(
4192        &self,
4193        query: &crate::fts::search::TermQuery,
4194        k: usize,
4195        control: QueryControl,
4196        filter: Option<&QueryFilter>,
4197    ) -> Result<crate::ingest::StoreLexicalSearchOutcome, crate::ingest::StoreLexicalError> {
4198        self.search_lexical_then(query, k, control, filter, |outcome, _, _, _, _, _| {
4199            Ok(outcome)
4200        })
4201    }
4202
4203    fn search_lexical_then<R>(
4204        &self,
4205        query: &crate::fts::search::TermQuery,
4206        k: usize,
4207        control: QueryControl,
4208        filter: Option<&QueryFilter>,
4209        finish: impl FnOnce(
4210            crate::ingest::StoreLexicalSearchOutcome,
4211            &PublishedSnapshot,
4212            &crate::ingest::ActiveSegment,
4213            &LexicalAssembly,
4214            &[crate::fts::search::ScoredDoc],
4215            &QueryCancellation<'_>,
4216        ) -> Result<R, crate::ingest::StoreLexicalError>,
4217    ) -> Result<R, crate::ingest::StoreLexicalError> {
4218        let control = control.with_clock(Arc::clone(&self.clock));
4219        let started = self.clock.now();
4220        let admission_started = timing_start(self.clock.as_ref());
4221        let AdmittedLexicalQuery {
4222            generation,
4223            active,
4224            snapshot,
4225            active_query,
4226        } = self.admit_lexical_query()?;
4227        let admission_time = timing_elapsed(self.clock.as_ref(), admission_started);
4228        let lexical_started = timing_start(self.clock.as_ref());
4229
4230        let lease = SnapshotLease::new_at(Arc::clone(&snapshot), generation);
4231        let cancellation = QueryCancellation::new(&control, &lease);
4232        cancellation.check_graph().map_err(QueryError::Scan)?;
4233        let assembly = assemble_lexical_index(
4234            LexicalInputs {
4235                filter,
4236                generation,
4237                cache: &self.lexical_index_cache,
4238                snapshot: &snapshot,
4239                active: &active,
4240                accounting: &self.accounting,
4241            },
4242            true,
4243            Some(&cancellation),
4244        )
4245        .map_err(map_store_lexical_assembly_error)?;
4246        let mut work = crate::fts::control::WorkCheck::new(|| {
4247            cancellation
4248                .check_graph()
4249                .map_err(QueryError::Scan)
4250                .map_err(crate::ingest::StoreLexicalError::from)
4251        });
4252        work.check_now()?;
4253        let mut allow_lists = Vec::with_capacity(assembly.alive_sets.len());
4254        for alive in &assembly.alive_sets {
4255            work.step()?;
4256            allow_lists.push(alive.alive_bitmap());
4257        }
4258        let lexical = crate::planner::search_lexical_filtered_refs_controlled(
4259            &assembly.index,
4260            query,
4261            k,
4262            crate::fts::bm25::Bm25Params::beir(),
4263            &allow_lists,
4264            None,
4265            || {
4266                cancellation
4267                    .check_graph()
4268                    .map_err(QueryError::Scan)
4269                    .map_err(crate::ingest::StoreLexicalError::from)
4270            },
4271        )?;
4272        cancellation.check_graph().map_err(QueryError::Scan)?;
4273        let mut candidates = Vec::with_capacity(lexical.result.hits.len());
4274        for hit in &lexical.result.hits {
4275            work.step()?;
4276            let document =
4277                structured_lexical_document(&snapshot, &active, &assembly.sources, hit.doc, true)
4278                    .map_err(map_store_lexical_document_error)?
4279                    .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
4280            candidates.push(crate::ingest::LexicalCandidate {
4281                document,
4282                score: hit.score,
4283            });
4284        }
4285        let mut diagnostics =
4286            crate::diag::QueryDiagnostics::lexical(crate::diag::LexicalDiagnostics {
4287                snapshot_generation: generation,
4288                indexed_through_seq: active
4289                    .indexed_through_seq()
4290                    .max(crate::wal::LogSeq::new(snapshot.absorbed_through())),
4291                requested_k: k,
4292                returned: candidates.len(),
4293                counters: lexical.result.counters,
4294                elapsed: self.clock.now().saturating_duration_since(started),
4295            });
4296        diagnostics.counters.lexical_cache_hits = usize::from(assembly.cache_hit);
4297        diagnostics.counters.lexical_cache_builds = usize::from(!assembly.cache_hit);
4298        diagnostics.timings =
4299            crate::diag::QUERY_TIMING_ENABLED.then(|| crate::diag::QueryTimings {
4300                admission: admission_time,
4301                lexical: timing_elapsed(self.clock.as_ref(), lexical_started),
4302                ..Default::default()
4303            });
4304        let outcome = crate::ingest::StoreLexicalSearchOutcome {
4305            candidates,
4306            generation,
4307            diagnostics,
4308        };
4309        work.check_now()?;
4310        let result = finish(
4311            outcome,
4312            &snapshot,
4313            &active,
4314            &assembly,
4315            &lexical.result.hits,
4316            &cancellation,
4317        );
4318        drop(active_query);
4319        result
4320    }
4321
4322    /// Returns the stored UTF-8 body for one exact document revision.
4323    ///
4324    /// Active memory and immutable region 14 are searched without adding a
4325    /// second text format. `None` means either that the exact revision is not
4326    /// present or that its text field was absent.
4327    pub fn stored_text(
4328        &self,
4329        version: crate::ingest::DocumentVersion,
4330    ) -> Result<Option<String>, crate::ingest::StoreLexicalError> {
4331        let AdmittedLexicalQuery {
4332            active,
4333            snapshot,
4334            active_query,
4335            ..
4336        } = self.admit_lexical_query()?;
4337        #[cfg(any(test, feature = "test-seams"))]
4338        self.text_materialization_work
4339            .reverse_version_lookups
4340            .fetch_add(1, Ordering::Relaxed);
4341        if let Some((row, active_version, _)) = active.existing(version.doc_id())
4342            && active_version == version
4343        {
4344            let text = active
4345                .text(row)
4346                .map_err(crate::ingest::StoreLexicalError::from)?;
4347            drop(active_query);
4348            return Ok(text.map(str::to_owned));
4349        }
4350        for segment in snapshot.segments() {
4351            #[cfg(any(test, feature = "test-seams"))]
4352            self.text_materialization_work
4353                .reverse_version_lookups
4354                .fetch_add(1, Ordering::Relaxed);
4355            let Some(row) = segment
4356                .query_row_for_document_version(version)
4357                .map_err(crate::ingest::StoreLexicalError::from)?
4358            else {
4359                continue;
4360            };
4361            let text = segment
4362                .query_stored_text()
4363                .map_err(StoreError::Segment)
4364                .map_err(crate::ingest::StoreLexicalError::from)?
4365                .and_then(|rows| rows.row(row).flatten())
4366                .map(str::to_owned);
4367            drop(active_query);
4368            return Ok(text);
4369        }
4370        drop(active_query);
4371        Ok(None)
4372    }
4373
4374    /// Reads live documents by stable id from one pinned generation.
4375    ///
4376    /// The result preserves request order and length. Unknown and tombstoned
4377    /// ids produce `None`; duplicate requested ids produce duplicate results.
4378    pub fn get_documents(
4379        &self,
4380        ids: &[crate::ingest::DocId],
4381        fields: DocumentFields,
4382    ) -> Result<Vec<Option<StoredDocument>>, StoreError> {
4383        self.get_documents_with_generation(ids, fields)
4384            .map(|(_, documents)| documents)
4385    }
4386
4387    /// Reads documents and returns the exact generation pinned by the call.
4388    ///
4389    /// This is the binding-facing form of [`Self::get_documents`].
4390    #[doc(hidden)]
4391    pub fn get_documents_with_generation(
4392        &self,
4393        ids: &[crate::ingest::DocId],
4394        fields: DocumentFields,
4395    ) -> Result<(u64, Vec<Option<StoredDocument>>), StoreError> {
4396        let AdmittedDocumentRead {
4397            generation,
4398            active,
4399            snapshot,
4400            active_query,
4401        } = self.admit_document_read()?;
4402        let mut documents = Vec::new();
4403        documents
4404            .try_reserve_exact(ids.len())
4405            .map_err(|_| StoreError::AllocationFailed {
4406                needed: allocation_bytes::<Option<StoredDocument>>(ids.len()),
4407                component: "get documents",
4408            })?;
4409        for id in ids {
4410            let mut document = active
4411                .existing(*id)
4412                .filter(|(row, _, _)| !active.is_tombstoned(*row))
4413                .map(|(row, _, _)| materialize_active_document(&active, row, fields))
4414                .transpose()?;
4415            for segment in snapshot.segments() {
4416                for row in segment.query_rows_for_doc_id(*id)? {
4417                    if document.is_some() {
4418                        return Err(StoreError::DuplicateLiveDocument { doc_id: *id });
4419                    }
4420                    document = Some(materialize_sealed_document(segment, row, fields)?);
4421                }
4422            }
4423            documents.push(document);
4424        }
4425        drop(active_query);
4426        Ok((generation, documents))
4427    }
4428
4429    /// Enumerates one deterministic, bounded page of live documents.
4430    ///
4431    /// The complete call is pinned to one generation. A continuation from a
4432    /// different generation is rejected with [`StoreError::ScanStale`].
4433    pub fn scan_documents(
4434        &self,
4435        request: DocumentScanRequest<'_>,
4436    ) -> Result<DocumentScanPage, QueryError> {
4437        let DocumentScanRequest {
4438            limit,
4439            fields,
4440            order,
4441            cursor,
4442            timestamp_range,
4443            predicate,
4444            control,
4445        } = request;
4446        if limit == 0 || limit > MAX_DOCUMENT_SCAN_LIMIT {
4447            return Err(QueryError::Store(StoreError::InvalidScan {
4448                detail: format!("limit must be in 1..={MAX_DOCUMENT_SCAN_LIMIT}, received {limit}"),
4449            }));
4450        }
4451        if let ScanOrder::Attribute { column, .. } = order {
4452            validate_scan_order_attribute(&self.schema, column)?;
4453        }
4454        if let Some(cursor) = cursor.filter(|cursor| cursor.order != order) {
4455            return Err(QueryError::Store(StoreError::InvalidScan {
4456                detail: format!(
4457                    "scan cursor was issued for order {:?}, but the request orders by {order:?}",
4458                    cursor.order
4459                ),
4460            }));
4461        }
4462        let predicate = document_scan_predicate(predicate, timestamp_range)?;
4463        if let Some(predicate) = &predicate {
4464            crate::planner::validate_predicate(predicate, &self.schema).map_err(|error| {
4465                QueryError::Store(StoreError::InvalidScan {
4466                    detail: error.to_string(),
4467                })
4468            })?;
4469        }
4470        let control = control.with_clock(Arc::clone(&self.clock));
4471        control.checkpoint()?;
4472        let AdmittedDocumentRead {
4473            generation,
4474            active,
4475            snapshot,
4476            active_query,
4477        } = self.admit_document_read().map_err(QueryError::Store)?;
4478        if let Some(cursor) = cursor.filter(|cursor| cursor.generation != generation) {
4479            return Err(QueryError::Store(StoreError::ScanStale {
4480                cursor_generation: cursor.generation,
4481                current_generation: generation,
4482            }));
4483        }
4484        let lease = SnapshotLease::new_at(Arc::clone(&snapshot), generation);
4485        let mut work = 0_usize;
4486        let rows = match order {
4487            ScanOrder::Storage => collect_storage_scan_rows(
4488                &snapshot,
4489                &active,
4490                &self.schema,
4491                predicate.as_ref(),
4492                cursor,
4493                limit,
4494                &control,
4495                &lease,
4496                &mut work,
4497            )?,
4498            ScanOrder::TimestampAscending
4499            | ScanOrder::TimestampDescending
4500            | ScanOrder::Attribute { .. } => collect_sorted_scan_rows(
4501                &snapshot,
4502                &active,
4503                &self.schema,
4504                predicate.as_ref(),
4505                cursor,
4506                limit,
4507                order,
4508                &control,
4509                &lease,
4510                &mut work,
4511            )?,
4512        };
4513        let continuation = rows.get(limit).map(|row| DocumentScanCursor {
4514            generation,
4515            order,
4516            source: row.source,
4517            next_row: row.row,
4518        });
4519        let result_len = rows.len().min(limit);
4520        let mut documents = Vec::new();
4521        documents.try_reserve_exact(result_len).map_err(|_| {
4522            QueryError::Store(StoreError::AllocationFailed {
4523                needed: allocation_bytes::<StoredDocument>(result_len),
4524                component: "scan documents",
4525            })
4526        })?;
4527        for row in rows.into_iter().take(result_len) {
4528            scan_control_checkpoint(&control, &lease, &mut work)?;
4529            let document = match row.source {
4530                DocumentScanSource::Active => {
4531                    materialize_active_document(&active, row.row as usize, fields)
4532                        .map_err(QueryError::Store)?
4533                }
4534                DocumentScanSource::Sealed(segment_id) => {
4535                    let segment = snapshot
4536                        .segments()
4537                        .iter()
4538                        .find(|segment| segment.meta().id == segment_id)
4539                        .ok_or_else(|| {
4540                            QueryError::Store(StoreError::InvalidScan {
4541                                detail: format!("scan row names absent segment {segment_id}"),
4542                            })
4543                        })?;
4544                    materialize_sealed_document(segment, row.row as usize, fields)
4545                        .map_err(QueryError::Store)?
4546                }
4547            };
4548            documents.push(document);
4549        }
4550        drop(active_query);
4551        Ok(DocumentScanPage {
4552            documents,
4553            generation,
4554            continuation,
4555        })
4556    }
4557
4558    /// Counts live documents matching an optional predicate and timestamp range.
4559    pub fn count_documents(
4560        &self,
4561        predicate: Option<&crate::meta::Predicate>,
4562        timestamp_range: Option<(i64, i64)>,
4563    ) -> Result<DocumentCount, QueryError> {
4564        let predicate = document_scan_predicate(predicate, timestamp_range)?;
4565        if let Some(predicate) = &predicate {
4566            crate::planner::validate_predicate(predicate, &self.schema).map_err(|error| {
4567                QueryError::Store(StoreError::InvalidScan {
4568                    detail: error.to_string(),
4569                })
4570            })?;
4571        }
4572        let AdmittedDocumentRead {
4573            generation,
4574            active,
4575            snapshot,
4576            active_query,
4577        } = self.admit_document_read().map_err(QueryError::Store)?;
4578        let mut count = 0_u64;
4579        for segment in snapshot.segments() {
4580            count = count
4581                .checked_add(sealed_scan_rows(segment, predicate.as_ref())?.cardinality())
4582                .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
4583        }
4584        count = count
4585            .checked_add(active_scan_rows(&active, &self.schema, predicate.as_ref())?.cardinality())
4586            .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
4587        drop(active_query);
4588        Ok(DocumentCount { count, generation })
4589    }
4590
4591    /// Ascending ids of the live documents in `active` and `segments` that
4592    /// match an already validated `predicate`, from the same per-segment
4593    /// bitmaps [`Self::count_documents`] sums. The caller pins that state.
4594    pub(crate) fn live_document_ids_matching(
4595        &self,
4596        active: &crate::ingest::ActiveSegment,
4597        segments: &[crate::segment::reader::SegmentReader],
4598        predicate: &crate::meta::Predicate,
4599    ) -> Result<Vec<crate::ingest::DocId>, QueryError> {
4600        let mut ids = Vec::new();
4601        for segment in segments {
4602            for row in sealed_scan_rows(segment, Some(predicate))?.iter() {
4603                ids.push(sealed_document_scan_row(segment, row)?.doc_id);
4604            }
4605        }
4606        for row in active_scan_rows(active, &self.schema, Some(predicate))?.iter() {
4607            ids.push(active_document_scan_row(active, row)?.doc_id);
4608        }
4609        ids.sort_unstable();
4610        ids.dedup();
4611        Ok(ids)
4612    }
4613
4614    /// Runs a structured lexical query and returns provenance plus snippets
4615    /// copied from the exact row text pinned for this generation.
4616    pub fn search_lexical_structured(
4617        &self,
4618        query: &crate::fts::query::LexicalQuery,
4619        k: usize,
4620        snippet_bytes: usize,
4621        control: QueryControl,
4622    ) -> Result<crate::ingest::StoreStructuredLexicalSearchOutcome, crate::ingest::StoreLexicalError>
4623    {
4624        self.search_lexical_structured_filtered(query, k, snippet_bytes, control, None)
4625    }
4626
4627    /// Runs structured lexical ranking with scan-compatible eligibility constraints.
4628    #[allow(clippy::too_many_arguments)]
4629    pub fn search_lexical_structured_filtered(
4630        &self,
4631        query: &crate::fts::query::LexicalQuery,
4632        k: usize,
4633        snippet_bytes: usize,
4634        control: QueryControl,
4635        filter: Option<&QueryFilter>,
4636    ) -> Result<crate::ingest::StoreStructuredLexicalSearchOutcome, crate::ingest::StoreLexicalError>
4637    {
4638        let control = control.with_clock(Arc::clone(&self.clock));
4639        let started = self.clock.now();
4640        let admission_started = timing_start(self.clock.as_ref());
4641        let AdmittedLexicalQuery {
4642            generation,
4643            active,
4644            snapshot,
4645            active_query,
4646        } = self.admit_lexical_query()?;
4647        let admission_time = timing_elapsed(self.clock.as_ref(), admission_started);
4648        let lexical_started = timing_start(self.clock.as_ref());
4649
4650        let lease = SnapshotLease::new_at(Arc::clone(&snapshot), generation);
4651        let cancellation = QueryCancellation::new(&control, &lease);
4652        cancellation.check_graph().map_err(QueryError::Scan)?;
4653        let assembly = assemble_lexical_index(
4654            LexicalInputs {
4655                filter,
4656                generation,
4657                cache: &self.lexical_index_cache,
4658                snapshot: &snapshot,
4659                active: &active,
4660                accounting: &self.accounting,
4661            },
4662            true,
4663            Some(&cancellation),
4664        )
4665        .map_err(map_store_lexical_assembly_error)?;
4666        let index = &assembly.index;
4667        let sources = &assembly.sources;
4668        let mut expansions = assembly
4669            .expand(query, &self.accounting, &cancellation)
4670            .map_err(|error| match error {
4671                expansion::ExpansionError::Query(error) => {
4672                    crate::ingest::StoreLexicalError::Query(error)
4673                }
4674                expansion::ExpansionError::Index(error) => crate::ingest::StoreLexicalError::from(
4675                    crate::planner::LexicalFilterError::from(error),
4676                ),
4677                expansion::ExpansionError::Shape(error) => {
4678                    crate::ingest::StoreLexicalError::Structured(error)
4679                }
4680            })?;
4681        let allow_lists = assembly
4682            .alive_sets
4683            .iter()
4684            .map(|alive| alive.alive_bitmap())
4685            .collect::<Vec<_>>();
4686        let mut prepared_memory =
4687            stats::AccountedCounter::new(&self.accounting, stats::AllocationComponent::Temporary)
4688                .map_err(QueryError::Store)?;
4689        let mut work = crate::fts::control::WorkCheck::new(|| cancellation.check_graph());
4690        let fields = query
4691            .fields_controlled(&mut work)
4692            .map_err(QueryError::Scan)?;
4693        prepared_memory
4694            .set(
4695                crate::fts::query::PreparedWeightedQuery::allocation_bytes_controlled(
4696                    &expansions,
4697                    &fields,
4698                    &mut work,
4699                )
4700                .map_err(QueryError::Scan)?
4701                .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?,
4702            )
4703            .map_err(QueryError::Store)?;
4704        let mut prepared = if index.total_tokens() == 0 || expansions.is_empty() {
4705            None
4706        } else {
4707            Some(
4708                crate::fts::query::PreparedWeightedQuery::new_controlled(
4709                    index,
4710                    std::mem::take(&mut expansions),
4711                    fields,
4712                    || cancellation.check_graph(),
4713                )
4714                .map_err(|error| match error {
4715                    crate::fts::search::ControlledSearchError::Index(error) => {
4716                        crate::ingest::StoreLexicalError::from(
4717                            crate::planner::LexicalFilterError::from(error),
4718                        )
4719                    }
4720                    crate::fts::search::ControlledSearchError::Control(error) => {
4721                        crate::ingest::StoreLexicalError::from(QueryError::Scan(error))
4722                    }
4723                })?,
4724            )
4725        };
4726        let mut scratch_memory =
4727            stats::AccountedCounter::new(&self.accounting, stats::AllocationComponent::Temporary)
4728                .map_err(QueryError::Store)?;
4729        let mut phrase = prepared_lexical::PhraseEligibility::new(
4730            index,
4731            Some(query),
4732            &self.accounting,
4733            &cancellation,
4734        )?;
4735        let result = match &prepared {
4736            None => crate::fts::prune::weighted::WeightedResult::default(),
4737            Some(prepared) => crate::fts::prune::weighted::search(
4738                index,
4739                prepared,
4740                k,
4741                &allow_lists,
4742                || {
4743                    cancellation
4744                        .check_graph()
4745                        .map_err(QueryError::Scan)
4746                        .map_err(crate::ingest::StoreLexicalError::from)
4747                },
4748                |doc| {
4749                    phrase
4750                        .matches(doc, &cancellation)
4751                        .map_err(crate::ingest::StoreLexicalError::from)
4752                },
4753                |bytes| {
4754                    prepared_lexical::reserve_weighted_scratch(&mut scratch_memory, bytes)
4755                        .map_err(crate::ingest::StoreLexicalError::from)
4756                },
4757            )
4758            .map_err(|error| match error {
4759                crate::fts::search::ControlledSearchError::Index(error) => {
4760                    crate::ingest::StoreLexicalError::from(
4761                        crate::planner::LexicalFilterError::from(error),
4762                    )
4763                }
4764                crate::fts::search::ControlledSearchError::Control(error) => error,
4765            })?,
4766        };
4767        let mut counters = result.counters;
4768        let scored = result.hits;
4769        let mut provenance_memory =
4770            stats::AccountedCounter::new(&self.accounting, stats::AllocationComponent::Temporary)
4771                .map_err(QueryError::Store)?;
4772        let mut candidates = Vec::with_capacity(scored.len());
4773        let mut provenance_work = crate::fts::control::WorkCheck::new(|| {
4774            cancellation.check_graph().map_err(QueryError::Scan)
4775        });
4776        for (doc, score) in scored {
4777            let mut provenance = Vec::new();
4778            let segment = index
4779                .segments()
4780                .get(doc.segment as usize)
4781                .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
4782            let scoring = prepared
4783                .as_ref()
4784                .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
4785            for expansion in scoring.expansions() {
4786                cancellation.check_graph().map_err(QueryError::Scan)?;
4787                let fields = &scoring.scoring().query().fields;
4788                prepared_lexical::reserve_weighted_scratch(
4789                    &mut provenance_memory,
4790                    crate::fts::sealed::TermStream::allocation_bytes(
4791                        segment,
4792                        &expansion.term,
4793                        fields,
4794                    ),
4795                )?;
4796                if let Some(mut stream) = crate::fts::sealed::TermStream::open_sized_controlled(
4797                    segment,
4798                    &expansion.term,
4799                    fields,
4800                    &mut provenance_work,
4801                )? {
4802                    stream.seek_controlled(doc.row, &mut provenance_work)?;
4803                    if stream.current_row() == Some(doc.row) {
4804                        provenance.push(expansion.clone());
4805                        counters.postings_decoded = counters.postings_decoded.saturating_add(1);
4806                    }
4807                    counters.blocks_decoded = counters
4808                        .blocks_decoded
4809                        .saturating_add(stream.blocks_decoded());
4810                    counters.blocks_skipped = counters
4811                        .blocks_skipped
4812                        .saturating_add(stream.blocks_skipped());
4813                }
4814                provenance_memory.set(0).map_err(QueryError::Store)?;
4815            }
4816            cancellation.check_graph().map_err(QueryError::Scan)?;
4817            let (text, document) = structured_lexical_row(&snapshot, &active, sources, doc)?;
4818            let terms = provenance
4819                .iter()
4820                .map(|entry| entry.term.clone())
4821                .collect::<Vec<_>>();
4822            let snippet = crate::fts::snippet::best_window(
4823                &self.tokenizer,
4824                text,
4825                &terms,
4826                snippet_bytes,
4827                true,
4828            )?
4829            .ok_or(crate::ingest::StoreLexicalError::MissingSnippetMatch {
4830                segment: doc.segment,
4831                row: doc.row,
4832            })?;
4833            let snippet_text = snippet
4834                .text(text)
4835                .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
4836            candidates.push(crate::ingest::ExplainedLexicalCandidate {
4837                document,
4838                score,
4839                provenance,
4840                snippet: crate::fts::query::OwnedLexicalSnippet {
4841                    text: snippet_text.to_owned(),
4842                    source: snippet.window,
4843                    highlights: snippet.highlights,
4844                    source_len: text.len(),
4845                },
4846            });
4847        }
4848        cancellation.check_graph().map_err(QueryError::Scan)?;
4849        let mut diagnostics =
4850            crate::diag::QueryDiagnostics::lexical(crate::diag::LexicalDiagnostics {
4851                snapshot_generation: generation,
4852                indexed_through_seq: active
4853                    .indexed_through_seq()
4854                    .max(crate::wal::LogSeq::new(snapshot.absorbed_through())),
4855                requested_k: k,
4856                returned: candidates.len(),
4857                counters,
4858                elapsed: self.clock.now().saturating_duration_since(started),
4859            });
4860        diagnostics.counters.lexical_cache_hits = usize::from(assembly.cache_hit);
4861        diagnostics.counters.lexical_cache_builds = usize::from(!assembly.cache_hit);
4862        diagnostics.timings =
4863            crate::diag::QUERY_TIMING_ENABLED.then(|| crate::diag::QueryTimings {
4864                admission: admission_time,
4865                lexical: timing_elapsed(self.clock.as_ref(), lexical_started),
4866                ..Default::default()
4867            });
4868        drop(active_query);
4869        Ok(crate::ingest::StoreStructuredLexicalSearchOutcome {
4870            candidates,
4871            expansions: prepared
4872                .as_mut()
4873                .map_or(expansions, |query| query.take_expansions()),
4874            generation,
4875            diagnostics,
4876        })
4877    }
4878
4879    /// Runs the store vector engine and exact lexical leg against one pinned
4880    /// generation, then delegates every blend decision to `crate::fusion`.
4881    pub fn search_hybrid(
4882        &self,
4883        vector_query: crate::ingest::SearchRequest<'_>,
4884        lexical_query: &crate::fts::search::TermQuery,
4885        hybrid_query: &crate::fusion::HybridQuery,
4886        options: impl Into<SearchOptions>,
4887        control: QueryControl,
4888    ) -> Result<crate::ingest::StoreHybridSearchOutcome, crate::fusion::FusionError> {
4889        self.search_hybrid_inner(
4890            vector_query,
4891            PinnedLexicalQuery::Term(lexical_query),
4892            hybrid_query,
4893            options.into(),
4894            control,
4895        )
4896    }
4897
4898    /// Runs vector work with any structured lexical operator against the
4899    /// same pinned snapshot and generation.
4900    pub fn search_hybrid_structured(
4901        &self,
4902        vector_query: crate::ingest::SearchRequest<'_>,
4903        lexical_query: &crate::fts::query::LexicalQuery,
4904        hybrid_query: &crate::fusion::HybridQuery,
4905        options: impl Into<SearchOptions>,
4906        control: QueryControl,
4907    ) -> Result<crate::ingest::StoreHybridSearchOutcome, crate::fusion::FusionError> {
4908        self.search_hybrid_inner(
4909            vector_query,
4910            PinnedLexicalQuery::Structured(lexical_query),
4911            hybrid_query,
4912            options.into(),
4913            control,
4914        )
4915    }
4916
4917    fn search_hybrid_inner(
4918        &self,
4919        vector_query: crate::ingest::SearchRequest<'_>,
4920        lexical_query: PinnedLexicalQuery<'_>,
4921        hybrid_query: &crate::fusion::HybridQuery,
4922        options: SearchOptions,
4923        control: QueryControl,
4924    ) -> Result<crate::ingest::StoreHybridSearchOutcome, crate::fusion::FusionError> {
4925        self.search_hybrid_then(
4926            vector_query,
4927            lexical_query,
4928            hybrid_query,
4929            options,
4930            control,
4931            false,
4932            |outcome, _, _, _, _| Ok(outcome),
4933        )
4934    }
4935
4936    #[allow(clippy::too_many_arguments)] // The existing query arguments plus its scoped continuation.
4937    fn search_hybrid_then<R>(
4938        &self,
4939        vector_query: crate::ingest::SearchRequest<'_>,
4940        lexical_query: PinnedLexicalQuery<'_>,
4941        hybrid_query: &crate::fusion::HybridQuery,
4942        options: SearchOptions,
4943        control: QueryControl,
4944        capture_rows: bool,
4945        finish: impl FnOnce(
4946            crate::ingest::StoreHybridSearchOutcome,
4947            &PublishedSnapshot,
4948            &crate::ingest::ActiveSegment,
4949            &QueryCancellation<'_>,
4950            Option<&materialize::HybridAddresses>,
4951        ) -> Result<R, crate::fusion::FusionError>,
4952    ) -> Result<R, crate::fusion::FusionError> {
4953        self.search_hybrid_prepared_then(
4954            || Ok::<_, std::convert::Infallible>(vector_query),
4955            vector_query.filter,
4956            vector_query.eligible,
4957            lexical_query,
4958            hybrid_query,
4959            options,
4960            control,
4961            capture_rows,
4962            None,
4963            finish,
4964        )
4965        .map_err(|error| match error {
4966            HybridPreparationError::Preparation(never) => match never {},
4967            HybridPreparationError::Search(error) => error,
4968        })
4969    }
4970
4971    #[allow(clippy::too_many_arguments)] // The existing query arguments plus its scoped continuation.
4972    fn search_hybrid_prepared_then<'vector, E, R>(
4973        &self,
4974        prepare_vector: impl FnOnce() -> Result<crate::ingest::SearchRequest<'vector>, E>,
4975        filter: Option<&QueryFilter>,
4976        known_eligible: Option<&[crate::ingest::DocId]>,
4977        lexical_query: PinnedLexicalQuery<'_>,
4978        hybrid_query: &crate::fusion::HybridQuery,
4979        options: SearchOptions,
4980        control: QueryControl,
4981        capture_rows: bool,
4982        pinned: Option<AdmittedVectorSearch<'_>>,
4983        finish: impl FnOnce(
4984            crate::ingest::StoreHybridSearchOutcome,
4985            &PublishedSnapshot,
4986            &crate::ingest::ActiveSegment,
4987            &QueryCancellation<'_>,
4988            Option<&materialize::HybridAddresses>,
4989        ) -> Result<R, crate::fusion::FusionError>,
4990    ) -> Result<R, HybridPreparationError<E>> {
4991        let control = if pinned.is_some() {
4992            control
4993        } else {
4994            control.with_clock(Arc::clone(&self.clock))
4995        };
4996        let started = self.clock.now();
4997        // Resolve known restrictions here, before either producer starts. Deferred
4998        // producers may supply ids later; their lexical ranking is refreshed below.
4999        let eligible_filter = known_eligible.map(|ids| {
5000            filter.cloned().map_or_else(
5001                || QueryFilter::eligible(&self.schema, ids),
5002                |filter| filter.with_eligible(ids),
5003            )
5004        });
5005        let filter = eligible_filter.as_ref().or(filter);
5006        let admission_started = timing_start(self.clock.as_ref());
5007        let mut options = options;
5008        let requested_tier = options.explicit_tier();
5009        // Hybrid once forced SearchTier::Exact unconditionally, because
5010        // fusion needs an anchor for unseen scores and only an exhaustive
5011        // scan could name the farthest alive row. The deterministic
5012        // ceiling now supplies that anchor on every tier, so hybrid
5013        // follows the caller's tier and reaches the graph like the dense
5014        // leg does.
5015        //
5016        // A segment with no published graph is different. There `Auto`
5017        // resolves to the Bit4 scan, whose scores are estimates, and
5018        // fusion may only fuse exactly rescored scores. Graph traversal
5019        // rescores its retained pool exactly, so it is safe; the Bit4
5020        // scan is not. When the caller stated no preference and no
5021        // segment has earned a graph yet, hybrid therefore still selects
5022        // Exact for itself. This is the adaptive ladder choosing the
5023        // tier that can honour the contract, not a fallback hiding one.
5024        let pinned_epoch = pinned
5025            .as_ref()
5026            .map(|admitted| admitted.snapshot.epoch_alias());
5027        let admitted = match pinned {
5028            Some(admitted) => admitted,
5029            None => self
5030                .admit_vector_search_for(options, requested_tier.is_none())
5031                .map_err(crate::fusion::FusionError::from)?,
5032        };
5033        options = admitted.execution_options;
5034        let tier_resolution = if requested_tier.is_none() {
5035            if snapshot_has_graph(&admitted.snapshot) {
5036                Some(crate::diag::HybridTierResolution::Auto)
5037            } else {
5038                options = options.with_tier(SearchTier::Exact);
5039                Some(crate::diag::HybridTierResolution::Exact)
5040            }
5041        } else {
5042            None
5043        };
5044        let corpus_rows = hybrid::corpus_rows(&admitted.snapshot, &admitted.active_segment)?;
5045        // Round zero asks each producer for the window plus one row, so the
5046        // element just past the window is the stability bound's unseen value.
5047        // A caller that disabled widening asks for the corpus at once, which
5048        // is the complete-list fusion this path used to run unconditionally.
5049        let mut width = if hybrid_query.max_rounds == 0 {
5050            corpus_rows
5051        } else {
5052            hybrid::hybrid_window(hybrid_query.k, corpus_rows)?.width
5053        };
5054        let panic_vector = self.consume_hybrid_test_fault(crate::fusion::FusionLeg::Vector);
5055        let panic_lexical = self.consume_hybrid_test_fault(crate::fusion::FusionLeg::Lexical);
5056        let lexical_worker = self.ensure_lexical_worker()?;
5057        let mut timings = crate::diag::QueryTimings {
5058            admission: timing_elapsed(self.clock.as_ref(), admission_started),
5059            ..Default::default()
5060        };
5061        let mut lexical_inputs = LexicalInputs {
5062            filter,
5063            generation: admitted.generation,
5064            cache: &self.lexical_index_cache,
5065            snapshot: &admitted.snapshot,
5066            active: &admitted.active_segment,
5067            accounting: &self.accounting,
5068        };
5069        #[cfg(any(test, feature = "test-seams"))]
5070        let preparation_observer = crate::fts::preparation_observer::current();
5071        let mut lexical_preparation = prepared_lexical::PreparedLexicalQuery::new(lexical_query);
5072        #[cfg(any(test, feature = "test-seams"))]
5073        let fresh_preparation = hybrid::fresh_round_test_control();
5074        let run_lexical_leg = |lexical_inputs: LexicalInputs<'_>,
5075                               preparation: &mut prepared_lexical::PreparedLexicalQuery<'_>,
5076                               bound,
5077                               queued| {
5078            #[cfg(any(test, feature = "test-seams"))]
5079            if fresh_preparation {
5080                preparation.reset_for_test();
5081            }
5082            #[cfg(any(test, feature = "test-seams"))]
5083            let _preparation_scope =
5084                crate::fts::preparation_observer::enter(preparation_observer.clone());
5085            let queue_time = timing_elapsed(self.clock.as_ref(), queued);
5086            let lexical_started = timing_start(self.clock.as_ref());
5087            let name = std::thread::current().name().map(str::to_owned);
5088            let result = (|| {
5089                maybe_trigger_hybrid_leg_panic(panic_lexical, "injected lexical hybrid leg panic");
5090                let lease =
5091                    SnapshotLease::new_at(Arc::clone(&admitted.snapshot), admitted.generation);
5092                let cancellation = QueryCancellation::new(&control, &lease);
5093                cancellation
5094                    .check_graph()
5095                    .map_err(QueryError::Scan)
5096                    .map_err(crate::fusion::FusionError::from)?;
5097                match lexical_query {
5098                    PinnedLexicalQuery::Term(_) => {
5099                        exact_lexical_leg(lexical_inputs, bound, &cancellation, preparation)
5100                    }
5101                    PinnedLexicalQuery::Structured(query) => exact_structured_lexical_leg(
5102                        lexical_inputs,
5103                        &self.tokenizer,
5104                        query,
5105                        bound,
5106                        &cancellation,
5107                        preparation,
5108                    ),
5109                }
5110            })();
5111            (
5112                name,
5113                result,
5114                queue_time,
5115                timing_elapsed(self.clock.as_ref(), lexical_started),
5116            )
5117        };
5118        let caller_chose_tier = requested_tier.is_some();
5119        let run_vector_leg = |vector_query: crate::ingest::SearchRequest<'_>,
5120                              vector_preparation: &mut prepared::PreparedVectorQuery<'_>,
5121                              k: usize| {
5122            let graph_available = snapshot_has_graph(&admitted.snapshot);
5123            let scan_reason_override = if caller_chose_tier || !graph_available {
5124                None
5125            } else if k > corpus_rows {
5126                Some(crate::planner::ScanReason::FullMaterialization)
5127            } else if graph_round_is_worth_it(k, corpus_rows) {
5128                None
5129            } else {
5130                Some(crate::planner::ScanReason::WideningCap {
5131                    ef: graph_round_ef(k),
5132                    rows: corpus_rows,
5133                })
5134            };
5135            search_pinned(
5136                admitted.pool.as_deref(),
5137                &admitted.snapshot,
5138                &admitted.active_segment,
5139                &self.accounting,
5140                admitted.generation,
5141                pinned_epoch.unwrap_or_else(|| self.epoch_identity()),
5142                vector_query,
5143                k,
5144                // A graph traversal only pays for itself while its
5145                // candidate pool is a small fraction of the segment. Once
5146                // a widening round would visit more than
5147                // `GRAPH_WIDENING_CAP_NUMERATOR/DENOMINATOR` of the rows,
5148                // the traversal is a scan wearing a graph costume: it
5149                // pays the graph's indirection and then reads most of the
5150                // segment anyway. Such a round runs as Exact instead,
5151                // which is both faster and exactly rescored.
5152                // Only when the caller stated no preference. An explicit
5153                // tier is a contract: honour it, and let fusion reject it
5154                // loudly if it cannot supply exact scores.
5155                if caller_chose_tier || graph_round_is_worth_it(k, corpus_rows) {
5156                    options
5157                } else {
5158                    options.with_tier(SearchTier::Exact)
5159                },
5160                control.clone(),
5161                GraphBoundMode::Shared,
5162                requested_tier,
5163                scan_reason_override,
5164                started,
5165                self.clock.as_ref(),
5166                vector_preparation,
5167                #[cfg(any(test, feature = "test-seams"))]
5168                self.vector_fault_controller.as_ref(),
5169            )
5170            .map_err(crate::fusion::FusionError::from)
5171        };
5172        let lexical_queued = timing_start(self.clock.as_ref());
5173        let (prepared_vector_result, lexical_result) = lexical_worker.run_scoped(
5174            || {
5175                run_lexical_leg(
5176                    lexical_inputs,
5177                    &mut lexical_preparation,
5178                    width.saturating_add(1),
5179                    lexical_queued,
5180                )
5181            },
5182            || {
5183                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5184                    let vector_query =
5185                        prepare_vector().map_err(HybridPreparationError::Preparation)?;
5186                    if vector_query.filter.is_some() && filter.is_none() {
5187                        return Err(HybridPreparationError::Search(
5188                            QueryError::Store(StoreError::InvalidScan {
5189                                detail:
5190                                    "deferred vector preparation cannot introduce a query filter"
5191                                        .to_owned(),
5192                            })
5193                            .into(),
5194                        ));
5195                    }
5196                    let deferred_filter = if known_eligible.is_none() {
5197                        vector_query.eligible.map(|ids| {
5198                            filter.cloned().map_or_else(
5199                                || QueryFilter::eligible(&self.schema, ids),
5200                                |filter| filter.with_eligible(ids),
5201                            )
5202                        })
5203                    } else {
5204                        None
5205                    };
5206                    let effective_vector_query = vector_query
5207                        .with_filter(deferred_filter.as_ref().or(filter).or(vector_query.filter));
5208                    let mut vector_preparation = prepared::PreparedVectorQuery::new(
5209                        vector_query.vector(),
5210                        pinned_epoch.unwrap_or_else(|| self.epoch_identity()),
5211                        &self.accounting,
5212                    );
5213                    maybe_trigger_hybrid_leg_panic(
5214                        panic_vector,
5215                        "injected vector hybrid leg panic",
5216                    );
5217                    let result = run_vector_leg(
5218                        effective_vector_query,
5219                        &mut vector_preparation,
5220                        width.saturating_add(1),
5221                    );
5222                    Ok((vector_query, deferred_filter, vector_preparation, result))
5223                }))
5224                .unwrap_or(Err(HybridPreparationError::Search(
5225                    crate::fusion::FusionError::LegPanic {
5226                        leg: crate::fusion::FusionLeg::Vector,
5227                        detail: "vector hybrid leg panicked",
5228                    },
5229                )))
5230            },
5231        )?;
5232        let (lexical_thread_name, lexical_result, queue_time, lexical_time) = lexical_result
5233            .unwrap_or_else(|error| {
5234                (
5235                    Some("zeppelin-fts".to_owned()),
5236                    Err(error),
5237                    std::time::Duration::ZERO,
5238                    std::time::Duration::ZERO,
5239                )
5240            });
5241        #[cfg(any(test, feature = "test-seams"))]
5242        if let Ok(mut receipt) = self.hybrid_execution_receipt.lock() {
5243            *receipt = Some(HybridExecutionReceipt {
5244                vector_thread: std::thread::current().id(),
5245                lexical_thread_name,
5246                vector_completed: true,
5247                lexical_completed: true,
5248            });
5249        }
5250        #[cfg(not(any(test, feature = "test-seams")))]
5251        let _ = lexical_thread_name;
5252        // run_scoped has joined lexical work even when vector preparation
5253        // failed or panicked. A stopped admission takes precedence over a late
5254        // producer error; otherwise retain the caller's original error type.
5255        let (vector_query, deferred_filter, mut vector_preparation, vector_result) =
5256            match prepared_vector_result {
5257                Ok(prepared) => prepared,
5258                Err(error) => {
5259                    let lease =
5260                        SnapshotLease::new_at(Arc::clone(&admitted.snapshot), admitted.generation);
5261                    QueryCancellation::new(&control, &lease)
5262                        .check_graph()
5263                        .map_err(map_scan_error)?;
5264                    if let Err(lexical_error) = lexical_result
5265                        && matches!(
5266                            lexical_error,
5267                            crate::fusion::FusionError::ReadCancelled { .. }
5268                                | crate::fusion::FusionError::Timeout { .. }
5269                                | crate::fusion::FusionError::Cancelled { .. }
5270                        )
5271                    {
5272                        return Err(HybridPreparationError::Search(lexical_error));
5273                    }
5274                    return Err(error);
5275                }
5276            };
5277        let vector_query =
5278            vector_query.with_filter(deferred_filter.as_ref().or(filter).or(vector_query.filter));
5279        let (
5280            mut vector_outcome,
5281            (mut lexical_hits, mut lexical_assembly, mut lexical_counters, mut lexical_cache_hit),
5282        ) = resolve_hybrid_leg_results(vector_result, lexical_result)?;
5283
5284        timings.lexical_queue = queue_time;
5285        timings.lexical = lexical_time;
5286        let mut deferred_initial_cache_hit = None;
5287        if let Some(filter) = deferred_filter.as_ref() {
5288            // Preserve lexical/vector overlap for deferred embedding. Eligibility
5289            // was unavailable to the first lexical pass: discard that ranking,
5290            // reset its prepared assembly, and rank the eligible rows before fusion.
5291            lexical_inputs.filter = Some(filter);
5292            lexical_preparation = prepared_lexical::PreparedLexicalQuery::new(lexical_query);
5293            let queued = timing_start(self.clock.as_ref());
5294            let (_, reranked) = lexical_worker.run_scoped(
5295                || {
5296                    run_lexical_leg(
5297                        lexical_inputs,
5298                        &mut lexical_preparation,
5299                        width.saturating_add(1),
5300                        queued,
5301                    )
5302                },
5303                || (),
5304            )?;
5305            let (_, reranked, queue_time, lexical_time) = reranked?;
5306            let (hits, assembly, mut counters, cache_hit) = reranked?;
5307            accumulate_search_counters(&mut counters, &lexical_counters);
5308            deferred_initial_cache_hit = Some(lexical_cache_hit);
5309            lexical_hits = hits;
5310            lexical_assembly = assembly;
5311            lexical_counters = counters;
5312            lexical_cache_hit = cache_hit;
5313            timings.lexical_queue += queue_time;
5314            timings.lexical += lexical_time;
5315        }
5316        let anchors = hybrid::HybridAnchors {
5317            vector_ceiling: vector_outcome.vector_ceiling,
5318            lexical_maximum: lexical_hits.first().map_or(0.0, |hit| hit.bm25),
5319        };
5320        let fusion_lease =
5321            SnapshotLease::new_at(Arc::clone(&admitted.snapshot), admitted.generation);
5322        let fusion_cancellation = QueryCancellation::new(&control, &fusion_lease);
5323        let mut work = hybrid::HybridWork::new();
5324        if let Some(cache_hit) = deferred_initial_cache_hit {
5325            work.lexical_cache_hits += usize::from(cache_hit);
5326            work.lexical_cache_builds += usize::from(!cache_hit);
5327        }
5328        let mut cross_scores = hybrid::CrossScoreCache::new(vector_outcome.epoch);
5329        let mut round_buffers = hybrid::HybridRoundBuffers::new();
5330        let mut fusion_scratch =
5331            crate::fusion::StoreFusionScratch::new(Arc::clone(&self.accounting));
5332        let mut rescore_counters = crate::diag::ScanRescoreCounters::default();
5333        let mut rounds = 1_usize;
5334        let mut budget_exhausted = false;
5335        let (fused, report_window, addresses) = loop {
5336            if let Some(vector_timings) = vector_outcome.diagnostics.timings {
5337                timings.vector += vector_timings.vector;
5338            }
5339            let fusion_started = timing_start(self.clock.as_ref());
5340            let round = hybrid::build_round(
5341                &admitted.snapshot,
5342                &admitted.active_segment,
5343                &lexical_assembly,
5344                &lexical_preparation,
5345                &self.tokenizer,
5346                lexical_query,
5347                vector_query.vector(),
5348                &vector_outcome.candidates,
5349                anchors,
5350                &lexical_hits,
5351                width,
5352                &fusion_cancellation,
5353                capture_rows,
5354                &self.accounting,
5355                &mut cross_scores,
5356                &mut round_buffers,
5357            )?;
5358            // Cross-fill reads full-precision rows on the caller thread,
5359            // outside the vector producer. Include those actual reads in
5360            // the same scan work receipt as the producer's scored rows.
5361            let cross_fill_dims = u64::try_from(round.vector_scores_computed)
5362                .ok()
5363                .and_then(|rows| rows.checked_mul(vector_query.vector().len() as u64))
5364                .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5365            let cross_fill_bytes = cross_fill_dims
5366                .checked_mul(4)
5367                .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5368            vector_outcome.stats.dims_touched = vector_outcome
5369                .stats
5370                .dims_touched
5371                .checked_add(cross_fill_dims)
5372                .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5373            vector_outcome.stats.bytes_read = vector_outcome
5374                .stats
5375                .bytes_read
5376                .checked_add(cross_fill_bytes)
5377                .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5378            if cross_fill_dims != 0 {
5379                let caller = std::thread::current().id();
5380                if !vector_outcome.stats.worker_thread_ids.contains(&caller) {
5381                    vector_outcome.stats.worker_thread_ids.push(caller);
5382                }
5383                vector_outcome.stats.threads_used = vector_outcome.stats.worker_thread_ids.len();
5384            }
5385            accumulate_search_counters(&mut lexical_counters, &round.lexical_counters);
5386            if let Some(counters) = vector_outcome.diagnostics.scan_rescore {
5387                rescored_scan::accumulate(&mut rescore_counters, counters)?;
5388            }
5389            work.complete_round(
5390                &vector_outcome.stats,
5391                vector_outcome.graph_stats,
5392                lexical_counters,
5393            )?;
5394            work.lexical_cache_hits += usize::from(lexical_cache_hit);
5395            work.lexical_cache_builds += usize::from(!lexical_cache_hit);
5396            work.plans.append(&mut vector_outcome.diagnostics.plan);
5397            work.cross_filled_vector = work
5398                .cross_filled_vector
5399                .checked_add(round.vector_scores_computed)
5400                .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5401            work.cross_filled_lexical = work
5402                .cross_filled_lexical
5403                .checked_add(round.lexical_scores_computed)
5404                .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5405            work.vector_candidates_produced = work
5406                .vector_candidates_produced
5407                .checked_add(vector_outcome.candidates.len())
5408                .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5409            work.lexical_candidates_produced = work
5410                .lexical_candidates_produced
5411                .checked_add(lexical_hits.len())
5412                .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5413            let provenance = hybrid::round_provenance(&vector_outcome, lexical_hits.len(), width);
5414            let fused = hybrid::fuse_round(
5415                hybrid_query,
5416                &round,
5417                provenance,
5418                requested_tier == Some(SearchTier::Exact),
5419                &mut fusion_scratch,
5420            )?;
5421            timings.fusion_cross_fill += timing_elapsed(self.clock.as_ref(), fusion_started);
5422            if fused.report.termination != crate::fusion::FusionTermination::WindowUnproven
5423                || width >= corpus_rows
5424            {
5425                break (
5426                    fused,
5427                    crate::diag::HybridReport {
5428                        normalization_policy_version:
5429                            crate::fusion::HYBRID_NORMALIZATION_POLICY_VERSION,
5430                        lexical_full_materializations: 0,
5431                        provenance,
5432                        window: width,
5433                        vector_returned: round.vector.len(),
5434                        lexical_returned: round.lexical.len(),
5435                        cross_filled_vector: round.cross_filled_vector,
5436                        cross_filled_lexical: round.cross_filled_lexical,
5437                        total_cross_filled_vector: work.cross_filled_vector,
5438                        total_cross_filled_lexical: work.cross_filled_lexical,
5439                        vector_candidates_produced: work.vector_candidates_produced,
5440                        lexical_candidates_produced: work.lexical_candidates_produced,
5441                    },
5442                    round.addresses,
5443                );
5444            }
5445            width = if rounds >= hybrid_query.max_rounds {
5446                budget_exhausted = true;
5447                corpus_rows
5448            } else {
5449                width.saturating_mul(2).min(corpus_rows)
5450            };
5451            rounds = rounds.saturating_add(1);
5452            let lexical_queued = timing_start(self.clock.as_ref());
5453            let (vector_result, lexical_result) = lexical_worker.run_scoped(
5454                || {
5455                    run_lexical_leg(
5456                        lexical_inputs,
5457                        &mut lexical_preparation,
5458                        width.saturating_add(1),
5459                        lexical_queued,
5460                    )
5461                },
5462                || {
5463                    run_vector_leg(
5464                        vector_query,
5465                        &mut vector_preparation,
5466                        width.saturating_add(1),
5467                    )
5468                },
5469            )?;
5470            let (_, lexical_result, queue_time, lexical_time) =
5471                lexical_result.unwrap_or_else(|error| {
5472                    (
5473                        Some("zeppelin-fts".to_owned()),
5474                        Err(error),
5475                        std::time::Duration::ZERO,
5476                        std::time::Duration::ZERO,
5477                    )
5478                });
5479            let (next_vector, (next_hits, next_sources, next_counters, next_cache_hit)) =
5480                resolve_hybrid_leg_results(vector_result, lexical_result)?;
5481            timings.lexical_queue += queue_time;
5482            timings.lexical += lexical_time;
5483            vector_outcome = next_vector;
5484            lexical_hits = next_hits;
5485            lexical_assembly = next_sources;
5486            lexical_counters = next_counters;
5487            lexical_cache_hit = next_cache_hit;
5488        };
5489
5490        let crate::ingest::SearchOutcome {
5491            candidates: _,
5492            vector_ceiling: _,
5493            stats: _,
5494            graph_stats: _,
5495            generation,
5496            epoch,
5497            diagnostics: vector_diagnostics,
5498        } = vector_outcome;
5499        let mut report = fused.report;
5500        report.rounds = rounds;
5501        if budget_exhausted {
5502            report.budget_exhausted = true;
5503            report.termination = crate::fusion::FusionTermination::BudgetFullMaterialization;
5504        }
5505        let mut diagnostics =
5506            crate::diag::QueryDiagnostics::hybrid(crate::diag::HybridDiagnostics {
5507                snapshot_generation: vector_diagnostics.snapshot_generation,
5508                indexed_through_seq: vector_diagnostics.indexed_through_seq,
5509                plan: work.plans,
5510                approximate: vector_diagnostics.approximate
5511                    || report.termination
5512                        == crate::fusion::FusionTermination::ApproximateCandidates,
5513                exact_rescore: vector_diagnostics.exact_rescore,
5514                requested_k: hybrid_query.k,
5515                returned: fused.hits.len(),
5516                scan: work.scan,
5517                graph: work.graph,
5518                lexical: work.lexical,
5519                report,
5520                hybrid: report_window,
5521                tier_resolution,
5522                epoch,
5523                elapsed: self.clock.now().saturating_duration_since(started),
5524            });
5525        diagnostics.counters.lexical_cache_hits = work.lexical_cache_hits;
5526        diagnostics.scan_rescore = options.scan_rescore().map(|_| rescore_counters);
5527        diagnostics.counters.lexical_cache_builds = work.lexical_cache_builds;
5528        diagnostics.timings = crate::diag::QUERY_TIMING_ENABLED.then_some(timings);
5529        let outcome = crate::ingest::StoreHybridSearchOutcome {
5530            hits: fused.hits,
5531            lexical_expansions: lexical_preparation.take_expansions()?,
5532            generation,
5533            diagnostics,
5534        };
5535        finish(
5536            outcome,
5537            &admitted.snapshot,
5538            &admitted.active_segment,
5539            &fusion_cancellation,
5540            addresses.as_ref(),
5541        )
5542        .map_err(HybridPreparationError::Search)
5543    }
5544
5545    /// Returns `(hits, builds)` for the assembled-lexical-index cache.
5546    ///
5547    /// Exists so a test can prove the cache is actually reused, and actually
5548    /// rebuilt when its inputs move, rather than inferring either from a
5549    /// timing.
5550    #[cfg(any(test, feature = "test-seams"))]
5551    #[must_use]
5552    pub fn lexical_index_cache_counters(&self) -> (u64, u64) {
5553        (
5554            self.lexical_index_cache.hits.load(Ordering::Relaxed),
5555            self.lexical_index_cache.builds.load(Ordering::Relaxed),
5556        )
5557    }
5558
5559    /// `(hits, builds)` of immutable lexical statistics contributions.
5560    #[cfg(any(test, feature = "test-seams"))]
5561    #[must_use]
5562    pub fn lexical_contribution_cache_counters(&self) -> (u64, u64) {
5563        (
5564            self.lexical_index_cache
5565                .contribution_hits
5566                .load(Ordering::Relaxed),
5567            self.lexical_index_cache
5568                .contribution_builds
5569                .load(Ordering::Relaxed),
5570        )
5571    }
5572
5573    fn consume_hybrid_test_fault(&self, leg: crate::fusion::FusionLeg) -> bool {
5574        #[cfg(any(test, feature = "test-seams"))]
5575        {
5576            let Ok(mut fault) = self.hybrid_leg_fault.lock() else {
5577                return false;
5578            };
5579            if matches!(*fault, Some(HybridLegTestFault::Panic(candidate)) if candidate == leg) {
5580                *fault = None;
5581                return true;
5582            }
5583        }
5584        #[cfg(not(any(test, feature = "test-seams")))]
5585        let _ = leg;
5586        false
5587    }
5588
5589    fn search_with_graph_bound_mode(
5590        &self,
5591        request: crate::ingest::SearchRequest<'_>,
5592        k: usize,
5593        options: SearchOptions,
5594        control: QueryControl,
5595        graph_bound_mode: GraphBoundMode,
5596    ) -> Result<crate::ingest::SearchOutcome, QueryError> {
5597        self.search_with_graph_bound_mode_then(
5598            request,
5599            k,
5600            options,
5601            control,
5602            graph_bound_mode,
5603            |outcome, _, _| Ok(outcome),
5604        )
5605    }
5606
5607    fn search_with_graph_bound_mode_then<R>(
5608        &self,
5609        request: crate::ingest::SearchRequest<'_>,
5610        k: usize,
5611        options: SearchOptions,
5612        control: QueryControl,
5613        graph_bound_mode: GraphBoundMode,
5614        finish: impl FnOnce(
5615            crate::ingest::SearchOutcome,
5616            &AdmittedVectorSearch<'_>,
5617            &QueryControl,
5618        ) -> Result<R, QueryError>,
5619    ) -> Result<R, QueryError> {
5620        let eligible_filter = request.eligible.map(|ids| {
5621            request.filter.cloned().map_or_else(
5622                || QueryFilter::eligible(&self.schema, ids),
5623                |filter| filter.with_eligible(ids),
5624            )
5625        });
5626        let request = request.with_filter(eligible_filter.as_ref().or(request.filter));
5627        let control = control.with_clock(Arc::clone(&self.clock));
5628        let started = self.clock.now();
5629        let admission_started = timing_start(self.clock.as_ref());
5630        let admitted = self.admit_vector_search(options)?;
5631        let admission_time = timing_elapsed(self.clock.as_ref(), admission_started);
5632        let mut vector_preparation = prepared::PreparedVectorQuery::new(
5633            request.vector(),
5634            self.epoch_identity(),
5635            &self.accounting,
5636        );
5637        let score = || {
5638            let mut outcome = search_pinned(
5639                admitted.pool.as_deref(),
5640                &admitted.snapshot,
5641                &admitted.active_segment,
5642                &self.accounting,
5643                admitted.generation,
5644                self.epoch_identity(),
5645                request,
5646                k,
5647                options,
5648                control.clone(),
5649                graph_bound_mode,
5650                options.explicit_tier(),
5651                None,
5652                started,
5653                self.clock.as_ref(),
5654                &mut vector_preparation,
5655                #[cfg(any(test, feature = "test-seams"))]
5656                self.vector_fault_controller.as_ref(),
5657            )?;
5658            if let Some(timings) = &mut outcome.diagnostics.timings {
5659                timings.admission = admission_time;
5660            }
5661            finish(outcome, &admitted, &control)
5662        };
5663        #[cfg(any(test, feature = "test-seams"))]
5664        let result = crate::kernels::vector_fault::run_store_scoring(
5665            self.kernel_fault_controller.as_ref(),
5666            score,
5667        );
5668        #[cfg(not(any(test, feature = "test-seams")))]
5669        let result = score();
5670        #[cfg(any(test, feature = "test-seams"))]
5671        {
5672            if let Some(controller) = self.vector_fault_controller.as_ref() {
5673                controller.finalize_search(result.is_ok());
5674            }
5675        }
5676        result
5677    }
5678
5679    fn ensure_query_pool(&self) -> Result<Arc<pool::QueryPool>, QueryError> {
5680        let mut pool_slot = self.query_pool.lock().map_err(|_| {
5681            QueryError::Store(StoreError::Synchronization {
5682                component: "query pool",
5683            })
5684        })?;
5685        if pool_slot.is_none() {
5686            let capacity = crate::scan::physical_thread_capacity().map_err(|error| {
5687                QueryError::Store(StoreError::QueryPoolCapacity {
5688                    source: error.to_string(),
5689                })
5690            })?;
5691            let pool =
5692                pool::QueryPool::start(capacity, &self.accounting).map_err(QueryError::Store)?;
5693            *pool_slot = Some(Arc::new(pool));
5694        }
5695        let pool = pool_slot.as_ref().cloned().ok_or({
5696            QueryError::Store(StoreError::Synchronization {
5697                component: "query pool initialization",
5698            })
5699        })?;
5700        drop(pool_slot);
5701        Ok(pool)
5702    }
5703
5704    fn ensure_lexical_worker(
5705        &self,
5706    ) -> Result<Arc<pool::LexicalWorker>, crate::fusion::FusionError> {
5707        let mut worker_slot =
5708            self.lexical_worker
5709                .lock()
5710                .map_err(|_| crate::fusion::FusionError::LegThreadStart {
5711                    leg: crate::fusion::FusionLeg::Lexical,
5712                    detail: "pooled lexical worker synchronization was poisoned".to_owned(),
5713                })?;
5714        if worker_slot.is_none() {
5715            *worker_slot = Some(Arc::new(pool::LexicalWorker::start()?));
5716        }
5717        let worker = worker_slot.as_ref().cloned().ok_or_else(|| {
5718            crate::fusion::FusionError::LegThreadStart {
5719                leg: crate::fusion::FusionLeg::Lexical,
5720                detail: "pooled lexical worker initialization failed".to_owned(),
5721            }
5722        })?;
5723        drop(worker_slot);
5724        Ok(worker)
5725    }
5726
5727    fn admit_vector_search(
5728        &self,
5729        options: SearchOptions,
5730    ) -> Result<AdmittedVectorSearch<'_>, QueryError> {
5731        self.admit_vector_search_for(options, false)
5732    }
5733
5734    fn admit_vector_search_for(
5735        &self,
5736        options: SearchOptions,
5737        hybrid_default: bool,
5738    ) -> Result<AdmittedVectorSearch<'_>, QueryError> {
5739        #[cfg(any(test, feature = "test-seams"))]
5740        self.text_materialization_work
5741            .admissions
5742            .fetch_add(1, Ordering::Relaxed);
5743        let state = self
5744            .state
5745            .lock()
5746            .map_err(|_| QueryError::Store(StoreError::Synchronization { component: "state" }))?;
5747        match *state {
5748            StoreState::Open => {}
5749            StoreState::Closing => return Err(QueryError::Store(StoreError::Closing)),
5750            StoreState::Closed => return Err(QueryError::Store(StoreError::Closed)),
5751        }
5752        let active_guard = self.active.lock().map_err(|_| {
5753            QueryError::Store(StoreError::Synchronization {
5754                component: "active segment",
5755            })
5756        })?;
5757        let active_state = active_guard
5758            .as_ref()
5759            .ok_or(QueryError::Store(StoreError::Closed))?;
5760        let generation = active_state.generation;
5761        let active_segment = Arc::clone(&active_state.segment);
5762        let snapshot = self
5763            .snapshot
5764            .read()
5765            .map_err(|_| {
5766                QueryError::Store(StoreError::Synchronization {
5767                    component: "published snapshot",
5768                })
5769            })?
5770            .as_ref()
5771            .cloned()
5772            .ok_or(QueryError::Store(StoreError::Closed))?;
5773        let execution_options = if hybrid_default && !snapshot_has_graph(&snapshot) {
5774            options.with_tier(SearchTier::Exact)
5775        } else {
5776            options
5777        };
5778        // Default hybrid may move from graph traversal to Exact while widening.
5779        // Admit that resource while the Store is still Open under this lock.
5780        let needs_query_pool = !active_segment.is_empty()
5781            || (hybrid_default && !snapshot.segments().is_empty())
5782            || match execution_options.tier() {
5783                SearchTier::Auto => snapshot.segments().iter().any(|segment| {
5784                    !segment.directory().iter().any(|entry| {
5785                        entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id()
5786                    })
5787                }),
5788                SearchTier::Exact => !snapshot.segments().is_empty(),
5789                SearchTier::Scan => true,
5790                SearchTier::Graph(_) => false,
5791            };
5792        let pool = if needs_query_pool {
5793            Some(self.ensure_query_pool()?)
5794        } else {
5795            None
5796        };
5797        self.active_queries.fetch_add(1, Ordering::Relaxed);
5798        let active_query = ActiveQuery {
5799            count: &self.active_queries,
5800        };
5801        drop(active_guard);
5802        drop(state);
5803        Ok(AdmittedVectorSearch {
5804            pool,
5805            execution_options,
5806            snapshot: snapshot::ReadSnapshot::new(snapshot),
5807            active_segment,
5808            generation,
5809            _active_query: Some(active_query),
5810        })
5811    }
5812
5813    #[cfg(test)]
5814    fn search_independent_for_test(
5815        &self,
5816        request: crate::ingest::SearchRequest<'_>,
5817        k: usize,
5818        options: SearchOptions,
5819        control: QueryControl,
5820    ) -> Result<crate::ingest::SearchOutcome, QueryError> {
5821        self.search_with_graph_bound_mode(request, k, options, control, GraphBoundMode::Independent)
5822    }
5823}
5824
5825#[derive(Clone, Copy)]
5826pub(crate) enum StructuredLexicalSource {
5827    Sealed(usize),
5828    Active,
5829}
5830
5831#[derive(Clone, Debug)]
5832struct EligibleIds {
5833    numeric: Vec<crate::ingest::DocId>,
5834    // The sealed reader permutation orders little-endian identity bytes, not u128s.
5835    // Sort once per query, shared by every sealed source and both hybrid legs.
5836    byte_order: Vec<[u8; 16]>,
5837}
5838
5839const ELIGIBLE_BYTE_ORDER_MIN_IDS: usize = 1_000;
5840
5841/// Validated scan-compatible query constraints. Ranking statistics remain corpus-wide.
5842#[derive(Clone, Debug)]
5843pub struct QueryFilter {
5844    predicate: Option<crate::meta::Predicate>,
5845    schema: crate::meta::Schema,
5846    eligible_ids: Option<EligibleIds>,
5847}
5848
5849impl QueryFilter {
5850    /// Upper bound for the owned backing of a fresh eligibility-only filter.
5851    /// Reserve before construction; the schema clone cannot exceed its source.
5852    #[cfg(feature = "graph-cypher")]
5853    pub(crate) fn eligible_capacity_bytes(
5854        schema: &crate::meta::Schema,
5855        count: usize,
5856    ) -> Option<usize> {
5857        let numeric = count.checked_mul(std::mem::size_of::<crate::ingest::DocId>())?;
5858        let byte_order = if count >= ELIGIBLE_BYTE_ORDER_MIN_IDS {
5859            count.checked_mul(std::mem::size_of::<[u8; 16]>())?
5860        } else {
5861            0
5862        };
5863        std::mem::size_of::<Self>()
5864            .checked_add(schema.resident_bytes()?)?
5865            .checked_add(numeric)?
5866            .checked_add(byte_order)
5867    }
5868
5869    /// Validates attributes and a half-open timestamp range against this schema.
5870    pub fn new(
5871        schema: &crate::meta::Schema,
5872        predicate: Option<&crate::meta::Predicate>,
5873        timestamp_range: Option<(i64, i64)>,
5874    ) -> Result<Option<Self>, QueryError> {
5875        let Some(predicate) = document_scan_predicate(predicate, timestamp_range)? else {
5876            return Ok(None);
5877        };
5878        crate::planner::validate_predicate(&predicate, schema).map_err(|error| {
5879            QueryError::Store(StoreError::InvalidScan {
5880                detail: error.to_string(),
5881            })
5882        })?;
5883        Ok(Some(Self {
5884            predicate: Some(predicate),
5885            schema: schema.clone(),
5886            eligible_ids: None,
5887        }))
5888    }
5889
5890    fn active_rows(
5891        &self,
5892        active: &crate::ingest::ActiveSegment,
5893    ) -> Result<crate::meta::DocBitmap, QueryError> {
5894        let mut rows = active_scan_rows(active, &self.schema, self.predicate.as_ref())?;
5895        if let Some(ids) = &self.eligible_ids {
5896            // Active rows have no identity index. Scan their bounded, u32-addressed
5897            // in-memory ids; no additional index or persisted state is needed.
5898            let mut eligible = crate::meta::DocBitmap::new();
5899            for (row, id) in active.doc_ids().iter().enumerate() {
5900                if ids.numeric.binary_search(id).is_ok() {
5901                    eligible.insert(
5902                        u32::try_from(row)
5903                            .map_err(|_| QueryError::Store(StoreError::ActiveRowOverflow))?,
5904                    );
5905                }
5906            }
5907            rows.intersect_with(&eligible);
5908        }
5909        Ok(rows)
5910    }
5911
5912    /// Restricts all query legs to these document ids, before top-k and fusion.
5913    /// An empty set selects no documents. Input order and duplicates do not matter.
5914    #[must_use]
5915    pub fn eligible(schema: &crate::meta::Schema, ids: &[crate::ingest::DocId]) -> Self {
5916        Self {
5917            predicate: None,
5918            schema: schema.clone(),
5919            eligible_ids: None,
5920        }
5921        .with_eligible(ids)
5922    }
5923
5924    /// Intersects these ids with existing eligibility and attribute/time constraints.
5925    #[must_use]
5926    pub fn with_eligible(mut self, ids: &[crate::ingest::DocId]) -> Self {
5927        let mut ids = ids.to_vec();
5928        ids.sort_unstable();
5929        ids.dedup();
5930        if let Some(existing) = &self.eligible_ids {
5931            ids.retain(|id| existing.numeric.binary_search(id).is_ok());
5932        }
5933        let mut byte_order = if ids.len() >= ELIGIBLE_BYTE_ORDER_MIN_IDS {
5934            ids.iter()
5935                .map(|id| id.get().to_le_bytes())
5936                .collect::<Vec<_>>()
5937        } else {
5938            Vec::new()
5939        };
5940        byte_order.sort_unstable();
5941        self.eligible_ids = Some(EligibleIds {
5942            numeric: ids,
5943            byte_order,
5944        });
5945        self
5946    }
5947
5948    fn sealed_rows(
5949        &self,
5950        segment: &crate::segment::reader::SegmentReader,
5951    ) -> Result<crate::meta::DocBitmap, QueryError> {
5952        let mut rows = sealed_scan_rows(segment, self.predicate.as_ref())?;
5953        if let Some(ids) = &self.eligible_ids {
5954            rows.intersect_with(
5955                &segment
5956                    .query_rows_for_eligible_ids(&ids.numeric, &ids.byte_order)
5957                    .map_err(QueryError::Store)?,
5958            );
5959        }
5960        Ok(rows)
5961    }
5962}
5963
5964struct LexicalAssembly {
5965    index: crate::fts::index::LexicalIndex,
5966    alive_sets: Vec<Arc<crate::meta::AliveSet>>,
5967    sources: Vec<StructuredLexicalSource>,
5968    contributions: Vec<Arc<CachedLexicalContribution>>,
5969    vocabulary: Arc<Mutex<Option<Arc<CachedVocabulary>>>>,
5970    // Filtered readers retain the owner of the shared vocabulary-slot charge.
5971    _base: Option<Arc<LexicalAssembly>>,
5972    // The last cache/query owner releases this charge, even after eviction.
5973    memory: stats::AccountedCounter,
5974}
5975
5976struct CachedVocabulary {
5977    view: crate::fts::vocabulary::Vocabulary,
5978    phonetic: Mutex<Option<Arc<expansion::CachedPhonetic>>>,
5979    // Charged for as long as any query or assembly retains the dictionary.
5980    _memory: stats::AccountedCounter,
5981}
5982
5983impl LexicalAssembly {
5984    fn vocabulary(
5985        &self,
5986        accounting: &Arc<stats::Accounting>,
5987        cancellation: &QueryCancellation<'_>,
5988    ) -> Result<Arc<CachedVocabulary>, QueryError> {
5989        let mut cached = cancellation.cache_lock(&self.vocabulary, "lexical vocabulary cache")?;
5990        if let Some(value) = cached.as_ref() {
5991            return Ok(Arc::clone(value));
5992        }
5993        let mut memory =
5994            stats::AccountedCounter::new(accounting, stats::AllocationComponent::Cache)
5995                .map_err(QueryError::Store)?;
5996        let mut temporary =
5997            stats::AccountedCounter::new(accounting, stats::AllocationComponent::Temporary)
5998                .map_err(QueryError::Store)?;
5999        let mut work = crate::fts::control::WorkCheck::new(|| {
6000            cancellation.check_graph().map_err(QueryError::Scan)
6001        });
6002        let view = crate::fts::vocabulary::Vocabulary::build_controlled(
6003            self.index.vocabulary_terms(),
6004            |scratch, owned| {
6005                cancellation.check_graph().map_err(QueryError::Scan)?;
6006                prepared_lexical::reserve_weighted_scratch(&mut temporary, scratch)?;
6007                let bytes = owned
6008                    .and_then(|bytes| bytes.checked_add(std::mem::size_of::<CachedVocabulary>()))
6009                    .ok_or(QueryError::Store(StoreError::BudgetExceeded {
6010                        needed: u64::MAX,
6011                        budget: u64::MAX,
6012                        component: "lexical vocabulary cache",
6013                    }))?;
6014                memory.set(bytes).map_err(QueryError::Store)
6015            },
6016            &mut work,
6017        )?;
6018        let value = Arc::new(CachedVocabulary {
6019            view,
6020            phonetic: Mutex::new(None),
6021            _memory: memory,
6022        });
6023        *cached = Some(Arc::clone(&value));
6024        Ok(value)
6025    }
6026
6027    /// Assembly spines only: contribution reservations own their shared
6028    /// counters/bitmaps, and current readers own decoded postings/alive views.
6029    fn owned_bytes(&self) -> Option<usize> {
6030        std::mem::size_of::<Self>()
6031            .checked_add(self.index.resident_spine_bytes()?)?
6032            .checked_add(
6033                self.alive_sets
6034                    .capacity()
6035                    .checked_mul(std::mem::size_of::<Arc<crate::meta::AliveSet>>())?,
6036            )?
6037            .checked_add(
6038                self.sources
6039                    .capacity()
6040                    .checked_mul(std::mem::size_of::<StructuredLexicalSource>())?,
6041            )?
6042            .checked_add(
6043                self.contributions
6044                    .capacity()
6045                    .checked_mul(std::mem::size_of::<Arc<CachedLexicalContribution>>())?,
6046            )
6047    }
6048}
6049
6050enum LexicalContributionKey {
6051    // Reader objects are recreated on publication. Same immutable file plus
6052    // freshly verified live membership identifies the same contribution.
6053    Sealed(crate::segment::SegmentMeta, (u64, u64)),
6054    Active(std::sync::Weak<crate::ingest::ActiveSegment>),
6055}
6056
6057struct CachedLexicalContribution {
6058    key: LexicalContributionKey,
6059    statistics: Arc<crate::fts::index::LiveSegmentStatistics>,
6060    // Only active alive sets are newly allocated here. Sealed sets remain the
6061    // current reader's owned/counted query view, never a retired reader's view.
6062    active_alive: Option<Arc<crate::meta::AliveSet>>,
6063    _memory: stats::AccountedCounter,
6064}
6065
6066impl CachedLexicalContribution {
6067    fn build(
6068        key: LexicalContributionKey,
6069        postings: &crate::fts::sealed::SealedSegment,
6070        alive: &Arc<crate::meta::AliveSet>,
6071        ordinal: usize,
6072        accounting: &Arc<stats::Accounting>,
6073        work: &mut crate::fts::control::WorkCheck<impl FnMut() -> Result<(), LexicalAssemblyError>>,
6074    ) -> Result<Arc<Self>, LexicalAssemblyError> {
6075        work.check_now()?;
6076        let active = matches!(key, LexicalContributionKey::Active(_));
6077        let alive_bytes = if active {
6078            alive
6079                .resident_bytes()
6080                .and_then(|n| n.checked_add(std::mem::size_of::<crate::meta::AliveSet>()))
6081                .and_then(|n| n.checked_add(2 * std::mem::size_of::<usize>()))
6082        } else {
6083            Some(0)
6084        };
6085        let bytes =
6086            crate::fts::index::LiveSegmentStatistics::allocation_bytes(alive.alive_bitmap())
6087                .and_then(|n| n.checked_add(std::mem::size_of::<Self>()))
6088                .and_then(|n| n.checked_add(2 * std::mem::size_of::<usize>()))
6089                .and_then(|n| n.checked_add(alive_bytes?))
6090                .ok_or(LexicalAssemblyError::Store(StoreError::BudgetExceeded {
6091                    needed: u64::MAX,
6092                    budget: u64::MAX,
6093                    component: "cache",
6094                }))?;
6095        let mut memory =
6096            stats::AccountedCounter::new(accounting, stats::AllocationComponent::Cache)
6097                .map_err(LexicalAssemblyError::Store)?;
6098        memory.set(bytes).map_err(LexicalAssemblyError::Store)?;
6099        let mut statistics = crate::fts::index::LiveSegmentStatistics::build_controlled(
6100            ordinal,
6101            postings,
6102            alive.alive_bitmap(),
6103            work,
6104        )?;
6105        if statistics.rows.cardinality() != 0
6106            && statistics.rows.cardinality() != u64::from(postings.row_count())
6107        {
6108            statistics.frequency_cache = Some(
6109                crate::fts::live_df::LiveFrequencyCache::new(accounting)
6110                    .map_err(LexicalAssemblyError::Store)?,
6111            );
6112        }
6113        let statistics = Arc::new(statistics);
6114        work.check_now()?;
6115        Ok(Arc::new(Self {
6116            key,
6117            statistics,
6118            active_alive: active.then(|| Arc::clone(alive)),
6119            _memory: memory,
6120        }))
6121    }
6122}
6123
6124enum LexicalAssemblyError {
6125    Store(StoreError),
6126    Lexical(crate::fts::index::IndexError),
6127    MissingDocumentIdentity(crate::segment::SegmentId),
6128    Cancelled(crate::scan::ScanError),
6129}
6130
6131impl From<crate::fts::index::IndexError> for LexicalAssemblyError {
6132    fn from(error: crate::fts::index::IndexError) -> Self {
6133        Self::Lexical(error)
6134    }
6135}
6136
6137impl From<StoreError> for LexicalAssemblyError {
6138    fn from(error: StoreError) -> Self {
6139        Self::Store(error)
6140    }
6141}
6142
6143/// One assembled lexical index, held for as long as the inputs it was built
6144/// from are still the store's inputs.
6145///
6146/// A complete assembly is reused while its snapshot/active identities match.
6147/// After mutation, independently reserved contributions reuse unchanged sealed
6148/// counters and membership while rebuilding current source ordinals and totals.
6149///
6150/// Reusing it is safe because both inputs are copy-on-write behind an `Arc`
6151/// and the borrow checker, not a convention, enforces that. A
6152/// `PublishedSnapshot` is replaced wholesale in `Store::snapshot` when a
6153/// generation is published, and `ActiveState::segment` is replaced with a
6154/// freshly built `Arc<ActiveSegment>` on every mutation; neither is ever
6155/// reached through `Arc::get_mut` or `Arc::make_mut`, so no shared value can
6156/// be edited in place. Pointer identity is therefore exactly "the same
6157/// inputs", and it is the same immutability that already lets
6158/// `SegmentReader::query_postings`, `SegmentReader::query_alive` and
6159/// `ActiveSegment::sealed_lexical` cache with no invalidation hook at all.
6160/// A key derived from observable state instead -- the generation, a row
6161/// count, a log sequence -- would have to enumerate every way the inputs can
6162/// move; pointer identity cannot miss one.
6163///
6164/// The handles are `Weak`, not `Arc`, for two reasons. A strong reference
6165/// would pin a retired snapshot's mappings and a retired active segment's
6166/// buffers past the point the store released them. And a `Weak` still keeps
6167/// the allocation reserved, so a dropped input's address can never be reused
6168/// by a different value and compare equal here.
6169struct CachedLexicalAssembly {
6170    generation: u64,
6171    snapshot: std::sync::Weak<PublishedSnapshot>,
6172    active: std::sync::Weak<crate::ingest::ActiveSegment>,
6173    /// Set when this assembly was built, or has since been proven, under
6174    /// `require_document_identity`. The hybrid legs do not require it and the
6175    /// standalone paths do, so an entry one path built is never handed to the
6176    /// other until the proof has actually run.
6177    document_identity_verified: bool,
6178    assembly: Arc<LexicalAssembly>,
6179}
6180
6181impl CachedLexicalAssembly {
6182    fn matches(
6183        &self,
6184        snapshot: &Arc<PublishedSnapshot>,
6185        active: &Arc<crate::ingest::ActiveSegment>,
6186    ) -> bool {
6187        self.snapshot
6188            .upgrade()
6189            .is_some_and(|cached| Arc::ptr_eq(&cached, snapshot))
6190            && self
6191                .active
6192                .upgrade()
6193                .is_some_and(|cached| Arc::ptr_eq(&cached, active))
6194    }
6195}
6196
6197/// The store's single-entry cache of assembled lexical indexes.
6198///
6199/// One entry bounds retained history. A builder holds the entry lock through
6200/// construction; old admissions may build their own answer, but cannot replace
6201/// a cached newer generation. Only immutable contribution Arcs cross rebuilds.
6202pub(crate) struct LexicalIndexCache {
6203    entry: Mutex<Option<CachedLexicalAssembly>>,
6204    #[cfg(any(test, feature = "test-seams"))]
6205    hits: AtomicU64,
6206    #[cfg(any(test, feature = "test-seams"))]
6207    builds: AtomicU64,
6208    #[cfg(any(test, feature = "test-seams"))]
6209    contribution_hits: AtomicU64,
6210    #[cfg(any(test, feature = "test-seams"))]
6211    contribution_builds: AtomicU64,
6212}
6213
6214impl LexicalIndexCache {
6215    pub(crate) const fn new() -> Self {
6216        Self {
6217            entry: Mutex::new(None),
6218            #[cfg(any(test, feature = "test-seams"))]
6219            hits: AtomicU64::new(0),
6220            #[cfg(any(test, feature = "test-seams"))]
6221            builds: AtomicU64::new(0),
6222            #[cfg(any(test, feature = "test-seams"))]
6223            contribution_hits: AtomicU64::new(0),
6224            #[cfg(any(test, feature = "test-seams"))]
6225            contribution_builds: AtomicU64::new(0),
6226        }
6227    }
6228
6229    #[cfg(any(test, feature = "test-seams"))]
6230    fn record_hit(&self) {
6231        self.hits.fetch_add(1, Ordering::Relaxed);
6232    }
6233
6234    #[cfg(not(any(test, feature = "test-seams")))]
6235    const fn record_hit(&self) {}
6236
6237    #[cfg(any(test, feature = "test-seams"))]
6238    fn record_build(&self) {
6239        self.builds.fetch_add(1, Ordering::Relaxed);
6240    }
6241
6242    #[cfg(not(any(test, feature = "test-seams")))]
6243    const fn record_build(&self) {}
6244
6245    fn record_contribution(&self, hit: bool) {
6246        #[cfg(any(test, feature = "test-seams"))]
6247        if hit {
6248            self.contribution_hits.fetch_add(1, Ordering::Relaxed);
6249        } else {
6250            self.contribution_builds.fetch_add(1, Ordering::Relaxed);
6251        }
6252        #[cfg(not(any(test, feature = "test-seams")))]
6253        let _ = hit;
6254    }
6255
6256    fn lock(
6257        &self,
6258    ) -> Result<std::sync::MutexGuard<'_, Option<CachedLexicalAssembly>>, LexicalAssemblyError>
6259    {
6260        self.entry.lock().map_err(|_| {
6261            LexicalAssemblyError::Store(StoreError::Synchronization {
6262                component: "lexical index cache",
6263            })
6264        })
6265    }
6266
6267    fn lock_controlled(
6268        &self,
6269        cancellation: Option<&QueryCancellation<'_>>,
6270    ) -> Result<std::sync::MutexGuard<'_, Option<CachedLexicalAssembly>>, LexicalAssemblyError>
6271    {
6272        let Some(cancellation) = cancellation else {
6273            return self.lock();
6274        };
6275        cancellation
6276            .check_graph()
6277            .map_err(LexicalAssemblyError::Cancelled)?;
6278        let guard = loop {
6279            match self.entry.try_lock() {
6280                Ok(guard) => break guard,
6281                Err(std::sync::TryLockError::Poisoned(_)) => {
6282                    return Err(LexicalAssemblyError::Store(StoreError::Synchronization {
6283                        component: "lexical index cache",
6284                    }));
6285                }
6286                Err(std::sync::TryLockError::WouldBlock) => {}
6287            }
6288            cancellation
6289                .check_graph()
6290                .map_err(LexicalAssemblyError::Cancelled)?;
6291            std::thread::park_timeout(Duration::from_millis(1));
6292        };
6293        cancellation
6294            .check_graph()
6295            .map_err(LexicalAssemblyError::Cancelled)?;
6296        Ok(guard)
6297    }
6298}
6299
6300/// Everything an assembled lexical index is built from, and the cache that
6301/// holds the one built last. Grouped because the two hybrid legs and the two
6302/// standalone paths all pass exactly this set through unchanged.
6303#[derive(Clone, Copy)]
6304struct LexicalInputs<'a> {
6305    filter: Option<&'a QueryFilter>,
6306    generation: u64,
6307    cache: &'a LexicalIndexCache,
6308    snapshot: &'a Arc<PublishedSnapshot>,
6309    active: &'a Arc<crate::ingest::ActiveSegment>,
6310    accounting: &'a Arc<stats::Accounting>,
6311}
6312
6313struct LexicalAssemblyReceipt {
6314    assembly: Arc<LexicalAssembly>,
6315    cache_hit: bool,
6316}
6317
6318impl std::ops::Deref for LexicalAssemblyReceipt {
6319    type Target = LexicalAssembly;
6320    fn deref(&self) -> &Self::Target {
6321        &self.assembly
6322    }
6323}
6324
6325fn map_eligibility_error(error: QueryError) -> LexicalAssemblyError {
6326    match error {
6327        QueryError::Store(error) => LexicalAssemblyError::Store(error),
6328        QueryError::Scan(error) => LexicalAssemblyError::Cancelled(error),
6329        error => LexicalAssemblyError::Store(StoreError::InvalidScan {
6330            detail: error.to_string(),
6331        }),
6332    }
6333}
6334
6335fn assemble_lexical_index(
6336    inputs: LexicalInputs<'_>,
6337    require_document_identity: bool,
6338    cancellation: Option<&QueryCancellation<'_>>,
6339) -> Result<LexicalAssemblyReceipt, LexicalAssemblyError> {
6340    let receipt =
6341        assemble_unfiltered_lexical_index(inputs, require_document_identity, cancellation)?;
6342    let Some(filter) = inputs.filter else {
6343        return Ok(receipt);
6344    };
6345    let mut alive_sets = Vec::with_capacity(receipt.alive_sets.len());
6346    for (source, alive) in receipt.sources.iter().zip(&receipt.alive_sets) {
6347        if let Some(control) = cancellation {
6348            control
6349                .check_graph()
6350                .map_err(LexicalAssemblyError::Cancelled)?;
6351        }
6352        let rows = match source {
6353            StructuredLexicalSource::Active => filter.active_rows(inputs.active),
6354            StructuredLexicalSource::Sealed(ordinal) => inputs
6355                .snapshot
6356                .segments()
6357                .get(*ordinal)
6358                .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))
6359                .and_then(|segment| filter.sealed_rows(segment)),
6360        }
6361        .map_err(map_eligibility_error)?;
6362        let mut eligible = (**alive).clone();
6363        #[cfg(any(test, feature = "test-seams"))]
6364        crate::fts::preparation_observer::record_filter_work(0, 0, 1);
6365        eligible.retain(&rows);
6366        alive_sets.push(Arc::new(eligible));
6367    }
6368    if let Some(control) = cancellation {
6369        control
6370            .check_graph()
6371            .map_err(LexicalAssemblyError::Cancelled)?;
6372    }
6373    let memory =
6374        stats::AccountedCounter::new(inputs.accounting, stats::AllocationComponent::Temporary)
6375            .map_err(LexicalAssemblyError::Store)?;
6376    let mut assembly = LexicalAssembly {
6377        index: receipt.index.clone(),
6378        alive_sets,
6379        sources: receipt.sources.clone(),
6380        contributions: receipt.contributions.clone(),
6381        vocabulary: Arc::clone(&receipt.vocabulary),
6382        _base: Some(Arc::clone(&receipt.assembly)),
6383        memory,
6384    };
6385    let bytes = assembly
6386        .alive_sets
6387        .iter()
6388        .fold(assembly.owned_bytes(), |bytes, alive| {
6389            bytes?
6390                .checked_add(alive.resident_bytes()?)?
6391                .checked_add(std::mem::size_of::<crate::meta::AliveSet>())?
6392                .checked_add(2 * std::mem::size_of::<usize>())
6393        })
6394        .ok_or(LexicalAssemblyError::Store(StoreError::ActiveRowOverflow))?;
6395    assembly
6396        .memory
6397        .set(bytes)
6398        .map_err(LexicalAssemblyError::Store)?;
6399    Ok(LexicalAssemblyReceipt {
6400        cache_hit: receipt.cache_hit,
6401        assembly: Arc::new(assembly),
6402    })
6403}
6404
6405fn assemble_unfiltered_lexical_index(
6406    inputs: LexicalInputs<'_>,
6407    require_document_identity: bool,
6408    cancellation: Option<&QueryCancellation<'_>>,
6409) -> Result<LexicalAssemblyReceipt, LexicalAssemblyError> {
6410    let LexicalInputs {
6411        filter: _,
6412        generation,
6413        cache,
6414        snapshot,
6415        active,
6416        accounting,
6417    } = inputs;
6418    let mut entry = cache.lock_controlled(cancellation)?;
6419    if let Some(cached) = entry.as_mut()
6420        && cached.matches(snapshot, active)
6421    {
6422        if require_document_identity && !cached.document_identity_verified {
6423            verify_document_identity(snapshot, cancellation)?;
6424            cached.document_identity_verified = true;
6425        }
6426        let assembly = Arc::clone(&cached.assembly);
6427        cache.record_hit();
6428        return Ok(LexicalAssemblyReceipt {
6429            assembly,
6430            cache_hit: true,
6431        });
6432    }
6433    // One builder at a time bounds concurrent allocation and prevents a late
6434    // old admission from repeatedly replacing a newer assembly. The old entry
6435    // remains accounted during construction and is replaced only on success.
6436    let previous = entry.as_ref().map(|cached| Arc::clone(&cached.assembly));
6437    let contributions = previous
6438        .as_ref()
6439        .map_or(&[][..], |a| a.contributions.as_slice());
6440    let (mut assembly, owned_bytes) = build_lexical_assembly(
6441        snapshot,
6442        active,
6443        accounting,
6444        require_document_identity,
6445        cancellation,
6446        cache,
6447        contributions,
6448    )?;
6449    cache.record_build();
6450    assembly
6451        .memory
6452        .set(owned_bytes)
6453        .map_err(LexicalAssemblyError::Store)?;
6454    if let Some(cancellation) = cancellation {
6455        cancellation
6456            .check_graph()
6457            .map_err(LexicalAssemblyError::Cancelled)?;
6458    }
6459    let assembly = Arc::new(assembly);
6460    if entry
6461        .as_ref()
6462        .is_none_or(|cached| generation >= cached.generation)
6463    {
6464        *entry = Some(CachedLexicalAssembly {
6465            generation,
6466            snapshot: Arc::downgrade(snapshot),
6467            active: Arc::downgrade(active),
6468            document_identity_verified: require_document_identity,
6469            assembly: Arc::clone(&assembly),
6470        });
6471    }
6472    drop(entry);
6473    Ok(LexicalAssemblyReceipt {
6474        assembly,
6475        cache_hit: false,
6476    })
6477}
6478
6479fn check_segment_document_identity(
6480    segment: &crate::segment::reader::SegmentReader,
6481    postings: &crate::fts::sealed::SealedSegment,
6482) -> Result<(), LexicalAssemblyError> {
6483    if postings.row_count() != 0
6484        && segment
6485            .document_version(0)
6486            .map_err(StoreError::Segment)
6487            .map_err(LexicalAssemblyError::Store)?
6488            .is_none()
6489    {
6490        return Err(LexicalAssemblyError::MissingDocumentIdentity(
6491            segment.meta().id,
6492        ));
6493    }
6494    Ok(())
6495}
6496
6497/// Proves document identity over a snapshot whose assembly was built without
6498/// that requirement.
6499///
6500/// Every segment's postings were already decoded to build that assembly, so
6501/// this reads the same cached values and adds no region decode.
6502fn verify_document_identity(
6503    snapshot: &PublishedSnapshot,
6504    cancellation: Option<&QueryCancellation<'_>>,
6505) -> Result<(), LexicalAssemblyError> {
6506    for segment in snapshot.segments() {
6507        if let Some(cancellation) = cancellation {
6508            cancellation
6509                .check_graph()
6510                .map_err(LexicalAssemblyError::Cancelled)?;
6511        }
6512        if let Some(postings) = segment
6513            .query_postings()
6514            .map_err(LexicalAssemblyError::Store)?
6515        {
6516            check_segment_document_identity(segment, &postings)?;
6517        }
6518    }
6519    Ok(())
6520}
6521
6522fn build_lexical_assembly(
6523    snapshot: &PublishedSnapshot,
6524    active: &Arc<crate::ingest::ActiveSegment>,
6525    accounting: &Arc<stats::Accounting>,
6526    require_document_identity: bool,
6527    cancellation: Option<&QueryCancellation<'_>>,
6528    cache: &LexicalIndexCache,
6529    previous: &[Arc<CachedLexicalContribution>],
6530) -> Result<(LexicalAssembly, usize), LexicalAssemblyError> {
6531    let mut work = crate::fts::control::WorkCheck::new(|| {
6532        cancellation
6533            .map_or(Ok(()), |control| control.check_graph())
6534            .map_err(LexicalAssemblyError::Cancelled)
6535    });
6536    work.check_now()?;
6537    let mut index = crate::fts::index::LexicalIndex::new();
6538    let mut alive_sets = Vec::new();
6539    let mut sources = Vec::new();
6540    let mut contributions = Vec::new();
6541    for (ordinal, segment) in snapshot.segments().iter().enumerate() {
6542        if let Some(cancellation) = cancellation {
6543            cancellation
6544                .check_graph()
6545                .map_err(LexicalAssemblyError::Cancelled)?;
6546        }
6547        if let Some(postings) = segment
6548            .query_postings()
6549            .map_err(LexicalAssemblyError::Store)?
6550        {
6551            if require_document_identity {
6552                check_segment_document_identity(segment, &postings)?;
6553            }
6554            let alive = segment.query_alive().map_err(LexicalAssemblyError::Store)?;
6555            let identity = segment
6556                .lexical_postings_identity()
6557                .map_err(StoreError::Segment)
6558                .map_err(LexicalAssemblyError::Store)?;
6559            let reused = previous
6560                .binary_search_by(|value| match &value.key {
6561                    LexicalContributionKey::Sealed(meta, _) => meta.id.cmp(&segment.meta().id),
6562                    LexicalContributionKey::Active(_) => std::cmp::Ordering::Greater,
6563                })
6564                .ok()
6565                .and_then(|slot| previous.get(slot))
6566                .filter(|value| {
6567                    matches!(&value.key, LexicalContributionKey::Sealed(meta, stored)
6568                    if meta.same_segment_file(segment.meta()) && *stored == identity)
6569                        && value.statistics.rows == *alive.alive_bitmap()
6570                });
6571            cache.record_contribution(reused.is_some());
6572            let contribution = match reused {
6573                Some(value) => Arc::clone(value),
6574                None => CachedLexicalContribution::build(
6575                    LexicalContributionKey::Sealed(segment.meta().clone(), identity),
6576                    &postings,
6577                    &alive,
6578                    index.segments().len(),
6579                    accounting,
6580                    &mut work,
6581                )?,
6582            };
6583            index.push_shared_with_statistics(postings, Arc::clone(&contribution.statistics));
6584            contributions.push(contribution);
6585            alive_sets.push(alive);
6586            sources.push(StructuredLexicalSource::Sealed(ordinal));
6587        }
6588    }
6589    if active.has_text_controlled(&mut work)? {
6590        let sealed = active.sealed_lexical_controlled(accounting, &mut work)?;
6591        let reused = previous.last().filter(|value| {
6592            matches!(&value.key, LexicalContributionKey::Active(cached)
6593                if cached.upgrade().is_some_and(|cached| Arc::ptr_eq(&cached, active)))
6594        });
6595        cache.record_contribution(reused.is_some());
6596        let contribution = match reused {
6597            Some(value) => Arc::clone(value),
6598            None => {
6599                let alive = Arc::new(active.alive_controlled(&mut work)?);
6600                CachedLexicalContribution::build(
6601                    LexicalContributionKey::Active(Arc::downgrade(active)),
6602                    &sealed,
6603                    &alive,
6604                    index.segments().len(),
6605                    accounting,
6606                    &mut work,
6607                )?
6608            }
6609        };
6610        let alive = contribution
6611            .active_alive
6612            .as_ref()
6613            .ok_or(LexicalAssemblyError::Store(StoreError::Synchronization {
6614                component: "active lexical contribution",
6615            }))?;
6616        index.push_shared_with_statistics(sealed, Arc::clone(&contribution.statistics));
6617        alive_sets.push(Arc::clone(alive));
6618        contributions.push(contribution);
6619        sources.push(StructuredLexicalSource::Active);
6620    }
6621    crate::fts::control::sort_by(
6622        &mut contributions,
6623        |left, right| match (&left.key, &right.key) {
6624            (LexicalContributionKey::Sealed(a, _), LexicalContributionKey::Sealed(b, _)) => {
6625                a.id.cmp(&b.id)
6626            }
6627            (LexicalContributionKey::Active(_), LexicalContributionKey::Active(_)) => {
6628                std::cmp::Ordering::Equal
6629            }
6630            (LexicalContributionKey::Active(_), _) => std::cmp::Ordering::Greater,
6631            (_, LexicalContributionKey::Active(_)) => std::cmp::Ordering::Less,
6632        },
6633        &mut work,
6634    )?;
6635    let assembly = LexicalAssembly {
6636        index,
6637        alive_sets,
6638        sources,
6639        contributions,
6640        vocabulary: Arc::new(Mutex::new(None)),
6641        _base: None,
6642        memory: stats::AccountedCounter::new(accounting, stats::AllocationComponent::Cache)
6643            .map_err(LexicalAssemblyError::Store)?,
6644    };
6645    let owned_bytes = assembly
6646        .owned_bytes()
6647        .and_then(|n| {
6648            n.checked_add(
6649                std::mem::size_of::<Mutex<Option<Arc<CachedVocabulary>>>>()
6650                    + 2 * std::mem::size_of::<usize>(),
6651            )
6652        })
6653        .ok_or(LexicalAssemblyError::Store(StoreError::BudgetExceeded {
6654            needed: u64::MAX,
6655            budget: u64::MAX,
6656            component: "cache",
6657        }))?;
6658    work.check_now()?;
6659    Ok((assembly, owned_bytes))
6660}
6661
6662fn map_store_lexical_assembly_error(
6663    error: LexicalAssemblyError,
6664) -> crate::ingest::StoreLexicalError {
6665    match error {
6666        LexicalAssemblyError::Store(error) => QueryError::Store(error).into(),
6667        LexicalAssemblyError::Lexical(error) => {
6668            crate::planner::LexicalFilterError::from(error).into()
6669        }
6670        LexicalAssemblyError::MissingDocumentIdentity(segment_id) => {
6671            crate::ingest::StoreLexicalError::MissingDocumentIdentity { segment_id }
6672        }
6673        LexicalAssemblyError::Cancelled(error) => QueryError::Scan(error).into(),
6674    }
6675}
6676
6677fn map_fusion_lexical_assembly_error(error: LexicalAssemblyError) -> crate::fusion::FusionError {
6678    match error {
6679        LexicalAssemblyError::Store(error) => crate::fusion::FusionError::Leg {
6680            leg: crate::fusion::FusionLeg::Lexical,
6681            kind: crate::fusion::LegFailureKind::Store(error.kind()),
6682            detail: error.to_string(),
6683        },
6684        LexicalAssemblyError::Lexical(error) => crate::fusion::FusionError::Leg {
6685            leg: crate::fusion::FusionLeg::Lexical,
6686            kind: crate::fusion::LegFailureKind::Lexical,
6687            detail: error.to_string(),
6688        },
6689        LexicalAssemblyError::MissingDocumentIdentity(segment_id) => {
6690            crate::fusion::FusionError::Leg {
6691                leg: crate::fusion::FusionLeg::Lexical,
6692                kind: crate::fusion::LegFailureKind::Invariant,
6693                detail: format!("sealed lexical segment {segment_id} has no document identity"),
6694            }
6695        }
6696        LexicalAssemblyError::Cancelled(error) => {
6697            crate::fusion::FusionError::from(QueryError::Scan(error))
6698        }
6699    }
6700}
6701
6702fn accumulate_search_counters(
6703    total: &mut crate::fts::search::SearchCounters,
6704    delta: &crate::fts::search::SearchCounters,
6705) {
6706    total.docs_evaluated = total.docs_evaluated.saturating_add(delta.docs_evaluated);
6707    total.postings_decoded = total
6708        .postings_decoded
6709        .saturating_add(delta.postings_decoded);
6710    total.blocks_decoded = total.blocks_decoded.saturating_add(delta.blocks_decoded);
6711    total.blocks_skipped = total.blocks_skipped.saturating_add(delta.blocks_skipped);
6712}
6713
6714fn map_fusion_controlled_lexical_error(
6715    error: crate::fts::search::ControlledSearchError<crate::scan::ScanError>,
6716) -> crate::fusion::FusionError {
6717    match error {
6718        crate::fts::search::ControlledSearchError::Index(error) => {
6719            crate::fusion::FusionError::Leg {
6720                leg: crate::fusion::FusionLeg::Lexical,
6721                kind: crate::fusion::LegFailureKind::Lexical,
6722                detail: error.to_string(),
6723            }
6724        }
6725        crate::fts::search::ControlledSearchError::Control(error) => {
6726            crate::fusion::FusionError::from(QueryError::Scan(error))
6727        }
6728    }
6729}
6730
6731#[derive(Clone, Copy)]
6732enum PinnedLexicalQuery<'a> {
6733    Term(&'a crate::fts::search::TermQuery),
6734    Structured(&'a crate::fts::query::LexicalQuery),
6735}
6736
6737enum LexicalDocumentError {
6738    SourceOrdinalOutOfRange,
6739    RowOverflow,
6740    SealedSourceAbsent,
6741    Segment(crate::segment::SegmentError),
6742    MissingDocumentIdentity(crate::segment::SegmentId),
6743    ActiveRowAbsent,
6744}
6745
6746fn structured_lexical_document(
6747    snapshot: &PublishedSnapshot,
6748    active: &crate::ingest::ActiveSegment,
6749    sources: &[StructuredLexicalSource],
6750    doc: crate::fts::search::GlobalDocId,
6751    require_document_identity: bool,
6752) -> Result<Option<crate::ingest::DocumentVersion>, LexicalDocumentError> {
6753    let source = usize::try_from(doc.segment)
6754        .ok()
6755        .and_then(|slot| sources.get(slot))
6756        .ok_or(LexicalDocumentError::SourceOrdinalOutOfRange)?;
6757    let row = usize::try_from(doc.row).map_err(|_| LexicalDocumentError::RowOverflow)?;
6758    match source {
6759        StructuredLexicalSource::Sealed(ordinal) => {
6760            let segment = snapshot
6761                .segments()
6762                .get(*ordinal)
6763                .ok_or(LexicalDocumentError::SealedSourceAbsent)?;
6764            let document = segment
6765                .document_version(row)
6766                .map_err(LexicalDocumentError::Segment)?;
6767            if require_document_identity && document.is_none() {
6768                return Err(LexicalDocumentError::MissingDocumentIdentity(
6769                    segment.meta().id,
6770                ));
6771            }
6772            Ok(document)
6773        }
6774        StructuredLexicalSource::Active => {
6775            let document = active.document(row);
6776            if require_document_identity && document.is_none() {
6777                return Err(LexicalDocumentError::ActiveRowAbsent);
6778            }
6779            Ok(document)
6780        }
6781    }
6782}
6783
6784fn map_store_lexical_document_error(
6785    error: LexicalDocumentError,
6786) -> crate::ingest::StoreLexicalError {
6787    match error {
6788        LexicalDocumentError::SourceOrdinalOutOfRange
6789        | LexicalDocumentError::RowOverflow
6790        | LexicalDocumentError::SealedSourceAbsent
6791        | LexicalDocumentError::ActiveRowAbsent => {
6792            QueryError::Store(StoreError::ActiveRowOverflow).into()
6793        }
6794        LexicalDocumentError::Segment(error) => {
6795            QueryError::Store(StoreError::Segment(error)).into()
6796        }
6797        LexicalDocumentError::MissingDocumentIdentity(segment_id) => {
6798            crate::ingest::StoreLexicalError::MissingDocumentIdentity { segment_id }
6799        }
6800    }
6801}
6802
6803fn map_fusion_lexical_document_error(
6804    error: LexicalDocumentError,
6805    invariant_kind: crate::fusion::LegFailureKind,
6806    segment_kind: crate::fusion::LegFailureKind,
6807) -> crate::fusion::FusionError {
6808    let (kind, detail) = match error {
6809        LexicalDocumentError::SourceOrdinalOutOfRange => (
6810            invariant_kind,
6811            "lexical source ordinal is out of range".to_owned(),
6812        ),
6813        LexicalDocumentError::RowOverflow => {
6814            (invariant_kind, "lexical row exceeds usize".to_owned())
6815        }
6816        LexicalDocumentError::SealedSourceAbsent => {
6817            (invariant_kind, "lexical sealed source is absent".to_owned())
6818        }
6819        LexicalDocumentError::Segment(error) => (segment_kind, error.to_string()),
6820        LexicalDocumentError::MissingDocumentIdentity(segment_id) => (
6821            invariant_kind,
6822            format!("sealed lexical segment {segment_id} has no document identity"),
6823        ),
6824        LexicalDocumentError::ActiveRowAbsent => (
6825            invariant_kind,
6826            "lexical active source row is absent".to_owned(),
6827        ),
6828    };
6829    crate::fusion::FusionError::Leg {
6830        leg: crate::fusion::FusionLeg::Lexical,
6831        kind,
6832        detail,
6833    }
6834}
6835
6836fn map_structured_leg_document_error(error: LexicalDocumentError) -> crate::fusion::FusionError {
6837    map_fusion_lexical_document_error(
6838        error,
6839        crate::fusion::LegFailureKind::Lexical,
6840        crate::fusion::LegFailureKind::Lexical,
6841    )
6842}
6843
6844fn map_term_leg_document_error(error: LexicalDocumentError) -> crate::fusion::FusionError {
6845    map_fusion_lexical_document_error(
6846        error,
6847        crate::fusion::LegFailureKind::Invariant,
6848        crate::fusion::LegFailureKind::Segment,
6849    )
6850}
6851
6852fn structured_lexical_row<'a>(
6853    snapshot: &'a PublishedSnapshot,
6854    active: &'a crate::ingest::ActiveSegment,
6855    sources: &[StructuredLexicalSource],
6856    doc: crate::fts::search::GlobalDocId,
6857) -> Result<(&'a str, crate::ingest::DocumentVersion), crate::ingest::StoreLexicalError> {
6858    let source = usize::try_from(doc.segment)
6859        .ok()
6860        .and_then(|slot| sources.get(slot))
6861        .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
6862    let row =
6863        usize::try_from(doc.row).map_err(|_| QueryError::Store(StoreError::ActiveRowOverflow))?;
6864    match source {
6865        StructuredLexicalSource::Sealed(ordinal) => {
6866            let segment = snapshot
6867                .segments()
6868                .get(*ordinal)
6869                .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
6870            let text = segment
6871                .query_stored_text()
6872                .map_err(StoreError::Segment)
6873                .map_err(QueryError::Store)?
6874                .and_then(|rows| rows.row(row).flatten())
6875                .ok_or(crate::ingest::StoreLexicalError::MissingStoredText {
6876                    segment_id: segment.meta().id,
6877                    row: doc.row,
6878                })?;
6879            let document = segment
6880                .document_version(row)
6881                .map_err(StoreError::Segment)
6882                .map_err(QueryError::Store)?
6883                .ok_or(crate::ingest::StoreLexicalError::MissingDocumentIdentity {
6884                    segment_id: segment.meta().id,
6885                })?;
6886            Ok((text, document))
6887        }
6888        StructuredLexicalSource::Active => {
6889            let text = active
6890                .text(row)
6891                .map_err(QueryError::Store)?
6892                .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
6893            let document = active
6894                .document(row)
6895                .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
6896            Ok((text, document))
6897        }
6898    }
6899}
6900
6901type ExactLexicalLeg = (
6902    Vec<hybrid::LexicalHit>,
6903    Arc<LexicalAssembly>,
6904    crate::fts::search::SearchCounters,
6905    bool,
6906);
6907
6908fn resolve_hybrid_leg_results<Vector, Lexical>(
6909    vector: Result<Vector, crate::fusion::FusionError>,
6910    lexical: Result<Lexical, crate::fusion::FusionError>,
6911) -> Result<(Vector, Lexical), crate::fusion::FusionError> {
6912    fn control_priority(error: &crate::fusion::FusionError) -> Option<u8> {
6913        match error {
6914            crate::fusion::FusionError::ReadCancelled { .. } => Some(0),
6915            crate::fusion::FusionError::Timeout { .. } => Some(1),
6916            crate::fusion::FusionError::Cancelled { .. } => Some(2),
6917            _ => None,
6918        }
6919    }
6920
6921    match (vector, lexical) {
6922        (Ok(vector), Ok(lexical)) => Ok((vector, lexical)),
6923        (Err(vector), Ok(_)) => Err(vector),
6924        (Ok(_), Err(lexical)) => Err(lexical),
6925        (Err(vector), Err(lexical)) => {
6926            match (control_priority(&vector), control_priority(&lexical)) {
6927                (Some(vector_priority), Some(lexical_priority)) => {
6928                    if vector_priority <= lexical_priority {
6929                        Err(vector)
6930                    } else {
6931                        Err(lexical)
6932                    }
6933                }
6934                (Some(_), None) => Err(vector),
6935                (None, Some(_)) => Err(lexical),
6936                (None, None) => Err(vector),
6937            }
6938        }
6939    }
6940}
6941
6942fn exact_structured_lexical_leg(
6943    inputs: LexicalInputs<'_>,
6944    _analyzer: &crate::fts::tokenizer::Analyzer,
6945    query: &crate::fts::query::LexicalQuery,
6946    bound: usize,
6947    cancellation: &QueryCancellation<'_>,
6948    preparation: &mut prepared_lexical::PreparedLexicalQuery<'_>,
6949) -> Result<ExactLexicalLeg, crate::fusion::FusionError> {
6950    let (snapshot, active) = (inputs.snapshot, inputs.active);
6951    let lexical_error = |detail: String| crate::fusion::FusionError::Leg {
6952        leg: crate::fusion::FusionLeg::Lexical,
6953        kind: crate::fusion::LegFailureKind::Lexical,
6954        detail,
6955    };
6956    let (prepared, cache_hit) = preparation.prepare_structured(inputs, cancellation)?;
6957    let assembly = &prepared.assembly;
6958    let index = &assembly.index;
6959    let sources = &assembly.sources;
6960    let Some(query_scoring) = prepared.query.as_ref() else {
6961        return Ok((
6962            Vec::new(),
6963            Arc::clone(assembly),
6964            crate::fts::search::SearchCounters::default(),
6965            cache_hit,
6966        ));
6967    };
6968    let allow_lists = assembly
6969        .alive_sets
6970        .iter()
6971        .map(|alive| alive.alive_bitmap())
6972        .collect::<Vec<_>>();
6973    let mut scratch_memory =
6974        stats::AccountedCounter::new(inputs.accounting, stats::AllocationComponent::Temporary)
6975            .map_err(QueryError::Store)?;
6976    let mut phrase = prepared_lexical::PhraseEligibility::new(
6977        index,
6978        Some(query),
6979        inputs.accounting,
6980        cancellation,
6981    )?;
6982    let result = crate::fts::prune::weighted::search(
6983        index,
6984        query_scoring,
6985        bound,
6986        &allow_lists,
6987        || {
6988            cancellation
6989                .check_graph()
6990                .map_err(QueryError::Scan)
6991                .map_err(crate::fusion::FusionError::from)
6992        },
6993        |doc| {
6994            phrase
6995                .matches(doc, cancellation)
6996                .map_err(crate::fusion::FusionError::from)
6997        },
6998        |bytes| {
6999            prepared_lexical::reserve_weighted_scratch(&mut scratch_memory, bytes)
7000                .map_err(crate::fusion::FusionError::from)
7001        },
7002    )
7003    .map_err(|error| match error {
7004        crate::fts::search::ControlledSearchError::Index(error) => lexical_error(error.to_string()),
7005        crate::fts::search::ControlledSearchError::Control(error) => error,
7006    })?;
7007    let counters = result.counters;
7008    let scored = result.hits;
7009    let mut joined = Vec::with_capacity(scored.len());
7010    for (doc, score) in scored {
7011        let document = structured_lexical_document(snapshot, active, sources, doc, false)
7012            .map_err(map_structured_leg_document_error)?;
7013        joined.push(hybrid::LexicalHit {
7014            doc,
7015            document,
7016            bm25: score,
7017        });
7018    }
7019    Ok((joined, Arc::clone(assembly), counters, cache_hit))
7020}
7021
7022fn exact_lexical_leg(
7023    inputs: LexicalInputs<'_>,
7024    bound: usize,
7025    cancellation: &QueryCancellation<'_>,
7026    preparation: &mut prepared_lexical::PreparedLexicalQuery<'_>,
7027) -> Result<ExactLexicalLeg, crate::fusion::FusionError> {
7028    let (snapshot, active) = (inputs.snapshot, inputs.active);
7029    let (prepared, cache_hit) = preparation.prepare_term(inputs, cancellation)?;
7030    let assembly = &prepared.assembly;
7031    let index = &assembly.index;
7032    let sources = &assembly.sources;
7033    if index.total_tokens() == 0 {
7034        return Ok((
7035            Vec::new(),
7036            Arc::clone(assembly),
7037            crate::fts::search::SearchCounters::default(),
7038            cache_hit,
7039        ));
7040    }
7041    let k = bound.min(usize::try_from(index.document_count()).unwrap_or(usize::MAX));
7042    let allow_lists = assembly
7043        .alive_sets
7044        .iter()
7045        .map(|alive| alive.alive_bitmap())
7046        .collect::<Vec<_>>();
7047    let result =
7048        exact_hybrid_lexical_search(index, prepared.scoring()?, k, &allow_lists, cancellation)?;
7049    let mut joined = Vec::with_capacity(result.hits.len());
7050    for hit in result.hits {
7051        let document = structured_lexical_document(snapshot, active, sources, hit.doc, false)
7052            .map_err(map_term_leg_document_error)?;
7053        joined.push(hybrid::LexicalHit {
7054            doc: hit.doc,
7055            document,
7056            bm25: hit.score,
7057        });
7058    }
7059    Ok((joined, Arc::clone(assembly), result.counters, cache_hit))
7060}
7061
7062fn exact_hybrid_lexical_search(
7063    index: &crate::fts::index::LexicalIndex,
7064    prepared: &crate::fts::search::PreparedTermQuery,
7065    k: usize,
7066    allow_lists: &[&crate::meta::DocBitmap],
7067    cancellation: &QueryCancellation<'_>,
7068) -> Result<crate::fts::search::SearchResult, crate::fusion::FusionError> {
7069    let allowed = allow_lists
7070        .iter()
7071        .map(|allow_list| allow_list.cardinality())
7072        .fold(0_u64, u64::saturating_add);
7073    if allowed.saturating_mul(crate::planner::LEXICAL_ALLOW_LIST_DIVISOR) <= index.document_count()
7074    {
7075        return crate::fts::search::search_allow_list_prepared_controlled(
7076            index,
7077            prepared,
7078            k,
7079            allow_lists,
7080            || cancellation.check_graph(),
7081        )
7082        .map_err(map_fusion_controlled_lexical_error);
7083    }
7084    crate::fts::prune::search_pruned_prepared_controlled(index, prepared, k, allow_lists, || {
7085        cancellation.check_graph()
7086    })
7087    .map_err(map_fusion_controlled_lexical_error)
7088}
7089
7090#[allow(clippy::too_many_arguments)]
7091fn search_pinned(
7092    pool: Option<&pool::QueryPool>,
7093    snapshot: &Arc<PublishedSnapshot>,
7094    active: &crate::ingest::ActiveSegment,
7095    accounting: &Arc<stats::Accounting>,
7096    generation: u64,
7097    epoch: Option<crate::epoch::EpochIdentity>,
7098    request: crate::ingest::SearchRequest<'_>,
7099    k: usize,
7100    options: SearchOptions,
7101    control: QueryControl,
7102    graph_bound_mode: GraphBoundMode,
7103    requested_tier: Option<SearchTier>,
7104    hybrid_scan_reason: Option<crate::planner::ScanReason>,
7105    started: std::time::Instant,
7106    clock: &dyn MonotonicClock,
7107    vector_preparation: &mut prepared::PreparedVectorQuery<'_>,
7108    #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
7109        &crate::scan::vector_fault::VectorFaultController,
7110    >,
7111) -> Result<crate::ingest::SearchOutcome, QueryError> {
7112    use crate::ingest::{GraphSearchStats, RowSource, SearchOutcome};
7113    use crate::quant::prepare_bit4_query;
7114    use crate::scan::{ScanQuery, ScanRequest, ScanRows, ScanStats};
7115
7116    let vector_started = timing_start(clock);
7117    let scan_options = options.scan();
7118    let rescore_options = options.scan_rescore();
7119    let mut rescore_counters = crate::diag::ScanRescoreCounters::default();
7120    if let Some(options) = rescore_options {
7121        options.frontier(k, 0)?;
7122    }
7123    let query = request.vector();
7124    prepared::validate_vector_coordinates(query, crate::kernels::MAX_DOT_I8_DIMENSION, |_| {
7125        Ok::<(), std::convert::Infallible>(())
7126    })
7127    .map_err(|error| match error {
7128        prepared::VectorValidationError::Data(error) => {
7129            QueryError::Scan(crate::scan::ScanError::Quant(error))
7130        }
7131        prepared::VectorValidationError::Control(never) => match never {},
7132    })?;
7133    vector_preparation.validate_binding(query, epoch)?;
7134    let bit4_query = std::cell::OnceCell::new();
7135    let int8_query = std::cell::OnceCell::new();
7136    let mut candidates = Vec::new();
7137    let mut dims_touched = 0_u64;
7138    let mut bytes_read = 0_u64;
7139    let mut worst_squared_l2: Option<f64> = None;
7140    let mut worst_exhaustive = true;
7141    let mut worker_thread_ids = Vec::new();
7142    let mut graph_stats = GraphSearchStats::default();
7143    let mut plans = Vec::new();
7144    let auto_graph_options = if matches!(options.tier(), SearchTier::Auto)
7145        && snapshot.segments().iter().any(|segment| {
7146            segment
7147                .directory()
7148                .iter()
7149                .any(|entry| entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id())
7150        }) {
7151        Some(auto_graph_search_options(snapshot)?)
7152    } else {
7153        None
7154    };
7155
7156    if matches!(options.tier(), SearchTier::Graph(_)) {
7157        for segment in snapshot.segments() {
7158            if !segment
7159                .directory()
7160                .iter()
7161                .any(|entry| entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id())
7162            {
7163                return Err(QueryError::Store(StoreError::GraphUnavailable {
7164                    segment_id: segment.meta().id,
7165                }));
7166            }
7167        }
7168    }
7169    let full_precision = matches!(options.tier(), SearchTier::Exact | SearchTier::Graph(_))
7170        || (matches!(options.tier(), SearchTier::Auto)
7171            && auto_uses_full_precision(snapshot, active));
7172
7173    // Reuse one bounded worker lane across this pinned query's segments.
7174    let exact_lane = if full_precision {
7175        pool.map_or(0, |pool| {
7176            pool.reserve_exact_lane(scan_options.thread_budget)
7177        })
7178    } else {
7179        0
7180    };
7181
7182    if !active.is_empty() {
7183        let mut alive = active.alive().map_err(QueryError::Store)?;
7184        if let Some(filter) = request.filter {
7185            alive.retain(&filter.active_rows(active)?);
7186        }
7187        let mut exact_score = full_precision;
7188        let mut approximate = false;
7189        let frontier = rescore_options
7190            .map(|options| options.frontier(k, alive.live_count()))
7191            .transpose()?
7192            .unwrap_or(k);
7193        let mut exact_memory = ExactScanMemory::new(accounting)?;
7194        let mut outcome = match options.tier() {
7195            SearchTier::Auto if full_precision => {
7196                let lease = SnapshotLease::new_at(Arc::clone(snapshot), generation);
7197                scan_active_squared_l2(
7198                    active,
7199                    &alive,
7200                    request.vector(),
7201                    frontier,
7202                    ExactScanDispatch {
7203                        pool,
7204                        accounting,
7205                        control: &control,
7206                        lease: &lease,
7207                        thread_budget: scan_options.thread_budget,
7208                        lane: exact_lane,
7209                    },
7210                    &mut exact_memory,
7211                    #[cfg(any(test, feature = "test-seams"))]
7212                    vector_fault_controller,
7213                    #[cfg(any(test, feature = "test-seams"))]
7214                    vector_fault_tier(options.tier()),
7215                )?
7216            }
7217            SearchTier::Auto | SearchTier::Scan => {
7218                let query_pool = pool.ok_or(QueryError::Store(StoreError::Synchronization {
7219                    component: "active scan-tier query pool",
7220                }))?;
7221                let bit4_query = bit4_query
7222                    .get_or_init(|| prepare_bit4_query(request.vector(), 0))
7223                    .as_ref()
7224                    .map_err(|error| crate::scan::ScanError::Quant(error.clone()))
7225                    .map_err(QueryError::Scan)?;
7226                execute_store_scan(
7227                    query_pool,
7228                    ScanRequest {
7229                        query: ScanQuery::Bit4(bit4_query),
7230                        rows: ScanRows::Bit4RowMajor {
7231                            codes: active.codes(),
7232                            factors: active.factors(),
7233                        },
7234                        row_mask: Some(alive.scan_mask()),
7235                    },
7236                    frontier,
7237                    scan_options,
7238                    control.clone(),
7239                    SnapshotLease::new_at(Arc::clone(snapshot), generation),
7240                    #[cfg(any(test, feature = "test-seams"))]
7241                    vector_fault_controller,
7242                    #[cfg(any(test, feature = "test-seams"))]
7243                    crate::scan::vector_fault::VectorRowSource::Active,
7244                    #[cfg(any(test, feature = "test-seams"))]
7245                    vector_fault_tier(options.tier()),
7246                )?
7247            }
7248            SearchTier::Exact | SearchTier::Graph(_) => {
7249                let lease = SnapshotLease::new_at(Arc::clone(snapshot), generation);
7250                scan_active_squared_l2(
7251                    active,
7252                    &alive,
7253                    request.vector(),
7254                    frontier,
7255                    ExactScanDispatch {
7256                        pool,
7257                        accounting,
7258                        control: &control,
7259                        lease: &lease,
7260                        thread_budget: scan_options.thread_budget,
7261                        lane: exact_lane,
7262                    },
7263                    &mut exact_memory,
7264                    #[cfg(any(test, feature = "test-seams"))]
7265                    vector_fault_controller,
7266                    #[cfg(any(test, feature = "test-seams"))]
7267                    vector_fault_tier(options.tier()),
7268                )?
7269            }
7270        };
7271        if let Some(rescore) = rescore_options {
7272            let lease = SnapshotLease::new_at(Arc::clone(snapshot), generation);
7273            let cancellation = QueryCancellation::new(&control, &lease);
7274            let (counters, exhaustive, worst) = rescored_scan::rescore(
7275                &mut outcome.candidates,
7276                &mut outcome.stats,
7277                active.vectors(),
7278                query,
7279                k,
7280                alive.live_count(),
7281                rescore,
7282                &cancellation,
7283                accounting,
7284                &mut exact_memory,
7285            )?;
7286            outcome.worst_score = worst;
7287            exact_score = true;
7288            approximate = !exhaustive;
7289            rescored_scan::accumulate(&mut rescore_counters, counters)?;
7290        }
7291        fold_worst_squared_l2(&mut worst_squared_l2, &mut worst_exhaustive, &outcome);
7292        merge_store_outcome(
7293            outcome,
7294            RowSource::Active,
7295            |row| Ok(active.document(row)),
7296            &mut candidates,
7297            &mut dims_touched,
7298            &mut bytes_read,
7299            &mut worker_thread_ids,
7300            exact_score,
7301            #[cfg(any(test, feature = "test-seams"))]
7302            vector_fault_controller,
7303            #[cfg(any(test, feature = "test-seams"))]
7304            vector_fault_tier(options.tier()),
7305        )?;
7306        let mut plan = crate::planner::SegmentPlan::unfiltered_scan(
7307            RowSource::Active,
7308            crate::planner::SegmentTier::ActiveScan,
7309            alive.live_count(),
7310            crate::planner::ScanReason::ActiveSegment,
7311        );
7312        plan.approximate = approximate;
7313        plans.push(plan);
7314        if matches!(graph_bound_mode, GraphBoundMode::Shared) {
7315            retain_global_top_k(&mut candidates, k);
7316        }
7317    }
7318
7319    let mut ordered_segments = snapshot.segments().iter().collect::<Vec<_>>();
7320    if matches!(graph_bound_mode, GraphBoundMode::Shared) {
7321        // Larger immutable segments have more opportunities to supply the
7322        // first competitive top-k. Row count is already in the manifest, so
7323        // this ordering tightens the bound without query-time artifact I/O.
7324        ordered_segments.sort_unstable_by(|left, right| {
7325            right
7326                .meta()
7327                .row_count
7328                .cmp(&left.meta().row_count)
7329                .then_with(|| left.meta().id.cmp(&right.meta().id))
7330        });
7331    }
7332
7333    for segment in ordered_segments {
7334        let original_alive = segment.query_alive().map_err(QueryError::Store)?;
7335        let mut filtered_alive;
7336        let alive = if let Some(filter) = request.filter {
7337            filtered_alive = (*original_alive).clone();
7338            filtered_alive.retain(&filter.sealed_rows(segment)?);
7339            &filtered_alive
7340        } else {
7341            original_alive.as_ref()
7342        };
7343        let source = RowSource::Sealed(segment.meta().id);
7344        // Automatic tiering follows the artifact that is atomically published
7345        // now, not the tier policy's desired future state. A due-but-unbuilt
7346        // graph is therefore scanned silently and correctly: that is the
7347        // adaptive ladder working, not a fallback hiding a broken contract.
7348        let graph_options = match options.tier() {
7349            SearchTier::Graph(graph_options) => Some(graph_options),
7350            SearchTier::Auto
7351                if segment.directory().iter().any(|entry| {
7352                    entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id()
7353                }) =>
7354            {
7355                auto_graph_options
7356            }
7357            SearchTier::Auto | SearchTier::Exact | SearchTier::Scan => None,
7358        };
7359        if let Some(graph_options) = graph_options {
7360            worst_exhaustive = false;
7361            let competitive_distance = global_competitive_distance(&candidates, k);
7362            let MergedSegmentGraph {
7363                plan,
7364                traversed,
7365                _result,
7366                _scratch,
7367                _lease,
7368            } = traverse_segment_graph(
7369                segment,
7370                alive,
7371                source,
7372                graph_options,
7373                graph_bound_mode,
7374                request,
7375                k,
7376                vector_preparation,
7377                accounting,
7378                snapshot,
7379                generation,
7380                &control,
7381                competitive_distance,
7382                #[cfg(any(test, feature = "test-seams"))]
7383                vector_fault_controller,
7384                options.tier(),
7385            )?
7386            .merge_into(
7387                segment,
7388                alive,
7389                source,
7390                &mut candidates,
7391                &mut dims_touched,
7392                &mut bytes_read,
7393                &mut worker_thread_ids,
7394                &mut graph_stats,
7395                #[cfg(any(test, feature = "test-seams"))]
7396                vector_fault_controller,
7397                options.tier(),
7398            )?;
7399            plans.push(plan);
7400            if traversed && matches!(graph_bound_mode, GraphBoundMode::Shared) {
7401                retain_global_top_k(&mut candidates, k);
7402            }
7403            continue;
7404        }
7405
7406        let mut exact_memory = ExactScanMemory::new(accounting)?;
7407        let frontier = rescore_options
7408            .map(|options| options.frontier(k, alive.live_count()))
7409            .transpose()?
7410            .unwrap_or(k);
7411        let mut outcome = scan_sealed_segment(
7412            pool,
7413            snapshot,
7414            segment,
7415            alive,
7416            generation,
7417            request,
7418            frontier,
7419            scan_options,
7420            &control,
7421            &bit4_query,
7422            &int8_query,
7423            full_precision,
7424            exact_lane,
7425            accounting,
7426            &mut exact_memory,
7427            source,
7428            options.tier(),
7429            #[cfg(any(test, feature = "test-seams"))]
7430            vector_fault_controller,
7431        )?;
7432        let mut exact_score = full_precision || segment.meta().scheme == 0;
7433        let mut approximate = false;
7434        if let Some(rescore) = rescore_options {
7435            let lease = SnapshotLease::new_at(Arc::clone(snapshot), generation);
7436            let cancellation = QueryCancellation::new(&control, &lease);
7437            let vectors = exact_rescore_rows_for_search(
7438                segment,
7439                #[cfg(any(test, feature = "test-seams"))]
7440                vector_fault_controller,
7441                #[cfg(any(test, feature = "test-seams"))]
7442                vector_fault_tier(options.tier()),
7443            )?;
7444            let (counters, exhaustive, worst) = rescored_scan::rescore(
7445                &mut outcome.candidates,
7446                &mut outcome.stats,
7447                vectors,
7448                query,
7449                k,
7450                alive.live_count(),
7451                rescore,
7452                &cancellation,
7453                accounting,
7454                &mut exact_memory,
7455            )?;
7456            outcome.worst_score = worst;
7457            exact_score = true;
7458            approximate = !exhaustive;
7459            rescored_scan::accumulate(&mut rescore_counters, counters)?;
7460        }
7461        fold_worst_squared_l2(&mut worst_squared_l2, &mut worst_exhaustive, &outcome);
7462        merge_store_outcome(
7463            outcome,
7464            source,
7465            |row| {
7466                segment
7467                    .document_version(row)
7468                    .map_err(StoreError::Segment)
7469                    .map_err(QueryError::Store)
7470            },
7471            &mut candidates,
7472            &mut dims_touched,
7473            &mut bytes_read,
7474            &mut worker_thread_ids,
7475            exact_score,
7476            #[cfg(any(test, feature = "test-seams"))]
7477            vector_fault_controller,
7478            #[cfg(any(test, feature = "test-seams"))]
7479            vector_fault_tier(options.tier()),
7480        )?;
7481        let tier = if segment
7482            .directory()
7483            .iter()
7484            .any(|entry| entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id())
7485        {
7486            crate::planner::SegmentTier::SealedGraph
7487        } else {
7488            crate::planner::SegmentTier::SealedScan
7489        };
7490        let mut plan = crate::planner::SegmentPlan::unfiltered_scan(
7491            source,
7492            tier,
7493            alive.live_count(),
7494            sealed_scan_reason(segment, requested_tier, hybrid_scan_reason),
7495        );
7496        plan.approximate = approximate;
7497        plans.push(plan);
7498        if matches!(graph_bound_mode, GraphBoundMode::Shared) {
7499            retain_global_top_k(&mut candidates, k);
7500        }
7501    }
7502
7503    retain_global_top_k(&mut candidates, k);
7504    let stats = ScanStats {
7505        dims_touched,
7506        bytes_read,
7507        threads_used: worker_thread_ids.len(),
7508        worker_thread_ids,
7509    };
7510    accounting.record_plans(&plans).map_err(QueryError::Store)?;
7511    // Ceiling construction is part of the vector query, including the first
7512    // immutable norm-enclosure pass. Finalize elapsed only after it completes.
7513    let vector_ceiling = Some(exact_vector_ceiling(snapshot, active, query)?);
7514    let mut diagnostics = crate::diag::QueryDiagnostics::vector(crate::diag::VectorDiagnostics {
7515        snapshot_generation: generation,
7516        indexed_through_seq: active
7517            .indexed_through_seq()
7518            .max(crate::wal::LogSeq::new(snapshot.absorbed_through())),
7519        approximate: plans.iter().any(|plan| plan.approximate),
7520        exact_rescore: candidates.iter().all(|candidate| candidate.exact_score()),
7521        requested_k: k,
7522        returned: candidates.len(),
7523        budget_exhausted: false,
7524        plan: plans,
7525        scan: stats.clone(),
7526        graph: graph_stats,
7527        epoch,
7528        elapsed: clock.now().saturating_duration_since(started),
7529    });
7530    diagnostics.scan_rescore = rescore_options.map(|_| rescore_counters);
7531    diagnostics.timings = crate::diag::QUERY_TIMING_ENABLED.then(|| crate::diag::QueryTimings {
7532        vector: timing_elapsed(clock, vector_started),
7533        ..Default::default()
7534    });
7535    Ok(SearchOutcome {
7536        candidates,
7537        // Every tier reports the same deterministic ceiling. A graph
7538        // traversal cannot name the farthest alive row, and letting the
7539        // anchor depend on the tier would let the tier change ranking,
7540        // so the ceiling is the anchor everywhere.
7541        vector_ceiling,
7542        stats,
7543        graph_stats,
7544        generation,
7545        epoch,
7546        diagnostics,
7547    })
7548}
7549
7550/// Reports whether any sealed segment has a published graph region.
7551///
7552/// Only a published artifact counts. A graph that policy says is due but
7553/// that maintenance has not built yet is not usable by a query.
7554/// Widening rounds visit `ef` rows per segment, and `ef` grows with the
7555/// requested `k`. Past this share of the corpus a traversal reads most of
7556/// the segment anyway, so the round is cheaper and exact as a scan.
7557const GRAPH_WIDENING_CAP_NUMERATOR: usize = 1;
7558/// Denominator of [`GRAPH_WIDENING_CAP_NUMERATOR`]; the cap is 25%.
7559const GRAPH_WIDENING_CAP_DENOMINATOR: usize = 4;
7560
7561/// Reports whether a widening round of width `k` should still use the graph.
7562///
7563/// `ef` follows `graph::search`: at least the SIFT floor, and otherwise a
7564/// small multiple of `k`. The angular multiple is used so the cap is
7565/// evaluated against the widest pool any profile would request, which
7566/// keeps the decision independent of the distance metric in play.
7567fn graph_round_is_worth_it(k: usize, corpus_rows: usize) -> bool {
7568    if corpus_rows == 0 {
7569        return true;
7570    }
7571    let ef = graph_round_ef(k);
7572    let cap =
7573        corpus_rows.saturating_mul(GRAPH_WIDENING_CAP_NUMERATOR) / GRAPH_WIDENING_CAP_DENOMINATOR;
7574    ef <= cap
7575}
7576
7577fn graph_round_ef(k: usize) -> usize {
7578    k.saturating_mul(4).max(140)
7579}
7580
7581fn sealed_scan_reason(
7582    segment: &crate::segment::reader::SegmentReader,
7583    requested_tier: Option<SearchTier>,
7584    hybrid_scan_reason: Option<crate::planner::ScanReason>,
7585) -> crate::planner::ScanReason {
7586    use crate::planner::{ExplicitScanTier, ScanReason};
7587
7588    let has_graph = segment
7589        .directory()
7590        .iter()
7591        .any(|entry| entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id());
7592    if has_graph && let Some(reason) = hybrid_scan_reason {
7593        return reason;
7594    }
7595    match requested_tier {
7596        Some(SearchTier::Exact) => return ScanReason::ExplicitTier(ExplicitScanTier::Exact),
7597        Some(SearchTier::Scan) => return ScanReason::ExplicitTier(ExplicitScanTier::Scan),
7598        Some(SearchTier::Auto | SearchTier::Graph(_)) | None => {}
7599    }
7600    let rows = segment.meta().row_count;
7601    let min_rows = crate::tier::thresholds::for_bucket(segment.meta().dims, segment.meta().scheme)
7602        .graph_min_rows;
7603    if rows < min_rows {
7604        ScanReason::BelowGraphThreshold { rows, min_rows }
7605    } else {
7606        ScanReason::GraphPending
7607    }
7608}
7609
7610fn snapshot_has_graph(snapshot: &PublishedSnapshot) -> bool {
7611    snapshot.segments().iter().any(|segment| {
7612        segment
7613            .directory()
7614            .iter()
7615            .any(|entry| entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id())
7616    })
7617}
7618
7619fn exact_vector_ceiling(
7620    snapshot: &PublishedSnapshot,
7621    active: &crate::ingest::ActiveSegment,
7622    query: &[f32],
7623) -> Result<f64, QueryError> {
7624    fold_exact_vector_ceiling(
7625        snapshot,
7626        active,
7627        query,
7628        remembered_segment_vector_ceiling_norm_range,
7629    )
7630}
7631
7632/// Returns a sealed segment's norm enclosure, walking its region only once.
7633///
7634/// The enclosure is the query-independent half of the ceiling: it depends on
7635/// the segment's immutable factor or rescore bytes alone. Every tier and every
7636/// hybrid widening round would otherwise rewalk every factor record of every
7637/// segment to reach the same two numbers.
7638fn remembered_segment_vector_ceiling_norm_range(
7639    segment: &crate::segment::reader::SegmentReader,
7640) -> Result<crate::graph::search::GraphSegmentNormRange, QueryError> {
7641    if let Some(range) = segment.cached_vector_ceiling_norm_range() {
7642        return Ok(range);
7643    }
7644    let range = compute_segment_vector_ceiling_norm_range(segment)?;
7645    segment.remember_vector_ceiling_norm_range(range);
7646    Ok(range)
7647}
7648
7649/// Folds every segment's norm enclosure into the query-wide ceiling.
7650///
7651/// `segment_range` supplies one sealed segment's query-independent enclosure.
7652/// Production passes the remembering source; the differential test passes the
7653/// recomputing one, so both walk identical fold arithmetic.
7654fn fold_exact_vector_ceiling(
7655    snapshot: &PublishedSnapshot,
7656    active: &crate::ingest::ActiveSegment,
7657    query: &[f32],
7658    mut segment_range: impl FnMut(
7659        &crate::segment::reader::SegmentReader,
7660    )
7661        -> Result<crate::graph::search::GraphSegmentNormRange, QueryError>,
7662) -> Result<f64, QueryError> {
7663    let mut ceiling = 0.0_f64;
7664    if !active.is_empty() {
7665        // The active segment is the one mutable row source, so its enclosure
7666        // is recomputed on every query and is never remembered.
7667        ceiling = fold_vector_ceiling(
7668            ceiling,
7669            crate::graph::search::GraphSegmentNormRange::from_factors(active.factors()),
7670            query,
7671        )?;
7672    }
7673    for segment in snapshot.segments() {
7674        let range = segment_range(segment)?;
7675        ceiling = fold_vector_ceiling(ceiling, range, query)?;
7676    }
7677    Ok(ceiling)
7678}
7679
7680/// Walks one sealed segment's factor or rescore region for its norm enclosure.
7681fn compute_segment_vector_ceiling_norm_range(
7682    segment: &crate::segment::reader::SegmentReader,
7683) -> Result<crate::graph::search::GraphSegmentNormRange, QueryError> {
7684    match segment.meta().scheme {
7685        4 => Ok(crate::graph::search::GraphSegmentNormRange::from_factors(
7686            segment
7687                .query_bit4_factors()
7688                .map_err(StoreError::Segment)
7689                .map_err(QueryError::Store)?,
7690        )),
7691        0 | 2 => Ok(
7692            crate::graph::search::GraphSegmentNormRange::from_exact_rows(
7693                segment
7694                    .query_rescore_f32()
7695                    .map_err(StoreError::Segment)
7696                    .map_err(QueryError::Store)?,
7697                segment.meta().dims as usize,
7698            ),
7699        ),
7700        scheme => Err(QueryError::Store(StoreError::Segment(
7701            crate::segment::SegmentError::Geometry(format!(
7702                "vector norm enclosure does not support scheme {scheme}"
7703            )),
7704        ))),
7705    }
7706}
7707
7708fn fold_vector_ceiling(
7709    current: f64,
7710    range: crate::graph::search::GraphSegmentNormRange,
7711    query: &[f32],
7712) -> Result<f64, QueryError> {
7713    let ceiling = range.squared_l2_upper_bound(query);
7714    if !ceiling.is_finite() {
7715        return Err(QueryError::Store(StoreError::Segment(
7716            crate::segment::SegmentError::Geometry(
7717                "vector norm enclosure did not produce a finite squared-L2 ceiling".to_owned(),
7718            ),
7719        )));
7720    }
7721    Ok(current.max(ceiling))
7722}
7723
7724/// Folds one segment's exhaustive worst score into the query-wide farthest
7725/// distance. A scan that returned candidates without a worst score did not
7726/// visit every allowed row, so the query-wide anchor is not available.
7727fn fold_worst_squared_l2(
7728    worst: &mut Option<f64>,
7729    exhaustive: &mut bool,
7730    outcome: &crate::scan::ScanOutcome,
7731) {
7732    match outcome.worst_score {
7733        Some(score) => {
7734            let squared_l2 = -f64::from(score);
7735            *worst = Some(worst.map_or(squared_l2, |current: f64| current.max(squared_l2)));
7736        }
7737        None => {
7738            if !outcome.candidates.is_empty() {
7739                *exhaustive = false;
7740            }
7741        }
7742    }
7743}
7744
7745// Boxing the traversed variant would add an allocation to every graph segment.
7746#[allow(clippy::large_enum_variant)]
7747enum SegmentGraphResult<'a> {
7748    Pruned {
7749        plan: crate::planner::SegmentPlan,
7750        graph_stats: crate::ingest::GraphSearchStats,
7751        _lease: SnapshotLease,
7752    },
7753    Traversed {
7754        result: crate::graph::search::GraphSearchResult,
7755        plan: crate::planner::SegmentPlan,
7756        dims_touched: u64,
7757        bytes_read: u64,
7758        caller_thread: std::thread::ThreadId,
7759        graph_stats: crate::ingest::GraphSearchStats,
7760        _scratch: graph_cache::GraphScratchLease<'a>,
7761        _lease: SnapshotLease,
7762    },
7763}
7764
7765struct MergedSegmentGraph<'a> {
7766    plan: crate::planner::SegmentPlan,
7767    traversed: bool,
7768    _result: Option<crate::graph::search::GraphSearchResult>,
7769    _scratch: Option<graph_cache::GraphScratchLease<'a>>,
7770    _lease: SnapshotLease,
7771}
7772
7773impl<'a> SegmentGraphResult<'a> {
7774    #[allow(clippy::too_many_arguments)]
7775    fn merge_into(
7776        self,
7777        segment: &crate::segment::reader::SegmentReader,
7778        alive: &crate::meta::AliveSet,
7779        source: crate::ingest::RowSource,
7780        candidates: &mut Vec<crate::ingest::SearchCandidate>,
7781        dims_touched: &mut u64,
7782        bytes_read: &mut u64,
7783        worker_thread_ids: &mut Vec<std::thread::ThreadId>,
7784        graph_stats: &mut crate::ingest::GraphSearchStats,
7785        #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
7786            &crate::scan::vector_fault::VectorFaultController,
7787        >,
7788        _tier: SearchTier,
7789    ) -> Result<MergedSegmentGraph<'a>, QueryError> {
7790        match self {
7791            Self::Pruned {
7792                plan,
7793                graph_stats: delta,
7794                _lease,
7795            } => {
7796                add_traversal_stats(graph_stats, delta)?;
7797                Ok(MergedSegmentGraph {
7798                    plan,
7799                    traversed: false,
7800                    _result: None,
7801                    _scratch: None,
7802                    _lease,
7803                })
7804            }
7805            Self::Traversed {
7806                result,
7807                plan,
7808                dims_touched: traversal_dims_touched,
7809                bytes_read: traversal_bytes_read,
7810                caller_thread,
7811                graph_stats: delta,
7812                _scratch,
7813                _lease,
7814            } => {
7815                *dims_touched = dims_touched
7816                    .checked_add(traversal_dims_touched)
7817                    .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
7818                *bytes_read = bytes_read
7819                    .checked_add(traversal_bytes_read)
7820                    .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
7821                if !worker_thread_ids.contains(&caller_thread) {
7822                    worker_thread_ids.push(caller_thread);
7823                }
7824                add_traversal_stats(graph_stats, delta)?;
7825                reserve_global_candidates(
7826                    candidates,
7827                    result
7828                        .candidates()
7829                        .iter()
7830                        .filter(|candidate| alive.is_alive(candidate.row_id()))
7831                        .count(),
7832                    #[cfg(any(test, feature = "test-seams"))]
7833                    vector_fault_controller,
7834                    #[cfg(any(test, feature = "test-seams"))]
7835                    vector_fault_tier(_tier),
7836                )?;
7837                for candidate in result.candidates() {
7838                    if !alive.is_alive(candidate.row_id()) {
7839                        continue;
7840                    }
7841                    let score = -(candidate.distance() as f32);
7842                    if !score.is_finite() {
7843                        return Err(QueryError::Graph(
7844                            crate::graph::search::GraphSearchError::Geometry(format!(
7845                                "graph result distance for row {} is not representable as f32",
7846                                candidate.row_id()
7847                            )),
7848                        ));
7849                    }
7850                    candidates.push(crate::ingest::SearchCandidate::new(
7851                        crate::ingest::GlobalRowId::new(source, candidate.row_id()),
7852                        segment
7853                            .document_version(candidate.row_id() as usize)
7854                            .map_err(StoreError::Segment)
7855                            .map_err(QueryError::Store)?,
7856                        score,
7857                        true,
7858                    ));
7859                }
7860                Ok(MergedSegmentGraph {
7861                    plan,
7862                    traversed: true,
7863                    _result: Some(result),
7864                    _scratch: Some(_scratch),
7865                    _lease,
7866                })
7867            }
7868        }
7869    }
7870}
7871
7872#[allow(clippy::too_many_arguments)]
7873fn traverse_segment_graph<'a>(
7874    segment: &'a crate::segment::reader::SegmentReader,
7875    alive: &crate::meta::AliveSet,
7876    source: crate::ingest::RowSource,
7877    graph_options: GraphSearchOptions,
7878    graph_bound_mode: GraphBoundMode,
7879    request: crate::ingest::SearchRequest<'_>,
7880    k: usize,
7881    vector_preparation: &mut prepared::PreparedVectorQuery<'_>,
7882    accounting: &Arc<stats::Accounting>,
7883    snapshot: &Arc<PublishedSnapshot>,
7884    generation: u64,
7885    control: &QueryControl,
7886    competitive_distance: Option<f32>,
7887    #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
7888        &crate::scan::vector_fault::VectorFaultController,
7889    >,
7890    _tier: SearchTier,
7891) -> Result<SegmentGraphResult<'a>, QueryError> {
7892    let lease = SnapshotLease::new_at(Arc::clone(snapshot), generation);
7893    let cancellation = QueryCancellation::new(control, &lease);
7894    cancellation.check_graph().map_err(map_scan_error)?;
7895    let (graph, graph_validated, prepared_entry_seed_discovered, norm_range) =
7896        match graph_bound_mode {
7897            GraphBoundMode::Shared => {
7898                let prepared = segment
7899                    .graph_search_cache
7900                    .prepare_shared(segment, &cancellation)
7901                    .map_err(map_graph_cache_error)?;
7902                (
7903                    prepared.graph,
7904                    prepared.graph_validated,
7905                    prepared.entry_seed_discovered,
7906                    Some(prepared.norm_range),
7907                )
7908            }
7909            #[cfg(test)]
7910            GraphBoundMode::Independent => {
7911                let (graph, graph_validated) = segment
7912                    .graph_search_cache
7913                    .bind_graph(segment)
7914                    .map_err(map_graph_cache_error)?;
7915                (graph, graph_validated, false, None)
7916            }
7917        };
7918    let rescore = query_rescore_rows_for_search(
7919        segment,
7920        #[cfg(any(test, feature = "test-seams"))]
7921        vector_fault_controller,
7922        #[cfg(any(test, feature = "test-seams"))]
7923        vector_fault_tier(_tier),
7924    )?;
7925    let node_count = graph.node_count() as usize;
7926    let segment_k = k.min(node_count);
7927    let has_tombstones = alive.tombstone_count() != 0;
7928    let target_live_k = if has_tombstones {
7929        usize::try_from(alive.live_count())
7930            .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?
7931            .min(k)
7932    } else {
7933        segment_k
7934    };
7935    let maximum_candidate_k = if has_tombstones {
7936        let tombstone_count = usize::try_from(alive.tombstone_count())
7937            .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
7938        target_live_k
7939            .checked_add(tombstone_count)
7940            .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?
7941            .min(node_count)
7942            .max(segment_k)
7943    } else {
7944        segment_k
7945    };
7946    if let Some(ef) = graph_options.ef()
7947        && ef < segment_k
7948    {
7949        return Err(QueryError::Graph(
7950            crate::graph::search::GraphSearchError::AdaptiveEf(
7951                crate::graph::search::AdaptiveEfError::ExplicitBelowK { k: segment_k, ef },
7952            ),
7953        ));
7954    }
7955    let request_for = |candidate_k| {
7956        let graph_request = crate::graph::search::GraphSearchRequest::new(
7957            request.vector(),
7958            candidate_k,
7959            graph_options.seed(),
7960        )
7961        .with_profile(graph_options.profile());
7962        // An explicit ef controls the caller's live-candidate window.
7963        // Tombstones are engine state, so reserve only the additional
7964        // width needed to replace deleted rows instead of turning a
7965        // previously valid ef=k request into an internal below-k error.
7966        graph_options.ef().map_or(graph_request, |ef| {
7967            graph_request.with_ef(ef.max(candidate_k))
7968        })
7969    };
7970    let mut candidate_k = segment_k;
7971    let mut graph_request = request_for(candidate_k);
7972    let effective_ef = graph_request
7973        .effective_ef(graph.node_count() as usize)
7974        .map_err(crate::graph::search::GraphSearchError::AdaptiveEf)
7975        .map_err(QueryError::Graph)?;
7976    let requestable_max_k = maximum_candidate_k;
7977    let scratch_ef = if has_tombstones && requestable_max_k > candidate_k {
7978        request_for(requestable_max_k)
7979            .effective_ef(node_count)
7980            .map_err(crate::graph::search::GraphSearchError::AdaptiveEf)
7981            .map_err(QueryError::Graph)?
7982    } else {
7983        effective_ef
7984    };
7985    if let (Some(norm_range), Some(competitive_distance)) = (norm_range, competitive_distance)
7986        && norm_range.squared_l2_upper_bound(request.vector()) <= f64::from(f32::MAX)
7987        && (norm_range.squared_l2_lower_bound(request.vector()) as f32) > competitive_distance
7988    {
7989        cancellation.check_graph().map_err(map_scan_error)?;
7990        let graph_stats = crate::ingest::GraphSearchStats {
7991            graph_validations: usize::from(graph_validated),
7992            entry_seed_discoveries: usize::from(prepared_entry_seed_discovered),
7993            segments_pruned_by_bound: 1,
7994            ..crate::ingest::GraphSearchStats::default()
7995        };
7996        let plan = crate::planner::SegmentPlan::unfiltered_pruned(source, alive.live_count());
7997        return Ok(SegmentGraphResult::Pruned {
7998            plan,
7999            graph_stats,
8000            _lease: lease,
8001        });
8002    }
8003    let mut scratch = segment
8004        .graph_search_cache
8005        .checkout(graph, scratch_ef, accounting, &cancellation)
8006        .map_err(map_graph_cache_error)?;
8007    let entry_seed_discovered = prepared_entry_seed_discovered || scratch.entry_seed_discovered();
8008    let entries = scratch.entries();
8009    let mut searcher = crate::graph::search::GraphSearcher::with_entry_row_ids(
8010        graph,
8011        rescore,
8012        entries,
8013        scratch.scratch_mut().map_err(map_graph_error)?,
8014    )
8015    .map_err(map_graph_error)?
8016    .with_rescore_validator(segment);
8017    #[cfg(any(test, feature = "test-seams"))]
8018    if let Some(after_hops) = graph_options.cancel_after_hops() {
8019        let token = cancellation.cancel_token_for_test().ok_or_else(|| {
8020            QueryError::Graph(crate::graph::search::GraphSearchError::Geometry(
8021                "test hop cancellation requires a Cancel query control".to_owned(),
8022            ))
8023        })?;
8024        let _observed_hops = searcher.cancel_after_hops(after_hops, token);
8025    }
8026    #[cfg(any(test, feature = "test-seams"))]
8027    if let Some(controller) = vector_fault_controller {
8028        searcher = searcher.with_vector_fault_controller(
8029            controller,
8030            vector_fault_source(source),
8031            crate::scan::vector_fault::VectorSearchTier::Graph,
8032        );
8033    }
8034    let mut traversal_dims_touched = 0_u64;
8035    let mut traversal_bytes_read = 0_u64;
8036    let mut traversal_epoch_clears = 0_usize;
8037    let mut traversal_candidates_scored = 0_usize;
8038    let mut traversal_candidates_rescored = 0_usize;
8039    let result = loop {
8040        let result = searcher.search_reusing(
8041            graph_request,
8042            Some(&cancellation),
8043            vector_preparation,
8044            segment,
8045            source,
8046        )?;
8047        let counters = result.counters();
8048        traversal_dims_touched = traversal_dims_touched
8049            .checked_add(counters.dims_touched())
8050            .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8051        traversal_bytes_read = traversal_bytes_read
8052            .checked_add(counters.bytes_read())
8053            .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8054        traversal_epoch_clears = traversal_epoch_clears
8055            .checked_add(usize::from(counters.visited_epoch_cleared()))
8056            .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8057        traversal_candidates_scored = traversal_candidates_scored
8058            .checked_add(counters.candidates_scored())
8059            .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8060        traversal_candidates_rescored = traversal_candidates_rescored
8061            .checked_add(counters.candidates_rescored())
8062            .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8063
8064        if !has_tombstones {
8065            break result;
8066        }
8067        let retained_live = result
8068            .candidates()
8069            .iter()
8070            .filter(|candidate| alive.is_alive(candidate.row_id()))
8071            .count();
8072        if retained_live >= target_live_k {
8073            break result;
8074        }
8075        if candidate_k >= requestable_max_k {
8076            return Err(QueryError::Graph(
8077                crate::graph::search::GraphSearchError::Geometry(format!(
8078                    "graph traversal exhausted {candidate_k} candidates but retained {retained_live} live rows, expected {target_live_k}"
8079                )),
8080            ));
8081        }
8082        let widened_k = candidate_k
8083            .saturating_mul(2)
8084            .max(candidate_k.saturating_add(1))
8085            .min(requestable_max_k);
8086        if widened_k <= candidate_k {
8087            return Err(QueryError::Graph(
8088                crate::graph::search::GraphSearchError::Geometry(format!(
8089                    "graph traversal could not widen beyond {candidate_k} candidates"
8090                )),
8091            ));
8092        }
8093        candidate_k = widened_k;
8094        graph_request = request_for(candidate_k);
8095    };
8096    let graph_stats = crate::ingest::GraphSearchStats {
8097        segments_traversed: 1,
8098        graph_validations: usize::from(graph_validated),
8099        entry_seed_discoveries: usize::from(entry_seed_discovered),
8100        visited_epoch_clears: traversal_epoch_clears,
8101        candidates_scored: traversal_candidates_scored,
8102        candidates_rescored: traversal_candidates_rescored,
8103        segments_pruned_by_bound: 0,
8104    };
8105    let plan = crate::planner::SegmentPlan::unfiltered_graph(
8106        source,
8107        alive.live_count(),
8108        graph_options.ef(),
8109        effective_ef,
8110        graph_options.profile(),
8111    );
8112    let caller_thread = std::thread::current().id();
8113    #[cfg(any(test, feature = "test-seams"))]
8114    drop(searcher);
8115    Ok(SegmentGraphResult::Traversed {
8116        result,
8117        plan,
8118        dims_touched: traversal_dims_touched,
8119        bytes_read: traversal_bytes_read,
8120        caller_thread,
8121        graph_stats,
8122        _scratch: scratch,
8123        _lease: lease,
8124    })
8125}
8126
8127fn add_traversal_stats(
8128    stats: &mut crate::ingest::GraphSearchStats,
8129    delta: crate::ingest::GraphSearchStats,
8130) -> Result<(), QueryError> {
8131    stats.segments_traversed = stats
8132        .segments_traversed
8133        .checked_add(delta.segments_traversed)
8134        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8135    stats.graph_validations = stats
8136        .graph_validations
8137        .checked_add(delta.graph_validations)
8138        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8139    stats.entry_seed_discoveries = stats
8140        .entry_seed_discoveries
8141        .checked_add(delta.entry_seed_discoveries)
8142        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8143    stats.visited_epoch_clears = stats
8144        .visited_epoch_clears
8145        .checked_add(delta.visited_epoch_clears)
8146        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8147    stats.candidates_scored = stats
8148        .candidates_scored
8149        .checked_add(delta.candidates_scored)
8150        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8151    stats.candidates_rescored = stats
8152        .candidates_rescored
8153        .checked_add(delta.candidates_rescored)
8154        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8155    stats.segments_pruned_by_bound = stats
8156        .segments_pruned_by_bound
8157        .checked_add(delta.segments_pruned_by_bound)
8158        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8159    Ok(())
8160}
8161
8162#[allow(clippy::too_many_arguments)]
8163fn scan_sealed_segment(
8164    pool: Option<&pool::QueryPool>,
8165    snapshot: &Arc<PublishedSnapshot>,
8166    segment: &crate::segment::reader::SegmentReader,
8167    alive: &crate::meta::AliveSet,
8168    generation: u64,
8169    request: crate::ingest::SearchRequest<'_>,
8170    k: usize,
8171    scan_options: crate::scan::ScanOptions,
8172    control: &QueryControl,
8173    bit4_query: &std::cell::OnceCell<Result<crate::quant::Bit4Query, crate::quant::QuantError>>,
8174    int8_query: &std::cell::OnceCell<Result<crate::quant::Int8Query, crate::quant::QuantError>>,
8175    full_precision: bool,
8176    exact_lane: usize,
8177    accounting: &Arc<stats::Accounting>,
8178    exact_memory: &mut ExactScanMemory,
8179    _source: crate::ingest::RowSource,
8180    _tier: SearchTier,
8181    #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
8182        &crate::scan::vector_fault::VectorFaultController,
8183    >,
8184) -> Result<crate::scan::ScanOutcome, QueryError> {
8185    use crate::scan::{ScanQuery, ScanRequest, ScanRows};
8186
8187    if full_precision {
8188        let lease = SnapshotLease::new_at(Arc::clone(snapshot), generation);
8189        let vectors = exact_rescore_rows_for_search(
8190            segment,
8191            #[cfg(any(test, feature = "test-seams"))]
8192            vector_fault_controller,
8193            #[cfg(any(test, feature = "test-seams"))]
8194            vector_fault_tier(_tier),
8195        )?;
8196        return scan_squared_l2(
8197            vectors,
8198            segment.meta().row_count as usize,
8199            alive,
8200            request.vector(),
8201            k,
8202            ExactScanDispatch {
8203                pool,
8204                accounting,
8205                control,
8206                lease: &lease,
8207                thread_budget: scan_options.thread_budget,
8208                lane: exact_lane,
8209            },
8210            exact_memory,
8211            #[cfg(any(test, feature = "test-seams"))]
8212            vector_fault_controller,
8213            #[cfg(any(test, feature = "test-seams"))]
8214            vector_fault_source(_source),
8215            #[cfg(any(test, feature = "test-seams"))]
8216            vector_fault_tier(_tier),
8217        );
8218    }
8219
8220    let query_pool = pool.ok_or(QueryError::Store(StoreError::Synchronization {
8221        component: "sealed scan-tier query pool",
8222    }))?;
8223    match segment.meta().scheme {
8224        0 => execute_store_scan(
8225            query_pool,
8226            ScanRequest {
8227                query: ScanQuery::F32(request.vector()),
8228                rows: ScanRows::F32BorrowedRowMajor(
8229                    segment
8230                        .query_f32_codes()
8231                        .map_err(StoreError::Segment)
8232                        .map_err(QueryError::Store)?,
8233                ),
8234                row_mask: Some(alive.scan_mask()),
8235            },
8236            k,
8237            scan_options,
8238            control.clone(),
8239            SnapshotLease::new_at(Arc::clone(snapshot), generation),
8240            #[cfg(any(test, feature = "test-seams"))]
8241            vector_fault_controller,
8242            #[cfg(any(test, feature = "test-seams"))]
8243            vector_fault_source(_source),
8244            #[cfg(any(test, feature = "test-seams"))]
8245            vector_fault_tier(_tier),
8246        ),
8247        4 => execute_store_scan(
8248            query_pool,
8249            ScanRequest {
8250                query: ScanQuery::Bit4(
8251                    bit4_query
8252                        .get_or_init(|| crate::quant::prepare_bit4_query(request.vector(), 0))
8253                        .as_ref()
8254                        .map_err(|error| crate::scan::ScanError::Quant(error.clone()))
8255                        .map_err(QueryError::Scan)?,
8256                ),
8257                rows: ScanRows::Bit4RowMajor {
8258                    codes: segment
8259                        .query_bit4_codes()
8260                        .map_err(StoreError::Segment)
8261                        .map_err(QueryError::Store)?,
8262                    factors: segment
8263                        .query_bit4_factors()
8264                        .map_err(StoreError::Segment)
8265                        .map_err(QueryError::Store)?,
8266                },
8267                row_mask: Some(alive.scan_mask()),
8268            },
8269            k,
8270            scan_options,
8271            control.clone(),
8272            SnapshotLease::new_at(Arc::clone(snapshot), generation),
8273            #[cfg(any(test, feature = "test-seams"))]
8274            vector_fault_controller,
8275            #[cfg(any(test, feature = "test-seams"))]
8276            vector_fault_source(_source),
8277            #[cfg(any(test, feature = "test-seams"))]
8278            vector_fault_tier(_tier),
8279        ),
8280        2 => {
8281            let factors = segment.query_int8_factors().map_err(QueryError::Store)?;
8282            execute_store_scan(
8283                query_pool,
8284                ScanRequest {
8285                    query: ScanQuery::Int8(
8286                        int8_query
8287                            .get_or_init(|| crate::quant::prepare_int8_query(request.vector()))
8288                            .as_ref()
8289                            .map_err(|error| crate::scan::ScanError::Quant(error.clone()))
8290                            .map_err(QueryError::Scan)?,
8291                    ),
8292                    rows: ScanRows::Int8RowMajor {
8293                        codes: segment
8294                            .query_int8_codes()
8295                            .map_err(StoreError::Segment)
8296                            .map_err(QueryError::Store)?,
8297                        factors: factors.as_slice(),
8298                    },
8299                    row_mask: Some(alive.scan_mask()),
8300                },
8301                k,
8302                scan_options,
8303                control.clone(),
8304                SnapshotLease::new_at(Arc::clone(snapshot), generation),
8305                #[cfg(any(test, feature = "test-seams"))]
8306                vector_fault_controller,
8307                #[cfg(any(test, feature = "test-seams"))]
8308                vector_fault_source(_source),
8309                #[cfg(any(test, feature = "test-seams"))]
8310                vector_fault_tier(_tier),
8311            )
8312        }
8313        scheme => Err(QueryError::Store(StoreError::Segment(
8314            crate::segment::SegmentError::Geometry(format!(
8315                "store search does not support sealed scheme {scheme}"
8316            )),
8317        ))),
8318    }
8319}
8320
8321#[allow(clippy::too_many_arguments)]
8322fn execute_store_scan(
8323    pool: &pool::QueryPool,
8324    request: crate::scan::ScanRequest<'_>,
8325    k: usize,
8326    options: crate::scan::ScanOptions,
8327    control: QueryControl,
8328    lease: SnapshotLease,
8329    #[cfg(any(test, feature = "test-seams"))] controller: Option<
8330        &crate::scan::vector_fault::VectorFaultController,
8331    >,
8332    #[cfg(any(test, feature = "test-seams"))] source: crate::scan::vector_fault::VectorRowSource,
8333    #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8334) -> Result<crate::scan::ScanOutcome, QueryError> {
8335    #[cfg(any(test, feature = "test-seams"))]
8336    if let Some(controller) = controller {
8337        let geometry = crate::scan::scan_geometry(request).map_err(QueryError::Scan)?;
8338        let cancellation = QueryCancellation::new(&control, &lease);
8339        let partition = crate::scan::scan_partition_with_vector_faults(
8340            request,
8341            k,
8342            0..geometry.row_count,
8343            Some(&cancellation),
8344            controller,
8345            source,
8346            tier,
8347        )
8348        .map_err(map_scan_error)?;
8349        return Ok(crate::scan::ScanOutcome {
8350            candidates: partition.candidates,
8351            worst_score: None,
8352            stats: crate::scan::ScanStats {
8353                dims_touched: partition.dims_touched,
8354                bytes_read: partition.bytes_read,
8355                threads_used: 1,
8356                worker_thread_ids: vec![partition.worker_thread_id],
8357            },
8358        });
8359    }
8360    pool.execute(request, k, options, control, lease)
8361}
8362
8363#[cfg(any(test, feature = "test-seams"))]
8364fn vector_fault_tier(tier: SearchTier) -> crate::scan::vector_fault::VectorSearchTier {
8365    match tier {
8366        SearchTier::Exact => crate::scan::vector_fault::VectorSearchTier::Exact,
8367        SearchTier::Scan => crate::scan::vector_fault::VectorSearchTier::Scan,
8368        SearchTier::Graph(_) => crate::scan::vector_fault::VectorSearchTier::Graph,
8369        SearchTier::Auto => crate::scan::vector_fault::VectorSearchTier::Auto,
8370    }
8371}
8372
8373#[cfg(any(test, feature = "test-seams"))]
8374fn vector_fault_source(
8375    source: crate::ingest::RowSource,
8376) -> crate::scan::vector_fault::VectorRowSource {
8377    match source {
8378        crate::ingest::RowSource::Active => crate::scan::vector_fault::VectorRowSource::Active,
8379        crate::ingest::RowSource::Sealed(segment) => {
8380            crate::scan::vector_fault::VectorRowSource::Sealed(*segment.as_bytes())
8381        }
8382    }
8383}
8384
8385pub(crate) fn auto_uses_full_precision(
8386    snapshot: &PublishedSnapshot,
8387    active: &crate::ingest::ActiveSegment,
8388) -> bool {
8389    let mut has_exact = false;
8390    let mut has_estimated = !active.is_empty();
8391    for segment in snapshot.segments() {
8392        if segment
8393            .directory()
8394            .iter()
8395            .any(|entry| entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id())
8396        {
8397            return true;
8398        }
8399        if segment.meta().row_count == 0 {
8400            continue;
8401        }
8402        match segment.meta().scheme {
8403            0 => has_exact = true,
8404            2 | 4 => has_estimated = true,
8405            _ => {}
8406        }
8407    }
8408    has_exact && has_estimated
8409}
8410
8411/// Returns the f32 rescore rows for a query-time exact score.
8412///
8413/// This is the query path, so it uses the validate-once accessor. The
8414/// verifying `rescore_f32` re-hashes the whole rescore region on every
8415/// call, and hybrid cross-fill calls this once per cross-filled
8416/// document: 50 calls over a 181 MB region cost about 219 ms of a
8417/// 221 ms hybrid query on 58,980 FiQA rows, while both legs together
8418/// cost under 3 ms. Uncached `rescore_f32` remains for the validation
8419/// and diagnostic callers.
8420pub(crate) fn exact_rescore_rows(
8421    segment: &crate::segment::reader::SegmentReader,
8422) -> Result<&[f32], QueryError> {
8423    segment
8424        .query_rescore_f32()
8425        .map_err(|source| QueryError::Store(StoreError::Segment(source)))
8426}
8427
8428#[cfg(not(any(test, feature = "test-seams")))]
8429use exact_rescore_rows as exact_rescore_rows_for_search;
8430
8431#[cfg(any(test, feature = "test-seams"))]
8432fn exact_rescore_rows_for_search<'a>(
8433    segment: &'a crate::segment::reader::SegmentReader,
8434    #[cfg(any(test, feature = "test-seams"))] controller: Option<
8435        &crate::scan::vector_fault::VectorFaultController,
8436    >,
8437    #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8438) -> Result<&'a [f32], QueryError> {
8439    #[cfg(any(test, feature = "test-seams"))]
8440    if let Some(available_rows) = controller.and_then(|controller| {
8441        controller.missing_rescore_rows(
8442            crate::scan::vector_fault::VectorRowSource::Sealed(*segment.meta().id.as_bytes()),
8443            tier,
8444            crate::scan::vector_fault::MissingRescoreSite::ExactRescoreRows,
8445            segment.meta().row_count,
8446        )
8447    }) {
8448        return Err(missing_rescore_rows_error(segment, available_rows));
8449    }
8450    exact_rescore_rows(segment)
8451}
8452
8453pub(crate) fn query_rescore_rows(
8454    segment: &crate::segment::reader::SegmentReader,
8455) -> Result<&[f32], QueryError> {
8456    segment
8457        .query_rescore_f32()
8458        .map_err(|source| QueryError::Store(StoreError::Segment(source)))
8459}
8460
8461#[cfg(not(any(test, feature = "test-seams")))]
8462use query_rescore_rows as query_rescore_rows_for_search;
8463
8464#[cfg(any(test, feature = "test-seams"))]
8465fn query_rescore_rows_for_search<'a>(
8466    segment: &'a crate::segment::reader::SegmentReader,
8467    #[cfg(any(test, feature = "test-seams"))] controller: Option<
8468        &crate::scan::vector_fault::VectorFaultController,
8469    >,
8470    #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8471) -> Result<&'a [f32], QueryError> {
8472    #[cfg(any(test, feature = "test-seams"))]
8473    if let Some(available_rows) = controller.and_then(|controller| {
8474        controller.missing_rescore_rows(
8475            crate::scan::vector_fault::VectorRowSource::Sealed(*segment.meta().id.as_bytes()),
8476            tier,
8477            crate::scan::vector_fault::MissingRescoreSite::QueryRescoreRows,
8478            segment.meta().row_count,
8479        )
8480    }) {
8481        return Err(missing_rescore_rows_error(segment, available_rows));
8482    }
8483    query_rescore_rows(segment)
8484}
8485
8486#[cfg(any(test, feature = "test-seams"))]
8487fn missing_rescore_rows_error(
8488    segment: &crate::segment::reader::SegmentReader,
8489    available_rows: u32,
8490) -> QueryError {
8491    QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Geometry(
8492        format!(
8493            "exact scores unavailable for segment {}: expected {} rows, got {available_rows}",
8494            segment.meta().id,
8495            segment.meta().row_count,
8496        ),
8497    )))
8498}
8499
8500fn retain_global_top_k(candidates: &mut Vec<crate::ingest::SearchCandidate>, k: usize) {
8501    candidates.sort_unstable_by(crate::ingest::compare_search_candidates);
8502    candidates.truncate(k);
8503}
8504
8505fn global_competitive_distance(
8506    candidates: &[crate::ingest::SearchCandidate],
8507    k: usize,
8508) -> Option<f32> {
8509    if k == 0 || candidates.len() < k {
8510        return None;
8511    }
8512    candidates
8513        .get(k.saturating_sub(1))
8514        .map(|candidate| -candidate.score())
8515        .filter(|distance| distance.is_finite() && *distance >= 0.0)
8516}
8517
8518fn map_graph_cache_error(error: graph_cache::GraphCacheError) -> QueryError {
8519    match error {
8520        graph_cache::GraphCacheError::Store(error) => QueryError::Store(error),
8521        graph_cache::GraphCacheError::Search(error) => map_graph_error(error),
8522    }
8523}
8524
8525pub(crate) fn map_graph_error(error: crate::graph::search::GraphSearchError) -> QueryError {
8526    match error {
8527        crate::graph::search::GraphSearchError::ExactRescoreUnavailable(detail) => {
8528            QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Geometry(
8529                detail,
8530            )))
8531        }
8532        crate::graph::search::GraphSearchError::Cancelled { partial } => {
8533            QueryError::Cancelled { partial }
8534        }
8535        crate::graph::search::GraphSearchError::Timeout { partial } => {
8536            QueryError::Timeout { partial }
8537        }
8538        crate::graph::search::GraphSearchError::ReadCancelled { partial } => {
8539            QueryError::ReadCancelled { partial }
8540        }
8541        crate::graph::search::GraphSearchError::Scan(error) => QueryError::Scan(error),
8542        error => QueryError::Graph(error),
8543    }
8544}
8545
8546#[allow(
8547    clippy::too_many_arguments,
8548    reason = "scoped dispatch and test source identity stay explicit"
8549)]
8550fn scan_active_squared_l2(
8551    active: &crate::ingest::ActiveSegment,
8552    alive: &crate::meta::AliveSet,
8553    query: &[f32],
8554    k: usize,
8555    dispatch: ExactScanDispatch<'_>,
8556    memory: &mut ExactScanMemory,
8557    #[cfg(any(test, feature = "test-seams"))] controller: Option<
8558        &crate::scan::vector_fault::VectorFaultController,
8559    >,
8560    #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8561) -> Result<crate::scan::ScanOutcome, QueryError> {
8562    scan_squared_l2(
8563        active.vectors(),
8564        active.row_count(),
8565        alive,
8566        query,
8567        k,
8568        dispatch,
8569        memory,
8570        #[cfg(any(test, feature = "test-seams"))]
8571        controller,
8572        #[cfg(any(test, feature = "test-seams"))]
8573        crate::scan::vector_fault::VectorRowSource::Active,
8574        #[cfg(any(test, feature = "test-seams"))]
8575        tier,
8576    )
8577}
8578
8579#[derive(Debug)]
8580enum ExactScanError {
8581    Query(QueryError),
8582    Narrowing { row_id: usize, score: f64 },
8583}
8584
8585impl From<QueryError> for ExactScanError {
8586    fn from(error: QueryError) -> Self {
8587        Self::Query(error)
8588    }
8589}
8590
8591impl ExactScanError {
8592    fn into_query(self) -> QueryError {
8593        match self {
8594            Self::Query(error) => error,
8595            Self::Narrowing { row_id, .. } => {
8596                QueryError::Scan(crate::scan::ScanError::NonFiniteScore { row_id })
8597            }
8598        }
8599    }
8600}
8601
8602pub(crate) struct ExactScanMemory {
8603    candidates: stats::AccountedCounter,
8604    worker_ids: stats::AccountedCounter,
8605}
8606
8607impl ExactScanMemory {
8608    pub(crate) fn new(accounting: &Arc<stats::Accounting>) -> Result<Self, QueryError> {
8609        Ok(Self {
8610            candidates: stats::AccountedCounter::new(
8611                accounting,
8612                stats::AllocationComponent::Temporary,
8613            )
8614            .map_err(QueryError::Store)?,
8615            worker_ids: stats::AccountedCounter::new(
8616                accounting,
8617                stats::AllocationComponent::Temporary,
8618            )
8619            .map_err(QueryError::Store)?,
8620        })
8621    }
8622}
8623
8624struct ExactScanDispatch<'a> {
8625    pool: Option<&'a pool::QueryPool>,
8626    accounting: &'a Arc<stats::Accounting>,
8627    control: &'a QueryControl,
8628    lease: &'a SnapshotLease,
8629    thread_budget: usize,
8630    lane: usize,
8631}
8632
8633#[allow(
8634    clippy::too_many_arguments,
8635    reason = "scoped dispatch and test source identity stay explicit"
8636)]
8637fn scan_squared_l2(
8638    vectors: &[f32],
8639    row_count: usize,
8640    alive: &crate::meta::AliveSet,
8641    query: &[f32],
8642    k: usize,
8643    dispatch: ExactScanDispatch<'_>,
8644    memory: &mut ExactScanMemory,
8645    #[cfg(any(test, feature = "test-seams"))] controller: Option<
8646        &crate::scan::vector_fault::VectorFaultController,
8647    >,
8648    #[cfg(any(test, feature = "test-seams"))] source: crate::scan::vector_fault::VectorRowSource,
8649    #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8650) -> Result<crate::scan::ScanOutcome, QueryError> {
8651    if let Some(pool) = dispatch.pool
8652        && pool.exact_workers(row_count, query.len(), dispatch.thread_budget) > 1
8653    {
8654        return pool.execute_exact(
8655            vectors,
8656            row_count,
8657            alive,
8658            query,
8659            k,
8660            dispatch.thread_budget,
8661            dispatch.lane,
8662            dispatch.control,
8663            dispatch.lease,
8664            dispatch.accounting,
8665            memory,
8666            #[cfg(any(test, feature = "test-seams"))]
8667            controller,
8668            #[cfg(any(test, feature = "test-seams"))]
8669            source,
8670            #[cfg(any(test, feature = "test-seams"))]
8671            tier,
8672        );
8673    }
8674    let cancellation = QueryCancellation::new(dispatch.control, dispatch.lease);
8675    #[cfg(any(test, feature = "test-seams"))]
8676    if controller.is_some_and(|controller| controller.has_exact_row_hooks()) {
8677        return scan_exact_partition::<false>(
8678            vectors,
8679            row_count,
8680            0..row_count,
8681            alive,
8682            query,
8683            k,
8684            &cancellation,
8685            memory,
8686            #[cfg(any(test, feature = "test-seams"))]
8687            controller,
8688            #[cfg(any(test, feature = "test-seams"))]
8689            source,
8690            #[cfg(any(test, feature = "test-seams"))]
8691            tier,
8692        )
8693        .map_err(ExactScanError::into_query);
8694    }
8695    scan_serial_squared_l2(
8696        vectors,
8697        row_count,
8698        alive,
8699        query,
8700        k,
8701        &cancellation,
8702        &mut memory.candidates,
8703        &mut memory.worker_ids,
8704        #[cfg(any(test, feature = "test-seams"))]
8705        controller,
8706        #[cfg(any(test, feature = "test-seams"))]
8707        source,
8708        #[cfg(any(test, feature = "test-seams"))]
8709        tier,
8710    )
8711}
8712
8713#[allow(clippy::too_many_arguments)]
8714fn scan_exact_partition<const OFFSET_ROWS: bool>(
8715    vectors: &[f32],
8716    row_count: usize,
8717    range: std::ops::Range<usize>,
8718    alive: &crate::meta::AliveSet,
8719    query: &[f32],
8720    k: usize,
8721    cancellation: &QueryCancellation<'_>,
8722    memory: &mut ExactScanMemory,
8723    #[cfg(any(test, feature = "test-seams"))] controller: Option<
8724        &crate::scan::vector_fault::VectorFaultController,
8725    >,
8726    #[cfg(any(test, feature = "test-seams"))] source: crate::scan::vector_fault::VectorRowSource,
8727    #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8728) -> Result<crate::scan::ScanOutcome, ExactScanError> {
8729    if query.is_empty() {
8730        return Err(QueryError::Scan(crate::scan::ScanError::ZeroDimension).into());
8731    }
8732    let expected = row_count
8733        .checked_mul(query.len())
8734        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8735    if vectors.len() != expected {
8736        return Err(QueryError::Scan(crate::scan::ScanError::RowDataLength {
8737            dimension: query.len(),
8738            actual: vectors.len(),
8739        })
8740        .into());
8741    }
8742    // Preserve the original physical-row cancellation preflight without storing
8743    // the rows. Scoring checks subsequently count eligible rows, as before.
8744    if range.start > range.end || range.end > row_count || (!OFFSET_ROWS && range.start != 0) {
8745        return Err(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow).into());
8746    }
8747    let mut live_rows = 0;
8748    for row in range.clone() {
8749        if row.is_multiple_of(64) {
8750            cancellation.check_graph().map_err(map_scan_error)?;
8751        }
8752        let local_row = u32::try_from(row)
8753            .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8754        live_rows += usize::from(alive.is_alive(local_row));
8755    }
8756    cancellation.check_graph().map_err(map_scan_error)?;
8757    let mut collector = crate::scan::topk::ExactTopK::try_new(k, live_rows, &mut memory.candidates)
8758        .map_err(QueryError::Store)?;
8759    let mut worst_score: Option<f32> = None;
8760    let mut narrowing_error: Option<(usize, f64)> = None;
8761    let start = range
8762        .start
8763        .checked_mul(query.len())
8764        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8765    let end = range
8766        .end
8767        .checked_mul(query.len())
8768        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8769    let partition_vectors = vectors
8770        .get(start..end)
8771        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8772    let rows = partition_vectors
8773        .chunks_exact(query.len())
8774        .enumerate()
8775        .map(|(local, vector)| {
8776            (
8777                if OFFSET_ROWS {
8778                    range.start + local
8779                } else {
8780                    local
8781                },
8782                vector,
8783            )
8784        })
8785        .filter(|(row, _)| u32::try_from(*row).is_ok_and(|row| alive.is_alive(row)));
8786    #[cfg(any(test, feature = "test-seams"))]
8787    let mut trace =
8788        controller.and_then(|controller| controller.trace_rows(source, tier, range.clone()));
8789    let scored_rows = crate::quant::exact_squared_l2_with_sink(
8790        query,
8791        rows,
8792        |_row, is_checkpoint| {
8793            #[cfg(any(test, feature = "test-seams"))]
8794            if let Some(controller) = controller {
8795                if let Some(trace) = &mut trace {
8796                    trace.checked(_row);
8797                }
8798                if is_checkpoint {
8799                    cancellation.check_graph().map_err(map_scan_error)?;
8800                }
8801                if controller.after_eligible_row(source, tier, _row) {
8802                    return Err(QueryError::Cancelled { partial: false });
8803                }
8804                return Ok(());
8805            }
8806            if is_checkpoint {
8807                cancellation.check_graph().map_err(map_scan_error)?;
8808            }
8809            Ok(())
8810        },
8811        |row_id, exact| {
8812            let score = exact as f32;
8813            if !score.is_finite() {
8814                // The old full f64 sort chose the best overflowing score, then
8815                // physical row ID. Defer this failure until every row check and
8816                // f64 rejection has run, including the final cancellation check.
8817                if narrowing_error.is_none_or(|(row, previous)| {
8818                    exact.total_cmp(&previous).is_gt()
8819                        || (exact.total_cmp(&previous).is_eq() && row_id < row)
8820                }) {
8821                    narrowing_error = Some((row_id, exact));
8822                }
8823                return Ok(());
8824            }
8825            if worst_score.is_none_or(|worst| score.total_cmp(&worst).is_lt()) {
8826                worst_score = Some(score);
8827            }
8828            collector
8829                .try_push(
8830                    crate::scan::ScanCandidate { row_id, score },
8831                    &mut memory.candidates,
8832                )
8833                .map_err(QueryError::Store)
8834        },
8835    )
8836    .map_err(|error| match error {
8837        crate::quant::RescoreCheckError::Rescore(error) => map_l2_rescore_error(error),
8838        crate::quant::RescoreCheckError::Check(error) => error,
8839    })?;
8840    cancellation.check_graph().map_err(map_scan_error)?;
8841    if let Some((row_id, score)) = narrowing_error {
8842        return Err(ExactScanError::Narrowing { row_id, score });
8843    }
8844    let (candidates, _collector_capacity) = collector
8845        .try_into_sorted_with_ties(&mut memory.candidates)
8846        .map_err(QueryError::Store)?;
8847    #[cfg(any(test, feature = "test-seams"))]
8848    if let Some(controller) = controller {
8849        controller.record_exact_scan(crate::scan::vector_fault::ExactScanWork {
8850            sorted_items: candidates.len(),
8851            collector_capacity: _collector_capacity,
8852            scored_rows,
8853            worst_score,
8854            ..Default::default()
8855        });
8856    }
8857    let dims = u64::try_from(query.len())
8858        .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8859    let scored_rows = u64::try_from(scored_rows)
8860        .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8861    let dims_touched = scored_rows
8862        .checked_mul(dims)
8863        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8864    let bytes_read = dims_touched
8865        .checked_mul(std::mem::size_of::<f32>() as u64)
8866        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8867    memory
8868        .worker_ids
8869        .set(std::mem::size_of::<std::thread::ThreadId>())
8870        .map_err(QueryError::Store)?;
8871    let mut worker_thread_ids = Vec::new();
8872    worker_thread_ids.try_reserve_exact(1).map_err(|_| {
8873        QueryError::Store(StoreError::AllocationFailed {
8874            needed: memory.worker_ids.bytes(),
8875            component: "exact worker IDs",
8876        })
8877    })?;
8878    worker_thread_ids.push(std::thread::current().id());
8879    Ok(crate::scan::ScanOutcome {
8880        candidates,
8881        worst_score,
8882        stats: crate::scan::ScanStats {
8883            dims_touched,
8884            bytes_read,
8885            threads_used: 1,
8886            worker_thread_ids,
8887        },
8888    })
8889}
8890
8891// Retain the plan-04 serial iterator and QueryError path. Partition offsets
8892// and narrowing-error aggregation belong only to worker scans.
8893#[allow(clippy::too_many_arguments)]
8894fn scan_serial_squared_l2(
8895    vectors: &[f32],
8896    row_count: usize,
8897    alive: &crate::meta::AliveSet,
8898    query: &[f32],
8899    k: usize,
8900    cancellation: &QueryCancellation<'_>,
8901    memory: &mut stats::AccountedCounter,
8902    worker_memory: &mut stats::AccountedCounter,
8903    #[cfg(any(test, feature = "test-seams"))] controller: Option<
8904        &crate::scan::vector_fault::VectorFaultController,
8905    >,
8906    #[cfg(any(test, feature = "test-seams"))] source: crate::scan::vector_fault::VectorRowSource,
8907    #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8908) -> Result<crate::scan::ScanOutcome, QueryError> {
8909    if query.is_empty() {
8910        return Err(QueryError::Scan(crate::scan::ScanError::ZeroDimension));
8911    }
8912    let expected = row_count
8913        .checked_mul(query.len())
8914        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8915    if vectors.len() != expected {
8916        return Err(QueryError::Scan(crate::scan::ScanError::RowDataLength {
8917            dimension: query.len(),
8918            actual: vectors.len(),
8919        }));
8920    }
8921    // Preserve the original physical-row cancellation preflight without storing
8922    // the rows. Scoring checks subsequently count eligible rows, as before.
8923    let mut live_rows = 0;
8924    for row in 0..row_count {
8925        if row.is_multiple_of(64) {
8926            cancellation.check_graph().map_err(map_scan_error)?;
8927        }
8928        let local_row = u32::try_from(row)
8929            .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8930        live_rows += usize::from(alive.is_alive(local_row));
8931    }
8932    cancellation.check_graph().map_err(map_scan_error)?;
8933    let mut collector =
8934        crate::scan::topk::ExactTopK::try_new(k, live_rows, memory).map_err(QueryError::Store)?;
8935    let mut worst_score: Option<f32> = None;
8936    let mut narrowing_error: Option<(usize, f64)> = None;
8937    let rows = vectors
8938        .chunks_exact(query.len())
8939        .enumerate()
8940        .filter(|(row, _)| u32::try_from(*row).is_ok_and(|row| alive.is_alive(row)));
8941    let scored_rows = crate::quant::exact_squared_l2_with_sink(
8942        query,
8943        rows,
8944        |_row, is_checkpoint| {
8945            if is_checkpoint {
8946                cancellation.check_graph().map_err(map_scan_error)?;
8947            }
8948            #[cfg(any(test, feature = "test-seams"))]
8949            if controller
8950                .is_some_and(|controller| controller.after_eligible_row(source, tier, _row))
8951            {
8952                return Err(QueryError::Cancelled { partial: false });
8953            }
8954            Ok(())
8955        },
8956        |row_id, exact| {
8957            let score = exact as f32;
8958            if !score.is_finite() {
8959                // The old full f64 sort chose the best overflowing score, then
8960                // physical row ID. Defer this failure until every row check and
8961                // f64 rejection has run, including the final cancellation check.
8962                if narrowing_error.is_none_or(|(row, previous)| {
8963                    exact.total_cmp(&previous).is_gt()
8964                        || (exact.total_cmp(&previous).is_eq() && row_id < row)
8965                }) {
8966                    narrowing_error = Some((row_id, exact));
8967                }
8968                return Ok(());
8969            }
8970            if worst_score.is_none_or(|worst| score.total_cmp(&worst).is_lt()) {
8971                worst_score = Some(score);
8972            }
8973            collector
8974                .try_push(crate::scan::ScanCandidate { row_id, score }, memory)
8975                .map_err(QueryError::Store)
8976        },
8977    )
8978    .map_err(|error| match error {
8979        crate::quant::RescoreCheckError::Rescore(error) => map_l2_rescore_error(error),
8980        crate::quant::RescoreCheckError::Check(error) => error,
8981    })?;
8982    cancellation.check_graph().map_err(map_scan_error)?;
8983    if let Some((row_id, _)) = narrowing_error {
8984        return Err(QueryError::Scan(crate::scan::ScanError::NonFiniteScore {
8985            row_id,
8986        }));
8987    }
8988    let (candidates, _collector_capacity) = collector
8989        .try_into_sorted_with_ties(memory)
8990        .map_err(QueryError::Store)?;
8991    #[cfg(any(test, feature = "test-seams"))]
8992    if let Some(controller) = controller {
8993        controller.record_exact_scan(crate::scan::vector_fault::ExactScanWork {
8994            sorted_items: candidates.len(),
8995            collector_capacity: _collector_capacity,
8996            scored_rows,
8997            worst_score,
8998            ..Default::default()
8999        });
9000    }
9001    let dims = u64::try_from(query.len())
9002        .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9003    let scored_rows = u64::try_from(scored_rows)
9004        .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9005    let dims_touched = scored_rows
9006        .checked_mul(dims)
9007        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9008    let bytes_read = dims_touched
9009        .checked_mul(std::mem::size_of::<f32>() as u64)
9010        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9011    worker_memory
9012        .set(std::mem::size_of::<std::thread::ThreadId>())
9013        .map_err(QueryError::Store)?;
9014    let mut worker_thread_ids = Vec::new();
9015    worker_thread_ids.try_reserve_exact(1).map_err(|_| {
9016        QueryError::Store(StoreError::AllocationFailed {
9017            needed: worker_memory.bytes(),
9018            component: "exact worker IDs",
9019        })
9020    })?;
9021    worker_thread_ids.push(std::thread::current().id());
9022    Ok(crate::scan::ScanOutcome {
9023        candidates,
9024        worst_score,
9025        stats: crate::scan::ScanStats {
9026            dims_touched,
9027            bytes_read,
9028            threads_used: 1,
9029            worker_thread_ids,
9030        },
9031    })
9032}
9033
9034fn map_l2_rescore_error(error: crate::quant::RescoreError) -> QueryError {
9035    let error = match error {
9036        crate::quant::RescoreError::ZeroDimension => crate::scan::ScanError::ZeroDimension,
9037        crate::quant::RescoreError::QueryDimension { expected, actual }
9038        | crate::quant::RescoreError::CoarseScoreCount { expected, actual }
9039        | crate::quant::RescoreError::CandidateRowCount { expected, actual } => {
9040            crate::scan::ScanError::RowDataLength {
9041                dimension: expected,
9042                actual,
9043            }
9044        }
9045        crate::quant::RescoreError::RowDataLength { dimension, actual } => {
9046            crate::scan::ScanError::RowDataLength { dimension, actual }
9047        }
9048        crate::quant::RescoreError::CandidateRowOutOfRange {
9049            row_index,
9050            row_count,
9051            ..
9052        } => crate::scan::ScanError::RowDataLength {
9053            dimension: row_count,
9054            actual: row_index,
9055        },
9056        crate::quant::RescoreError::NonFiniteCoarseScore { index }
9057        | crate::quant::RescoreError::NonFiniteExactScore { row_index: index } => {
9058            crate::scan::ScanError::NonFiniteScore { row_id: index }
9059        }
9060        crate::quant::RescoreError::ZeroK
9061        | crate::quant::RescoreError::ZeroOversample
9062        | crate::quant::RescoreError::InsufficientCandidates { .. }
9063        | crate::quant::RescoreError::ArithmeticOverflow => {
9064            crate::scan::ScanError::ArithmeticOverflow
9065        }
9066        error @ (crate::quant::RescoreError::ZeroCandidateLimit
9067        | crate::quant::RescoreError::CandidateLimitExceeded { .. }) => {
9068            crate::scan::ScanError::Rescore(error)
9069        }
9070    };
9071    QueryError::Scan(error)
9072}
9073
9074fn map_scan_error(error: crate::scan::ScanError) -> QueryError {
9075    match error {
9076        crate::scan::ScanError::Cancelled { partial } => QueryError::Cancelled { partial },
9077        crate::scan::ScanError::Timeout { partial } => QueryError::Timeout { partial },
9078        crate::scan::ScanError::ReadCancelled { partial } => QueryError::ReadCancelled { partial },
9079        error => QueryError::Scan(error),
9080    }
9081}
9082
9083#[allow(clippy::too_many_arguments)]
9084fn merge_store_outcome(
9085    outcome: crate::scan::ScanOutcome,
9086    source: crate::ingest::RowSource,
9087    document: impl Fn(usize) -> Result<Option<crate::ingest::DocumentVersion>, QueryError>,
9088    candidates: &mut Vec<crate::ingest::SearchCandidate>,
9089    dims_touched: &mut u64,
9090    bytes_read: &mut u64,
9091    worker_thread_ids: &mut Vec<std::thread::ThreadId>,
9092    exact_score: bool,
9093    #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
9094        &crate::scan::vector_fault::VectorFaultController,
9095    >,
9096    #[cfg(any(test, feature = "test-seams"))]
9097    vector_fault_tier: crate::scan::vector_fault::VectorSearchTier,
9098) -> Result<(), QueryError> {
9099    *dims_touched = dims_touched
9100        .checked_add(outcome.stats.dims_touched)
9101        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9102    *bytes_read = bytes_read
9103        .checked_add(outcome.stats.bytes_read)
9104        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9105    for worker in outcome.stats.worker_thread_ids {
9106        if !worker_thread_ids.contains(&worker) {
9107            worker_thread_ids.push(worker);
9108        }
9109    }
9110    reserve_global_candidates(
9111        candidates,
9112        outcome.candidates.len(),
9113        #[cfg(any(test, feature = "test-seams"))]
9114        vector_fault_controller,
9115        #[cfg(any(test, feature = "test-seams"))]
9116        vector_fault_tier,
9117    )?;
9118    for candidate in outcome.candidates {
9119        let local_row = u32::try_from(candidate.row_id)
9120            .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9121        candidates.push(crate::ingest::SearchCandidate::new(
9122            crate::ingest::GlobalRowId::new(source, local_row),
9123            document(candidate.row_id)?,
9124            candidate.score,
9125            exact_score,
9126        ));
9127    }
9128    Ok(())
9129}
9130
9131pub(crate) fn reserve_global_candidates(
9132    candidates: &mut Vec<crate::ingest::SearchCandidate>,
9133    additional: usize,
9134    #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
9135        &crate::scan::vector_fault::VectorFaultController,
9136    >,
9137    #[cfg(any(test, feature = "test-seams"))]
9138    vector_fault_tier: crate::scan::vector_fault::VectorSearchTier,
9139) -> Result<(), QueryError> {
9140    if additional == 0 {
9141        return Ok(());
9142    }
9143    let items = u64::try_from(additional)
9144        .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9145    let candidate_bytes = u64::try_from(std::mem::size_of::<crate::ingest::SearchCandidate>())
9146        .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9147    let needed = items
9148        .checked_mul(candidate_bytes)
9149        .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9150    #[cfg(any(test, feature = "test-seams"))]
9151    if vector_fault_controller.is_some_and(|controller| {
9152        controller.deny_global_candidate_allocation(vector_fault_tier, items, needed)
9153    }) {
9154        return Err(QueryError::Store(StoreError::AllocationFailed {
9155            needed,
9156            component: "vector search global candidates",
9157        }));
9158    }
9159    candidates.try_reserve_exact(additional).map_err(|_| {
9160        QueryError::Store(StoreError::AllocationFailed {
9161            needed,
9162            component: "vector search global candidates",
9163        })
9164    })
9165}
9166
9167struct AdmittedLexicalQuery<'a> {
9168    generation: u64,
9169    active: Arc<crate::ingest::ActiveSegment>,
9170    snapshot: snapshot::ReadSnapshot,
9171    active_query: ActiveQuery<'a>,
9172}
9173
9174struct AdmittedDocumentRead<'a> {
9175    generation: u64,
9176    active: Arc<crate::ingest::ActiveSegment>,
9177    snapshot: snapshot::ReadSnapshot,
9178    active_query: ActiveQuery<'a>,
9179}
9180
9181struct AdmittedVectorSearch<'a> {
9182    execution_options: SearchOptions,
9183    pool: Option<Arc<pool::QueryPool>>,
9184    snapshot: snapshot::ReadSnapshot,
9185    active_segment: Arc<crate::ingest::ActiveSegment>,
9186    generation: u64,
9187    _active_query: Option<ActiveQuery<'a>>,
9188}
9189
9190struct ActiveQuery<'a> {
9191    count: &'a AtomicU64,
9192}
9193
9194impl Drop for ActiveQuery<'_> {
9195    fn drop(&mut self) {
9196        self.count.fetch_sub(1, Ordering::Relaxed);
9197    }
9198}
9199
9200impl Drop for Store {
9201    fn drop(&mut self) {
9202        self.close_best_effort();
9203    }
9204}
9205
9206#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9207struct DocumentScanRow {
9208    source: DocumentScanSource,
9209    row: u32,
9210    timestamp: i64,
9211    doc_id: crate::ingest::DocId,
9212    /// Order-preserving key of the [`ScanOrder::Attribute`] value, or `None`
9213    /// when the value is missing or NaN or the order is not by attribute.
9214    attribute_key: Option<u64>,
9215}
9216
9217fn validate_scan_order_attribute(
9218    schema: &crate::meta::Schema,
9219    column: crate::meta::ColumnId,
9220) -> Result<(), QueryError> {
9221    use crate::meta::ColumnType;
9222
9223    let id = column.get();
9224    let detail = if column == crate::meta::TIMESTAMP_COLUMN {
9225        Some(format!(
9226            "scan order attribute {id} is the document timestamp; use a timestamp order"
9227        ))
9228    } else {
9229        match schema
9230            .column(column)
9231            .map(crate::meta::ColumnDefinition::column_type)
9232        {
9233            None => Some(format!(
9234                "scan order attribute {id} is not a declared attribute"
9235            )),
9236            Some(ColumnType::U64 | ColumnType::I64 | ColumnType::F64) => None,
9237            Some(other) => Some(format!(
9238                "scan order attribute {id} has type {other:?}; only u64, i64 and f64 attributes are orderable"
9239            )),
9240        }
9241    };
9242    detail.map_or(Ok(()), |detail| {
9243        Err(QueryError::Store(StoreError::InvalidScan { detail }))
9244    })
9245}
9246
9247/// Maps one attribute value to a `u64` whose unsigned order is the value
9248/// order: `U64` unchanged, `I64` with the sign bit flipped, and `F64` by the
9249/// IEEE-754 total-order bit transform after folding `-0.0` into `+0.0`.
9250/// Missing and NaN values have no key.
9251fn attribute_order_key(column: &crate::meta::Column, row: u32) -> Result<Option<u64>, QueryError> {
9252    use crate::meta::Column;
9253    const SIGN: u64 = 1 << 63;
9254
9255    match column {
9256        Column::U64(values) => Ok(values.get(row)),
9257        Column::I64(values) => Ok(values.get(row).map(|value| value.cast_unsigned() ^ SIGN)),
9258        Column::F64(values) => Ok(values
9259            .get(row)
9260            .filter(|value| !value.is_nan())
9261            .map(|value| {
9262                // Adding +0.0 folds -0.0 into +0.0 and leaves every other
9263                // non-NaN value unchanged.
9264                let bits = (value + 0.0).to_bits();
9265                if bits & SIGN == 0 { bits | SIGN } else { !bits }
9266            })),
9267        Column::Id128(_) | Column::Bool(_) | Column::DictionaryString(_) | Column::RawString(_) => {
9268            Err(QueryError::Store(StoreError::InvalidScan {
9269                detail: "scan order attribute column is not numeric".to_owned(),
9270            }))
9271        }
9272    }
9273}
9274
9275fn keyed_scan_row(
9276    mut row: DocumentScanRow,
9277    columns: Option<(&crate::meta::ColumnStore, crate::meta::ColumnId)>,
9278) -> Result<DocumentScanRow, QueryError> {
9279    if let Some((columns, column)) = columns {
9280        let values = columns.column(column).ok_or_else(|| {
9281            QueryError::Store(StoreError::InvalidScan {
9282                detail: format!("scan order attribute {} has no stored column", column.get()),
9283            })
9284        })?;
9285        row.attribute_key = attribute_order_key(values, row.row)?;
9286    }
9287    Ok(row)
9288}
9289
9290fn document_scan_predicate(
9291    predicate: Option<&crate::meta::Predicate>,
9292    timestamp_range: Option<(i64, i64)>,
9293) -> Result<Option<crate::meta::Predicate>, QueryError> {
9294    use crate::meta::{Predicate, PredicateValue, RangeBound, RangePredicate, TIMESTAMP_COLUMN};
9295
9296    let timestamp = timestamp_range
9297        .map(|(start, end)| {
9298            if start > end {
9299                return Err(QueryError::Store(StoreError::InvalidScan {
9300                    detail: format!("timestamp range start {start} exceeds end {end}"),
9301                }));
9302            }
9303            Ok(Predicate::Range(RangePredicate {
9304                column: TIMESTAMP_COLUMN,
9305                lower: Some(RangeBound::inclusive(PredicateValue::I64(start))),
9306                upper: Some(RangeBound::exclusive(PredicateValue::I64(end))),
9307            }))
9308        })
9309        .transpose()?;
9310    Ok(match (predicate, timestamp) {
9311        (Some(predicate), Some(timestamp)) => {
9312            Some(Predicate::And(vec![predicate.clone(), timestamp]))
9313        }
9314        (Some(predicate), None) => Some(predicate.clone()),
9315        (None, Some(timestamp)) => Some(timestamp),
9316        (None, None) => None,
9317    })
9318}
9319
9320fn scan_control_checkpoint(
9321    control: &QueryControl,
9322    lease: &SnapshotLease,
9323    work: &mut usize,
9324) -> Result<(), QueryError> {
9325    *work = work.saturating_add(1);
9326    if (*work).is_multiple_of(64) {
9327        control.checkpoint()?;
9328        lease.check_active().map_err(QueryError::Store)?;
9329    }
9330    Ok(())
9331}
9332
9333fn active_scan_columns(
9334    active: &crate::ingest::ActiveSegment,
9335    schema: &crate::meta::Schema,
9336) -> Result<crate::meta::ColumnStore, QueryError> {
9337    let mut builder = crate::meta::ColumnStoreBuilder::new(schema.clone());
9338    for (row, timestamp) in active.timestamps().iter().copied().enumerate() {
9339        let values = active.column_values(row).map_err(QueryError::Store)?;
9340        builder
9341            .push_row(timestamp, &crate::ingest::column_inputs(&values))
9342            .map_err(|error| {
9343                QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Columns(
9344                    error.to_string(),
9345                )))
9346            })?;
9347    }
9348    builder.finish().map_err(|error| {
9349        QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Columns(
9350            error.to_string(),
9351        )))
9352    })
9353}
9354
9355fn sealed_scan_rows(
9356    segment: &crate::segment::reader::SegmentReader,
9357    predicate: Option<&crate::meta::Predicate>,
9358) -> Result<crate::meta::DocBitmap, QueryError> {
9359    if predicate.is_some_and(|predicate| {
9360        !crate::planner::segment_may_match(segment.meta().clustering_key_range, predicate)
9361    }) {
9362        return Ok(crate::meta::DocBitmap::new());
9363    }
9364    let alive = segment.query_alive().map_err(QueryError::Store)?;
9365    match predicate {
9366        Some(predicate) => {
9367            let columns = segment.query_columns().map_err(QueryError::Store)?;
9368            crate::meta::evaluate(predicate, &columns, &alive).map_err(|error| {
9369                QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Columns(
9370                    error.to_string(),
9371                )))
9372            })
9373        }
9374        None => Ok(alive.alive_bitmap().clone()),
9375    }
9376}
9377
9378fn active_scan_rows(
9379    active: &crate::ingest::ActiveSegment,
9380    schema: &crate::meta::Schema,
9381    predicate: Option<&crate::meta::Predicate>,
9382) -> Result<crate::meta::DocBitmap, QueryError> {
9383    let alive = active.alive().map_err(QueryError::Store)?;
9384    match predicate {
9385        Some(predicate) => {
9386            let columns = active_scan_columns(active, schema)?;
9387            crate::meta::evaluate(predicate, &columns, &alive).map_err(|error| {
9388                QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Columns(
9389                    error.to_string(),
9390                )))
9391            })
9392        }
9393        None => Ok(alive.alive_bitmap().clone()),
9394    }
9395}
9396
9397fn sealed_document_scan_row(
9398    segment: &crate::segment::reader::SegmentReader,
9399    row: u32,
9400) -> Result<DocumentScanRow, QueryError> {
9401    let version = segment
9402        .document_version(row as usize)
9403        .map_err(StoreError::Segment)
9404        .map_err(QueryError::Store)?
9405        .ok_or_else(|| {
9406            QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Geometry(
9407                format!("sealed document row {row} has no identity"),
9408            )))
9409        })?;
9410    let columns = segment.query_columns().map_err(QueryError::Store)?;
9411    let timestamp = columns.timestamp(row).ok_or_else(|| {
9412        QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Geometry(
9413            format!("sealed document row {row} has no timestamp"),
9414        )))
9415    })?;
9416    Ok(DocumentScanRow {
9417        source: DocumentScanSource::Sealed(segment.meta().id),
9418        row,
9419        timestamp,
9420        doc_id: version.doc_id(),
9421        attribute_key: None,
9422    })
9423}
9424
9425fn active_document_scan_row(
9426    active: &crate::ingest::ActiveSegment,
9427    row: u32,
9428) -> Result<DocumentScanRow, QueryError> {
9429    let index = row as usize;
9430    let version = active
9431        .document(index)
9432        .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
9433    let timestamp = active
9434        .timestamps()
9435        .get(index)
9436        .copied()
9437        .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
9438    Ok(DocumentScanRow {
9439        source: DocumentScanSource::Active,
9440        row,
9441        timestamp,
9442        doc_id: version.doc_id(),
9443        attribute_key: None,
9444    })
9445}
9446
9447#[allow(clippy::too_many_arguments)]
9448fn collect_storage_scan_rows(
9449    snapshot: &PublishedSnapshot,
9450    active: &crate::ingest::ActiveSegment,
9451    schema: &crate::meta::Schema,
9452    predicate: Option<&crate::meta::Predicate>,
9453    cursor: Option<DocumentScanCursor>,
9454    limit: usize,
9455    control: &QueryControl,
9456    lease: &SnapshotLease,
9457    work: &mut usize,
9458) -> Result<Vec<DocumentScanRow>, QueryError> {
9459    let capacity = limit.saturating_add(1);
9460    let mut result = Vec::new();
9461    result.try_reserve_exact(capacity).map_err(|_| {
9462        QueryError::Store(StoreError::AllocationFailed {
9463            needed: allocation_bytes::<DocumentScanRow>(capacity),
9464            component: "scan positions",
9465        })
9466    })?;
9467    let mut started = cursor.is_none();
9468    'segments: for segment in snapshot.segments() {
9469        let source = DocumentScanSource::Sealed(segment.meta().id);
9470        let rows = sealed_scan_rows(segment, predicate)?;
9471        for row in rows.iter() {
9472            scan_control_checkpoint(control, lease, work)?;
9473            if !started {
9474                started =
9475                    cursor.is_some_and(|cursor| cursor.source == source && cursor.next_row == row);
9476                if !started {
9477                    continue;
9478                }
9479            }
9480            result.push(sealed_document_scan_row(segment, row)?);
9481            if result.len() == capacity {
9482                break 'segments;
9483            }
9484        }
9485    }
9486    if result.len() < capacity {
9487        let source = DocumentScanSource::Active;
9488        let rows = active_scan_rows(active, schema, predicate)?;
9489        for row in rows.iter() {
9490            scan_control_checkpoint(control, lease, work)?;
9491            if !started {
9492                started =
9493                    cursor.is_some_and(|cursor| cursor.source == source && cursor.next_row == row);
9494                if !started {
9495                    continue;
9496                }
9497            }
9498            result.push(active_document_scan_row(active, row)?);
9499            if result.len() == capacity {
9500                break;
9501            }
9502        }
9503    }
9504    if !started {
9505        return Err(QueryError::Store(StoreError::InvalidScan {
9506            detail: "scan cursor does not name a matching row".to_owned(),
9507        }));
9508    }
9509    Ok(result)
9510}
9511
9512fn document_scan_row_cmp(
9513    left: &DocumentScanRow,
9514    right: &DocumentScanRow,
9515    order: ScanOrder,
9516) -> std::cmp::Ordering {
9517    let primary = match order {
9518        ScanOrder::Storage | ScanOrder::TimestampAscending => left.timestamp.cmp(&right.timestamp),
9519        ScanOrder::TimestampDescending => right.timestamp.cmp(&left.timestamp),
9520        ScanOrder::Attribute { direction, .. } => match (left.attribute_key, right.attribute_key) {
9521            (Some(left), Some(right)) => match direction {
9522                ScanDirection::Ascending => left.cmp(&right),
9523                ScanDirection::Descending => right.cmp(&left),
9524            },
9525            (Some(_), None) => std::cmp::Ordering::Less,
9526            (None, Some(_)) => std::cmp::Ordering::Greater,
9527            (None, None) => std::cmp::Ordering::Equal,
9528        },
9529    };
9530    primary
9531        .then_with(|| left.doc_id.cmp(&right.doc_id))
9532        .then_with(|| document_scan_source_cmp(left.source, right.source))
9533        .then_with(|| left.row.cmp(&right.row))
9534}
9535
9536fn document_scan_source_cmp(
9537    left: DocumentScanSource,
9538    right: DocumentScanSource,
9539) -> std::cmp::Ordering {
9540    match (left, right) {
9541        (DocumentScanSource::Sealed(left), DocumentScanSource::Sealed(right)) => left.cmp(&right),
9542        (DocumentScanSource::Sealed(_), DocumentScanSource::Active) => std::cmp::Ordering::Less,
9543        (DocumentScanSource::Active, DocumentScanSource::Sealed(_)) => std::cmp::Ordering::Greater,
9544        (DocumentScanSource::Active, DocumentScanSource::Active) => std::cmp::Ordering::Equal,
9545    }
9546}
9547
9548fn insert_timestamp_scan_row(
9549    rows: &mut Vec<DocumentScanRow>,
9550    candidate: DocumentScanRow,
9551    capacity: usize,
9552    order: ScanOrder,
9553) {
9554    let position = rows.partition_point(|row| {
9555        document_scan_row_cmp(row, &candidate, order) != std::cmp::Ordering::Greater
9556    });
9557    if position < capacity {
9558        rows.insert(position, candidate);
9559        if rows.len() > capacity {
9560            let _ = rows.pop();
9561        }
9562    }
9563}
9564
9565fn clustering_range_cmp(
9566    left: crate::segment::ClusteringKeyRange,
9567    right: crate::segment::ClusteringKeyRange,
9568    order: ScanOrder,
9569) -> std::cmp::Ordering {
9570    use crate::segment::ClusteringKeyRange::{Bounded, Empty, Unstamped};
9571
9572    match (left, right) {
9573        (Bounded { min_ts: left, .. }, Bounded { min_ts: right, .. })
9574            if order == ScanOrder::TimestampAscending =>
9575        {
9576            left.cmp(&right)
9577        }
9578        (Bounded { max_ts: left, .. }, Bounded { max_ts: right, .. }) => right.cmp(&left),
9579        (Unstamped, Unstamped) | (Empty, Empty) => std::cmp::Ordering::Equal,
9580        (Unstamped, _) => std::cmp::Ordering::Less,
9581        (_, Unstamped) => std::cmp::Ordering::Greater,
9582        (Empty, _) => std::cmp::Ordering::Greater,
9583        (_, Empty) => std::cmp::Ordering::Less,
9584    }
9585}
9586
9587/// Collects the first `limit + 1` rows at or after `cursor` under a timestamp
9588/// or attribute order; every row is compared, so segment order only affects
9589/// how early the bounded buffer fills.
9590#[allow(clippy::too_many_arguments)]
9591fn collect_sorted_scan_rows(
9592    snapshot: &PublishedSnapshot,
9593    active: &crate::ingest::ActiveSegment,
9594    schema: &crate::meta::Schema,
9595    predicate: Option<&crate::meta::Predicate>,
9596    cursor: Option<DocumentScanCursor>,
9597    limit: usize,
9598    order: ScanOrder,
9599    control: &QueryControl,
9600    lease: &SnapshotLease,
9601    work: &mut usize,
9602) -> Result<Vec<DocumentScanRow>, QueryError> {
9603    let capacity = limit.saturating_add(1);
9604    let mut result = Vec::new();
9605    result.try_reserve_exact(capacity).map_err(|_| {
9606        QueryError::Store(StoreError::AllocationFailed {
9607            needed: allocation_bytes::<DocumentScanRow>(capacity),
9608            component: "scan positions",
9609        })
9610    })?;
9611    let attribute = match order {
9612        ScanOrder::Attribute { column, .. } => Some(column),
9613        ScanOrder::Storage | ScanOrder::TimestampAscending | ScanOrder::TimestampDescending => None,
9614    };
9615    let active_columns = attribute
9616        .map(|_| active_scan_columns(active, schema))
9617        .transpose()?;
9618    let active_keys = active_columns.as_ref().zip(attribute);
9619    let sealed_columns = |segment: &crate::segment::reader::SegmentReader| {
9620        attribute
9621            .map(|_| segment.query_columns().map_err(QueryError::Store))
9622            .transpose()
9623    };
9624    let cursor_row = cursor
9625        .map(|cursor| match cursor.source {
9626            DocumentScanSource::Active => keyed_scan_row(
9627                active_document_scan_row(active, cursor.next_row)?,
9628                active_keys,
9629            ),
9630            DocumentScanSource::Sealed(segment_id) => {
9631                let segment = snapshot
9632                    .segments()
9633                    .iter()
9634                    .find(|segment| segment.meta().id == segment_id)
9635                    .ok_or_else(|| {
9636                        QueryError::Store(StoreError::InvalidScan {
9637                            detail: format!("scan cursor names absent segment {segment_id}"),
9638                        })
9639                    })?;
9640                let columns = sealed_columns(segment)?;
9641                keyed_scan_row(
9642                    sealed_document_scan_row(segment, cursor.next_row)?,
9643                    columns.as_deref().zip(attribute),
9644                )
9645            }
9646        })
9647        .transpose()?;
9648    let mut segments = snapshot.segments().iter().collect::<Vec<_>>();
9649    segments.sort_by(|left, right| {
9650        clustering_range_cmp(
9651            left.meta().clustering_key_range,
9652            right.meta().clustering_key_range,
9653            order,
9654        )
9655        .then_with(|| left.meta().id.cmp(&right.meta().id))
9656    });
9657    for segment in segments {
9658        let rows = sealed_scan_rows(segment, predicate)?;
9659        let columns = sealed_columns(segment)?;
9660        for row in rows.iter() {
9661            scan_control_checkpoint(control, lease, work)?;
9662            let candidate = keyed_scan_row(
9663                sealed_document_scan_row(segment, row)?,
9664                columns.as_deref().zip(attribute),
9665            )?;
9666            if cursor_row.as_ref().is_none_or(|cursor_row| {
9667                document_scan_row_cmp(&candidate, cursor_row, order) != std::cmp::Ordering::Less
9668            }) {
9669                insert_timestamp_scan_row(&mut result, candidate, capacity, order);
9670            }
9671        }
9672    }
9673    for row in active_scan_rows(active, schema, predicate)?.iter() {
9674        scan_control_checkpoint(control, lease, work)?;
9675        let candidate = keyed_scan_row(active_document_scan_row(active, row)?, active_keys)?;
9676        if cursor_row.as_ref().is_none_or(|cursor_row| {
9677            document_scan_row_cmp(&candidate, cursor_row, order) != std::cmp::Ordering::Less
9678        }) {
9679            insert_timestamp_scan_row(&mut result, candidate, capacity, order);
9680        }
9681    }
9682    if let Some(cursor) = cursor {
9683        let starts_at_cursor = result
9684            .first()
9685            .is_some_and(|row| row.source == cursor.source && row.row == cursor.next_row);
9686        if !starts_at_cursor {
9687            return Err(QueryError::Store(StoreError::InvalidScan {
9688                detail: "scan cursor does not name a matching row".to_owned(),
9689            }));
9690        }
9691    }
9692    Ok(result)
9693}
9694
9695fn materialize_active_document(
9696    active: &crate::ingest::ActiveSegment,
9697    row: usize,
9698    fields: DocumentFields,
9699) -> Result<StoredDocument, StoreError> {
9700    let version = active.document(row).ok_or(StoreError::ActiveRowOverflow)?;
9701    let timestamp = active
9702        .timestamps()
9703        .get(row)
9704        .copied()
9705        .ok_or(StoreError::ActiveRowOverflow)?;
9706    let vector = if fields.contains(DocumentFields::VECTOR) {
9707        let dimensions = active.dims().ok_or(StoreError::ActiveRowOverflow)?;
9708        let start = row
9709            .checked_mul(dimensions)
9710            .ok_or(StoreError::ActiveRowOverflow)?;
9711        let end = start
9712            .checked_add(dimensions)
9713            .ok_or(StoreError::ActiveRowOverflow)?;
9714        let values = active
9715            .vectors()
9716            .get(start..end)
9717            .ok_or(StoreError::ActiveRowOverflow)?;
9718        Some(copy_slice(values, "get vector")?)
9719    } else {
9720        None
9721    };
9722    let text = if fields.contains(DocumentFields::TEXT) {
9723        active
9724            .text(row)?
9725            .map(|value| copy_string(value, "get text"))
9726            .transpose()?
9727    } else {
9728        None
9729    };
9730    let metadata = if fields.contains(DocumentFields::METADATA) {
9731        active
9732            .metadata(row)
9733            .map(|value| copy_slice(value, "get metadata"))
9734            .transpose()?
9735    } else {
9736        None
9737    };
9738    let attributes = if fields.contains(DocumentFields::ATTRIBUTES) {
9739        Some(active.column_values(row)?)
9740    } else {
9741        None
9742    };
9743    Ok(StoredDocument {
9744        doc_id: version.doc_id(),
9745        revision: version.revision(),
9746        timestamp,
9747        vector,
9748        text,
9749        metadata,
9750        attributes,
9751    })
9752}
9753
9754fn materialize_sealed_document(
9755    segment: &crate::segment::reader::SegmentReader,
9756    row: usize,
9757    fields: DocumentFields,
9758) -> Result<StoredDocument, StoreError> {
9759    let row_u32 = u32::try_from(row).map_err(|_| {
9760        StoreError::Segment(crate::segment::SegmentError::Geometry(
9761            "sealed document row exceeds u32".to_owned(),
9762        ))
9763    })?;
9764    let version = segment
9765        .document_version(row)
9766        .map_err(StoreError::Segment)?
9767        .ok_or_else(|| {
9768            StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
9769                "sealed document row {row} has no identity"
9770            )))
9771        })?;
9772    let columns = segment.query_columns()?;
9773    let timestamp = columns.timestamp(row_u32).ok_or_else(|| {
9774        StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
9775            "sealed document row {row} has no timestamp"
9776        )))
9777    })?;
9778    let vector = if fields.contains(DocumentFields::VECTOR) {
9779        segment
9780            .validate_rescore_rows(&[row_u32])
9781            .map_err(StoreError::Segment)?;
9782        let dimensions = segment.meta().dims as usize;
9783        let start = row.checked_mul(dimensions).ok_or_else(|| {
9784            StoreError::Segment(crate::segment::SegmentError::Geometry(
9785                "sealed vector row offset overflow".to_owned(),
9786            ))
9787        })?;
9788        let end = start.checked_add(dimensions).ok_or_else(|| {
9789            StoreError::Segment(crate::segment::SegmentError::Geometry(
9790                "sealed vector row end overflow".to_owned(),
9791            ))
9792        })?;
9793        let rescore = segment.query_rescore_f32().map_err(StoreError::Segment)?;
9794        let values = rescore.get(start..end).ok_or_else(|| {
9795            StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
9796                "sealed vector row {row} is missing"
9797            )))
9798        })?;
9799        Some(copy_slice(values, "get vector")?)
9800    } else {
9801        None
9802    };
9803    let text = if fields.contains(DocumentFields::TEXT) {
9804        match segment.query_stored_text().map_err(StoreError::Segment)? {
9805            Some(rows) => rows
9806                .row(row)
9807                .ok_or_else(|| {
9808                    StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
9809                        "stored-text row {row} is invalid"
9810                    )))
9811                })?
9812                .map(|value| copy_string(value, "get text"))
9813                .transpose()?,
9814            None => None,
9815        }
9816    } else {
9817        None
9818    };
9819    let metadata = if fields.contains(DocumentFields::METADATA) {
9820        match segment
9821            .query_stored_metadata()
9822            .map_err(StoreError::Segment)?
9823        {
9824            Some(rows) => Some(copy_slice(
9825                rows.row(row).ok_or_else(|| {
9826                    StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
9827                        "stored-metadata row {row} is invalid"
9828                    )))
9829                })?,
9830                "get metadata",
9831            )?),
9832            None => None,
9833        }
9834    } else {
9835        None
9836    };
9837    let attributes = fields
9838        .contains(DocumentFields::ATTRIBUTES)
9839        .then(|| materialize_sealed_attributes(&columns, row_u32))
9840        .transpose()?;
9841    Ok(StoredDocument {
9842        doc_id: version.doc_id(),
9843        revision: version.revision(),
9844        timestamp,
9845        vector,
9846        text,
9847        metadata,
9848        attributes,
9849    })
9850}
9851
9852fn materialize_sealed_attributes(
9853    columns: &crate::meta::ColumnStore,
9854    row: u32,
9855) -> Result<Vec<(crate::meta::ColumnId, crate::meta::PredicateValue)>, StoreError> {
9856    let mut attributes = Vec::new();
9857    attributes
9858        .try_reserve_exact(columns.schema().user_column_count())
9859        .map_err(|_| StoreError::AllocationFailed {
9860            needed: allocation_bytes::<(crate::meta::ColumnId, crate::meta::PredicateValue)>(
9861                columns.schema().user_column_count(),
9862            ),
9863            component: "get attributes",
9864        })?;
9865    for definition in columns
9866        .schema()
9867        .columns()
9868        .iter()
9869        .filter(|definition| definition.id() != crate::meta::TIMESTAMP_COLUMN)
9870    {
9871        let column = columns.column(definition.id()).ok_or_else(|| {
9872            StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
9873                "sealed attribute column {} is missing",
9874                definition.id().get()
9875            )))
9876        })?;
9877        let value = predicate_value_for_row(column, row)?;
9878        if value.is_none() && !definition.is_nullable() {
9879            return Err(StoreError::Segment(crate::segment::SegmentError::Geometry(
9880                format!(
9881                    "sealed required attribute {} is null at row {row}",
9882                    definition.id().get()
9883                ),
9884            )));
9885        }
9886        if let Some(value) = value {
9887            attributes.push((definition.id(), value));
9888        }
9889    }
9890    Ok(attributes)
9891}
9892
9893fn predicate_value_for_row(
9894    column: &crate::meta::Column,
9895    row: u32,
9896) -> Result<Option<crate::meta::PredicateValue>, StoreError> {
9897    use crate::meta::{Column, PredicateValue};
9898
9899    match column {
9900        Column::Id128(values) => Ok(values.get(row).map(PredicateValue::Id128)),
9901        Column::U64(values) => Ok(values.get(row).map(PredicateValue::U64)),
9902        Column::I64(values) => Ok(values.get(row).map(PredicateValue::I64)),
9903        Column::F64(values) => Ok(values.get(row).map(PredicateValue::F64)),
9904        Column::Bool(values) => Ok(values.get(row).map(PredicateValue::Bool)),
9905        Column::DictionaryString(values) => values
9906            .get(row)
9907            .map(|value| copy_string(value, "get attribute string").map(PredicateValue::String))
9908            .transpose(),
9909        Column::RawString(values) => values
9910            .get(row)
9911            .map(|value| copy_string(value, "get attribute string").map(PredicateValue::String))
9912            .transpose(),
9913    }
9914}
9915
9916fn copy_slice<T: Copy>(values: &[T], component: &'static str) -> Result<Vec<T>, StoreError> {
9917    let mut copied = Vec::new();
9918    copied
9919        .try_reserve_exact(values.len())
9920        .map_err(|_| StoreError::AllocationFailed {
9921            needed: allocation_bytes::<T>(values.len()),
9922            component,
9923        })?;
9924    copied.extend_from_slice(values);
9925    Ok(copied)
9926}
9927
9928fn copy_string(value: &str, component: &'static str) -> Result<String, StoreError> {
9929    let mut copied = String::new();
9930    copied
9931        .try_reserve_exact(value.len())
9932        .map_err(|_| StoreError::AllocationFailed {
9933            needed: u64::try_from(value.len()).unwrap_or(u64::MAX),
9934            component,
9935        })?;
9936    copied.push_str(value);
9937    Ok(copied)
9938}
9939
9940fn allocation_bytes<T>(count: usize) -> u64 {
9941    count
9942        .checked_mul(std::mem::size_of::<T>())
9943        .and_then(|bytes| u64::try_from(bytes).ok())
9944        .unwrap_or(u64::MAX)
9945}
9946
9947#[cfg(test)]
9948mod lexical_assembly_tests;
9949
9950#[cfg(test)]
9951mod get_tests;
9952#[cfg(test)]
9953mod group_count_tests;
9954#[cfg(test)]
9955mod scan_attribute_order_tests;
9956#[cfg(test)]
9957mod scan_document_tests;
9958
9959#[cfg(test)]
9960#[allow(clippy::expect_used, clippy::panic)]
9961mod tests {
9962    use std::error::Error;
9963    use std::sync::Arc;
9964
9965    use tempfile::tempdir;
9966
9967    use crate::lifecycle::durability::DurabilityPolicy;
9968    use crate::manifest::io::commit_manifest;
9969    use crate::manifest::{Manifest, ManifestError};
9970    use crate::meta::Schema;
9971    use crate::vfs::StdVfs;
9972    #[cfg(feature = "graph-cypher")]
9973    use crate::vfs::Vfs;
9974    use crate::vfs::crash::MemoryVfs;
9975
9976    use super::durability::{CommitTier, DurabilityMode, DurabilityPolicyError};
9977    use super::{
9978        CancelToken, GraphSearchOptions, ManualMonotonicClock, OpenOptions, QueryControl,
9979        SearchOptions, SearchTier, Store, StoreError, StoreTestDependencies,
9980        resolve_hybrid_leg_results,
9981    };
9982
9983    #[cfg(feature = "graph-cypher")]
9984    #[test]
9985    fn graph_free_initial_manifest_bytes_survive_a_failed_writer_open_as_on_main() {
9986        use super::native_graph::tests::publication::{FaultPoint, RecordingVfs};
9987        let directory = tempfile::tempdir().expect("fixture");
9988        let schema = crate::meta::Schema::new(vec![crate::meta::ColumnDefinition::new(
9989            crate::meta::ColumnId::new(1),
9990            "extra",
9991            crate::meta::ColumnType::U64,
9992            true,
9993        )])
9994        .expect("fixture");
9995        let vfs = Arc::new(RecordingVfs::default());
9996        vfs.arm_fault(FaultPoint::OpenAppend);
9997        assert!(
9998            Store::open_with_test_dependencies(
9999                directory.path(),
10000                OpenOptions::default().with_schema(schema.clone()),
10001                StoreTestDependencies::new(vfs.clone(), Arc::new(super::SystemMonotonicClock)),
10002            )
10003            .is_err()
10004        );
10005        vfs.assert_fired_once();
10006        let expected = crate::manifest::encode_manifest(&crate::manifest::Manifest {
10007            graph: None,
10008            generation: 0,
10009            log_seq: 0,
10010            segments: Vec::new(),
10011            epochs: Vec::new(),
10012            epoch_alias: None,
10013            schema,
10014        })
10015        .expect("fixture");
10016        assert_eq!(
10017            StdVfs
10018                .read(&directory.path().join("manifest.ze"))
10019                .expect("main publishes the initial manifest before opening the writer"),
10020            expected
10021        );
10022    }
10023
10024    #[cfg(feature = "graph-cypher")]
10025    #[test]
10026    fn graph_free_schema_evolution_bytes_survive_a_failed_writer_open_as_on_main() {
10027        use super::native_graph::tests::publication::{FaultPoint, RecordingVfs};
10028        let directory = tempdir().expect("directory");
10029        let store = Store::open(directory.path(), OpenOptions::default()).expect("store");
10030        store
10031            .ingest(crate::ingest::IngestBatch::new(vec![
10032                crate::ingest::IngestDocument::new(
10033                    crate::ingest::DocumentVersion::new(
10034                        crate::ingest::DocId::new(91),
10035                        crate::ingest::Revision::new(1),
10036                    ),
10037                    vec![1.0],
10038                ),
10039            ]))
10040            .expect("document");
10041        store.seal().expect("seal");
10042        store.close().expect("close");
10043        let schema = Schema::new(vec![crate::meta::ColumnDefinition::new(
10044            crate::meta::ColumnId::new(1),
10045            "extra",
10046            crate::meta::ColumnType::U64,
10047            true,
10048        )])
10049        .expect("schema");
10050        let mut expected = crate::manifest::io::load_manifest(
10051            &StdVfs,
10052            &directory.path().join("manifest.ze"),
10053            u64::MAX,
10054        )
10055        .expect("prior manifest");
10056        expected.generation += 1;
10057        expected.schema = schema.clone();
10058        let snapshot = || {
10059            std::fs::read_dir(directory.path())
10060                .expect("files")
10061                .map(|entry| {
10062                    let entry = entry.expect("entry");
10063                    (
10064                        entry.file_name(),
10065                        StdVfs.read(&entry.path()).expect("bytes"),
10066                    )
10067                })
10068                .collect::<std::collections::BTreeMap<_, _>>()
10069        };
10070        let mut expected_files = snapshot();
10071        expected_files.insert(
10072            std::ffi::OsString::from("manifest.ze"),
10073            crate::manifest::encode_manifest(&expected).expect("main bytes"),
10074        );
10075        let vfs = Arc::new(RecordingVfs::default());
10076        vfs.arm_fault(FaultPoint::OpenAppend);
10077        assert!(
10078            Store::open_with_test_dependencies(
10079                directory.path(),
10080                OpenOptions::default().with_schema(schema),
10081                StoreTestDependencies::new(vfs.clone(), Arc::new(super::SystemMonotonicClock)),
10082            )
10083            .is_err()
10084        );
10085        vfs.assert_fired_once();
10086        assert_eq!(
10087            crate::manifest::io::load_manifest(
10088                &StdVfs,
10089                &directory.path().join("manifest.ze"),
10090                u64::MAX
10091            )
10092            .expect("published schema")
10093            .generation,
10094            expected.generation,
10095            "main commits schema before opening the writer"
10096        );
10097        assert_eq!(snapshot(), expected_files);
10098    }
10099
10100    #[cfg(feature = "graph-cypher")]
10101    #[test]
10102    pub(super) fn replacement_snapshot_backup_reopens_with_nonzero_watermarks() {
10103        use crate::ingest::{DocId, DocumentVersion, IngestBatch, IngestDocument, Revision};
10104        use crate::meta::{AliveSet, ColumnStoreBuilder, Schema};
10105        let directory = tempdir().expect("source");
10106        let parent = tempdir().expect("backup parent");
10107        let options =
10108            OpenOptions::new().with_durability(DurabilityMode::Durable, CommitTier::Durable);
10109        let store = Store::open(directory.path(), options.clone()).expect("open source");
10110        assert_eq!(store.enable_graph().expect("enable graph"), 1);
10111        let document = |id| {
10112            IngestDocument::new(
10113                DocumentVersion::new(DocId::new(id), Revision::new(1)),
10114                vec![1.0, 0.0],
10115            )
10116        };
10117        let ack = store
10118            .ingest(IngestBatch::new(vec![document(1)]))
10119            .expect("ingest");
10120        assert_eq!(ack.seq().get(), 1);
10121        assert_eq!(store.seal().expect("seal"), 3);
10122        let columns = ColumnStoreBuilder::new(Schema::timestamp_only())
10123            .finish()
10124            .expect("columns");
10125        let alive = AliveSet::new(0);
10126        let prepared = store
10127            .prepare_segment(super::InMemorySegment {
10128                id: crate::segment::SegmentId::new(1, [2; 10]),
10129                scheme: 4,
10130                dims: 2,
10131                codes: Vec::new(),
10132                factors: super::InMemorySegmentFactors::Bit4(Vec::new()),
10133                rescore: Vec::new(),
10134                columns: &columns,
10135                alive: &alive,
10136            })
10137            .expect("prepare empty replacement");
10138        assert_eq!(store.seal_snapshot(prepared).expect("replace snapshot"), 4);
10139        let target = parent.path().join("backup");
10140        assert_eq!(
10141            store.write_snapshot(&target).expect("acknowledge backup"),
10142            4
10143        );
10144        // No crash or source close: a successfully acknowledged backup must open.
10145        for access in [super::AccessMode::ReadOnly, super::AccessMode::ReadWrite] {
10146            let backup = Store::open(&target, options.clone().with_access_mode(access))
10147                .expect("acknowledged replacement backup must open");
10148            assert_eq!(backup.snapshot().expect("snapshot").generation(), 4);
10149            assert_eq!(backup.count_documents(None, None).expect("count").count, 0);
10150            assert!(backup.admit_native_read().is_ok());
10151            backup.close().expect("close backup");
10152        }
10153        let manifest = crate::manifest::io::load_manifest(&StdVfs, &target.join("manifest.ze"), 1)
10154            .expect("backup manifest");
10155        assert_eq!(manifest.log_seq, 1);
10156        assert_eq!(
10157            manifest
10158                .graph
10159                .expect("graph section")
10160                .graph_absorbed_through,
10161            1
10162        );
10163        let backup = Store::open(&target, options.clone()).expect("writable backup");
10164        let ack = backup
10165            .ingest(IngestBatch::new(vec![document(2)]))
10166            .expect("continue backup WAL");
10167        assert_eq!(ack.seq().get(), 2);
10168        assert_eq!(ack.generation(), 5);
10169        backup.close().expect("close continued backup");
10170        store.close().expect("close source");
10171        let source = Store::open(directory.path(), options).expect("reopen replacement source");
10172        assert_eq!(
10173            source
10174                .count_documents(None, None)
10175                .expect("source count")
10176                .count,
10177            0
10178        );
10179        assert_eq!(source.snapshot().expect("source snapshot").generation(), 4);
10180    }
10181
10182    #[cfg(feature = "graph-cypher")]
10183    #[test]
10184    fn enable_graph_retry_completes_a_failed_install() {
10185        enable_graph_retry_completes(
10186            super::native_graph::tests::publication::FaultPoint::Enumeration,
10187        );
10188    }
10189
10190    #[cfg(feature = "graph-cypher")]
10191    #[test]
10192    fn enable_graph_retry_completes_a_failed_directory_sync() {
10193        enable_graph_retry_completes(
10194            super::native_graph::tests::publication::FaultPoint::SelectorSync,
10195        );
10196    }
10197
10198    #[cfg(feature = "graph-cypher")]
10199    #[test]
10200    fn enable_graph_retry_survives_a_failed_writer_reopen() {
10201        enable_graph_retry_with_writer_reopen_failure(
10202            super::native_graph::tests::publication::FaultPoint::Enumeration,
10203            true,
10204        );
10205    }
10206
10207    #[cfg(feature = "graph-cypher")]
10208    pub(super) fn enable_graph_retry_completes(
10209        point: super::native_graph::tests::publication::FaultPoint,
10210    ) {
10211        enable_graph_retry_with_writer_reopen_failure(point, false)
10212    }
10213
10214    #[cfg(feature = "graph-cypher")]
10215    fn enable_graph_retry_with_writer_reopen_failure(
10216        point: super::native_graph::tests::publication::FaultPoint,
10217        fail_writer_reopen: bool,
10218    ) {
10219        use super::native_graph::tests::publication::{DurabilityEvent, RecordingVfs};
10220        let directory = tempdir().expect("directory");
10221        let vfs = Arc::new(RecordingVfs::default());
10222        let store = Store::open_with_test_dependencies(
10223            directory.path(),
10224            OpenOptions::new().with_max_resident_bytes(256 * 1024 * 1024),
10225            StoreTestDependencies::new(vfs.clone(), Arc::new(super::SystemMonotonicClock)),
10226        )
10227        .expect("open");
10228        let wal = StdVfs.read(&directory.path().join("wal.ze")).expect("WAL");
10229        vfs.arm_fault(point);
10230        assert!(store.enable_graph().is_err());
10231        vfs.assert_fired_once();
10232        assert!(
10233            store
10234                .ingest(crate::ingest::IngestBatch::new(vec![
10235                    crate::ingest::IngestDocument::new(
10236                        crate::ingest::DocumentVersion::new(
10237                            crate::ingest::DocId::new(99),
10238                            crate::ingest::Revision::new(1)
10239                        ),
10240                        vec![1.0, 0.0],
10241                    )
10242                ]))
10243                .is_err(),
10244            "failed enable must fence document writes until the retry completes"
10245        );
10246        if fail_writer_reopen {
10247            vfs.arm_fault(super::native_graph::tests::publication::FaultPoint::OpenAppend);
10248            assert!(store.enable_graph().is_err());
10249            vfs.assert_fired_once();
10250        }
10251        vfs.clear_events();
10252        assert_eq!(store.enable_graph().expect("retry completes upgrade"), 1);
10253        assert_eq!(store.snapshot().expect("snapshot").generation(), 1);
10254        assert!(
10255            store.admit_native_read().is_ok(),
10256            "retry must install the bundle"
10257        );
10258        assert!(
10259            vfs.take().iter().any(|event| matches!(event,
10260                DurabilityEvent::Sync(path, crate::vfs::SyncKind::Full) if path == directory.path()
10261            )),
10262            "retry must complete the directory durability barrier"
10263        );
10264        assert_eq!(
10265            StdVfs.read(&directory.path().join("wal.ze")).expect("WAL"),
10266            wal
10267        );
10268        assert_eq!(store.enable_graph().expect("idempotent"), 1);
10269        let ack = store
10270            .ingest(crate::ingest::IngestBatch::new(vec![
10271                crate::ingest::IngestDocument::new(
10272                    crate::ingest::DocumentVersion::new(
10273                        crate::ingest::DocId::new(91),
10274                        crate::ingest::Revision::new(1),
10275                    ),
10276                    vec![1.0, 0.0],
10277                ),
10278            ]))
10279            .expect("document writer repaired");
10280        assert_eq!(ack.generation(), 2);
10281        drop(store);
10282        let reopened = Store::open(
10283            directory.path(),
10284            OpenOptions::new().with_max_resident_bytes(256 * 1024 * 1024),
10285        )
10286        .expect("reopen");
10287        assert_eq!(reopened.snapshot().expect("snapshot").generation(), 2);
10288        assert!(reopened.admit_native_read().is_ok());
10289    }
10290
10291    #[test]
10292    fn astra_12_vocabulary_charge_survives_eviction_and_close() {
10293        use crate::fts::{index::DEFAULT_FIELD, query::LexicalQuery};
10294        use crate::ingest::{DocId, DocumentVersion, IngestBatch, IngestDocument, Revision};
10295        let directory = tempdir().expect("directory");
10296        let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
10297        store
10298            .ingest(IngestBatch::new(vec![
10299                IngestDocument::new(
10300                    DocumentVersion::new(DocId::new(1), Revision::new(1)),
10301                    vec![1.0, 0.0],
10302                )
10303                .with_text("alpha alphabet beta"),
10304            ]))
10305            .expect("ingest");
10306        store
10307            .search_lexical_structured(
10308                &LexicalQuery::prefix(b"alph".to_vec(), DEFAULT_FIELD),
10309                1,
10310                64,
10311                QueryControl::Cancel(CancelToken::new()),
10312            )
10313            .expect("build cache");
10314        let dictionary = {
10315            let entry = store.lexical_index_cache.entry.lock().expect("assembly");
10316            let vocabulary = entry
10317                .as_ref()
10318                .expect("assembly")
10319                .assembly
10320                .vocabulary
10321                .lock()
10322                .expect("vocabulary");
10323            Arc::clone(vocabulary.as_ref().expect("cached"))
10324        };
10325        let bytes = dictionary._memory.bytes();
10326        assert!(bytes > 17);
10327        let second_owner = Arc::clone(&dictionary);
10328        drop(
10329            store
10330                .lexical_index_cache
10331                .entry
10332                .lock()
10333                .expect("evict")
10334                .take(),
10335        );
10336        assert_eq!(store.stats().expect("held bytes").cache_bytes, bytes);
10337        store
10338            .search_lexical_structured(
10339                &LexicalQuery::prefix(b"alph".to_vec(), DEFAULT_FIELD),
10340                1,
10341                64,
10342                QueryControl::Cancel(CancelToken::new()),
10343            )
10344            .expect("replacement cache");
10345        assert!(store.stats().expect("both dictionaries").cache_bytes > bytes);
10346        store.close().expect("close");
10347        assert_eq!(
10348            store
10349                .accounting
10350                .audit()
10351                .expect("closed accounting")
10352                .cache_bytes,
10353            bytes
10354        );
10355        drop(dictionary);
10356        assert_eq!(
10357            store.accounting.audit().expect("second owner").cache_bytes,
10358            bytes
10359        );
10360        assert_eq!(
10361            second_owner.view.iter().collect::<Vec<_>>(),
10362            vec![b"alpha".as_slice(), b"alphabet", b"beta"]
10363        );
10364        drop(second_owner);
10365        assert_eq!(store.accounting.audit().expect("last owner").cache_bytes, 0);
10366        assert_eq!(
10367            store.accounting.audit().expect("scratch").temporary_bytes,
10368            0
10369        );
10370    }
10371
10372    #[test]
10373    fn astra_12_refused_vocabulary_is_not_published() {
10374        use crate::fts::{index::DEFAULT_FIELD, query::LexicalQuery};
10375        use crate::ingest::{
10376            DocId, DocumentVersion, IngestBatch, IngestDocument, Revision, StoreLexicalError,
10377        };
10378        let directory = tempdir().expect("directory");
10379        let writer = Store::open(directory.path(), OpenOptions::default()).expect("open");
10380        writer
10381            .ingest(IngestBatch::new(vec![
10382                IngestDocument::new(
10383                    DocumentVersion::new(DocId::new(1), Revision::new(1)),
10384                    vec![1.0, 0.0],
10385                )
10386                .with_text("alpha alphabet beta"),
10387            ]))
10388            .expect("ingest");
10389        writer.seal().expect("seal");
10390        writer.close().expect("close writer");
10391        let store = Store::open(
10392            directory.path(),
10393            OpenOptions::default().with_max_temp_bytes(1),
10394        )
10395        .expect("small budget");
10396        for _ in 0..2 {
10397            let result = store.search_lexical_structured(
10398                &LexicalQuery::prefix(b"alph".to_vec(), DEFAULT_FIELD),
10399                1,
10400                64,
10401                QueryControl::Cancel(CancelToken::new()),
10402            );
10403            assert!(
10404                matches!(
10405                    result,
10406                    Err(StoreLexicalError::Query(super::QueryError::Store(
10407                        StoreError::BudgetExceeded {
10408                            component: "temporary",
10409                            ..
10410                        }
10411                    )))
10412                ),
10413                "{result:?}"
10414            );
10415            assert_eq!(store.stats().expect("released").temporary_bytes, 0);
10416            let entry = store.lexical_index_cache.entry.lock().expect("assembly");
10417            assert!(
10418                entry
10419                    .as_ref()
10420                    .expect("assembled")
10421                    .assembly
10422                    .vocabulary
10423                    .lock()
10424                    .expect("dictionary")
10425                    .is_none()
10426            );
10427        }
10428        store.close().expect("close");
10429        assert_eq!(
10430            store
10431                .accounting
10432                .audit()
10433                .expect("released cache")
10434                .cache_bytes,
10435            0
10436        );
10437    }
10438
10439    #[test]
10440    fn astra_03_lexical_assembly_charge_survives_cache_eviction() {
10441        use crate::fts::index::DEFAULT_FIELD;
10442        use crate::fts::search::TermQuery;
10443        use crate::ingest::{DocId, DocumentVersion, IngestBatch, IngestDocument, Revision};
10444        let directory = tempdir().expect("directory");
10445        let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
10446        store
10447            .ingest(IngestBatch::new(vec![
10448                IngestDocument::new(
10449                    DocumentVersion::new(DocId::new(1), Revision::new(1)),
10450                    vec![1.0, 0.0],
10451                )
10452                .with_text("alpha beta"),
10453            ]))
10454            .expect("ingest");
10455        store
10456            .search_lexical(
10457                &TermQuery::flat(vec![b"alpha".to_vec()], &[DEFAULT_FIELD]),
10458                1,
10459                QueryControl::Cancel(CancelToken::new()),
10460            )
10461            .expect("build lexical cache");
10462        let (first, second) = {
10463            let entry = store.lexical_index_cache.entry.lock().expect("cache lock");
10464            let assembly = &entry.as_ref().expect("cached assembly").assembly;
10465            (Arc::clone(assembly), Arc::clone(assembly))
10466        };
10467        let charged = store.stats().expect("charged stats").cache_bytes;
10468        let evicted = store
10469            .lexical_index_cache
10470            .entry
10471            .lock()
10472            .expect("cache lock")
10473            .take();
10474        drop(evicted);
10475        assert_eq!(
10476            first.index.document_count(),
10477            1,
10478            "eviction left the query data alive"
10479        );
10480        assert_eq!(
10481            store.stats().expect("held stats").cache_bytes,
10482            charged,
10483            "cache eviction must not release a query-owned assembly's charge"
10484        );
10485        drop(first);
10486        assert_eq!(
10487            store.stats().expect("second owner stats").cache_bytes,
10488            charged
10489        );
10490        drop(second);
10491        let released = store.stats().expect("released stats").cache_bytes;
10492        assert_eq!(
10493            released, 0,
10494            "the final owner must release the full assembly charge"
10495        );
10496        println!("assembly cache bytes: retained={charged}, after final owner={released}");
10497        store.close().expect("close");
10498    }
10499
10500    /// Vectors whose norms differ enough that the ceiling is not degenerate.
10501    const CEILING_ROWS: [[f32; 4]; 3] = [
10502        [0.5, -0.25, 0.75, 0.125],
10503        [3.0, 1.5, -2.5, 0.25],
10504        [0.0, 0.0, 0.0, 0.0],
10505    ];
10506
10507    /// Publishes a scheme-0 segment that no seal path can produce.
10508    fn publish_f32_segment(directory: &std::path::Path) {
10509        let created = Store::open(directory, OpenOptions::default()).expect("create f32 store");
10510        created.close().expect("close f32 store");
10511
10512        let schema = Schema::timestamp_only();
10513        let mut columns = crate::meta::ColumnStoreBuilder::new(schema.clone());
10514        for row in 0..CEILING_ROWS.len() {
10515            columns
10516                .push_row(i64::try_from(row).expect("row timestamp"), &[])
10517                .expect("timestamp row");
10518        }
10519        let columns = columns.finish().expect("columns");
10520        let alive =
10521            crate::meta::AliveSet::new(u32::try_from(CEILING_ROWS.len()).expect("row count"));
10522        let rescore = CEILING_ROWS.iter().flatten().copied().collect::<Vec<f32>>();
10523        let codes = rescore
10524            .iter()
10525            .flat_map(|value| value.to_bits().to_le_bytes())
10526            .collect::<Vec<u8>>();
10527        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
10528            .expect("f32 fixture durability");
10529        let meta = crate::segment::writer::write_segment(
10530            &StdVfs,
10531            directory,
10532            crate::segment::writer::SegmentBuild {
10533                id: crate::segment::SegmentId::new(11, [7; 10]),
10534                scheme: 0,
10535                dims: 4,
10536                codes: &codes,
10537                factors: crate::segment::writer::SegmentFactors::F32,
10538                rescore: &rescore,
10539                columns: &columns,
10540                alive: &alive,
10541            },
10542            policy,
10543        )
10544        .expect("write scheme-0 segment");
10545        commit_manifest(
10546            &StdVfs,
10547            directory,
10548            &Manifest {
10549                #[cfg(feature = "graph-cypher")]
10550                graph: None,
10551                generation: 1,
10552                log_seq: 0,
10553                segments: vec![meta],
10554                epochs: Vec::new(),
10555                epoch_alias: None,
10556                schema,
10557            },
10558            policy,
10559        )
10560        .expect("publish scheme-0 segment");
10561    }
10562
10563    /// Ingests `CEILING_ROWS`, sealing every row except an optional tail that
10564    /// stays in the mutable active segment.
10565    fn ingest_ceiling_rows(store: &Store, keep_active: usize, one_segment_per_row: bool) {
10566        use crate::ingest::{DocId, DocumentVersion, IngestBatch, IngestDocument, Revision};
10567
10568        let sealed = CEILING_ROWS.len() - keep_active;
10569        for (index, row) in CEILING_ROWS.iter().enumerate() {
10570            let version = DocumentVersion::new(
10571                DocId::new(u128::try_from(index).expect("document id")),
10572                Revision::new(1),
10573            );
10574            store
10575                .ingest(IngestBatch::new(vec![IngestDocument::new(
10576                    version,
10577                    row.to_vec(),
10578                )]))
10579                .expect("ingest ceiling row");
10580            let seal_here = if one_segment_per_row {
10581                index < sealed
10582            } else {
10583                index + 1 == sealed
10584            };
10585            if seal_here {
10586                store.seal().expect("seal ceiling rows");
10587            }
10588        }
10589    }
10590
10591    /// Proves the remembered per-segment enclosure reproduces the ceiling
10592    /// exactly, for one store, over several queries.
10593    fn assert_remembered_ceiling_is_exact(store: &Store, expected_segments: usize) {
10594        let queries: [Vec<f32>; 3] = [
10595            vec![0.0, 0.0, 0.0, 0.0],
10596            vec![1.0, -1.0, 0.5, 2.0],
10597            vec![7.5, 0.25, -3.5, 0.125],
10598        ];
10599        let admitted = store
10600            .admit_vector_search(SearchOptions::default())
10601            .expect("admit ceiling query");
10602        let snapshot = &admitted.snapshot;
10603        let active = &admitted.active_segment;
10604        assert_eq!(snapshot.segments().len(), expected_segments);
10605        for segment in snapshot.segments() {
10606            assert!(
10607                segment.cached_vector_ceiling_norm_range().is_none(),
10608                "a segment remembered an enclosure before any query walked it"
10609            );
10610        }
10611
10612        for query in &queries {
10613            let cold = super::exact_vector_ceiling(snapshot, active, query).expect("cold ceiling");
10614            for segment in snapshot.segments() {
10615                let remembered = segment
10616                    .cached_vector_ceiling_norm_range()
10617                    .expect("a walked segment must remember its enclosure");
10618                let recomputed = super::compute_segment_vector_ceiling_norm_range(segment)
10619                    .expect("recomputed enclosure");
10620                assert_eq!(
10621                    remembered.endpoint_bits(),
10622                    recomputed.endpoint_bits(),
10623                    "remembered enclosure differs from the recomputed one"
10624                );
10625            }
10626            let warm = super::exact_vector_ceiling(snapshot, active, query).expect("warm ceiling");
10627            let recomputed = super::fold_exact_vector_ceiling(
10628                snapshot,
10629                active,
10630                query,
10631                super::compute_segment_vector_ceiling_norm_range,
10632            )
10633            .expect("recomputed ceiling");
10634            assert_eq!(
10635                cold.to_bits(),
10636                warm.to_bits(),
10637                "the remembered ceiling changed between queries"
10638            );
10639            assert_eq!(
10640                cold.to_bits(),
10641                recomputed.to_bits(),
10642                "the remembered ceiling is not the recomputed ceiling"
10643            );
10644        }
10645    }
10646
10647    #[test]
10648    fn astra_00_elapsed_includes_ceiling_work() {
10649        use std::time::{Duration, Instant};
10650        struct CeilingClock {
10651            base: Instant,
10652            snapshot: Arc<super::PublishedSnapshot>,
10653        }
10654        impl super::MonotonicClock for CeilingClock {
10655            fn now(&self) -> Instant {
10656                // Advance exactly when the production ceiling has completed
10657                // its immutable enclosure. No sleeping or CPU timing oracle.
10658                if self
10659                    .snapshot
10660                    .segments()
10661                    .iter()
10662                    .all(|segment| segment.cached_vector_ceiling_norm_range().is_some())
10663                {
10664                    self.base + Duration::from_secs(7)
10665                } else {
10666                    self.base
10667                }
10668            }
10669        }
10670        let directory = tempdir().expect("ceiling timer store");
10671        let mut store = Store::open(directory.path(), OpenOptions::default()).expect("open");
10672        ingest_ceiling_rows(&store, 0, false);
10673        let admitted = store
10674            .admit_vector_search(SearchOptions::default())
10675            .expect("snapshot");
10676        assert_eq!(admitted.snapshot.segments().len(), 1);
10677        assert!(
10678            admitted
10679                .snapshot
10680                .segments()
10681                .iter()
10682                .all(|segment| { segment.cached_vector_ceiling_norm_range().is_none() })
10683        );
10684        let snapshot = Arc::clone(&admitted.snapshot);
10685        drop(admitted);
10686        store.clock = Arc::new(CeilingClock {
10687            base: Instant::now(),
10688            snapshot,
10689        });
10690        let outcome = store
10691            .search(
10692                crate::ingest::SearchRequest::new(&[1.0, 0.0, 0.0, 0.0]),
10693                1,
10694                SearchOptions::default().with_tier(SearchTier::Exact),
10695                QueryControl::Cancel(CancelToken::new()),
10696            )
10697            .expect("exact query");
10698        assert_eq!(outcome.diagnostics.elapsed, Duration::from_secs(7));
10699        // Release the test clock's snapshot before close waits for all pins.
10700        store.clock = Arc::new(super::SystemMonotonicClock);
10701        store.close().expect("close");
10702    }
10703
10704    #[test]
10705    fn the_remembered_vector_ceiling_is_the_recomputed_ceiling_for_every_scheme() {
10706        let bit4_directory = tempdir().expect("Bit4 ceiling store directory");
10707        let bit4 = Store::open(bit4_directory.path(), OpenOptions::default())
10708            .expect("open Bit4 ceiling store");
10709        ingest_ceiling_rows(&bit4, 0, false);
10710        assert_remembered_ceiling_is_exact(&bit4, 1);
10711        bit4.close().expect("close Bit4 ceiling store");
10712
10713        let split_directory = tempdir().expect("multi-segment ceiling store directory");
10714        let split = Store::open(split_directory.path(), OpenOptions::default())
10715            .expect("open multi-segment ceiling store");
10716        ingest_ceiling_rows(&split, 0, true);
10717        assert_remembered_ceiling_is_exact(&split, CEILING_ROWS.len());
10718        split.close().expect("close multi-segment ceiling store");
10719
10720        let int8_directory = tempdir().expect("Int8 ceiling store directory");
10721        let int8 = Store::open_with_test_dependencies(
10722            int8_directory.path(),
10723            OpenOptions::default(),
10724            StoreTestDependencies::new(Arc::new(StdVfs), Arc::new(super::SystemMonotonicClock))
10725                .with_vector_seal_scheme(crate::quant::QuantScheme::Int8),
10726        )
10727        .expect("open Int8 ceiling store");
10728        ingest_ceiling_rows(&int8, 0, false);
10729        assert_remembered_ceiling_is_exact(&int8, 1);
10730        int8.close().expect("close Int8 ceiling store");
10731
10732        let f32_directory = tempdir().expect("F32 ceiling store directory");
10733        publish_f32_segment(f32_directory.path());
10734        let f32_store = Store::open(f32_directory.path(), OpenOptions::default())
10735            .expect("open F32 ceiling store");
10736        assert_remembered_ceiling_is_exact(&f32_store, 1);
10737        f32_store.close().expect("close F32 ceiling store");
10738
10739        let active_directory = tempdir().expect("active ceiling store directory");
10740        let active = Store::open(active_directory.path(), OpenOptions::default())
10741            .expect("open active ceiling store");
10742        ingest_ceiling_rows(&active, 2, false);
10743        assert!(
10744            !active
10745                .admit_vector_search(SearchOptions::default())
10746                .expect("admit active ceiling query")
10747                .active_segment
10748                .is_empty(),
10749            "the active-segment case sealed every row"
10750        );
10751        assert_remembered_ceiling_is_exact(&active, 1);
10752        active.close().expect("close active ceiling store");
10753    }
10754
10755    #[test]
10756    fn exact_and_graph_tiers_preserve_quantizer_validation_errors() {
10757        let directory = tempdir().expect("store directory");
10758        let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
10759        let query = vec![0.0_f32; crate::kernels::MAX_DOT_I8_DIMENSION + 1];
10760
10761        for tier in [
10762            SearchTier::Exact,
10763            SearchTier::Graph(GraphSearchOptions::default()),
10764        ] {
10765            let error = store
10766                .search(
10767                    crate::ingest::SearchRequest::new(&query),
10768                    1,
10769                    SearchOptions::default().with_tier(tier),
10770                    QueryControl::Cancel(CancelToken::new()),
10771                )
10772                .expect_err("oversized quantized query must retain its error");
10773            assert!(matches!(
10774                error,
10775                super::QueryError::Scan(crate::scan::ScanError::Quant(
10776                    crate::quant::QuantError::DimensionTooLarge { actual, maximum }
10777                )) if actual == query.len()
10778                    && maximum == crate::kernels::MAX_DOT_I8_DIMENSION
10779            ));
10780        }
10781    }
10782
10783    #[cfg(feature = "allocation-audit")]
10784    #[test]
10785    fn exact_tier_query_does_not_prepare_quantized_forms() {
10786        let directory = tempdir().expect("store directory");
10787        let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
10788        let query = [0.25_f32; 128];
10789        let options = SearchOptions::default().with_tier(SearchTier::Exact);
10790        let warm = store
10791            .search(
10792                crate::ingest::SearchRequest::new(&query),
10793                1,
10794                options,
10795                QueryControl::Cancel(CancelToken::new()),
10796            )
10797            .expect("warm exact-tier search");
10798        assert!(warm.candidates.is_empty());
10799        let control = QueryControl::Cancel(CancelToken::new());
10800
10801        let (outcome, report) = crate::allocation_audit::audit_engine_path(|| {
10802            store.search(
10803                crate::ingest::SearchRequest::new(&query),
10804                1,
10805                options,
10806                control,
10807            )
10808        });
10809        let outcome = outcome.expect("audited exact-tier search");
10810        assert!(outcome.candidates.is_empty());
10811        assert_eq!(report.allocations, 0, "Exact tier prepared quantized forms");
10812        assert_eq!(report.attributed_bytes, 0);
10813        assert_eq!(report.unattributed_bytes, 0);
10814    }
10815
10816    #[test]
10817    fn astra_18_scoped_work_is_joined_before_cancel_returns() {
10818        use std::sync::atomic::{AtomicBool, Ordering};
10819        use std::sync::mpsc;
10820        let worker = super::pool::LexicalWorker::start().expect("worker");
10821        let token = CancelToken::new();
10822        let work_token = token.clone();
10823        let finished = AtomicBool::new(false);
10824        let mut borrowed_rows = Vec::new();
10825        let (started_tx, started_rx) = mpsc::sync_channel(0);
10826        let (release_tx, release_rx) = mpsc::sync_channel(0);
10827        let worker_rows = &mut borrowed_rows;
10828        let worker_finished = &finished;
10829        let (caller, lexical) = worker
10830            .run_scoped(
10831                move || {
10832                    started_tx.send(()).expect("entered borrowed job");
10833                    release_rx.recv().expect("caller has cancelled");
10834                    assert!(work_token.is_cancelled());
10835                    worker_rows.push(42_u32);
10836                    worker_finished.store(true, Ordering::Release);
10837                    Err::<(), _>(crate::fusion::FusionError::Cancelled { partial: false })
10838                },
10839                || {
10840                    started_rx.recv().expect("worker entered");
10841                    token.cancel();
10842                    assert!(!finished.load(Ordering::Acquire));
10843                    release_tx.send(()).expect("release borrowed job");
10844                    Err::<(), _>(crate::fusion::FusionError::Cancelled { partial: false })
10845                },
10846            )
10847            .expect("scoped dispatch");
10848        assert!(finished.load(Ordering::Acquire));
10849        assert_eq!(borrowed_rows, [42]);
10850        assert_eq!(
10851            resolve_hybrid_leg_results(caller, lexical.expect("worker returned")),
10852            Err(crate::fusion::FusionError::Cancelled { partial: false }),
10853        );
10854        worker.stop_and_join().expect("join worker");
10855    }
10856
10857    #[test]
10858    fn astra_18_control_error_precedence_is_completion_order_independent() {
10859        use crate::fusion::{FusionError, FusionLeg, LegFailureKind};
10860        use std::sync::mpsc;
10861        let failures = [
10862            FusionError::Leg {
10863                leg: FusionLeg::Vector,
10864                kind: LegFailureKind::Scan,
10865                detail: "scan".to_owned(),
10866            },
10867            FusionError::Cancelled { partial: false },
10868            FusionError::Timeout { partial: false },
10869            FusionError::ReadCancelled { partial: false },
10870        ];
10871        let worker = super::pool::LexicalWorker::start().expect("worker");
10872        for (a, vector) in failures.iter().enumerate() {
10873            for (b, lexical) in failures.iter().enumerate() {
10874                for lexical_first in [false, true] {
10875                    let (lexical_tx, lexical_rx) = mpsc::sync_channel(1);
10876                    let (vector_tx, vector_rx) = mpsc::sync_channel(1);
10877                    let (vector_result, lexical_result) = worker
10878                        .run_scoped(
10879                            move || {
10880                                if !lexical_first {
10881                                    vector_rx.recv().expect("vector completed");
10882                                }
10883                                if lexical_first {
10884                                    lexical_tx.send(()).expect("lexical completed");
10885                                }
10886                                Err::<(), _>(lexical.clone())
10887                            },
10888                            || {
10889                                if lexical_first {
10890                                    lexical_rx.recv().expect("lexical completed");
10891                                }
10892                                if !lexical_first {
10893                                    vector_tx.send(()).expect("vector completed");
10894                                }
10895                                Err::<(), _>(vector.clone())
10896                            },
10897                        )
10898                        .expect("dispatch");
10899                    assert_eq!(
10900                        resolve_hybrid_leg_results(vector_result, lexical_result.expect("joined")),
10901                        Err(failures.get(a.max(b)).expect("known failure index").clone()),
10902                        "vector={a} lexical={b} lexical_first={lexical_first}",
10903                    );
10904                }
10905            }
10906        }
10907        worker.stop_and_join().expect("join worker");
10908    }
10909
10910    #[test]
10911    fn hybrid_dual_failure_precedence_is_independent_of_leg_completion_order() {
10912        use crate::fusion::{FusionError, FusionLeg, LegFailureKind};
10913
10914        let vector_failure = FusionError::Leg {
10915            leg: FusionLeg::Vector,
10916            kind: LegFailureKind::Scan,
10917            detail: "vector failure".to_owned(),
10918        };
10919        let lexical_failure = FusionError::Leg {
10920            leg: FusionLeg::Lexical,
10921            kind: LegFailureKind::Lexical,
10922            detail: "lexical failure".to_owned(),
10923        };
10924        assert_eq!(
10925            resolve_hybrid_leg_results::<(), ()>(
10926                Err(vector_failure.clone()),
10927                Err(lexical_failure.clone()),
10928            ),
10929            Err(vector_failure.clone()),
10930        );
10931        assert_eq!(
10932            resolve_hybrid_leg_results::<(), ()>(
10933                Err(vector_failure.clone()),
10934                Err(FusionError::Cancelled { partial: false }),
10935            ),
10936            Err(FusionError::Cancelled { partial: false }),
10937        );
10938        assert_eq!(
10939            resolve_hybrid_leg_results::<(), ()>(
10940                Err(FusionError::Timeout { partial: false }),
10941                Err(FusionError::ReadCancelled { partial: false }),
10942            ),
10943            Err(FusionError::ReadCancelled { partial: false }),
10944        );
10945        assert_eq!(
10946            resolve_hybrid_leg_results::<(), ()>(
10947                Err(FusionError::Cancelled { partial: false }),
10948                Err(FusionError::Timeout { partial: false }),
10949            ),
10950            Err(FusionError::Timeout { partial: false }),
10951        );
10952        assert_eq!(
10953            resolve_hybrid_leg_results::<(), ()>(
10954                Err(FusionError::ReadCancelled { partial: false }),
10955                Err(lexical_failure),
10956            ),
10957            Err(FusionError::ReadCancelled { partial: false }),
10958        );
10959    }
10960
10961    #[test]
10962    fn accounting_errors_report_the_typed_context() {
10963        let statistics = StoreError::Statistics {
10964            component: "mapped resident bytes",
10965            source: std::io::Error::other("mincore failed"),
10966        };
10967        assert_eq!(
10968            statistics.to_string(),
10969            "store statistics mapped resident bytes: mincore failed"
10970        );
10971        assert_eq!(
10972            statistics.source().map(ToString::to_string),
10973            Some("mincore failed".to_owned())
10974        );
10975
10976        let budget = StoreError::BudgetExceeded {
10977            needed: 65,
10978            budget: 64,
10979            component: "wal",
10980        };
10981        assert_eq!(
10982            budget.to_string(),
10983            "store wal allocation needs 65 bytes, budget is 64 bytes"
10984        );
10985        assert!(budget.source().is_none());
10986
10987        let allocation = StoreError::AllocationFailed {
10988            needed: 23,
10989            component: "snapshot",
10990        };
10991        assert_eq!(
10992            allocation.to_string(),
10993            "store snapshot allocator rejected 23 bytes"
10994        );
10995        assert!(allocation.source().is_none());
10996    }
10997
10998    #[test]
10999    fn open_options_resolve_durability_before_filesystem_mutation() {
11000        let parent = tempdir().expect("parent directory");
11001        let store_path = parent.path().join("not-created");
11002        let options =
11003            OpenOptions::new().with_durability(DurabilityMode::Attached, CommitTier::Durable);
11004
11005        let error = match Store::open(&store_path, options) {
11006            Ok(store) => {
11007                drop(store);
11008                panic!("attached durability unexpectedly opened")
11009            }
11010            Err(error) => error,
11011        };
11012
11013        assert!(matches!(
11014            error,
11015            StoreError::Durability(DurabilityPolicyError::AttachedNotYetSupported)
11016        ));
11017        assert!(!store_path.exists(), "rejected open created store files");
11018    }
11019
11020    #[test]
11021    fn memory_vfs_writable_open_requires_host_lease_directory() {
11022        let parent = tempdir().expect("parent directory");
11023        let store_path = parent.path().join("memory-only-store");
11024        let dependencies = StoreTestDependencies::new(
11025            Arc::new(MemoryVfs::new()),
11026            Arc::new(ManualMonotonicClock::new()),
11027        );
11028        let result =
11029            Store::open_with_test_dependencies(&store_path, OpenOptions::default(), dependencies);
11030        assert!(
11031            matches!(result, Err(StoreError::Lock(super::lock::StoreLockError::Io { ref source, .. })) if source.kind() == std::io::ErrorKind::NotFound),
11032            "writable open must refuse the missing host lease directory"
11033        );
11034        assert!(!store_path.exists());
11035    }
11036
11037    #[test]
11038    fn memory_vfs_store_retains_real_reader_lease() {
11039        let directory = tempdir().expect("host lease directory");
11040        let dependencies = StoreTestDependencies::new(
11041            Arc::new(MemoryVfs::new()),
11042            Arc::new(ManualMonotonicClock::new()),
11043        );
11044        let store = Store::open_with_test_dependencies(
11045            directory.path(),
11046            OpenOptions::default(),
11047            dependencies,
11048        )
11049        .expect("MemoryVfs store with host lease");
11050        let snapshot = store.open_snapshot().expect("snapshot view");
11051        assert!(matches!(
11052            super::lock::StoreLock::reclaim_exclusive(directory.path()),
11053            Err(super::lock::StoreLockError::Io { source, .. })
11054                if source.kind() == std::io::ErrorKind::WouldBlock
11055        ));
11056        drop(store);
11057        assert!(matches!(
11058            super::lock::StoreLock::reclaim_exclusive(directory.path()),
11059            Err(super::lock::StoreLockError::Io { source, .. })
11060                if source.kind() == std::io::ErrorKind::WouldBlock
11061        ));
11062        drop(snapshot);
11063        drop(super::lock::StoreLock::reclaim_exclusive(directory.path()).expect("released lease"));
11064    }
11065
11066    #[test]
11067    fn open_refuses_a_manifest_ahead_of_the_checked_wal() {
11068        let directory = tempdir().expect("store directory");
11069        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::Ordered)
11070            .expect("derived policy");
11071        commit_manifest(
11072            &StdVfs,
11073            directory.path(),
11074            &Manifest {
11075                #[cfg(feature = "graph-cypher")]
11076                graph: None,
11077                generation: 1,
11078                log_seq: 1,
11079                segments: Vec::new(),
11080                epochs: Vec::new(),
11081                epoch_alias: None,
11082                schema: Schema::new(Vec::new()).expect("schema"),
11083            },
11084            policy,
11085        )
11086        .expect("manifest");
11087
11088        let error = match Store::open(directory.path(), OpenOptions::default()) {
11089            Ok(store) => {
11090                drop(store);
11091                panic!("manifest ahead of missing WAL unexpectedly opened")
11092            }
11093            Err(error) => error,
11094        };
11095
11096        assert!(matches!(
11097            error,
11098            StoreError::Manifest(ManifestError::AheadOfLog {
11099                snapshot: 1,
11100                durable: 0
11101            })
11102        ));
11103    }
11104}
11105
11106#[cfg(test)]
11107mod shared_bound_tests;
11108
11109#[cfg(test)]
11110mod hybrid_overlap_tests;