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