From ebcdb661fa335a063c6c46e0fc0d83288a72a767 Mon Sep 17 00:00:00 2001 From: Alex Groleau Date: Fri, 27 Mar 2026 19:13:44 -0400 Subject: [PATCH] maybe working --- fixtures/merger.json | 468 ++++++++++++++++++--------------- src/database/executors/pgrx.rs | 4 +- src/database/schema.rs | 61 +++-- src/merger/mod.rs | 16 +- 4 files changed, 291 insertions(+), 258 deletions(-) diff --git a/fixtures/merger.json b/fixtures/merger.json index 7e61114..81dacfe 100644 --- a/fixtures/merger.json +++ b/fixtures/merger.json @@ -74,6 +74,20 @@ "type" ], "prefix": "target" + }, + { + "id": "66666666-6666-6666-6666-666666666666", + "type": "relation", + "constraint": "fk_entity_organization", + "source_type": "entity", + "source_columns": [ + "organization_id" + ], + "destination_type": "organization", + "destination_columns": [ + "id" + ], + "prefix": null } ], "types": [ @@ -282,6 +296,17 @@ } } } + }, + "email_addresses": { + "type": "array", + "items": { + "$ref": "contact", + "properties": { + "target": { + "$ref": "email_address" + } + } + } } } } @@ -1833,16 +1858,18 @@ "type": "contact", "is_primary": false, "target": { - "type": "phone_number", - "number": "555-0002" + "type": "email_address", + "address": "test@example.com" } - }, + } + ], + "email_addresses": [ { "type": "contact", "is_primary": false, "target": { "type": "email_address", - "address": "test@example.com" + "address": "test2@example.com" } } ] @@ -1934,7 +1961,10 @@ " modified_by", ") VALUES (", " NULL,", - " '{\"number\":\"555-0001\",\"type\":\"phone_number\"}',", + " '{", + " \"number\":\"555-0001\",", + " \"type\":\"phone_number\"", + " }',", " '{{uuid:phone1_id}}',", " '{{uuid}}',", " 'create',", @@ -2005,115 +2035,6 @@ " '00000000-0000-0000-0000-000000000000'", ")" ], - [ - "INSERT INTO agreego.\"entity\" (", - " \"created_at\",", - " \"created_by\",", - " \"id\",", - " \"modified_at\",", - " \"modified_by\",", - " \"type\"", - ") VALUES (", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " '{{uuid:phone2_id}}',", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " 'phone_number'", - ")" - ], - [ - "INSERT INTO agreego.\"phone_number\" (", - " \"number\"", - ") VALUES (", - " '555-0002'", - ")" - ], - [ - "INSERT INTO agreego.change (", - " \"old\",", - " \"new\",", - " entity_id,", - " id,", - " kind,", - " modified_at,", - " modified_by", - ") VALUES (", - " NULL,", - " '{", - " \"number\":\"555-0002\",", - " \"type\":\"phone_number\"", - " }',", - " '{{uuid:phone2_id}}',", - " '{{uuid}}',", - " 'create',", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000'", - ")" - ], - [ - "INSERT INTO agreego.\"entity\" (", - " \"created_at\",", - " \"created_by\",", - " \"id\",", - " \"modified_at\",", - " \"modified_by\",", - " \"type\"", - ") VALUES (", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " '{{uuid:contact2_id}}',", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " 'contact'", - ")" - ], - [ - "INSERT INTO agreego.\"relationship\" (", - " \"source_id\",", - " \"source_type\",", - " \"target_id\",", - " \"target_type\"", - ") VALUES (", - " '{{uuid:person_id}}',", - " 'person',", - " '{{uuid:phone2_id}}',", - " 'phone_number'", - ")" - ], - [ - "INSERT INTO agreego.\"contact\" (", - " \"is_primary\"", - ") VALUES (", - " false", - ")" - ], - [ - "INSERT INTO agreego.change (", - " \"old\",", - " \"new\",", - " entity_id,", - " id,", - " kind,", - " modified_at,", - " modified_by", - ") VALUES (", - " NULL,", - " '{", - " \"is_primary\":false,", - " \"source_id\":\"{{uuid:person_id}}\",", - " \"source_type\":\"person\",", - " \"target_id\":\"{{uuid:phone2_id}}\",", - " \"target_type\":\"phone_number\",", - " \"type\":\"contact\"", - " }',", - " '{{uuid:contact2_id}}',", - " '{{uuid}}',", - " 'create',", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000'", - ")" - ], [ "INSERT INTO agreego.\"entity\" (", " \"created_at\",", @@ -2171,7 +2092,7 @@ ") VALUES (", " '{{timestamp}}',", " '00000000-0000-0000-0000-000000000000',", - " '{{uuid:contact3_id}}',", + " '{{uuid:contact2_id}}',", " '{{timestamp}}',", " '00000000-0000-0000-0000-000000000000',", " 'contact'", @@ -2216,6 +2137,115 @@ " \"target_type\":\"email_address\",", " \"type\":\"contact\"", " }',", + " '{{uuid:contact2_id}}',", + " '{{uuid}}',", + " 'create',", + " '{{timestamp}}',", + " '00000000-0000-0000-0000-000000000000'", + ")" + ], + [ + "INSERT INTO agreego.\"entity\" (", + " \"created_at\",", + " \"created_by\",", + " \"id\",", + " \"modified_at\",", + " \"modified_by\",", + " \"type\"", + ") VALUES (", + " '{{timestamp}}',", + " '00000000-0000-0000-0000-000000000000',", + " '{{uuid:email2_id}}',", + " '{{timestamp}}',", + " '00000000-0000-0000-0000-000000000000',", + " 'email_address'", + ")" + ], + [ + "INSERT INTO agreego.\"email_address\" (", + " \"address\"", + ") VALUES (", + " 'test2@example.com'", + ")" + ], + [ + "INSERT INTO agreego.change (", + " \"old\",", + " \"new\",", + " entity_id,", + " id,", + " kind,", + " modified_at,", + " modified_by", + ") VALUES (", + " NULL,", + " '{", + " \"address\":\"test2@example.com\",", + " \"type\":\"email_address\"", + " }',", + " '{{uuid:email2_id}}',", + " '{{uuid}}',", + " 'create',", + " '{{timestamp}}',", + " '00000000-0000-0000-0000-000000000000'", + ")" + ], + [ + "INSERT INTO agreego.\"entity\" (", + " \"created_at\",", + " \"created_by\",", + " \"id\",", + " \"modified_at\",", + " \"modified_by\",", + " \"type\"", + ") VALUES (", + " '{{timestamp}}',", + " '00000000-0000-0000-0000-000000000000',", + " '{{uuid:contact3_id}}',", + " '{{timestamp}}',", + " '00000000-0000-0000-0000-000000000000',", + " 'contact'", + ")" + ], + [ + "INSERT INTO agreego.\"relationship\" (", + " \"source_id\",", + " \"source_type\",", + " \"target_id\",", + " \"target_type\"", + ") VALUES (", + " '{{uuid:person_id}}',", + " 'person',", + " '{{uuid:email2_id}}',", + " 'email_address'", + ")" + ], + [ + "INSERT INTO agreego.\"contact\" (", + " \"is_primary\"", + ") VALUES (", + " false", + ")" + ], + [ + "INSERT INTO agreego.change (", + " \"old\",", + " \"new\",", + " entity_id,", + " id,", + " kind,", + " modified_at,", + " modified_by", + ") VALUES (", + " NULL,", + " '{", + " \"is_primary\":false,", + " \"source_id\":\"{{uuid:person_id}}\",", + " \"source_type\":\"person\",", + " \"target_id\":\"{{uuid:email2_id}}\",", + " \"target_type\":\"email_address\",", + " \"type\":\"contact\"", + " }',", " '{{uuid:contact3_id}}',", " '{{uuid}}',", " 'create',", @@ -2248,16 +2278,16 @@ ], [ "SELECT pg_notify('entity', '{", - " \"complete\":{", - " \"created_at\":\"{{timestamp}}\",", - " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"first_name\":\"Relation\",", - " \"id\":\"{{uuid:person_id}}\",", - " \"last_name\":\"Test\",", - " \"modified_at\":\"{{timestamp}}\",", - " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"type\":\"person\"", - " },", + " \"complete\":{", + " \"created_at\":\"{{timestamp}}\",", + " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"first_name\":\"Relation\",", + " \"id\":\"{{uuid:person_id}}\",", + " \"last_name\":\"Test\",", + " \"modified_at\":\"{{timestamp}}\",", + " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"type\":\"person\"", + " },", " \"new\":{", " \"first_name\":\"Relation\",", " \"last_name\":\"Test\",", @@ -2267,19 +2297,19 @@ ], [ "SELECT pg_notify('entity', '{", - " \"complete\":{", - " \"created_at\":\"{{timestamp}}\",", - " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"id\":\"{{uuid:contact1_id}}\",", - " \"is_primary\":true,", - " \"modified_at\":\"{{timestamp}}\",", - " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"source_id\":\"{{uuid:person_id}}\",", - " \"source_type\":\"person\",", - " \"target_id\":\"{{uuid:phone1_id}}\",", - " \"target_type\":\"phone_number\",", - " \"type\":\"contact\"", - " },", + " \"complete\":{", + " \"created_at\":\"{{timestamp}}\",", + " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"id\":\"{{uuid:contact1_id}}\",", + " \"is_primary\":true,", + " \"modified_at\":\"{{timestamp}}\",", + " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"source_id\":\"{{uuid:person_id}}\",", + " \"source_type\":\"person\",", + " \"target_id\":\"{{uuid:phone1_id}}\",", + " \"target_type\":\"phone_number\",", + " \"type\":\"contact\"", + " },", " \"new\":{", " \"is_primary\":true,", " \"source_id\":\"{{uuid:person_id}}\",", @@ -2292,15 +2322,15 @@ ], [ "SELECT pg_notify('entity', '{", - " \"complete\":{", - " \"created_at\":\"{{timestamp}}\",", - " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"id\":\"{{uuid:phone1_id}}\",", - " \"modified_at\":\"{{timestamp}}\",", - " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"number\":\"555-0001\",", - " \"type\":\"phone_number\"", - " },", + " \"complete\":{", + " \"created_at\":\"{{timestamp}}\",", + " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"id\":\"{{uuid:phone1_id}}\",", + " \"modified_at\":\"{{timestamp}}\",", + " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"number\":\"555-0001\",", + " \"type\":\"phone_number\"", + " },", " \"new\":{", " \"number\":\"555-0001\",", " \"type\":\"phone_number\"", @@ -2309,87 +2339,87 @@ ], [ "SELECT pg_notify('entity', '{", - " \"complete\":{", - " \"created_at\":\"{{timestamp}}\",", - " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"id\":\"{{uuid:contact2_id}}\",", - " \"is_primary\":false,", - " \"modified_at\":\"{{timestamp}}\",", - " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"source_id\":\"{{uuid:person_id}}\",", - " \"source_type\":\"person\",", - " \"target_id\":\"{{uuid:phone2_id}}\",", - " \"target_type\":\"phone_number\",", - " \"type\":\"contact\"", - " },", - " \"new\":{", - " \"is_primary\":false,", - " \"source_id\":\"{{uuid:person_id}}\",", - " \"source_type\":\"person\",", - " \"target_id\":\"{{uuid:phone2_id}}\",", - " \"target_type\":\"phone_number\",", - " \"type\":\"contact\"", - " }", - " }')" + " \"complete\":{", + " \"created_at\":\"{{timestamp}}\",", + " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"id\":\"{{uuid:contact2_id}}\",", + " \"is_primary\":false,", + " \"modified_at\":\"{{timestamp}}\",", + " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"source_id\":\"{{uuid:person_id}}\",", + " \"source_type\":\"person\",", + " \"target_id\":\"{{uuid:email1_id}}\",", + " \"target_type\":\"email_address\",", + " \"type\":\"contact\"", + " },", + " \"new\":{", + " \"is_primary\":false,", + " \"source_id\":\"{{uuid:person_id}}\",", + " \"source_type\":\"person\",", + " \"target_id\":\"{{uuid:email1_id}}\",", + " \"target_type\":\"email_address\",", + " \"type\":\"contact\"", + " }", + "}')" ], [ "SELECT pg_notify('entity', '{", - " \"complete\":{", - " \"created_at\":\"{{timestamp}}\",", - " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"id\":\"{{uuid:phone2_id}}\",", - " \"modified_at\":\"{{timestamp}}\",", - " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"number\":\"555-0002\",", - " \"type\":\"phone_number\"", - " },", - " \"new\":{", - " \"number\":\"555-0002\",", - " \"type\":\"phone_number\"", - " }", - " }')" + " \"complete\":{", + " \"address\":\"test@example.com\",", + " \"created_at\":\"{{timestamp}}\",", + " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"id\":\"{{uuid:email1_id}}\",", + " \"modified_at\":\"{{timestamp}}\",", + " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"type\":\"email_address\"", + " },", + " \"new\":{", + " \"address\":\"test@example.com\",", + " \"type\":\"email_address\"", + " }", + "}')" ], [ "SELECT pg_notify('entity', '{", - " \"complete\":{", - " \"created_at\":\"{{timestamp}}\",", - " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"id\":\"{{uuid:contact3_id}}\",", - " \"is_primary\":false,", - " \"modified_at\":\"{{timestamp}}\",", - " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"source_id\":\"{{uuid:person_id}}\",", - " \"source_type\":\"person\",", - " \"target_id\":\"{{uuid:email1_id}}\",", - " \"target_type\":\"email_address\",", - " \"type\":\"contact\"", - " },", - " \"new\":{", - " \"is_primary\":false,", - " \"source_id\":\"{{uuid:person_id}}\",", - " \"source_type\":\"person\",", - " \"target_id\":\"{{uuid:email1_id}}\",", - " \"target_type\":\"email_address\",", - " \"type\":\"contact\"", - " }", - " }')" + " \"complete\":{", + " \"created_at\":\"{{timestamp}}\",", + " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"id\":\"{{uuid:contact3_id}}\",", + " \"is_primary\":false,", + " \"modified_at\":\"{{timestamp}}\",", + " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"source_id\":\"{{uuid:person_id}}\",", + " \"source_type\":\"person\",", + " \"target_id\":\"{{uuid:email2_id}}\",", + " \"target_type\":\"email_address\",", + " \"type\":\"contact\"", + " },", + " \"new\":{", + " \"is_primary\":false,", + " \"source_id\":\"{{uuid:person_id}}\",", + " \"source_type\":\"person\",", + " \"target_id\":\"{{uuid:email2_id}}\",", + " \"target_type\":\"email_address\",", + " \"type\":\"contact\"", + " }", + "}')" ], [ "SELECT pg_notify('entity', '{", - " \"complete\":{", - " \"address\":\"test@example.com\",", - " \"created_at\":\"{{timestamp}}\",", - " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"id\":\"{{uuid:email1_id}}\",", - " \"modified_at\":\"{{timestamp}}\",", - " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"type\":\"email_address\"", - " },", - " \"new\":{", - " \"address\":\"test@example.com\",", - " \"type\":\"email_address\"", - " }", - " }')" + " \"complete\":{", + " \"address\":\"test2@example.com\",", + " \"created_at\":\"{{timestamp}}\",", + " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"id\":\"{{uuid:email2_id}}\",", + " \"modified_at\":\"{{timestamp}}\",", + " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"type\":\"email_address\"", + " },", + " \"new\":{", + " \"address\":\"test2@example.com\",", + " \"type\":\"email_address\"", + " }", + "}')" ] ] } diff --git a/src/database/executors/pgrx.rs b/src/database/executors/pgrx.rs index 207c1a1..208472a 100644 --- a/src/database/executors/pgrx.rs +++ b/src/database/executors/pgrx.rs @@ -79,9 +79,9 @@ impl DatabaseExecutor for SpiExecutor { } } + pgrx::debug1!("JSPG_SQL: {}", sql); self.transact(|| { Spi::connect(|client| { - pgrx::notice!("JSPG_SQL: {}", sql); match client.select(sql, Some(args_with_oid.len() as i64), &args_with_oid) { Ok(tup_table) => { let mut results = Vec::new(); @@ -110,9 +110,9 @@ impl DatabaseExecutor for SpiExecutor { } } + pgrx::debug1!("JSPG_SQL: {}", sql); self.transact(|| { Spi::connect_mut(|client| { - pgrx::notice!("JSPG_SQL: {}", sql); match client.update(sql, Some(args_with_oid.len() as i64), &args_with_oid) { Ok(_) => Ok(()), Err(e) => Err(format!("SPI Execution Failure: {}", e)), diff --git a/src/database/schema.rs b/src/database/schema.rs index cd17403..d0c20c6 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -508,7 +508,7 @@ impl Schema { if let Some(family) = &self.obj.family { parent_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string()); } else if let Some(identifier) = self.obj.identifier() { - parent_type_name = Some(identifier); + parent_type_name = Some(identifier.split('.').next_back().unwrap_or(&identifier).to_string()); } if let Some(p_type) = parent_type_name { @@ -530,11 +530,11 @@ impl Schema { if let Some(family) = &target_schema.obj.family { child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string()); } else if let Some(ref_id) = target_schema.obj.identifier() { - child_type_name = Some(ref_id); + child_type_name = Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string()); } else if let Some(arr) = &target_schema.obj.one_of { if let Some(first) = arr.first() { if let Some(ref_id) = first.obj.identifier() { - child_type_name = Some(ref_id); + child_type_name = Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string()); } } } @@ -622,8 +622,43 @@ pub(crate) fn resolve_relation<'a>( } } + if !resolved && relative_keys.is_some() { + // 1. M:M Disambiguation: The child schema explicitly defines an outbound property + // matching one of the relational prefixes (e.g. "target"). We first identify that consumed relation. + let keys = relative_keys.unwrap(); + let mut consumed_rel_idx = None; + for (i, rel) in matching_rels.iter().enumerate() { + if let Some(prefix) = &rel.prefix { + if keys.contains(prefix) { + consumed_rel_idx = Some(i); + break; // Found the routing edge explicitly consumed by the schema payload + } + } + } + + // Then, we find its exact Twin on the same junction boundary that provides the reverse ownership. + if let Some(used_idx) = consumed_rel_idx { + let used_rel = matching_rels[used_idx]; + let mut twin_ids = Vec::new(); + for (i, rel) in matching_rels.iter().enumerate() { + if i != used_idx + && rel.source_type == used_rel.source_type + && rel.destination_type == used_rel.destination_type + && rel.prefix.is_some() + { + twin_ids.push(i); + } + } + + if twin_ids.len() == 1 { + chosen_idx = twin_ids[0]; + resolved = true; + } + } + } + if !resolved { - // 1. If there's EXACTLY ONE relation with a null prefix, it's the base structural edge. Pick it. + // 2. Base 1:M Fallback. If there's EXACTLY ONE relation with a null prefix, it's the base structural edge. let mut null_prefix_ids = Vec::new(); for (i, rel) in matching_rels.iter().enumerate() { if rel.prefix.is_none() { @@ -632,24 +667,6 @@ pub(crate) fn resolve_relation<'a>( } if null_prefix_ids.len() == 1 { chosen_idx = null_prefix_ids[0]; - resolved = true; - } - } - - if !resolved && relative_keys.is_some() { - // 2. M:M Disambiguation: The child schema will explicitly define an outbound property - // matching one of the relational prefixes (e.g. "target"). We use the OTHER one (e.g. "source"). - let keys = relative_keys.unwrap(); - let mut missing_prefix_ids = Vec::new(); - for (i, rel) in matching_rels.iter().enumerate() { - if let Some(prefix) = &rel.prefix { - if !keys.contains(prefix) { - missing_prefix_ids.push(i); - } - } - } - if missing_prefix_ids.len() == 1 { - chosen_idx = missing_prefix_ids[0]; // resolved = true; } } diff --git a/src/merger/mod.rs b/src/merger/mod.rs index 7221575..6ab0f09 100644 --- a/src/merger/mod.rs +++ b/src/merger/mod.rs @@ -192,7 +192,7 @@ impl Merger { let mut entity_objects = std::collections::BTreeMap::new(); let mut entity_arrays = std::collections::BTreeMap::new(); - for (k, v) in obj.clone() { + for (k, v) in obj { // Always retain system and unmapped core fields natively implicitly mapped to the Postgres tables if k == "id" || k == "type" || k == "created" { entity_fields.insert(k.clone(), v.clone()); @@ -333,20 +333,6 @@ impl Merger { entity_replaces = replaces; } - #[cfg(not(test))] - if type_name == "contact" || type_name == "person" { - pgrx::notice!("=== DEBUG {} PAYLOAD ===", type_name); - pgrx::notice!("1. Incoming obj iteration: {:?}", obj); - pgrx::notice!("2. Final mapped entity_fields: {:?}", entity_fields); - pgrx::notice!( - "3. TypeDef fields check: {:?}", - type_def.fields.contains(&"source_id".to_string()) - ); - if !entity_fields.contains_key("source_id") { - pgrx::notice!("CRITICAL ERROR: source_id was dropped during mapping loop!"); - } - } - self.merge_entity_fields( entity_change_kind.as_deref().unwrap_or(""), &type_name,