use crate::datatypes::values::Value;
use super::event::{CdcChange, CdcEvent, CdcEventKind, EdgeState, NodeState};
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CdcSelector {
element_type: Option<ElementType>,
operation: Option<CdcEventKind>,
node_type: Option<String>,
relationship_type: Option<String>,
src_type: Option<String>,
tgt_type: Option<String>,
node_id: Option<Value>,
src_id: Option<Value>,
tgt_id: Option<Value>,
labels: Vec<String>,
changes_to: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ElementType {
Node,
Relationship,
}
const ACCEPTED_KEYS: &[&str] = &[
"elementType",
"operation",
"nodeType",
"relationshipType",
"srcType",
"tgtType",
"nodeId",
"srcId",
"tgtId",
"labels",
"changesTo",
];
pub fn parse_selectors(value: &Value) -> Result<Vec<CdcSelector>, String> {
let Value::List(items) = value else {
return Err(format!(
"db.cdc.query: 'selectors' must be a list of maps, got {value:?}. Each map is one \
filter and an event matches if *any* of them matches it, so \
[{{operation: 'delete'}}, {{nodeType: 'Person'}}] reads every delete plus every \
change to a Person."
));
};
items.iter().map(parse_selector).collect()
}
fn parse_selector(value: &Value) -> Result<CdcSelector, String> {
let Value::Map(map) = value else {
return Err(format!(
"db.cdc.query: every entry in 'selectors' must be a map of constraints, got \
{value:?}."
));
};
if map.is_empty() {
return Err(
"db.cdc.query: an empty selector map constrains nothing, so it would match every \
change while looking like a filter — which is what a selector built from an empty \
set of conditions produces. Omit 'selectors' entirely to read everything."
.to_string(),
);
}
let mut selector = CdcSelector::default();
for (key, value) in map.iter() {
match key {
"elementType" => {
selector.element_type = Some(match string_of(key, value)?.as_str() {
"node" => ElementType::Node,
"relationship" => ElementType::Relationship,
other => {
return Err(format!(
"db.cdc.query: 'elementType' must be 'node' or 'relationship', got \
'{other}'. These are the values the elementType column reports."
))
}
});
}
"operation" => {
selector.operation = Some(match string_of(key, value)?.as_str() {
"create" => CdcEventKind::Create,
"update" => CdcEventKind::Update,
"delete" => CdcEventKind::Delete,
other => {
return Err(format!(
"db.cdc.query: 'operation' must be 'create', 'update' or 'delete', \
got '{other}'. These are the values the operation column reports — \
KGLite does not use Neo4j's single-letter 'c'/'u'/'d' spelling on \
either side."
))
}
});
}
"nodeType" => selector.node_type = Some(string_of(key, value)?),
"relationshipType" => selector.relationship_type = Some(string_of(key, value)?),
"srcType" => selector.src_type = Some(string_of(key, value)?),
"tgtType" => selector.tgt_type = Some(string_of(key, value)?),
"nodeId" => selector.node_id = Some(scalar_of(key, value)?),
"srcId" => selector.src_id = Some(scalar_of(key, value)?),
"tgtId" => selector.tgt_id = Some(scalar_of(key, value)?),
"labels" => selector.labels = string_list_of(key, value)?,
"changesTo" => selector.changes_to = string_list_of(key, value)?,
other => {
return Err(format!(
"db.cdc.query: unknown selector key '{other}'. Accepted: {}.",
ACCEPTED_KEYS.join(", ")
))
}
}
}
Ok(selector)
}
fn string_of(key: &str, value: &Value) -> Result<String, String> {
match value {
Value::String(text) => Ok(text.clone()),
other => Err(format!(
"db.cdc.query: selector key '{key}' must be a string, got {other:?}."
)),
}
}
fn scalar_of(key: &str, value: &Value) -> Result<Value, String> {
match value {
Value::List(_) | Value::Map(_) | Value::Null => Err(format!(
"db.cdc.query: selector key '{key}' must be a single id value, got {value:?}."
)),
other => Ok(other.clone()),
}
}
fn string_list_of(key: &str, value: &Value) -> Result<Vec<String>, String> {
let Value::List(items) = value else {
return Err(format!(
"db.cdc.query: selector key '{key}' must be a list of strings, got {value:?}."
));
};
if items.is_empty() {
return Err(format!(
"db.cdc.query: selector key '{key}' was given an empty list, which constrains \
nothing. Drop the key instead."
));
}
items.iter().map(|item| string_of(key, item)).collect()
}
pub fn selected(selectors: &[CdcSelector], event: &CdcEvent) -> bool {
selectors.is_empty() || selectors.iter().any(|selector| selector.matches(event))
}
pub fn needs_before_images(selectors: &[CdcSelector]) -> bool {
selectors
.iter()
.any(|selector| !selector.changes_to.is_empty())
}
impl CdcSelector {
fn matches(&self, event: &CdcEvent) -> bool {
if let Some(operation) = self.operation {
if event.kind != operation {
return false;
}
}
match &event.change {
CdcChange::Node {
node_type,
id,
before,
after,
} => {
self.element_type != Some(ElementType::Relationship)
&& self.relationship_type.is_none()
&& self.src_type.is_none()
&& self.tgt_type.is_none()
&& self.src_id.is_none()
&& self.tgt_id.is_none()
&& matches_opt(&self.node_type, node_type)
&& self.node_id.as_ref().is_none_or(|want| ids_equal(want, id))
&& self.labels_match(before.as_ref(), after.as_ref())
&& self.changes_match(
before.as_ref().map(node_properties),
after.as_ref().map(node_properties),
)
}
CdcChange::Edge {
conn_type,
src_type,
src_id,
tgt_type,
tgt_id,
before,
after,
} => {
self.element_type != Some(ElementType::Node)
&& self.node_type.is_none()
&& self.node_id.is_none()
&& self.labels.is_empty()
&& matches_opt(&self.relationship_type, conn_type)
&& matches_opt(&self.src_type, src_type)
&& matches_opt(&self.tgt_type, tgt_type)
&& self.src_id.as_ref().is_none_or(|want| ids_equal(want, src_id))
&& self.tgt_id.as_ref().is_none_or(|want| ids_equal(want, tgt_id))
&& self.changes_match(
before.as_ref().map(edge_properties),
after.as_ref().map(edge_properties),
)
}
}
}
fn labels_match(&self, before: Option<&NodeState>, after: Option<&NodeState>) -> bool {
if self.labels.is_empty() {
return true;
}
let Some(state) = after.or(before) else {
return false;
};
self.labels
.iter()
.all(|wanted| state.labels.contains(wanted))
}
fn changes_match(
&self,
before: Option<Vec<(&String, &Value)>>,
after: Option<Vec<(&String, &Value)>>,
) -> bool {
if self.changes_to.is_empty() {
return true;
}
let before = before.unwrap_or_default();
let after = after.unwrap_or_default();
self.changes_to.iter().any(|name| {
let was = lookup(&before, name);
let now = lookup(&after, name);
match (was, now) {
(None, None) => false,
(Some(a), Some(b)) => !values_equal(a, b),
_ => true,
}
})
}
}
fn node_properties(state: &NodeState) -> Vec<(&String, &Value)> {
state.properties.iter().map(|(k, v)| (k, v)).collect()
}
fn edge_properties(state: &EdgeState) -> Vec<(&String, &Value)> {
state.properties.iter().map(|(k, v)| (k, v)).collect()
}
fn lookup<'a>(properties: &[(&'a String, &'a Value)], name: &str) -> Option<&'a Value> {
properties
.iter()
.find(|(key, _)| key.as_str() == name)
.map(|(_, value)| *value)
}
fn matches_opt(wanted: &Option<String>, actual: &str) -> bool {
wanted.as_ref().is_none_or(|want| want == actual)
}
fn ids_equal(wanted: &Value, actual: &Value) -> bool {
wanted == actual
}
fn values_equal(a: &Value, b: &Value) -> bool {
a == b
}