use super::catalog::{GraphInterpretation, Symbol, SymbolHighWaters, SymbolKind};
use super::storage::artifact::ArtifactId;
use super::*;
mod bounded;
mod encode;
mod images;
mod integrity;
mod memory;
mod overlay;
mod symbols;
pub use images::{
NodeImageBudget, NodeImageBuilder, RelationshipImageBudget, RelationshipImageBuilder,
StatementImages,
};
pub use memory::{WriteAdoptionError, WriteMemory, WriteReservation};
pub use overlay::{BatchEntityRef, GraphBatchReadView, OverlayCounters};
mod result;
pub use result::{
MaterializedBatch, ResultLayout, ResultMaterializer, ResultRegistration,
ScopedMaterializedBatch, stage_structured_with_results,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BaseIdentity {
pub store: StoreInstanceId,
pub generation: GraphGeneration,
pub roots: Option<ArtifactId>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct HighWaters {
pub node: u128,
pub relationship: u128,
pub symbols: SymbolHighWaters,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WritePhase {
Validate,
Identity,
Allocate,
Canonical,
Incident,
CoreResult,
AbiResult,
Overlay,
Finalize,
}
pub type WriteControl<'a> = dyn FnMut(WritePhase) -> Result<(), StageError> + 'a;
#[derive(Debug)]
pub enum StageError {
Limit,
InvalidLimits,
Cancelled,
Endpoint,
ViewMismatch,
IdentityOverflow,
MissingEntity,
IncidentRelationship,
DeletedEntity,
InvalidInput,
Lifecycle(KeyLifecycleError),
Canonical(CanonicalError),
Catalog(catalog::CatalogError),
Memory(crate::lifecycle::StoreError),
NativeStorage(crate::property_graph::storage::tree::directory::TreeError),
}
impl std::fmt::Display for StageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Lifecycle(e) => e.fmt(f),
Self::Canonical(e) => e.fmt(f),
Self::Catalog(e) => e.fmt(f),
Self::Memory(e) => e.fmt(f),
Self::NativeStorage(e) => e.fmt(f),
other => write!(f, "graph staging rejected: {other:?}"),
}
}
}
impl std::error::Error for StageError {}
impl From<KeyLifecycleError> for StageError {
fn from(e: KeyLifecycleError) -> Self {
Self::Lifecycle(e)
}
}
impl From<CanonicalError> for StageError {
fn from(e: CanonicalError) -> Self {
Self::Canonical(e)
}
}
impl From<catalog::CatalogError> for StageError {
fn from(e: catalog::CatalogError) -> Self {
Self::Catalog(e)
}
}
impl From<crate::lifecycle::StoreError> for StageError {
fn from(e: crate::lifecycle::StoreError) -> Self {
Self::Memory(e)
}
}
impl From<crate::property_graph::storage::tree::directory::TreeError> for StageError {
fn from(e: crate::property_graph::storage::tree::directory::TreeError) -> Self {
Self::NativeStorage(e)
}
}
#[derive(Clone, Copy, Debug)]
pub struct WriteLimits {
pub changes: usize,
pub input_bytes: usize,
pub writer_bytes: usize,
pub result_rows: usize,
pub result_bytes: usize,
}
impl Default for WriteLimits {
fn default() -> Self {
Self {
changes: MAX_GRAPH_CHANGES,
input_bytes: MAX_GRAPH_INPUT_BYTES,
writer_bytes: 64 * 1024 * 1024,
result_rows: 65_536,
result_bytes: 4 * 1024 * 1024,
}
}
}
impl WriteLimits {
fn validate(self) -> Result<(), StageError> {
let max = Self::default();
if self.changes > max.changes
|| self.input_bytes > max.input_bytes
|| self.writer_bytes > max.writer_bytes
|| self.result_rows > max.result_rows
|| self.result_bytes > max.result_bytes
{
return Err(StageError::InvalidLimits);
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Membership {
pub text: bool,
pub vector: bool,
}
#[derive(Clone, Copy)]
pub struct BaseEntity<'a> {
pub view: BaseIdentity,
pub provenance: OperationProvenance<'a>,
pub shape: EntityShape<'a>,
pub fingerprint: CanonicalFingerprint,
pub source: &'a dyn CanonicalSource,
pub membership: Membership,
}
pub trait CanonicalSource {
fn read_at(&self, offset: u64, output: &mut [u8]) -> std::io::Result<usize>;
}
pub struct CanonicalSlice<'a>(pub &'a [u8]);
impl CanonicalSource for CanonicalSlice<'_> {
fn read_at(&self, offset: u64, output: &mut [u8]) -> std::io::Result<usize> {
let offset = usize::try_from(offset).map_err(|_| std::io::ErrorKind::InvalidInput)?;
let source = self
.0
.get(offset..)
.ok_or(std::io::ErrorKind::UnexpectedEof)?;
let count = source.len().min(output.len());
output
.get_mut(..count)
.ok_or(std::io::ErrorKind::InvalidInput)?
.copy_from_slice(
source
.get(..count)
.ok_or(std::io::ErrorKind::InvalidInput)?,
);
Ok(count)
}
}
pub enum BaseKeyState<'a> {
NeverUsed,
Live(BaseEntity<'a>),
Deleted(BaseIdentity, OperationProvenance<'a>),
}
pub trait AdmittedBase {
fn has_relationship_rules(&self) -> bool {
false
}
fn visit_incoming_rules(
&self,
_node: NodeId,
_visit: &mut dyn FnMut(RelId, NodeId, catalog::OnDelete) -> Result<(), StageError>,
_control: &mut WriteControl<'_>,
) -> Result<(), StageError> {
Err(StageError::InvalidInput)
}
fn identity(&self) -> BaseIdentity;
fn high_waters(&self) -> HighWaters;
fn interpretation(&self) -> GraphInterpretation<'_>;
fn key(
&self,
key: ApplicationKey<'_>,
control: &mut WriteControl<'_>,
) -> Result<BaseKeyState<'_>, StageError>;
fn entity(
&self,
id: EntityId,
control: &mut WriteControl<'_>,
) -> Result<Option<BaseEntity<'_>>, StageError>;
fn has_live_incident(
&self,
node: NodeId,
removed: &[RelId],
control: &mut WriteControl<'_>,
) -> Result<bool, StageError>;
fn property(
&self,
entity: EntityId,
name: GraphName<'_>,
control: &mut WriteControl<'_>,
) -> Result<Option<PropertyValue<'_>>, StageError>;
fn stored_text(
&self,
node: NodeId,
control: &mut WriteControl<'_>,
) -> Result<Option<&str>, StageError>;
fn symbol(
&self,
kind: SymbolKind,
name: GraphName<'_>,
control: &mut WriteControl<'_>,
) -> Result<Option<Symbol>, StageError>;
}
#[derive(Clone, Copy)]
pub enum WriteImage<'a, 'batch> {
Node(&'a CanonicalContents<'a>),
Relationship {
source: NodeRef<'batch>,
target: NodeRef<'batch>,
relationship_type: GraphName<'a>,
properties: &'a [GraphProperty<'a>],
},
}
#[derive(Clone, Copy)]
pub struct StructuredWrite<'a, 'batch> {
pub key: ApplicationKey<'a>,
pub revision: GraphRevision,
pub operation: StructuredOperation,
pub image: Option<WriteImage<'a, 'batch>>,
}
#[derive(Clone, Copy, Debug)]
pub enum StructuredOperation {
Create,
Put(EntityId),
Delete(EntityId, GraphDeleteMode),
Recreate(GraphRevision),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ItemReceipt {
pub entity: EntityId,
pub revision: GraphRevision,
pub generation: GraphGeneration,
pub replayed: bool,
}
pub struct NormalizedDelta<'a> {
provenance: OperationProvenance<'a>,
canonical: Option<memory::Arena<'a, u8>>,
shape: Option<EntityShape<'a>>,
before: Membership,
after: Membership,
}
impl<'a> NormalizedDelta<'a> {
pub const fn provenance(&self) -> OperationProvenance<'a> {
self.provenance
}
pub fn canonical(&self) -> Option<&[u8]> {
self.canonical.as_deref()
}
pub const fn shape(&self) -> Option<EntityShape<'a>> {
self.shape
}
pub const fn membership(&self) -> (Membership, Membership) {
(self.before, self.after)
}
}
pub struct StagedBatch<'a> {
base: BaseIdentity,
high_waters: HighWaters,
receipts: memory::Arena<'a, ItemReceipt>,
deltas: memory::Arena<'a, NormalizedDelta<'a>>,
disposition: BatchDisposition,
symbols: memory::Arena<'a, catalog::SymbolEntry<'a>>,
}
impl StagedBatch<'_> {
#[cfg(any(test, feature = "test-seams"))]
pub(crate) fn jump_allocators_for_test(
&mut self,
next_node: crate::property_graph::NodeId,
next_relationship: crate::property_graph::RelId,
) -> Result<(), StageError> {
let node = next_node
.get()
.checked_sub(1)
.ok_or(StageError::InvalidInput)?;
let relationship = next_relationship
.get()
.checked_sub(1)
.ok_or(StageError::InvalidInput)?;
if !self.deltas.is_empty()
|| !self.receipts.is_empty()
|| !self.symbols.is_empty()
|| node < self.high_waters.node
|| relationship < self.high_waters.relationship
|| (node == self.high_waters.node && relationship == self.high_waters.relationship)
{
return Err(StageError::InvalidInput);
}
self.high_waters.node = node;
self.high_waters.relationship = relationship;
self.disposition = BatchDisposition::Changed;
Ok(())
}
pub(crate) fn uses_memory(&self, memory: &WriteMemory<'_>) -> bool {
self.deltas.uses_memory(memory)
&& self.receipts.uses_memory(memory)
&& self.symbols.uses_memory(memory)
}
pub const fn base(&self) -> BaseIdentity {
self.base
}
pub const fn high_waters(&self) -> HighWaters {
self.high_waters
}
pub fn deltas(&self) -> &[NormalizedDelta<'_>] {
&self.deltas
}
pub fn symbols(&self) -> &[catalog::SymbolEntry<'_>] {
&self.symbols
}
pub const fn disposition(&self) -> BatchDisposition {
self.disposition
}
pub fn receipts(&self) -> &[ItemReceipt] {
&self.receipts
}
}
mod structured;
pub use structured::stage_structured;
#[cfg(all(test, feature = "allocation-audit"))]
mod tests;