Skip to main content

uqa_storage/diskann_index/build/
capture.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Build membership comes from the retained canonical source, not its fingerprint or writer allocation order.
8
9use std::path::Path;
10
11use super::{
12    invalid, DiskANNBuildInput, DiskANNBuildSink, DiskANNGenerationOptions, DiskANNMergedGraph,
13    DiskANNTemporaryBudget,
14};
15use crate::diskann_index::{
16    format::{
17        DiskANNBuildCoverage, DiskANNChangeIdentity, DiskANNGeneration, DiskANNManifest,
18        DiskANNOriginSummary,
19    },
20    DiskANNCanonicalRead,
21};
22use crate::{read_control::StorageReadControl, StorageBackendResult};
23
24mod origins;
25#[cfg(test)]
26mod tests;
27
28/// Encrypted build input and the exact committed/private source used to produce it. Moving the source keeps its original leases and controls alive through construction.
29pub struct DiskANNBuildCapture<S> {
30    input: DiskANNBuildInput,
31    source: S,
32    origins: origins::Origins,
33}
34
35impl<S: DiskANNCanonicalRead> DiskANNBuildCapture<S> {
36    pub fn capture(
37        generation: DiskANNGeneration,
38        source: S,
39        directory: &Path,
40        temporary: &DiskANNTemporaryBudget,
41        control: &StorageReadControl,
42    ) -> StorageBackendResult<Self> {
43        source.check_control(control)?;
44        let mut origins = origins::Origins::new(directory, temporary, control)?;
45        let input = DiskANNBuildInput::capture(
46            generation,
47            source.dimensions(),
48            directory,
49            temporary,
50            control,
51            |visit| origins.capture(&source, control, visit),
52        )?;
53        source.check_control(control)?;
54        Ok(Self {
55            input,
56            source,
57            origins,
58        })
59    }
60
61    pub fn input(&self) -> &DiskANNBuildInput {
62        &self.input
63    }
64
65    /// Write graph/side artifacts and every captured document origin to the same unpublished generation. Sealing and publication remain explicit owner operations.
66    pub fn write_generation(
67        &self,
68        graph: &DiskANNMergedGraph,
69        options: DiskANNGenerationOptions,
70        sink: &mut dyn DiskANNBuildSink,
71    ) -> StorageBackendResult<DiskANNManifest> {
72        let control = &self.input.control;
73        self.source.check_control(control)?;
74        control.check_value_size(DiskANNManifest::MAX_ENCODED_BYTES, options.max_record_bytes)?;
75        self.origins.check_record_limit(
76            self.input.coverage().generation(),
77            self.input.dimensions(),
78            options.max_record_bytes,
79            control,
80        )?;
81        let manifest = self.input.write_generation(graph, options, sink)?;
82        self.origins.write(
83            self.input.coverage().generation(),
84            self.input.dimensions(),
85            sink,
86            options.max_record_bytes,
87            control,
88        )?;
89        self.source.check_control(control)?;
90        manifest.with_origins(self.origins.summary())
91    }
92
93    /// Bind the completed manifest to this capture and release temporary input. Physical sealing, provider/index binding and atomic publication still belong to their storage owners.
94    pub fn finish(
95        self,
96        manifest: &DiskANNManifest,
97        control: &StorageReadControl,
98    ) -> StorageBackendResult<DiskANNCanonicalCoverage<S>> {
99        self.input.control.check()?;
100        self.source.check_control(control)?;
101        let actual = manifest.input();
102        if actual.coverage != self.input.coverage()
103            || actual.nodes != self.input.node_count()
104            || actual.side_vectors != self.input.side_count()
105            || manifest
106                .origins()
107                .is_some_and(|origins| origins != self.origins.summary())
108        {
109            return Err(invalid(
110                "manifest does not match the retained build capture",
111            ));
112        }
113        control.check()?;
114        Ok(DiskANNCanonicalCoverage {
115            source: self.source,
116            fingerprint: self.input.coverage(),
117            control: self.input.control.clone(),
118            origins: self.origins.summary(),
119            manifest: *manifest,
120        })
121    }
122}
123
124/// Exact origin membership on one completed build's retained source, including empty tensors. This process-owned evidence cannot be reconstructed from a manifest hash after losing its source.
125pub struct DiskANNCanonicalCoverage<S> {
126    source: S,
127    fingerprint: DiskANNBuildCoverage,
128    control: StorageReadControl,
129    origins: DiskANNOriginSummary,
130    manifest: DiskANNManifest,
131}
132
133impl<S: DiskANNCanonicalRead> DiskANNCanonicalCoverage<S> {
134    /// Preserve the original capture's controls independently of later operation allowances.
135    pub fn check_control(&self, control: &StorageReadControl) -> StorageBackendResult<()> {
136        self.control.check()?;
137        self.source.check_control(control)
138    }
139
140    /// The exact manifest accepted by this capture, including separate graph/side counts and artifact identities. Publication must compare it with the actual sealed record.
141    pub fn manifest(&self) -> &DiskANNManifest {
142        &self.manifest
143    }
144
145    pub fn fingerprint(&self) -> DiskANNBuildCoverage {
146        self.fingerprint
147    }
148
149    /// Expected complete origin artifact from the original capture, including empty tensors. Publication must match it to the actually sealed manifest while this source is retained.
150    pub fn origins(&self) -> DiskANNOriginSummary {
151        self.origins
152    }
153
154    /// Preserve the concrete provider source for its owner to validate table/field identity before consuming coverage in a publication or retirement operation.
155    pub fn source(&self) -> &S {
156        &self.source
157    }
158
159    /// A change is covered exactly when this source selects its complete canonical tensor with the same original writer and mutation revision. Neither absence nor an earlier writer allocation implies coverage.
160    pub fn contains(
161        &self,
162        change: DiskANNChangeIdentity,
163        control: &StorageReadControl,
164    ) -> StorageBackendResult<bool> {
165        self.control.check()?;
166        self.source.check_control(control)?;
167        let actual = self.source.origin(change.document(), control)?;
168        self.control.check()?;
169        self.source.check_control(control)?;
170        Ok(actual == Some(change.version()))
171    }
172}