Skip to content

Commit d993ac8

Browse files
committed
fix(bolt): Derive all integer IDs from element_id hash for uniqueness
- All Node.id values now derived from element_id via hash - All Relationship.id, start_node_id, end_node_id derived from element_ids - Fixes self-loop visualization in Neo4j Browser when User:1 and Post:1 had same raw ID value (now they have different hashed IDs) - element_id remains source of truth for round-trip capability - Updated: transform_to_node, transform_to_relationship, find_node_in_row_with_label, find_node_in_row, find_relationship_in_row_with_type, create_node_with_label, create_relationship_with_type
1 parent 0269d87 commit d993ac8

1 file changed

Lines changed: 65 additions & 57 deletions

File tree

src/server/bolt_protocol/result_transformer.rs

Lines changed: 65 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -531,13 +531,8 @@ fn transform_to_node(
531531
let id_value_refs: Vec<&str> = id_values.iter().map(|s| s.as_str()).collect();
532532
let element_id = generate_node_element_id(&label, &id_value_refs);
533533

534-
// Try to extract numeric ID for legacy `id` field
535-
// For single-column numeric IDs, parse as i64; otherwise use 0
536-
let legacy_id: i64 = if id_values.len() == 1 {
537-
id_values[0].parse().unwrap_or(0)
538-
} else {
539-
0 // Composite IDs can't be represented as single i64
540-
};
534+
// Derive integer ID from element_id (ensures uniqueness across labels)
535+
let id: i64 = generate_id_from_element_id(&element_id);
541536

542537
// Create Node struct
543538
// Use inferred label if original labels was empty
@@ -548,7 +543,7 @@ fn transform_to_node(
548543
};
549544

550545
Ok(Node {
551-
id: legacy_id,
546+
id,
552547
labels: final_labels,
553548
properties,
554549
element_id,
@@ -687,11 +682,16 @@ fn transform_to_relationship(
687682
let start_node_element_id = generate_node_element_id(from_node_label, &from_id_refs);
688683
let end_node_element_id = generate_node_element_id(to_node_label, &to_id_refs);
689684

685+
// Derive integer IDs from element_ids (ensures uniqueness across labels)
686+
let rel_id = generate_id_from_element_id(&element_id);
687+
let start_node_id = generate_id_from_element_id(&start_node_element_id);
688+
let end_node_id = generate_id_from_element_id(&end_node_element_id);
689+
690690
// Create Relationship struct
691691
Ok(Relationship {
692-
id: 0, // Legacy ID (unused in Neo4j 5.x)
693-
start_node_id: 0, // Legacy ID
694-
end_node_id: 0, // Legacy ID
692+
id: rel_id,
693+
start_node_id,
694+
end_node_id,
695695
rel_type: rel_type.to_string(),
696696
properties,
697697
element_id,
@@ -885,9 +885,10 @@ fn transform_path_from_json(row: &HashMap<String, Value>) -> Result<Path, String
885885
}
886886
};
887887

888-
// Create start node with explicit label and properties
889-
let start_id = extract_id_from_props(&start_props, "user_id", "post_id", "id");
890-
let start_element_id = generate_node_element_id(&start_label, &[&start_id.to_string()]);
888+
// Create start node - element_id is source of truth, integer id derived from it
889+
let start_prop_id = extract_id_from_props(&start_props, "user_id", "post_id", "id");
890+
let start_element_id = generate_node_element_id(&start_label, &[&start_prop_id.to_string()]);
891+
let start_id = generate_id_from_element_id(&start_element_id);
891892
// Clean property keys (remove table alias prefix like "t1_0.")
892893
let start_props_clean = clean_property_keys(start_props);
893894
let start_node = Node::new(
@@ -897,9 +898,10 @@ fn transform_path_from_json(row: &HashMap<String, Value>) -> Result<Path, String
897898
start_element_id,
898899
);
899900

900-
// Create end node with explicit label and properties
901-
let end_id = extract_id_from_props(&end_props, "user_id", "post_id", "id");
902-
let end_element_id = generate_node_element_id(&end_label, &[&end_id.to_string()]);
901+
// Create end node - element_id is source of truth, integer id derived from it
902+
let end_prop_id = extract_id_from_props(&end_props, "user_id", "post_id", "id");
903+
let end_element_id = generate_node_element_id(&end_label, &[&end_prop_id.to_string()]);
904+
let end_id = generate_id_from_element_id(&end_element_id);
903905
// Clean property keys
904906
let end_props_clean = clean_property_keys(end_props);
905907
let end_node = Node::new(
@@ -909,16 +911,18 @@ fn transform_path_from_json(row: &HashMap<String, Value>) -> Result<Path, String
909911
end_element_id,
910912
);
911913

912-
// Create relationship with type and properties
913-
let rel_id = extract_id_from_props(&rel_props, "from_id", "follower_id", "user_id");
914-
let from_id_str = start_id.to_string();
915-
let to_id_str = end_id.to_string();
916-
let rel_element_id = generate_relationship_element_id(&rel_type, &from_id_str, &to_id_str);
914+
// Create relationship - element_id is source of truth, integer id derived from it
915+
let rel_element_id = generate_relationship_element_id(
916+
&rel_type,
917+
&start_prop_id.to_string(),
918+
&end_prop_id.to_string(),
919+
);
920+
let rel_id = generate_id_from_element_id(&rel_element_id);
917921
let rel_props_clean = clean_property_keys(rel_props);
918922
let relationship = Relationship::new(
919923
rel_id,
920-
start_id, // start_node_id
921-
end_id, // end_node_id
924+
start_id, // start_node_id - derived from start_element_id
925+
end_id, // end_node_id - derived from end_element_id
922926
rel_type.clone(),
923927
rel_props_clean,
924928
rel_element_id,
@@ -1003,6 +1007,18 @@ fn extract_id_from_props(props: &HashMap<String, Value>, id1: &str, id2: &str, i
10031007
0
10041008
}
10051009

1010+
/// Generate a unique integer node ID from element_id
1011+
/// This ensures round-trip: element_id is the source of truth, integer id is derived from it
1012+
fn generate_id_from_element_id(element_id: &str) -> i64 {
1013+
use std::collections::hash_map::DefaultHasher;
1014+
use std::hash::{Hash, Hasher};
1015+
1016+
let mut hasher = DefaultHasher::new();
1017+
element_id.hash(&mut hasher);
1018+
// Use absolute value and mask to ensure positive i64
1019+
(hasher.finish() as i64).abs()
1020+
}
1021+
10061022
fn value_to_i64(val: &Value) -> Option<i64> {
10071023
match val {
10081024
Value::Number(n) => n.as_i64(),
@@ -1082,7 +1098,8 @@ fn find_node_in_row_with_label(
10821098
label,
10831099
&id_values.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
10841100
);
1085-
let id: i64 = id_values.first().and_then(|s| s.parse().ok()).unwrap_or(0);
1101+
// Derive integer ID from element_id (ensures uniqueness across labels)
1102+
let id: i64 = generate_id_from_element_id(&element_id);
10861103

10871104
log::info!(
10881105
"✅ Found node '{}' in row: label={}, properties={}, element_id={}",
@@ -1154,7 +1171,8 @@ fn find_node_in_row(
11541171

11551172
if !id_values.is_empty() {
11561173
let element_id = format!("{}:{}", label, id_values.join("|"));
1157-
let id: i64 = id_values.first().and_then(|s| s.parse().ok()).unwrap_or(0);
1174+
// Derive integer ID from element_id (ensures uniqueness across labels)
1175+
let id: i64 = generate_id_from_element_id(&element_id);
11581176

11591177
return Some(Node {
11601178
id,
@@ -1229,26 +1247,13 @@ fn find_relationship_in_row_with_type(
12291247
properties.len()
12301248
);
12311249

1232-
// Extract node IDs from element_ids (format: "Label:id")
1233-
let start_id = start_element_id
1234-
.split(':')
1235-
.nth(1)
1236-
.and_then(|s| s.parse::<i64>().ok())
1237-
.unwrap_or(0);
1238-
let end_id = end_element_id
1239-
.split(':')
1240-
.nth(1)
1241-
.and_then(|s| s.parse::<i64>().ok())
1242-
.unwrap_or(0);
1243-
1244-
// Generate unique relationship ID by combining start and end IDs
1245-
// Use a simple hash: (start_id << 32) | end_id
1246-
// This ensures each unique (from, to) pair gets a unique ID
1247-
let rel_id = if start_id > 0 && end_id > 0 {
1248-
((start_id as i64) << 20) | (end_id as i64)
1249-
} else {
1250-
0
1251-
};
1250+
// Node IDs are derived from element_id hash (same as the nodes themselves)
1251+
let start_id = generate_id_from_element_id(start_element_id);
1252+
let end_id = generate_id_from_element_id(end_element_id);
1253+
1254+
// Generate relationship element_id from type and node element_ids
1255+
let rel_element_id = format!("{}:{}->{}", rel_type, start_element_id, end_element_id);
1256+
let rel_id = generate_id_from_element_id(&rel_element_id);
12521257

12531258
// Create relationship with extracted properties
12541259
Some(Relationship {
@@ -1257,7 +1262,7 @@ fn find_relationship_in_row_with_type(
12571262
end_node_id: end_id,
12581263
rel_type: rel_type.clone(),
12591264
properties,
1260-
element_id: format!("{}:{}->{}", rel_type, start_id, end_id),
1265+
element_id: rel_element_id,
12611266
start_node_element_id: start_element_id.to_string(),
12621267
end_node_element_id: end_element_id.to_string(),
12631268
})
@@ -1373,12 +1378,14 @@ fn create_placeholder_relationship(
13731378
}
13741379

13751380
/// Create a node with a known label but no data
1376-
fn create_node_with_label(label: &str, id: i64) -> Node {
1381+
fn create_node_with_label(label: &str, idx: i64) -> Node {
1382+
let element_id = format!("{}:{}", label, idx);
1383+
let id = generate_id_from_element_id(&element_id);
13771384
Node {
13781385
id,
13791386
labels: vec![label.to_string()],
13801387
properties: std::collections::HashMap::new(),
1381-
element_id: format!("{}:{}", label, id),
1388+
element_id,
13821389
}
13831390
}
13841391

@@ -1388,18 +1395,19 @@ fn create_relationship_with_type(
13881395
start_element_id: &str,
13891396
end_element_id: &str,
13901397
) -> Relationship {
1391-
// Extract just the ID portions from element IDs (format: "Label:id")
1392-
let start_id = start_element_id.split(':').last().unwrap_or("0");
1393-
let end_id = end_element_id.split(':').last().unwrap_or("0");
1398+
// Generate element_id and derive all integer IDs from element_ids
1399+
let element_id = format!("{}:{}->{}", rel_type, start_element_id, end_element_id);
1400+
let id = generate_id_from_element_id(&element_id);
1401+
let start_node_id = generate_id_from_element_id(start_element_id);
1402+
let end_node_id = generate_id_from_element_id(end_element_id);
13941403

13951404
Relationship {
1396-
id: 0,
1397-
start_node_id: 0,
1398-
end_node_id: 0,
1405+
id,
1406+
start_node_id,
1407+
end_node_id,
13991408
rel_type: rel_type.to_string(),
14001409
properties: std::collections::HashMap::new(),
1401-
// Use simpler elementId format: TYPE:from_id->to_id
1402-
element_id: format!("{}:{}->{}", rel_type, start_id, end_id),
1410+
element_id,
14031411
start_node_element_id: start_element_id.to_string(),
14041412
end_node_element_id: end_element_id.to_string(),
14051413
}

0 commit comments

Comments
 (0)