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