Skip to main content

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}