use std::borrow::Cow;
use std::fmt;
use super::step_property::check_cardinality;
use super::step_property_map::{PropertyEntry, WriteTarget, write_entries};
use crate::graph::GraphStorage;
use crate::traversal::step::filter_steps::step_has::{edge_has, vertex_has};
use crate::traversal::step::flatmap_steps::step_choose::option_without_branch;
use crate::*;
pub(crate) const KEY_ID: &str = "T.id";
pub(crate) const KEY_LABEL: &str = "T.label";
pub(crate) const KEY_OUT: &str = "Direction.OUT";
pub(crate) const KEY_IN: &str = "Direction.IN";
pub(crate) const PLACEHOLDER_OUT: &str = "Merge.outV";
pub(crate) const PLACEHOLDER_IN: &str = "Merge.inV";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum MergeKind {
Vertex,
Edge,
}
impl MergeKind {
pub(crate) fn step(self) -> &'static str {
match self {
MergeKind::Vertex => "merge_v",
MergeKind::Edge => "merge_e",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum MergeRole {
Merge,
OnCreate,
OnMatch,
VertexLookup,
}
impl MergeRole {
pub(crate) fn argument(self) -> &'static str {
match self {
MergeRole::Merge => "the merge() map",
MergeRole::OnCreate => "the option(onCreate) map",
MergeRole::OnMatch => "the option(onMatch) map",
MergeRole::VertexLookup => "the option(outV/inV) map",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum EndpointRef {
Id(CompactString),
Placeholder,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct ParsedMap {
pub(crate) id: Option<CompactString>,
pub(crate) label: Option<CompactString>,
pub(crate) out_v: Option<EndpointRef>,
pub(crate) in_v: Option<EndpointRef>,
pub(crate) entries: Vec<PropertyEntry>,
pub(crate) rejected: Option<String>,
}
impl ParsedMap {
pub(crate) fn with_default_cardinality(mut self, default: Option<Cardinality>) -> Self {
if default.is_some() {
for entry in &mut self.entries {
entry.cardinality = entry.cardinality.or(default);
}
}
self
}
pub(crate) fn inherit(&self, on_create: &ParsedMap) -> ParsedMap {
let mut merged = self.clone();
merged.id = merged.id.or_else(|| on_create.id.clone());
merged.label = merged.label.or_else(|| on_create.label.clone());
merged.out_v = merged.out_v.or_else(|| on_create.out_v.clone());
merged.in_v = merged.in_v.or_else(|| on_create.in_v.clone());
for entry in &on_create.entries {
if !self.entries.iter().any(|own| own.key == entry.key) {
merged.entries.push(entry.clone());
}
}
merged
}
}
impl fmt::Display for ParsedMap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use crate::value::literal::{rhai_key, rhai_literal, rhai_string};
if let Some(rejected) = &self.rejected {
return f.write_str(rejected);
}
let mut parts: Vec<String> = Vec::new();
let quote = |text: &str| rhai_string(text);
if let Some(id) = &self.id {
parts.push(format!("\"{KEY_ID}\": {}", quote(id)));
}
if let Some(label) = &self.label {
parts.push(format!("\"{KEY_LABEL}\": {}", quote(label)));
}
for (key, endpoint, placeholder) in [
(KEY_OUT, &self.out_v, PLACEHOLDER_OUT),
(KEY_IN, &self.in_v, PLACEHOLDER_IN),
] {
match endpoint {
Some(EndpointRef::Id(id)) => parts.push(format!("\"{key}\": {}", quote(id))),
Some(EndpointRef::Placeholder) => parts.push(format!("\"{key}\": {placeholder}")),
None => {}
}
}
for entry in &self.entries {
match entry.cardinality {
Some(cardinality) => parts.push(format!(
"{}: Cardinality.{cardinality}({})",
rhai_key(&entry.key),
rhai_literal(&entry.value)
)),
None => parts.push(format!(
"{}: {}",
rhai_key(&entry.key),
rhai_literal(&entry.value)
)),
}
}
write!(f, "#{{{}}}", parts.join(", "))
}
}
pub(crate) enum MergeArg<'g, S: GraphStorage + 'g> {
Constant(ParsedMap),
Traversal(AnonymousTraversal<'g, S>),
}
impl<'g, S: GraphStorage + 'g> Clone for MergeArg<'g, S> {
fn clone(&self) -> Self {
match self {
MergeArg::Constant(map) => MergeArg::Constant(map.clone()),
MergeArg::Traversal(traversal) => MergeArg::Traversal(traversal.clone()),
}
}
}
impl<'g, S: GraphStorage + 'g> fmt::Display for MergeArg<'g, S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MergeArg::Constant(map) => write!(f, "{map}"),
MergeArg::Traversal(traversal) => write!(f, "{traversal}"),
}
}
}
#[derive(Clone, Debug)]
pub(crate) enum MergeProblem {
Invalid {
kind: MergeKind,
argument: &'static str,
reason: String,
},
Override {
kind: MergeKind,
key: String,
merge_value: String,
on_create_value: String,
},
EdgeCardinality {
kind: MergeKind,
cardinality: Cardinality,
},
}
impl MergeProblem {
pub(crate) fn invalid(kind: MergeKind, role: MergeRole, reason: impl Into<String>) -> Self {
Self::invalid_at(kind, role.argument(), reason)
}
pub(crate) fn invalid_at(
kind: MergeKind,
argument: &'static str,
reason: impl Into<String>,
) -> Self {
MergeProblem::Invalid {
kind,
argument,
reason: reason.into(),
}
}
}
impl From<MergeProblem> for BoxedTraverserError {
fn from(problem: MergeProblem) -> Self {
Box::new(match problem {
MergeProblem::Invalid {
kind,
argument,
reason,
} => TraverserError::InvalidMergeMap {
step: kind.step().to_string(),
argument: argument.to_string(),
reason,
},
MergeProblem::Override {
kind,
key,
merge_value,
on_create_value,
} => TraverserError::MergeOverride {
step: kind.step().to_string(),
key,
merge_value,
on_create_value,
},
MergeProblem::EdgeCardinality { kind, cardinality } => {
TraverserError::UnsupportedCardinality {
step: kind.step().to_string(),
cardinality,
on_edge: true,
}
}
})
}
}
fn type_name(value: &ElementProperty) -> String {
crate::graph::type_of(value)
.map(|ty| ty.to_string())
.unwrap_or_else(|| match value {
ElementProperty::Vertex(_) => "vertex".to_string(),
ElementProperty::Edge(_) => "edge".to_string(),
_ => "unknown".to_string(),
})
}
pub(crate) fn entries_of(
value: &ElementProperty,
kind: MergeKind,
role: MergeRole,
) -> Result<Vec<PropertyEntry>, MergeProblem> {
match value {
ElementProperty::Null => Ok(Vec::new()),
ElementProperty::Object(object) => Ok(object
.iter()
.map(|(key, value)| PropertyEntry {
key: key.clone(),
value: value.clone(),
cardinality: None,
})
.collect()),
other => Err(MergeProblem::invalid(
kind,
role,
format!("{}() expects a map, got {}", kind.step(), type_name(other)),
)),
}
}
pub(crate) fn parse(
map: &ElementProperty,
kind: MergeKind,
role: MergeRole,
) -> Result<ParsedMap, MergeProblem> {
parse_entries(entries_of(map, kind, role)?, kind, role)
}
pub(crate) fn id_text(value: &ElementProperty) -> Option<CompactString> {
match value {
ElementProperty::String(text) => Some(text.clone()),
ElementProperty::Number(number) => number
.as_i64()
.map(|int| compact_str::format_compact!("{int}")),
_ => None,
}
}
pub(crate) fn parse_entries(
entries: Vec<PropertyEntry>,
kind: MergeKind,
role: MergeRole,
) -> Result<ParsedMap, MergeProblem> {
let invalid = |reason: String| MergeProblem::invalid(kind, role, reason);
let mut parsed = ParsedMap::default();
for entry in entries {
let key = entry.key.as_str();
if key.starts_with('~') {
return Err(invalid(format!(
"Property key can not be a hidden key: {key}"
)));
}
let is_token = key == KEY_ID || key == KEY_LABEL || key == KEY_OUT || key == KEY_IN;
if is_token && role == MergeRole::OnMatch {
return Err(invalid(
"option(onMatch) cannot change T.id, T.label or the edge's vertices of an existing element".to_string(),
));
}
match key {
KEY_ID => match id_text(&entry.value) {
Some(id) => parsed.id = Some(id),
None => {
return Err(invalid(format!(
"T.id must be a string or an integer, got {}",
type_name(&entry.value)
)));
}
},
KEY_LABEL => match &entry.value {
ElementProperty::String(label) if label.starts_with('~') => {
return Err(invalid(format!("Label can not be a hidden key: {label}")));
}
ElementProperty::String(label) if !label.is_empty() => {
parsed.label = Some(label.clone())
}
other => {
return Err(invalid(format!(
"T.label must be a non-empty string, got {}",
type_name(other)
)));
}
},
KEY_OUT | KEY_IN => {
if kind == MergeKind::Vertex {
return Err(invalid(format!(
"merge_v() does not accept Direction keys ({key}); they name the vertices of an edge in merge_e()"
)));
}
if role == MergeRole::VertexLookup {
return Err(invalid(format!(
"a vertex lookup map does not accept Direction keys ({key})"
)));
}
let (own, other) = if key == KEY_OUT {
(PLACEHOLDER_OUT, PLACEHOLDER_IN)
} else {
(PLACEHOLDER_IN, PLACEHOLDER_OUT)
};
let endpoint = match &entry.value {
ElementProperty::String(text) if text == own => EndpointRef::Placeholder,
ElementProperty::String(text) if text == other => {
return Err(invalid(format!(
"{key} cannot take the placeholder {other}; use {own}"
)));
}
ElementProperty::Vertex(vertex) => {
EndpointRef::Id(CompactString::new(vertex.id()))
}
value => match id_text(value) {
Some(id) => EndpointRef::Id(id),
None => {
return Err(invalid(format!(
"{key} must be a vertex, a vertex id or {own}, got {}",
type_name(value)
)));
}
},
};
if key == KEY_OUT {
parsed.out_v = Some(endpoint);
} else {
parsed.in_v = Some(endpoint);
}
}
_ if key.starts_with("T.") || key.starts_with("Direction.") => {
return Err(invalid(format!(
"unsupported token key \"{key}\": a merge map takes only the token keys \"T.id\", \"T.label\", \"Direction.OUT\" and \"Direction.IN\""
)));
}
_ => {
if let (MergeKind::Edge, Some(cardinality)) = (kind, entry.cardinality) {
return Err(MergeProblem::EdgeCardinality { kind, cardinality });
}
parsed.entries.push(entry);
}
}
}
Ok(parsed)
}
pub(crate) fn check_override(
kind: MergeKind,
merge: &ParsedMap,
on_create: &ParsedMap,
) -> Result<(), MergeProblem> {
let conflict =
|key: &str, merge_value: String, on_create_value: String| MergeProblem::Override {
kind,
key: key.to_string(),
merge_value,
on_create_value,
};
let quoted = |text: &str| format!("\"{text}\"");
if let (Some(a), Some(b)) = (&merge.id, &on_create.id)
&& a != b
{
return Err(conflict(KEY_ID, quoted(a), quoted(b)));
}
if let (Some(a), Some(b)) = (&merge.label, &on_create.label)
&& a != b
{
return Err(conflict(KEY_LABEL, quoted(a), quoted(b)));
}
for (key, a, b, placeholder) in [
(KEY_OUT, &merge.out_v, &on_create.out_v, PLACEHOLDER_OUT),
(KEY_IN, &merge.in_v, &on_create.in_v, PLACEHOLDER_IN),
] {
if let (Some(a), Some(b)) = (a, b)
&& a != b
{
let render = |endpoint: &EndpointRef| match endpoint {
EndpointRef::Id(id) => quoted(id),
EndpointRef::Placeholder => placeholder.to_string(),
};
return Err(conflict(key, render(a), render(b)));
}
}
for entry in &on_create.entries {
if let Some(own) = merge.entries.iter().find(|own| own.key == entry.key)
&& own.value != entry.value
{
return Err(conflict(
&entry.key,
crate::value::literal::rhai_literal(&own.value),
crate::value::literal::rhai_literal(&entry.value),
));
}
}
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Lookup {
Id,
Label,
Scan,
OutEdges,
InEdges,
}
impl Lookup {
pub(crate) fn of(map: &ParsedMap) -> Self {
if map.id.is_some() {
Lookup::Id
} else if map.label.is_some() {
Lookup::Label
} else {
Lookup::Scan
}
}
pub(crate) fn of_edge(map: &ParsedMap) -> Self {
if map.out_v.is_some() {
Lookup::OutEdges
} else if map.in_v.is_some() {
Lookup::InEdges
} else {
Lookup::of(map)
}
}
}
impl fmt::Display for Lookup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Lookup::Id => "id",
Lookup::Label => "label",
Lookup::Scan => "scan",
Lookup::OutEdges => "out-edges",
Lookup::InEdges => "in-edges",
})
}
}
pub(crate) fn find_vertices<S: GraphStorage>(storage: &S, map: &ParsedMap) -> Vec<S::VertexId> {
let matches = |node: &S::VertexId| {
map.label
.as_deref()
.is_none_or(|label| storage.vertex_has_label(*node, label))
&& map
.entries
.iter()
.all(|entry| vertex_has(storage, *node, &entry.key, &entry.value))
};
match Lookup::of(map) {
Lookup::Id => storage
.get_vertex_by_id(map.id.as_deref().unwrap_or_default())
.into_iter()
.filter(matches)
.collect(),
Lookup::Label => storage
.vertices_by_label(map.label.as_deref().unwrap_or_default())
.filter(matches)
.collect(),
Lookup::Scan | Lookup::OutEdges | Lookup::InEdges => {
storage.vertices().filter(matches).collect()
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Endpoint<V> {
Vertex(V),
Unknown(CompactString),
}
pub(crate) fn find_edges<S: GraphStorage>(
storage: &S,
map: &ParsedMap,
out_v: Option<&Endpoint<S::VertexId>>,
in_v: Option<&Endpoint<S::VertexId>>,
) -> Vec<S::EdgeId> {
let vertex = |endpoint: Option<&Endpoint<S::VertexId>>| match endpoint {
Some(Endpoint::Vertex(node)) => Ok(Some(*node)),
Some(Endpoint::Unknown(_)) => Err(()),
None => Ok(None),
};
let (Ok(out_v), Ok(in_v)) = (vertex(out_v), vertex(in_v)) else {
return Vec::new();
};
let matches = |edge: &S::EdgeId| {
let endpoints = storage.edge_endpoints(*edge);
map.id
.as_deref()
.is_none_or(|id| storage.edge_id(*edge) == Some(id))
&& map
.label
.as_deref()
.is_none_or(|label| storage.edge_label(*edge) == Some(label))
&& out_v.is_none_or(|node| endpoints.is_some_and(|(from, _)| from == node))
&& in_v.is_none_or(|node| endpoints.is_some_and(|(_, to)| to == node))
&& map
.entries
.iter()
.all(|entry| edge_has(storage, *edge, &entry.key, &entry.value))
};
match (out_v, in_v, Lookup::of_edge(map)) {
(Some(node), _, _) => storage.out_edges(node).filter(matches).collect(),
(None, Some(node), _) => storage.in_edges(node).filter(matches).collect(),
(None, None, Lookup::Id) => storage
.get_edge_by_id(map.id.as_deref().unwrap_or_default())
.into_iter()
.filter(matches)
.collect(),
(None, None, Lookup::Label) => storage
.edges_by_label(map.label.as_deref().unwrap_or_default())
.filter(matches)
.collect(),
(None, None, Lookup::Scan | Lookup::OutEdges | Lookup::InEdges) => {
storage.edges().filter(matches).collect()
}
}
}
pub(crate) fn create_edge<S: GraphStorage>(
storage: &mut S,
map: &ParsedMap,
out_v: S::VertexId,
in_v: S::VertexId,
create: &crate::graph::schema::defaults::CreateContext,
) -> TraverserResult<S::EdgeId> {
let properties: PropertyMap = map
.entries
.iter()
.map(|entry| (entry.key.clone(), entry.value.clone()))
.collect();
Ok(storage.add_edge_with(
map.id.clone(),
map.label.clone(),
out_v,
in_v,
properties,
create,
)?)
}
pub(crate) fn apply_entries<S: GraphStorage>(
storage: &mut S,
target: WriteTarget<S>,
entries: &[PropertyEntry],
default: Option<Cardinality>,
kind: MergeKind,
) -> TraverserResult<()> {
write_entries(storage, target, entries, default, kind.step())
}
pub(crate) fn create_vertex<S: GraphStorage>(
storage: &mut S,
map: &ParsedMap,
create: &crate::graph::schema::defaults::CreateContext,
) -> TraverserResult<S::VertexId> {
for entry in &map.entries {
check_cardinality(MergeKind::Vertex.step(), entry.cardinality, false)?;
}
let properties: PropertyMap = map
.entries
.iter()
.map(|entry| (entry.key.clone(), entry.value.clone()))
.collect();
let labels = map.label.iter().cloned().collect();
Ok(storage.add_vertex_with(map.id.clone(), labels, properties, create)?)
}
pub(crate) enum MergeInput<'g, S: GraphStorage + 'g> {
Value(ElementProperty),
Entries(Vec<PropertyEntry>),
Traversal(AnonymousTraversal<'g, S>),
}
impl<'g, S: GraphStorage + 'g> MergeInput<'g, S> {
pub(crate) fn into_arg(
self,
kind: MergeKind,
role: MergeRole,
default: Option<Cardinality>,
) -> (MergeArg<'g, S>, Option<MergeProblem>) {
let (parsed, written): (_, Box<dyn FnOnce() -> String>) = match self {
MergeInput::Traversal(traversal) => return (MergeArg::Traversal(traversal), None),
MergeInput::Value(value) => {
let parsed = parse(&value, kind, role);
(
parsed,
Box::new(move || crate::value::literal::rhai_literal(&value)),
)
}
MergeInput::Entries(entries) => {
let as_written = ParsedMap {
entries: entries.clone(),
..ParsedMap::default()
};
(
parse_entries(entries, kind, role),
Box::new(move || as_written.to_string()),
)
}
};
let parsed = parsed.and_then(|map| {
let map = map.with_default_cardinality(default);
if kind == MergeKind::Edge
&& let Some(cardinality) = default
{
return Err(MergeProblem::EdgeCardinality { kind, cardinality });
}
Ok(map)
});
match parsed {
Ok(map) => (MergeArg::Constant(map), None),
Err(problem) => (
MergeArg::Constant(ParsedMap {
rejected: Some(written()),
..ParsedMap::default()
}),
Some(problem),
),
}
}
}
pub(crate) trait MergeStep<'g, S: GraphStorage + 'g> {
fn set_option(
&mut self,
token: Merge,
input: MergeInput<'g, S>,
cardinality: Option<Cardinality>,
);
}
pub(crate) struct MergeOption<'g, S: GraphStorage + 'g> {
pub(crate) arg: MergeArg<'g, S>,
pub(crate) cardinality: Option<Cardinality>,
}
impl<'g, S: GraphStorage + 'g> Clone for MergeOption<'g, S> {
fn clone(&self) -> Self {
Self {
arg: self.arg.clone(),
cardinality: self.cardinality,
}
}
}
pub(crate) fn evaluate_map<'g, S: GraphStorage + 'g>(
traversal: &AnonymousTraversal<'g, S>,
context: &mut TraverserContext<'_, S>,
traverser: &Traverser<S>,
kind: MergeKind,
role: MergeRole,
) -> TraverserResult<Option<ParsedMap>> {
match traversal.first(context, traverser)? {
Some(first) => {
let value = first.value.into_node_property(context.storage)?;
Ok(Some(parse(&value, kind, role)?))
}
None => Ok(None),
}
}
pub(crate) fn merge_map<'a, 'g, S: GraphStorage + 'g>(
merge: &'a MergeArg<'g, S>,
context: &mut TraverserContext<'_, S>,
traverser: &Traverser<S>,
kind: MergeKind,
) -> TraverserResult<Cow<'a, ParsedMap>> {
match merge {
MergeArg::Constant(map) => Ok(Cow::Borrowed(map)),
MergeArg::Traversal(traversal) => {
match evaluate_map(traversal, context, traverser, kind, MergeRole::Merge)? {
Some(map) => Ok(Cow::Owned(map)),
None => Err(MergeProblem::invalid(
kind,
MergeRole::Merge,
format!("{}() traversal produced no map", kind.step()),
)
.into()),
}
}
}
}
pub(crate) fn option_map<'a, 'g, S: GraphStorage + 'g>(
option: &'a Option<MergeOption<'g, S>>,
context: &mut TraverserContext<'_, S>,
traverser: &Traverser<S>,
kind: MergeKind,
role: MergeRole,
) -> TraverserResult<Option<Cow<'a, ParsedMap>>> {
let Some(option) = option else {
return Ok(None);
};
match &option.arg {
MergeArg::Constant(map) => Ok(Some(Cow::Borrowed(map))),
MergeArg::Traversal(traversal) => {
Ok(evaluate_map(traversal, context, traverser, kind, role)?
.map(|map| Cow::Owned(map.with_default_cardinality(option.cardinality))))
}
}
}
pub(crate) fn first_problem<'g, S: GraphStorage + 'g>(
kind: MergeKind,
problems: &[&Option<MergeProblem>],
merge: &MergeArg<'g, S>,
on_create: &Option<MergeOption<'g, S>>,
) -> Option<MergeProblem> {
if let Some(problem) = problems.iter().find_map(|problem| problem.as_ref()) {
return Some(problem.clone());
}
if let (
MergeArg::Constant(merge),
Some(MergeOption {
arg: MergeArg::Constant(on_create),
..
}),
) = (merge, on_create)
{
return check_override(kind, merge, on_create).err();
}
None
}
pub(crate) fn attach_merge_option<'g, S: GraphStorage + 'g>(
steps: &mut Vec<GraphStep<'g, S>>,
token: Merge,
input: MergeInput<'g, S>,
cardinality: Option<Cardinality>,
) {
let target = steps
.iter_mut()
.rev()
.find(|step| !matches!(step, GraphStep::Modulator(_, _) | GraphStep::As(_, _)));
if let Some(GraphStep::SideEffectMut(_, step)) = target
&& let Some(merge) = step.as_merge_mut()
{
merge.set_option(token, input, cardinality);
return;
}
let rendered = match &input {
MergeInput::Value(value) => format!(
"option(Merge.{token}, {})",
crate::value::literal::rhai_literal(value)
),
MergeInput::Entries(entries) => {
let map = ParsedMap {
entries: entries.clone(),
..ParsedMap::default()
};
format!("option(Merge.{token}, {map})")
}
MergeInput::Traversal(traversal) => format!("option(Merge.{token}, {traversal})"),
};
steps.push(option_without_branch(rendered));
}