diff --git a/fixtures/queryer.json b/fixtures/queryer.json index 1cc3dcb..b241dd7 100644 --- a/fixtures/queryer.json +++ b/fixtures/queryer.json @@ -1016,7 +1016,7 @@ " JOIN agreego.entity entity_8 ON entity_8.id = address_7.id", " WHERE", " NOT entity_8.archived", - " AND relationship_5.target_id = address_7.id),", + " AND relationship_5.target_id = entity_8.id),", " 'type', entity_6.type", " )), '[]'::jsonb)", " FROM agreego.contact contact_4", @@ -1024,7 +1024,7 @@ " JOIN agreego.entity entity_6 ON entity_6.id = relationship_5.id", " WHERE", " NOT entity_6.archived", - " AND contact_4.source_id = entity_3.id),", + " AND relationship_5.source_id = entity_3.id),", " 'age', person_1.age,", " 'archived', entity_3.archived,", " 'contacts',", @@ -1048,7 +1048,7 @@ " JOIN agreego.entity entity_17 ON entity_17.id = address_16.id", " WHERE", " NOT entity_17.archived", - " AND relationship_10.target_id = address_16.id))", + " AND relationship_10.target_id = entity_17.id))", " WHEN entity_11.target_type = 'email_address' THEN", " ((SELECT jsonb_build_object(", " 'address', email_address_14.address,", @@ -1062,7 +1062,7 @@ " JOIN agreego.entity entity_15 ON entity_15.id = email_address_14.id", " WHERE", " NOT entity_15.archived", - " AND relationship_10.target_id = email_address_14.id))", + " AND relationship_10.target_id = entity_15.id))", " WHEN entity_11.target_type = 'phone_number' THEN", " ((SELECT jsonb_build_object(", " 'archived', entity_13.archived,", @@ -1076,7 +1076,7 @@ " JOIN agreego.entity entity_13 ON entity_13.id = phone_number_12.id", " WHERE", " NOT entity_13.archived", - " AND relationship_10.target_id = phone_number_12.id))", + " AND relationship_10.target_id = entity_13.id))", " ELSE NULL END,", " 'type', entity_11.type", " )), '[]'::jsonb)", @@ -1085,7 +1085,7 @@ " JOIN agreego.entity entity_11 ON entity_11.id = relationship_10.id", " WHERE", " NOT entity_11.archived", - " AND contact_9.source_id = entity_3.id),", + " AND relationship_10.source_id = entity_3.id),", " 'created_at', entity_3.created_at,", " 'email_addresses',", " (SELECT COALESCE(jsonb_agg(jsonb_build_object(", @@ -1107,7 +1107,7 @@ " JOIN agreego.entity entity_22 ON entity_22.id = email_address_21.id", " WHERE", " NOT entity_22.archived", - " AND relationship_19.target_id = email_address_21.id),", + " AND relationship_19.target_id = entity_22.id),", " 'type', entity_20.type", " )), '[]'::jsonb)", " FROM agreego.contact contact_18", @@ -1115,7 +1115,7 @@ " JOIN agreego.entity entity_20 ON entity_20.id = relationship_19.id", " WHERE", " NOT entity_20.archived", - " AND contact_18.source_id = entity_3.id),", + " AND relationship_19.source_id = entity_3.id),", " 'first_name', person_1.first_name,", " 'id', entity_3.id,", " 'last_name', person_1.last_name,", @@ -1140,7 +1140,7 @@ " JOIN agreego.entity entity_27 ON entity_27.id = phone_number_26.id", " WHERE", " NOT entity_27.archived", - " AND relationship_24.target_id = phone_number_26.id),", + " AND relationship_24.target_id = entity_27.id),", " 'type', entity_25.type", " )), '[]'::jsonb)", " FROM agreego.contact contact_23", @@ -1148,7 +1148,7 @@ " JOIN agreego.entity entity_25 ON entity_25.id = relationship_24.id", " WHERE", " NOT entity_25.archived", - " AND contact_23.source_id = entity_3.id),", + " AND relationship_24.source_id = entity_3.id),", " 'type', entity_3.type", ")", "FROM agreego.person person_1", @@ -1253,7 +1253,7 @@ " JOIN agreego.entity entity_8 ON entity_8.id = address_7.id", " WHERE", " NOT entity_8.archived", - " AND relationship_5.target_id = address_7.id),", + " AND relationship_5.target_id = entity_8.id),", " 'type', entity_6.type", " )), '[]'::jsonb)", " FROM agreego.contact contact_4", @@ -1261,7 +1261,7 @@ " JOIN agreego.entity entity_6 ON entity_6.id = relationship_5.id", " WHERE", " NOT entity_6.archived", - " AND contact_4.source_id = entity_3.id),", + " AND relationship_5.source_id = entity_3.id),", " 'age', person_1.age,", " 'archived', entity_3.archived,", " 'contacts',", @@ -1285,7 +1285,7 @@ " JOIN agreego.entity entity_17 ON entity_17.id = address_16.id", " WHERE", " NOT entity_17.archived", - " AND relationship_10.target_id = address_16.id))", + " AND relationship_10.target_id = entity_17.id))", " WHEN entity_11.target_type = 'email_address' THEN", " ((SELECT jsonb_build_object(", " 'address', email_address_14.address,", @@ -1299,7 +1299,7 @@ " JOIN agreego.entity entity_15 ON entity_15.id = email_address_14.id", " WHERE", " NOT entity_15.archived", - " AND relationship_10.target_id = email_address_14.id))", + " AND relationship_10.target_id = entity_15.id))", " WHEN entity_11.target_type = 'phone_number' THEN", " ((SELECT jsonb_build_object(", " 'archived', entity_13.archived,", @@ -1313,7 +1313,7 @@ " JOIN agreego.entity entity_13 ON entity_13.id = phone_number_12.id", " WHERE", " NOT entity_13.archived", - " AND relationship_10.target_id = phone_number_12.id))", + " AND relationship_10.target_id = entity_13.id))", " ELSE NULL END,", " 'type', entity_11.type", " )), '[]'::jsonb)", @@ -1323,7 +1323,7 @@ " WHERE", " NOT entity_11.archived", " AND contact_9.is_primary = ($11#>>'{}')::boolean", - " AND contact_9.source_id = entity_3.id),", + " AND relationship_10.source_id = entity_3.id),", " 'created_at', entity_3.created_at,", " 'email_addresses',", " (SELECT COALESCE(jsonb_agg(jsonb_build_object(", @@ -1345,7 +1345,7 @@ " JOIN agreego.entity entity_22 ON entity_22.id = email_address_21.id", " WHERE", " NOT entity_22.archived", - " AND relationship_19.target_id = email_address_21.id),", + " AND relationship_19.target_id = entity_22.id),", " 'type', entity_20.type", " )), '[]'::jsonb)", " FROM agreego.contact contact_18", @@ -1353,7 +1353,7 @@ " JOIN agreego.entity entity_20 ON entity_20.id = relationship_19.id", " WHERE", " NOT entity_20.archived", - " AND contact_18.source_id = entity_3.id),", + " AND relationship_19.source_id = entity_3.id),", " 'first_name', person_1.first_name,", " 'id', entity_3.id,", " 'last_name', person_1.last_name,", @@ -1379,7 +1379,7 @@ " WHERE", " NOT entity_27.archived", " AND phone_number_26.number ILIKE $32#>>'{}'", - " AND relationship_24.target_id = phone_number_26.id),", + " AND relationship_24.target_id = entity_27.id),", " 'type', entity_25.type", " )), '[]'::jsonb)", " FROM agreego.contact contact_23", @@ -1387,7 +1387,7 @@ " JOIN agreego.entity entity_25 ON entity_25.id = relationship_24.id", " WHERE", " NOT entity_25.archived", - " AND contact_23.source_id = entity_3.id),", + " AND relationship_24.source_id = entity_3.id),", " 'type', entity_3.type", ")", "FROM agreego.person person_1", @@ -1457,7 +1457,7 @@ " JOIN agreego.entity entity_5 ON entity_5.id = phone_number_4.id", " WHERE", " NOT entity_5.archived", - " AND relationship_2.target_id = phone_number_4.id", + " AND relationship_2.target_id = entity_5.id", " ),", " 'type', entity_3.type", ")", diff --git a/src/database/mod.rs b/src/database/mod.rs index 92b751e..499da76 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -32,7 +32,7 @@ pub struct Database { pub enums: HashMap, pub types: HashMap, pub puncs: HashMap, - pub relations: HashMap<(String, String), Vec>, + pub relations: Vec, pub schemas: HashMap, // Map of Schema ID -> { Entity Type -> Target Subschema Arc } pub stems: HashMap>>, @@ -46,7 +46,7 @@ impl Database { let mut db = Self { enums: HashMap::new(), types: HashMap::new(), - relations: HashMap::new(), + relations: Vec::new(), puncs: HashMap::new(), schemas: HashMap::new(), stems: HashMap::new(), @@ -74,12 +74,15 @@ impl Database { } } - let mut raw_relations = Vec::new(); if let Some(arr) = val.get("relations").and_then(|v| v.as_array()) { for item in arr { match serde_json::from_value::(item.clone()) { Ok(def) => { - raw_relations.push(def); + if db.types.contains_key(&def.source_type) + && db.types.contains_key(&def.destination_type) + { + db.relations.push(def); + } } Err(e) => println!("DATABASE RELATION PARSE FAILED: {:?}", e), } @@ -108,7 +111,7 @@ impl Database { } } - db.compile(raw_relations)?; + db.compile()?; Ok(db) } @@ -138,12 +141,10 @@ impl Database { self.executor.timestamp() } - /// Organizes the graph of the database, compiling regex, format functions, and caching relationships. - pub fn compile(&mut self, raw_relations: Vec) -> Result<(), crate::drop::Drop> { + pub fn compile(&mut self) -> Result<(), crate::drop::Drop> { self.collect_schemas(); self.collect_depths(); self.collect_descendants(); - self.collect_relations(raw_relations); self.compile_schemas(); self.collect_stems()?; @@ -228,93 +229,77 @@ impl Database { self.descendants = descendants; } - fn collect_relations(&mut self, raw_relations: Vec) { - let mut edges: HashMap<(String, String), Vec> = HashMap::new(); - - // For every relation, map it across all polymorphic inheritance permutations - for relation in raw_relations { - if let Some(_source_type_def) = self.types.get(&relation.source_type) { - if let Some(_dest_type_def) = self.types.get(&relation.destination_type) { - let mut src_descendants = Vec::new(); - let mut dest_descendants = Vec::new(); - - for (t_name, t_def) in &self.types { - if t_def.hierarchy.contains(&relation.source_type) { - src_descendants.push(t_name.clone()); - } - if t_def.hierarchy.contains(&relation.destination_type) { - dest_descendants.push(t_name.clone()); - } - } - - for p_type in &src_descendants { - for c_type in &dest_descendants { - // Ignore entity <-> entity generic fallbacks, they aren't useful edges - if p_type == "entity" && c_type == "entity" { - continue; - } - - // Forward edge - edges - .entry((p_type.clone(), c_type.clone())) - .or_default() - .push(relation.clone()); - - // Reverse edge (only if types are different to avoid duplicating self-referential edges like activity parent_id) - if p_type != c_type { - edges - .entry((c_type.clone(), p_type.clone())) - .or_default() - .push(relation.clone()); - } - } - } - } - } - } - self.relations = edges; - } - pub fn get_relation( &self, parent_type: &str, child_type: &str, prop_name: &str, relative_keys: Option<&Vec>, - ) -> Option<&Relation> { - if let Some(relations) = self - .relations - .get(&(parent_type.to_string(), child_type.to_string())) - { - if relations.len() == 1 { - return Some(&relations[0]); - } + ) -> Option<(&Relation, bool)> { + if parent_type == "entity" && child_type == "entity" { + return None; // Ignore entity <-> entity generic fallbacks, they aren't useful edges + } - // Reduce ambiguity with prefix - for rel in relations { - if let Some(prefix) = &rel.prefix { - if prefix == prop_name { - return Some(rel); - } - } - } + let p_def = self.types.get(parent_type)?; + let c_def = self.types.get(child_type)?; - // Reduce ambiguity by checking if relative payload OMITS the prefix (M:M heuristic) - if let Some(keys) = relative_keys { - let mut missing_prefix_rels = Vec::new(); - for rel in relations { - if let Some(prefix) = &rel.prefix { - if !keys.contains(prefix) { - missing_prefix_rels.push(rel); - } - } - } - if missing_prefix_rels.len() == 1 { - return Some(missing_prefix_rels[0]); + let mut matching_rels = Vec::new(); + let mut directions = Vec::new(); + + for rel in &self.relations { + let is_forward = p_def.hierarchy.contains(&rel.source_type) + && c_def.hierarchy.contains(&rel.destination_type); + let is_reverse = p_def.hierarchy.contains(&rel.destination_type) + && c_def.hierarchy.contains(&rel.source_type); + + if is_forward { + matching_rels.push(rel); + directions.push(true); + } else if is_reverse { + matching_rels.push(rel); + directions.push(false); + } + } + + if matching_rels.is_empty() { + return None; + } + + if matching_rels.len() == 1 { + return Some((matching_rels[0], directions[0])); + } + + let mut chosen_idx = 0; + let mut resolved = false; + + // Reduce ambiguity with prefix + for (i, rel) in matching_rels.iter().enumerate() { + if let Some(prefix) = &rel.prefix { + if prefix == prop_name { + chosen_idx = i; + resolved = true; + break; } } } - None + + // Reduce ambiguity by checking if relative payload OMITS the prefix (M:M heuristic) + if !resolved && relative_keys.is_some() { + 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]; + } + } + + Some((matching_rels[chosen_idx], directions[chosen_idx])) } fn collect_descendants_recursively( @@ -427,7 +412,7 @@ impl Database { let expected_col = format!("{}_id", prop); let mut found = false; - if let Some(rel) = db.get_relation(pt, &entity_type, prop, None) { + if let Some((rel, _)) = db.get_relation(pt, &entity_type, prop, None) { if rel.source_columns.contains(&expected_col) { relation_col = Some(expected_col.clone()); found = true; diff --git a/src/merger/mod.rs b/src/merger/mod.rs index 7c28e3b..7065316 100644 --- a/src/merger/mod.rs +++ b/src/merger/mod.rs @@ -189,8 +189,7 @@ impl Merger { Some(&relative_keys), ); - if let Some(relation) = relative_relation { - let parent_is_source = type_def.hierarchy.contains(&relation.source_type); + if let Some((relation, parent_is_source)) = relative_relation { if parent_is_source { // Parent holds FK to Child. Child MUST be generated FIRST. @@ -292,7 +291,7 @@ impl Merger { Some(&relative_keys), ); - if let Some(relation) = relative_relation { + if let Some((relation, _)) = relative_relation { let mut relative_responses = Vec::new(); for relative_item_val in relative_arr { if let Value::Object(mut relative_item) = relative_item_val { diff --git a/src/queryer/compiler.rs b/src/queryer/compiler.rs index 42f1c50..13b3399 100644 --- a/src/queryer/compiler.rs +++ b/src/queryer/compiler.rs @@ -517,7 +517,7 @@ impl<'a> Compiler<'a> { where_clauses.push(format!("NOT {}.archived", entity_alias)); self.compile_filter_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses); - self.compile_relation_conditions(r#type, &node, &base_alias, &mut where_clauses)?; + self.compile_relation_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses)?; Ok(where_clauses) } @@ -655,44 +655,65 @@ impl<'a> Compiler<'a> { fn compile_relation_conditions( &self, r#type: &crate::database::r#type::Type, + type_aliases: &std::collections::HashMap, node: &Node, base_alias: &str, where_clauses: &mut Vec, ) -> Result<(), String> { if let Some(prop_ref) = &node.property_name { let prop = prop_ref.as_str(); - let mut relation_alias = node.parent_alias.clone(); - let mut relation_resolved = false; + let mut parent_relation_alias = node.parent_alias.clone(); + let mut child_relation_alias = base_alias.to_string(); if let Some(parent_type) = node.parent_type { let merged_props = self.get_merged_properties(node.schema.as_ref()); let relative_keys: Vec = merged_props.keys().cloned().collect(); - if let Some(relation) = self.db.get_relation(&parent_type.name, &r#type.name, prop, Some(&relative_keys)) { - let source_col = &relation.source_columns[0]; - let dest_col = &relation.destination_columns[0]; + let (relation, is_parent_source) = self + .db + .get_relation(&parent_type.name, &r#type.name, prop, Some(&relative_keys)) + .ok_or_else(|| { + format!( + "Could not dynamically resolve database relation mapping for {} -> {} on property {}", + parent_type.name, r#type.name, prop + ) + })?; - if let Some(pta) = &node.parent_type_aliases { - if let Some(a) = pta.get(&relation.source_type) { - relation_alias = a.clone(); - } else if let Some(a) = pta.get(&relation.destination_type) { - relation_alias = a.clone(); - } - } + let source_col = &relation.source_columns[0]; + let dest_col = &relation.destination_columns[0]; - if relation.source_type == parent_type.name || parent_type.hierarchy.contains(&relation.source_type) { - where_clauses.push(format!("{}.{} = {}.{}", relation_alias, source_col, base_alias, dest_col)); - relation_resolved = true; - } else if relation.destination_type == parent_type.name || parent_type.hierarchy.contains(&relation.destination_type) { - where_clauses.push(format!("{}.{} = {}.{}", base_alias, source_col, relation_alias, dest_col)); - relation_resolved = true; + if let Some(pta) = &node.parent_type_aliases { + let p_search_type = if is_parent_source { + &relation.source_type + } else { + &relation.destination_type + }; + if let Some(a) = pta.get(p_search_type) { + parent_relation_alias = a.clone(); } } - } - if !relation_resolved { - let parent_name = node.parent_type.map(|t| t.name.as_str()).unwrap_or("unknown"); - return Err(format!("Could not dynamically resolve database relation mapping for {} -> {} on property {}", parent_name, r#type.name, prop)); + let c_search_type = if is_parent_source { + &relation.destination_type + } else { + &relation.source_type + }; + if let Some(a) = type_aliases.get(c_search_type) { + child_relation_alias = a.clone(); + } + + let sql_string = if is_parent_source { + format!( + "{}.{} = {}.{}", + parent_relation_alias, source_col, child_relation_alias, dest_col + ) + } else { + format!( + "{}.{} = {}.{}", + child_relation_alias, source_col, parent_relation_alias, dest_col + ) + }; + where_clauses.push(sql_string); } } Ok(())