1#[doc(hidden)]
18pub mod seed;
19
20use crate::VectorExecutionReport;
21
22mod text;
23mod vector_index;
24use hawdb_core::{HawDBError, Result, RuntimeTaskContext};
25use hawdb_plan_cypher::VectorPhysicalPlan;
26use std::collections::BTreeMap;
27use std::mem::size_of;
28use std::num::NonZeroUsize;
29pub use text::{TextSeedExecutionOutput, TextSeedExecutionRequest, TextSeedExecutionRow};
30pub use vector_index::{
31 VectorIndexExecutionOutput, VectorIndexExecutionRequest, VectorIndexExecutionRow,
32};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct ExternalReadResultBudget {
36 pub max_rows: usize,
37 pub max_memory_bytes: NonZeroUsize,
38}
39
40#[derive(Debug, Clone, Copy)]
41pub struct ExternalReadResourceContract<'a> {
42 pub priority: u8,
43 pub max_parallelism: NonZeroUsize,
44 pub max_working_memory_bytes: NonZeroUsize,
45 pub result: ExternalReadResultBudget,
46 pub task_context: Option<&'a RuntimeTaskContext>,
47}
48
49impl ExternalReadResourceContract<'_> {
50 pub fn checkpoint(&self) -> Result<()> {
51 match self.task_context {
52 Some(task_context) => task_context.checkpoint().map_err(|reason| {
53 HawDBError::Execution(format!("external read task stopped: {reason}"))
54 }),
55 None => Ok(()),
56 }
57 }
58
59 pub fn reserved_memory_bytes(&self) -> usize {
60 self.max_working_memory_bytes
61 .get()
62 .saturating_add(self.result.max_memory_bytes.get())
63 }
64}
65
66pub struct VectorSeedExecutionRequest<'a> {
67 pub embedding: &'a [f32],
68 pub metadata_filters: &'a BTreeMap<String, String>,
69 pub vector_plan: &'a VectorPhysicalPlan,
70 pub resources: ExternalReadResourceContract<'a>,
71}
72
73pub struct VectorSeedExecutionRow {
74 pub id: String,
75 pub external_id: Option<String>,
76 pub score: f64,
77}
78
79pub struct VectorSeedExecutionOutput {
80 pub rows: Vec<VectorSeedExecutionRow>,
81 pub report: VectorExecutionReport,
82}
83
84impl VectorSeedExecutionOutput {
85 pub fn estimated_memory_bytes(&self) -> usize {
86 self.rows
87 .capacity()
88 .saturating_mul(size_of::<VectorSeedExecutionRow>())
89 .saturating_add(self.rows.iter().fold(0usize, |total, row| {
90 total.saturating_add(row.id.capacity()).saturating_add(
91 row.external_id
92 .as_ref()
93 .map_or(0, |external_id| external_id.capacity()),
94 )
95 }))
96 }
97
98 pub fn validate_result_budget(&self, budget: ExternalReadResultBudget) -> Result<()> {
99 if self.rows.len() > budget.max_rows {
100 return Err(HawDBError::read_budget_exceeded(
101 hawdb_core::ReadBudgetResource::Rows,
102 budget.max_rows,
103 format!(
104 "external vector read returned {} rows, exceeding result row budget {}",
105 self.rows.len(),
106 budget.max_rows
107 ),
108 ));
109 }
110 let memory_bytes = self.estimated_memory_bytes();
111 if memory_bytes > budget.max_memory_bytes.get() {
112 return Err(HawDBError::Execution(format!(
113 "external vector read returned {memory_bytes} estimated bytes, exceeding result memory budget {}",
114 budget.max_memory_bytes
115 )));
116 }
117 Ok(())
118 }
119}
120
121pub trait ExternalReadOperator {
122 fn execute_vector_index(
123 &mut self,
124 request: VectorIndexExecutionRequest<'_>,
125 ) -> Result<Box<dyn VectorIndexExecutionOutput>> {
126 request.resources.checkpoint()?;
127 Err(HawDBError::Execution(
128 "vector index search requires a bound canonical/index read domain".into(),
129 ))
130 }
131
132 fn execute_text_seed(
133 &mut self,
134 request: TextSeedExecutionRequest<'_>,
135 ) -> Result<TextSeedExecutionOutput> {
136 request.resources.checkpoint()?;
137 Err(HawDBError::Execution(
138 "text search capability is unavailable without a text projection".into(),
139 ))
140 }
141
142 fn execute_vector_seed(
143 &mut self,
144 request: VectorSeedExecutionRequest<'_>,
145 ) -> Result<VectorSeedExecutionOutput>;
146}
147
148#[doc(hidden)]
149pub struct NoExternalReadOperator;
150
151impl ExternalReadOperator for NoExternalReadOperator {
152 fn execute_vector_seed(
153 &mut self,
154 _request: VectorSeedExecutionRequest<'_>,
155 ) -> Result<VectorSeedExecutionOutput> {
156 Err(HawDBError::Execution(
157 "vector search capability is unavailable without a search projection".to_string(),
158 ))
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::{
166 VectorCompressionMode, VectorExecutionBackend, VectorFallbackReasonCode, VectorScoreSource,
167 };
168 use hawdb_core::RuntimeCancellationToken;
169 use hawdb_plan_cypher::VectorCandidateSource;
170
171 pub(super) fn empty_report() -> VectorExecutionReport {
172 VectorExecutionReport {
173 backend: VectorExecutionBackend::ScalarFlat,
174 compression_mode: VectorCompressionMode::Disabled,
175 candidate_source: VectorCandidateSource::Scalar,
176 backend_selection_reason: None,
177 estimated_raw_vector_bytes: None,
178 filter_selectivity_per_million: None,
179 candidate_score_source: VectorScoreSource::RawVector,
180 final_score_source: VectorScoreSource::RawVector,
181 generated_candidate_count: 0,
182 descriptor_pruned_count: 0,
183 scalar_filtered_count: 0,
184 residual_filtered_count: 0,
185 candidate_scan_rounds: 0,
186 reranked_candidate_count: 0,
187 returned_count: 0,
188 raw_vector_bytes_read: 0,
189 candidate_scan_metrics: None,
190 index_covered_document_count: None,
191 index_candidate_document_count: None,
192 index_coverage_complete: None,
193 fallback_reason_codes: Vec::<VectorFallbackReasonCode>::new(),
194 }
195 }
196
197 #[test]
198 fn external_vector_output_fails_closed_on_row_and_memory_budgets() {
199 let output = VectorSeedExecutionOutput {
200 rows: vec![VectorSeedExecutionRow {
201 id: "memory-1".to_string(),
202 external_id: Some("external-1".to_string()),
203 score: 1.0,
204 }],
205 report: empty_report(),
206 };
207
208 let row_error = output
209 .validate_result_budget(ExternalReadResultBudget {
210 max_rows: 0,
211 max_memory_bytes: NonZeroUsize::new(1024).unwrap(),
212 })
213 .unwrap_err();
214 assert!(matches!(row_error, HawDBError::ReadBudgetExceeded(cause)
215 if cause.resource == hawdb_core::ReadBudgetResource::Rows && cause.limit == 0));
216 let memory_error = output
217 .validate_result_budget(ExternalReadResultBudget {
218 max_rows: 1,
219 max_memory_bytes: NonZeroUsize::MIN,
220 })
221 .unwrap_err();
222 assert!(matches!(memory_error, HawDBError::Execution(_)));
223 }
224
225 #[test]
226 fn external_read_contract_observes_cancellation() {
227 let cancellation = RuntimeCancellationToken::new();
228 let task_context = RuntimeTaskContext::without_deadline(cancellation.clone());
229 let contract = ExternalReadResourceContract {
230 priority: 128,
231 max_parallelism: NonZeroUsize::MIN,
232 max_working_memory_bytes: NonZeroUsize::MIN,
233 result: ExternalReadResultBudget {
234 max_rows: 1,
235 max_memory_bytes: NonZeroUsize::MIN,
236 },
237 task_context: Some(&task_context),
238 };
239
240 assert!(contract.checkpoint().is_ok());
241 assert!(cancellation.cancel());
242 assert!(contract.checkpoint().is_err());
243 }
244}