From 46fc0320265d1e4517a3e06cce7f9d5777cbaf11 Mon Sep 17 00:00:00 2001 From: Alex Groleau Date: Thu, 26 Mar 2026 23:49:52 -0400 Subject: [PATCH] fixed merge lookup issue --- fixtures/merger.json | 116 ++++++++++++++++++++++++++++++++- src/database/executors/mock.rs | 102 +++++++++++++++++------------ src/merger/mod.rs | 21 ++++-- src/tests/fixtures.rs | 6 ++ 4 files changed, 196 insertions(+), 49 deletions(-) diff --git a/fixtures/merger.json b/fixtures/merger.json index 925aada..1474fd5 100644 --- a/fixtures/merger.json +++ b/fixtures/merger.json @@ -972,7 +972,119 @@ "LEFT JOIN agreego.\"user\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"organization\" t3 ON t3.id = t1.id", "LEFT JOIN agreego.\"entity\" t4 ON t4.id = t1.id", - "WHERE \"first_name\" = 'LookupFirst' AND \"last_name\" = 'LookupLast' AND \"date_of_birth\" = '1990-01-01T00:00:00Z' AND \"pronouns\" = 'they/them'" + "WHERE (", + " \"first_name\" = 'LookupFirst'", + " AND \"last_name\" = 'LookupLast'", + " AND \"date_of_birth\" = '1990-01-01T00:00:00Z'", + " AND \"pronouns\" = 'they/them'", + ")" + ], + [ + "UPDATE agreego.\"person\"", + "SET", + " \"contact_id\" = 'abc-contact'", + "WHERE", + " id = '22222222-2222-2222-2222-222222222222'" + ], + [ + "UPDATE agreego.\"entity\"", + "SET", + " \"modified_at\" = '2026-03-10T00:00:00Z',", + " \"modified_by\" = '00000000-0000-0000-0000-000000000000'", + "WHERE", + " id = '22222222-2222-2222-2222-222222222222'" + ], + [ + "INSERT INTO agreego.change (", + " \"old\",", + " \"new\",", + " entity_id,", + " id,", + " kind,", + " modified_at,", + " modified_by", + ")", + "VALUES (", + " '{", + " \"contact_id\":\"old-contact\"", + " }',", + " '{", + " \"contact_id\":\"abc-contact\",", + " \"type\":\"person\"", + " }',", + " '22222222-2222-2222-2222-222222222222',", + " '{{uuid}}',", + " 'update',", + " '{{timestamp}}',", + " '00000000-0000-0000-0000-000000000000'", + ")" + ], + [ + "SELECT pg_notify('entity', '{", + " \"complete\":{", + " \"contact_id\":\"abc-contact\",", + " \"date_of_birth\":\"1990-01-01T00:00:00Z\",", + " \"first_name\":\"LookupFirst\",", + " \"id\":\"22222222-2222-2222-2222-222222222222\",", + " \"last_name\":\"LookupLast\",", + " \"modified_at\":\"2026-03-10T00:00:00Z\",", + " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"pronouns\":\"they/them\",", + " \"type\":\"person\"", + " },", + " \"new\":{", + " \"contact_id\":\"abc-contact\",", + " \"type\":\"person\"", + " },", + " \"old\":{", + " \"contact_id\":\"old-contact\"", + " }", + " }')" + ] + ] + } + }, + { + "description": "Update existing person with id (lookup)", + "action": "merge", + "data": { + "id": "33333333-3333-3333-3333-333333333333", + "type": "person", + "first_name": "LookupFirst", + "last_name": "LookupLast", + "date_of_birth": "1990-01-01T00:00:00Z", + "pronouns": "they/them", + "contact_id": "abc-contact" + }, + "mocks": [ + { + "id": "22222222-2222-2222-2222-222222222222", + "type": "person", + "first_name": "LookupFirst", + "last_name": "LookupLast", + "date_of_birth": "1990-01-01T00:00:00Z", + "pronouns": "they/them", + "contact_id": "old-contact" + } + ], + "schema_id": "person", + "expect": { + "success": true, + "sql": [ + [ + "SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", + "FROM agreego.\"person\" t1", + "LEFT JOIN agreego.\"user\" t2 ON t2.id = t1.id", + "LEFT JOIN agreego.\"organization\" t3 ON t3.id = t1.id", + "LEFT JOIN agreego.\"entity\" t4 ON t4.id = t1.id", + "WHERE", + " t1.id = '33333333-3333-3333-3333-333333333333'", + " OR (", + " \"first_name\" = 'LookupFirst'", + " AND \"last_name\" = 'LookupLast'", + " AND \"date_of_birth\" = '1990-01-01T00:00:00Z'", + " AND \"pronouns\" = 'they/them'", + " )" ], [ "UPDATE agreego.\"person\"", @@ -1484,7 +1596,7 @@ "SELECT to_jsonb(t1.*) || to_jsonb(t2.*)", "FROM agreego.\"order\" t1", "LEFT JOIN agreego.\"entity\" t2 ON t2.id = t1.id", - "WHERE t1.id = 'abc'" + "WHERE t1.id = 'abc' OR (\"id\" = 'abc')" ], [ "INSERT INTO agreego.\"entity\" (", diff --git a/src/database/executors/mock.rs b/src/database/executors/mock.rs index 356bbaa..6086093 100644 --- a/src/database/executors/mock.rs +++ b/src/database/executors/mock.rs @@ -124,42 +124,23 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option> { return None; }; - // 2. Extract WHERE conditions - let mut conditions = Vec::new(); + // 2. Extract WHERE conditions string + let mut where_clause = String::new(); if let Some(where_idx) = sql_upper.find(" WHERE ") { - let mut where_end = sql_upper.find(" ORDER BY ").unwrap_or(sql.len()); + let mut where_end = sql_upper.find(" ORDER BY ").unwrap_or(sql_upper.len()); if let Some(limit_idx) = sql_upper.find(" LIMIT ") { if limit_idx < where_end { where_end = limit_idx; } } - let where_clause = &sql[where_idx + 7..where_end]; - let and_regex = Regex::new(r"(?i)\s+AND\s+").ok()?; - let parts = and_regex.split(where_clause); - for part in parts { - if let Some(eq_idx) = part.find('=') { - let left = part[..eq_idx] - .trim() - .split('.') - .last() - .unwrap_or("") - .trim_matches('"'); - let right = part[eq_idx + 1..].trim().trim_matches('\''); - conditions.push((left.to_string(), right.to_string())); - } else if part.to_uppercase().contains(" IS NULL") { - let left = part[..part.to_uppercase().find(" IS NULL").unwrap()] - .trim() - .split('.') - .last() - .unwrap_or("") - .replace('"', ""); // Remove quotes explicitly - conditions.push((left, "null".to_string())); - } - } + where_clause = sql[where_idx + 7..where_end].to_string(); } // 3. Find matching mocks let mut matches = Vec::new(); + let or_regex = Regex::new(r"(?i)\s+OR\s+").ok()?; + let and_regex = Regex::new(r"(?i)\s+AND\s+").ok()?; + for mock in mocks { if let Some(mock_obj) = mock.as_object() { if let Some(t) = mock_obj.get("type") { @@ -168,25 +149,66 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option> { } } - let mut matches_all = true; - for (k, v) in &conditions { - let mock_val_str = match mock_obj.get(k) { - Some(Value::String(s)) => s.clone(), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => b.to_string(), - Some(Value::Null) => "null".to_string(), - _ => { - matches_all = false; - break; + if where_clause.is_empty() { + matches.push(mock.clone()); + continue; + } + + let or_parts = or_regex.split(&where_clause); + let mut any_branch_matched = false; + + for or_part in or_parts { + let branch_str = or_part.replace('(', "").replace(')', ""); + let mut branch_matches = true; + + for part in and_regex.split(&branch_str) { + if let Some(eq_idx) = part.find('=') { + let left = part[..eq_idx] + .trim() + .split('.') + .last() + .unwrap_or("") + .trim_matches('"'); + let right = part[eq_idx + 1..].trim().trim_matches('\''); + + let mock_val_str = match mock_obj.get(left) { + Some(Value::String(s)) => s.clone(), + Some(Value::Number(n)) => n.to_string(), + Some(Value::Bool(b)) => b.to_string(), + Some(Value::Null) => "null".to_string(), + _ => "".to_string(), + }; + if mock_val_str != right { + branch_matches = false; + break; + } + } else if part.to_uppercase().contains(" IS NULL") { + let left = part[..part.to_uppercase().find(" IS NULL").unwrap()] + .trim() + .split('.') + .last() + .unwrap_or("") + .trim_matches('"'); + + let mock_val_str = match mock_obj.get(left) { + Some(Value::Null) => "null".to_string(), + _ => "".to_string(), + }; + + if mock_val_str != "null" { + branch_matches = false; + break; + } } - }; - if mock_val_str != *v { - matches_all = false; + } + + if branch_matches { + any_branch_matched = true; break; } } - if matches_all { + if any_branch_matched { matches.push(mock.clone()); } } diff --git a/src/merger/mod.rs b/src/merger/mod.rs index da96766..ad3740e 100644 --- a/src/merger/mod.rs +++ b/src/merger/mod.rs @@ -585,11 +585,14 @@ impl Merger { template }; - let where_clause = if let Some(id) = id_val { - format!("WHERE t1.id = {}", Self::quote_literal(id)) - } else if lookup_complete { - let mut lookup_predicates = Vec::new(); + let mut where_parts = Vec::new(); + if let Some(id) = id_val { + where_parts.push(format!("t1.id = {}", Self::quote_literal(id))); + } + + if lookup_complete { + let mut lookup_predicates = Vec::new(); for column in &entity_type.lookup_fields { let val = entity_fields.get(column).unwrap_or(&Value::Null); if column == "type" { @@ -598,10 +601,14 @@ impl Merger { lookup_predicates.push(format!("\"{}\" = {}", column, Self::quote_literal(val))); } } - format!("WHERE {}", lookup_predicates.join(" AND ")) - } else { + where_parts.push(format!("({})", lookup_predicates.join(" AND "))); + } + + if where_parts.is_empty() { return Ok(None); - }; + } + + let where_clause = format!("WHERE {}", where_parts.join(" OR ")); let final_sql = format!("{} {}", fetch_sql_template, where_clause); diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index fa7ff5f..7952b31 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -8596,3 +8596,9 @@ fn test_merger_0_10() { let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR")); crate::tests::runner::run_test_case(&path, 0, 10).unwrap(); } + +#[test] +fn test_merger_0_11() { + let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 11).unwrap(); +}