mod graph;
mod pagination;
mod sparse;
mod sql;
mod text;
mod vector;
use crate::error::StorageError;
pub type StorageResult<T> = Result<T, StorageError>;
pub use graph::{
DirectedNeighborHit, Direction, Edge, EdgeEndpointBaseCounts, EdgeFilter, EdgeSeekPage,
EdgeSortField, EdgeUpsertDisposition, EdgeUpsertRefusal, EdgeUpsertRequest, EdgeUpsertResult,
GraphPath, GuardedBatchOutcome, GuardedBatchRefusal, GuardedEdgeBatchRefusal,
GuardedEdgeBatchUpsertOutcome, GuardedEdgeUpsertOutcome, GuardedWriteOutcome, LinkId,
MissingEndpoints, NeighborCursor, NeighborHit, NeighborQuery, PathNode, SortDirection,
SortOrder, TimeRange, TraversalExecutionBudget, TraversalOptions, TraversalRequest,
DEFAULT_TRAVERSAL_LIMIT, MAX_TRAVERSAL_DEPTH, MAX_TRAVERSAL_LIMIT, MAX_TRAVERSAL_MILLIS,
MAX_TRAVERSAL_ROOTS, MAX_TRAVERSAL_WORK,
};
pub use pagination::{BoundedCount, LimitReport, Page, PageRequest, SeekCursor, SeekPage};
pub use sparse::{
SparseRecord, SparseSearchHit, SparseSearchRequest, SparseVector, MAX_SPARSE_SEARCH_TOP_K,
};
pub use sql::{SqlColumn, SqlColumnError, SqlRow, SqlStatement, SqlValue};
pub use text::{
IndexRebuildScope, TextDocument, TextFilter, TextGatherMode, TextIndexStats, TextQueryMode,
TextSearchHit, TextSearchOptions, TextSearchRequest, TextTermStats, TextTermStatsRequest,
};
pub use vector::{
OrphanSweepConfig, OrphanSweepResult, PropertyFilter, PropertyOp, VectorIndexKind,
VectorMetadataFilter, VectorProvenance, VectorRecord, VectorScanEntry, VectorScanPage,
VectorScanRequest, VectorSearchHit, VectorSearchRequest, VectorStoreCapabilities,
VectorStoreInfo,
};
use serde::{Deserialize, Serialize};
pub const MAX_BATCH_WRITE_ERROR_DETAILS: usize = 128;
pub const MAX_BATCH_WRITE_ERROR_MESSAGE_CHARS: usize = 512;
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BatchWriteErrorClass {
InvalidInput,
Constraint,
Conflict,
BatchAborted,
Serialization,
Busy,
Cancelled,
Driver,
Unknown,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BatchWriteRetryability {
Permanent,
Transient,
Unknown,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct BatchWriteError {
pub index: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub item_id: Option<String>,
pub class: BatchWriteErrorClass,
pub retryability: BatchWriteRetryability,
pub message: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct BatchWriteErrorCount {
pub class: BatchWriteErrorClass,
pub retryability: BatchWriteRetryability,
pub count: u64,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct BatchWriteSummary {
pub attempted: u64,
pub affected: u64,
pub failed: u64,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub first_error: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<BatchWriteError>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub error_counts: Vec<BatchWriteErrorCount>,
#[serde(default, skip_serializing_if = "is_false")]
pub errors_truncated: bool,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub errors_omitted: u64,
}
fn is_zero_u64(value: &u64) -> bool {
*value == 0
}
fn is_false(value: &bool) -> bool {
!*value
}
impl BatchWriteSummary {
pub fn record_failure(
&mut self,
index: usize,
item_id: Option<String>,
class: BatchWriteErrorClass,
retryability: BatchWriteRetryability,
message: impl Into<String>,
) {
self.record_failure_with(index, class, retryability, || (item_id, message.into()));
}
pub fn record_failure_with(
&mut self,
index: usize,
class: BatchWriteErrorClass,
retryability: BatchWriteRetryability,
detail: impl FnOnce() -> (Option<String>, String),
) {
self.failed = self.failed.saturating_add(1);
match self
.error_counts
.iter_mut()
.find(|count| count.class == class && count.retryability == retryability)
{
Some(count) => count.count = count.count.saturating_add(1),
None => {
self.error_counts.push(BatchWriteErrorCount {
class,
retryability,
count: 1,
});
self.error_counts
.sort_by_key(|count| (count.class, count.retryability));
}
}
let sampled = self.errors.len() < MAX_BATCH_WRITE_ERROR_DETAILS;
let detail = (sampled || self.first_error.is_empty()).then(detail);
if let Some((_, message)) = &detail {
if self.first_error.is_empty() {
self.first_error = message.clone();
}
}
if let Some((item_id, message)) = detail.filter(|_| sampled) {
self.errors.push(BatchWriteError {
index: u64::try_from(index).unwrap_or(u64::MAX),
item_id,
class,
retryability,
message: bounded_batch_error_message(&message),
});
} else {
self.errors_truncated = true;
self.errors_omitted = self.errors_omitted.saturating_add(1);
}
}
}
fn bounded_batch_error_message(message: &str) -> String {
if message.chars().count() <= MAX_BATCH_WRITE_ERROR_MESSAGE_CHARS {
return message.to_owned();
}
let mut bounded: String = message
.chars()
.take(MAX_BATCH_WRITE_ERROR_MESSAGE_CHARS.saturating_sub(1))
.collect();
bounded.push('\u{2026}');
bounded
}
impl GuardedBatchRefusal {
fn error_metadata(&self, index: usize) -> (BatchWriteErrorClass, BatchWriteRetryability) {
if index == self.entry_index {
(
BatchWriteErrorClass::InvalidInput,
BatchWriteRetryability::Permanent,
)
} else {
(
BatchWriteErrorClass::BatchAborted,
BatchWriteRetryability::Unknown,
)
}
}
fn format_failure(
&self,
index: usize,
edge: &Edge,
first_error: &str,
) -> (Option<String>, String) {
let message = if index == self.entry_index {
first_error.to_owned()
} else {
format!(
"batch entry {index} was not written because guarded batch entry {} was refused",
self.entry_index,
)
};
(Some(edge.id.to_string()), message)
}
pub fn record_failure(
&self,
summary: &mut BatchWriteSummary,
index: usize,
edge: &Edge,
first_error: &str,
) {
let (class, retryability) = self.error_metadata(index);
summary.record_failure_with(index, class, retryability, || {
self.format_failure(index, edge, first_error)
});
}
}
impl GuardedBatchOutcome {
pub fn refusal_page(
&self,
original_edges: &[Edge],
class: Option<BatchWriteErrorClass>,
page: PageRequest,
) -> StorageResult<Page<BatchWriteError>> {
self.refusal_page_with(
original_edges,
class,
page,
GuardedBatchRefusal::format_failure,
)
}
fn refusal_page_with(
&self,
original_edges: &[Edge],
class: Option<BatchWriteErrorClass>,
page: PageRequest,
mut format: impl FnMut(&GuardedBatchRefusal, usize, &Edge, &str) -> (Option<String>, String),
) -> StorageResult<Page<BatchWriteError>> {
let invalid = |message| StorageError::InvalidInput {
capability: crate::capability::StorageCapability::Graph,
operation: "guarded_batch_refusal_page".into(),
message,
};
let len = u64::try_from(original_edges.len()).unwrap_or(u64::MAX);
if len != self.summary.attempted {
return Err(invalid(format!(
"original batch length {len} does not match attempted count {}",
self.summary.attempted,
)));
}
let Some(refusal) = &self.refused else {
return Ok(Page {
items: Vec::new(),
total: Some(0),
});
};
if refusal.entry_index >= original_edges.len() {
return Err(invalid(format!(
"refusal index {} is outside original batch length {len}",
refusal.entry_index,
)));
}
let matching = (0..original_edges.len())
.filter(|index| class.is_none_or(|class| refusal.error_metadata(*index).0 == class));
let total = matching.clone().count() as u64;
let offset = usize::try_from(page.offset).unwrap_or(usize::MAX);
let limit = (page.limit as usize).min(MAX_BATCH_WRITE_ERROR_DETAILS);
let items = matching
.skip(offset)
.take(limit)
.map(|index| {
let (class, retryability) = refusal.error_metadata(index);
let (item_id, message) = format(
refusal,
index,
&original_edges[index],
&self.summary.first_error,
);
BatchWriteError {
index: index as u64,
item_id,
class,
retryability,
message: bounded_batch_error_message(&message),
}
})
.collect();
Ok(Page {
items,
total: Some(total),
})
}
}
#[cfg(test)]
#[path = "batch_write_summary_tests.rs"]
mod batch_write_summary_tests;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DeleteMode {
Soft,
Hard,
}