Skip to main content

hyphae_engine/
facade.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::{
4    collections::BTreeSet,
5    path::Path,
6    time::{Duration, Instant},
7};
8
9use hyphae_core::{Q15Vector, VectorSpaceDefinition, VectorSpaceName};
10use hyphae_query::{
11    BoundedQueryError, ExecutionLimits, Query, QueryError, QueryResult, Record, execute,
12    execute_with_byte_limit, validate_query,
13};
14use hyphae_retrieval::{
15    DurableVectorRecord, ExactRetrievalError, ExactRetrievalLimits, ExactRetrievalOutcome,
16    ExactRetrievalRequest, HybridError, HybridOutcome, HybridRequest, LexicalError,
17    LexicalIndexDefinition, LexicalLimits, LexicalOutcome, LexicalRequest, RetrievalError,
18    RetrievalLimits, RetrievalOutcome, RetrievalRequest, VectorRecord, fuse_hybrid, retrieve,
19    retrieve_exact, retrieve_lexical_materialized, tokenize_v1_checked,
20};
21use hyphae_storage::{
22    AppendOutcome, BackupError, BackupInfo, CompactionOutcome, MAX_SCAN_PAGE_ENTRIES,
23    MaintenanceLimits, Mutation, RestoreInfo, ScanPageError, SnapshotError, SnapshotInfo,
24    StorageEngine, StorageError, StorageLimitError, StorageLimits, StorageRecoveryReport,
25    VectorEntriesError, restore_backup, verify_backup,
26};
27use thiserror::Error;
28use uuid::Uuid;
29
30use crate::{
31    DocumentError, ExactRetrievalProof, ExactRetrievalProofArtifact, HybridRetrievalProof,
32    HybridRetrievalProofArtifact, LexicalRetrievalProof, LexicalRetrievalProofArtifact, ProofError,
33    ResultProof, ResultProofArtifact, RetrievalProofError, decode_document, encode_document,
34};
35
36/// Failure while operating the embeddable Hyphae facade.
37#[derive(Debug, Error)]
38pub enum EngineError {
39    /// Durable embedded storage failed.
40    #[error(transparent)]
41    Storage(#[from] StorageError),
42
43    /// Portable backup creation, verification, or restore failed.
44    #[error(transparent)]
45    Backup(#[from] BackupError),
46
47    /// Canonical document encoding or verification failed.
48    #[error(transparent)]
49    Document(#[from] DocumentError),
50
51    /// Structured query validation or execution failed.
52    #[error(transparent)]
53    Query(#[from] QueryError),
54
55    /// Exact semantic retrieval failed.
56    #[error(transparent)]
57    Retrieval(#[from] RetrievalError),
58
59    /// Durable exact retrieval failed.
60    #[error(transparent)]
61    ExactRetrieval(#[from] ExactRetrievalError),
62
63    /// Canonical result-proof creation failed.
64    #[error(transparent)]
65    Proof(#[from] ProofError),
66
67    /// Canonical retrieval-proof creation failed.
68    #[error(transparent)]
69    RetrievalProof(#[from] RetrievalProofError),
70
71    /// Provider-free lexical retrieval failed.
72    #[error(transparent)]
73    Lexical(#[from] LexicalError),
74
75    /// Deterministic hybrid fusion failed.
76    #[error(transparent)]
77    Hybrid(#[from] HybridError),
78
79    /// One atomic document batch repeats a key.
80    #[error("atomic document batch contains a duplicate key")]
81    DuplicateDocumentKey,
82
83    /// An atomic batch must contain at least one item.
84    #[error("atomic batch must contain at least one item")]
85    EmptyBatch,
86}
87
88/// Failure from the additive engine query entry points that enforce an
89/// aggregate durable byte budget.
90#[derive(Debug, Error)]
91pub enum BoundedEngineQueryError {
92    /// Ordinary embedded-engine failure.
93    #[error(transparent)]
94    Engine(#[from] EngineError),
95    /// Aggregate durable key and canonical-document bytes exceeded policy.
96    #[error("global scanned-byte budget exceeded: {maximum}")]
97    ScannedByteBudgetExceeded {
98        /// Configured maximum.
99        maximum: u64,
100    },
101}
102
103impl From<BoundedQueryError> for BoundedEngineQueryError {
104    fn from(source: BoundedQueryError) -> Self {
105        match source {
106            BoundedQueryError::Query(source) => Self::Engine(EngineError::Query(source)),
107            BoundedQueryError::RecordDocument(source) => {
108                Self::Engine(EngineError::Document(source))
109            }
110            BoundedQueryError::ScannedByteBudgetExceeded { maximum } => {
111                Self::ScannedByteBudgetExceeded { maximum }
112            }
113        }
114    }
115}
116
117/// Newly opened embeddable engine and durable recovery evidence.
118#[derive(Debug)]
119pub struct OpenedEngine {
120    /// Ready engine facade.
121    pub engine: HyphaeEngine,
122    /// Log verification and index replay evidence.
123    pub recovery: StorageRecoveryReport,
124}
125
126#[derive(Debug)]
127struct HybridExecution {
128    started: Instant,
129    total_timeout: Duration,
130    lexical: LexicalOutcome,
131    vector: ExactRetrievalOutcome,
132    outcome: HybridOutcome,
133}
134
135/// Embeddable autonomous Hyphae engine.
136#[derive(Debug)]
137pub struct HyphaeEngine {
138    storage: StorageEngine,
139}
140
141impl HyphaeEngine {
142    /// Opens one exclusively owned data directory and completes recovery.
143    ///
144    /// # Errors
145    ///
146    /// Returns an error for directory contention, corruption, unsupported
147    /// formats, snapshot mismatch, or failed index replay.
148    pub fn open(path: impl AsRef<Path>) -> Result<OpenedEngine, EngineError> {
149        let opened = StorageEngine::open(path)?;
150        Ok(OpenedEngine {
151            engine: Self {
152                storage: opened.storage,
153            },
154            recovery: opened.recovery,
155        })
156    }
157
158    /// Opens one data directory under explicit finite recovery and
159    /// maintenance limits.
160    ///
161    /// # Errors
162    ///
163    /// Returns an error for invalid limits, contention, corruption, exhausted
164    /// recovery policy, timeout, unsupported formats, or failed replay.
165    pub fn open_with_limits(
166        path: impl AsRef<Path>,
167        limits: StorageLimits,
168    ) -> Result<OpenedEngine, EngineError> {
169        let opened = StorageEngine::open_with_limits(path, limits)?;
170        Ok(OpenedEngine {
171            engine: Self {
172                storage: opened.storage,
173            },
174            recovery: opened.recovery,
175        })
176    }
177
178    /// Returns the owned data-directory path.
179    pub fn data_path(&self) -> &Path {
180        self.storage.data_path()
181    }
182
183    /// Atomically stores one canonical structured record.
184    ///
185    /// # Errors
186    ///
187    /// Returns a document codec or durable storage error.
188    pub fn put_record(
189        &mut self,
190        transaction_id: Uuid,
191        record: &Record,
192    ) -> Result<AppendOutcome, EngineError> {
193        self.put_records(transaction_id, std::slice::from_ref(record))
194    }
195
196    /// Atomically stores a batch of canonical structured records.
197    ///
198    /// Encoding every document and checking duplicate keys happens before the
199    /// log append, so a codec failure cannot partially commit the batch.
200    ///
201    /// # Errors
202    ///
203    /// Returns an error for duplicate batch keys, document bounds, key bounds,
204    /// idempotency conflicts, or durable storage failures.
205    pub fn put_records(
206        &mut self,
207        transaction_id: Uuid,
208        records: &[Record],
209    ) -> Result<AppendOutcome, EngineError> {
210        if records.is_empty() {
211            return Err(EngineError::EmptyBatch);
212        }
213        let mut keys = BTreeSet::new();
214        let mut mutations = Vec::with_capacity(records.len());
215        for record in records {
216            if !keys.insert(record.key.as_slice()) {
217                return Err(EngineError::DuplicateDocumentKey);
218            }
219            mutations.push(Mutation::put(
220                record.key.clone(),
221                encode_document(&record.value)?,
222            ));
223        }
224        Ok(self.storage.write(transaction_id, &mutations)?)
225    }
226
227    /// Atomically deletes one structured record.
228    ///
229    /// # Errors
230    ///
231    /// Returns a key-validation, idempotency, or durable storage error.
232    pub fn delete_record(
233        &mut self,
234        transaction_id: Uuid,
235        key: &[u8],
236    ) -> Result<AppendOutcome, EngineError> {
237        self.delete_records(transaction_id, &[key])
238    }
239
240    /// Atomically deletes a batch of structured records.
241    ///
242    /// Duplicate keys are rejected before the log append. Deleting a missing
243    /// key remains a successful durable operation.
244    ///
245    /// # Errors
246    ///
247    /// Returns an error for duplicate keys, invalid key bounds, idempotency
248    /// conflicts, or durable storage failures.
249    pub fn delete_records(
250        &mut self,
251        transaction_id: Uuid,
252        keys: &[&[u8]],
253    ) -> Result<AppendOutcome, EngineError> {
254        if keys.is_empty() {
255            return Err(EngineError::EmptyBatch);
256        }
257        let mut unique = BTreeSet::new();
258        let mut mutations = Vec::with_capacity(keys.len());
259        for key in keys {
260            if !unique.insert(*key) {
261                return Err(EngineError::DuplicateDocumentKey);
262            }
263            mutations.push(Mutation::delete(*key));
264        }
265        Ok(self.storage.write(transaction_id, &mutations)?)
266    }
267
268    /// Gets and verifies one structured record by binary key.
269    ///
270    /// # Errors
271    ///
272    /// Returns a key, storage, or canonical document verification error.
273    pub fn get_record(&self, key: &[u8]) -> Result<Option<Record>, EngineError> {
274        self.storage
275            .get(key)?
276            .map(|encoded| {
277                Ok(Record {
278                    key: key.to_vec(),
279                    value: decode_document(&encoded)?,
280                })
281            })
282            .transpose()
283    }
284
285    /// Gets one structured record and binds the complete result, including
286    /// absence, to a canonical snapshot witness.
287    ///
288    /// # Errors
289    ///
290    /// Returns a key, storage, document, snapshot, or result-proof error.
291    pub fn get_record_with_proof(&self, key: &[u8]) -> Result<ResultProofArtifact, EngineError> {
292        let result = self.get_record(key)?;
293        let snapshot = self.snapshot()?;
294        let proof = ResultProof::for_get(&snapshot, key.to_vec(), result)?;
295        Ok(ResultProofArtifact { proof, snapshot })
296    }
297
298    /// Gets one record and creates its witness under explicit maintenance
299    /// limits.
300    ///
301    /// # Errors
302    ///
303    /// Returns a key, storage, document, snapshot-limit, timeout, or
304    /// result-proof error.
305    pub fn get_record_with_proof_with_limits(
306        &self,
307        key: &[u8],
308        maintenance: &MaintenanceLimits,
309    ) -> Result<ResultProofArtifact, EngineError> {
310        let started = Instant::now();
311        let total_timeout = maintenance.timeout;
312        let result = self.get_record(key)?;
313        let maintenance = remaining_maintenance_limits(maintenance, started, total_timeout)?;
314        let snapshot = self.snapshot_with_limits(&maintenance)?;
315        let proof = ResultProof::for_get(&snapshot, key.to_vec(), result)?;
316        ensure_total_timeout(started, total_timeout)?;
317        Ok(ResultProofArtifact { proof, snapshot })
318    }
319
320    /// Executes deterministic structured query over all durable documents.
321    ///
322    /// Storage scan and document decoding consume the same wall-clock timeout;
323    /// the reference executor receives only the remaining duration.
324    ///
325    /// # Errors
326    ///
327    /// Returns a storage, document, query validation, global budget, aggregate,
328    /// or timeout error. No partial page is returned.
329    pub fn query(
330        &self,
331        query: &Query,
332        limits: &ExecutionLimits,
333    ) -> Result<QueryResult, EngineError> {
334        match self.query_internal(query, limits, None) {
335            Ok(result) => Ok(result),
336            Err(BoundedEngineQueryError::Engine(source)) => Err(source),
337            Err(BoundedEngineQueryError::ScannedByteBudgetExceeded { .. }) => {
338                unreachable!("legacy query execution does not enforce an aggregate byte limit")
339            }
340        }
341    }
342
343    /// Executes deterministic structured query under an explicit aggregate
344    /// durable key/document byte budget.
345    ///
346    /// The budget is enforced inside each storage page before its key/value
347    /// bytes are cloned and again by the reference executor. Existing
348    /// Legacy [`Self::query`] callers retain the 0.2.0 count-bounded behavior.
349    ///
350    /// # Errors
351    ///
352    /// Returns a storage, document, query validation, global count/byte
353    /// budget, aggregate, or timeout error. No partial page is returned.
354    pub fn query_with_byte_limit(
355        &self,
356        query: &Query,
357        limits: &ExecutionLimits,
358        max_scanned_bytes: u64,
359    ) -> Result<QueryResult, BoundedEngineQueryError> {
360        self.query_internal(query, limits, Some(max_scanned_bytes))
361    }
362
363    fn query_internal(
364        &self,
365        query: &Query,
366        limits: &ExecutionLimits,
367        max_scanned_bytes: Option<u64>,
368    ) -> Result<QueryResult, BoundedEngineQueryError> {
369        validate_query(query, limits).map_err(EngineError::from)?;
370        let started = Instant::now();
371        let mut records = Vec::new();
372        let mut after = None;
373        let mut scanned_bytes = 0_u64;
374        loop {
375            if started.elapsed() >= limits.timeout {
376                return Err(EngineError::from(QueryError::TimedOut).into());
377            }
378            let loaded = u64::try_from(records.len()).unwrap_or(u64::MAX);
379            let remaining = limits.max_scanned_records.saturating_sub(loaded);
380            let remaining_entries = match usize::try_from(remaining) {
381                Ok(value) => value,
382                Err(_) => usize::MAX,
383            };
384            let page_limit = remaining_entries
385                .saturating_add(1)
386                .min(MAX_SCAN_PAGE_ENTRIES);
387            let page = if let Some(maximum) = max_scanned_bytes {
388                let remaining_bytes = maximum.saturating_sub(scanned_bytes);
389                match self.storage.scan_page_with_byte_limit(
390                    after.as_deref(),
391                    page_limit,
392                    remaining_bytes,
393                ) {
394                    Err(ScanPageError::ByteBudgetExceeded { .. }) => {
395                        return Err(BoundedEngineQueryError::ScannedByteBudgetExceeded { maximum });
396                    }
397                    Err(ScanPageError::Storage(source)) => {
398                        return Err(EngineError::from(source).into());
399                    }
400                    Ok(page) => page,
401                }
402            } else {
403                self.storage
404                    .scan_page(after.as_deref(), page_limit)
405                    .map_err(EngineError::from)?
406            };
407            for entry in page.entries {
408                if u64::try_from(records.len()).unwrap_or(u64::MAX) >= limits.max_scanned_records {
409                    return Err(EngineError::from(QueryError::ScannedBudgetExceeded {
410                        maximum: limits.max_scanned_records,
411                    })
412                    .into());
413                }
414                if let Some(maximum) = max_scanned_bytes {
415                    let entry_bytes = u64::try_from(entry.key.len())
416                        .ok()
417                        .and_then(|key_bytes| {
418                            u64::try_from(entry.value.len())
419                                .ok()
420                                .and_then(|value_bytes| key_bytes.checked_add(value_bytes))
421                        })
422                        .ok_or(BoundedEngineQueryError::ScannedByteBudgetExceeded { maximum })?;
423                    scanned_bytes = scanned_bytes
424                        .checked_add(entry_bytes)
425                        .ok_or(BoundedEngineQueryError::ScannedByteBudgetExceeded { maximum })?;
426                    if scanned_bytes > maximum {
427                        return Err(BoundedEngineQueryError::ScannedByteBudgetExceeded { maximum });
428                    }
429                }
430                records.push(Record {
431                    key: entry.key,
432                    value: decode_document(&entry.value).map_err(EngineError::from)?,
433                });
434            }
435            let Some(next_after) = page.next_after else {
436                break;
437            };
438            after = Some(next_after);
439        }
440
441        let elapsed = started.elapsed();
442        let Some(timeout) = limits.timeout.checked_sub(elapsed) else {
443            return Err(EngineError::from(QueryError::TimedOut).into());
444        };
445        if timeout.is_zero() {
446            return Err(EngineError::from(QueryError::TimedOut).into());
447        }
448        let execution_limits = ExecutionLimits {
449            timeout,
450            ..limits.clone()
451        };
452        match max_scanned_bytes {
453            Some(maximum) => {
454                execute_with_byte_limit(&[records.as_slice()], query, &execution_limits, maximum)
455                    .map_err(BoundedEngineQueryError::from)
456            }
457            None => execute(&[records.as_slice()], query, &execution_limits)
458                .map_err(EngineError::from)
459                .map_err(BoundedEngineQueryError::from),
460        }
461    }
462
463    /// Executes one structured query and binds its complete logical result to
464    /// a canonical snapshot witness at the same locked checkpoint.
465    ///
466    /// # Errors
467    ///
468    /// Returns any ordinary query error plus snapshot or proof creation
469    /// failures. No proof is returned for a partial or failed query.
470    pub fn query_with_proof(
471        &self,
472        query: &Query,
473        limits: &ExecutionLimits,
474    ) -> Result<ResultProofArtifact, EngineError> {
475        let result = self.query(query, limits)?;
476        let snapshot = self.snapshot()?;
477        let proof = ResultProof::for_query(&snapshot, query.clone(), result)?;
478        Ok(ResultProofArtifact { proof, snapshot })
479    }
480
481    /// Executes one query and creates its witness under the query's remaining
482    /// end-to-end timeout and explicit maintenance bounds.
483    ///
484    /// # Errors
485    ///
486    /// Returns any ordinary query error plus snapshot-limit, timeout, or
487    /// proof-creation failures.
488    pub fn query_with_proof_with_limits(
489        &self,
490        query: &Query,
491        limits: &ExecutionLimits,
492        max_scanned_bytes: u64,
493        maintenance: &MaintenanceLimits,
494    ) -> Result<ResultProofArtifact, BoundedEngineQueryError> {
495        let started = Instant::now();
496        let result = self.query_with_byte_limit(query, limits, max_scanned_bytes)?;
497        let maintenance = remaining_maintenance_limits(maintenance, started, limits.timeout)
498            .map_err(BoundedEngineQueryError::from)?;
499        let snapshot = self
500            .snapshot_with_limits(&maintenance)
501            .map_err(BoundedEngineQueryError::from)?;
502        let proof = ResultProof::for_query(&snapshot, query.clone(), result)
503            .map_err(EngineError::from)
504            .map_err(BoundedEngineQueryError::from)?;
505        ensure_total_timeout(started, limits.timeout).map_err(BoundedEngineQueryError::from)?;
506        Ok(ResultProofArtifact { proof, snapshot })
507    }
508
509    /// Executes exact provider-neutral vector retrieval without persisting or
510    /// producing embeddings.
511    ///
512    /// # Errors
513    ///
514    /// Returns vector, shape, duplicate-key, budget, or timeout errors.
515    pub fn retrieve_vectors(
516        shards: &[&[VectorRecord]],
517        request: &RetrievalRequest,
518        limits: &RetrievalLimits,
519    ) -> Result<RetrievalOutcome, EngineError> {
520        Ok(retrieve(shards, request, limits)?)
521    }
522
523    /// Defines one immutable durable named vector space.
524    ///
525    /// Repeating the identical definition is idempotent; changing dimension
526    /// or metric for an existing name fails before the log append.
527    ///
528    /// # Errors
529    ///
530    /// Returns an idempotency, immutable-definition, or durable storage error.
531    pub fn define_vector_space(
532        &mut self,
533        transaction_id: Uuid,
534        definition: VectorSpaceDefinition,
535    ) -> Result<AppendOutcome, EngineError> {
536        Ok(self
537            .storage
538            .write(transaction_id, &[Mutation::define_vector_space(definition)])?)
539    }
540
541    /// Atomically stores vectors in one named space.
542    ///
543    /// # Errors
544    ///
545    /// Returns an error before append for duplicate keys, an unknown space,
546    /// wrong dimensions, invalid keys, or invalid vectors.
547    pub fn put_vectors(
548        &mut self,
549        transaction_id: Uuid,
550        space: &VectorSpaceName,
551        vectors: &[(Vec<u8>, Q15Vector)],
552    ) -> Result<AppendOutcome, EngineError> {
553        if vectors.is_empty() {
554            return Err(EngineError::EmptyBatch);
555        }
556        let mut keys = BTreeSet::new();
557        let mut mutations = Vec::with_capacity(vectors.len());
558        for (key, vector) in vectors {
559            if !keys.insert(key.as_slice()) {
560                return Err(EngineError::DuplicateDocumentKey);
561            }
562            mutations.push(Mutation::upsert_vector(
563                space.clone(),
564                key.clone(),
565                vector.clone(),
566            ));
567        }
568        Ok(self.storage.write(transaction_id, &mutations)?)
569    }
570
571    /// Atomically deletes vectors from one named space.
572    ///
573    /// # Errors
574    ///
575    /// Returns an error before append for duplicate/invalid keys or an unknown
576    /// vector space.
577    pub fn delete_vectors(
578        &mut self,
579        transaction_id: Uuid,
580        space: &VectorSpaceName,
581        keys: &[&[u8]],
582    ) -> Result<AppendOutcome, EngineError> {
583        if keys.is_empty() {
584            return Err(EngineError::EmptyBatch);
585        }
586        let mut unique = BTreeSet::new();
587        let mut mutations = Vec::with_capacity(keys.len());
588        for key in keys {
589            if !unique.insert(*key) {
590                return Err(EngineError::DuplicateDocumentKey);
591            }
592            mutations.push(Mutation::delete_vector(space.clone(), *key));
593        }
594        Ok(self.storage.write(transaction_id, &mutations)?)
595    }
596
597    /// Executes exact retrieval over the latest caught-up durable vector
598    /// state. Storage budgets are enforced before returning candidates, then
599    /// the canonical executor applies scoring and timeout policy.
600    ///
601    /// # Errors
602    ///
603    /// Returns an error for an unknown space, wrong query dimension, exhausted
604    /// budget, timeout, stale storage, or malformed durable state. No partial
605    /// ranking is returned.
606    pub fn retrieve_exact(
607        &self,
608        request: &ExactRetrievalRequest,
609        limits: &ExactRetrievalLimits,
610    ) -> Result<ExactRetrievalOutcome, EngineError> {
611        let started = Instant::now();
612        let Some(definition) = self.storage.vector_space(&request.vector_space)? else {
613            return Err(StorageError::from(
614                hyphae_storage::MaterializedIndexError::UnknownVectorSpace {
615                    name: request.vector_space.as_str().to_owned(),
616                },
617            )
618            .into());
619        };
620        definition
621            .validate_vector(&request.query)
622            .map_err(|source| {
623                StorageError::from(hyphae_storage::MaterializedIndexError::from(source))
624            })?;
625        let validation_limits = ExactRetrievalLimits {
626            timeout: Duration::MAX,
627            ..limits.clone()
628        };
629        retrieve_exact(&[], request, &validation_limits)?;
630        let timeout = remaining_exact_timeout(started, limits.timeout)?;
631        let candidates = match self.storage.vector_entries_with_timeout(
632            &request.vector_space,
633            limits.max_candidates,
634            limits.max_candidate_bytes,
635            timeout,
636        ) {
637            Ok(candidates) => candidates,
638            Err(VectorEntriesError::ExactRetrieval(error)) => return Err(error.into()),
639            Err(VectorEntriesError::Storage(StorageError::Index { source })) => match *source {
640                hyphae_storage::MaterializedIndexError::VectorCandidateBudgetExceeded {
641                    maximum,
642                } => {
643                    return Err(ExactRetrievalError::CandidateBudgetExceeded { maximum }.into());
644                }
645                hyphae_storage::MaterializedIndexError::VectorByteBudgetExceeded { maximum } => {
646                    return Err(ExactRetrievalError::CandidateByteBudgetExceeded { maximum }.into());
647                }
648                source => {
649                    return Err(StorageError::Index {
650                        source: Box::new(source),
651                    }
652                    .into());
653                }
654            },
655            Err(VectorEntriesError::Storage(error)) => return Err(error.into()),
656        };
657        let mut durable_candidates = Vec::with_capacity(candidates.len());
658        for entry in candidates {
659            remaining_exact_timeout(started, limits.timeout)?;
660            durable_candidates.push(DurableVectorRecord {
661                key: entry.key,
662                vector: entry.vector,
663            });
664        }
665        let execution_limits = ExactRetrievalLimits {
666            timeout: remaining_exact_timeout(started, limits.timeout)?,
667            ..limits.clone()
668        };
669        Ok(retrieve_exact(
670            &durable_candidates,
671            request,
672            &execution_limits,
673        )?)
674    }
675
676    /// Executes exact durable retrieval and binds its complete outcome to a
677    /// canonical format-2 snapshot witness.
678    ///
679    /// # Errors
680    ///
681    /// Returns any exact-retrieval, snapshot, or retrieval-proof error. No
682    /// proof is emitted for failed or partial execution.
683    pub fn retrieve_exact_with_proof(
684        &self,
685        request: &ExactRetrievalRequest,
686        limits: &ExactRetrievalLimits,
687    ) -> Result<ExactRetrievalProofArtifact, EngineError> {
688        let outcome = self.retrieve_exact(request, limits)?;
689        let snapshot = self.snapshot()?;
690        let proof = ExactRetrievalProof::new(&snapshot, request.clone(), outcome)?;
691        Ok(ExactRetrievalProofArtifact { proof, snapshot })
692    }
693
694    /// Executes exact retrieval and creates its witness under the retrieval
695    /// operation's remaining end-to-end timeout and explicit maintenance
696    /// bounds.
697    ///
698    /// # Errors
699    ///
700    /// Returns any exact-retrieval error plus snapshot-limit, timeout, or
701    /// proof-creation failures.
702    pub fn retrieve_exact_with_proof_with_limits(
703        &self,
704        request: &ExactRetrievalRequest,
705        limits: &ExactRetrievalLimits,
706        maintenance: &MaintenanceLimits,
707    ) -> Result<ExactRetrievalProofArtifact, EngineError> {
708        let started = Instant::now();
709        let outcome = self.retrieve_exact(request, limits)?;
710        let maintenance = remaining_maintenance_limits(maintenance, started, limits.timeout)?;
711        let snapshot = self.snapshot_with_limits(&maintenance)?;
712        let proof = ExactRetrievalProof::new(&snapshot, request.clone(), outcome)?;
713        ensure_total_timeout(started, limits.timeout)?;
714        Ok(ExactRetrievalProofArtifact { proof, snapshot })
715    }
716
717    /// Defines one immutable provider-free lexical index.
718    ///
719    /// Repeating the identical definition is idempotent. Any change to an
720    /// existing definition fails before the durable append.
721    ///
722    /// # Errors
723    ///
724    /// Returns an immutable-definition, idempotency, or storage error.
725    pub fn define_lexical_index(
726        &mut self,
727        transaction_id: Uuid,
728        definition: LexicalIndexDefinition,
729    ) -> Result<AppendOutcome, EngineError> {
730        Ok(self.storage.write(
731            transaction_id,
732            &[Mutation::define_lexical_index(definition)],
733        )?)
734    }
735
736    /// Executes provider-free lexical retrieval from the rebuildable durable
737    /// posting projection.
738    ///
739    /// Posting lookup, candidate materialization, and reference scoring share
740    /// one lexical timeout and never return a partial ranking.
741    ///
742    /// # Errors
743    ///
744    /// Returns an unknown-index, document, budget, timeout, or storage error.
745    pub fn retrieve_lexical(
746        &self,
747        request: &LexicalRequest,
748        limits: &LexicalLimits,
749    ) -> Result<LexicalOutcome, EngineError> {
750        let started = Instant::now();
751        let Some(definition) = self.storage.lexical_index(&request.index)? else {
752            return Err(StorageError::from(
753                hyphae_storage::MaterializedIndexError::UnknownLexicalIndex {
754                    name: request.index.as_str().to_owned(),
755                },
756            )
757            .into());
758        };
759        let query_tokens = tokenize_v1_checked(
760            &request.query,
761            || ensure_lexical_timeout(started, limits.timeout),
762            || ensure_lexical_timeout(started, limits.timeout),
763        )?
764        .into_iter()
765        .collect::<BTreeSet<_>>()
766        .into_iter()
767        .collect::<Vec<_>>();
768        if query_tokens.is_empty() {
769            return Err(LexicalError::EmptyQuery.into());
770        }
771        if u64::try_from(query_tokens.len()).unwrap_or(u64::MAX) > limits.max_tokens {
772            return Err(LexicalError::TokenBudgetExceeded {
773                maximum: limits.max_tokens,
774            }
775            .into());
776        }
777        let timeout = remaining_lexical_timeout(started, limits.timeout)?;
778        let corpus = match self.storage.lexical_corpus(
779            &definition,
780            &query_tokens,
781            limits.max_candidates,
782            timeout,
783        ) {
784            Ok(corpus) => corpus,
785            Err(StorageError::Index { source }) => match *source {
786                hyphae_storage::MaterializedIndexError::Lexical(error) => {
787                    return Err(error.into());
788                }
789                source => {
790                    return Err(StorageError::Index {
791                        source: Box::new(source),
792                    }
793                    .into());
794                }
795            },
796            Err(error) => return Err(error.into()),
797        };
798        let Some(timeout) = limits.timeout.checked_sub(started.elapsed()) else {
799            return Err(LexicalError::TimedOut.into());
800        };
801        if timeout.is_zero() {
802            return Err(LexicalError::TimedOut.into());
803        }
804        let execution_limits = LexicalLimits {
805            timeout,
806            ..limits.clone()
807        };
808        Ok(retrieve_lexical_materialized(
809            &corpus,
810            &definition,
811            request,
812            &execution_limits,
813        )?)
814    }
815
816    /// Executes lexical retrieval and binds the complete outcome to a
817    /// canonical format-2 snapshot witness.
818    ///
819    /// # Errors
820    ///
821    /// Returns any lexical, snapshot, or retrieval-proof error.
822    pub fn retrieve_lexical_with_proof(
823        &self,
824        request: &LexicalRequest,
825        limits: &LexicalLimits,
826    ) -> Result<LexicalRetrievalProofArtifact, EngineError> {
827        let outcome = self.retrieve_lexical(request, limits)?;
828        let snapshot = self.snapshot()?;
829        let proof = LexicalRetrievalProof::new(&snapshot, request.clone(), outcome)?;
830        Ok(LexicalRetrievalProofArtifact { proof, snapshot })
831    }
832
833    /// Executes lexical retrieval and creates its witness under the
834    /// retrieval operation's remaining timeout and explicit maintenance
835    /// bounds.
836    ///
837    /// # Errors
838    ///
839    /// Returns any lexical error plus snapshot-limit, timeout, or
840    /// proof-creation failures.
841    pub fn retrieve_lexical_with_proof_with_limits(
842        &self,
843        request: &LexicalRequest,
844        limits: &LexicalLimits,
845        maintenance: &MaintenanceLimits,
846    ) -> Result<LexicalRetrievalProofArtifact, EngineError> {
847        let started = Instant::now();
848        let outcome = self.retrieve_lexical(request, limits)?;
849        let maintenance = remaining_maintenance_limits(maintenance, started, limits.timeout)?;
850        let snapshot = self.snapshot_with_limits(&maintenance)?;
851        let proof = LexicalRetrievalProof::new(&snapshot, request.clone(), outcome)?;
852        ensure_total_timeout(started, limits.timeout)?;
853        Ok(LexicalRetrievalProofArtifact { proof, snapshot })
854    }
855
856    /// Executes both durable branches and fuses their complete outcomes using
857    /// deterministic RRF semantics.
858    ///
859    /// # Errors
860    ///
861    /// Returns any lexical, exact-vector, storage, budget, timeout, or fusion
862    /// error. Branch failures never silently downgrade to single-modality
863    /// success.
864    pub fn retrieve_hybrid(
865        &self,
866        lexical_request: &LexicalRequest,
867        lexical_limits: &LexicalLimits,
868        vector_request: &ExactRetrievalRequest,
869        vector_limits: &ExactRetrievalLimits,
870        hybrid_request: &HybridRequest,
871    ) -> Result<HybridOutcome, EngineError> {
872        Ok(self
873            .execute_hybrid(
874                lexical_request,
875                lexical_limits,
876                vector_request,
877                vector_limits,
878                hybrid_request,
879            )?
880            .outcome)
881    }
882
883    /// Executes lexical and exact-vector branches, fuses their complete
884    /// outcomes, and binds all three outcomes to one canonical snapshot.
885    ///
886    /// # Errors
887    ///
888    /// Returns any branch, fusion, snapshot, or retrieval-proof error.
889    pub fn retrieve_hybrid_with_proof(
890        &self,
891        lexical_request: &LexicalRequest,
892        lexical_limits: &LexicalLimits,
893        vector_request: &ExactRetrievalRequest,
894        vector_limits: &ExactRetrievalLimits,
895        hybrid_request: &HybridRequest,
896    ) -> Result<HybridRetrievalProofArtifact, EngineError> {
897        let execution = self.execute_hybrid(
898            lexical_request,
899            lexical_limits,
900            vector_request,
901            vector_limits,
902            hybrid_request,
903        )?;
904        let snapshot = self.snapshot()?;
905        let proof = HybridRetrievalProof::new(
906            &snapshot,
907            lexical_request.clone(),
908            execution.lexical,
909            vector_request.clone(),
910            execution.vector,
911            hybrid_request.clone(),
912            execution.outcome,
913        )?;
914        Ok(HybridRetrievalProofArtifact { proof, snapshot })
915    }
916
917    /// Executes both hybrid branches and creates their shared witness under
918    /// one combined remaining branch timeout and explicit maintenance bounds.
919    ///
920    /// # Errors
921    ///
922    /// Returns any branch, fusion, snapshot-limit, timeout, or proof error.
923    #[allow(clippy::too_many_arguments)]
924    pub fn retrieve_hybrid_with_proof_with_limits(
925        &self,
926        lexical_request: &LexicalRequest,
927        lexical_limits: &LexicalLimits,
928        vector_request: &ExactRetrievalRequest,
929        vector_limits: &ExactRetrievalLimits,
930        hybrid_request: &HybridRequest,
931        maintenance: &MaintenanceLimits,
932    ) -> Result<HybridRetrievalProofArtifact, EngineError> {
933        let execution = self.execute_hybrid(
934            lexical_request,
935            lexical_limits,
936            vector_request,
937            vector_limits,
938            hybrid_request,
939        )?;
940        let maintenance =
941            remaining_maintenance_limits(maintenance, execution.started, execution.total_timeout)?;
942        let snapshot = self.snapshot_with_limits(&maintenance)?;
943        let proof = HybridRetrievalProof::new(
944            &snapshot,
945            lexical_request.clone(),
946            execution.lexical,
947            vector_request.clone(),
948            execution.vector,
949            hybrid_request.clone(),
950            execution.outcome,
951        )?;
952        ensure_total_timeout(execution.started, execution.total_timeout)?;
953        Ok(HybridRetrievalProofArtifact { proof, snapshot })
954    }
955
956    fn execute_hybrid(
957        &self,
958        lexical_request: &LexicalRequest,
959        lexical_limits: &LexicalLimits,
960        vector_request: &ExactRetrievalRequest,
961        vector_limits: &ExactRetrievalLimits,
962        hybrid_request: &HybridRequest,
963    ) -> Result<HybridExecution, EngineError> {
964        let started = Instant::now();
965        self.execute_hybrid_with_elapsed(
966            lexical_request,
967            lexical_limits,
968            vector_request,
969            vector_limits,
970            hybrid_request,
971            started,
972            || started.elapsed(),
973        )
974    }
975
976    #[allow(clippy::too_many_arguments)]
977    fn execute_hybrid_with_elapsed(
978        &self,
979        lexical_request: &LexicalRequest,
980        lexical_limits: &LexicalLimits,
981        vector_request: &ExactRetrievalRequest,
982        vector_limits: &ExactRetrievalLimits,
983        hybrid_request: &HybridRequest,
984        started: Instant,
985        mut elapsed: impl FnMut() -> Duration,
986    ) -> Result<HybridExecution, EngineError> {
987        let total_timeout = lexical_limits
988            .timeout
989            .checked_add(vector_limits.timeout)
990            .unwrap_or(Duration::MAX);
991
992        let mut bounded_lexical = lexical_limits.clone();
993        bounded_lexical.timeout = bounded_lexical
994            .timeout
995            .min(remaining_exact_timeout_after(total_timeout, elapsed())?);
996        let lexical = self.retrieve_lexical(lexical_request, &bounded_lexical)?;
997
998        let mut bounded_vector = vector_limits.clone();
999        bounded_vector.timeout = bounded_vector
1000            .timeout
1001            .min(remaining_exact_timeout_after(total_timeout, elapsed())?);
1002        let vector = self.retrieve_exact(vector_request, &bounded_vector)?;
1003
1004        let outcome = fuse_hybrid(&lexical, &vector, hybrid_request)?;
1005        remaining_exact_timeout_after(total_timeout, elapsed())?;
1006        Ok(HybridExecution {
1007            started,
1008            total_timeout,
1009            lexical,
1010            vector,
1011            outcome,
1012        })
1013    }
1014
1015    /// Creates or reuses a verified logical snapshot.
1016    ///
1017    /// # Errors
1018    ///
1019    /// Returns a stale-handle, index, or snapshot error.
1020    pub fn snapshot(&self) -> Result<SnapshotInfo, EngineError> {
1021        Ok(self.storage.snapshot()?)
1022    }
1023
1024    /// Creates or reuses a verified logical snapshot under explicit limits.
1025    ///
1026    /// # Errors
1027    ///
1028    /// Returns a limit, timeout, stale-handle, index, or snapshot error.
1029    pub fn snapshot_with_limits(
1030        &self,
1031        limits: &MaintenanceLimits,
1032    ) -> Result<SnapshotInfo, EngineError> {
1033        Ok(self.storage.snapshot_with_limits(limits)?)
1034    }
1035
1036    /// Commits an anchored compaction generation.
1037    ///
1038    /// # Errors
1039    ///
1040    /// Returns a stale-handle, snapshot, segment, or manifest error.
1041    pub fn compact(&mut self) -> Result<CompactionOutcome, EngineError> {
1042        Ok(self.storage.compact()?)
1043    }
1044
1045    /// Commits an anchored compaction generation under one finite deadline.
1046    ///
1047    /// # Errors
1048    ///
1049    /// Returns a limit, timeout, stale-handle, snapshot, segment, or manifest
1050    /// error before the manifest commit point.
1051    pub fn compact_with_limits(
1052        &mut self,
1053        limits: &MaintenanceLimits,
1054    ) -> Result<CompactionOutcome, EngineError> {
1055        Ok(self.storage.compact_with_limits(limits)?)
1056    }
1057
1058    /// Creates an atomic portable backup at the locked logical checkpoint.
1059    ///
1060    /// # Errors
1061    ///
1062    /// Returns a snapshot, destination, synchronization, or promotion error.
1063    pub fn backup(&self, destination: impl AsRef<Path>) -> Result<BackupInfo, EngineError> {
1064        Ok(self.storage.backup(destination)?)
1065    }
1066
1067    /// Verifies a portable backup without opening a live data directory.
1068    ///
1069    /// # Errors
1070    ///
1071    /// Returns an error for a malformed layout, metadata mismatch, or corrupt
1072    /// snapshot.
1073    pub fn verify_backup(path: impl AsRef<Path>) -> Result<BackupInfo, EngineError> {
1074        Ok(verify_backup(path)?)
1075    }
1076
1077    /// Restores a backup to a new atomically activated data directory.
1078    ///
1079    /// # Errors
1080    ///
1081    /// Returns an error before destination activation if verification, index
1082    /// reconstruction, reopen, or filesystem synchronization fails.
1083    pub fn restore_backup(
1084        backup: impl AsRef<Path>,
1085        destination: impl AsRef<Path>,
1086    ) -> Result<RestoreInfo, EngineError> {
1087        Ok(restore_backup(backup, destination)?)
1088    }
1089}
1090
1091fn remaining_exact_timeout(
1092    started: Instant,
1093    total_timeout: Duration,
1094) -> Result<Duration, ExactRetrievalError> {
1095    remaining_exact_timeout_after(total_timeout, started.elapsed())
1096}
1097
1098fn remaining_exact_timeout_after(
1099    total_timeout: Duration,
1100    elapsed: Duration,
1101) -> Result<Duration, ExactRetrievalError> {
1102    total_timeout
1103        .checked_sub(elapsed)
1104        .filter(|remaining| !remaining.is_zero())
1105        .ok_or(ExactRetrievalError::TimedOut)
1106}
1107
1108fn ensure_lexical_timeout(started: Instant, total_timeout: Duration) -> Result<(), LexicalError> {
1109    remaining_lexical_timeout(started, total_timeout)?;
1110    Ok(())
1111}
1112
1113fn remaining_lexical_timeout(
1114    started: Instant,
1115    total_timeout: Duration,
1116) -> Result<Duration, LexicalError> {
1117    total_timeout
1118        .checked_sub(started.elapsed())
1119        .filter(|remaining| !remaining.is_zero())
1120        .ok_or(LexicalError::TimedOut)
1121}
1122
1123fn ensure_total_timeout(started: Instant, total_timeout: Duration) -> Result<(), EngineError> {
1124    if started.elapsed() >= total_timeout {
1125        Err(StorageError::from(SnapshotError::from(StorageLimitError::TimedOut)).into())
1126    } else {
1127        Ok(())
1128    }
1129}
1130
1131fn remaining_maintenance_limits(
1132    template: &MaintenanceLimits,
1133    started: Instant,
1134    total_timeout: Duration,
1135) -> Result<MaintenanceLimits, EngineError> {
1136    let Some(remaining) = total_timeout.checked_sub(started.elapsed()) else {
1137        return Err(StorageError::from(SnapshotError::from(StorageLimitError::TimedOut)).into());
1138    };
1139    if remaining.is_zero() {
1140        return Err(StorageError::from(SnapshotError::from(StorageLimitError::TimedOut)).into());
1141    }
1142    Ok(MaintenanceLimits {
1143        timeout: template.timeout.min(remaining),
1144        snapshot: template.snapshot.clone(),
1145    })
1146}
1147
1148#[cfg(test)]
1149mod tests {
1150    use std::{
1151        collections::BTreeMap,
1152        fs,
1153        path::PathBuf,
1154        time::{Duration, Instant},
1155    };
1156
1157    use hyphae_core::{Q15Vector, VectorSpaceDefinition, VectorSpaceName};
1158    use hyphae_query::{
1159        AggregationPlan, CompareOperator, FieldPath, Filter, Metric, MetricValue, NamedMetric,
1160        NullPlacement, SortDirection, SortField, Value, encoded_document_len,
1161    };
1162    use uuid::Uuid;
1163
1164    use hyphae_retrieval::{
1165        ExactRetrievalError, ExactRetrievalLimits, ExactRetrievalOutcome, ExactRetrievalRequest,
1166        HybridOutcome, HybridRequest, LexicalField, LexicalIndexDefinition, LexicalLimits,
1167        LexicalOutcome, LexicalRequest, retrieve_lexical,
1168    };
1169
1170    use super::{
1171        BoundedEngineQueryError, EngineError, ExecutionLimits, HyphaeEngine, Query, Record,
1172    };
1173
1174    struct TestDirectory {
1175        path: PathBuf,
1176    }
1177
1178    impl TestDirectory {
1179        fn new(name: &str) -> std::io::Result<Self> {
1180            let path = std::env::temp_dir().join(format!(
1181                "hyphae-engine-{name}-{}-{}",
1182                std::process::id(),
1183                Uuid::now_v7()
1184            ));
1185            fs::create_dir_all(&path)?;
1186            Ok(Self { path })
1187        }
1188
1189        fn path(&self) -> &std::path::Path {
1190            &self.path
1191        }
1192    }
1193
1194    impl Drop for TestDirectory {
1195        fn drop(&mut self) {
1196            let _ignored = fs::remove_dir_all(&self.path);
1197        }
1198    }
1199
1200    fn value(score: i64, group: &str) -> Value {
1201        Value::Object(BTreeMap::from([
1202            ("group".to_owned(), Value::String(group.to_owned())),
1203            ("score".to_owned(), Value::Integer(score)),
1204        ]))
1205    }
1206
1207    #[test]
1208    fn durable_documents_query_identically_after_compaction_and_reopen()
1209    -> Result<(), Box<dyn std::error::Error>> {
1210        let temporary = TestDirectory::new("engine-query-reopen")?;
1211        let root = temporary.path().join("data");
1212        let mut opened = HyphaeEngine::open(&root)?;
1213        opened.engine.put_records(
1214            Uuid::now_v7(),
1215            &[
1216                Record::new(b"a", value(10, "x")),
1217                Record::new(b"b", value(8, "x")),
1218                Record::new(b"c", value(7, "y")),
1219                Record::new(b"d", value(2, "y")),
1220            ],
1221        )?;
1222        let request = Query {
1223            filter: Filter::Compare {
1224                path: FieldPath::field("score"),
1225                operator: CompareOperator::GreaterOrEqual,
1226                value: Value::Integer(7),
1227            },
1228            sort: vec![SortField {
1229                path: FieldPath::field("score"),
1230                direction: SortDirection::Descending,
1231                nulls: NullPlacement::Last,
1232            }],
1233            cursor: None,
1234            limit: 2,
1235            aggregation: Some(AggregationPlan {
1236                group_by: Vec::new(),
1237                metrics: vec![NamedMetric {
1238                    name: "count".to_owned(),
1239                    metric: Metric::Count,
1240                }],
1241            }),
1242        };
1243        let before = opened.engine.query(&request, &ExecutionLimits::default())?;
1244        assert_eq!(before.rows.len(), 2);
1245        assert_eq!(
1246            before
1247                .aggregation
1248                .as_ref()
1249                .map(|aggregation| { aggregation.groups[0].metrics[0].value.clone() }),
1250            Some(MetricValue::Count(3))
1251        );
1252        opened.engine.compact()?;
1253        drop(opened);
1254
1255        let reopened = HyphaeEngine::open(&root)?;
1256        let after = reopened
1257            .engine
1258            .query(&request, &ExecutionLimits::default())?;
1259        assert_eq!(before, after);
1260        assert_eq!(
1261            reopened.engine.get_record(b"a")?.map(|record| record.value),
1262            Some(value(10, "x"))
1263        );
1264        Ok(())
1265    }
1266
1267    #[test]
1268    fn facade_enforces_scan_budget_before_building_a_partial_page()
1269    -> Result<(), Box<dyn std::error::Error>> {
1270        let temporary = TestDirectory::new("engine-query-budget")?;
1271        let mut opened = HyphaeEngine::open(temporary.path().join("data"))?;
1272        opened.engine.put_records(
1273            Uuid::now_v7(),
1274            &[
1275                Record::new(b"a", Value::Null),
1276                Record::new(b"b", Value::Null),
1277            ],
1278        )?;
1279        let limits = ExecutionLimits {
1280            max_scanned_records: 1,
1281            ..ExecutionLimits::default()
1282        };
1283        let result = opened.engine.query(
1284            &Query {
1285                filter: Filter::MatchAll,
1286                sort: Vec::new(),
1287                cursor: None,
1288                limit: 1,
1289                aggregation: None,
1290            },
1291            &limits,
1292        );
1293        assert!(matches!(
1294            result,
1295            Err(EngineError::Query(
1296                hyphae_query::QueryError::ScannedBudgetExceeded { maximum: 1 }
1297            ))
1298        ));
1299        Ok(())
1300    }
1301
1302    #[test]
1303    fn facade_enforces_query_scan_byte_budget_before_decode()
1304    -> Result<(), Box<dyn std::error::Error>> {
1305        let temporary = TestDirectory::new("engine-query-byte-budget")?;
1306        let mut opened = HyphaeEngine::open(temporary.path().join("data"))?;
1307        let records = [
1308            Record::new(b"a", Value::Null),
1309            Record::new(b"b", Value::String("bounded".to_owned())),
1310        ];
1311        let total_bytes = records.iter().try_fold(0_u64, |total, record| {
1312            let document = crate::encode_document(&record.value)?;
1313            let bytes = u64::try_from(record.key.len() + document.len())?;
1314            Ok::<_, Box<dyn std::error::Error>>(total.checked_add(bytes).ok_or("byte overflow")?)
1315        })?;
1316        opened.engine.put_records(Uuid::now_v7(), &records)?;
1317        let request = Query {
1318            filter: Filter::MatchAll,
1319            sort: Vec::new(),
1320            cursor: None,
1321            limit: 2,
1322            aggregation: None,
1323        };
1324        let result = opened.engine.query_with_byte_limit(
1325            &request,
1326            &ExecutionLimits::default(),
1327            total_bytes,
1328        )?;
1329        assert_eq!(result.rows, records);
1330        assert!(matches!(
1331            opened.engine.query_with_byte_limit(
1332                &request,
1333                &ExecutionLimits::default(),
1334                total_bytes - 1,
1335            ),
1336            Err(BoundedEngineQueryError::ScannedByteBudgetExceeded { maximum })
1337                if maximum == total_bytes - 1
1338        ));
1339        Ok(())
1340    }
1341
1342    #[test]
1343    fn durable_scan_byte_budget_is_exact_across_storage_pages()
1344    -> Result<(), Box<dyn std::error::Error>> {
1345        let temporary = TestDirectory::new("engine-query-byte-pages")?;
1346        let mut opened = HyphaeEngine::open(temporary.path().join("data"))?;
1347        let records = (0_u64..4_097)
1348            .map(|value| Record::new(value.to_be_bytes(), Value::Null))
1349            .collect::<Vec<_>>();
1350        let total_bytes = records.iter().try_fold(0_u64, |total, record| {
1351            let key_bytes = u64::try_from(record.key.len())?;
1352            let document_bytes = u64::try_from(encoded_document_len(&record.value)?)?;
1353            Ok::<_, Box<dyn std::error::Error>>(
1354                total
1355                    .checked_add(key_bytes)
1356                    .and_then(|next| next.checked_add(document_bytes))
1357                    .ok_or_else(|| std::io::Error::other("scan byte total overflow"))?,
1358            )
1359        })?;
1360        opened.engine.put_records(Uuid::now_v7(), &records)?;
1361        let query = Query {
1362            filter: Filter::MatchAll,
1363            sort: Vec::new(),
1364            cursor: None,
1365            limit: 1,
1366            aggregation: None,
1367        };
1368
1369        let exact = opened.engine.query_with_byte_limit(
1370            &query,
1371            &ExecutionLimits::default(),
1372            total_bytes,
1373        )?;
1374        assert_eq!(exact.rows.len(), 1);
1375        assert!(matches!(
1376            opened.engine.query_with_byte_limit(
1377                &query,
1378                &ExecutionLimits::default(),
1379                total_bytes - 1,
1380            ),
1381            Err(BoundedEngineQueryError::ScannedByteBudgetExceeded { maximum })
1382                if maximum == total_bytes - 1
1383        ));
1384        Ok(())
1385    }
1386
1387    #[test]
1388    fn durable_vectors_survive_compaction_backup_restore_and_index_rebuild()
1389    -> Result<(), Box<dyn std::error::Error>> {
1390        let temporary = TestDirectory::new("durable-vectors-lifecycle")?;
1391        let root = temporary.path().join("data");
1392        let backup = temporary.path().join("backup");
1393        let restored = temporary.path().join("restored");
1394        let space = VectorSpaceName::new("semantic.v1")?;
1395        let definition = VectorSpaceDefinition::cosine(space.clone(), 3)?;
1396        let mut opened = HyphaeEngine::open(&root)?;
1397        opened
1398            .engine
1399            .define_vector_space(Uuid::now_v7(), definition.clone())?;
1400        opened.engine.put_vectors(
1401            Uuid::now_v7(),
1402            &space,
1403            &[
1404                (b"alpha".to_vec(), Q15Vector::new(vec![32_767, 0, 0])?),
1405                (b"beta".to_vec(), Q15Vector::new(vec![0, 32_767, 0])?),
1406            ],
1407        )?;
1408        let request = ExactRetrievalRequest {
1409            vector_space: space.clone(),
1410            query: Q15Vector::new(vec![32_767, 0, 0])?,
1411            limit: 2,
1412            minimum_score_nanos: -1_000_000_000,
1413            minimum_margin_nanos: 0,
1414        };
1415        let limits = ExactRetrievalLimits {
1416            max_candidates: 10,
1417            max_candidate_bytes: 64 * 1024,
1418            max_returned: 10,
1419            timeout: Duration::from_secs(1),
1420        };
1421        let expected = opened.engine.retrieve_exact(&request, &limits)?;
1422        assert!(matches!(
1423            &expected,
1424            ExactRetrievalOutcome::Matches { matches, .. }
1425                if matches.first().is_some_and(|matched| matched.key == b"alpha")
1426        ));
1427        let exact_candidate_bytes =
1428            u64::try_from(b"alpha".len() + b"beta".len() + (2 * 3 * std::mem::size_of::<i16>()))?;
1429        let exact_limits = ExactRetrievalLimits {
1430            max_candidate_bytes: exact_candidate_bytes,
1431            ..limits.clone()
1432        };
1433        assert_eq!(
1434            opened.engine.retrieve_exact(&request, &exact_limits)?,
1435            expected
1436        );
1437        let one_byte_short = ExactRetrievalLimits {
1438            max_candidate_bytes: exact_candidate_bytes - 1,
1439            ..limits.clone()
1440        };
1441        assert!(matches!(
1442            opened.engine.retrieve_exact(&request, &one_byte_short),
1443            Err(EngineError::ExactRetrieval(
1444                hyphae_retrieval::ExactRetrievalError::CandidateByteBudgetExceeded {
1445                    maximum
1446                }
1447            )) if maximum == exact_candidate_bytes - 1
1448        ));
1449        assert!(matches!(
1450            opened.engine.retrieve_exact(
1451                &request,
1452                &ExactRetrievalLimits {
1453                    timeout: Duration::ZERO,
1454                    ..limits.clone()
1455                }
1456            ),
1457            Err(EngineError::ExactRetrieval(
1458                hyphae_retrieval::ExactRetrievalError::TimedOut
1459            ))
1460        ));
1461        opened.engine.compact()?;
1462        assert_eq!(opened.engine.retrieve_exact(&request, &limits)?, expected);
1463        opened.engine.backup(&backup)?;
1464        drop(opened);
1465
1466        let reopened = HyphaeEngine::open(&root)?;
1467        assert_eq!(reopened.engine.retrieve_exact(&request, &limits)?, expected);
1468        drop(reopened);
1469        fs::remove_file(root.join("indexes/primary.redb"))?;
1470        let rebuilt = HyphaeEngine::open(&root)?;
1471        assert_eq!(rebuilt.engine.retrieve_exact(&request, &limits)?, expected);
1472        drop(rebuilt);
1473
1474        HyphaeEngine::restore_backup(&backup, &restored)?;
1475        let restored = HyphaeEngine::open(&restored)?;
1476        assert_eq!(restored.engine.retrieve_exact(&request, &limits)?, expected);
1477        Ok(())
1478    }
1479
1480    #[test]
1481    fn mixed_validity_vector_batch_is_rejected_without_partial_visibility()
1482    -> Result<(), Box<dyn std::error::Error>> {
1483        let temporary = TestDirectory::new("vector-batch-rollback")?;
1484        let root = temporary.path().join("data");
1485        let space = VectorSpaceName::new("semantic")?;
1486        let mut opened = HyphaeEngine::open(&root)?;
1487        opened.engine.define_vector_space(
1488            Uuid::now_v7(),
1489            VectorSpaceDefinition::cosine(space.clone(), 2)?,
1490        )?;
1491        let result = opened.engine.put_vectors(
1492            Uuid::now_v7(),
1493            &space,
1494            &[
1495                (b"valid".to_vec(), Q15Vector::new(vec![32_767, 0])?),
1496                (b"wrong".to_vec(), Q15Vector::new(vec![32_767, 0, 0])?),
1497            ],
1498        );
1499        assert!(result.is_err());
1500        let request = ExactRetrievalRequest {
1501            vector_space: space,
1502            query: Q15Vector::new(vec![32_767, 0])?,
1503            limit: 10,
1504            minimum_score_nanos: -1_000_000_000,
1505            minimum_margin_nanos: 0,
1506        };
1507        assert!(matches!(
1508            opened
1509                .engine
1510                .retrieve_exact(&request, &ExactRetrievalLimits::default())?,
1511            ExactRetrievalOutcome::Abstained(_)
1512        ));
1513        Ok(())
1514    }
1515
1516    fn lexical_value(title: &str, body: &str) -> Value {
1517        Value::Object(BTreeMap::from([
1518            ("body".to_owned(), Value::String(body.to_owned())),
1519            ("title".to_owned(), Value::String(title.to_owned())),
1520        ]))
1521    }
1522
1523    #[test]
1524    fn hybrid_deadline_includes_fusion_after_both_branches()
1525    -> Result<(), Box<dyn std::error::Error>> {
1526        let temporary = TestDirectory::new("hybrid-total-deadline")?;
1527        let name = VectorSpaceName::new("documents.v1")?;
1528        let mut opened = HyphaeEngine::open(temporary.path().join("data"))?;
1529        opened.engine.define_lexical_index(
1530            Uuid::now_v7(),
1531            LexicalIndexDefinition::new(
1532                name.clone(),
1533                vec![LexicalField {
1534                    path: FieldPath::field("title"),
1535                    weight_micros: 1_000_000,
1536                }],
1537            )?,
1538        )?;
1539        opened.engine.define_vector_space(
1540            Uuid::now_v7(),
1541            VectorSpaceDefinition::cosine(name.clone(), 2)?,
1542        )?;
1543        let lexical_request = LexicalRequest {
1544            index: name.clone(),
1545            query: "durable".into(),
1546            limit: 1,
1547        };
1548        let lexical_limits = LexicalLimits {
1549            timeout: Duration::from_secs(5),
1550            ..LexicalLimits::default()
1551        };
1552        let vector_request = ExactRetrievalRequest {
1553            vector_space: name,
1554            query: Q15Vector::new(vec![32_767, 0])?,
1555            limit: 1,
1556            minimum_score_nanos: -1_000_000_000,
1557            minimum_margin_nanos: 0,
1558        };
1559        let vector_limits = ExactRetrievalLimits {
1560            timeout: Duration::from_secs(5),
1561            ..ExactRetrievalLimits::default()
1562        };
1563        let hybrid_request = HybridRequest {
1564            lexical_weight: 1,
1565            vector_weight: 1,
1566            limit: 1,
1567        };
1568        let mut elapsed = [
1569            Duration::ZERO,
1570            Duration::from_secs(4),
1571            Duration::from_secs(10),
1572        ]
1573        .into_iter();
1574        let Err(error) = opened.engine.execute_hybrid_with_elapsed(
1575            &lexical_request,
1576            &lexical_limits,
1577            &vector_request,
1578            &vector_limits,
1579            &hybrid_request,
1580            Instant::now(),
1581            || elapsed.next().unwrap_or(Duration::MAX),
1582        ) else {
1583            return Err("fusion at the total deadline unexpectedly succeeded".into());
1584        };
1585        assert!(matches!(
1586            error,
1587            EngineError::ExactRetrieval(ExactRetrievalError::TimedOut)
1588        ));
1589        assert!(elapsed.next().is_none());
1590        Ok(())
1591    }
1592
1593    #[test]
1594    #[allow(clippy::too_many_lines)]
1595    fn lexical_and_hybrid_retrieval_survive_every_durable_lifecycle()
1596    -> Result<(), Box<dyn std::error::Error>> {
1597        let temporary = TestDirectory::new("lexical-hybrid-lifecycle")?;
1598        let root = temporary.path().join("data");
1599        let backup = temporary.path().join("backup");
1600        let restored = temporary.path().join("restored");
1601        let name = VectorSpaceName::new("documents.v1")?;
1602        let lexical_definition = LexicalIndexDefinition::new(
1603            name.clone(),
1604            vec![
1605                LexicalField {
1606                    path: FieldPath::field("body"),
1607                    weight_micros: 1_000_000,
1608                },
1609                LexicalField {
1610                    path: FieldPath::field("title"),
1611                    weight_micros: 2_000_000,
1612                },
1613            ],
1614        )?;
1615        let vector_definition = VectorSpaceDefinition::cosine(name.clone(), 2)?;
1616        let mut opened = HyphaeEngine::open(&root)?;
1617        opened.engine.put_records(
1618            Uuid::now_v7(),
1619            &[
1620                Record::new(b"alpha", lexical_value("Durable Rust", "offline engine")),
1621                Record::new(b"beta", lexical_value("Other", "durable storage")),
1622                Record::new(b"gamma", lexical_value("Unrelated", "nothing")),
1623            ],
1624        )?;
1625        opened
1626            .engine
1627            .define_lexical_index(Uuid::now_v7(), lexical_definition)?;
1628        opened
1629            .engine
1630            .define_vector_space(Uuid::now_v7(), vector_definition)?;
1631        opened.engine.put_vectors(
1632            Uuid::now_v7(),
1633            &name,
1634            &[
1635                (b"alpha".to_vec(), Q15Vector::new(vec![32_767, 0])?),
1636                (b"beta".to_vec(), Q15Vector::new(vec![30_000, 2_000])?),
1637                (b"gamma".to_vec(), Q15Vector::new(vec![0, 32_767])?),
1638            ],
1639        )?;
1640        let lexical_request = LexicalRequest {
1641            index: name.clone(),
1642            query: "durable".into(),
1643            limit: 3,
1644        };
1645        let lexical_limits = LexicalLimits {
1646            max_documents: 10,
1647            max_tokens: 100,
1648            max_candidates: 10,
1649            max_returned: 10,
1650            timeout: Duration::from_secs(2),
1651        };
1652        let vector_request = ExactRetrievalRequest {
1653            vector_space: name,
1654            query: Q15Vector::new(vec![32_767, 0])?,
1655            limit: 3,
1656            minimum_score_nanos: -1_000_000_000,
1657            minimum_margin_nanos: 0,
1658        };
1659        let vector_limits = ExactRetrievalLimits {
1660            max_candidates: 10,
1661            max_candidate_bytes: 64 * 1024,
1662            max_returned: 10,
1663            timeout: Duration::from_secs(2),
1664        };
1665        let hybrid_request = HybridRequest {
1666            lexical_weight: 1,
1667            vector_weight: 1,
1668            limit: 3,
1669        };
1670        let expected_lexical = opened
1671            .engine
1672            .retrieve_lexical(&lexical_request, &lexical_limits)?;
1673        assert!(matches!(
1674            &expected_lexical,
1675            LexicalOutcome::Matches { matches, .. }
1676                if matches.first().is_some_and(|matched| matched.key == b"alpha")
1677        ));
1678        let expected_hybrid = opened.engine.retrieve_hybrid(
1679            &lexical_request,
1680            &lexical_limits,
1681            &vector_request,
1682            &vector_limits,
1683            &hybrid_request,
1684        )?;
1685        assert!(matches!(
1686            &expected_hybrid,
1687            HybridOutcome::Matches { matches, .. }
1688                if matches.first().is_some_and(|matched| matched.key == b"alpha")
1689        ));
1690        opened.engine.compact()?;
1691        assert_eq!(
1692            opened
1693                .engine
1694                .retrieve_lexical(&lexical_request, &lexical_limits)?,
1695            expected_lexical
1696        );
1697        opened.engine.backup(&backup)?;
1698        drop(opened);
1699
1700        let reopened = HyphaeEngine::open(&root)?;
1701        assert_eq!(
1702            reopened.engine.retrieve_hybrid(
1703                &lexical_request,
1704                &lexical_limits,
1705                &vector_request,
1706                &vector_limits,
1707                &hybrid_request,
1708            )?,
1709            expected_hybrid
1710        );
1711        drop(reopened);
1712        fs::remove_file(root.join("indexes/primary.redb"))?;
1713        let rebuilt = HyphaeEngine::open(&root)?;
1714        assert_eq!(
1715            rebuilt
1716                .engine
1717                .retrieve_lexical(&lexical_request, &lexical_limits)?,
1718            expected_lexical
1719        );
1720        drop(rebuilt);
1721
1722        HyphaeEngine::restore_backup(&backup, &restored)?;
1723        let restored = HyphaeEngine::open(&restored)?;
1724        assert_eq!(
1725            restored.engine.retrieve_hybrid(
1726                &lexical_request,
1727                &lexical_limits,
1728                &vector_request,
1729                &vector_limits,
1730                &hybrid_request,
1731            )?,
1732            expected_hybrid
1733        );
1734        assert_eq!(restored.engine.snapshot()?.lexical_index_count, 1);
1735        Ok(())
1736    }
1737
1738    #[test]
1739    fn lexical_document_budget_returns_no_partial_ranking() -> Result<(), Box<dyn std::error::Error>>
1740    {
1741        let temporary = TestDirectory::new("lexical-budget")?;
1742        let name = VectorSpaceName::new("documents")?;
1743        let mut opened = HyphaeEngine::open(temporary.path().join("data"))?;
1744        opened.engine.put_records(
1745            Uuid::now_v7(),
1746            &[
1747                Record::new(b"a", lexical_value("one", "durable")),
1748                Record::new(b"b", lexical_value("two", "durable")),
1749            ],
1750        )?;
1751        opened.engine.define_lexical_index(
1752            Uuid::now_v7(),
1753            LexicalIndexDefinition::new(
1754                name.clone(),
1755                vec![LexicalField {
1756                    path: FieldPath::field("body"),
1757                    weight_micros: 1_000_000,
1758                }],
1759            )?,
1760        )?;
1761        let outcome = opened.engine.retrieve_lexical(
1762            &LexicalRequest {
1763                index: name,
1764                query: "durable".into(),
1765                limit: 2,
1766            },
1767            &LexicalLimits {
1768                max_documents: 1,
1769                ..LexicalLimits::default()
1770            },
1771        );
1772        assert!(matches!(
1773            outcome,
1774            Err(EngineError::Lexical(
1775                hyphae_retrieval::LexicalError::DocumentBudgetExceeded { maximum: 1 }
1776            ))
1777        ));
1778        Ok(())
1779    }
1780
1781    #[test]
1782    fn lexical_materialization_timeout_returns_typed_timeout()
1783    -> Result<(), Box<dyn std::error::Error>> {
1784        let temporary = TestDirectory::new("lexical-timeout")?;
1785        let name = VectorSpaceName::new("documents.timeout")?;
1786        let mut opened = HyphaeEngine::open(temporary.path().join("data"))?;
1787        opened.engine.put_record(
1788            Uuid::now_v7(),
1789            &Record::new(b"a", lexical_value("one", "durable")),
1790        )?;
1791        opened.engine.define_lexical_index(
1792            Uuid::now_v7(),
1793            LexicalIndexDefinition::new(
1794                name.clone(),
1795                vec![LexicalField {
1796                    path: FieldPath::field("body"),
1797                    weight_micros: 1_000_000,
1798                }],
1799            )?,
1800        )?;
1801
1802        let outcome = opened.engine.retrieve_lexical(
1803            &LexicalRequest {
1804                index: name,
1805                query: "durable".into(),
1806                limit: 1,
1807            },
1808            &LexicalLimits {
1809                timeout: Duration::ZERO,
1810                ..LexicalLimits::default()
1811            },
1812        );
1813
1814        assert!(matches!(
1815            outcome,
1816            Err(EngineError::Lexical(
1817                hyphae_retrieval::LexicalError::TimedOut
1818            ))
1819        ));
1820        Ok(())
1821    }
1822
1823    #[test]
1824    fn materialized_lexical_index_matches_reference_after_update_delete_and_rebuild()
1825    -> Result<(), Box<dyn std::error::Error>> {
1826        let temporary = TestDirectory::new("lexical-reference-equivalence")?;
1827        let root = temporary.path().join("data");
1828        let name = VectorSpaceName::new("documents.reference")?;
1829        let definition = LexicalIndexDefinition::new(
1830            name.clone(),
1831            vec![
1832                LexicalField {
1833                    path: FieldPath::field("body"),
1834                    weight_micros: 1_000_000,
1835                },
1836                LexicalField {
1837                    path: FieldPath::field("title"),
1838                    weight_micros: 2_000_000,
1839                },
1840            ],
1841        )?;
1842        let request = LexicalRequest {
1843            index: name,
1844            query: "durable rust engine".into(),
1845            limit: 10,
1846        };
1847        let limits = LexicalLimits {
1848            max_documents: 100,
1849            max_tokens: 10_000,
1850            max_candidates: 100,
1851            max_returned: 100,
1852            timeout: Duration::from_secs(2),
1853        };
1854        let mut records = vec![
1855            Record::new(
1856                b"alpha",
1857                lexical_value("Durable Rust", "offline engine durable durable"),
1858            ),
1859            Record::new(
1860                b"beta",
1861                lexical_value("Storage Engine", "rust transactions"),
1862            ),
1863            Record::new(b"gamma", lexical_value("Unrelated", "nothing relevant")),
1864            Record::new(
1865                b"delta",
1866                lexical_value("Rust Engine", "durable local search"),
1867            ),
1868        ];
1869        let mut opened = HyphaeEngine::open(&root)?;
1870        opened.engine.put_records(Uuid::now_v7(), &records)?;
1871        opened
1872            .engine
1873            .define_lexical_index(Uuid::now_v7(), definition.clone())?;
1874
1875        let reference = retrieve_lexical(&records, &definition, &request, &limits)?;
1876        assert_eq!(
1877            opened.engine.retrieve_lexical(&request, &limits)?,
1878            reference
1879        );
1880
1881        let updated = Record::new(
1882            b"gamma",
1883            lexical_value("Durable Engine", "rust rust offline"),
1884        );
1885        opened.engine.put_record(Uuid::now_v7(), &updated)?;
1886        records.retain(|record| record.key != b"gamma");
1887        records.push(updated);
1888        opened.engine.delete_record(Uuid::now_v7(), b"beta")?;
1889        records.retain(|record| record.key != b"beta");
1890
1891        let updated_reference = retrieve_lexical(&records, &definition, &request, &limits)?;
1892        assert_eq!(
1893            opened.engine.retrieve_lexical(&request, &limits)?,
1894            updated_reference
1895        );
1896        drop(opened);
1897
1898        fs::remove_file(root.join("indexes/primary.redb"))?;
1899        let rebuilt = HyphaeEngine::open(&root)?;
1900        assert_eq!(
1901            rebuilt.engine.retrieve_lexical(&request, &limits)?,
1902            updated_reference
1903        );
1904        Ok(())
1905    }
1906}