1use std::num::NonZeroUsize;
36use std::sync::Arc;
37
38use haem_frames::embedding::{ItemCode, input_refusal};
39use haematite::db::ObservedChange;
40use haematite::tree::{DiffBudget, DiffInterval, Hash, empty_root_hash};
41use serde_json::Value;
42
43use super::embed_job::Reach;
44use super::embedded::{self, Declaration};
45use super::{
46 Captured, Catalogue, DocumentRevision, Failure, Held, Storage, document_key, identifier, reads,
47 revision_key,
48};
49use crate::serve::protocol::{Selection, Version};
50
51const DIFF_NODES: usize = 4096;
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum Waiting {
57 Text {
59 id: String,
60 made_from: Version,
61 text: String,
62 },
63 Removed {
64 id: String,
65 },
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct Pending {
71 pub toward: Version,
72 pub waiting: Vec<Waiting>,
73 pub no_text: Vec<String>,
76 pub too_long: Vec<String>,
79 pub empty_input: Vec<String>,
83 pub reach: Reach,
84}
85
86impl Storage {
87 pub(in crate::serve) fn pending_reader(&self) -> PendingReader {
88 PendingReader {
89 reader: Arc::clone(&self.reader),
90 catalogue: self.catalogue(),
91 }
92 }
93
94 pub fn embed_pending(
103 &self,
104 branch: &str,
105 collection: &str,
106 field: &str,
107 page_records: NonZeroUsize,
108 input_bytes: NonZeroUsize,
109 ) -> Result<Option<Pending>, Failure> {
110 self.pending_reader()
111 .read(branch, collection, field, page_records, input_bytes)
112 .map(|page| page.map(|(pending, _)| pending))
113 }
114}
115
116pub(in crate::serve) struct PendingReader {
118 reader: Arc<haematite::db::ReadOnlyDatabase>,
119 catalogue: Catalogue,
120}
121
122pub(in crate::serve) struct PendingPage {
123 pub pending: Pending,
124 pub declaration: Declaration,
125 pub position: Option<embedded::Position>,
126}
127
128impl PendingReader {
129 pub fn read(
130 &self,
131 branch: &str,
132 collection: &str,
133 field: &str,
134 page_records: NonZeroUsize,
135 input_bytes: NonZeroUsize,
136 ) -> Result<Option<(Pending, Declaration)>, Failure> {
137 self.read_page(branch, collection, field, page_records, input_bytes)
138 .map(|page| page.map(|page| (page.pending, page.declaration)))
139 }
140
141 pub fn read_page(
142 &self,
143 branch: &str,
144 collection: &str,
145 field: &str,
146 page_records: NonZeroUsize,
147 input_bytes: NonZeroUsize,
148 ) -> Result<Option<PendingPage>, Failure> {
149 identifier(branch)?;
150 identifier(collection)?;
151 identifier(field)?;
152 let head = self.catalogue.capture_held(&Selection {
153 branch: branch.to_owned(),
154 version: None,
155 })?;
156 let declaration = self.declared(&head, collection, field)?;
157 let companion = self.catalogue.capture_companion_held(branch)?;
158 let position = self.position(companion.as_ref(), collection, field)?;
159 let (from_branch, through) = position
160 .as_ref()
161 .map_or((branch, None), |at| (at.branch.as_str(), at.through));
162 let open = position.as_ref().and_then(|at| at.toward.as_ref());
163 let toward = open.map_or(head.version, |open| open.version);
164 if open.is_none() && from_branch == branch && through == Some(head.version) {
165 return Ok(None);
166 }
167 let rewalk = open.is_some_and(|open| through == Some(open.version))
169 && self.rewalking(companion.as_ref(), collection, field)?;
170 let from_hold = through
171 .filter(|_| !rewalk)
172 .map(|through| self.kept(from_branch, through))
173 .transpose()?;
174 let from: Vec<Hash> = from_hold.as_ref().map_or_else(
175 || vec![empty_root_hash(); self.reader.shard_count()],
176 |held| held.shards.iter().map(|shard| shard.head).collect(),
177 );
178 let to = self.kept(branch, toward)?;
179 let after = open.map(|open| revision_key(collection, &open.after));
180 let (changes, more) =
181 self.walk_page(collection, &from, &to, after.as_deref(), page_records)?;
182 let mut pending = Pending {
183 toward,
184 waiting: Vec::new(),
185 no_text: Vec::new(),
186 too_long: Vec::new(),
187 empty_input: Vec::new(),
188 reach: Reach::End,
189 };
190 let sort = Sort {
191 to: &to,
192 declaration: &declaration,
193 collection,
194 field,
195 limit: u64::try_from(input_bytes.get()).unwrap_or(u64::MAX),
196 rewalk: companion.as_deref().filter(|_| rewalk),
197 };
198 for change in &changes {
199 self.sort(&mut pending, change, &sort)?;
200 }
201 if more {
202 let last = changes.last().ok_or_else(|| {
203 Failure::new(
204 "embed_walk_stalled",
205 "a page with more after it holds no change",
206 )
207 })?;
208 pending.reach = Reach::Upto(record_id(collection, last.key())?);
209 }
210 Ok(Some(PendingPage {
211 pending,
212 declaration,
213 position,
214 }))
215 }
216
217 fn declared(
218 &self,
219 at: &Captured,
220 collection: &str,
221 field: &str,
222 ) -> Result<Declaration, Failure> {
223 let undeclared = || {
224 Failure::new(
225 "embed_field_not_declared",
226 format!("{collection}.{field} is not declared embedded"),
227 )
228 };
229 let bytes = reads::get_bytes(
230 &self.reader,
231 at,
232 &super::embed_job::field_key(collection, field),
233 )?
234 .ok_or_else(undeclared)?;
235 match embedded::read_field(&bytes)? {
236 embedded::Field::Embedded(declaration) => Ok(declaration),
237 embedded::Field::Supplied { .. } => Err(undeclared()),
238 }
239 }
240
241 fn sort(
249 &self,
250 pending: &mut Pending,
251 change: &ObservedChange,
252 sort: &Sort<'_>,
253 ) -> Result<(), Failure> {
254 let id = record_id(sort.collection, change.key())?;
255 let revision = match change {
256 ObservedChange::Removed { .. } => {
257 pending.waiting.push(Waiting::Removed { id });
258 return Ok(());
259 }
260 ObservedChange::Added { value, .. }
261 | ObservedChange::Modified {
262 new_value: value, ..
263 } => value,
264 };
265 if let Some(companion) = sort.rewalk
266 && self.has_vectors(companion, sort, &id)?
267 {
268 return Ok(());
269 }
270 let made_from = serde_json::from_slice::<DocumentRevision>(revision)
271 .map_err(Failure::storage)?
272 .written_at;
273 let named = sort.rewalk.is_none();
274 match text(
275 &self.reader,
276 sort.to,
277 sort.declaration,
278 sort.collection,
279 &id,
280 )? {
281 None if named => pending.no_text.push(id),
282 Some(text) => match input_refusal(&text, sort.limit) {
283 None => pending.waiting.push(Waiting::Text {
284 id,
285 made_from,
286 text,
287 }),
288 Some(ItemCode::EmbedInputEmpty) if named => pending.empty_input.push(id),
289 Some(_) if named => pending.too_long.push(id),
290 Some(_) => {}
291 },
292 None => {}
293 }
294 Ok(())
295 }
296
297 fn rewalking(
300 &self,
301 companion: Option<&Held>,
302 collection: &str,
303 field: &str,
304 ) -> Result<bool, Failure> {
305 let Some(companion) = companion else {
306 return Ok(false);
307 };
308 let key = embedded::rewalk_key(collection, field);
309 Ok(reads::get_bytes(&self.reader, companion, &key)?.is_some())
310 }
311
312 fn has_vectors(
315 &self,
316 companion: &Captured,
317 sort: &Sort<'_>,
318 id: &str,
319 ) -> Result<bool, Failure> {
320 let vector = embedded::vector_key(sort.collection, sort.field, id);
321 let window = embedded::window_key(sort.collection, sort.field, id, 0);
322 Ok(
323 reads::get_bytes(&self.reader, companion, &vector)?.is_some()
324 || reads::get_bytes(&self.reader, companion, &window)?.is_some(),
325 )
326 }
327
328 fn position(
329 &self,
330 companion: Option<&Held>,
331 collection: &str,
332 field: &str,
333 ) -> Result<Option<embedded::Position>, Failure> {
334 let Some(companion) = companion else {
335 return Ok(None);
336 };
337 reads::get_bytes(
338 &self.reader,
339 companion,
340 &embedded::position_key(collection, field),
341 )?
342 .map(|bytes| embedded::read_position(&bytes))
343 .transpose()
344 }
345
346 fn walk_page(
349 &self,
350 collection: &str,
351 from: &[Hash],
352 to: &Captured,
353 after: Option<&[u8]>,
354 page_records: NonZeroUsize,
355 ) -> Result<(Vec<ObservedChange>, bool), Failure> {
356 let mut after = after.map(<[u8]>::to_vec);
357 loop {
358 let (changes, stop) =
359 self.walk_once(collection, from, to, after.as_deref(), page_records)?;
360 match stop {
361 Some(stop) if changes.is_empty() => after = Some(stop),
362 stop => return Ok((changes, stop.is_some())),
363 }
364 }
365 }
366
367 fn walk_once(
370 &self,
371 collection: &str,
372 from: &[Hash],
373 to: &Captured,
374 after: Option<&[u8]>,
375 page_records: NonZeroUsize,
376 ) -> Result<(Vec<ObservedChange>, Option<Vec<u8>>), Failure> {
377 let lower = format!("m\0{collection}\0").into_bytes();
378 let upper = format!("m\0{collection}\u{1}").into_bytes();
379 let budget = DiffBudget {
380 max_changes: page_records.checked_add(1).ok_or_else(|| {
383 Failure::new(
384 "embed_page_records_overflow",
385 "the page setting leaves no room to look one change past it",
386 )
387 })?,
388 max_nodes: NonZeroUsize::new(DIFF_NODES)
389 .ok_or_else(|| Failure::new("diff_budget", "node budget must be nonzero"))?,
390 };
391 let mut changes = Vec::new();
392 let mut bound: Option<Vec<u8>> = None;
394 for shard in &to.shards {
395 let from_root = from.get(shard.shard_id).copied().ok_or_else(|| {
396 Failure::new(
397 "incomplete_roots",
398 "the walk's start does not name every shard",
399 )
400 })?;
401 let page = self
402 .reader
403 .changes_page_in_range(
404 shard.shard_id,
405 from_root,
406 shard.head,
407 DiffInterval {
408 lower: Some(&lower),
409 upper: Some(&upper),
410 },
411 after,
412 budget,
413 )
414 .map_err(Failure::observer)?;
415 if let Some(next) = page.next_after
416 && bound.as_ref().is_none_or(|bound| next < *bound)
417 {
418 bound = Some(next);
419 }
420 changes.extend(page.entries);
421 }
422 if let Some(bound) = &bound {
423 changes.retain(|change| change.key() <= bound.as_slice());
424 }
425 changes.sort_by(|left, right| left.key().cmp(right.key()));
426 if changes.len() > page_records.get() {
427 changes.truncate(page_records.get());
428 let last = changes.last().map(|change| change.key().to_vec());
429 return Ok((changes, last));
430 }
431 Ok((changes, bound))
432 }
433
434 fn kept(&self, branch: &str, at: Version) -> Result<Held, Failure> {
437 self.catalogue
438 .capture_held(&Selection {
439 branch: branch.to_owned(),
440 version: Some(at),
441 })
442 .map_err(|failure| walk_unreadable(branch, at, failure))
443 }
444}
445
446fn walk_unreadable(branch: &str, at: Version, failure: Failure) -> Failure {
451 const UNREADABLE: [&str; 3] = [
452 "version_missing",
453 "version_released",
454 "version_generation_ended",
455 ];
456 if UNREADABLE.contains(&failure.code.as_str()) {
457 Failure::new(
458 "embed_walk_released",
459 format!(
460 "{branch} at {}.{} is no longer kept, so the walk cannot be read ({}: {})",
461 at.generation, at.sequence, failure.code, failure.message
462 ),
463 )
464 } else {
465 failure
466 }
467}
468
469struct Sort<'a> {
472 to: &'a Captured,
473 declaration: &'a Declaration,
474 collection: &'a str,
475 field: &'a str,
476 limit: u64,
478 rewalk: Option<&'a Captured>,
480}
481
482fn record_id(collection: &str, key: &[u8]) -> Result<String, Failure> {
483 let corrupt = || Failure::new("embed_record_id_corrupt", "a revision key names no record");
484 let prefix = format!("m\0{collection}\0");
485 let id = key.strip_prefix(prefix.as_bytes()).ok_or_else(corrupt)?;
486 String::from_utf8(id.to_vec()).map_err(|_not_utf8| corrupt())
487}
488
489fn text(
491 reader: &haematite::db::ReadOnlyDatabase,
492 at: &Captured,
493 declaration: &Declaration,
494 collection: &str,
495 id: &str,
496) -> Result<Option<String>, Failure> {
497 let Some(bytes) = reads::get_bytes(reader, at, &document_key(collection, id))? else {
498 return Ok(None);
499 };
500 let document: Value = serde_json::from_slice(&bytes).map_err(Failure::storage)?;
501 Ok(document
502 .get(&declaration.source)
503 .and_then(Value::as_str)
504 .map(str::to_owned))
505}
506
507#[cfg(test)]
508#[path = "embed_pending_tests.rs"]
509mod tests;