use super::*;
pub(super) fn read_edge(row: &rusqlite::Row<'_>) -> Result<Edge, rusqlite::Error> {
let namespace: String = row.get(0)?;
let id_str: String = row.get(1)?;
let source_str: String = row.get(2)?;
let target_str: String = row.get(3)?;
let relation_str: String = row.get(4)?;
let weight: f64 = row.get(5)?;
let created_micros: i64 = row.get(6)?;
let updated_micros: i64 = row.get(7)?;
let deleted_micros: Option<i64> = row.get(8)?;
let metadata_str: Option<String> = row.get(9)?;
let target_backend: Option<String> = row.get(10)?;
let id = parse_uuid(&id_str)?;
let source_id = parse_uuid(&source_str)?;
let target_id = parse_uuid(&target_str)?;
let created_at = micros_to_datetime(created_micros);
let relation = relation_str.parse::<EdgeRelation>().map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Text, Box::new(e))
})?;
let metadata = match metadata_str {
Some(s) => {
let v = serde_json::from_str(&s).map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
9,
rusqlite::types::Type::Text,
Box::new(e),
)
})?;
Some(v)
}
None => None,
};
Ok(Edge {
id: id.into(),
namespace,
source_id,
target_id,
relation,
weight,
created_at,
updated_at: micros_to_datetime(updated_micros),
deleted_at: deleted_micros.map(micros_to_datetime),
metadata,
target_backend,
})
}
pub(super) fn parse_uuid(s: &str) -> Result<Uuid, rusqlite::Error> {
Uuid::parse_str(s).map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(e))
})
}
pub(super) fn neighbor_extra_clause(
query: &NeighborQuery,
start_param_idx: usize,
after: Option<&NeighborCursor>,
neighbor_kinds: Option<&[String]>,
) -> (String, String, Vec<Box<dyn rusqlite::types::ToSql>>) {
let mut conditions: Vec<String> = Vec::new();
let mut extra_params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut param_idx = start_param_idx;
if let Some(ref rels) = query.relations {
if !rels.is_empty() {
let placeholders: Vec<String> = rels
.iter()
.map(|r| {
extra_params.push(Box::new(r.to_string()));
let p = format!("?{}", param_idx);
param_idx += 1;
p
})
.collect();
conditions.push(format!("relation IN ({})", placeholders.join(",")));
}
}
if let Some(min_w) = query.min_weight {
extra_params.push(Box::new(min_w));
conditions.push(format!("weight >= ?{}", param_idx));
param_idx += 1;
}
if let Some(cursor) = after {
extra_params.push(Box::new(cursor.weight));
let weight_idx = param_idx;
param_idx += 1;
extra_params.push(Box::new(cursor.node_id.to_string()));
let node_idx = param_idx;
param_idx += 1;
extra_params.push(Box::new(cursor.edge_id.to_string()));
let edge_idx = param_idx;
param_idx += 1;
conditions.push(format!(
"(weight < ?{weight_idx} OR (weight = ?{weight_idx} AND node_id > ?{node_idx}) OR (weight = ?{weight_idx} AND node_id = ?{node_idx} AND edge_id > ?{edge_idx}))"
));
}
if let Some(kinds) = neighbor_kinds.filter(|kinds| !kinds.is_empty()) {
let placeholders: Vec<String> = kinds
.iter()
.map(|kind| {
extra_params.push(Box::new(kind.clone()));
let p = format!("?{param_idx}");
param_idx += 1;
p
})
.collect();
let entity_placeholders = placeholders.join(",");
let note_placeholders: Vec<String> = kinds
.iter()
.map(|kind| {
extra_params.push(Box::new(kind.clone()));
let p = format!("?{param_idx}");
param_idx += 1;
p
})
.collect();
conditions.push(format!(
"(EXISTS (SELECT 1 FROM entities AS neighbor_entities WHERE neighbor_entities.id = node_id AND neighbor_entities.namespace = ?1 AND neighbor_entities.deleted_at IS NULL AND neighbor_entities.kind IN ({entity_placeholders})) OR EXISTS (SELECT 1 FROM notes AS neighbor_notes WHERE neighbor_notes.id = node_id AND neighbor_notes.namespace = ?1 AND neighbor_notes.deleted_at IS NULL AND neighbor_notes.kind IN ({})))",
note_placeholders.join(",")
));
}
let where_extra = if conditions.is_empty() {
String::new()
} else {
format!(" WHERE {}", conditions.join(" AND "))
};
let limit_clause = if let Some(lim) = query.limit {
extra_params.push(Box::new(lim as i64));
format!(" LIMIT ?{}", param_idx)
} else {
String::new()
};
(where_extra, limit_clause, extra_params)
}
#[cfg(test)]
pub(super) static NEIGHBOR_SELECT_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
pub(super) fn count_neighbor_select() {
NEIGHBOR_SELECT_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
#[cfg(not(test))]
pub(super) fn count_neighbor_select() {}
#[cfg(test)]
pub(crate) fn reset_neighbor_select_count() {
NEIGHBOR_SELECT_COUNT.store(0, std::sync::atomic::Ordering::Relaxed);
}
#[cfg(test)]
pub(crate) fn neighbor_select_count() -> usize {
NEIGHBOR_SELECT_COUNT.load(std::sync::atomic::Ordering::Relaxed)
}
pub(super) fn micros_to_datetime(micros: i64) -> DateTime<Utc> {
Utc.timestamp_micros(micros)
.single()
.unwrap_or_else(Utc::now)
}
pub(super) fn edge_order_clause(sort: &[SortOrder<EdgeSortField>]) -> String {
if sort.is_empty() {
return " ORDER BY created_at DESC, id DESC".to_string();
}
let mut parts: Vec<String> = sort
.iter()
.map(|s| {
let dir = match s.direction {
SortDirection::Asc => "ASC",
SortDirection::Desc => "DESC",
};
format!("{} {}", edge_sort_col(&s.field), dir)
})
.collect();
let dir = match sort.last().map(|s| &s.direction) {
Some(SortDirection::Asc) => "ASC",
_ => "DESC",
};
parts.push(format!("id {dir}"));
format!(" ORDER BY {}", parts.join(", "))
}
pub(super) fn endpoint_base_case(column: &str) -> String {
format!(
"CASE WHEN EXISTS (SELECT 1 FROM entities be WHERE be.id = graph_edges.{column}) \
THEN 'entity' \
WHEN EXISTS (SELECT 1 FROM notes bn WHERE bn.id = graph_edges.{column}) \
THEN 'note' ELSE 'none' END"
)
}
pub(super) fn fold_endpoint_base_row(
counts: &mut EdgeEndpointBaseCounts,
source: &str,
target: &str,
n: u64,
) {
let slot = match (source, target) {
("entity", "entity") => &mut counts.entity_entity,
("entity", "note") => &mut counts.entity_note,
("note", "entity") => &mut counts.note_entity,
("note", "note") => &mut counts.note_note,
_ => &mut counts.unresolved,
};
*slot = slot.saturating_add(n);
}
pub(super) const LIVE_ENDPOINTS_CONDITION: &str = "NOT EXISTS (SELECT 1 FROM entities le \
WHERE le.id = graph_edges.source_id AND le.deleted_at IS NOT NULL) \
AND NOT EXISTS (SELECT 1 FROM entities le \
WHERE le.id = graph_edges.target_id AND le.deleted_at IS NOT NULL) \
AND NOT EXISTS (SELECT 1 FROM notes ln \
WHERE ln.id = graph_edges.source_id AND ln.deleted_at IS NOT NULL) \
AND NOT EXISTS (SELECT 1 FROM notes ln \
WHERE ln.id = graph_edges.target_id AND ln.deleted_at IS NOT NULL)";
pub(super) fn with_live_endpoints(where_clause: &str) -> String {
format!("{where_clause} AND {LIVE_ENDPOINTS_CONDITION}")
}
pub(super) fn build_edge_filter_sql(
namespace: &str,
filter: &EdgeFilter,
) -> (String, Vec<Box<dyn rusqlite::types::ToSql>>) {
build_edge_filter_sql_for_namespaces(&[namespace.to_string()], filter)
}
pub(super) fn build_edge_filter_sql_for_namespaces(
namespaces: &[String],
filter: &EdgeFilter,
) -> (String, Vec<Box<dyn rusqlite::types::ToSql>>) {
let params: Vec<Box<dyn rusqlite::types::ToSql>> = namespaces
.iter()
.map(|namespace| -> Box<dyn rusqlite::types::ToSql> { Box::new(namespace.clone()) })
.collect();
let namespace_condition = match namespaces.len() {
0 => "0".to_string(),
1 => "namespace = ?1".to_string(),
_ => {
let placeholders: Vec<String> =
(1..=namespaces.len()).map(|i| format!("?{i}")).collect();
format!("namespace IN ({})", placeholders.join(", "))
}
};
build_edge_filter_conditions(namespace_condition, params, filter)
}
pub(super) fn build_edge_filter_sql_for_namespaces_json(
namespaces_json: &str,
filter: &EdgeFilter,
) -> (String, Vec<Box<dyn rusqlite::types::ToSql>>) {
let params: Vec<Box<dyn rusqlite::types::ToSql>> = vec![Box::new(namespaces_json.to_string())];
let namespace_condition = "namespace IN (SELECT value FROM json_each(?1))".to_string();
build_edge_filter_conditions(namespace_condition, params, filter)
}
pub(super) fn build_edge_filter_conditions(
namespace_condition: String,
mut params: Vec<Box<dyn rusqlite::types::ToSql>>,
filter: &EdgeFilter,
) -> (String, Vec<Box<dyn rusqlite::types::ToSql>>) {
let mut conditions = vec![namespace_condition, "deleted_at IS NULL".to_string()];
if !filter.ids.is_empty() {
let placeholders: Vec<String> = filter
.ids
.iter()
.map(|id| {
params.push(Box::new(id.to_string()));
format!("?{}", params.len())
})
.collect();
conditions.push(format!("id IN ({})", placeholders.join(",")));
}
if !filter.source_ids.is_empty() {
let placeholders: Vec<String> = filter
.source_ids
.iter()
.map(|id| {
params.push(Box::new(id.to_string()));
format!("?{}", params.len())
})
.collect();
conditions.push(format!("source_id IN ({})", placeholders.join(",")));
}
if !filter.target_ids.is_empty() {
let placeholders: Vec<String> = filter
.target_ids
.iter()
.map(|id| {
params.push(Box::new(id.to_string()));
format!("?{}", params.len())
})
.collect();
conditions.push(format!("target_id IN ({})", placeholders.join(",")));
}
if !filter.relations.is_empty() {
let placeholders: Vec<String> = filter
.relations
.iter()
.map(|r| {
params.push(Box::new(r.to_string()));
format!("?{}", params.len())
})
.collect();
conditions.push(format!("relation IN ({})", placeholders.join(",")));
}
if let Some(min_w) = filter.min_weight {
params.push(Box::new(min_w));
conditions.push(format!("weight >= ?{}", params.len()));
}
if let Some(max_w) = filter.max_weight {
params.push(Box::new(max_w));
conditions.push(format!("weight <= ?{}", params.len()));
}
if let Some(ref time_range) = filter.created_at {
if let Some(start) = time_range.start {
params.push(Box::new(start.timestamp_micros()));
conditions.push(format!("created_at >= ?{}", params.len()));
}
if let Some(end) = time_range.end {
params.push(Box::new(end.timestamp_micros()));
conditions.push(format!("created_at < ?{}", params.len()));
}
}
let clause = format!(" WHERE {}", conditions.join(" AND "));
(clause, params)
}
pub(super) fn edge_sort_col(field: &EdgeSortField) -> &'static str {
match field {
EdgeSortField::CreatedAt => "created_at",
EdgeSortField::Weight => "weight",
EdgeSortField::Relation => "relation",
}
}