graphforge_core/lib.rs
1//! GraphForge core — public error type, span, and shared value model.
2//!
3//! This is the **foundation crate**: every other `graphforge-*` crate depends on it. The
4//! `GraphForge` engine facade lives in the top-level `graphforge-api` crate (it needs the
5//! pipeline crates, which depend on `graphforge-core` — so it cannot live here without a
6//! cycle; see #716).
7#![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// ---------------------------------------------------------------------------
18// Span
19// ---------------------------------------------------------------------------
20
21/// Byte-offset range in the original source text.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
23pub struct Span {
24 /// Start byte offset (inclusive).
25 pub start: usize,
26 /// End byte offset (exclusive).
27 pub end: usize,
28}
29
30impl Span {
31 /// Create a new span.
32 #[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// ---------------------------------------------------------------------------
45// TypeId
46// ---------------------------------------------------------------------------
47
48/// Opaque integer identifier for any ontology type (entity, relation, or property).
49///
50/// IDs are assigned at compile time by the ontology compiler and are stable
51/// for the lifetime of a loaded ontology.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
53pub struct TypeId(pub u32);
54
55/// Opaque integer identifier for a property type.
56///
57/// Distinct from [`TypeId`] (which identifies entity/relation types) so that
58/// the type system prevents accidental interchangeability. IDs are assigned
59/// at compile time by the ontology compiler.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
61pub struct PropId(pub u32);
62
63/// Serialisation format of an ontology definition file.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum OntologyFormat {
66 /// YAML (`.yaml` or `.yml`).
67 Yaml,
68 /// JSON (`.json`).
69 Json,
70}
71
72/// How the binder resolves unknown labels, relation types, and property names.
73///
74/// Serialises as a lowercase string (`"exploratory"`, `"advisory"`,
75/// `"strict"`). Lives in `graphforge-core` so that the project manifest
76/// ([`manifest::ProjectManifest`]) and the binder (`graphforge-ir`) share one
77/// definition; `graphforge-ir` re-exports it as `graphforge_ir::OntologyMode`.
78#[derive(
79 Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
80)]
81#[serde(rename_all = "lowercase")]
82pub enum OntologyMode {
83 /// No ontology required; the runtime catalog auto-assigns integer IDs for
84 /// every observed label/type.
85 #[default]
86 Exploratory,
87 /// Ontology present; violations produce warnings, not errors.
88 Advisory,
89 /// Ontology required; violations produce a bind error.
90 Strict,
91}
92
93// ---------------------------------------------------------------------------
94// GfError — public error enum
95// ---------------------------------------------------------------------------
96
97/// All errors produced by GraphForge.
98///
99/// Variant names correspond 1-to-1 with the Python exception hierarchy in
100/// `graphforge.exceptions` so that binding layers can map them without
101/// string-matching.
102#[derive(thiserror::Error, Debug)]
103pub enum GfError {
104 /// Feature exists in the API but has not been implemented yet.
105 #[error("not implemented: {0}")]
106 NotImplemented(&'static str),
107
108 /// The Cypher parser rejected the input.
109 #[error("parse error at {span}: {msg}")]
110 Parse {
111 /// Human-readable description of the parse failure.
112 msg: String,
113 /// Location of the bad token in the source.
114 span: Span,
115 },
116
117 /// The binder rejected the query (e.g. undeclared variable, strict-mode
118 /// unknown label). Carries the source span of the *first* error so callers
119 /// can point at the offending token; `msg` lists every binder error. Shares
120 /// the `PlanError` fault domain with [`GfError::Plan`] in the binding layers
121 /// — it is the span-rich sibling for binder-phase failures.
122 #[error("bind error at {span}: {msg}")]
123 Bind {
124 /// Human-readable description (all binder errors, joined with `; `).
125 msg: String,
126 /// Source location of the first offending token.
127 span: Span,
128 },
129
130 /// The binder or query planner could not produce a valid plan.
131 #[error("plan error: {0}")]
132 Plan(String),
133
134 /// A runtime fault occurred during query execution.
135 #[error("execution error: {0}")]
136 Execution(String),
137
138 /// A redacted configured-provider invocation failed.
139 #[error("provider error: class={class} provider={provider} model={model}")]
140 Provider {
141 /// Stable provider failure class.
142 class: String,
143 /// Normalized non-secret provider identifier.
144 provider: String,
145 /// Non-secret model identifier.
146 model: String,
147 },
148
149 /// A storage I/O operation failed.
150 #[error("storage error: {0}")]
151 Storage(String),
152
153 /// The project container cannot be resolved safely.
154 #[error("{code}: {message}")]
155 Project {
156 /// Stable public error code.
157 code: ProjectErrorCode,
158 /// Safe diagnostic without participant contents or unrestricted paths.
159 message: String,
160 },
161
162 /// A structured M20/M21 public API failure.
163 #[error("{code}: {message}")]
164 Api {
165 /// Stable closed public error code.
166 code: ApiErrorCode,
167 /// Safe diagnostic without record contents.
168 message: String,
169 },
170
171 /// An operation was invalid for the current instance lifecycle state.
172 #[error("lifecycle error: {0}")]
173 Lifecycle(String),
174
175 /// Input failed validation at the API boundary.
176 #[error("validation error: {0}")]
177 Validation(String),
178
179 /// An ontology file could not be loaded or applied.
180 #[error("ontology error: {0}")]
181 Ontology(String),
182}
183
184impl GfError {
185 /// Return the stable public error code for this failure.
186 #[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/// Stable non-project M20/M21 API error codes.
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum ApiErrorCode {
206 /// Requested UUID does not exist in the selected capability snapshot.
207 NotFound,
208 /// Operation was cancelled at a deterministic checkpoint.
209 Cancelled,
210 /// Request exceeded a registered bounded-resource limit.
211 ResourceLimit,
212 /// Page token is malformed or incompatible with the method.
213 PageInvalid,
214 /// Page token names a generation that is no longer the selected snapshot.
215 PageSnapshotGone,
216 /// Persisted Arrow schema or registered fingerprint is incompatible.
217 SchemaMismatch,
218 /// Caller supplied an unknown argument.
219 UnknownArgument,
220 /// Explicit projection policy could not resolve an epistemic ambiguity.
221 AmbiguousProjection,
222 /// An identity was reused for different immutable content.
223 IdentityConflict,
224 /// Canonical fingerprint collision was detected.
225 FingerprintCollision,
226 /// A completed run did not retain its result rows.
227 ResultNotRetained,
228}
229
230impl ApiErrorCode {
231 /// Frozen external spelling.
232 #[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/// Stable project-format and project-generation error codes.
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum ProjectErrorCode {
259 /// The root is not the current supported project format.
260 UnsupportedProjectFormat,
261 /// The v1 container has no committed generation.
262 ProjectUninitialized,
263 /// The commit pointer or selected generation is internally inconsistent.
264 ProjectCorrupt,
265 /// The project filesystem cannot provide the required atomic semantics.
266 UnsupportedFilesystem,
267 /// Another process currently owns the project writer lock.
268 WriterBusy,
269 /// A staged write cannot be applied to the latest committed generation.
270 WriteConflict,
271 /// Compatible contention exceeded the caller's bounded rebase attempts.
272 RebaseExhausted,
273 /// A transaction UUID was reused with different immutable inputs.
274 TransactionConflict,
275 /// A generation failed before or after its commit point.
276 PublicationFailed,
277 /// A capability ID/version is not implemented by this binary.
278 UnsupportedCapabilityVersion,
279 /// A capability-specific operation was requested before enablement.
280 CapabilityDisabled,
281 /// A transaction failed before publication.
282 TransactionFailed,
283 /// The named checkpoint already exists.
284 CheckpointExists,
285 /// The named checkpoint does not exist.
286 CheckpointNotFound,
287 /// Checkpoint registry state is unsafe or inconsistent.
288 CheckpointRegistryCorrupt,
289 /// A mutation was attempted through an immutable checkpoint view.
290 ReadOnlyView,
291 /// A bounded checkpoint resource limit was exceeded.
292 ResourceLimit,
293}
294
295impl ProjectErrorCode {
296 /// Return the frozen public error-code spelling.
297 #[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// ---------------------------------------------------------------------------
328// PropValue — minimal property value type
329// ---------------------------------------------------------------------------
330
331/// A graph property value.
332#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
333#[non_exhaustive]
334pub enum PropValue {
335 /// Null / missing value.
336 Null,
337 /// Boolean.
338 Bool(bool),
339 /// 64-bit signed integer.
340 Int(i64),
341 /// 64-bit float.
342 Float(f64),
343 /// UTF-8 string.
344 Str(String),
345 /// Ordered list.
346 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// ---------------------------------------------------------------------------
372// NodeHandle / EdgeHandle
373// ---------------------------------------------------------------------------
374
375/// Opaque graph-instance identity used to reject handles from another graph.
376#[doc(hidden)]
377#[derive(Clone, Default)]
378pub struct GraphIdentity(Arc<()>);
379
380impl GraphIdentity {
381 /// Create a fresh graph-instance identity.
382 #[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/// Opaque UUID handle to a node created by one GraphForge instance.
395#[derive(Debug, Clone)]
396pub struct NodeHandle {
397 /// Stable public node identity.
398 pub uuid: ::uuid::Uuid,
399 /// Primary label.
400 pub label: String,
401 owner: GraphIdentity,
402}
403
404impl NodeHandle {
405 /// Construct a handle owned by `owner`.
406 #[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 /// Whether this handle belongs to the supplied graph instance.
417 #[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/// Typed public node identity accepted by path algorithms.
439#[derive(Debug, Clone, PartialEq)]
440pub enum NodeSelector {
441 /// Stable UUID identity.
442 Uuid(::uuid::Uuid),
443 /// A UUID handle owned by one graph instance.
444 Handle(NodeHandle),
445 /// A unique node selected by one typed property match within a label.
446 Match {
447 /// Required node label.
448 label: String,
449 /// Required property name.
450 property: String,
451 /// Exact property value.
452 value: PropValue,
453 },
454}
455
456impl NodeSelector {
457 /// Parse a canonical UUID selector with a structured validation error.
458 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/// Opaque handle to an edge created via `GraphForge::add_edge`.
466#[derive(Debug, Clone)]
467pub struct EdgeHandle {
468 /// Stable public edge identity.
469 pub uuid: ::uuid::Uuid,
470 /// Relationship type.
471 pub rel_type: String,
472}
473
474impl EdgeHandle {
475 /// Construct a UUID-backed edge handle.
476 #[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// ---------------------------------------------------------------------------
501// Option structs for analyst verbs and find
502// ---------------------------------------------------------------------------
503
504/// Options for `GraphForge::rank`.
505#[derive(Debug, Clone)]
506pub struct RankOptions {
507 /// Algorithm name (e.g. `"pagerank"`).
508 pub by: algorithms::RankAlgorithm,
509 /// Optional relationship type filter.
510 pub via: Option<String>,
511 /// Whether to treat edges as directed.
512 pub directed: bool,
513 /// Optional property name to write scores back to nodes.
514 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/// Options for `GraphForge::cluster`.
529#[derive(Debug, Clone, Default)]
530pub struct ClusterOptions {
531 /// Algorithm name (e.g. `"louvain"`).
532 pub by: algorithms::ClusterAlgorithm,
533 /// Node property containing the feature vector for vector clustering.
534 pub vector_property: Option<String>,
535 /// Optional relationship type filter.
536 pub via: Option<String>,
537 /// Whether to treat edges as directed.
538 pub directed: bool,
539 /// Optional property name to write community IDs back to nodes.
540 pub write_property: Option<String>,
541}
542
543/// Options for `GraphForge::find`.
544#[derive(Debug, Clone)]
545pub struct FindOptions {
546 /// Optional text query.
547 pub query: Option<String>,
548 /// Node label filter.
549 pub label: Option<String>,
550 /// Optional dense vector for similarity search.
551 pub vector: Option<Vec<f32>>,
552 /// Optional existing graph node whose vector is read from the selected space.
553 pub similar_to: Option<NodeSelector>,
554 /// Optional text embedded with the selected space's compatible provider contract.
555 pub semantic_query: Option<String>,
556 /// Maximum number of results to return.
557 pub limit: usize,
558 /// Vector space identifier (e.g. `"sbert"`).
559 pub space: Option<String>,
560 /// Explicitly allow the last complete substantially stale generation.
561 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/// Options for `GraphForge::paths`.
580#[derive(Debug, Clone)]
581pub struct PathsOptions {
582 /// Algorithm name (e.g. `"dijkstra"`, `"bfs"`, `"max_flow"`).
583 pub by: algorithms::PathAlgorithm,
584 /// Optional relationship type filter.
585 pub via: Option<String>,
586 /// Whether to treat edges as directed.
587 pub directed: bool,
588 /// Number of paths to return (e.g. Yen's k-shortest).
589 pub k: usize,
590 /// Optional edge-weight property name.
591 pub weight: Option<String>,
592 /// Optional graph-native capacity property for flow algorithms.
593 pub capacity_property: Option<String>,
594 /// Required graph-native unit-cost property for min-cost flow algorithms.
595 pub cost_property: Option<String>,
596 /// Optional node property containing an A* heuristic estimate.
597 pub heuristic: Option<String>,
598 /// Maximum number of edge transitions for random-walk paths.
599 pub walk_length: Option<usize>,
600 /// Seed for reproducible random-walk paths.
601 pub seed: Option<u64>,
602 /// Canonical resolved terminal UUIDs for explicit multi-terminal algorithms.
603 pub terminal_uuids: Vec<[u8; 16]>,
604 /// Graph-native node property containing prizes for prize-collecting Steiner trees.
605 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/// Options for `GraphForge::analyze`.
628#[derive(Debug, Clone)]
629pub struct AnalyzeOptions {
630 /// Algorithm name (e.g. `"minimum_spanning_tree"`, `"is_dag"`).
631 pub by: algorithms::AnalyzeAlgorithm,
632 /// Optional relationship type filter.
633 pub via: Option<String>,
634 /// Whether to treat edges as directed.
635 pub directed: bool,
636 /// Optional edge-weight property name.
637 pub weight: Option<String>,
638 /// Requested result count for analyses that enumerate multiple results.
639 pub k: Option<usize>,
640 /// Optional node property that identifies a graph partition.
641 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/// Options for `GraphForge::similar`.
658#[derive(Debug, Clone)]
659pub struct SimilarOptions {
660 /// Algorithm name (e.g. `"node_similarity"`, `"knn"`, `"cosine"`).
661 pub by: algorithms::SimilarAlgorithm,
662 /// Number of neighbours to return.
663 pub k: usize,
664 /// Optional vector property for vector-based similarity.
665 pub vector_property: Option<String>,
666 /// Optional relationship type filter.
667 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// ---------------------------------------------------------------------------
682// ExplainStage
683// ---------------------------------------------------------------------------
684
685/// Which compiler stage to inspect with `graphforge_cypher::explain_stage`.
686///
687/// Use `graphforge_cypher::explain_stage(cypher, stage)` directly.
688///
689/// `GraphForge::explain` is a stub that returns [`GfError::NotImplemented`] —
690/// the implementation lives in `graphforge_cypher` to avoid circular crate dependencies.
691#[derive(Debug, Clone, Copy, PartialEq, Eq)]
692pub enum ExplainStage {
693 /// Pretty-printed JSON of the parsed [`graphforge_ast::AstQuery`].
694 Ast,
695 /// Bound AST after name resolution.
696 ///
697 /// **Deferred** — the binder produces a `GraphPlan` directly and does not
698 /// annotate the `AstQuery`. This variant returns [`GfError::NotImplemented`]
699 /// until a future milestone adds a separate annotation pass.
700 BoundAst,
701 /// Serialised [`graphforge_ir::GraphPlan`] produced by the binder.
702 ///
703 /// Runs in [`graphforge_ir::OntologyMode::Exploratory`]; all unknown labels and
704 /// relation types are auto-interned by the [`graphforge_ir::RuntimeCatalog`].
705 GraphIr,
706 /// DataFusion logical plan (not yet implemented).
707 LogicalPlan,
708 /// DataFusion physical plan (not yet implemented).
709 PhysicalPlan,
710}
711
712// ---------------------------------------------------------------------------
713// GraphForge — public facade
714// ---------------------------------------------------------------------------
715//
716// The `GraphForge` engine facade and its interim `RecordBatch` result type live
717// in the top-level `graphforge-api` crate, not here: `graphforge-core` is the foundation crate
718// that every other crate depends on, so it cannot depend on the pipeline crates
719// (`graphforge-cypher`/`graphforge-rel`/`graphforge-exec`) the facade needs without a dependency cycle.
720// See #716 / #583. `graphforge-core` keeps the shared value types below
721// ([`GfError`], [`OntologyMode`], [`NodeHandle`], [`RankOptions`], …) that the
722// facade composes.
723
724#[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}