#![allow(
clippy::result_large_err,
reason = "the typed graph cause stays unboxed and allocation-free, as in GraphQueryError"
)]
use crate::epoch::EmbeddingTower;
use crate::lifecycle::durability::{CommitTier, DurabilityMode};
use crate::lifecycle::native_graph::NativeGraphError;
use crate::lifecycle::{AccessMode, OpenOptions, QueryControl, Store, StoreErrorKind};
use crate::property_graph::query::completed::{
GraphQueryCause, GraphQueryError, GraphQueryErrorKind, native_graph_error_kind,
};
use crate::property_graph::query::plan::PlanNodeId;
use crate::property_graph::query::runtime::WorkCounters;
use crate::property_graph::resources::GraphResources;
use crate::property_graph::staging::{ItemReceipt, StageError, StructuredWrite};
use crate::property_graph::{BatchDisposition, GraphGeneration};
use std::path::{Path, PathBuf};
#[cfg(any(test, feature = "test-seams"))]
pub(crate) mod commit_recovery_test_support;
mod maintenance;
pub use maintenance::{GraphMaintenancePolicy, GraphMaintenanceReport};
mod query;
pub use query::{GraphPlanBacking, GraphQueryPlan};
pub struct GraphStore {
store: Store,
}
impl std::fmt::Debug for GraphStore {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("GraphStore")
.field("directory", &self.store.directory)
.finish_non_exhaustive()
}
}
impl GraphStore {
pub fn create_with_relationship_types(
path: impl AsRef<Path>,
options: OpenOptions,
document: Option<EmbeddingTower>,
rules: &[crate::property_graph::catalog::RelationshipRule<'_>],
) -> Result<Self, GraphStoreError> {
refuse_unsupported_platform()?;
let path = path.as_ref();
refuse_legacy_directory(path)?;
let rules =
crate::property_graph::catalog::RelationshipRules::new(rules).map_err(|error| {
GraphStoreError::graph(NativeGraphError::Stage(StageError::Catalog(error)))
})?;
let store = Store::create_native_graph_with_relationship_types(
path,
graph_options(options, AccessMode::ReadWrite),
document,
rules,
)?;
Ok(Self { store })
}
pub fn create(
path: impl AsRef<Path>,
options: OpenOptions,
document: Option<EmbeddingTower>,
) -> Result<Self, GraphStoreError> {
refuse_unsupported_platform()?;
let path = path.as_ref();
refuse_legacy_directory(path)?;
let store = Store::create_native_graph(
path,
graph_options(options, AccessMode::ReadWrite),
document,
)?;
Ok(Self { store })
}
pub fn open(
path: impl AsRef<Path>,
options: OpenOptions,
document: Option<EmbeddingTower>,
) -> Result<Self, GraphStoreError> {
Self::open_as(path.as_ref(), options, document, AccessMode::ReadWrite)
}
pub fn open_read_only(
path: impl AsRef<Path>,
options: OpenOptions,
document: Option<EmbeddingTower>,
) -> Result<Self, GraphStoreError> {
Self::open_as(path.as_ref(), options, document, AccessMode::ReadOnly)
}
fn open_as(
path: &Path,
options: OpenOptions,
document: Option<EmbeddingTower>,
access: AccessMode,
) -> Result<Self, GraphStoreError> {
refuse_unsupported_platform()?;
refuse_legacy_directory(path)?;
let store = Store::open_native_graph(path, graph_options(options, access), document)?;
Ok(Self { store })
}
pub fn close(&self) -> Result<(), GraphStoreError> {
let mut checkpoint = Ok(());
let closed = self.store.close_with_final_writer(|| {
checkpoint = self.store.checkpoint_native_graph_for_close();
});
checkpoint.map_err(GraphStoreError::graph)?;
closed.map_err(|error| GraphStoreError::graph(NativeGraphError::Store(error)))
}
pub fn apply_batch(
&self,
requests: &[StructuredWrite<'_, '_>],
control: &QueryControl,
) -> Result<GraphWriteResult, GraphStoreError> {
let prepared = self.store.apply_native_graph(requests, control)?;
let outcome = match (prepared.changed_generation(), prepared.disposition()) {
(Some(generation), _) => GraphWriteOutcome::Committed { generation },
(None, BatchDisposition::Replayed) => GraphWriteOutcome::Replayed,
(None, BatchDisposition::NoOp) => GraphWriteOutcome::NoOp,
(None, BatchDisposition::Changed) => {
return Err(GraphStoreError::contract(
"changed batch returned without a published generation",
));
}
};
let admitted_generation = prepared.admitted_generation();
let receipts = prepared.into_registration().into_receipts();
Ok(GraphWriteResult {
receipts,
outcome,
admitted_generation,
})
}
#[doc(hidden)]
pub fn apply_batch_with_materializer<M: crate::property_graph::staging::ResultMaterializer>(
&self,
requests: &[StructuredWrite<'_, '_>],
control: &QueryControl,
materializer: &mut M,
) -> Result<(GraphWriteOutcome, GraphGeneration, M::Registration), GraphStoreError> {
let prepared =
self.store
.apply_native_graph_with_materializer(requests, control, materializer)?;
let outcome = match (prepared.changed_generation(), prepared.disposition()) {
(Some(generation), _) => GraphWriteOutcome::Committed { generation },
(None, BatchDisposition::Replayed) => GraphWriteOutcome::Replayed,
(None, BatchDisposition::NoOp) => GraphWriteOutcome::NoOp,
(None, BatchDisposition::Changed) => {
return Err(GraphStoreError::contract(
"changed batch returned without a published generation",
));
}
};
let admitted = prepared.admitted_generation();
Ok((outcome, admitted, prepared.into_registration()))
}
pub fn resources(&self) -> Result<GraphResources, GraphStoreError> {
GraphResources::from_store(&self.store)
.map_err(|error| GraphStoreError::graph(NativeGraphError::Store(error)))
}
#[doc(hidden)]
#[must_use]
pub const fn statement_store(&self) -> &Store {
&self.store
}
#[cfg(any(test, feature = "test-seams"))]
#[doc(hidden)]
pub fn create_with_allocator_seed_for_test(
path: impl AsRef<Path>,
options: OpenOptions,
first_node: crate::property_graph::NodeId,
first_relationship: crate::property_graph::RelId,
) -> Result<Self, GraphStoreError> {
let path = path.as_ref();
refuse_legacy_directory(path)?;
let store = Store::create_native_graph_with_allocator_seed_for_test(
path,
graph_options(options, AccessMode::ReadWrite),
None,
first_node,
first_relationship,
)?;
Ok(Self { store })
}
#[cfg(feature = "test-seams")]
#[doc(hidden)]
pub fn jump_allocators_for_test(
&self,
next_node: crate::property_graph::NodeId,
next_relationship: crate::property_graph::RelId,
control: &QueryControl,
) -> Result<crate::property_graph::GraphGeneration, GraphStoreError> {
Ok(self.store.jump_native_graph_allocators_for_test(
next_node,
next_relationship,
control,
)?)
}
#[cfg(test)]
pub(crate) const fn store_for_test(&self) -> &Store {
&self.store
}
}
const fn graph_options(options: OpenOptions, access: AccessMode) -> OpenOptions {
options
.with_access_mode(access)
.with_durability(DurabilityMode::Durable, CommitTier::Durable)
}
#[cfg(any(target_os = "macos", test))]
pub(crate) const GRAPH_MIN_MACOS: (u32, u32) = (14, 0);
#[cfg(any(target_os = "macos", test))]
pub(crate) const fn macos_admits_graph(observed: (u32, u32)) -> bool {
observed.0 > GRAPH_MIN_MACOS.0
|| (observed.0 == GRAPH_MIN_MACOS.0 && observed.1 >= GRAPH_MIN_MACOS.1)
}
fn refuse_unsupported_platform() -> Result<(), GraphStoreError> {
#[cfg(target_os = "macos")]
{
match crate::sys::darwin::os_product_version() {
Ok(observed) if macos_admits_graph(observed) => Ok(()),
Ok(observed) => Err(GraphStoreError {
cause: Cause::UnsupportedPlatform {
required: GRAPH_MIN_MACOS,
observed: Some(observed),
probe_error: String::new(),
},
}),
Err(error) => Err(GraphStoreError {
cause: Cause::UnsupportedPlatform {
required: GRAPH_MIN_MACOS,
observed: None,
probe_error: error.to_string(),
},
}),
}
}
#[cfg(not(target_os = "macos"))]
{
Ok(())
}
}
fn refuse_legacy_directory(path: &Path) -> Result<(), GraphStoreError> {
let legacy = crate::lifecycle::is_legacy_store_directory(&crate::vfs::StdVfs, path)
.map_err(|error| GraphStoreError::graph(NativeGraphError::Store(error)))?;
if legacy {
return Err(GraphStoreError {
cause: Cause::LegacyStore {
path: path.to_path_buf(),
},
});
}
Ok(())
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GraphWriteOutcome {
Committed {
generation: GraphGeneration,
},
Replayed,
NoOp,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GraphWriteResult {
receipts: Box<[ItemReceipt]>,
outcome: GraphWriteOutcome,
admitted_generation: GraphGeneration,
}
impl GraphWriteResult {
#[must_use]
pub fn receipts(&self) -> &[ItemReceipt] {
&self.receipts
}
#[must_use]
pub const fn outcome(&self) -> GraphWriteOutcome {
self.outcome
}
#[must_use]
pub const fn admitted_generation(&self) -> GraphGeneration {
self.admitted_generation
}
#[must_use]
pub fn into_receipts(self) -> Box<[ItemReceipt]> {
self.receipts
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum GraphStoreErrorKind {
LegacyStore,
Busy,
ReadOnly,
InvalidRequest,
Constraint,
Limit,
Cancelled,
Timeout,
Closed,
Corruption,
Storage,
Unavailable,
WriteIndeterminate,
Unsupported,
Internal,
}
#[derive(Debug)]
#[allow(
clippy::large_enum_variant,
reason = "the typed graph and query causes stay unboxed and allocation-free, as in GraphQueryError"
)]
enum Cause {
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
UnsupportedPlatform {
required: (u32, u32),
observed: Option<(u32, u32)>,
probe_error: String,
},
LegacyStore {
path: PathBuf,
},
Graph(NativeGraphError),
Query(GraphQueryError),
Contract(&'static str),
Limit(&'static str),
}
const fn from_graph_query_kind(kind: GraphQueryErrorKind) -> GraphStoreErrorKind {
match kind {
GraphQueryErrorKind::InvalidPlan
| GraphQueryErrorKind::Parameter
| GraphQueryErrorKind::Expression => GraphStoreErrorKind::InvalidRequest,
GraphQueryErrorKind::Constraint => GraphStoreErrorKind::Constraint,
GraphQueryErrorKind::Limit => GraphStoreErrorKind::Limit,
GraphQueryErrorKind::Cancelled => GraphStoreErrorKind::Cancelled,
GraphQueryErrorKind::Timeout => GraphStoreErrorKind::Timeout,
GraphQueryErrorKind::Closed => GraphStoreErrorKind::Closed,
GraphQueryErrorKind::Corruption => GraphStoreErrorKind::Corruption,
GraphQueryErrorKind::Internal => GraphStoreErrorKind::Internal,
GraphQueryErrorKind::Storage => GraphStoreErrorKind::Storage,
GraphQueryErrorKind::Unavailable => GraphStoreErrorKind::Unavailable,
GraphQueryErrorKind::WriteIndeterminate => GraphStoreErrorKind::WriteIndeterminate,
}
}
#[derive(Debug)]
pub struct GraphStoreError {
cause: Cause,
}
impl GraphStoreError {
const fn graph(error: NativeGraphError) -> Self {
Self {
cause: Cause::Graph(error),
}
}
const fn contract(reason: &'static str) -> Self {
Self {
cause: Cause::Contract(reason),
}
}
const fn limit(reason: &'static str) -> Self {
Self {
cause: Cause::Limit(reason),
}
}
#[must_use]
pub fn kind(&self) -> GraphStoreErrorKind {
match &self.cause {
Cause::UnsupportedPlatform { .. } => GraphStoreErrorKind::Unsupported,
Cause::LegacyStore { .. } => GraphStoreErrorKind::LegacyStore,
Cause::Contract(_) => GraphStoreErrorKind::Corruption,
Cause::Limit(_) => GraphStoreErrorKind::Limit,
Cause::Graph(NativeGraphError::Store(error))
if error.kind() == StoreErrorKind::StoreBusy =>
{
GraphStoreErrorKind::Busy
}
Cause::Graph(NativeGraphError::Store(error))
if error.kind() == StoreErrorKind::ReadOnly =>
{
GraphStoreErrorKind::ReadOnly
}
Cause::Graph(error) => from_graph_query_kind(native_graph_error_kind(error)),
Cause::Query(error)
if matches!(
error.cause(),
GraphQueryCause::Graph(NativeGraphError::Store(inner))
if inner.kind() == StoreErrorKind::ReadOnly
) =>
{
GraphStoreErrorKind::ReadOnly
}
Cause::Query(error) => from_graph_query_kind(error.kind()),
}
}
#[must_use]
pub const fn operator(&self) -> Option<PlanNodeId> {
match &self.cause {
Cause::Query(error) => error.operator(),
Cause::UnsupportedPlatform { .. }
| Cause::LegacyStore { .. }
| Cause::Graph(_)
| Cause::Contract(_)
| Cause::Limit(_) => None,
}
}
#[must_use]
pub const fn counters(&self) -> Option<WorkCounters> {
match &self.cause {
Cause::Query(error) => error.counters(),
Cause::UnsupportedPlatform { .. }
| Cause::LegacyStore { .. }
| Cause::Graph(_)
| Cause::Contract(_)
| Cause::Limit(_) => None,
}
}
#[must_use]
pub fn nothing_committed(&self) -> bool {
self.kind() != GraphStoreErrorKind::WriteIndeterminate
}
#[must_use]
pub const fn stage_error(&self) -> Option<&StageError> {
match &self.cause {
Cause::Graph(NativeGraphError::Stage(error)) => Some(error),
_ => None,
}
}
#[must_use]
pub fn legacy_store_path(&self) -> Option<&Path> {
match &self.cause {
Cause::LegacyStore { path } => Some(path),
_ => None,
}
}
}
impl From<NativeGraphError> for GraphStoreError {
fn from(error: NativeGraphError) -> Self {
Self::graph(error)
}
}
impl From<GraphQueryError> for GraphStoreError {
fn from(error: GraphQueryError) -> Self {
Self {
cause: Cause::Query(error),
}
}
}
impl std::fmt::Display for GraphStoreError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "graph store rejected ({:?}): ", self.kind())?;
match &self.cause {
Cause::UnsupportedPlatform {
required,
observed,
probe_error,
} => {
write!(
formatter,
"graph store requires macOS {}.{} or newer; ",
required.0, required.1
)?;
if let Some(observed) = observed {
write!(formatter, "this host reports {}.{}", observed.0, observed.1)
} else {
write!(
formatter,
"could not determine the macOS version: {}",
probe_error
)
}
}
Cause::LegacyStore { path } => write!(
formatter,
"legacy store directory cannot be opened as a graph store: {}",
path.display()
),
Cause::Graph(error) => error.fmt(formatter),
Cause::Query(error) => error.fmt(formatter),
Cause::Contract(reason) | Cause::Limit(reason) => formatter.write_str(reason),
}
}
}
impl std::error::Error for GraphStoreError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.cause {
Cause::Graph(error) => Some(error),
Cause::Query(error) => Some(error),
Cause::UnsupportedPlatform { .. }
| Cause::LegacyStore { .. }
| Cause::Contract(_)
| Cause::Limit(_) => None,
}
}
}
mod get;
pub use get::{GraphGetOptions, GraphNodesResult, GraphRelationshipsResult};
#[cfg(test)]
mod tests;