use std::collections::HashMap;
use super::CypherExecutor;
use crate::datatypes::values::Value;
use crate::graph::languages::cypher::ast::CreateNodePattern;
use crate::graph::languages::cypher::result::ResultRow;
use crate::graph::schema::DirGraph;
use crate::graph::storage::GraphRead;
#[derive(Default)]
pub(super) struct IdentityAliases {
id: Option<String>,
title: Option<String>,
}
impl IdentityAliases {
pub(super) fn for_type(graph: &DirGraph, node_type: &str) -> Self {
if graph.id_field_aliases.is_empty() && graph.title_field_aliases.is_empty() {
return Self::default();
}
Self {
id: graph.id_field_aliases.get(node_type).cloned(),
title: graph.title_field_aliases.get(node_type).cloned(),
}
}
pub(super) fn id_field(&self) -> Option<&str> {
self.id.as_deref()
}
pub(super) fn title_field(&self) -> Option<&str> {
self.title.as_deref()
}
pub(super) fn canonical<'a>(&self, property: &'a str) -> &'a str {
if self.id.as_deref() == Some(property) {
return "id";
}
if self.title.as_deref() == Some(property) {
return "title";
}
property
}
}
pub(super) struct CreatedIdentity {
pub(super) id: Value,
pub(super) title: Value,
pub(super) title_supplied: bool,
}
pub(super) fn create_identity(
graph: &mut DirGraph,
node_pat: &CreateNodePattern,
label: &str,
aliases: &IdentityAliases,
properties: &mut HashMap<String, Value>,
) -> Result<CreatedIdentity, String> {
let aliased_id = aliases
.id
.as_deref()
.and_then(|alias| properties.remove(alias));
let literal_id = properties.remove("id");
if let (Some(aliased), Some(literal)) = (&aliased_id, &literal_id) {
if aliased != literal {
let alias = aliases.id.as_deref().unwrap_or("id");
return Err(format!(
"CREATE gives node type '{label}' two different identities: '{alias}' is \
its declared id field (value {aliased}) and 'id' is the identity spelling \
every type accepts (value {literal}). Both name the same field, so supply \
one of them."
));
}
}
let id = match aliased_id.or(literal_id) {
Some(explicit) => {
graph.observe_explicit_id(&explicit);
explicit
}
None => graph.next_auto_node_id(),
};
let supplied_title = aliases
.title
.as_deref()
.and_then(|alias| properties.remove(alias))
.or_else(|| {
properties
.get("name")
.or_else(|| properties.get("title"))
.cloned()
});
let title_supplied = supplied_title.is_some();
let title = supplied_title.unwrap_or_else(|| {
let label = node_pat.label.as_deref().unwrap_or("Node");
Value::String(format!("{}_{}", label, graph.graph.node_bound()))
});
Ok(CreatedIdentity {
id,
title,
title_supplied,
})
}
pub(super) fn check_identity_uniqueness(
graph: &mut DirGraph,
label: &str,
id: &Value,
) -> Result<(), String> {
let primary_key_on_id = graph.primary_key_for(label) == Some("id");
let durable = graph.graph.is_wal_owner();
if !(primary_key_on_id || durable) || graph.lookup_by_id_readonly(label, id).is_none() {
return Ok(());
}
Err(if primary_key_on_id {
format!(
"duplicate primary key: node type '{label}' declares a primary key and a \
node with id {id} already exists. Use MERGE to upsert instead of CREATE, \
or remove the duplicate."
)
} else {
format!(
"duplicate id in a durable graph: node type '{label}' already has a node \
with id {id}, and the write-ahead log identifies every node by its \
(type, id) — a second one could not be recovered, so reopening the graph \
would merge the two and lose a node. Use MERGE to upsert, give the new \
node a distinct id, or declare a primary key (define_schema) to enforce \
this in every storage mode."
)
})
}
pub(super) fn remove_write_field<'a>(
graph: &DirGraph,
node_type: &str,
property: &'a str,
) -> Result<&'a str, String> {
match IdentityAliases::for_type(graph, node_type).canonical(property) {
"id" => Err(format!(
"Cannot REMOVE node id — it is immutable ('{property}' is the id field of \
node type '{node_type}', so it names the identity)"
)),
"title" => Ok("title"),
other => Ok(other),
}
}
pub(super) fn merge_expected_props<'p>(
executor: &CypherExecutor<'_>,
node_pat: &'p CreateNodePattern,
row: &ResultRow,
graph: &DirGraph,
) -> Result<Vec<(&'p str, Value)>, String> {
let aliases = IdentityAliases::for_type(graph, node_pat.label.as_deref().unwrap_or("Node"));
node_pat
.properties
.iter()
.map(|(key, expr)| {
executor
.evaluate_expression(expr, row)
.map(|val| (aliases.canonical(key.as_str()), val))
})
.collect()
}