codebase-graph 1.1.6

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use super::rows::{EdgeRowsById, EdgeStagedRow, NodeRowsById, NodeStagedRow};
use serde_json::Value;

pub(super) fn merge_node_row(rows: &mut NodeRowsById, row_id: String, incoming: NodeStagedRow) {
    let Some(existing) = rows.get_mut(&row_id) else {
        rows.insert(row_id, incoming);
        return;
    };

    merge_string(&mut existing.id, incoming.id);
    merge_string(&mut existing.label, incoming.label);
    merge_string(&mut existing.kind, incoming.kind);
    merge_string(&mut existing.language, incoming.language);
    merge_string(&mut existing.path, incoming.path);
    merge_string(&mut existing.qualified_name, incoming.qualified_name);
    merge_string(&mut existing.scope_id, incoming.scope_id);
    merge_optional_i64(&mut existing.line_start, incoming.line_start);
    merge_optional_i64(&mut existing.line_end, incoming.line_end);
    merge_optional_i64(&mut existing.byte_start, incoming.byte_start);
    merge_optional_i64(&mut existing.byte_end, incoming.byte_end);
    merge_string(
        &mut existing.tree_sitter_node_type,
        incoming.tree_sitter_node_type,
    );
    merge_string(&mut existing.capture_name, incoming.capture_name);
    merge_string(&mut existing.summary, incoming.summary);
    merge_value(&mut existing.metadata, incoming.metadata);
    merge_optional_string(&mut existing.content_hash, incoming.content_hash);
}

pub(super) fn merge_edge_row(rows: &mut EdgeRowsById, row_id: String, incoming: EdgeStagedRow) {
    let Some(existing) = rows.get_mut(&row_id) else {
        rows.insert(row_id, incoming);
        return;
    };

    merge_string(&mut existing.id, incoming.id);
    merge_string(&mut existing.kind, incoming.kind);
    merge_string(&mut existing.source_id, incoming.source_id);
    merge_string(&mut existing.target_id, incoming.target_id);
    if (existing.confidence - 1.0).abs() < f64::EPSILON
        && (incoming.confidence - 1.0).abs() >= f64::EPSILON
    {
        existing.confidence = incoming.confidence;
    }
    merge_optional_i64(&mut existing.line_start, incoming.line_start);
    merge_optional_i64(&mut existing.line_end, incoming.line_end);
    merge_optional_i64(&mut existing.byte_start, incoming.byte_start);
    merge_optional_i64(&mut existing.byte_end, incoming.byte_end);
    merge_value(&mut existing.metadata, incoming.metadata);
}

fn merge_string(existing: &mut String, incoming: String) {
    if existing.is_empty() && !incoming.is_empty() {
        *existing = incoming;
    }
}

fn merge_optional_string(existing: &mut Option<String>, incoming: Option<String>) {
    if let Some(incoming) = incoming {
        if !incoming.is_empty() && existing.as_ref().is_none_or(|current| current.is_empty()) {
            *existing = Some(incoming);
        }
    }
}

fn merge_optional_i64(existing: &mut Option<i64>, incoming: Option<i64>) {
    if existing.is_none() && incoming.is_some() {
        *existing = incoming;
    }
}

fn merge_value(existing: &mut Value, incoming: Value) {
    if json_value_is_empty(existing) && !json_value_is_empty(&incoming) {
        *existing = incoming;
    }
}

fn json_value_is_empty(value: &Value) -> bool {
    match value {
        Value::Null => true,
        Value::String(value) => value.is_empty(),
        Value::Array(value) => value.is_empty(),
        Value::Object(value) => value.is_empty(),
        Value::Bool(_) | Value::Number(_) => false,
    }
}