1#![forbid(unsafe_code)]
9
10mod algorithm_error;
11mod bind_error;
12mod cypher_feature;
13mod lowering_error;
14mod parse_error;
15pub use algorithm_error::AlgorithmError;
16pub use bind_error::{BindError, BindErrorKind};
17pub use cypher_feature::UnsupportedCypherFeature;
18pub use lowering_error::LoweringError;
19pub use parse_error::{ParseError, ParseErrorKind};
20
21pub mod algorithms;
22pub mod canonical;
23pub mod embedding_options;
24pub mod identifier;
25pub mod manifest;
26pub mod uuid;
27
28use std::{fmt, sync::Arc};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
36pub struct Span {
37 pub start: usize,
39 pub end: usize,
41}
42
43impl Span {
44 #[must_use]
46 pub const fn new(start: usize, end: usize) -> Self {
47 Self { start, end }
48 }
49}
50
51impl fmt::Display for Span {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, "{}..{}", self.start, self.end)
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
66pub struct TypeId(pub u32);
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
74pub struct PropId(pub u32);
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum OntologyFormat {
79 Yaml,
81 Json,
83}
84
85#[derive(
92 Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
93)]
94#[serde(rename_all = "lowercase")]
95pub enum OntologyMode {
96 #[default]
99 Exploratory,
100 Advisory,
102 Strict,
104}
105
106#[derive(thiserror::Error, Debug, Clone)]
116pub enum GfError {
117 #[error("not implemented: {0}")]
119 NotImplemented(&'static str),
120
121 #[error("parse error at {span}: {msg}")]
123 Parse {
124 diagnostic: Option<Box<ParseError>>,
126 msg: String,
128 span: Span,
130 },
131
132 #[error("bind error at {span}: {msg}")]
139 Bind {
140 diagnostics: Vec<BindError>,
142 msg: String,
144 span: Span,
146 },
147
148 #[error("{domain} error: {0}", domain = .0.fault_domain())]
150 Lowering(#[source] LoweringError),
151
152 #[error("execution error: {0}")]
155 LoweringExecution(#[source] LoweringError),
156
157 #[error("plan error: {msg}")]
159 BindPlan {
160 msg: String,
162 diagnostics: Vec<BindError>,
164 },
165
166 #[error("validation error: {msg}")]
168 BindValidation {
169 msg: String,
171 diagnostics: Vec<BindError>,
173 },
174
175 #[error("{domain} error: {0}", domain = .0.fault_domain())]
177 Algorithm(#[source] AlgorithmError),
178
179 #[error("plan error: {0}")]
181 Plan(String),
182
183 #[error("execution error: {0}")]
185 Execution(String),
186
187 #[error("provider error: class={class} provider={provider} model={model}")]
189 Provider {
190 class: String,
192 provider: String,
194 model: String,
196 },
197
198 #[error("storage error: {0}")]
200 Storage(String),
201
202 #[error("{code}: {message}")]
204 Project {
205 code: ProjectErrorCode,
207 message: String,
209 },
210
211 #[error("{code}: {message}")]
213 Api {
214 code: ApiErrorCode,
216 message: String,
218 },
219
220 #[error("lifecycle error: {0}")]
222 Lifecycle(String),
223
224 #[error("validation error: {0}")]
226 Validation(String),
227
228 #[error("ontology error: {0}")]
230 Ontology(String),
231}
232
233mod error_conversion;
234
235impl GfError {
236 #[must_use]
238 pub const fn code(&self) -> &'static str {
239 match self {
240 Self::Lowering(LoweringError::InvalidType(_))
241 | Self::BindValidation { .. }
242 | Self::Validation(_)
243 | Self::Algorithm(
244 AlgorithmError::Unavailable { .. } | AlgorithmError::DuplicateCapability { .. },
245 ) => "GF_VALIDATION",
246 Self::Lowering(_) | Self::BindPlan { .. } | Self::Plan(_) => "GF_PLAN",
247 Self::Algorithm(_)
248 | Self::LoweringExecution(_)
249 | Self::Execution(_)
250 | Self::Provider { .. } => "GF_EXECUTION",
251 Self::NotImplemented(_) => "GF_NOT_IMPLEMENTED",
252 Self::Parse { .. } | Self::Bind { .. } => "GF_PARSE",
253 Self::Storage(_) => "GF_IO",
254 Self::Project { code, .. } => code.as_str(),
255 Self::Api { code, .. } => code.as_str(),
256 Self::Lifecycle(_) => "GF_LIFECYCLE",
257 Self::Ontology(_) => "GF_ONTOLOGY",
258 }
259 }
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub enum ApiErrorCode {
265 NotFound,
267 Cancelled,
269 ResourceLimit,
271 PageInvalid,
273 PageSnapshotGone,
275 SchemaMismatch,
277 UnknownArgument,
279 AmbiguousProjection,
281 IdentityConflict,
283 FingerprintCollision,
285 ResultNotRetained,
287}
288
289impl ApiErrorCode {
290 #[must_use]
292 pub const fn as_str(self) -> &'static str {
293 match self {
294 Self::NotFound => "GF_NOT_FOUND",
295 Self::Cancelled => "GF_CANCELLED",
296 Self::ResourceLimit => "GF_RESOURCE_LIMIT",
297 Self::PageInvalid => "GF_PAGE_INVALID",
298 Self::PageSnapshotGone => "GF_PAGE_SNAPSHOT_GONE",
299 Self::SchemaMismatch => "GF_SCHEMA_MISMATCH",
300 Self::UnknownArgument => "GF_UNKNOWN_ARGUMENT",
301 Self::AmbiguousProjection => "GF_AMBIGUOUS_PROJECTION",
302 Self::IdentityConflict => "GF_IDENTITY_CONFLICT",
303 Self::FingerprintCollision => "GF_FINGERPRINT_COLLISION",
304 Self::ResultNotRetained => "GF_RESULT_NOT_RETAINED",
305 }
306 }
307}
308
309impl fmt::Display for ApiErrorCode {
310 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311 f.write_str(self.as_str())
312 }
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum ProjectErrorCode {
318 UnsupportedProjectFormat,
320 ProjectUninitialized,
322 ProjectCorrupt,
324 UnsupportedFilesystem,
326 WriterBusy,
328 WriteConflict,
330 RebaseExhausted,
332 TransactionConflict,
334 PublicationFailed,
336 UnsupportedCapabilityVersion,
338 CapabilityDisabled,
340 TransactionFailed,
342 CheckpointExists,
344 CheckpointNotFound,
346 CheckpointRegistryCorrupt,
348 ReadOnlyView,
350 ResourceLimit,
352}
353
354impl ProjectErrorCode {
355 #[must_use]
357 pub const fn as_str(self) -> &'static str {
358 match self {
359 Self::UnsupportedProjectFormat => "GF_UNSUPPORTED_PROJECT_FORMAT",
360 Self::ProjectUninitialized => "GF_PROJECT_UNINITIALIZED",
361 Self::ProjectCorrupt => "GF_PROJECT_CORRUPT",
362 Self::UnsupportedFilesystem => "GF_UNSUPPORTED_FILESYSTEM",
363 Self::WriterBusy => "GF_WRITER_BUSY",
364 Self::WriteConflict => "GF_WRITE_CONFLICT",
365 Self::RebaseExhausted => "GF_REBASE_EXHAUSTED",
366 Self::TransactionConflict => "GF_IDEMPOTENCY_CONFLICT",
367 Self::PublicationFailed => "GF_PUBLICATION_FAILED",
368 Self::UnsupportedCapabilityVersion => "GF_UNSUPPORTED_CAPABILITY_VERSION",
369 Self::CapabilityDisabled => "GF_CAPABILITY_DISABLED",
370 Self::TransactionFailed => "GF_TRANSACTION_FAILED",
371 Self::CheckpointExists => "GF_CHECKPOINT_EXISTS",
372 Self::CheckpointNotFound => "GF_CHECKPOINT_NOT_FOUND",
373 Self::CheckpointRegistryCorrupt => "GF_CHECKPOINT_REGISTRY_CORRUPT",
374 Self::ReadOnlyView => "GF_READ_ONLY_VIEW",
375 Self::ResourceLimit => "GF_RESOURCE_LIMIT",
376 }
377 }
378}
379
380impl fmt::Display for ProjectErrorCode {
381 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382 f.write_str(self.as_str())
383 }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
396#[serde(tag = "type", rename_all = "snake_case")]
397pub enum TemporalValue {
398 Duration {
400 months: i64,
402 days: i64,
404 seconds: i64,
406 nanos: i64,
408 },
409 UtcDateTime {
411 epoch_micros: i64,
413 },
414 Date {
416 epoch_days: i64,
418 },
419 LocalDateTime {
421 epoch_days: i64,
423 nanos: i64,
425 },
426 LocalTime {
428 nanos: i64,
430 },
431 OffsetTime {
433 nanos: i64,
435 offset_seconds: i32,
437 },
438 ZonedDateTime {
440 epoch_days: i64,
442 nanos: i64,
444 offset_seconds: i32,
446 zone: Option<String>,
448 },
449}
450
451impl TemporalValue {
452 pub fn validate(&self) -> Result<(), GfError> {
454 const NANOS_PER_DAY: i64 = 86_400_000_000_000;
455 const MAX_OFFSET_SECONDS: i32 = 18 * 60 * 60;
456 let validate_time = |nanos: i64| {
457 if (0..NANOS_PER_DAY).contains(&nanos) {
458 Ok(())
459 } else {
460 Err(GfError::Validation(
461 "temporal nanoseconds must be within one day".into(),
462 ))
463 }
464 };
465 let validate_offset = |offset: i32| {
466 if (-MAX_OFFSET_SECONDS..=MAX_OFFSET_SECONDS).contains(&offset) {
467 Ok(())
468 } else {
469 Err(GfError::Validation(
470 "temporal UTC offset must be within plus or minus 18 hours".into(),
471 ))
472 }
473 };
474 match self {
475 Self::Duration { nanos, .. } if !(-999_999_999..=999_999_999).contains(nanos) => {
476 Err(GfError::Validation(
477 "duration nanoseconds must be between -999999999 and 999999999".into(),
478 ))
479 }
480 Self::LocalDateTime { nanos, .. } | Self::LocalTime { nanos } => validate_time(*nanos),
481 Self::OffsetTime {
482 nanos,
483 offset_seconds,
484 } => {
485 validate_time(*nanos)?;
486 validate_offset(*offset_seconds)
487 }
488 Self::ZonedDateTime {
489 nanos,
490 offset_seconds,
491 zone,
492 ..
493 } => {
494 validate_time(*nanos)?;
495 validate_offset(*offset_seconds)?;
496 if let Some(zone) = zone
497 && (zone.is_empty() || zone.len() > 255 || zone.chars().any(char::is_control))
498 {
499 return Err(GfError::Validation(
500 "temporal zone must be nonempty, control-free UTF-8 up to 255 bytes".into(),
501 ));
502 }
503 Ok(())
504 }
505 _ => Ok(()),
506 }
507 }
508}
509
510#[derive(Debug, Clone, PartialEq, Eq, Hash)]
512pub enum SpatialCrs {
513 Epsg4326,
515 Epsg3857,
517 Preserved(String),
519}
520
521impl serde::Serialize for SpatialCrs {
522 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
523 where
524 S: serde::Serializer,
525 {
526 serializer.serialize_str(match self {
527 Self::Epsg4326 => "EPSG:4326",
528 Self::Epsg3857 => "EPSG:3857",
529 Self::Preserved(value) => value,
530 })
531 }
532}
533
534impl<'de> serde::Deserialize<'de> for SpatialCrs {
535 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
536 where
537 D: serde::Deserializer<'de>,
538 {
539 let value = <String as serde::Deserialize>::deserialize(deserializer)?;
540 Ok(match value.as_str() {
541 "EPSG:4326" => Self::Epsg4326,
542 "EPSG:3857" => Self::Epsg3857,
543 _ => Self::Preserved(value),
544 })
545 }
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
550#[serde(rename_all = "snake_case")]
551pub enum SpatialGeometryType {
552 Point,
554 LineString,
556 Polygon,
558 MultiPoint,
560 MultiLineString,
562 MultiPolygon,
564}
565
566#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
568pub struct SpatialType {
569 pub geometry: SpatialGeometryType,
571 pub crs: SpatialCrs,
573}
574
575#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
577pub enum SpatialCoordinates {
578 Point([f64; 2]),
580 LineString(Vec<[f64; 2]>),
582 Polygon(Vec<Vec<[f64; 2]>>),
584 MultiPoint(Vec<[f64; 2]>),
586 MultiLineString(Vec<Vec<[f64; 2]>>),
588 MultiPolygon(Vec<Vec<Vec<[f64; 2]>>>),
590}
591
592#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
594pub struct SpatialValue {
595 pub spatial_type: SpatialType,
597 pub coordinates: SpatialCoordinates,
599 #[serde(default, skip_serializing_if = "Option::is_none")]
601 pub extension_name: Option<String>,
602 #[serde(default, skip_serializing_if = "Option::is_none")]
604 pub extension_metadata: Option<String>,
605}
606
607impl SpatialValue {
608 #[must_use]
610 pub fn is_preserved_only(&self) -> bool {
611 matches!(self.spatial_type.crs, SpatialCrs::Preserved(_))
612 || self.extension_name.is_some()
613 || self.extension_metadata.is_some()
614 }
615
616 pub fn validate_interchange_profile(&self) -> Result<(), &'static str> {
618 if !self.is_preserved_only() {
619 return Ok(());
620 }
621 let (Some(name), Some(metadata)) = (&self.extension_name, &self.extension_metadata) else {
622 return Err(
623 "preserved-only spatial values require extension_name and extension_metadata",
624 );
625 };
626 if name.is_empty() {
627 return Err("preserved-only spatial extension_name must not be empty");
628 }
629 let trimmed = metadata.trim();
630 let valid_metadata = trimmed.starts_with('{')
631 && trimmed.ends_with('}')
632 && trimmed.contains("\"crs\"")
633 && trimmed.contains(':');
634 if !valid_metadata {
635 return Err("preserved-only spatial extension_metadata must contain a CRS");
636 }
637 Ok(())
638 }
639}
640
641#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
643#[non_exhaustive]
644pub enum PropValue {
645 Null,
647 Bool(bool),
649 Int(i64),
651 Float(f64),
653 Str(String),
655 List(Vec<PropValue>),
657 Temporal(TemporalValue),
659 Spatial(SpatialValue),
661}
662
663impl fmt::Display for PropValue {
664 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
665 match self {
666 Self::Null => write!(f, "null"),
667 Self::Bool(b) => write!(f, "{b}"),
668 Self::Int(i) => write!(f, "{i}"),
669 Self::Float(fl) => write!(f, "{fl}"),
670 Self::Str(s) => write!(f, "{s}"),
671 Self::List(l) => {
672 write!(f, "[")?;
673 for (i, v) in l.iter().enumerate() {
674 if i > 0 {
675 write!(f, ", ")?;
676 }
677 write!(f, "{v}")?;
678 }
679 write!(f, "]")
680 }
681 Self::Temporal(value) => write!(f, "temporal({value:?})"),
682 Self::Spatial(value) => write!(
683 f,
684 "spatial({:?}, {:?})",
685 value.spatial_type, value.coordinates
686 ),
687 }
688 }
689}
690
691#[doc(hidden)]
697#[derive(Clone, Default)]
698pub struct GraphIdentity(Arc<()>);
699
700impl GraphIdentity {
701 #[must_use]
703 pub fn new() -> Self {
704 Self::default()
705 }
706}
707
708impl fmt::Debug for GraphIdentity {
709 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
710 f.write_str("GraphIdentity(..)")
711 }
712}
713
714#[derive(Debug, Clone)]
716pub struct NodeHandle {
717 pub uuid: ::uuid::Uuid,
719 pub label: String,
721 owner: GraphIdentity,
722}
723
724impl NodeHandle {
725 #[doc(hidden)]
727 #[must_use]
728 pub fn new(uuid: ::uuid::Uuid, label: impl Into<String>, owner: GraphIdentity) -> Self {
729 Self {
730 uuid,
731 label: label.into(),
732 owner,
733 }
734 }
735
736 #[doc(hidden)]
738 #[must_use]
739 pub fn belongs_to(&self, owner: &GraphIdentity) -> bool {
740 Arc::ptr_eq(&self.owner.0, &owner.0)
741 }
742}
743
744impl PartialEq for NodeHandle {
745 fn eq(&self, other: &Self) -> bool {
746 self.uuid == other.uuid
747 }
748}
749
750impl Eq for NodeHandle {}
751
752impl fmt::Display for NodeHandle {
753 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
754 write!(f, "{}(uuid={})", self.label, self.uuid)
755 }
756}
757
758#[derive(Debug, Clone, PartialEq)]
760pub enum NodeSelector {
761 Uuid(::uuid::Uuid),
763 Handle(NodeHandle),
765 Match {
767 label: String,
769 property: String,
771 value: PropValue,
773 },
774}
775
776impl NodeSelector {
777 pub fn uuid(value: &str) -> Result<Self, GfError> {
779 ::uuid::Uuid::parse_str(value)
780 .map(Self::Uuid)
781 .map_err(|_| GfError::Validation(format!("invalid node UUID {value:?}")))
782 }
783}
784
785#[derive(Debug, Clone)]
787pub struct EdgeHandle {
788 pub uuid: ::uuid::Uuid,
790 pub rel_type: String,
792}
793
794impl EdgeHandle {
795 #[doc(hidden)]
797 #[must_use]
798 pub fn new(uuid: ::uuid::Uuid, rel_type: impl Into<String>) -> Self {
799 Self {
800 uuid,
801 rel_type: rel_type.into(),
802 }
803 }
804}
805
806impl PartialEq for EdgeHandle {
807 fn eq(&self, other: &Self) -> bool {
808 self.uuid == other.uuid
809 }
810}
811
812impl Eq for EdgeHandle {}
813
814impl fmt::Display for EdgeHandle {
815 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
816 write!(f, "{}(uuid={})", self.rel_type, self.uuid)
817 }
818}
819
820#[derive(Debug, Clone, Copy, PartialEq)]
826pub struct PageRankOptions {
827 pub damping: f64,
829 pub iterations: Option<u32>,
831}
832
833impl Default for PageRankOptions {
834 fn default() -> Self {
835 Self {
836 damping: 0.85,
837 iterations: None,
838 }
839 }
840}
841
842#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
844pub enum ClusteringNormalization {
845 #[default]
847 Fagiolo,
848 NeighborEdges,
850}
851
852#[derive(Debug, Clone, PartialEq, Eq)]
855pub struct SynchronousLabelPropagationOptions {
856 pub iterations: u32,
858 pub initial_label_property: Option<String>,
860}
861
862#[derive(Debug, Clone)]
864pub struct RankOptions {
865 pub by: algorithms::RankAlgorithm,
867 pub via: Option<String>,
869 pub directed: bool,
871 pub write_property: Option<String>,
873 pub pagerank: Option<PageRankOptions>,
875 pub clustering_normalization: Option<ClusteringNormalization>,
877}
878
879impl Default for RankOptions {
880 fn default() -> Self {
881 Self {
882 by: algorithms::RankAlgorithm::default(),
883 via: None,
884 directed: true,
885 write_property: None,
886 pagerank: None,
887 clustering_normalization: None,
888 }
889 }
890}
891
892#[derive(Debug, Clone, Default)]
894pub struct ClusterOptions {
895 pub by: algorithms::ClusterAlgorithm,
897 pub vector_property: Option<String>,
899 pub via: Option<String>,
901 pub directed: bool,
903 pub write_property: Option<String>,
905 pub synchronous_label_propagation: Option<SynchronousLabelPropagationOptions>,
907}
908
909#[derive(Debug, Clone)]
911pub struct FindOptions {
912 pub query: Option<String>,
914 pub label: Option<String>,
916 pub vector: Option<Vec<f32>>,
918 pub similar_to: Option<NodeSelector>,
920 pub semantic_query: Option<String>,
922 pub limit: usize,
924 pub space: Option<String>,
926 pub force_stale: bool,
928}
929
930impl Default for FindOptions {
931 fn default() -> Self {
932 Self {
933 query: None,
934 label: None,
935 vector: None,
936 similar_to: None,
937 semantic_query: None,
938 limit: 10,
939 space: None,
940 force_stale: false,
941 }
942 }
943}
944
945#[derive(Debug, Clone)]
947pub struct PathsOptions {
948 pub by: algorithms::PathAlgorithm,
950 pub via: Option<String>,
952 pub directed: bool,
954 pub k: usize,
956 pub weight: Option<String>,
958 pub capacity_property: Option<String>,
960 pub cost_property: Option<String>,
962 pub heuristic: Option<String>,
964 pub walk_length: Option<usize>,
966 pub seed: Option<u64>,
968 pub terminal_uuids: Vec<[u8; 16]>,
970 pub prize_property: Option<String>,
972}
973
974impl Default for PathsOptions {
975 fn default() -> Self {
976 Self {
977 by: algorithms::PathAlgorithm::Bfs,
978 via: None,
979 directed: true,
980 k: 1,
981 weight: None,
982 capacity_property: None,
983 cost_property: None,
984 heuristic: None,
985 walk_length: None,
986 seed: None,
987 terminal_uuids: Vec::new(),
988 prize_property: None,
989 }
990 }
991}
992
993#[derive(Debug, Clone)]
995pub struct AnalyzeOptions {
996 pub by: algorithms::AnalyzeAlgorithm,
998 pub via: Option<String>,
1000 pub directed: bool,
1002 pub weight: Option<String>,
1004 pub k: Option<usize>,
1006 pub partition_property: Option<String>,
1008}
1009
1010impl Default for AnalyzeOptions {
1011 fn default() -> Self {
1012 Self {
1013 by: algorithms::AnalyzeAlgorithm::IsDag,
1014 via: None,
1015 directed: true,
1016 weight: None,
1017 k: None,
1018 partition_property: None,
1019 }
1020 }
1021}
1022
1023#[derive(Debug, Clone)]
1025pub struct SimilarOptions {
1026 pub by: algorithms::SimilarAlgorithm,
1028 pub k: usize,
1030 pub vector_property: Option<String>,
1032 pub via: Option<String>,
1034}
1035
1036impl Default for SimilarOptions {
1037 fn default() -> Self {
1038 Self {
1039 by: algorithms::SimilarAlgorithm::default(),
1040 k: 10,
1041 vector_property: None,
1042 via: None,
1043 }
1044 }
1045}
1046
1047#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1056pub enum ExplainStage {
1057 Ast,
1059 BoundAst,
1065 GraphIr,
1070 LogicalPlan,
1072 PhysicalPlan,
1074}
1075
1076#[cfg(test)]
1088mod tests {
1089 use super::*;
1090
1091 #[test]
1092 fn span_display() {
1093 assert_eq!(Span::new(0, 5).to_string(), "0..5");
1094 }
1095
1096 #[test]
1097 fn gf_error_not_implemented() {
1098 let e = GfError::NotImplemented("execute");
1099 assert!(e.to_string().contains("execute"));
1100 }
1101
1102 #[test]
1103 fn node_handle_display() {
1104 let owner = GraphIdentity::new();
1105 let uuid = ::uuid::Uuid::from_bytes([1; 16]);
1106 let h = NodeHandle::new(uuid, "Person", owner.clone());
1107 assert!(h.to_string().contains("Person"));
1108 assert!(h.to_string().contains(&uuid.to_string()));
1109 assert!(h.belongs_to(&owner));
1110 assert!(!h.belongs_to(&GraphIdentity::new()));
1111 assert_eq!(h, NodeHandle::new(uuid, "Other", GraphIdentity::new()));
1112 }
1113
1114 #[test]
1115 fn edge_handle_identity_and_display_are_uuid_based() {
1116 let uuid = ::uuid::Uuid::from_bytes([2; 16]);
1117 let handle = EdgeHandle::new(uuid, "KNOWS");
1118 assert_eq!(handle.uuid, uuid);
1119 assert_eq!(handle.rel_type, "KNOWS");
1120 assert_eq!(handle, EdgeHandle::new(uuid, "OTHER"));
1121 assert_ne!(
1122 handle,
1123 EdgeHandle::new(::uuid::Uuid::from_bytes([3; 16]), "KNOWS"),
1124 );
1125 assert_eq!(handle.to_string(), format!("KNOWS(uuid={uuid})"));
1126 assert!(!handle.to_string().starts_with("Edge(id="));
1127 }
1128
1129 #[test]
1130 fn paths_options_default_to_the_canonical_bfs_contract() {
1131 let options = PathsOptions::default();
1132 assert_eq!(options.by, algorithms::PathAlgorithm::Bfs);
1133 assert_eq!(options.via, None);
1134 assert!(options.directed);
1135 assert_eq!(options.k, 1);
1136 assert_eq!(options.weight, None);
1137 assert!(options.terminal_uuids.is_empty());
1138 assert_eq!(options.prize_property, None);
1139 }
1140
1141 #[test]
1142 fn analyze_options_default_to_the_canonical_is_dag_contract() {
1143 let options = AnalyzeOptions::default();
1144 assert_eq!(options.by, algorithms::AnalyzeAlgorithm::IsDag);
1145 assert_eq!(options.via, None);
1146 assert!(options.directed);
1147 }
1148
1149 #[test]
1150 fn find_options_default_to_no_query_or_stale_override() {
1151 let options = FindOptions::default();
1152 assert_eq!(options.query, None);
1153 assert_eq!(options.label, None);
1154 assert_eq!(options.vector, None);
1155 assert_eq!(options.similar_to, None);
1156 assert_eq!(options.semantic_query, None);
1157 assert_eq!(options.limit, 10);
1158 assert_eq!(options.space, None);
1159 assert!(!options.force_stale);
1160 }
1161
1162 #[test]
1163 fn stable_error_code_enums_cover_every_public_variant() {
1164 let api = [
1165 (ApiErrorCode::NotFound, "GF_NOT_FOUND"),
1166 (ApiErrorCode::Cancelled, "GF_CANCELLED"),
1167 (ApiErrorCode::ResourceLimit, "GF_RESOURCE_LIMIT"),
1168 (ApiErrorCode::PageInvalid, "GF_PAGE_INVALID"),
1169 (ApiErrorCode::PageSnapshotGone, "GF_PAGE_SNAPSHOT_GONE"),
1170 (ApiErrorCode::SchemaMismatch, "GF_SCHEMA_MISMATCH"),
1171 (ApiErrorCode::UnknownArgument, "GF_UNKNOWN_ARGUMENT"),
1172 (ApiErrorCode::AmbiguousProjection, "GF_AMBIGUOUS_PROJECTION"),
1173 (ApiErrorCode::IdentityConflict, "GF_IDENTITY_CONFLICT"),
1174 (
1175 ApiErrorCode::FingerprintCollision,
1176 "GF_FINGERPRINT_COLLISION",
1177 ),
1178 (ApiErrorCode::ResultNotRetained, "GF_RESULT_NOT_RETAINED"),
1179 ];
1180 for (code, spelling) in api {
1181 assert_eq!(code.as_str(), spelling);
1182 assert_eq!(code.to_string(), spelling);
1183 }
1184
1185 let project = [
1186 (
1187 ProjectErrorCode::UnsupportedProjectFormat,
1188 "GF_UNSUPPORTED_PROJECT_FORMAT",
1189 ),
1190 (
1191 ProjectErrorCode::ProjectUninitialized,
1192 "GF_PROJECT_UNINITIALIZED",
1193 ),
1194 (ProjectErrorCode::ProjectCorrupt, "GF_PROJECT_CORRUPT"),
1195 (
1196 ProjectErrorCode::UnsupportedFilesystem,
1197 "GF_UNSUPPORTED_FILESYSTEM",
1198 ),
1199 (ProjectErrorCode::WriterBusy, "GF_WRITER_BUSY"),
1200 (ProjectErrorCode::WriteConflict, "GF_WRITE_CONFLICT"),
1201 (ProjectErrorCode::RebaseExhausted, "GF_REBASE_EXHAUSTED"),
1202 (
1203 ProjectErrorCode::TransactionConflict,
1204 "GF_IDEMPOTENCY_CONFLICT",
1205 ),
1206 (ProjectErrorCode::PublicationFailed, "GF_PUBLICATION_FAILED"),
1207 (
1208 ProjectErrorCode::UnsupportedCapabilityVersion,
1209 "GF_UNSUPPORTED_CAPABILITY_VERSION",
1210 ),
1211 (
1212 ProjectErrorCode::CapabilityDisabled,
1213 "GF_CAPABILITY_DISABLED",
1214 ),
1215 (ProjectErrorCode::TransactionFailed, "GF_TRANSACTION_FAILED"),
1216 (ProjectErrorCode::CheckpointExists, "GF_CHECKPOINT_EXISTS"),
1217 (
1218 ProjectErrorCode::CheckpointNotFound,
1219 "GF_CHECKPOINT_NOT_FOUND",
1220 ),
1221 (
1222 ProjectErrorCode::CheckpointRegistryCorrupt,
1223 "GF_CHECKPOINT_REGISTRY_CORRUPT",
1224 ),
1225 (ProjectErrorCode::ReadOnlyView, "GF_READ_ONLY_VIEW"),
1226 (ProjectErrorCode::ResourceLimit, "GF_RESOURCE_LIMIT"),
1227 ];
1228 for (code, spelling) in project {
1229 assert_eq!(code.as_str(), spelling);
1230 assert_eq!(code.to_string(), spelling);
1231 }
1232 }
1233
1234 #[test]
1235 fn public_value_display_and_selector_validation_cover_all_shapes() {
1236 let values = [
1237 (PropValue::Null, "null"),
1238 (PropValue::Bool(true), "true"),
1239 (PropValue::Int(-7), "-7"),
1240 (PropValue::Float(1.5), "1.5"),
1241 (PropValue::Str("x".into()), "x"),
1242 (
1243 PropValue::List(vec![PropValue::Int(1), PropValue::Null]),
1244 "[1, null]",
1245 ),
1246 ];
1247 for (value, rendered) in values {
1248 assert_eq!(value.to_string(), rendered);
1249 }
1250 assert!(matches!(
1251 NodeSelector::uuid("00000000-0000-0000-0000-000000000001"),
1252 Ok(NodeSelector::Uuid(_))
1253 ));
1254 assert!(matches!(
1255 NodeSelector::uuid("not-a-uuid"),
1256 Err(GfError::Validation(_))
1257 ));
1258 assert_eq!(format!("{:?}", GraphIdentity::new()), "GraphIdentity(..)");
1259 }
1260
1261 #[test]
1262 fn every_gf_error_fault_domain_has_a_stable_code() {
1263 let span = Span::new(1, 2);
1264 let errors = [
1265 (GfError::NotImplemented("x"), "GF_NOT_IMPLEMENTED"),
1266 (
1267 GfError::Parse {
1268 diagnostic: None,
1269 msg: "x".into(),
1270 span,
1271 },
1272 "GF_PARSE",
1273 ),
1274 (
1275 GfError::Bind {
1276 diagnostics: Vec::new(),
1277 msg: "x".into(),
1278 span,
1279 },
1280 "GF_PARSE",
1281 ),
1282 (GfError::Plan("x".into()), "GF_PLAN"),
1283 (GfError::Execution("x".into()), "GF_EXECUTION"),
1284 (
1285 GfError::Provider {
1286 class: "c".into(),
1287 provider: "p".into(),
1288 model: "m".into(),
1289 },
1290 "GF_EXECUTION",
1291 ),
1292 (GfError::Storage("x".into()), "GF_IO"),
1293 (
1294 GfError::Project {
1295 code: ProjectErrorCode::ProjectCorrupt,
1296 message: "x".into(),
1297 },
1298 "GF_PROJECT_CORRUPT",
1299 ),
1300 (
1301 GfError::Api {
1302 code: ApiErrorCode::NotFound,
1303 message: "x".into(),
1304 },
1305 "GF_NOT_FOUND",
1306 ),
1307 (GfError::Lifecycle("x".into()), "GF_LIFECYCLE"),
1308 (GfError::Validation("x".into()), "GF_VALIDATION"),
1309 (GfError::Ontology("x".into()), "GF_ONTOLOGY"),
1310 ];
1311 for (error, code) in errors {
1312 assert_eq!(error.code(), code);
1313 }
1314 }
1315}
1316
1317pub mod portable;
1319
1320pub mod storage_receipt;
1322
1323pub mod hash_observation;