1#![forbid(unsafe_code)]
8
9pub mod algorithms;
10pub mod canonical;
11pub mod embedding_options;
12pub mod manifest;
13pub mod uuid;
14
15use std::{fmt, sync::Arc};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
23pub struct Span {
24 pub start: usize,
26 pub end: usize,
28}
29
30impl Span {
31 #[must_use]
33 pub const fn new(start: usize, end: usize) -> Self {
34 Self { start, end }
35 }
36}
37
38impl fmt::Display for Span {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 write!(f, "{}..{}", self.start, self.end)
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
53pub struct TypeId(pub u32);
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
61pub struct PropId(pub u32);
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum OntologyFormat {
66 Yaml,
68 Json,
70}
71
72#[derive(
79 Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
80)]
81#[serde(rename_all = "lowercase")]
82pub enum OntologyMode {
83 #[default]
86 Exploratory,
87 Advisory,
89 Strict,
91}
92
93#[derive(thiserror::Error, Debug)]
103pub enum GfError {
104 #[error("not implemented: {0}")]
106 NotImplemented(&'static str),
107
108 #[error("parse error at {span}: {msg}")]
110 Parse {
111 msg: String,
113 span: Span,
115 },
116
117 #[error("bind error at {span}: {msg}")]
123 Bind {
124 msg: String,
126 span: Span,
128 },
129
130 #[error("plan error: {0}")]
132 Plan(String),
133
134 #[error("execution error: {0}")]
136 Execution(String),
137
138 #[error("provider error: class={class} provider={provider} model={model}")]
140 Provider {
141 class: String,
143 provider: String,
145 model: String,
147 },
148
149 #[error("storage error: {0}")]
151 Storage(String),
152
153 #[error("{code}: {message}")]
155 Project {
156 code: ProjectErrorCode,
158 message: String,
160 },
161
162 #[error("{code}: {message}")]
164 Api {
165 code: ApiErrorCode,
167 message: String,
169 },
170
171 #[error("lifecycle error: {0}")]
173 Lifecycle(String),
174
175 #[error("validation error: {0}")]
177 Validation(String),
178
179 #[error("ontology error: {0}")]
181 Ontology(String),
182}
183
184impl GfError {
185 #[must_use]
187 pub const fn code(&self) -> &'static str {
188 match self {
189 Self::NotImplemented(_) => "GF_NOT_IMPLEMENTED",
190 Self::Parse { .. } => "GF_PARSE",
191 Self::Bind { .. } | Self::Plan(_) => "GF_PLAN",
192 Self::Execution(_) | Self::Provider { .. } => "GF_EXECUTION",
193 Self::Storage(_) => "GF_IO",
194 Self::Project { code, .. } => code.as_str(),
195 Self::Api { code, .. } => code.as_str(),
196 Self::Lifecycle(_) => "GF_LIFECYCLE",
197 Self::Validation(_) => "GF_VALIDATION",
198 Self::Ontology(_) => "GF_ONTOLOGY",
199 }
200 }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum ApiErrorCode {
206 NotFound,
208 Cancelled,
210 ResourceLimit,
212 PageInvalid,
214 PageSnapshotGone,
216 SchemaMismatch,
218 UnknownArgument,
220 AmbiguousProjection,
222 IdentityConflict,
224 FingerprintCollision,
226 ResultNotRetained,
228}
229
230impl ApiErrorCode {
231 #[must_use]
233 pub const fn as_str(self) -> &'static str {
234 match self {
235 Self::NotFound => "GF_NOT_FOUND",
236 Self::Cancelled => "GF_CANCELLED",
237 Self::ResourceLimit => "GF_RESOURCE_LIMIT",
238 Self::PageInvalid => "GF_PAGE_INVALID",
239 Self::PageSnapshotGone => "GF_PAGE_SNAPSHOT_GONE",
240 Self::SchemaMismatch => "GF_SCHEMA_MISMATCH",
241 Self::UnknownArgument => "GF_UNKNOWN_ARGUMENT",
242 Self::AmbiguousProjection => "GF_AMBIGUOUS_PROJECTION",
243 Self::IdentityConflict => "GF_IDENTITY_CONFLICT",
244 Self::FingerprintCollision => "GF_FINGERPRINT_COLLISION",
245 Self::ResultNotRetained => "GF_RESULT_NOT_RETAINED",
246 }
247 }
248}
249
250impl fmt::Display for ApiErrorCode {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 f.write_str(self.as_str())
253 }
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum ProjectErrorCode {
259 UnsupportedProjectFormat,
261 ProjectUninitialized,
263 ProjectCorrupt,
265 UnsupportedFilesystem,
267 WriterBusy,
269 WriteConflict,
271 RebaseExhausted,
273 TransactionConflict,
275 PublicationFailed,
277 UnsupportedCapabilityVersion,
279 CapabilityDisabled,
281 TransactionFailed,
283 CheckpointExists,
285 CheckpointNotFound,
287 CheckpointRegistryCorrupt,
289 ReadOnlyView,
291 ResourceLimit,
293}
294
295impl ProjectErrorCode {
296 #[must_use]
298 pub const fn as_str(self) -> &'static str {
299 match self {
300 Self::UnsupportedProjectFormat => "GF_UNSUPPORTED_PROJECT_FORMAT",
301 Self::ProjectUninitialized => "GF_PROJECT_UNINITIALIZED",
302 Self::ProjectCorrupt => "GF_PROJECT_CORRUPT",
303 Self::UnsupportedFilesystem => "GF_UNSUPPORTED_FILESYSTEM",
304 Self::WriterBusy => "GF_WRITER_BUSY",
305 Self::WriteConflict => "GF_WRITE_CONFLICT",
306 Self::RebaseExhausted => "GF_REBASE_EXHAUSTED",
307 Self::TransactionConflict => "GF_IDEMPOTENCY_CONFLICT",
308 Self::PublicationFailed => "GF_PUBLICATION_FAILED",
309 Self::UnsupportedCapabilityVersion => "GF_UNSUPPORTED_CAPABILITY_VERSION",
310 Self::CapabilityDisabled => "GF_CAPABILITY_DISABLED",
311 Self::TransactionFailed => "GF_TRANSACTION_FAILED",
312 Self::CheckpointExists => "GF_CHECKPOINT_EXISTS",
313 Self::CheckpointNotFound => "GF_CHECKPOINT_NOT_FOUND",
314 Self::CheckpointRegistryCorrupt => "GF_CHECKPOINT_REGISTRY_CORRUPT",
315 Self::ReadOnlyView => "GF_READ_ONLY_VIEW",
316 Self::ResourceLimit => "GF_RESOURCE_LIMIT",
317 }
318 }
319}
320
321impl fmt::Display for ProjectErrorCode {
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 f.write_str(self.as_str())
324 }
325}
326
327#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
333#[non_exhaustive]
334pub enum PropValue {
335 Null,
337 Bool(bool),
339 Int(i64),
341 Float(f64),
343 Str(String),
345 List(Vec<PropValue>),
347}
348
349impl fmt::Display for PropValue {
350 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351 match self {
352 Self::Null => write!(f, "null"),
353 Self::Bool(b) => write!(f, "{b}"),
354 Self::Int(i) => write!(f, "{i}"),
355 Self::Float(fl) => write!(f, "{fl}"),
356 Self::Str(s) => write!(f, "{s}"),
357 Self::List(l) => {
358 write!(f, "[")?;
359 for (i, v) in l.iter().enumerate() {
360 if i > 0 {
361 write!(f, ", ")?;
362 }
363 write!(f, "{v}")?;
364 }
365 write!(f, "]")
366 }
367 }
368 }
369}
370
371#[doc(hidden)]
377#[derive(Clone, Default)]
378pub struct GraphIdentity(Arc<()>);
379
380impl GraphIdentity {
381 #[must_use]
383 pub fn new() -> Self {
384 Self::default()
385 }
386}
387
388impl fmt::Debug for GraphIdentity {
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 f.write_str("GraphIdentity(..)")
391 }
392}
393
394#[derive(Debug, Clone)]
396pub struct NodeHandle {
397 pub uuid: ::uuid::Uuid,
399 pub label: String,
401 owner: GraphIdentity,
402}
403
404impl NodeHandle {
405 #[doc(hidden)]
407 #[must_use]
408 pub fn new(uuid: ::uuid::Uuid, label: impl Into<String>, owner: GraphIdentity) -> Self {
409 Self {
410 uuid,
411 label: label.into(),
412 owner,
413 }
414 }
415
416 #[doc(hidden)]
418 #[must_use]
419 pub fn belongs_to(&self, owner: &GraphIdentity) -> bool {
420 Arc::ptr_eq(&self.owner.0, &owner.0)
421 }
422}
423
424impl PartialEq for NodeHandle {
425 fn eq(&self, other: &Self) -> bool {
426 self.uuid == other.uuid
427 }
428}
429
430impl Eq for NodeHandle {}
431
432impl fmt::Display for NodeHandle {
433 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
434 write!(f, "{}(uuid={})", self.label, self.uuid)
435 }
436}
437
438#[derive(Debug, Clone, PartialEq)]
440pub enum NodeSelector {
441 Uuid(::uuid::Uuid),
443 Handle(NodeHandle),
445 Match {
447 label: String,
449 property: String,
451 value: PropValue,
453 },
454}
455
456impl NodeSelector {
457 pub fn uuid(value: &str) -> Result<Self, GfError> {
459 ::uuid::Uuid::parse_str(value)
460 .map(Self::Uuid)
461 .map_err(|_| GfError::Validation(format!("invalid node UUID {value:?}")))
462 }
463}
464
465#[derive(Debug, Clone)]
467pub struct EdgeHandle {
468 pub uuid: ::uuid::Uuid,
470 pub rel_type: String,
472}
473
474impl EdgeHandle {
475 #[doc(hidden)]
477 #[must_use]
478 pub fn new(uuid: ::uuid::Uuid, rel_type: impl Into<String>) -> Self {
479 Self {
480 uuid,
481 rel_type: rel_type.into(),
482 }
483 }
484}
485
486impl PartialEq for EdgeHandle {
487 fn eq(&self, other: &Self) -> bool {
488 self.uuid == other.uuid
489 }
490}
491
492impl Eq for EdgeHandle {}
493
494impl fmt::Display for EdgeHandle {
495 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
496 write!(f, "{}(uuid={})", self.rel_type, self.uuid)
497 }
498}
499
500#[derive(Debug, Clone)]
506pub struct RankOptions {
507 pub by: algorithms::RankAlgorithm,
509 pub via: Option<String>,
511 pub directed: bool,
513 pub write_property: Option<String>,
515}
516
517impl Default for RankOptions {
518 fn default() -> Self {
519 Self {
520 by: algorithms::RankAlgorithm::default(),
521 via: None,
522 directed: true,
523 write_property: None,
524 }
525 }
526}
527
528#[derive(Debug, Clone, Default)]
530pub struct ClusterOptions {
531 pub by: algorithms::ClusterAlgorithm,
533 pub vector_property: Option<String>,
535 pub via: Option<String>,
537 pub directed: bool,
539 pub write_property: Option<String>,
541}
542
543#[derive(Debug, Clone)]
545pub struct FindOptions {
546 pub query: Option<String>,
548 pub label: Option<String>,
550 pub vector: Option<Vec<f32>>,
552 pub similar_to: Option<NodeSelector>,
554 pub semantic_query: Option<String>,
556 pub limit: usize,
558 pub space: Option<String>,
560 pub force_stale: bool,
562}
563
564impl Default for FindOptions {
565 fn default() -> Self {
566 Self {
567 query: None,
568 label: None,
569 vector: None,
570 similar_to: None,
571 semantic_query: None,
572 limit: 10,
573 space: None,
574 force_stale: false,
575 }
576 }
577}
578
579#[derive(Debug, Clone)]
581pub struct PathsOptions {
582 pub by: algorithms::PathAlgorithm,
584 pub via: Option<String>,
586 pub directed: bool,
588 pub k: usize,
590 pub weight: Option<String>,
592 pub capacity_property: Option<String>,
594 pub cost_property: Option<String>,
596 pub heuristic: Option<String>,
598 pub walk_length: Option<usize>,
600 pub seed: Option<u64>,
602 pub terminal_uuids: Vec<[u8; 16]>,
604 pub prize_property: Option<String>,
606}
607
608impl Default for PathsOptions {
609 fn default() -> Self {
610 Self {
611 by: algorithms::PathAlgorithm::Bfs,
612 via: None,
613 directed: true,
614 k: 1,
615 weight: None,
616 capacity_property: None,
617 cost_property: None,
618 heuristic: None,
619 walk_length: None,
620 seed: None,
621 terminal_uuids: Vec::new(),
622 prize_property: None,
623 }
624 }
625}
626
627#[derive(Debug, Clone)]
629pub struct AnalyzeOptions {
630 pub by: algorithms::AnalyzeAlgorithm,
632 pub via: Option<String>,
634 pub directed: bool,
636 pub weight: Option<String>,
638 pub k: Option<usize>,
640 pub partition_property: Option<String>,
642}
643
644impl Default for AnalyzeOptions {
645 fn default() -> Self {
646 Self {
647 by: algorithms::AnalyzeAlgorithm::IsDag,
648 via: None,
649 directed: true,
650 weight: None,
651 k: None,
652 partition_property: None,
653 }
654 }
655}
656
657#[derive(Debug, Clone)]
659pub struct SimilarOptions {
660 pub by: algorithms::SimilarAlgorithm,
662 pub k: usize,
664 pub vector_property: Option<String>,
666 pub via: Option<String>,
668}
669
670impl Default for SimilarOptions {
671 fn default() -> Self {
672 Self {
673 by: algorithms::SimilarAlgorithm::default(),
674 k: 10,
675 vector_property: None,
676 via: None,
677 }
678 }
679}
680
681#[derive(Debug, Clone, Copy, PartialEq, Eq)]
692pub enum ExplainStage {
693 Ast,
695 BoundAst,
701 GraphIr,
706 LogicalPlan,
708 PhysicalPlan,
710}
711
712#[cfg(test)]
725mod tests {
726 use super::*;
727
728 #[test]
729 fn span_display() {
730 assert_eq!(Span::new(0, 5).to_string(), "0..5");
731 }
732
733 #[test]
734 fn gf_error_not_implemented() {
735 let e = GfError::NotImplemented("execute");
736 assert!(e.to_string().contains("execute"));
737 }
738
739 #[test]
740 fn node_handle_display() {
741 let owner = GraphIdentity::new();
742 let uuid = ::uuid::Uuid::from_bytes([1; 16]);
743 let h = NodeHandle::new(uuid, "Person", owner.clone());
744 assert!(h.to_string().contains("Person"));
745 assert!(h.to_string().contains(&uuid.to_string()));
746 assert!(h.belongs_to(&owner));
747 assert!(!h.belongs_to(&GraphIdentity::new()));
748 assert_eq!(h, NodeHandle::new(uuid, "Other", GraphIdentity::new()));
749 }
750
751 #[test]
752 fn edge_handle_identity_and_display_are_uuid_based() {
753 let uuid = ::uuid::Uuid::from_bytes([2; 16]);
754 let handle = EdgeHandle::new(uuid, "KNOWS");
755 assert_eq!(handle.uuid, uuid);
756 assert_eq!(handle.rel_type, "KNOWS");
757 assert_eq!(handle, EdgeHandle::new(uuid, "OTHER"));
758 assert_ne!(
759 handle,
760 EdgeHandle::new(::uuid::Uuid::from_bytes([3; 16]), "KNOWS"),
761 );
762 assert_eq!(handle.to_string(), format!("KNOWS(uuid={uuid})"));
763 assert!(!handle.to_string().starts_with("Edge(id="));
764 }
765
766 #[test]
767 fn paths_options_default_to_the_canonical_bfs_contract() {
768 let options = PathsOptions::default();
769 assert_eq!(options.by, algorithms::PathAlgorithm::Bfs);
770 assert_eq!(options.via, None);
771 assert!(options.directed);
772 assert_eq!(options.k, 1);
773 assert_eq!(options.weight, None);
774 assert!(options.terminal_uuids.is_empty());
775 assert_eq!(options.prize_property, None);
776 }
777
778 #[test]
779 fn analyze_options_default_to_the_canonical_is_dag_contract() {
780 let options = AnalyzeOptions::default();
781 assert_eq!(options.by, algorithms::AnalyzeAlgorithm::IsDag);
782 assert_eq!(options.via, None);
783 assert!(options.directed);
784 }
785
786 #[test]
787 fn find_options_default_to_no_query_or_stale_override() {
788 let options = FindOptions::default();
789 assert_eq!(options.query, None);
790 assert_eq!(options.label, None);
791 assert_eq!(options.vector, None);
792 assert_eq!(options.similar_to, None);
793 assert_eq!(options.semantic_query, None);
794 assert_eq!(options.limit, 10);
795 assert_eq!(options.space, None);
796 assert!(!options.force_stale);
797 }
798
799 #[test]
800 fn stable_error_code_enums_cover_every_public_variant() {
801 let api = [
802 (ApiErrorCode::NotFound, "GF_NOT_FOUND"),
803 (ApiErrorCode::Cancelled, "GF_CANCELLED"),
804 (ApiErrorCode::ResourceLimit, "GF_RESOURCE_LIMIT"),
805 (ApiErrorCode::PageInvalid, "GF_PAGE_INVALID"),
806 (ApiErrorCode::PageSnapshotGone, "GF_PAGE_SNAPSHOT_GONE"),
807 (ApiErrorCode::SchemaMismatch, "GF_SCHEMA_MISMATCH"),
808 (ApiErrorCode::UnknownArgument, "GF_UNKNOWN_ARGUMENT"),
809 (ApiErrorCode::AmbiguousProjection, "GF_AMBIGUOUS_PROJECTION"),
810 (ApiErrorCode::IdentityConflict, "GF_IDENTITY_CONFLICT"),
811 (
812 ApiErrorCode::FingerprintCollision,
813 "GF_FINGERPRINT_COLLISION",
814 ),
815 (ApiErrorCode::ResultNotRetained, "GF_RESULT_NOT_RETAINED"),
816 ];
817 for (code, spelling) in api {
818 assert_eq!(code.as_str(), spelling);
819 assert_eq!(code.to_string(), spelling);
820 }
821
822 let project = [
823 (
824 ProjectErrorCode::UnsupportedProjectFormat,
825 "GF_UNSUPPORTED_PROJECT_FORMAT",
826 ),
827 (
828 ProjectErrorCode::ProjectUninitialized,
829 "GF_PROJECT_UNINITIALIZED",
830 ),
831 (ProjectErrorCode::ProjectCorrupt, "GF_PROJECT_CORRUPT"),
832 (
833 ProjectErrorCode::UnsupportedFilesystem,
834 "GF_UNSUPPORTED_FILESYSTEM",
835 ),
836 (ProjectErrorCode::WriterBusy, "GF_WRITER_BUSY"),
837 (ProjectErrorCode::WriteConflict, "GF_WRITE_CONFLICT"),
838 (ProjectErrorCode::RebaseExhausted, "GF_REBASE_EXHAUSTED"),
839 (
840 ProjectErrorCode::TransactionConflict,
841 "GF_IDEMPOTENCY_CONFLICT",
842 ),
843 (ProjectErrorCode::PublicationFailed, "GF_PUBLICATION_FAILED"),
844 (
845 ProjectErrorCode::UnsupportedCapabilityVersion,
846 "GF_UNSUPPORTED_CAPABILITY_VERSION",
847 ),
848 (
849 ProjectErrorCode::CapabilityDisabled,
850 "GF_CAPABILITY_DISABLED",
851 ),
852 (ProjectErrorCode::TransactionFailed, "GF_TRANSACTION_FAILED"),
853 (ProjectErrorCode::CheckpointExists, "GF_CHECKPOINT_EXISTS"),
854 (
855 ProjectErrorCode::CheckpointNotFound,
856 "GF_CHECKPOINT_NOT_FOUND",
857 ),
858 (
859 ProjectErrorCode::CheckpointRegistryCorrupt,
860 "GF_CHECKPOINT_REGISTRY_CORRUPT",
861 ),
862 (ProjectErrorCode::ReadOnlyView, "GF_READ_ONLY_VIEW"),
863 (ProjectErrorCode::ResourceLimit, "GF_RESOURCE_LIMIT"),
864 ];
865 for (code, spelling) in project {
866 assert_eq!(code.as_str(), spelling);
867 assert_eq!(code.to_string(), spelling);
868 }
869 }
870
871 #[test]
872 fn public_value_display_and_selector_validation_cover_all_shapes() {
873 let values = [
874 (PropValue::Null, "null"),
875 (PropValue::Bool(true), "true"),
876 (PropValue::Int(-7), "-7"),
877 (PropValue::Float(1.5), "1.5"),
878 (PropValue::Str("x".into()), "x"),
879 (
880 PropValue::List(vec![PropValue::Int(1), PropValue::Null]),
881 "[1, null]",
882 ),
883 ];
884 for (value, rendered) in values {
885 assert_eq!(value.to_string(), rendered);
886 }
887 assert!(matches!(
888 NodeSelector::uuid("00000000-0000-0000-0000-000000000001"),
889 Ok(NodeSelector::Uuid(_))
890 ));
891 assert!(matches!(
892 NodeSelector::uuid("not-a-uuid"),
893 Err(GfError::Validation(_))
894 ));
895 assert_eq!(format!("{:?}", GraphIdentity::new()), "GraphIdentity(..)");
896 }
897
898 #[test]
899 fn every_gf_error_fault_domain_has_a_stable_code() {
900 let span = Span::new(1, 2);
901 let errors = [
902 (GfError::NotImplemented("x"), "GF_NOT_IMPLEMENTED"),
903 (
904 GfError::Parse {
905 msg: "x".into(),
906 span,
907 },
908 "GF_PARSE",
909 ),
910 (
911 GfError::Bind {
912 msg: "x".into(),
913 span,
914 },
915 "GF_PLAN",
916 ),
917 (GfError::Plan("x".into()), "GF_PLAN"),
918 (GfError::Execution("x".into()), "GF_EXECUTION"),
919 (
920 GfError::Provider {
921 class: "c".into(),
922 provider: "p".into(),
923 model: "m".into(),
924 },
925 "GF_EXECUTION",
926 ),
927 (GfError::Storage("x".into()), "GF_IO"),
928 (
929 GfError::Project {
930 code: ProjectErrorCode::ProjectCorrupt,
931 message: "x".into(),
932 },
933 "GF_PROJECT_CORRUPT",
934 ),
935 (
936 GfError::Api {
937 code: ApiErrorCode::NotFound,
938 message: "x".into(),
939 },
940 "GF_NOT_FOUND",
941 ),
942 (GfError::Lifecycle("x".into()), "GF_LIFECYCLE"),
943 (GfError::Validation("x".into()), "GF_VALIDATION"),
944 (GfError::Ontology("x".into()), "GF_ONTOLOGY"),
945 ];
946 for (error, code) in errors {
947 assert_eq!(error.code(), code);
948 }
949 }
950}