#[cfg(any(feature = "apoc-create", feature = "apoc-refactor"))]
use crate::error::Error;
use crate::error::Result;
use crate::reader::GraphReader;
use crate::value::Value;
#[cfg(any(feature = "apoc-create", feature = "apoc-refactor"))]
use crate::writer::GraphWriter;
#[cfg(any(feature = "apoc-create", feature = "apoc-refactor"))]
use meshdb_core::Edge;
#[cfg(feature = "apoc-create")]
use meshdb_core::Node;
use meshdb_core::Property;
use std::collections::{BTreeSet, HashMap};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcType {
String,
Integer,
Float,
Number,
Boolean,
Any,
}
impl ProcType {
pub fn parse(s: &str) -> Self {
let trimmed = s.trim().trim_end_matches('?').trim();
match trimmed.to_ascii_uppercase().as_str() {
"STRING" => ProcType::String,
"INTEGER" | "INT" => ProcType::Integer,
"FLOAT" => ProcType::Float,
"NUMBER" | "NUMERIC" => ProcType::Number,
"BOOLEAN" | "BOOL" => ProcType::Boolean,
_ => ProcType::Any,
}
}
pub fn accepts(&self, value: &Value) -> bool {
if matches!(value, Value::Null) {
return true;
}
match (self, value) {
(ProcType::Any, _) => true,
(ProcType::String, Value::Property(Property::String(_))) => true,
(ProcType::Integer, Value::Property(Property::Int64(_))) => true,
(ProcType::Float, Value::Property(Property::Float64(_))) => true,
(ProcType::Float, Value::Property(Property::Int64(_))) => true,
(ProcType::Number, Value::Property(Property::Int64(_))) => true,
(ProcType::Number, Value::Property(Property::Float64(_))) => true,
(ProcType::Boolean, Value::Property(Property::Bool(_))) => true,
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub struct ProcArgSpec {
pub name: String,
pub ty: ProcType,
}
#[derive(Debug, Clone)]
pub struct ProcOutSpec {
pub name: String,
pub ty: ProcType,
}
#[derive(Debug, Clone)]
pub struct Procedure {
pub qualified_name: Vec<String>,
pub inputs: Vec<ProcArgSpec>,
pub outputs: Vec<ProcOutSpec>,
pub rows: Vec<ProcRow>,
pub builtin: Option<BuiltinProc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuiltinProc {
DbLabels,
DbRelationshipTypes,
DbPropertyKeys,
DbConstraints,
#[cfg(feature = "apoc-create")]
ApocCreateNode,
#[cfg(feature = "apoc-create")]
ApocCreateRelationship,
#[cfg(feature = "apoc-create")]
ApocCreateAddLabels,
#[cfg(feature = "apoc-create")]
ApocCreateRemoveLabels,
#[cfg(feature = "apoc-create")]
ApocCreateSetLabels,
#[cfg(feature = "apoc-create")]
ApocCreateSetProperty,
#[cfg(feature = "apoc-create")]
ApocCreateSetRelProperty,
#[cfg(feature = "apoc-refactor")]
ApocRefactorSetType,
#[cfg(feature = "apoc-meta")]
ApocMetaSchema,
}
pub type ProcRow = HashMap<String, Value>;
impl Procedure {
pub fn row_matches(&self, row: &ProcRow, args: &[Value]) -> bool {
for (spec, arg) in self.inputs.iter().zip(args.iter()) {
let cell = row.get(&spec.name).unwrap_or(&Value::Null);
if !values_equal_for_procedure(cell, arg) {
return false;
}
}
true
}
pub fn is_write_builtin(&self) -> bool {
match self.builtin {
#[cfg(feature = "apoc-create")]
Some(
BuiltinProc::ApocCreateNode
| BuiltinProc::ApocCreateRelationship
| BuiltinProc::ApocCreateAddLabels
| BuiltinProc::ApocCreateRemoveLabels
| BuiltinProc::ApocCreateSetLabels
| BuiltinProc::ApocCreateSetProperty
| BuiltinProc::ApocCreateSetRelProperty,
) => true,
#[cfg(feature = "apoc-refactor")]
Some(BuiltinProc::ApocRefactorSetType) => true,
_ => false,
}
}
pub fn resolve_rows(&self, reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
match self.builtin {
None => Ok(self.rows.clone()),
Some(BuiltinProc::DbLabels) => builtin_db_labels(reader),
Some(BuiltinProc::DbRelationshipTypes) => builtin_db_relationship_types(reader),
Some(BuiltinProc::DbPropertyKeys) => builtin_db_property_keys(reader),
Some(BuiltinProc::DbConstraints) => builtin_db_constraints(reader),
#[cfg(feature = "apoc-meta")]
Some(BuiltinProc::ApocMetaSchema) => builtin_apoc_meta_schema(reader),
#[cfg(feature = "apoc-create")]
Some(BuiltinProc::ApocCreateNode) => Err(Error::Procedure(
"apoc.create.node is a write procedure — call via resolve_write_rows".into(),
)),
#[cfg(feature = "apoc-create")]
Some(BuiltinProc::ApocCreateRelationship) => Err(Error::Procedure(
"apoc.create.relationship is a write procedure — call via resolve_write_rows"
.into(),
)),
#[cfg(feature = "apoc-create")]
Some(
BuiltinProc::ApocCreateAddLabels
| BuiltinProc::ApocCreateRemoveLabels
| BuiltinProc::ApocCreateSetLabels
| BuiltinProc::ApocCreateSetProperty
| BuiltinProc::ApocCreateSetRelProperty,
) => Err(Error::Procedure(
"apoc.create.* mutator is a write procedure — call via resolve_write_rows".into(),
)),
#[cfg(feature = "apoc-refactor")]
Some(BuiltinProc::ApocRefactorSetType) => Err(Error::Procedure(
"apoc.refactor.setType is a write procedure — call via resolve_write_rows".into(),
)),
}
}
#[cfg(any(feature = "apoc-create", feature = "apoc-refactor"))]
pub fn resolve_write_rows(
&self,
reader: &dyn GraphReader,
writer: &dyn GraphWriter,
args: &[Value],
) -> Result<Vec<ProcRow>> {
match self.builtin {
#[cfg(feature = "apoc-create")]
Some(BuiltinProc::ApocCreateNode) => apoc_create_node(writer, args),
#[cfg(feature = "apoc-create")]
Some(BuiltinProc::ApocCreateRelationship) => apoc_create_relationship(writer, args),
#[cfg(feature = "apoc-create")]
Some(BuiltinProc::ApocCreateAddLabels) => {
apoc_label_mutator(reader, writer, args, LabelMode::Add)
}
#[cfg(feature = "apoc-create")]
Some(BuiltinProc::ApocCreateRemoveLabels) => {
apoc_label_mutator(reader, writer, args, LabelMode::Remove)
}
#[cfg(feature = "apoc-create")]
Some(BuiltinProc::ApocCreateSetLabels) => {
apoc_label_mutator(reader, writer, args, LabelMode::Set)
}
#[cfg(feature = "apoc-create")]
Some(BuiltinProc::ApocCreateSetProperty) => {
apoc_set_node_property(reader, writer, args)
}
#[cfg(feature = "apoc-create")]
Some(BuiltinProc::ApocCreateSetRelProperty) => {
apoc_set_rel_property(reader, writer, args)
}
#[cfg(feature = "apoc-refactor")]
Some(BuiltinProc::ApocRefactorSetType) => apoc_refactor_set_type(reader, writer, args),
_ => Err(Error::Procedure("procedure is not a write builtin".into())),
}
}
}
fn str_row(column: &str, value: String) -> ProcRow {
let mut row = HashMap::new();
row.insert(column.to_string(), Value::Property(Property::String(value)));
row
}
fn builtin_db_labels(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
let mut labels: BTreeSet<String> = BTreeSet::new();
for id in reader.all_node_ids()? {
if let Some(n) = reader.get_node(id)? {
for l in n.labels {
labels.insert(l);
}
}
}
Ok(labels.into_iter().map(|l| str_row("label", l)).collect())
}
fn builtin_db_relationship_types(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
let mut types: BTreeSet<String> = BTreeSet::new();
for id in reader.all_node_ids()? {
for (edge_id, _) in reader.outgoing(id)? {
if let Some(e) = reader.get_edge(edge_id)? {
types.insert(e.edge_type);
}
}
}
Ok(types
.into_iter()
.map(|t| str_row("relationshipType", t))
.collect())
}
fn builtin_db_constraints(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
let specs = reader.list_property_constraints()?;
Ok(specs
.into_iter()
.map(|spec| {
let mut row: ProcRow = HashMap::new();
row.insert("name".into(), Value::Property(Property::String(spec.name)));
let (scope_tag, target) = match spec.scope {
meshdb_storage::ConstraintScope::Node(l) => ("NODE", l),
meshdb_storage::ConstraintScope::Relationship(t) => ("RELATIONSHIP", t),
};
row.insert(
"scope".into(),
Value::Property(Property::String(scope_tag.into())),
);
row.insert("label".into(), Value::Property(Property::String(target)));
let props: Vec<Property> = spec.properties.into_iter().map(Property::String).collect();
row.insert("properties".into(), Value::Property(Property::List(props)));
row.insert(
"type".into(),
Value::Property(Property::String(spec.kind.as_string())),
);
row
})
.collect())
}
fn builtin_db_property_keys(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
let mut keys: BTreeSet<String> = BTreeSet::new();
for id in reader.all_node_ids()? {
if let Some(n) = reader.get_node(id)? {
for k in n.properties.keys() {
keys.insert(k.clone());
}
for (edge_id, _) in reader.outgoing(id)? {
if let Some(e) = reader.get_edge(edge_id)? {
for k in e.properties.keys() {
keys.insert(k.clone());
}
}
}
}
}
Ok(keys
.into_iter()
.map(|k| str_row("propertyKey", k))
.collect())
}
#[cfg(feature = "apoc-meta")]
fn builtin_apoc_meta_schema(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
use std::collections::HashSet;
let mut label_count: HashMap<String, i64> = HashMap::new();
let mut label_props: HashMap<String, HashMap<String, HashSet<&'static str>>> = HashMap::new();
let mut edge_count: HashMap<String, i64> = HashMap::new();
let mut edge_props: HashMap<String, HashMap<String, HashSet<&'static str>>> = HashMap::new();
for id in reader.all_node_ids()? {
let node = match reader.get_node(id)? {
Some(n) => n,
None => continue,
};
for label in &node.labels {
*label_count.entry(label.clone()).or_insert(0) += 1;
let per_label = label_props.entry(label.clone()).or_default();
for (k, v) in &node.properties {
per_label
.entry(k.clone())
.or_default()
.insert(apoc_schema_type(v));
}
}
for (edge_id, _) in reader.outgoing(id)? {
let edge = match reader.get_edge(edge_id)? {
Some(e) => e,
None => continue,
};
*edge_count.entry(edge.edge_type.clone()).or_insert(0) += 1;
let per_type = edge_props.entry(edge.edge_type.clone()).or_default();
for (k, v) in &edge.properties {
per_type
.entry(k.clone())
.or_default()
.insert(apoc_schema_type(v));
}
}
}
let mut node_indexed: HashMap<String, HashSet<String>> = HashMap::new();
for (label, props) in reader.list_property_indexes()? {
let entry = node_indexed.entry(label).or_default();
for p in props {
entry.insert(p);
}
}
let mut edge_indexed: HashMap<String, HashSet<String>> = HashMap::new();
for (edge_type, props) in reader.list_edge_property_indexes()? {
let entry = edge_indexed.entry(edge_type).or_default();
for p in props {
entry.insert(p);
}
}
let mut schema: HashMap<String, Property> = HashMap::new();
for (label, count) in label_count {
let props = label_props.remove(&label).unwrap_or_default();
let indexed = node_indexed.remove(&label).unwrap_or_default();
schema.insert(
label,
Property::Map(schema_entry(count, "node", props, Some(&indexed))),
);
}
for (edge_type, count) in edge_count {
let props = edge_props.remove(&edge_type).unwrap_or_default();
let indexed = edge_indexed.remove(&edge_type).unwrap_or_default();
schema.insert(
edge_type,
Property::Map(schema_entry(count, "relationship", props, Some(&indexed))),
);
}
let mut row = HashMap::new();
row.insert("value".to_string(), Value::Property(Property::Map(schema)));
Ok(vec![row])
}
#[cfg(feature = "apoc-meta")]
fn schema_entry(
count: i64,
type_tag: &str,
props: HashMap<String, std::collections::HashSet<&'static str>>,
indexed: Option<&std::collections::HashSet<String>>,
) -> HashMap<String, Property> {
let mut out: HashMap<String, Property> = HashMap::new();
out.insert("type".into(), Property::String(type_tag.into()));
out.insert("count".into(), Property::Int64(count));
let mut properties: HashMap<String, Property> = HashMap::new();
for (k, types) in props {
let mut sorted: Vec<&'static str> = types.into_iter().collect();
sorted.sort_unstable();
let ty = sorted.join("|");
let mut info: HashMap<String, Property> = HashMap::new();
info.insert("type".into(), Property::String(ty));
if let Some(idx) = indexed {
info.insert("indexed".into(), Property::Bool(idx.contains(&k)));
}
properties.insert(k, Property::Map(info));
}
out.insert("properties".into(), Property::Map(properties));
out
}
#[cfg(feature = "apoc-meta")]
fn apoc_schema_type(p: &Property) -> &'static str {
match p {
Property::Null => "NULL",
Property::String(_) => "STRING",
Property::Int64(_) => "INTEGER",
Property::Float64(_) => "FLOAT",
Property::Bool(_) => "BOOLEAN",
Property::List(_) => "LIST",
Property::Map(_) => "MAP",
Property::DateTime { .. } => "DATE_TIME",
Property::LocalDateTime(_) => "LOCAL_DATE_TIME",
Property::Date(_) => "DATE",
Property::Time { .. } => "TIME",
Property::Duration(_) => "DURATION",
Property::Point(_) => "POINT",
}
}
#[cfg(feature = "apoc-create")]
fn apoc_create_node(writer: &dyn GraphWriter, args: &[Value]) -> Result<Vec<ProcRow>> {
let labels = match &args[0] {
Value::Null | Value::Property(Property::Null) => Vec::new(),
Value::List(items) => items
.iter()
.map(|v| match v {
Value::Property(Property::String(s)) => Ok(s.clone()),
other => Err(Error::Procedure(format!(
"apoc.create.node: labels must be strings, got {other:?}"
))),
})
.collect::<Result<Vec<_>>>()?,
Value::Property(Property::List(items)) => items
.iter()
.map(|p| match p {
Property::String(s) => Ok(s.clone()),
other => Err(Error::Procedure(format!(
"apoc.create.node: labels must be strings, got {other:?}"
))),
})
.collect::<Result<Vec<_>>>()?,
other => {
return Err(Error::Procedure(format!(
"apoc.create.node: first argument must be a list of strings, got {other:?}"
)));
}
};
let props: HashMap<String, Property> = match &args[1] {
Value::Null | Value::Property(Property::Null) => HashMap::new(),
Value::Map(pairs) => pairs
.iter()
.map(|(k, v)| Ok((k.clone(), value_to_storable_property(v)?)))
.collect::<Result<HashMap<_, _>>>()?,
Value::Property(Property::Map(entries)) => entries
.iter()
.map(|(k, p)| (k.clone(), p.clone()))
.collect(),
other => {
return Err(Error::Procedure(format!(
"apoc.create.node: second argument must be a map, got {other:?}"
)));
}
};
let mut node = Node::new();
node.labels = labels;
for (k, p) in props {
if !matches!(p, Property::Null) {
node.properties.insert(k, p);
}
}
writer.put_node(&node)?;
let mut row = HashMap::new();
row.insert("node".to_string(), Value::Node(node));
Ok(vec![row])
}
#[cfg(feature = "apoc-create")]
fn apoc_create_relationship(writer: &dyn GraphWriter, args: &[Value]) -> Result<Vec<ProcRow>> {
let from = expect_node_id(&args[0], "first argument (from)")?;
let rel_type = match &args[1] {
Value::Property(Property::String(s)) => s.clone(),
Value::Null | Value::Property(Property::Null) => {
return Err(Error::Procedure(
"apoc.create.relationship: relationship type must not be null".into(),
));
}
other => {
return Err(Error::Procedure(format!(
"apoc.create.relationship: relationship type must be a string, got {other:?}"
)));
}
};
let props: HashMap<String, Property> = match &args[2] {
Value::Null | Value::Property(Property::Null) => HashMap::new(),
Value::Map(pairs) => pairs
.iter()
.map(|(k, v)| Ok((k.clone(), value_to_storable_property(v)?)))
.collect::<Result<HashMap<_, _>>>()?,
Value::Property(Property::Map(entries)) => entries
.iter()
.map(|(k, p)| (k.clone(), p.clone()))
.collect(),
other => {
return Err(Error::Procedure(format!(
"apoc.create.relationship: third argument must be a map, got {other:?}"
)));
}
};
let to = expect_node_id(&args[3], "fourth argument (to)")?;
let mut edge = Edge::new(rel_type, from, to);
for (k, p) in props {
if !matches!(p, Property::Null) {
edge.properties.insert(k, p);
}
}
writer.put_edge(&edge)?;
let mut row = HashMap::new();
row.insert("rel".to_string(), Value::Edge(edge));
Ok(vec![row])
}
#[cfg(feature = "apoc-refactor")]
fn apoc_refactor_set_type(
reader: &dyn GraphReader,
writer: &dyn GraphWriter,
args: &[Value],
) -> Result<Vec<ProcRow>> {
let old_id = match &args[0] {
Value::Edge(e) => e.id,
Value::Null | Value::Property(Property::Null) => {
return Err(Error::Procedure(
"apoc.refactor.setType: relationship argument must not be null".into(),
));
}
other => {
return Err(Error::Procedure(format!(
"apoc.refactor.setType: first argument must be a relationship, got {other:?}"
)));
}
};
let new_type = match &args[1] {
Value::Property(Property::String(s)) => s.clone(),
Value::Null | Value::Property(Property::Null) => {
return Err(Error::Procedure(
"apoc.refactor.setType: new type must not be null".into(),
));
}
other => {
return Err(Error::Procedure(format!(
"apoc.refactor.setType: new type must be a string, got {other:?}"
)));
}
};
let old = reader
.get_edge(old_id)?
.ok_or_else(|| Error::Procedure(format!("edge {old_id:?} no longer exists")))?;
if old.edge_type == new_type {
let mut row = HashMap::new();
row.insert("rel".to_string(), Value::Edge(old));
return Ok(vec![row]);
}
let mut new_edge = Edge::new(new_type, old.source, old.target);
new_edge.properties = old.properties.clone();
writer.delete_edge(old_id)?;
writer.put_edge(&new_edge)?;
let mut row = HashMap::new();
row.insert("rel".to_string(), Value::Edge(new_edge));
Ok(vec![row])
}
#[cfg(feature = "apoc-create")]
#[derive(Debug, Clone, Copy)]
enum LabelMode {
Add,
Remove,
Set,
}
#[cfg(feature = "apoc-create")]
fn apoc_label_mutator(
reader: &dyn GraphReader,
writer: &dyn GraphWriter,
args: &[Value],
mode: LabelMode,
) -> Result<Vec<ProcRow>> {
let node_ids = expect_node_or_node_list(&args[0], "first argument")?;
let labels = expect_string_list(&args[1], "second argument (labels)")?;
let mut out: Vec<ProcRow> = Vec::with_capacity(node_ids.len());
for nid in node_ids {
let mut node = reader
.get_node(nid)?
.ok_or_else(|| Error::Procedure(format!("node {nid:?} no longer exists")))?;
match mode {
LabelMode::Add => {
for l in &labels {
if !node.labels.iter().any(|existing| existing == l) {
node.labels.push(l.clone());
}
}
}
LabelMode::Remove => {
node.labels
.retain(|existing| !labels.iter().any(|l| l == existing));
}
LabelMode::Set => {
node.labels = labels.clone();
}
}
writer.put_node(&node)?;
let mut row = HashMap::new();
row.insert("node".to_string(), Value::Node(node));
out.push(row);
}
Ok(out)
}
#[cfg(feature = "apoc-create")]
fn apoc_set_node_property(
reader: &dyn GraphReader,
writer: &dyn GraphWriter,
args: &[Value],
) -> Result<Vec<ProcRow>> {
let node_ids = expect_node_or_node_list(&args[0], "first argument")?;
let key = expect_string(&args[1], "second argument (key)")?;
let value = value_to_storable_property(&args[2])?;
let mut out: Vec<ProcRow> = Vec::with_capacity(node_ids.len());
for nid in node_ids {
let mut node = reader
.get_node(nid)?
.ok_or_else(|| Error::Procedure(format!("node {nid:?} no longer exists")))?;
if matches!(value, Property::Null) {
node.properties.remove(&key);
} else {
node.properties.insert(key.clone(), value.clone());
}
writer.put_node(&node)?;
let mut row = HashMap::new();
row.insert("node".to_string(), Value::Node(node));
out.push(row);
}
Ok(out)
}
#[cfg(feature = "apoc-create")]
fn apoc_set_rel_property(
reader: &dyn GraphReader,
writer: &dyn GraphWriter,
args: &[Value],
) -> Result<Vec<ProcRow>> {
let edge_ids = expect_edge_or_edge_list(&args[0], "first argument")?;
let key = expect_string(&args[1], "second argument (key)")?;
let value = value_to_storable_property(&args[2])?;
let mut out: Vec<ProcRow> = Vec::with_capacity(edge_ids.len());
for eid in edge_ids {
let mut edge = reader
.get_edge(eid)?
.ok_or_else(|| Error::Procedure(format!("edge {eid:?} no longer exists")))?;
if matches!(value, Property::Null) {
edge.properties.remove(&key);
} else {
edge.properties.insert(key.clone(), value.clone());
}
writer.put_edge(&edge)?;
let mut row = HashMap::new();
row.insert("rel".to_string(), Value::Edge(edge));
out.push(row);
}
Ok(out)
}
#[cfg(feature = "apoc-create")]
fn expect_node_or_node_list(v: &Value, position: &str) -> Result<Vec<meshdb_core::NodeId>> {
let mut ids: Vec<meshdb_core::NodeId> = Vec::new();
collect_node_ids(v, position, &mut ids)?;
if ids.is_empty() {
return Err(Error::Procedure(format!(
"apoc.create.*: {position} resolved to zero nodes"
)));
}
Ok(ids)
}
#[cfg(feature = "apoc-create")]
fn collect_node_ids(v: &Value, position: &str, out: &mut Vec<meshdb_core::NodeId>) -> Result<()> {
match v {
Value::Node(n) => {
out.push(n.id);
Ok(())
}
Value::List(items) => {
for item in items {
collect_node_ids(item, position, out)?;
}
Ok(())
}
Value::Null | Value::Property(Property::Null) => Ok(()),
other => Err(Error::Procedure(format!(
"apoc.create.*: {position} must be a node or list of nodes, got {other:?}"
))),
}
}
#[cfg(feature = "apoc-create")]
fn expect_edge_or_edge_list(v: &Value, position: &str) -> Result<Vec<meshdb_core::EdgeId>> {
let mut ids: Vec<meshdb_core::EdgeId> = Vec::new();
collect_edge_ids(v, position, &mut ids)?;
if ids.is_empty() {
return Err(Error::Procedure(format!(
"apoc.create.*: {position} resolved to zero relationships"
)));
}
Ok(ids)
}
#[cfg(feature = "apoc-create")]
fn collect_edge_ids(v: &Value, position: &str, out: &mut Vec<meshdb_core::EdgeId>) -> Result<()> {
match v {
Value::Edge(e) => {
out.push(e.id);
Ok(())
}
Value::List(items) => {
for item in items {
collect_edge_ids(item, position, out)?;
}
Ok(())
}
Value::Null | Value::Property(Property::Null) => Ok(()),
other => Err(Error::Procedure(format!(
"apoc.create.*: {position} must be a relationship or list of relationships, got {other:?}"
))),
}
}
#[cfg(feature = "apoc-create")]
fn expect_string_list(v: &Value, position: &str) -> Result<Vec<String>> {
match v {
Value::Null | Value::Property(Property::Null) => Ok(Vec::new()),
Value::List(items) => items
.iter()
.map(|item| match item {
Value::Property(Property::String(s)) => Ok(s.clone()),
other => Err(Error::Procedure(format!(
"apoc.create.*: {position} must contain strings, got {other:?}"
))),
})
.collect(),
Value::Property(Property::List(items)) => items
.iter()
.map(|p| match p {
Property::String(s) => Ok(s.clone()),
other => Err(Error::Procedure(format!(
"apoc.create.*: {position} must contain strings, got {other:?}"
))),
})
.collect(),
other => Err(Error::Procedure(format!(
"apoc.create.*: {position} must be a list of strings, got {other:?}"
))),
}
}
#[cfg(feature = "apoc-create")]
fn expect_string(v: &Value, position: &str) -> Result<String> {
match v {
Value::Property(Property::String(s)) => Ok(s.clone()),
other => Err(Error::Procedure(format!(
"apoc.create.*: {position} must be a string, got {other:?}"
))),
}
}
#[cfg(feature = "apoc-create")]
fn expect_node_id(v: &Value, position: &str) -> Result<meshdb_core::NodeId> {
match v {
Value::Node(n) => Ok(n.id),
Value::Null | Value::Property(Property::Null) => Err(Error::Procedure(format!(
"apoc.create.relationship: {position} must be a node, got null"
))),
other => Err(Error::Procedure(format!(
"apoc.create.relationship: {position} must be a node, got {other:?}"
))),
}
}
#[cfg(feature = "apoc-create")]
fn value_to_storable_property(v: &Value) -> Result<Property> {
match v {
Value::Property(p) => Ok(p.clone()),
Value::Null => Ok(Property::Null),
Value::List(items) => {
let props = items
.iter()
.map(value_to_storable_property)
.collect::<Result<Vec<_>>>()?;
Ok(Property::List(props))
}
Value::Map(_) | Value::Node(_) | Value::Edge(_) | Value::Path { .. } => {
Err(Error::Procedure(
"apoc.create.node: property values can't be graph elements or graph-aware maps"
.into(),
))
}
}
}
fn values_equal_for_procedure(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Null, Value::Null) => true,
(Value::Null, _) | (_, Value::Null) => false,
(Value::Property(Property::Int64(x)), Value::Property(Property::Int64(y))) => x == y,
(Value::Property(Property::Float64(x)), Value::Property(Property::Float64(y))) => x == y,
(Value::Property(Property::Int64(i)), Value::Property(Property::Float64(f)))
| (Value::Property(Property::Float64(f)), Value::Property(Property::Int64(i))) => {
*f == (*i as f64)
}
(Value::Property(Property::String(x)), Value::Property(Property::String(y))) => x == y,
(Value::Property(Property::Bool(x)), Value::Property(Property::Bool(y))) => x == y,
_ => a == b,
}
}
#[derive(Debug, Clone, Default)]
pub struct ProcedureRegistry {
procs: HashMap<String, Procedure>,
}
impl ProcedureRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, proc: Procedure) {
let key = proc.qualified_name.join(".");
self.procs.insert(key, proc);
}
pub fn get(&self, qualified_name: &[String]) -> Option<&Procedure> {
self.procs.get(&qualified_name.join("."))
}
pub fn register_defaults(&mut self) {
self.register(Procedure {
qualified_name: vec!["db".into(), "labels".into()],
inputs: Vec::new(),
outputs: vec![ProcOutSpec {
name: "label".into(),
ty: ProcType::String,
}],
rows: Vec::new(),
builtin: Some(BuiltinProc::DbLabels),
});
self.register(Procedure {
qualified_name: vec!["db".into(), "relationshipTypes".into()],
inputs: Vec::new(),
outputs: vec![ProcOutSpec {
name: "relationshipType".into(),
ty: ProcType::String,
}],
rows: Vec::new(),
builtin: Some(BuiltinProc::DbRelationshipTypes),
});
self.register(Procedure {
qualified_name: vec!["db".into(), "propertyKeys".into()],
inputs: Vec::new(),
outputs: vec![ProcOutSpec {
name: "propertyKey".into(),
ty: ProcType::String,
}],
rows: Vec::new(),
builtin: Some(BuiltinProc::DbPropertyKeys),
});
self.register(Procedure {
qualified_name: vec!["db".into(), "constraints".into()],
inputs: Vec::new(),
outputs: vec![
ProcOutSpec {
name: "name".into(),
ty: ProcType::String,
},
ProcOutSpec {
name: "scope".into(),
ty: ProcType::String,
},
ProcOutSpec {
name: "label".into(),
ty: ProcType::String,
},
ProcOutSpec {
name: "properties".into(),
ty: ProcType::Any,
},
ProcOutSpec {
name: "type".into(),
ty: ProcType::String,
},
],
rows: Vec::new(),
builtin: Some(BuiltinProc::DbConstraints),
});
#[cfg(feature = "apoc-create")]
self.register(Procedure {
qualified_name: vec!["apoc".into(), "create".into(), "node".into()],
inputs: vec![
ProcArgSpec {
name: "labels".into(),
ty: ProcType::Any,
},
ProcArgSpec {
name: "props".into(),
ty: ProcType::Any,
},
],
outputs: vec![ProcOutSpec {
name: "node".into(),
ty: ProcType::Any,
}],
rows: Vec::new(),
builtin: Some(BuiltinProc::ApocCreateNode),
});
#[cfg(feature = "apoc-create")]
self.register(Procedure {
qualified_name: vec!["apoc".into(), "create".into(), "relationship".into()],
inputs: vec![
ProcArgSpec {
name: "from".into(),
ty: ProcType::Any,
},
ProcArgSpec {
name: "relType".into(),
ty: ProcType::String,
},
ProcArgSpec {
name: "props".into(),
ty: ProcType::Any,
},
ProcArgSpec {
name: "to".into(),
ty: ProcType::Any,
},
],
outputs: vec![ProcOutSpec {
name: "rel".into(),
ty: ProcType::Any,
}],
rows: Vec::new(),
builtin: Some(BuiltinProc::ApocCreateRelationship),
});
#[cfg(feature = "apoc-create")]
for (name, builtin) in [
("addLabels", BuiltinProc::ApocCreateAddLabels),
("removeLabels", BuiltinProc::ApocCreateRemoveLabels),
("setLabels", BuiltinProc::ApocCreateSetLabels),
] {
self.register(Procedure {
qualified_name: vec!["apoc".into(), "create".into(), name.into()],
inputs: vec![
ProcArgSpec {
name: "nodes".into(),
ty: ProcType::Any,
},
ProcArgSpec {
name: "labels".into(),
ty: ProcType::Any,
},
],
outputs: vec![ProcOutSpec {
name: "node".into(),
ty: ProcType::Any,
}],
rows: Vec::new(),
builtin: Some(builtin),
});
}
#[cfg(feature = "apoc-create")]
self.register(Procedure {
qualified_name: vec!["apoc".into(), "create".into(), "setProperty".into()],
inputs: vec![
ProcArgSpec {
name: "nodes".into(),
ty: ProcType::Any,
},
ProcArgSpec {
name: "key".into(),
ty: ProcType::String,
},
ProcArgSpec {
name: "value".into(),
ty: ProcType::Any,
},
],
outputs: vec![ProcOutSpec {
name: "node".into(),
ty: ProcType::Any,
}],
rows: Vec::new(),
builtin: Some(BuiltinProc::ApocCreateSetProperty),
});
#[cfg(feature = "apoc-create")]
self.register(Procedure {
qualified_name: vec!["apoc".into(), "create".into(), "setRelProperty".into()],
inputs: vec![
ProcArgSpec {
name: "rels".into(),
ty: ProcType::Any,
},
ProcArgSpec {
name: "key".into(),
ty: ProcType::String,
},
ProcArgSpec {
name: "value".into(),
ty: ProcType::Any,
},
],
outputs: vec![ProcOutSpec {
name: "rel".into(),
ty: ProcType::Any,
}],
rows: Vec::new(),
builtin: Some(BuiltinProc::ApocCreateSetRelProperty),
});
#[cfg(feature = "apoc-meta")]
self.register(Procedure {
qualified_name: vec!["apoc".into(), "meta".into(), "schema".into()],
inputs: Vec::new(),
outputs: vec![ProcOutSpec {
name: "value".into(),
ty: ProcType::Any,
}],
rows: Vec::new(),
builtin: Some(BuiltinProc::ApocMetaSchema),
});
#[cfg(feature = "apoc-refactor")]
self.register(Procedure {
qualified_name: vec!["apoc".into(), "refactor".into(), "setType".into()],
inputs: vec![
ProcArgSpec {
name: "rel".into(),
ty: ProcType::Any,
},
ProcArgSpec {
name: "newType".into(),
ty: ProcType::String,
},
],
outputs: vec![ProcOutSpec {
name: "rel".into(),
ty: ProcType::Any,
}],
rows: Vec::new(),
builtin: Some(BuiltinProc::ApocRefactorSetType),
});
}
}