use std::io::{self, Read, Write};
use super::canonical::Encoder;
use super::{
ApplicationKey, CanonicalComparison, CanonicalError, CanonicalFingerprint, CanonicalStats,
EntityId, EntityKind, GraphGeneration, GraphRevision, compare_canonical_streams,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GraphOperation {
StructuredCreate,
StructuredPut,
StructuredDelete,
StructuredRecreate,
CypherEdit,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExpectedGraphState {
Absent,
Entity(EntityId),
Deletion(GraphRevision),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GraphDeleteMode {
Restrict,
Detach,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct OperationFields<'a> {
pub operation: GraphOperation,
pub key: Option<ApplicationKey<'a>>,
pub requested_revision: GraphRevision,
pub installed_revision: GraphRevision,
pub expected: ExpectedGraphState,
pub incarnation: EntityId,
pub delete_mode: Option<GraphDeleteMode>,
pub original_generation: GraphGeneration,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct OperationProvenance<'a> {
fields: OperationFields<'a>,
encoded_len: u64,
}
impl<'a> OperationProvenance<'a> {
pub fn from_fields(
version: Option<u16>,
fields: OperationFields<'a>,
) -> Result<Self, CanonicalError> {
if version != Some(1) {
return Err(CanonicalError::UnsupportedProvenanceVersion);
}
let kind = fields.incarnation.kind();
if fields.key.is_some_and(|key| key.kind() != kind)
|| matches!(fields.expected, ExpectedGraphState::Entity(id) if id.kind() != kind)
{
return Err(CanonicalError::ProvenanceKindMismatch);
}
let mut value = Self {
fields,
encoded_len: 0,
};
value.encoded_len = value.write_to(&mut io::sink(), &mut || Ok(()))?.bytes;
Ok(value)
}
#[must_use]
pub const fn version(self) -> u16 {
1
}
#[must_use]
pub const fn fields(self) -> OperationFields<'a> {
self.fields
}
#[must_use]
pub const fn encoded_len(self) -> u64 {
self.encoded_len
}
pub fn write_to(
&self,
output: &mut dyn Write,
checkpoint: &mut dyn FnMut() -> Result<(), CanonicalError>,
) -> Result<CanonicalStats, CanonicalError> {
encode_operation(
output,
self.fields.key,
self.fields.expected,
Some(self.fields),
checkpoint,
)
}
}
pub(super) fn measure_operation_framing(
key: Option<ApplicationKey<'_>>,
expected: ExpectedGraphState,
checkpoint: &mut dyn FnMut() -> Result<(), CanonicalError>,
) -> Result<u64, CanonicalError> {
Ok(encode_operation(&mut io::sink(), key, expected, None, checkpoint)?.bytes)
}
fn encode_operation(
output: &mut dyn Write,
key: Option<ApplicationKey<'_>>,
expected: ExpectedGraphState,
fields: Option<OperationFields<'_>>,
checkpoint: &mut dyn FnMut() -> Result<(), CanonicalError>,
) -> Result<CanonicalStats, CanonicalError> {
let mut e = Encoder::new(output, checkpoint);
e.emit(b"ZGOP")?;
e.emit(&1u16.to_le_bytes())?;
e.byte(fields.map_or(0, |f| match f.operation {
GraphOperation::StructuredCreate => 1,
GraphOperation::StructuredPut => 2,
GraphOperation::StructuredDelete => 3,
GraphOperation::StructuredRecreate => 4,
GraphOperation::CypherEdit => 5,
}))?;
e.byte(u8::from(key.is_some()))?;
if let Some(key) = key {
e.byte(kind_tag(key.kind()))?;
e.blob(key.namespace().as_str().as_bytes())?;
e.blob(key.key().as_str().as_bytes())?;
}
e.emit(
&fields
.map_or(0, |f| f.requested_revision.get())
.to_le_bytes(),
)?;
e.emit(
&fields
.map_or(0, |f| f.installed_revision.get())
.to_le_bytes(),
)?;
match expected {
ExpectedGraphState::Absent => e.byte(1)?,
ExpectedGraphState::Entity(id) => {
e.byte(2)?;
encode_id(&mut e, id)?;
}
ExpectedGraphState::Deletion(revision) => {
e.byte(3)?;
e.emit(&revision.get().to_le_bytes())?;
}
}
if let Some(fields) = fields {
encode_id(&mut e, fields.incarnation)?;
} else {
e.emit(&[0u8; 17])?;
}
e.byte(match fields.and_then(|f| f.delete_mode) {
None => 0,
Some(GraphDeleteMode::Restrict) => 1,
Some(GraphDeleteMode::Detach) => 2,
})?;
e.emit(
&fields
.map_or(0, |f| f.original_generation.get())
.to_le_bytes(),
)?;
Ok(e.stats())
}
fn kind_tag(kind: EntityKind) -> u8 {
match kind {
EntityKind::Node => 1,
EntityKind::Relationship => 2,
}
}
fn encode_id(e: &mut Encoder<'_>, id: EntityId) -> Result<(), CanonicalError> {
e.byte(kind_tag(id.kind()))?;
let value = match id {
EntityId::Node(id) => id.get(),
EntityId::Relationship(id) => id.get(),
};
e.emit(&value.to_le_bytes())
}
pub struct ReplayEvidence<'a> {
pub provenance: OperationProvenance<'a>,
pub fingerprint: CanonicalFingerprint,
pub contents: &'a mut dyn Read,
}
pub fn compare_replay_evidence(
left: ReplayEvidence<'_>,
right: ReplayEvidence<'_>,
scratch: &mut [u8],
checkpoint: &mut dyn FnMut() -> Result<(), CanonicalError>,
) -> Result<CanonicalComparison, CanonicalError> {
checkpoint()?;
if left.provenance != right.provenance {
return Ok(CanonicalComparison {
equal: false,
bytes_compared: 0,
});
}
compare_canonical_streams(
left.contents,
left.fingerprint,
right.contents,
right.fingerprint,
scratch,
checkpoint,
)
}
impl<'a> OperationProvenance<'a> {
pub fn from_fields_with_control(
version: Option<u16>,
fields: OperationFields<'a>,
checkpoint: &mut dyn FnMut() -> Result<(), CanonicalError>,
) -> Result<Self, CanonicalError> {
checkpoint()?;
if version != Some(1) {
return Err(CanonicalError::UnsupportedProvenanceVersion);
}
let kind = fields.incarnation.kind();
if fields.key.is_some_and(|key| key.kind() != kind)
|| matches!(fields.expected, ExpectedGraphState::Entity(id) if id.kind() != kind)
{
return Err(CanonicalError::ProvenanceKindMismatch);
}
let mut value = Self {
fields,
encoded_len: 0,
};
value.encoded_len = value.write_to(&mut io::sink(), checkpoint)?.bytes;
Ok(value)
}
}