Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
413
vendor/ruvector/crates/rvlite/src/cypher/ast.rs
vendored
Normal file
413
vendor/ruvector/crates/rvlite/src/cypher/ast.rs
vendored
Normal file
@@ -0,0 +1,413 @@
|
||||
//! Abstract Syntax Tree definitions for Cypher query language
|
||||
//!
|
||||
//! Represents the parsed structure of Cypher queries including:
|
||||
//! - Pattern matching (MATCH, OPTIONAL MATCH)
|
||||
//! - Filtering (WHERE)
|
||||
//! - Projections (RETURN, WITH)
|
||||
//! - Mutations (CREATE, MERGE, DELETE, SET)
|
||||
//! - Aggregations and ordering
|
||||
//! - Hyperedge support for N-ary relationships
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Top-level query representation
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Query {
|
||||
pub statements: Vec<Statement>,
|
||||
}
|
||||
|
||||
/// Individual query statement
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Statement {
|
||||
Match(MatchClause),
|
||||
Create(CreateClause),
|
||||
Merge(MergeClause),
|
||||
Delete(DeleteClause),
|
||||
Set(SetClause),
|
||||
Remove(RemoveClause),
|
||||
Return(ReturnClause),
|
||||
With(WithClause),
|
||||
}
|
||||
|
||||
/// MATCH clause for pattern matching
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MatchClause {
|
||||
pub optional: bool,
|
||||
pub patterns: Vec<Pattern>,
|
||||
pub where_clause: Option<WhereClause>,
|
||||
}
|
||||
|
||||
/// Pattern matching expressions
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Pattern {
|
||||
/// Simple node pattern: (n:Label {props})
|
||||
Node(NodePattern),
|
||||
/// Relationship pattern: (a)-[r:TYPE]->(b)
|
||||
Relationship(RelationshipPattern),
|
||||
/// Path pattern: p = (a)-[*1..5]->(b)
|
||||
Path(PathPattern),
|
||||
/// Hyperedge pattern for N-ary relationships: (a)-[r:TYPE]->(b,c,d)
|
||||
Hyperedge(HyperedgePattern),
|
||||
}
|
||||
|
||||
/// Node pattern: (variable:Label {property: value})
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NodePattern {
|
||||
pub variable: Option<String>,
|
||||
pub labels: Vec<String>,
|
||||
pub properties: Option<PropertyMap>,
|
||||
}
|
||||
|
||||
/// Relationship pattern: [variable:Type {properties}]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RelationshipPattern {
|
||||
pub variable: Option<String>,
|
||||
pub rel_type: Option<String>,
|
||||
pub properties: Option<PropertyMap>,
|
||||
pub direction: Direction,
|
||||
pub range: Option<RelationshipRange>,
|
||||
/// Source node pattern
|
||||
pub from: Box<NodePattern>,
|
||||
/// Target - can be a NodePattern or another Pattern for chained relationships
|
||||
/// For simple relationships like (a)-[r]->(b), this is just the node
|
||||
/// For chained patterns like (a)-[r]->(b)<-[s]-(c), the target is nested
|
||||
pub to: Box<Pattern>,
|
||||
}
|
||||
|
||||
/// Hyperedge pattern for N-ary relationships
|
||||
/// Example: (person)-[r:TRANSACTION]->(account1, account2, merchant)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct HyperedgePattern {
|
||||
pub variable: Option<String>,
|
||||
pub rel_type: String,
|
||||
pub properties: Option<PropertyMap>,
|
||||
pub from: Box<NodePattern>,
|
||||
pub to: Vec<NodePattern>, // Multiple target nodes for N-ary relationships
|
||||
pub arity: usize, // Number of participating nodes (including source)
|
||||
}
|
||||
|
||||
/// Relationship direction
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Direction {
|
||||
Outgoing, // ->
|
||||
Incoming, // <-
|
||||
Undirected, // -
|
||||
}
|
||||
|
||||
/// Relationship range for path queries: [*min..max]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RelationshipRange {
|
||||
pub min: Option<usize>,
|
||||
pub max: Option<usize>,
|
||||
}
|
||||
|
||||
/// Path pattern: p = (a)-[*]->(b)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PathPattern {
|
||||
pub variable: String,
|
||||
pub pattern: Box<Pattern>,
|
||||
}
|
||||
|
||||
/// Property map: {key: value, ...}
|
||||
pub type PropertyMap = HashMap<String, Expression>;
|
||||
|
||||
/// WHERE clause for filtering
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WhereClause {
|
||||
pub condition: Expression,
|
||||
}
|
||||
|
||||
/// CREATE clause for creating nodes and relationships
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CreateClause {
|
||||
pub patterns: Vec<Pattern>,
|
||||
}
|
||||
|
||||
/// MERGE clause for create-or-match
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MergeClause {
|
||||
pub pattern: Pattern,
|
||||
pub on_create: Option<SetClause>,
|
||||
pub on_match: Option<SetClause>,
|
||||
}
|
||||
|
||||
/// DELETE clause
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DeleteClause {
|
||||
pub detach: bool,
|
||||
pub expressions: Vec<Expression>,
|
||||
}
|
||||
|
||||
/// SET clause for updating properties
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SetClause {
|
||||
pub items: Vec<SetItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SetItem {
|
||||
Property {
|
||||
variable: String,
|
||||
property: String,
|
||||
value: Expression,
|
||||
},
|
||||
Variable {
|
||||
variable: String,
|
||||
value: Expression,
|
||||
},
|
||||
Labels {
|
||||
variable: String,
|
||||
labels: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// REMOVE clause for removing properties or labels
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RemoveClause {
|
||||
pub items: Vec<RemoveItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum RemoveItem {
|
||||
/// Remove a property: REMOVE n.property
|
||||
Property { variable: String, property: String },
|
||||
/// Remove labels: REMOVE n:Label1:Label2
|
||||
Labels {
|
||||
variable: String,
|
||||
labels: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// RETURN clause for projection
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ReturnClause {
|
||||
pub distinct: bool,
|
||||
pub items: Vec<ReturnItem>,
|
||||
pub order_by: Option<OrderBy>,
|
||||
pub skip: Option<Expression>,
|
||||
pub limit: Option<Expression>,
|
||||
}
|
||||
|
||||
/// WITH clause for chaining queries
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WithClause {
|
||||
pub distinct: bool,
|
||||
pub items: Vec<ReturnItem>,
|
||||
pub where_clause: Option<WhereClause>,
|
||||
pub order_by: Option<OrderBy>,
|
||||
pub skip: Option<Expression>,
|
||||
pub limit: Option<Expression>,
|
||||
}
|
||||
|
||||
/// Return item: expression AS alias
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ReturnItem {
|
||||
pub expression: Expression,
|
||||
pub alias: Option<String>,
|
||||
}
|
||||
|
||||
/// ORDER BY clause
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OrderBy {
|
||||
pub items: Vec<OrderByItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OrderByItem {
|
||||
pub expression: Expression,
|
||||
pub ascending: bool,
|
||||
}
|
||||
|
||||
/// Expression tree
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Expression {
|
||||
// Literals
|
||||
Integer(i64),
|
||||
Float(f64),
|
||||
String(String),
|
||||
Boolean(bool),
|
||||
Null,
|
||||
|
||||
// Variables and properties
|
||||
Variable(String),
|
||||
Property {
|
||||
object: Box<Expression>,
|
||||
property: String,
|
||||
},
|
||||
|
||||
// Collections
|
||||
List(Vec<Expression>),
|
||||
Map(HashMap<String, Expression>),
|
||||
|
||||
// Operators
|
||||
BinaryOp {
|
||||
left: Box<Expression>,
|
||||
op: BinaryOperator,
|
||||
right: Box<Expression>,
|
||||
},
|
||||
UnaryOp {
|
||||
op: UnaryOperator,
|
||||
operand: Box<Expression>,
|
||||
},
|
||||
|
||||
// Functions and aggregations
|
||||
FunctionCall {
|
||||
name: String,
|
||||
args: Vec<Expression>,
|
||||
},
|
||||
Aggregation {
|
||||
function: AggregationFunction,
|
||||
expression: Box<Expression>,
|
||||
distinct: bool,
|
||||
},
|
||||
|
||||
// Pattern predicates
|
||||
PatternPredicate(Box<Pattern>),
|
||||
|
||||
// Case expressions
|
||||
Case {
|
||||
expression: Option<Box<Expression>>,
|
||||
alternatives: Vec<(Expression, Expression)>,
|
||||
default: Option<Box<Expression>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Binary operators
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BinaryOperator {
|
||||
// Arithmetic
|
||||
Add,
|
||||
Subtract,
|
||||
Multiply,
|
||||
Divide,
|
||||
Modulo,
|
||||
Power,
|
||||
|
||||
// Comparison
|
||||
Equal,
|
||||
NotEqual,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
GreaterThan,
|
||||
GreaterThanOrEqual,
|
||||
|
||||
// Logical
|
||||
And,
|
||||
Or,
|
||||
Xor,
|
||||
|
||||
// String
|
||||
Contains,
|
||||
StartsWith,
|
||||
EndsWith,
|
||||
Matches, // Regex
|
||||
|
||||
// Collection
|
||||
In,
|
||||
|
||||
// Null checking
|
||||
Is,
|
||||
IsNot,
|
||||
}
|
||||
|
||||
/// Unary operators
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum UnaryOperator {
|
||||
Not,
|
||||
Minus,
|
||||
Plus,
|
||||
IsNull,
|
||||
IsNotNull,
|
||||
}
|
||||
|
||||
/// Aggregation functions
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AggregationFunction {
|
||||
Count,
|
||||
Sum,
|
||||
Avg,
|
||||
Min,
|
||||
Max,
|
||||
Collect,
|
||||
StdDev,
|
||||
StdDevP,
|
||||
Percentile,
|
||||
}
|
||||
|
||||
impl Query {
|
||||
pub fn new(statements: Vec<Statement>) -> Self {
|
||||
Self { statements }
|
||||
}
|
||||
|
||||
/// Check if query contains only read operations
|
||||
pub fn is_read_only(&self) -> bool {
|
||||
self.statements.iter().all(|stmt| {
|
||||
matches!(
|
||||
stmt,
|
||||
Statement::Match(_) | Statement::Return(_) | Statement::With(_)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if query contains hyperedges
|
||||
pub fn has_hyperedges(&self) -> bool {
|
||||
self.statements.iter().any(|stmt| match stmt {
|
||||
Statement::Match(m) => m
|
||||
.patterns
|
||||
.iter()
|
||||
.any(|p| matches!(p, Pattern::Hyperedge(_))),
|
||||
Statement::Create(c) => c
|
||||
.patterns
|
||||
.iter()
|
||||
.any(|p| matches!(p, Pattern::Hyperedge(_))),
|
||||
Statement::Merge(m) => matches!(&m.pattern, Pattern::Hyperedge(_)),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
/// Get the arity of the pattern (number of nodes involved)
|
||||
pub fn arity(&self) -> usize {
|
||||
match self {
|
||||
Pattern::Node(_) => 1,
|
||||
Pattern::Relationship(_) => 2,
|
||||
Pattern::Path(_) => 2, // Simplified, could be variable
|
||||
Pattern::Hyperedge(h) => h.arity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
/// Check if expression is constant (no variables)
|
||||
pub fn is_constant(&self) -> bool {
|
||||
match self {
|
||||
Expression::Integer(_)
|
||||
| Expression::Float(_)
|
||||
| Expression::String(_)
|
||||
| Expression::Boolean(_)
|
||||
| Expression::Null => true,
|
||||
Expression::List(items) => items.iter().all(|e| e.is_constant()),
|
||||
Expression::Map(map) => map.values().all(|e| e.is_constant()),
|
||||
Expression::BinaryOp { left, right, .. } => left.is_constant() && right.is_constant(),
|
||||
Expression::UnaryOp { operand, .. } => operand.is_constant(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if expression contains aggregation
|
||||
pub fn has_aggregation(&self) -> bool {
|
||||
match self {
|
||||
Expression::Aggregation { .. } => true,
|
||||
Expression::BinaryOp { left, right, .. } => {
|
||||
left.has_aggregation() || right.has_aggregation()
|
||||
}
|
||||
Expression::UnaryOp { operand, .. } => operand.has_aggregation(),
|
||||
Expression::FunctionCall { args, .. } => args.iter().any(|e| e.has_aggregation()),
|
||||
Expression::List(items) => items.iter().any(|e| e.has_aggregation()),
|
||||
Expression::Property { object, .. } => object.has_aggregation(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
616
vendor/ruvector/crates/rvlite/src/cypher/executor.rs
vendored
Normal file
616
vendor/ruvector/crates/rvlite/src/cypher/executor.rs
vendored
Normal file
@@ -0,0 +1,616 @@
|
||||
//! Cypher query executor for in-memory property graph
|
||||
|
||||
use super::ast::*;
|
||||
use super::graph_store::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ExecutionError {
|
||||
#[error("Graph error: {0}")]
|
||||
GraphError(#[from] GraphError),
|
||||
#[error("Variable not found: {0}")]
|
||||
VariableNotFound(String),
|
||||
#[error("Type error: {0}")]
|
||||
TypeError(String),
|
||||
#[error("Unsupported operation: {0}")]
|
||||
UnsupportedOperation(String),
|
||||
#[error("Execution error: {0}")]
|
||||
ExecutionError(String),
|
||||
}
|
||||
|
||||
/// Execution context holding variable bindings
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecutionContext {
|
||||
pub variables: HashMap<String, ContextValue>,
|
||||
}
|
||||
|
||||
impl ExecutionContext {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
variables: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind(&mut self, name: String, value: ContextValue) {
|
||||
self.variables.insert(name, value);
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&ContextValue> {
|
||||
self.variables.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ExecutionContext {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Value in execution context (node, edge, or property value)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ContextValue {
|
||||
Node(Node),
|
||||
Edge(Edge),
|
||||
Value(Value),
|
||||
List(Vec<ContextValue>),
|
||||
Map(HashMap<String, ContextValue>),
|
||||
}
|
||||
|
||||
impl ContextValue {
|
||||
pub fn as_node(&self) -> Option<&Node> {
|
||||
match self {
|
||||
ContextValue::Node(n) => Some(n),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_value(&self) -> Option<&Value> {
|
||||
match self {
|
||||
ContextValue::Value(v) => Some(v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Query execution result
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExecutionResult {
|
||||
pub columns: Vec<String>,
|
||||
pub rows: Vec<HashMap<String, ContextValue>>,
|
||||
}
|
||||
|
||||
impl ExecutionResult {
|
||||
pub fn new(columns: Vec<String>) -> Self {
|
||||
Self {
|
||||
columns,
|
||||
rows: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_row(&mut self, row: HashMap<String, ContextValue>) {
|
||||
self.rows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cypher query executor
|
||||
pub struct Executor<'a> {
|
||||
graph: &'a mut PropertyGraph,
|
||||
}
|
||||
|
||||
impl<'a> Executor<'a> {
|
||||
pub fn new(graph: &'a mut PropertyGraph) -> Self {
|
||||
Self { graph }
|
||||
}
|
||||
|
||||
/// Execute a parsed Cypher query
|
||||
pub fn execute(&mut self, query: &Query) -> Result<ExecutionResult, ExecutionError> {
|
||||
let mut context = ExecutionContext::new();
|
||||
let mut result = None;
|
||||
|
||||
for statement in &query.statements {
|
||||
result = Some(self.execute_statement(statement, &mut context)?);
|
||||
}
|
||||
|
||||
result.ok_or_else(|| ExecutionError::ExecutionError("No statements to execute".to_string()))
|
||||
}
|
||||
|
||||
fn execute_statement(
|
||||
&mut self,
|
||||
statement: &Statement,
|
||||
context: &mut ExecutionContext,
|
||||
) -> Result<ExecutionResult, ExecutionError> {
|
||||
match statement {
|
||||
Statement::Create(clause) => self.execute_create(clause, context),
|
||||
Statement::Match(clause) => self.execute_match(clause, context),
|
||||
Statement::Return(clause) => self.execute_return(clause, context),
|
||||
Statement::Set(clause) => self.execute_set(clause, context),
|
||||
Statement::Delete(clause) => self.execute_delete(clause, context),
|
||||
_ => Err(ExecutionError::UnsupportedOperation(format!(
|
||||
"Statement {:?} not yet implemented",
|
||||
statement
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_create(
|
||||
&mut self,
|
||||
clause: &CreateClause,
|
||||
context: &mut ExecutionContext,
|
||||
) -> Result<ExecutionResult, ExecutionError> {
|
||||
for pattern in &clause.patterns {
|
||||
self.create_pattern(pattern, context)?;
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::new(vec![]))
|
||||
}
|
||||
|
||||
fn create_pattern(
|
||||
&mut self,
|
||||
pattern: &Pattern,
|
||||
context: &mut ExecutionContext,
|
||||
) -> Result<(), ExecutionError> {
|
||||
match pattern {
|
||||
Pattern::Node(node_pattern) => {
|
||||
let node = self.create_node(node_pattern)?;
|
||||
if let Some(var) = &node_pattern.variable {
|
||||
context.bind(var.clone(), ContextValue::Node(node));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Pattern::Relationship(rel_pattern) => {
|
||||
self.create_relationship(rel_pattern, context)?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ExecutionError::UnsupportedOperation(
|
||||
"Only simple node and relationship patterns supported in CREATE".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_node(&mut self, pattern: &NodePattern) -> Result<Node, ExecutionError> {
|
||||
let id = self.graph.generate_node_id();
|
||||
let mut node = Node::new(id).with_labels(pattern.labels.clone());
|
||||
|
||||
// Set properties
|
||||
if let Some(props) = &pattern.properties {
|
||||
for (key, expr) in props {
|
||||
let value = self.evaluate_expression(expr, &ExecutionContext::new())?;
|
||||
node.set_property(key.clone(), value);
|
||||
}
|
||||
}
|
||||
|
||||
let node_id = self.graph.add_node(node.clone());
|
||||
node.id = node_id;
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
fn create_relationship(
|
||||
&mut self,
|
||||
pattern: &RelationshipPattern,
|
||||
context: &mut ExecutionContext,
|
||||
) -> Result<(), ExecutionError> {
|
||||
// Get or create source node
|
||||
let from_node = if let Some(var) = &pattern.from.variable {
|
||||
if let Some(ContextValue::Node(n)) = context.get(var) {
|
||||
n.clone()
|
||||
} else {
|
||||
self.create_node(&pattern.from)?
|
||||
}
|
||||
} else {
|
||||
self.create_node(&pattern.from)?
|
||||
};
|
||||
|
||||
// Get or create target node (only handle simple node targets for now)
|
||||
let to_node = match &*pattern.to {
|
||||
Pattern::Node(node_pattern) => {
|
||||
if let Some(var) = &node_pattern.variable {
|
||||
if let Some(ContextValue::Node(n)) = context.get(var) {
|
||||
n.clone()
|
||||
} else {
|
||||
self.create_node(node_pattern)?
|
||||
}
|
||||
} else {
|
||||
self.create_node(node_pattern)?
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(ExecutionError::UnsupportedOperation(
|
||||
"Complex relationship targets not yet supported".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Create the edge
|
||||
let edge_type = pattern
|
||||
.rel_type
|
||||
.clone()
|
||||
.unwrap_or_else(|| "RELATED_TO".to_string());
|
||||
let edge_id = self.graph.generate_edge_id();
|
||||
let mut edge = Edge::new(edge_id, from_node.id.clone(), to_node.id.clone(), edge_type);
|
||||
|
||||
// Set properties
|
||||
if let Some(props) = &pattern.properties {
|
||||
for (key, expr) in props {
|
||||
let value = self.evaluate_expression(expr, context)?;
|
||||
edge.set_property(key.clone(), value);
|
||||
}
|
||||
}
|
||||
|
||||
let edge_id = self.graph.add_edge(edge.clone())?;
|
||||
if let Some(var) = &pattern.variable {
|
||||
edge.id = edge_id;
|
||||
context.bind(var.clone(), ContextValue::Edge(edge));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn execute_match(
|
||||
&mut self,
|
||||
clause: &MatchClause,
|
||||
context: &mut ExecutionContext,
|
||||
) -> Result<ExecutionResult, ExecutionError> {
|
||||
let mut matches = Vec::new();
|
||||
|
||||
for pattern in &clause.patterns {
|
||||
let pattern_matches = self.match_pattern(pattern)?;
|
||||
matches.extend(pattern_matches);
|
||||
}
|
||||
|
||||
// Apply WHERE filter if present
|
||||
if let Some(where_clause) = &clause.where_clause {
|
||||
matches.retain(|ctx| {
|
||||
self.evaluate_condition(&where_clause.condition, ctx)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
}
|
||||
|
||||
// Merge matches into context
|
||||
for match_ctx in matches {
|
||||
for (var, val) in match_ctx.variables {
|
||||
context.bind(var, val);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::new(vec![]))
|
||||
}
|
||||
|
||||
fn match_pattern(&self, pattern: &Pattern) -> Result<Vec<ExecutionContext>, ExecutionError> {
|
||||
match pattern {
|
||||
Pattern::Node(node_pattern) => self.match_node_pattern(node_pattern),
|
||||
Pattern::Relationship(rel_pattern) => self.match_relationship_pattern(rel_pattern),
|
||||
_ => Err(ExecutionError::UnsupportedOperation(
|
||||
"Pattern type not yet supported in MATCH".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn match_node_pattern(
|
||||
&self,
|
||||
pattern: &NodePattern,
|
||||
) -> Result<Vec<ExecutionContext>, ExecutionError> {
|
||||
let mut contexts = Vec::new();
|
||||
|
||||
// Find nodes matching labels
|
||||
let candidates: Vec<&Node> = if pattern.labels.is_empty() {
|
||||
self.graph.find_nodes(|_| true)
|
||||
} else {
|
||||
let mut nodes = Vec::new();
|
||||
for label in &pattern.labels {
|
||||
nodes.extend(self.graph.find_nodes_by_label(label));
|
||||
}
|
||||
nodes
|
||||
};
|
||||
|
||||
// Filter by properties
|
||||
for node in candidates {
|
||||
if let Some(props) = &pattern.properties {
|
||||
let mut matches = true;
|
||||
for (key, expr) in props {
|
||||
let expected_value =
|
||||
self.evaluate_expression(expr, &ExecutionContext::new())?;
|
||||
if node.get_property(key) != Some(&expected_value) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !matches {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let mut ctx = ExecutionContext::new();
|
||||
if let Some(var) = &pattern.variable {
|
||||
ctx.bind(var.clone(), ContextValue::Node(node.clone()));
|
||||
}
|
||||
contexts.push(ctx);
|
||||
}
|
||||
|
||||
Ok(contexts)
|
||||
}
|
||||
|
||||
fn match_relationship_pattern(
|
||||
&self,
|
||||
pattern: &RelationshipPattern,
|
||||
) -> Result<Vec<ExecutionContext>, ExecutionError> {
|
||||
let mut contexts = Vec::new();
|
||||
|
||||
// Match source nodes
|
||||
let from_contexts = self.match_node_pattern(&pattern.from)?;
|
||||
|
||||
for from_ctx in from_contexts {
|
||||
// Get the source node
|
||||
let from_node = if let Some(var) = &pattern.from.variable {
|
||||
from_ctx
|
||||
.get(var)
|
||||
.and_then(|v| v.as_node())
|
||||
.ok_or_else(|| ExecutionError::VariableNotFound(var.clone()))?
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Find matching edges
|
||||
let edges = match pattern.direction {
|
||||
Direction::Outgoing => self.graph.get_outgoing_edges(&from_node.id),
|
||||
Direction::Incoming => self.graph.get_incoming_edges(&from_node.id),
|
||||
Direction::Undirected => {
|
||||
let mut all = self.graph.get_outgoing_edges(&from_node.id);
|
||||
all.extend(self.graph.get_incoming_edges(&from_node.id));
|
||||
all
|
||||
}
|
||||
};
|
||||
|
||||
for edge in edges {
|
||||
// Filter by type
|
||||
if let Some(rel_type) = &pattern.rel_type {
|
||||
if &edge.edge_type != rel_type {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by properties
|
||||
if let Some(props) = &pattern.properties {
|
||||
let mut matches = true;
|
||||
for (key, expr) in props {
|
||||
let expected_value =
|
||||
self.evaluate_expression(expr, &ExecutionContext::new())?;
|
||||
if edge.get_property(key) != Some(&expected_value) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !matches {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Get target node
|
||||
let to_node_id = if pattern.direction == Direction::Incoming {
|
||||
&edge.from
|
||||
} else {
|
||||
&edge.to
|
||||
};
|
||||
|
||||
if let Some(to_node) = self.graph.get_node(to_node_id) {
|
||||
let mut ctx = from_ctx.clone();
|
||||
if let Some(var) = &pattern.variable {
|
||||
ctx.bind(var.clone(), ContextValue::Edge(edge.clone()));
|
||||
}
|
||||
|
||||
// Bind target node if it's a simple node pattern
|
||||
if let Pattern::Node(to_pattern) = &*pattern.to {
|
||||
if let Some(var) = &to_pattern.variable {
|
||||
ctx.bind(var.clone(), ContextValue::Node(to_node.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
contexts.push(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(contexts)
|
||||
}
|
||||
|
||||
fn execute_return(
|
||||
&self,
|
||||
clause: &ReturnClause,
|
||||
context: &ExecutionContext,
|
||||
) -> Result<ExecutionResult, ExecutionError> {
|
||||
let mut columns = Vec::new();
|
||||
let mut row = HashMap::new();
|
||||
|
||||
for item in &clause.items {
|
||||
let col_name = item
|
||||
.alias
|
||||
.clone()
|
||||
.unwrap_or_else(|| match &item.expression {
|
||||
Expression::Variable(var) => var.clone(),
|
||||
_ => "?column?".to_string(),
|
||||
});
|
||||
|
||||
columns.push(col_name.clone());
|
||||
|
||||
let value = self.evaluate_expression_ctx(&item.expression, context)?;
|
||||
row.insert(col_name, value);
|
||||
}
|
||||
|
||||
let mut result = ExecutionResult::new(columns);
|
||||
result.add_row(row);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn execute_set(
|
||||
&mut self,
|
||||
clause: &SetClause,
|
||||
context: &ExecutionContext,
|
||||
) -> Result<ExecutionResult, ExecutionError> {
|
||||
for item in &clause.items {
|
||||
match item {
|
||||
SetItem::Property {
|
||||
variable,
|
||||
property,
|
||||
value,
|
||||
} => {
|
||||
let val = self.evaluate_expression(value, context)?;
|
||||
if let Some(ContextValue::Node(node)) = context.get(variable) {
|
||||
if let Some(node_mut) = self.graph.get_node_mut(&node.id) {
|
||||
node_mut.set_property(property.clone(), val);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(ExecutionError::UnsupportedOperation(
|
||||
"Only property SET supported".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::new(vec![]))
|
||||
}
|
||||
|
||||
fn execute_delete(
|
||||
&mut self,
|
||||
clause: &DeleteClause,
|
||||
context: &ExecutionContext,
|
||||
) -> Result<ExecutionResult, ExecutionError> {
|
||||
for expr in &clause.expressions {
|
||||
if let Expression::Variable(var) = expr {
|
||||
if let Some(ctx_val) = context.get(var) {
|
||||
match ctx_val {
|
||||
ContextValue::Node(node) => {
|
||||
if clause.detach {
|
||||
self.graph.delete_node(&node.id)?;
|
||||
} else {
|
||||
return Err(ExecutionError::ExecutionError(
|
||||
"Cannot delete node with relationships without DETACH"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
ContextValue::Edge(edge) => {
|
||||
self.graph.delete_edge(&edge.id)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::new(vec![]))
|
||||
}
|
||||
|
||||
fn evaluate_expression(
|
||||
&self,
|
||||
expr: &Expression,
|
||||
context: &ExecutionContext,
|
||||
) -> Result<Value, ExecutionError> {
|
||||
match expr {
|
||||
Expression::Integer(n) => Ok(Value::Integer(*n)),
|
||||
Expression::Float(f) => Ok(Value::Float(*f)),
|
||||
Expression::String(s) => Ok(Value::String(s.clone())),
|
||||
Expression::Boolean(b) => Ok(Value::Boolean(*b)),
|
||||
Expression::Null => Ok(Value::Null),
|
||||
Expression::Variable(var) => {
|
||||
if let Some(ContextValue::Value(v)) = context.get(var) {
|
||||
Ok(v.clone())
|
||||
} else {
|
||||
Err(ExecutionError::VariableNotFound(var.clone()))
|
||||
}
|
||||
}
|
||||
Expression::Property { object, property } => {
|
||||
if let Expression::Variable(var) = &**object {
|
||||
if let Some(ContextValue::Node(node)) = context.get(var) {
|
||||
Ok(node.get_property(property).cloned().unwrap_or(Value::Null))
|
||||
} else {
|
||||
Err(ExecutionError::VariableNotFound(var.clone()))
|
||||
}
|
||||
} else {
|
||||
Err(ExecutionError::UnsupportedOperation(
|
||||
"Nested property access not supported".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => Err(ExecutionError::UnsupportedOperation(format!(
|
||||
"Expression {:?} not yet implemented",
|
||||
expr
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_expression_ctx(
|
||||
&self,
|
||||
expr: &Expression,
|
||||
context: &ExecutionContext,
|
||||
) -> Result<ContextValue, ExecutionError> {
|
||||
match expr {
|
||||
Expression::Variable(var) => context
|
||||
.get(var)
|
||||
.cloned()
|
||||
.ok_or_else(|| ExecutionError::VariableNotFound(var.clone())),
|
||||
Expression::Property { object, property } => {
|
||||
if let Expression::Variable(var) = &**object {
|
||||
if let Some(ContextValue::Node(node)) = context.get(var) {
|
||||
Ok(ContextValue::Value(
|
||||
node.get_property(property).cloned().unwrap_or(Value::Null),
|
||||
))
|
||||
} else {
|
||||
Err(ExecutionError::VariableNotFound(var.clone()))
|
||||
}
|
||||
} else {
|
||||
Err(ExecutionError::UnsupportedOperation(
|
||||
"Nested property access not supported".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let val = self.evaluate_expression(expr, context)?;
|
||||
Ok(ContextValue::Value(val))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_condition(
|
||||
&self,
|
||||
expr: &Expression,
|
||||
context: &ExecutionContext,
|
||||
) -> Result<bool, ExecutionError> {
|
||||
match expr {
|
||||
Expression::Boolean(b) => Ok(*b),
|
||||
Expression::BinaryOp { left, op, right } => {
|
||||
let left_val = self.evaluate_expression(left, context)?;
|
||||
let right_val = self.evaluate_expression(right, context)?;
|
||||
|
||||
match op {
|
||||
BinaryOperator::Equal => Ok(left_val == right_val),
|
||||
BinaryOperator::NotEqual => Ok(left_val != right_val),
|
||||
BinaryOperator::GreaterThan => {
|
||||
if let (Some(l), Some(r)) = (left_val.as_i64(), right_val.as_i64()) {
|
||||
Ok(l > r)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
BinaryOperator::LessThan => {
|
||||
if let (Some(l), Some(r)) = (left_val.as_i64(), right_val.as_i64()) {
|
||||
Ok(l < r)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
_ => Err(ExecutionError::UnsupportedOperation(format!(
|
||||
"Operator {:?} not implemented",
|
||||
op
|
||||
))),
|
||||
}
|
||||
}
|
||||
_ => Err(ExecutionError::UnsupportedOperation(
|
||||
"Complex conditions not yet supported".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
438
vendor/ruvector/crates/rvlite/src/cypher/graph_store.rs
vendored
Normal file
438
vendor/ruvector/crates/rvlite/src/cypher/graph_store.rs
vendored
Normal file
@@ -0,0 +1,438 @@
|
||||
//! In-memory property graph storage for WASM-compatible Cypher execution
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
pub type NodeId = String;
|
||||
pub type EdgeId = String;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GraphError {
|
||||
#[error("Node not found: {0}")]
|
||||
NodeNotFound(NodeId),
|
||||
#[error("Edge not found: {0}")]
|
||||
EdgeNotFound(EdgeId),
|
||||
#[error("Invalid operation: {0}")]
|
||||
InvalidOperation(String),
|
||||
}
|
||||
|
||||
/// Property value that can be stored in nodes/edges
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Value {
|
||||
Null,
|
||||
Boolean(bool),
|
||||
Integer(i64),
|
||||
Float(f64),
|
||||
String(String),
|
||||
List(Vec<Value>),
|
||||
Map(HashMap<String, Value>),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn as_i64(&self) -> Option<i64> {
|
||||
match self {
|
||||
Value::Integer(n) => Some(*n),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_f64(&self) -> Option<f64> {
|
||||
match self {
|
||||
Value::Float(f) => Some(*f),
|
||||
Value::Integer(i) => Some(*i as f64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
Value::String(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_bool(&self) -> Option<bool> {
|
||||
match self {
|
||||
Value::Boolean(b) => Some(*b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Value {
|
||||
fn from(b: bool) -> Self {
|
||||
Value::Boolean(b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Value {
|
||||
fn from(n: i64) -> Self {
|
||||
Value::Integer(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Value {
|
||||
fn from(f: f64) -> Self {
|
||||
Value::Float(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Value {
|
||||
fn from(s: String) -> Self {
|
||||
Value::String(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Value {
|
||||
fn from(s: &str) -> Self {
|
||||
Value::String(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Node in the property graph
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Node {
|
||||
pub id: NodeId,
|
||||
pub labels: Vec<String>,
|
||||
pub properties: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn new(id: NodeId) -> Self {
|
||||
Self {
|
||||
id,
|
||||
labels: Vec::new(),
|
||||
properties: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_label(mut self, label: String) -> Self {
|
||||
self.labels.push(label);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_labels(mut self, labels: Vec<String>) -> Self {
|
||||
self.labels = labels;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_property(mut self, key: String, value: Value) -> Self {
|
||||
self.properties.insert(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_label(&self, label: &str) -> bool {
|
||||
self.labels.iter().any(|l| l == label)
|
||||
}
|
||||
|
||||
pub fn get_property(&self, key: &str) -> Option<&Value> {
|
||||
self.properties.get(key)
|
||||
}
|
||||
|
||||
pub fn set_property(&mut self, key: String, value: Value) {
|
||||
self.properties.insert(key, value);
|
||||
}
|
||||
|
||||
pub fn remove_property(&mut self, key: &str) -> Option<Value> {
|
||||
self.properties.remove(key)
|
||||
}
|
||||
|
||||
pub fn add_label(&mut self, label: String) {
|
||||
if !self.has_label(&label) {
|
||||
self.labels.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_label(&mut self, label: &str) {
|
||||
self.labels.retain(|l| l != label);
|
||||
}
|
||||
}
|
||||
|
||||
/// Edge/Relationship in the property graph
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Edge {
|
||||
pub id: EdgeId,
|
||||
pub from: NodeId,
|
||||
pub to: NodeId,
|
||||
pub edge_type: String,
|
||||
pub properties: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl Edge {
|
||||
pub fn new(id: EdgeId, from: NodeId, to: NodeId, edge_type: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
edge_type,
|
||||
properties: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_property(mut self, key: String, value: Value) -> Self {
|
||||
self.properties.insert(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_property(&self, key: &str) -> Option<&Value> {
|
||||
self.properties.get(key)
|
||||
}
|
||||
|
||||
pub fn set_property(&mut self, key: String, value: Value) {
|
||||
self.properties.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// In-memory property graph store
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PropertyGraph {
|
||||
nodes: HashMap<NodeId, Node>,
|
||||
edges: HashMap<EdgeId, Edge>,
|
||||
// Indexes for faster lookups
|
||||
label_index: HashMap<String, Vec<NodeId>>,
|
||||
edge_type_index: HashMap<String, Vec<EdgeId>>,
|
||||
outgoing_edges: HashMap<NodeId, Vec<EdgeId>>,
|
||||
incoming_edges: HashMap<NodeId, Vec<EdgeId>>,
|
||||
next_node_id: usize,
|
||||
next_edge_id: usize,
|
||||
}
|
||||
|
||||
impl PropertyGraph {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nodes: HashMap::new(),
|
||||
edges: HashMap::new(),
|
||||
label_index: HashMap::new(),
|
||||
edge_type_index: HashMap::new(),
|
||||
outgoing_edges: HashMap::new(),
|
||||
incoming_edges: HashMap::new(),
|
||||
next_node_id: 0,
|
||||
next_edge_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a unique node ID
|
||||
pub fn generate_node_id(&mut self) -> NodeId {
|
||||
let id = format!("n{}", self.next_node_id);
|
||||
self.next_node_id += 1;
|
||||
id
|
||||
}
|
||||
|
||||
/// Generate a unique edge ID
|
||||
pub fn generate_edge_id(&mut self) -> EdgeId {
|
||||
let id = format!("e{}", self.next_edge_id);
|
||||
self.next_edge_id += 1;
|
||||
id
|
||||
}
|
||||
|
||||
/// Add a node to the graph
|
||||
pub fn add_node(&mut self, node: Node) -> NodeId {
|
||||
let id = node.id.clone();
|
||||
|
||||
// Update label index
|
||||
for label in &node.labels {
|
||||
self.label_index
|
||||
.entry(label.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(id.clone());
|
||||
}
|
||||
|
||||
self.nodes.insert(id.clone(), node);
|
||||
id
|
||||
}
|
||||
|
||||
/// Get a node by ID
|
||||
pub fn get_node(&self, id: &NodeId) -> Option<&Node> {
|
||||
self.nodes.get(id)
|
||||
}
|
||||
|
||||
/// Get a mutable reference to a node
|
||||
pub fn get_node_mut(&mut self, id: &NodeId) -> Option<&mut Node> {
|
||||
self.nodes.get_mut(id)
|
||||
}
|
||||
|
||||
/// Find nodes by label
|
||||
pub fn find_nodes_by_label(&self, label: &str) -> Vec<&Node> {
|
||||
if let Some(node_ids) = self.label_index.get(label) {
|
||||
node_ids
|
||||
.iter()
|
||||
.filter_map(|id| self.nodes.get(id))
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Find all nodes matching a predicate
|
||||
pub fn find_nodes<F>(&self, predicate: F) -> Vec<&Node>
|
||||
where
|
||||
F: Fn(&Node) -> bool,
|
||||
{
|
||||
self.nodes.values().filter(|n| predicate(n)).collect()
|
||||
}
|
||||
|
||||
/// Add an edge to the graph
|
||||
pub fn add_edge(&mut self, edge: Edge) -> Result<EdgeId, GraphError> {
|
||||
// Verify nodes exist
|
||||
if !self.nodes.contains_key(&edge.from) {
|
||||
return Err(GraphError::NodeNotFound(edge.from.clone()));
|
||||
}
|
||||
if !self.nodes.contains_key(&edge.to) {
|
||||
return Err(GraphError::NodeNotFound(edge.to.clone()));
|
||||
}
|
||||
|
||||
let id = edge.id.clone();
|
||||
let from = edge.from.clone();
|
||||
let to = edge.to.clone();
|
||||
let edge_type = edge.edge_type.clone();
|
||||
|
||||
// Update indexes
|
||||
self.edge_type_index
|
||||
.entry(edge_type)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(id.clone());
|
||||
|
||||
self.outgoing_edges
|
||||
.entry(from)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(id.clone());
|
||||
|
||||
self.incoming_edges
|
||||
.entry(to)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(id.clone());
|
||||
|
||||
self.edges.insert(id.clone(), edge);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Get an edge by ID
|
||||
pub fn get_edge(&self, id: &EdgeId) -> Option<&Edge> {
|
||||
self.edges.get(id)
|
||||
}
|
||||
|
||||
/// Get outgoing edges from a node
|
||||
pub fn get_outgoing_edges(&self, node_id: &NodeId) -> Vec<&Edge> {
|
||||
if let Some(edge_ids) = self.outgoing_edges.get(node_id) {
|
||||
edge_ids
|
||||
.iter()
|
||||
.filter_map(|id| self.edges.get(id))
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get incoming edges to a node
|
||||
pub fn get_incoming_edges(&self, node_id: &NodeId) -> Vec<&Edge> {
|
||||
if let Some(edge_ids) = self.incoming_edges.get(node_id) {
|
||||
edge_ids
|
||||
.iter()
|
||||
.filter_map(|id| self.edges.get(id))
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all edges of a specific type
|
||||
pub fn find_edges_by_type(&self, edge_type: &str) -> Vec<&Edge> {
|
||||
if let Some(edge_ids) = self.edge_type_index.get(edge_type) {
|
||||
edge_ids
|
||||
.iter()
|
||||
.filter_map(|id| self.edges.get(id))
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a node and its connected edges
|
||||
pub fn delete_node(&mut self, id: &NodeId) -> Result<(), GraphError> {
|
||||
let node = self
|
||||
.nodes
|
||||
.remove(id)
|
||||
.ok_or_else(|| GraphError::NodeNotFound(id.clone()))?;
|
||||
|
||||
// Remove from label index
|
||||
for label in &node.labels {
|
||||
if let Some(ids) = self.label_index.get_mut(label) {
|
||||
ids.retain(|nid| nid != id);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove connected edges
|
||||
if let Some(edge_ids) = self.outgoing_edges.remove(id) {
|
||||
for edge_id in edge_ids {
|
||||
self.edges.remove(&edge_id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(edge_ids) = self.incoming_edges.remove(id) {
|
||||
for edge_id in edge_ids {
|
||||
self.edges.remove(&edge_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete an edge
|
||||
pub fn delete_edge(&mut self, id: &EdgeId) -> Result<(), GraphError> {
|
||||
let edge = self
|
||||
.edges
|
||||
.remove(id)
|
||||
.ok_or_else(|| GraphError::EdgeNotFound(id.clone()))?;
|
||||
|
||||
// Remove from type index
|
||||
if let Some(ids) = self.edge_type_index.get_mut(&edge.edge_type) {
|
||||
ids.retain(|eid| eid != id);
|
||||
}
|
||||
|
||||
// Remove from node edge lists
|
||||
if let Some(ids) = self.outgoing_edges.get_mut(&edge.from) {
|
||||
ids.retain(|eid| eid != id);
|
||||
}
|
||||
|
||||
if let Some(ids) = self.incoming_edges.get_mut(&edge.to) {
|
||||
ids.retain(|eid| eid != id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get statistics about the graph
|
||||
pub fn stats(&self) -> GraphStats {
|
||||
GraphStats {
|
||||
node_count: self.nodes.len(),
|
||||
edge_count: self.edges.len(),
|
||||
label_count: self.label_index.len(),
|
||||
edge_type_count: self.edge_type_index.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all nodes in the graph
|
||||
pub fn all_nodes(&self) -> Vec<&Node> {
|
||||
self.nodes.values().collect()
|
||||
}
|
||||
|
||||
/// Get all edges in the graph
|
||||
pub fn all_edges(&self) -> Vec<&Edge> {
|
||||
self.edges.values().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PropertyGraph {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GraphStats {
|
||||
pub node_count: usize,
|
||||
pub edge_count: usize,
|
||||
pub label_count: usize,
|
||||
pub edge_type_count: usize,
|
||||
}
|
||||
607
vendor/ruvector/crates/rvlite/src/cypher/lexer.rs
vendored
Normal file
607
vendor/ruvector/crates/rvlite/src/cypher/lexer.rs
vendored
Normal file
@@ -0,0 +1,607 @@
|
||||
//! Lexical analyzer (tokenizer) for Cypher query language
|
||||
//!
|
||||
//! Hand-rolled lexer for WASM compatibility - no external dependencies.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::iter::Peekable;
|
||||
use std::str::Chars;
|
||||
|
||||
/// Token with kind and location information
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Token {
|
||||
pub kind: TokenKind,
|
||||
pub lexeme: String,
|
||||
pub position: Position,
|
||||
}
|
||||
|
||||
/// Source position for error reporting
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct Position {
|
||||
pub line: usize,
|
||||
pub column: usize,
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
/// Token kinds
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum TokenKind {
|
||||
// Keywords
|
||||
Match,
|
||||
OptionalMatch,
|
||||
Where,
|
||||
Return,
|
||||
Create,
|
||||
Merge,
|
||||
Delete,
|
||||
DetachDelete,
|
||||
Set,
|
||||
Remove,
|
||||
With,
|
||||
OrderBy,
|
||||
Limit,
|
||||
Skip,
|
||||
Distinct,
|
||||
As,
|
||||
Asc,
|
||||
Desc,
|
||||
Case,
|
||||
When,
|
||||
Then,
|
||||
Else,
|
||||
End,
|
||||
And,
|
||||
Or,
|
||||
Xor,
|
||||
Not,
|
||||
In,
|
||||
Is,
|
||||
Null,
|
||||
True,
|
||||
False,
|
||||
OnCreate,
|
||||
OnMatch,
|
||||
|
||||
// Identifiers and literals
|
||||
Identifier(String),
|
||||
Integer(i64),
|
||||
Float(f64),
|
||||
String(String),
|
||||
|
||||
// Operators
|
||||
Plus,
|
||||
Minus,
|
||||
Star,
|
||||
Slash,
|
||||
Percent,
|
||||
Caret,
|
||||
Equal,
|
||||
NotEqual,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
GreaterThan,
|
||||
GreaterThanOrEqual,
|
||||
Arrow, // ->
|
||||
LeftArrow, // <-
|
||||
Dash, // -
|
||||
|
||||
// Delimiters
|
||||
LeftParen,
|
||||
RightParen,
|
||||
LeftBracket,
|
||||
RightBracket,
|
||||
LeftBrace,
|
||||
RightBrace,
|
||||
Comma,
|
||||
Dot,
|
||||
Colon,
|
||||
Semicolon,
|
||||
Pipe,
|
||||
|
||||
// Special
|
||||
DotDot, // ..
|
||||
Eof,
|
||||
}
|
||||
|
||||
impl fmt::Display for TokenKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
TokenKind::Identifier(s) => write!(f, "identifier '{}'", s),
|
||||
TokenKind::Integer(n) => write!(f, "integer {}", n),
|
||||
TokenKind::Float(n) => write!(f, "float {}", n),
|
||||
TokenKind::String(s) => write!(f, "string \"{}\"", s),
|
||||
_ => write!(f, "{:?}", self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lexer error
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LexerError {
|
||||
pub message: String,
|
||||
pub position: Position,
|
||||
}
|
||||
|
||||
impl fmt::Display for LexerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Lexer error at {}:{}: {}",
|
||||
self.position.line, self.position.column, self.message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LexerError {}
|
||||
|
||||
/// Hand-rolled Cypher lexer
|
||||
pub struct Lexer<'a> {
|
||||
input: &'a str,
|
||||
chars: Peekable<Chars<'a>>,
|
||||
position: Position,
|
||||
current_offset: usize,
|
||||
}
|
||||
|
||||
impl<'a> Lexer<'a> {
|
||||
pub fn new(input: &'a str) -> Self {
|
||||
Self {
|
||||
input,
|
||||
chars: input.chars().peekable(),
|
||||
position: Position {
|
||||
line: 1,
|
||||
column: 1,
|
||||
offset: 0,
|
||||
},
|
||||
current_offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn peek(&mut self) -> Option<char> {
|
||||
self.chars.peek().copied()
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> Option<char> {
|
||||
let ch = self.chars.next()?;
|
||||
self.current_offset += ch.len_utf8();
|
||||
if ch == '\n' {
|
||||
self.position.line += 1;
|
||||
self.position.column = 1;
|
||||
} else {
|
||||
self.position.column += 1;
|
||||
}
|
||||
self.position.offset = self.current_offset;
|
||||
Some(ch)
|
||||
}
|
||||
|
||||
fn skip_whitespace(&mut self) {
|
||||
while let Some(ch) = self.peek() {
|
||||
if ch.is_whitespace() {
|
||||
self.advance();
|
||||
} else if ch == '/' && self.lookahead(1) == Some('/') {
|
||||
// Skip line comments
|
||||
while let Some(c) = self.peek() {
|
||||
if c == '\n' {
|
||||
break;
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lookahead(&self, n: usize) -> Option<char> {
|
||||
self.input[self.current_offset..].chars().nth(n)
|
||||
}
|
||||
|
||||
fn make_token(&self, kind: TokenKind, lexeme: &str, start_pos: Position) -> Token {
|
||||
Token {
|
||||
kind,
|
||||
lexeme: lexeme.to_string(),
|
||||
position: start_pos,
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_string(&mut self, quote: char) -> Result<Token, LexerError> {
|
||||
let start = self.position;
|
||||
self.advance(); // consume opening quote
|
||||
let mut value = String::new();
|
||||
|
||||
while let Some(ch) = self.peek() {
|
||||
if ch == quote {
|
||||
self.advance(); // consume closing quote
|
||||
return Ok(self.make_token(TokenKind::String(value.clone()), &value, start));
|
||||
} else if ch == '\\' {
|
||||
self.advance();
|
||||
match self.peek() {
|
||||
Some('n') => {
|
||||
value.push('\n');
|
||||
self.advance();
|
||||
}
|
||||
Some('t') => {
|
||||
value.push('\t');
|
||||
self.advance();
|
||||
}
|
||||
Some('r') => {
|
||||
value.push('\r');
|
||||
self.advance();
|
||||
}
|
||||
Some('\\') => {
|
||||
value.push('\\');
|
||||
self.advance();
|
||||
}
|
||||
Some(c) if c == quote => {
|
||||
value.push(c);
|
||||
self.advance();
|
||||
}
|
||||
_ => value.push('\\'),
|
||||
}
|
||||
} else {
|
||||
value.push(ch);
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
|
||||
Err(LexerError {
|
||||
message: "Unterminated string".to_string(),
|
||||
position: start,
|
||||
})
|
||||
}
|
||||
|
||||
fn scan_number(&mut self) -> Token {
|
||||
let start = self.position;
|
||||
let start_offset = self.current_offset;
|
||||
|
||||
while let Some(ch) = self.peek() {
|
||||
if ch.is_ascii_digit() {
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for decimal
|
||||
if self.peek() == Some('.')
|
||||
&& self
|
||||
.lookahead(1)
|
||||
.map(|c| c.is_ascii_digit())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
self.advance(); // consume '.'
|
||||
while let Some(ch) = self.peek() {
|
||||
if ch.is_ascii_digit() {
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let lexeme = &self.input[start_offset..self.current_offset];
|
||||
let value: f64 = lexeme.parse().unwrap_or(0.0);
|
||||
return self.make_token(TokenKind::Float(value), lexeme, start);
|
||||
}
|
||||
|
||||
// Check for exponent
|
||||
if matches!(self.peek(), Some('e') | Some('E')) {
|
||||
self.advance();
|
||||
if matches!(self.peek(), Some('+') | Some('-')) {
|
||||
self.advance();
|
||||
}
|
||||
while let Some(ch) = self.peek() {
|
||||
if ch.is_ascii_digit() {
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let lexeme = &self.input[start_offset..self.current_offset];
|
||||
let value: f64 = lexeme.parse().unwrap_or(0.0);
|
||||
return self.make_token(TokenKind::Float(value), lexeme, start);
|
||||
}
|
||||
|
||||
let lexeme = &self.input[start_offset..self.current_offset];
|
||||
let value: i64 = lexeme.parse().unwrap_or(0);
|
||||
self.make_token(TokenKind::Integer(value), lexeme, start)
|
||||
}
|
||||
|
||||
fn scan_identifier(&mut self) -> Token {
|
||||
let start = self.position;
|
||||
let start_offset = self.current_offset;
|
||||
|
||||
while let Some(ch) = self.peek() {
|
||||
if ch.is_ascii_alphanumeric() || ch == '_' {
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let lexeme = &self.input[start_offset..self.current_offset];
|
||||
let kind = match lexeme.to_uppercase().as_str() {
|
||||
"MATCH" => TokenKind::Match,
|
||||
"OPTIONAL" if self.peek_keyword("MATCH") => {
|
||||
self.skip_whitespace();
|
||||
self.scan_keyword("MATCH");
|
||||
TokenKind::OptionalMatch
|
||||
}
|
||||
"WHERE" => TokenKind::Where,
|
||||
"RETURN" => TokenKind::Return,
|
||||
"CREATE" => TokenKind::Create,
|
||||
"MERGE" => TokenKind::Merge,
|
||||
"DELETE" => TokenKind::Delete,
|
||||
"DETACH" if self.peek_keyword("DELETE") => {
|
||||
self.skip_whitespace();
|
||||
self.scan_keyword("DELETE");
|
||||
TokenKind::DetachDelete
|
||||
}
|
||||
"SET" => TokenKind::Set,
|
||||
"REMOVE" => TokenKind::Remove,
|
||||
"WITH" => TokenKind::With,
|
||||
"ORDER" if self.peek_keyword("BY") => {
|
||||
self.skip_whitespace();
|
||||
self.scan_keyword("BY");
|
||||
TokenKind::OrderBy
|
||||
}
|
||||
"LIMIT" => TokenKind::Limit,
|
||||
"SKIP" => TokenKind::Skip,
|
||||
"DISTINCT" => TokenKind::Distinct,
|
||||
"AS" => TokenKind::As,
|
||||
"ASC" => TokenKind::Asc,
|
||||
"DESC" => TokenKind::Desc,
|
||||
"CASE" => TokenKind::Case,
|
||||
"WHEN" => TokenKind::When,
|
||||
"THEN" => TokenKind::Then,
|
||||
"ELSE" => TokenKind::Else,
|
||||
"END" => TokenKind::End,
|
||||
"AND" => TokenKind::And,
|
||||
"OR" => TokenKind::Or,
|
||||
"XOR" => TokenKind::Xor,
|
||||
"NOT" => TokenKind::Not,
|
||||
"IN" => TokenKind::In,
|
||||
"IS" => TokenKind::Is,
|
||||
"NULL" => TokenKind::Null,
|
||||
"TRUE" => TokenKind::True,
|
||||
"FALSE" => TokenKind::False,
|
||||
"ON" if self.peek_keyword("CREATE") => {
|
||||
self.skip_whitespace();
|
||||
self.scan_keyword("CREATE");
|
||||
TokenKind::OnCreate
|
||||
}
|
||||
_ if lexeme.to_uppercase() == "ON" && self.peek_keyword("MATCH") => {
|
||||
self.skip_whitespace();
|
||||
self.scan_keyword("MATCH");
|
||||
TokenKind::OnMatch
|
||||
}
|
||||
_ => TokenKind::Identifier(lexeme.to_string()),
|
||||
};
|
||||
|
||||
self.make_token(kind, lexeme, start)
|
||||
}
|
||||
|
||||
fn peek_keyword(&mut self, keyword: &str) -> bool {
|
||||
let saved_offset = self.current_offset;
|
||||
self.skip_whitespace();
|
||||
let remaining = &self.input[self.current_offset..];
|
||||
let matches = remaining.to_uppercase().starts_with(keyword)
|
||||
&& remaining
|
||||
.chars()
|
||||
.nth(keyword.len())
|
||||
.map(|c| !c.is_ascii_alphanumeric() && c != '_')
|
||||
.unwrap_or(true);
|
||||
// Reset position if not consuming
|
||||
if !matches {
|
||||
self.current_offset = saved_offset;
|
||||
self.chars = self.input[saved_offset..].chars().peekable();
|
||||
}
|
||||
matches
|
||||
}
|
||||
|
||||
fn scan_keyword(&mut self, keyword: &str) {
|
||||
for _ in 0..keyword.len() {
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_token(&mut self) -> Result<Token, LexerError> {
|
||||
self.skip_whitespace();
|
||||
|
||||
let start = self.position;
|
||||
|
||||
match self.peek() {
|
||||
None => Ok(self.make_token(TokenKind::Eof, "", start)),
|
||||
Some(ch) => {
|
||||
match ch {
|
||||
// Strings
|
||||
'"' | '\'' => self.scan_string(ch),
|
||||
|
||||
// Numbers
|
||||
'0'..='9' => Ok(self.scan_number()),
|
||||
|
||||
// Identifiers
|
||||
'a'..='z' | 'A'..='Z' | '_' | '$' => Ok(self.scan_identifier()),
|
||||
|
||||
// Backtick-quoted identifiers
|
||||
'`' => {
|
||||
self.advance();
|
||||
let id_start = self.current_offset;
|
||||
while let Some(c) = self.peek() {
|
||||
if c == '`' {
|
||||
break;
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
let id = self.input[id_start..self.current_offset].to_string();
|
||||
self.advance(); // consume closing backtick
|
||||
Ok(self.make_token(TokenKind::Identifier(id.clone()), &id, start))
|
||||
}
|
||||
|
||||
// Two-character operators
|
||||
'<' => {
|
||||
self.advance();
|
||||
match self.peek() {
|
||||
Some('=') => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::LessThanOrEqual, "<=", start))
|
||||
}
|
||||
Some('>') => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::NotEqual, "<>", start))
|
||||
}
|
||||
Some('-') => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::LeftArrow, "<-", start))
|
||||
}
|
||||
_ => Ok(self.make_token(TokenKind::LessThan, "<", start)),
|
||||
}
|
||||
}
|
||||
'>' => {
|
||||
self.advance();
|
||||
if self.peek() == Some('=') {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::GreaterThanOrEqual, ">=", start))
|
||||
} else {
|
||||
Ok(self.make_token(TokenKind::GreaterThan, ">", start))
|
||||
}
|
||||
}
|
||||
'-' => {
|
||||
self.advance();
|
||||
if self.peek() == Some('>') {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::Arrow, "->", start))
|
||||
} else {
|
||||
Ok(self.make_token(TokenKind::Dash, "-", start))
|
||||
}
|
||||
}
|
||||
'.' => {
|
||||
self.advance();
|
||||
if self.peek() == Some('.') {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::DotDot, "..", start))
|
||||
} else {
|
||||
Ok(self.make_token(TokenKind::Dot, ".", start))
|
||||
}
|
||||
}
|
||||
'=' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::Equal, "=", start))
|
||||
}
|
||||
|
||||
// Single-character tokens
|
||||
'(' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::LeftParen, "(", start))
|
||||
}
|
||||
')' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::RightParen, ")", start))
|
||||
}
|
||||
'[' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::LeftBracket, "[", start))
|
||||
}
|
||||
']' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::RightBracket, "]", start))
|
||||
}
|
||||
'{' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::LeftBrace, "{", start))
|
||||
}
|
||||
'}' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::RightBrace, "}", start))
|
||||
}
|
||||
',' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::Comma, ",", start))
|
||||
}
|
||||
':' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::Colon, ":", start))
|
||||
}
|
||||
';' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::Semicolon, ";", start))
|
||||
}
|
||||
'|' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::Pipe, "|", start))
|
||||
}
|
||||
'+' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::Plus, "+", start))
|
||||
}
|
||||
'*' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::Star, "*", start))
|
||||
}
|
||||
'/' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::Slash, "/", start))
|
||||
}
|
||||
'%' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::Percent, "%", start))
|
||||
}
|
||||
'^' => {
|
||||
self.advance();
|
||||
Ok(self.make_token(TokenKind::Caret, "^", start))
|
||||
}
|
||||
|
||||
_ => Err(LexerError {
|
||||
message: format!("Unexpected character: '{}'", ch),
|
||||
position: start,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tokenize a Cypher query string
|
||||
pub fn tokenize(input: &str) -> Result<Vec<Token>, LexerError> {
|
||||
let mut lexer = Lexer::new(input);
|
||||
let mut tokens = Vec::new();
|
||||
|
||||
loop {
|
||||
let token = lexer.next_token()?;
|
||||
let is_eof = token.kind == TokenKind::Eof;
|
||||
tokens.push(token);
|
||||
if is_eof {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_simple_tokens() {
|
||||
let tokens = tokenize("MATCH (n) RETURN n").unwrap();
|
||||
assert_eq!(tokens[0].kind, TokenKind::Match);
|
||||
assert_eq!(tokens[1].kind, TokenKind::LeftParen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string() {
|
||||
let tokens = tokenize("'hello world'").unwrap();
|
||||
assert_eq!(tokens[0].kind, TokenKind::String("hello world".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number() {
|
||||
let tokens = tokenize("42 3.14").unwrap();
|
||||
assert_eq!(tokens[0].kind, TokenKind::Integer(42));
|
||||
assert_eq!(tokens[1].kind, TokenKind::Float(3.14));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relationship() {
|
||||
let tokens = tokenize("(a)-[:KNOWS]->(b)").unwrap();
|
||||
assert!(tokens.iter().any(|t| t.kind == TokenKind::Arrow));
|
||||
}
|
||||
}
|
||||
266
vendor/ruvector/crates/rvlite/src/cypher/mod.rs
vendored
Normal file
266
vendor/ruvector/crates/rvlite/src/cypher/mod.rs
vendored
Normal file
@@ -0,0 +1,266 @@
|
||||
//! Cypher query language parser and execution engine for WASM
|
||||
//!
|
||||
//! This module provides a WASM-compatible Cypher implementation including:
|
||||
//! - Lexical analysis (tokenization)
|
||||
//! - Syntax parsing (AST generation)
|
||||
//! - In-memory property graph storage
|
||||
//! - Query execution engine
|
||||
//!
|
||||
//! Supported operations:
|
||||
//! - CREATE: Create nodes and relationships
|
||||
//! - MATCH: Pattern matching
|
||||
//! - WHERE: Filtering
|
||||
//! - RETURN: Projection
|
||||
//! - SET: Update properties
|
||||
//! - DELETE/DETACH DELETE: Remove nodes and edges
|
||||
|
||||
pub mod ast;
|
||||
pub mod executor;
|
||||
pub mod graph_store;
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
|
||||
pub use ast::{Expression, Pattern, Query, Statement};
|
||||
pub use executor::{ContextValue, ExecutionError, ExecutionResult, Executor};
|
||||
pub use graph_store::{Edge, EdgeId, Node, NodeId, PropertyGraph, Value};
|
||||
pub use lexer::{tokenize, Token, TokenKind};
|
||||
pub use parser::{parse_cypher, ParseError};
|
||||
|
||||
use crate::storage::state::{EdgeState, GraphState, NodeState, PropertyValue};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// WASM-compatible Cypher engine
|
||||
#[wasm_bindgen]
|
||||
pub struct CypherEngine {
|
||||
graph: PropertyGraph,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl CypherEngine {
|
||||
/// Create a new Cypher engine with empty graph
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
graph: PropertyGraph::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a Cypher query and return JSON results
|
||||
pub fn execute(&mut self, query: &str) -> Result<JsValue, JsValue> {
|
||||
// Parse the query
|
||||
let ast =
|
||||
parse_cypher(query).map_err(|e| JsValue::from_str(&format!("Parse error: {}", e)))?;
|
||||
|
||||
// Execute the query
|
||||
let mut executor = Executor::new(&mut self.graph);
|
||||
let result = executor
|
||||
.execute(&ast)
|
||||
.map_err(|e| JsValue::from_str(&format!("Execution error: {}", e)))?;
|
||||
|
||||
// Convert to JS value
|
||||
serde_wasm_bindgen::to_value(&result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
|
||||
}
|
||||
|
||||
/// Get graph statistics
|
||||
pub fn stats(&self) -> Result<JsValue, JsValue> {
|
||||
let stats = self.graph.stats();
|
||||
serde_wasm_bindgen::to_value(&stats)
|
||||
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
|
||||
}
|
||||
|
||||
/// Clear the graph
|
||||
pub fn clear(&mut self) {
|
||||
self.graph = PropertyGraph::new();
|
||||
}
|
||||
}
|
||||
|
||||
impl CypherEngine {
|
||||
/// Export graph state for persistence
|
||||
pub fn export_state(&self) -> GraphState {
|
||||
let nodes: Vec<NodeState> = self
|
||||
.graph
|
||||
.all_nodes()
|
||||
.into_iter()
|
||||
.map(|n| NodeState {
|
||||
id: n.id.clone(),
|
||||
labels: n.labels.clone(),
|
||||
properties: n
|
||||
.properties
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), value_to_property(v)))
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let edges: Vec<EdgeState> = self
|
||||
.graph
|
||||
.all_edges()
|
||||
.into_iter()
|
||||
.map(|e| EdgeState {
|
||||
id: e.id.clone(),
|
||||
from: e.from.clone(),
|
||||
to: e.to.clone(),
|
||||
edge_type: e.edge_type.clone(),
|
||||
properties: e
|
||||
.properties
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), value_to_property(v)))
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let stats = self.graph.stats();
|
||||
|
||||
GraphState {
|
||||
nodes,
|
||||
edges,
|
||||
next_node_id: stats.node_count,
|
||||
next_edge_id: stats.edge_count,
|
||||
}
|
||||
}
|
||||
|
||||
/// Import state to restore the graph
|
||||
pub fn import_state(&mut self, state: &GraphState) -> Result<(), JsValue> {
|
||||
self.graph = PropertyGraph::new();
|
||||
|
||||
// Import nodes first
|
||||
for node_state in &state.nodes {
|
||||
let mut node = Node::new(node_state.id.clone());
|
||||
for label in &node_state.labels {
|
||||
node = node.with_label(label.clone());
|
||||
}
|
||||
for (key, value) in &node_state.properties {
|
||||
node = node.with_property(key.clone(), property_to_value(value));
|
||||
}
|
||||
self.graph.add_node(node);
|
||||
}
|
||||
|
||||
// Then import edges
|
||||
for edge_state in &state.edges {
|
||||
let mut edge = Edge::new(
|
||||
edge_state.id.clone(),
|
||||
edge_state.from.clone(),
|
||||
edge_state.to.clone(),
|
||||
edge_state.edge_type.clone(),
|
||||
);
|
||||
for (key, value) in &edge_state.properties {
|
||||
edge = edge.with_property(key.clone(), property_to_value(value));
|
||||
}
|
||||
if let Err(e) = self.graph.add_edge(edge) {
|
||||
return Err(JsValue::from_str(&format!("Failed to import edge: {}", e)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CypherEngine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert graph Value to serializable PropertyValue
|
||||
fn value_to_property(v: &Value) -> PropertyValue {
|
||||
match v {
|
||||
Value::Null => PropertyValue::Null,
|
||||
Value::Boolean(b) => PropertyValue::Boolean(*b),
|
||||
Value::Integer(i) => PropertyValue::Integer(*i),
|
||||
Value::Float(f) => PropertyValue::Float(*f),
|
||||
Value::String(s) => PropertyValue::String(s.clone()),
|
||||
Value::List(list) => PropertyValue::List(list.iter().map(value_to_property).collect()),
|
||||
Value::Map(map) => PropertyValue::Map(
|
||||
map.iter()
|
||||
.map(|(k, v)| (k.clone(), value_to_property(v)))
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert PropertyValue back to graph Value
|
||||
fn property_to_value(p: &PropertyValue) -> Value {
|
||||
match p {
|
||||
PropertyValue::Null => Value::Null,
|
||||
PropertyValue::Boolean(b) => Value::Boolean(*b),
|
||||
PropertyValue::Integer(i) => Value::Integer(*i),
|
||||
PropertyValue::Float(f) => Value::Float(*f),
|
||||
PropertyValue::String(s) => Value::String(s.clone()),
|
||||
PropertyValue::List(list) => Value::List(list.iter().map(property_to_value).collect()),
|
||||
PropertyValue::Map(map) => Value::Map(
|
||||
map.iter()
|
||||
.map(|(k, v)| (k.clone(), property_to_value(v)))
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_node() {
|
||||
let mut engine = CypherEngine::new();
|
||||
let query = "CREATE (n:Person {name: 'Alice', age: 30})";
|
||||
|
||||
let ast = parse_cypher(query).unwrap();
|
||||
let mut executor = Executor::new(&mut engine.graph);
|
||||
let result = executor.execute(&ast);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(engine.graph.stats().node_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_relationship() {
|
||||
let mut engine = CypherEngine::new();
|
||||
let query = "CREATE (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person {name: 'Bob'})";
|
||||
|
||||
let ast = parse_cypher(query).unwrap();
|
||||
let mut executor = Executor::new(&mut engine.graph);
|
||||
let result = executor.execute(&ast);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let stats = engine.graph.stats();
|
||||
assert_eq!(stats.node_count, 2);
|
||||
assert_eq!(stats.edge_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_nodes() {
|
||||
let mut engine = CypherEngine::new();
|
||||
|
||||
// Create data
|
||||
let create = "CREATE (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})";
|
||||
let ast = parse_cypher(create).unwrap();
|
||||
let mut executor = Executor::new(&mut engine.graph);
|
||||
executor.execute(&ast).unwrap();
|
||||
|
||||
// Match nodes
|
||||
let match_query = "MATCH (n:Person) RETURN n";
|
||||
let ast = parse_cypher(match_query).unwrap();
|
||||
let mut executor = Executor::new(&mut engine.graph);
|
||||
let result = executor.execute(&ast);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
let queries = vec![
|
||||
"MATCH (n:Person) RETURN n",
|
||||
"CREATE (n:Person {name: 'Alice'})",
|
||||
"MATCH (a)-[r:KNOWS]->(b) RETURN a, r, b",
|
||||
"CREATE (a:Person)-[r:KNOWS]->(b:Person)",
|
||||
];
|
||||
|
||||
for query in queries {
|
||||
let result = parse_cypher(query);
|
||||
assert!(result.is_ok(), "Failed to parse: {}", query);
|
||||
}
|
||||
}
|
||||
}
|
||||
1295
vendor/ruvector/crates/rvlite/src/cypher/parser.rs
vendored
Normal file
1295
vendor/ruvector/crates/rvlite/src/cypher/parser.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user