1use std::num::NonZeroUsize;
43
44use haematite::branch::{CommitDurability, CommitRequest};
45
46use super::embedded::{self, Declaration, Field, ModelName, Position, Provenance, Toward};
47use super::{
48 BranchKind, Captured, DocumentRevision, Failure, Storage, branch_name, create_branch_with_kind,
49 current_timestamp, identifier, open_branch, reads, revision_key, version,
50};
51use crate::serve::protocol::Version;
52
53pub const DEFAULT_PAGE_RECORDS: usize = 64;
55
56#[derive(Debug, Clone, PartialEq)]
58pub struct Made {
59 pub id: String,
60 pub made_from: Version,
61 pub vector: Vec<f32>,
62}
63
64#[derive(Debug, Clone, PartialEq)]
67pub struct Window {
68 pub start: u64,
69 pub end: u64,
70 pub vector: Vec<f32>,
71}
72
73#[derive(Debug, Clone, PartialEq)]
76pub struct MadeWindows {
77 pub id: String,
78 pub made_from: Version,
79 pub windows: Vec<Window>,
80}
81
82#[derive(Debug, Clone, PartialEq)]
84pub struct Page {
85 pub collection: String,
86 pub field: String,
87 pub model: ModelName,
89 pub made: Vec<Made>,
90 pub windowed: Vec<MadeWindows>,
93 pub removed: Vec<String>,
96 pub cleared: Vec<String>,
100 pub toward: Version,
102 pub reach: Reach,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub enum Reach {
109 Upto(String),
112 End,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct Committed {
119 pub written: usize,
120 pub removed: usize,
121 pub skipped: Vec<String>,
123 pub position: Position,
124}
125
126const fn order(at: Version) -> (u64, u64) {
127 (at.generation, at.sequence)
128}
129
130#[must_use]
132pub fn field_key(collection: &str, field: &str) -> Vec<u8> {
133 format!("w\0{collection}\0{field}").into_bytes()
134}
135
136fn walk(
146 previous: Option<&Position>,
147 branch: &str,
148 page: &Page,
149) -> Result<(String, Option<Version>, Option<String>), Failure> {
150 let Some(previous) = previous else {
151 return Ok((branch.to_owned(), None, None));
152 };
153 if let Some(open) = &previous.toward {
154 if open.version != page.toward {
155 return Err(Failure::new(
156 "embed_walk_open",
157 "a new walk cannot begin while one is open",
158 ));
159 }
160 if let Reach::Upto(after) = &page.reach
161 && after.as_bytes() < open.after.as_bytes()
162 {
163 return Err(Failure::new(
164 "embed_walk_backwards",
165 "a page cannot reach back past the walk's last record",
166 ));
167 }
168 return Ok((
169 previous.branch.clone(),
170 previous.through,
171 Some(open.after.clone()),
172 ));
173 }
174 if previous.branch == branch
175 && previous
176 .through
177 .is_some_and(|through| order(page.toward) <= order(through))
178 {
179 return Err(Failure::new(
180 "embed_position_backwards",
181 "a walk must reach past the job's position",
182 ));
183 }
184 Ok((previous.branch.clone(), previous.through, None))
185}
186
187struct Writes<'page> {
189 at: Option<Captured>,
191 page: &'page Page,
192 pending: Vec<(Vec<u8>, Option<Vec<u8>>)>,
193 dimensions: usize,
194}
195
196impl Writes<'_> {
197 fn length(&self, id: &str, vector: &[f32]) -> Result<(), Failure> {
200 if vector.len() == self.dimensions {
201 return Ok(());
202 }
203 Err(Failure::new(
204 "embed_vector_length",
205 format!(
206 "record {id} has {} members; the field is declared with {}",
207 vector.len(),
208 self.dimensions
209 ),
210 ))
211 }
212}
213
214fn stale(id: &str) -> Failure {
215 Failure::new(
216 "embed_page_stale_text",
217 format!("record {id} was not read at the page's version"),
218 )
219}
220
221fn page_size(branch: &str, page: &Page, page_records: NonZeroUsize) -> Result<usize, Failure> {
222 identifier(branch)?;
223 identifier(&page.collection)?;
224 identifier(&page.field)?;
225 let named = page
226 .made
227 .len()
228 .saturating_add(page.windowed.len())
229 .saturating_add(page.removed.len())
230 .saturating_add(page.cleared.len());
231 if named > page_records.get() {
232 return Err(Failure::new(
233 "embed_page_too_large",
234 format!("a page names {named} records; the page setting is {page_records}"),
235 ));
236 }
237 Ok(named)
238}
239
240fn page_position(branch: &str, page: &Page, owner: String, through: Option<Version>) -> Position {
241 match &page.reach {
242 Reach::Upto(after) => Position {
243 branch: owner,
244 through,
245 toward: Some(Toward {
246 version: page.toward,
247 after: after.clone(),
248 }),
249 },
250 Reach::End => Position {
251 branch: branch.to_owned(),
252 through: Some(page.toward),
253 toward: None,
254 },
255 }
256}
257
258fn validate_page_walk(page: &Page, done: Option<&str>) -> Result<(), Failure> {
259 let ids = page
260 .made
261 .iter()
262 .map(|made| &made.id)
263 .chain(page.windowed.iter().map(|made| &made.id))
264 .chain(&page.removed)
265 .chain(&page.cleared);
266 for id in ids {
267 let past = done.is_some_and(|done| id.as_bytes() <= done.as_bytes());
268 let beyond = match &page.reach {
269 Reach::Upto(after) => id.as_bytes() > after.as_bytes(),
270 Reach::End => false,
271 };
272 if past || beyond {
273 return Err(Failure::new(
274 "embed_page_outside_walk",
275 format!("record {id} is not between the walk's last record and this page's"),
276 ));
277 }
278 }
279 Ok(())
280}
281
282impl Storage {
283 pub fn embed_commit(
295 &mut self,
296 branch: &str,
297 page: &Page,
298 page_records: NonZeroUsize,
299 ) -> Result<Committed, Failure> {
300 let named = page_size(branch, page, page_records)?;
301 let record = self
302 .refs
303 .get(&branch_name(branch))
304 .ok_or_else(|| Failure::new("branch_missing", branch))?;
305 let head = Captured {
306 branch: branch.to_owned(),
307 version: version(record),
308 shards: record.shards.clone(),
309 };
310 if order(page.toward) > order(head.version) {
311 return Err(Failure::new(
312 "embed_position_ahead",
313 "a page cannot complete a version the branch has not reached",
314 ));
315 }
316 let declaration = self.declared(&head, &page.collection, &page.field)?;
317 let tag = embedded::model_tag(&page.model)?;
318 if tag != embedded::model_tag(&declaration.model)? {
319 return Err(Failure::new(
320 "embed_model_mismatch",
321 format!(
322 "the vectors are {}'s; the field is declared for {}",
323 page.model.name, declaration.model.name
324 ),
325 ));
326 }
327 let companion = embedded::companion_name(branch);
328 let position_key = embedded::position_key(&page.collection, &page.field);
329 let previous = self.position(&companion, &position_key)?;
330 let (owner, through, done) = walk(previous.as_ref(), branch, page)?;
331 let position = page_position(branch, page, owner, through);
332 validate_page_walk(page, done.as_deref())?;
333 let mut writes = Writes {
334 at: self.companion_at(&companion),
335 page,
336 pending: Vec::with_capacity(named.saturating_add(1)),
337 dimensions: declaration.dimensions.get(),
338 };
339 let (mut written, mut skipped) = (0_usize, Vec::new());
340 for made in &page.made {
341 if self.write_made(&head, &mut writes, made, tag)? {
342 written = written.saturating_add(1);
343 } else {
344 skipped.push(made.id.clone());
345 }
346 }
347 for made in &page.windowed {
348 if self.write_windows(&head, &mut writes, made, tag)? {
349 written = written.saturating_add(1);
350 } else {
351 skipped.push(made.id.clone());
352 }
353 }
354 for id in &page.removed {
355 if let Some(now) = self.revision(&head, &page.collection, id)?
356 && order(now) <= order(page.toward)
357 {
358 return Err(stale(id));
359 }
360 let key = embedded::vector_key(&page.collection, &page.field, id);
361 writes.pending.push((key, None));
362 self.clear_windows(&mut writes, id, 0)?;
363 }
364 for id in &page.cleared {
367 let key = embedded::vector_key(&page.collection, &page.field, id);
368 writes.pending.push((key, None));
369 self.clear_windows(&mut writes, id, 0)?;
370 }
371 self.end_rewalk(&mut writes)?;
372 let mut writes = writes.pending;
373 writes.push((position_key, Some(embedded::encode_position(&position)?)));
374 if self.refs.get(&companion).is_none() {
375 create_branch_with_kind(
376 &companion,
377 self.writer.base_roots().map_err(Failure::database)?,
378 BranchKind::Namespace,
379 &mut self.refs,
380 &self.registry,
381 current_timestamp(),
382 )
383 .map_err(Failure::branch_commit)?;
384 }
385 self.commit_namespace(&companion, &writes)?;
386 Ok(Committed {
387 written,
388 removed: page.removed.len(),
389 skipped,
390 position,
391 })
392 }
393
394 fn write_made(
400 &self,
401 head: &Captured,
402 writes: &mut Writes<'_>,
403 made: &Made,
404 tag: embedded::ModelTag,
405 ) -> Result<bool, Failure> {
406 writes.length(&made.id, &made.vector)?;
407 if !self.current(head, writes.page, &made.id, made.made_from)? {
408 return Ok(false);
409 }
410 let provenance = Provenance {
411 made_from: made.made_from,
412 tag,
413 };
414 let value = embedded::encode_embedded(&provenance, &made.vector);
415 let key = embedded::vector_key(&writes.page.collection, &writes.page.field, &made.id);
416 writes.pending.push((key, Some(value)));
417 self.clear_windows(writes, &made.id, 0)?;
418 Ok(true)
419 }
420
421 fn end_rewalk(&self, writes: &mut Writes<'_>) -> Result<(), Failure> {
427 let (Reach::End, Some(at)) = (&writes.page.reach, &writes.at) else {
428 return Ok(());
429 };
430 let key = embedded::rewalk_key(&writes.page.collection, &writes.page.field);
431 if reads::get_bytes(&self.reader, at, &key)?.is_some() {
432 writes.pending.push((key, None));
433 }
434 Ok(())
435 }
436
437 fn current(
443 &self,
444 head: &Captured,
445 page: &Page,
446 id: &str,
447 made_from: Version,
448 ) -> Result<bool, Failure> {
449 if order(made_from) > order(page.toward) {
450 return Err(stale(id));
451 }
452 match self.revision(head, &page.collection, id)? {
453 Some(now) if now == made_from => Ok(true),
454 Some(now) if order(now) <= order(page.toward) => Err(stale(id)),
455 _ => Ok(false),
456 }
457 }
458
459 fn write_windows(
468 &self,
469 head: &Captured,
470 writes: &mut Writes<'_>,
471 made: &MadeWindows,
472 tag: embedded::ModelTag,
473 ) -> Result<bool, Failure> {
474 let page = writes.page;
475 let invalid = || {
476 Failure::new(
477 "embed_windows_invalid",
478 format!(
479 "record {}'s windows do not cover its text in order",
480 made.id
481 ),
482 )
483 };
484 let count = u32::try_from(made.windows.len()).map_err(|_| invalid())?;
485 if !(2..=embedded::WINDOWS_MOST).contains(&count) {
486 return Err(invalid());
487 }
488 let mut previous: Option<&Window> = None;
489 for window in &made.windows {
490 writes.length(&made.id, &window.vector)?;
491 let follows = previous.map_or(window.start == 0, |before| {
492 (before.start..=before.end).contains(&window.start)
493 && window.start != before.start
494 && window.end > before.end
495 });
496 if !follows || window.end <= window.start {
497 return Err(invalid());
498 }
499 previous = Some(window);
500 }
501 if !self.current(head, page, &made.id, made.made_from)? {
502 return Ok(false);
503 }
504 let provenance = Provenance {
505 made_from: made.made_from,
506 tag,
507 };
508 let key = embedded::vector_key(&page.collection, &page.field, &made.id);
509 writes.pending.push((key, None));
510 for (index, window) in (0..count).zip(&made.windows) {
511 let span = embedded::Span {
512 count,
513 start: window.start,
514 end: window.end,
515 };
516 let key = embedded::window_key(&page.collection, &page.field, &made.id, index);
517 let value = embedded::encode_window(span, &provenance, &window.vector);
518 writes.pending.push((key, Some(value)));
519 }
520 self.clear_windows(writes, &made.id, count)?;
521 Ok(true)
522 }
523
524 fn clear_windows(&self, writes: &mut Writes<'_>, id: &str, keep: u32) -> Result<(), Failure> {
532 let Some(at) = &writes.at else {
533 return Ok(());
534 };
535 let page = writes.page;
536 let first = embedded::window_key(&page.collection, &page.field, id, 0);
537 let Some(bytes) = reads::get_bytes(&self.reader, at, &first)? else {
538 return Ok(());
539 };
540 let count = embedded::window_span(&bytes)?.count;
541 for index in keep..count {
542 let key = embedded::window_key(&page.collection, &page.field, id, index);
543 writes.pending.push((key, None));
544 }
545 Ok(())
546 }
547
548 fn companion_at(&self, companion: &str) -> Option<Captured> {
550 self.refs.get(companion).map(|record| Captured {
551 branch: companion.to_owned(),
552 version: version(record),
553 shards: record.shards.clone(),
554 })
555 }
556
557 pub(super) fn declared(
559 &self,
560 head: &Captured,
561 collection: &str,
562 field: &str,
563 ) -> Result<Declaration, Failure> {
564 let not_declared = || {
565 Failure::new(
566 "embed_field_not_declared",
567 format!("{collection}.{field} is not declared embedded"),
568 )
569 };
570 let bytes = reads::get_bytes(&self.reader, head, &field_key(collection, field))?
571 .ok_or_else(not_declared)?;
572 match embedded::read_field(&bytes)? {
573 Field::Embedded(declaration) => Ok(declaration),
574 Field::Supplied { .. } => Err(not_declared()),
575 }
576 }
577
578 fn revision(
580 &self,
581 head: &Captured,
582 collection: &str,
583 id: &str,
584 ) -> Result<Option<Version>, Failure> {
585 reads::get_bytes(&self.reader, head, &revision_key(collection, id))?
586 .map(|bytes| {
587 serde_json::from_slice::<DocumentRevision>(&bytes)
588 .map(|row| row.written_at)
589 .map_err(Failure::storage)
590 })
591 .transpose()
592 }
593
594 pub fn position(&self, companion: &str, key: &[u8]) -> Result<Option<Position>, Failure> {
599 let Some(record) = self.refs.get(companion) else {
600 return Ok(None);
601 };
602 let at = Captured {
603 branch: companion.to_owned(),
604 version: version(record),
605 shards: record.shards.clone(),
606 };
607 reads::get_bytes(&self.reader, &at, key)?
608 .map(|bytes| embedded::read_position(&bytes))
609 .transpose()
610 }
611
612 #[cfg(test)]
619 pub fn declare_for_tests(
620 &mut self,
621 branch: &str,
622 collection: &str,
623 field: &str,
624 declaration: &Declaration,
625 ) -> Result<(), Failure> {
626 let bytes = embedded::encode_declaration(declaration)?;
627 let key = field_key(collection, field);
628 self.commit_namespace(&branch_name(branch), &[(key, Some(bytes))])
629 }
630
631 #[cfg(test)]
638 pub fn position_for_tests(
639 &mut self,
640 owner: &str,
641 collection: &str,
642 field: &str,
643 position: &Position,
644 ) -> Result<(), Failure> {
645 let companion = embedded::companion_name(owner);
646 if self.refs.get(&companion).is_none() {
647 create_branch_with_kind(
648 &companion,
649 self.writer.base_roots().map_err(Failure::database)?,
650 BranchKind::Namespace,
651 &mut self.refs,
652 &self.registry,
653 current_timestamp(),
654 )
655 .map_err(Failure::branch_commit)?;
656 }
657 let key = embedded::position_key(collection, field);
658 let bytes = embedded::encode_position(position)?;
659 self.commit_namespace(&companion, &[(key, Some(bytes))])
660 }
661
662 pub(super) fn commit_namespace(
665 &mut self,
666 name: &str,
667 writes: &[(Vec<u8>, Option<Vec<u8>>)],
668 ) -> Result<(), Failure> {
669 let at = self
670 .refs
671 .get(name)
672 .map(version)
673 .ok_or_else(|| Failure::new("reference_missing", name))?;
674 let handle =
675 open_branch(name, &self.refs, &self.registry).map_err(Failure::branch_commit)?;
676 for (key, value) in writes {
677 let shard = self.writer.shard_for(key);
678 value
679 .as_ref()
680 .map_or_else(
681 || handle.delete(shard, key),
682 |value| handle.put(shard, key, value),
683 )
684 .map_err(Failure::branch)?;
685 }
686 self.writer
687 .commit(
688 &handle,
689 &self.registry,
690 CommitRequest {
691 durability: CommitDurability::Durable {
692 refs: &mut self.refs,
693 },
694 extra_parents: &[],
695 timestamp: current_timestamp(),
696 },
697 )
698 .map_err(|error| {
699 let installed = self.refs.get(name).is_some_and(|now| version(now) != at);
700 Failure::new(
701 if installed {
702 "embed_durability_unknown"
703 } else {
704 "embed_write_refused"
705 },
706 error.to_string(),
707 )
708 })?;
709 self.publish_record(name)
710 }
711}
712
713#[cfg(test)]
714#[path = "embed_job_tests.rs"]
715mod tests;