Skip to main content

plexus_engine/
lib.rs

1use std::cmp::Ordering;
2use std::collections::{BTreeMap, HashMap, HashSet};
3
4pub use capabilities::{
5    check_version_compat, op_ordering_contract, required_capabilities,
6    validate_plan_against_capabilities, CapabilityError, EngineCapabilities,
7    EngineCapabilityDocument, ExprKind, OpKind, OpOrderingContract, OpOrderingDocument, PlanSemver,
8    RequiredCapabilities, VersionRange, ALL_EXPR_KINDS, ALL_OP_KINDS,
9};
10pub use embedded_profile::{
11    assess_embedded_graph_rag_capabilities, assess_embedded_graph_rag_plan,
12    validate_embedded_graph_rag_plan, EmbeddedGraphRagAssessment, EmbeddedGraphRagCapabilityGap,
13    EmbeddedGraphRagProfileError, EMBEDDED_GRAPH_RAG_PROFILE, EMBEDDED_PHASE_1_RELEASE_CANDIDATE,
14    PLEXUS_CAPABILITY_REJECTION_SCENARIO,
15};
16pub use independent_consumer::{proof_fixture_graph, IndependentConsumerEngine};
17use plexus_serde::{
18    deserialize_plan, validate_plan_structure, AggFn, CmpOp, ExpandDir, Expr, Op, Plan, SortDir,
19};
20pub use reference_topology::{
21    build_compiled_plan_cache_descriptor, flagship_graph_rag_reference_contract,
22    CompiledPlanCacheContract, CompiledPlanCacheDescriptor, CompiledPlanCacheRequest,
23    FlagshipGraphRagReferenceContract, COMPILED_PLAN_CACHE_NAMESPACE, COMPILED_PLAN_CACHE_SCHEMA,
24    FLAGSHIP_COMPATIBILITY_PROFILE, FLAGSHIP_GRAPH_RAG_REFERENCE_TOPOLOGY,
25    FLAGSHIP_PHASE_3_RELEASE_CANDIDATE, RHODIUM_COMPILED_PLAN_CACHE_ROLE,
26};
27
28#[derive(Debug, Clone, PartialEq)]
29pub enum Value {
30    Null,
31    Bool(bool),
32    Int(i64),
33    Float(f64),
34    String(String),
35    NodeRef(u64),
36    RelRef(u64),
37    List(Vec<Value>),
38    Map(BTreeMap<String, Value>),
39}
40
41pub type Row = Vec<Value>;
42
43type RowSet = Vec<Row>;
44
45#[derive(Debug, Clone, PartialEq)]
46pub struct QueryResult {
47    pub rows: Vec<Row>,
48    /// Engine-emitted cursor for the next page. Present only when the plan
49    /// contained a `Limit` with `emit_cursor = true` and rows were truncated.
50    /// The reference engine always sets this to `None`; production engines
51    /// (e.g. Iridium) encode their internal resume state here.
52    pub continuation: Option<Vec<u8>>,
53}
54
55impl QueryResult {
56    pub fn empty() -> Self {
57        Self {
58            rows: Vec::new(),
59            continuation: None,
60        }
61    }
62}
63
64#[derive(Debug, Clone, PartialEq)]
65pub struct Node {
66    pub id: u64,
67    pub labels: HashSet<String>,
68    pub props: HashMap<String, Value>,
69}
70
71#[derive(Debug, Clone, PartialEq)]
72pub struct Relationship {
73    pub id: u64,
74    pub src: u64,
75    pub dst: u64,
76    pub typ: String,
77    pub props: HashMap<String, Value>,
78}
79
80/// Simple in-memory graph representation for the reference engine.
81///
82/// **Not representative of production performance.** All lookups are O(n) linear
83/// scans over `Vec`. Production engines should use indexed storage (e.g.,
84/// `HashMap<u64, Node>`) or implement [`StorageAdapter`] to map Plexus ops onto
85/// their native storage layer. See `docs/storage-engine-mapping.md`.
86#[derive(Debug, Clone, PartialEq, Default)]
87pub struct Graph {
88    pub nodes: Vec<Node>,
89    pub rels: Vec<Relationship>,
90}
91
92impl Graph {
93    /// O(n) linear scan — production engines should use indexed lookups.
94    pub fn node_by_id(&self, id: u64) -> Option<&Node> {
95        self.nodes.iter().find(|n| n.id == id)
96    }
97
98    /// O(n) linear scan — production engines should use indexed lookups.
99    pub fn node_by_id_mut(&mut self, id: u64) -> Option<&mut Node> {
100        self.nodes.iter_mut().find(|n| n.id == id)
101    }
102
103    /// O(n) linear scan — production engines should use indexed lookups.
104    pub fn rel_by_id(&self, id: u64) -> Option<&Relationship> {
105        self.rels.iter().find(|r| r.id == id)
106    }
107
108    /// O(n) linear scan — production engines should use indexed lookups.
109    pub fn rel_by_id_mut(&mut self, id: u64) -> Option<&mut Relationship> {
110        self.rels.iter_mut().find(|r| r.id == id)
111    }
112}
113
114#[derive(Debug, thiserror::Error)]
115pub enum ExecutionError {
116    #[error("invalid op reference index {0}")]
117    InvalidOpRef(u32),
118    #[error("missing op output at index {0}")]
119    MissingOpOutput(u32),
120    #[error("invalid root op index {0}")]
121    InvalidRootOp(u32),
122    #[error("row column {idx} out of bounds (len={len})")]
123    ColumnOutOfBounds { idx: usize, len: usize },
124    #[error("expected a boolean value")]
125    ExpectedBool,
126    #[error("expected a numeric value")]
127    ExpectedNumeric,
128    #[error("expected aggregate expression")]
129    ExpectedAggregateExpr,
130    #[error("sort keys/dirs length mismatch ({keys} vs {dirs})")]
131    SortArityMismatch { keys: usize, dirs: usize },
132    #[error("unknown node id {0}")]
133    UnknownNode(u64),
134    #[error("unknown relationship id {0}")]
135    UnknownRel(u64),
136    #[error("expected node reference in column {idx}")]
137    ExpectedNodeRef { idx: usize },
138    #[error("expected relationship reference in column {idx}")]
139    ExpectedRelRef { idx: usize },
140    #[error("expected node or relationship reference in column {idx}")]
141    ExpectedEntityRef { idx: usize },
142    #[error("cannot delete node {0} without detach while relationships exist")]
143    DeleteRequiresDetach(u64),
144    #[error("expected map or null for properties payload")]
145    ExpectedMapPayload,
146    #[error("unsupported op in reference engine: {0}")]
147    UnsupportedOp(&'static str),
148    #[error("unsupported expression in reference engine: {0}")]
149    UnsupportedExpr(&'static str),
150    #[error("unbound parameter: {0}")]
151    UnboundParam(String),
152    #[error("plan requires multi-graph support but engine declares supports_multi_graph=false")]
153    MultiGraphUnsupported,
154}
155
156#[derive(Debug, thiserror::Error)]
157pub enum EngineError<E: std::error::Error + 'static> {
158    #[error("invalid serialized Plexus plan: {0}")]
159    Deserialize(#[from] plexus_serde::SerdeError),
160    #[error("invalid plan structure: {0}")]
161    InvalidStructure(plexus_serde::PlanValidationError),
162    #[error("engine execution failed: {0}")]
163    Engine(E),
164}
165
166/// Engine-facing API for consuming validated/deserialized Plexus plans.
167pub trait PlanEngine {
168    type Error: std::error::Error + Send + Sync + 'static;
169
170    /// Execute a deserialized plan.
171    fn execute_plan(&mut self, plan: &Plan) -> Result<QueryResult, Self::Error>;
172}
173
174/// Optional mutation capability for engines that execute DML ops.
175pub trait MutationEngine {
176    type Error: std::error::Error + Send + Sync + 'static;
177
178    fn create_node(
179        &mut self,
180        labels: &[String],
181        props: HashMap<String, Value>,
182    ) -> Result<u64, Self::Error>;
183    fn create_rel(
184        &mut self,
185        src: u64,
186        dst: u64,
187        rel_type: &str,
188        props: HashMap<String, Value>,
189    ) -> Result<u64, Self::Error>;
190    fn merge_pattern(
191        &mut self,
192        pattern: &Expr,
193        on_create_props: &Expr,
194        on_match_props: &Expr,
195        schema: &[plexus_serde::ColDef],
196        row: &Row,
197    ) -> Result<(), Self::Error>;
198    fn delete_entity(&mut self, target: &Value, detach: bool) -> Result<(), Self::Error>;
199    fn set_property(&mut self, target: &Value, key: &str, value: Value) -> Result<(), Self::Error>;
200    fn remove_property(&mut self, target: &Value, key: &str) -> Result<(), Self::Error>;
201}
202
203/// Deserialize, validate, and execute a serialized Plexus plan with the provided engine.
204///
205/// Validates plan structure (no cycles, valid references, valid root_op) before
206/// execution. Returns the first structural error encountered, if any.
207pub fn execute_serialized<E: PlanEngine>(
208    engine: &mut E,
209    bytes: &[u8],
210) -> Result<QueryResult, EngineError<E::Error>> {
211    let plan = deserialize_plan(bytes)?;
212    if let Err(errors) = validate_plan_structure(&plan) {
213        return Err(EngineError::InvalidStructure(
214            errors.into_iter().next().unwrap(),
215        ));
216    }
217    engine.execute_plan(&plan).map_err(EngineError::Engine)
218}
219
220/// Reference in-memory engine for integration testing and conformance validation.
221///
222/// This engine prioritizes correctness and readability over performance. It uses
223/// O(n) linear scans for entity lookups and clones row sets between pipeline
224/// stages. **Do not use as a template for production engine performance patterns.**
225///
226/// Production engines should implement [`PlanEngine`] (and optionally
227/// [`MutationEngine`]) against their own storage, or use the [`StorageAdapter`]
228/// trait for property-graph mapping. See `docs/engine-implementor-guide.md`.
229#[derive(Debug, Clone, Default)]
230pub struct InMemoryEngine {
231    pub graph: Graph,
232    pub params: HashMap<String, Value>,
233}
234
235impl InMemoryEngine {
236    pub fn new(graph: Graph) -> Self {
237        Self {
238            graph,
239            params: HashMap::new(),
240        }
241    }
242
243    pub fn with_params(graph: Graph, params: HashMap<String, Value>) -> Self {
244        Self { graph, params }
245    }
246
247    pub fn set_param(&mut self, name: impl Into<String>, value: Value) {
248        self.params.insert(name.into(), value);
249    }
250
251    pub fn clear_params(&mut self) {
252        self.params.clear();
253    }
254}
255
256#[derive(Debug, Clone, PartialEq)]
257pub struct VectorCollectionEntry {
258    pub node_id: u64,
259    pub embedding: Vec<f64>,
260}
261
262#[derive(Debug, Clone, Default)]
263pub struct MockVectorEngine {
264    pub base: InMemoryEngine,
265    pub collections: HashMap<String, Vec<VectorCollectionEntry>>,
266}
267
268impl MockVectorEngine {
269    pub fn new(graph: Graph) -> Self {
270        Self {
271            base: InMemoryEngine::new(graph),
272            collections: HashMap::new(),
273        }
274    }
275
276    pub fn with_collections(
277        graph: Graph,
278        collections: HashMap<String, Vec<VectorCollectionEntry>>,
279    ) -> Self {
280        Self {
281            base: InMemoryEngine::new(graph),
282            collections,
283        }
284    }
285
286    pub fn insert_collection(
287        &mut self,
288        name: impl Into<String>,
289        entries: Vec<VectorCollectionEntry>,
290    ) {
291        self.collections.insert(name.into(), entries);
292    }
293
294    pub fn set_param(&mut self, name: impl Into<String>, value: Value) {
295        self.base.set_param(name, value);
296    }
297
298    pub fn clear_params(&mut self) {
299        self.base.clear_params();
300    }
301}
302
303mod capabilities;
304mod embedded_profile;
305mod independent_consumer;
306mod reference_topology;
307mod storage;
308pub use storage::{EntityRef, StorageAdapter};
309mod engine;
310
311#[cfg(test)]
312mod tests;