diff --git a/src/database/mod.rs b/src/database/mod.rs index 4a4a4f9..b5446a6 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, + pub relations: HashMap<(String, String), Vec>, pub schemas: HashMap, // Map of Schema ID -> { Entity Type -> Target Subschema Arc } pub stems: HashMap>>, @@ -74,11 +74,12 @@ 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) => { - db.relations.insert(def.constraint.clone(), def); + raw_relations.push(def); } Err(e) => println!("DATABASE RELATION PARSE FAILED: {:?}", e), } @@ -107,7 +108,7 @@ impl Database { } } - db.compile()?; + db.compile(raw_relations)?; Ok(db) } @@ -138,10 +139,11 @@ impl Database { } /// Organizes the graph of the database, compiling regex, format functions, and caching relationships. - pub fn compile(&mut self) -> Result<(), crate::drop::Drop> { + pub fn compile(&mut self, raw_relations: Vec) -> Result<(), crate::drop::Drop> { self.collect_schemas(); self.collect_depths(); self.collect_descendants(); + self.collect_relations(raw_relations); self.compile_schemas(); self.collect_stems()?; @@ -226,6 +228,93 @@ 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]); + } + + // Reduce ambiguity with prefix + for rel in relations { + if let Some(prefix) = &rel.prefix { + if prefix == prop_name { + return Some(rel); + } + } + } + + // 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]); + } + } + } + None + } + fn collect_descendants_recursively( target: &str, direct_refs: &HashMap>, @@ -335,17 +424,14 @@ impl Database { if let (Some(pt), Some(prop)) = (&parent_type, &property_name) { let expected_col = format!("{}_id", prop); let mut found = false; - for rel in db.relations.values() { - if (rel.source_type == *pt && rel.destination_type == entity_type) - || (rel.source_type == entity_type && rel.destination_type == *pt) - { - if rel.source_columns.contains(&expected_col) { - relation_col = Some(expected_col.clone()); - found = true; - break; - } + + 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; } } + if !found { relation_col = Some(expected_col); } diff --git a/src/merger/mod.rs b/src/merger/mod.rs index b55ab20..a6fd731 100644 --- a/src/merger/mod.rs +++ b/src/merger/mod.rs @@ -164,7 +164,16 @@ impl Merger { _ => continue, }; - let relative_relation = self.get_entity_relation(type_def, &relative, &relation_name)?; + // Attempt to extract relative object type name + let relative_type_name = match relative.get("type").and_then(|v| v.as_str()) { + Some(t) => t, + None => continue, + }; + + let relative_keys: Vec = relative.keys().cloned().collect(); + + // Call central Database O(1) graph logic + let relative_relation = self.db.get_relation(&type_def.name, relative_type_name, &relation_name, Some(&relative_keys)); if let Some(relation) = relative_relation { let parent_is_source = type_def.hierarchy.contains(&relation.source_type); @@ -253,7 +262,16 @@ impl Merger { _ => continue, }; - let relative_relation = self.get_entity_relation(type_def, first_relative, &relation_name)?; + // Attempt to extract relative object type name + let relative_type_name = match first_relative.get("type").and_then(|v| v.as_str()) { + Some(t) => t, + None => continue, + }; + + let relative_keys: Vec = first_relative.keys().cloned().collect(); + + // Call central Database O(1) graph logic + let relative_relation = self.db.get_relation(&type_def.name, relative_type_name, &relation_name, Some(&relative_keys)); if let Some(relation) = relative_relation { let mut relative_responses = Vec::new(); @@ -766,101 +784,7 @@ impl Merger { changes } - fn reduce_entity_relations( - &self, - mut matching_relations: Vec, - relative: &serde_json::Map, - relation_name: &str, - ) -> Result, String> { - if matching_relations.is_empty() { - return Ok(None); - } - if matching_relations.len() == 1 { - return Ok(Some(matching_relations.pop().unwrap())); - } - - let exact_match: Vec<_> = matching_relations - .iter() - .filter(|r| r.prefix.as_deref() == Some(relation_name)) - .cloned() - .collect(); - if exact_match.len() == 1 { - return Ok(Some(exact_match.into_iter().next().unwrap())); - } - - matching_relations.retain(|r| { - if let Some(prefix) = &r.prefix { - !relative.contains_key(prefix) - } else { - true - } - }); - - if matching_relations.len() == 1 { - Ok(Some(matching_relations.pop().unwrap())) - } else { - let constraints: Vec<_> = matching_relations - .iter() - .map(|r| r.constraint.clone()) - .collect(); - Err(format!( - "AMBIGUOUS_TYPE_RELATIONS: Could not reduce ambiguous type relations: {}", - constraints.join(", ") - )) - } - } - - fn get_entity_relation( - &self, - entity_type: &crate::database::r#type::Type, - relative: &serde_json::Map, - relation_name: &str, - ) -> Result, String> { - let relative_type_name = match relative.get("type").and_then(|v| v.as_str()) { - Some(t) => t, - None => return Ok(None), - }; - - let relative_type = match self.db.types.get(relative_type_name) { - Some(t) => t, - None => return Ok(None), - }; - - let mut relative_relations: Vec = Vec::new(); - - for r in self.db.relations.values() { - if r.source_type != "entity" && r.destination_type != "entity" { - let condition1 = relative_type.hierarchy.contains(&r.source_type) - && entity_type.hierarchy.contains(&r.destination_type); - let condition2 = entity_type.hierarchy.contains(&r.source_type) - && relative_type.hierarchy.contains(&r.destination_type); - - if condition1 || condition2 { - relative_relations.push(r.clone()); - } - } - } - - let mut relative_relation = - self.reduce_entity_relations(relative_relations, relative, relation_name)?; - - if relative_relation.is_none() { - let mut poly_relations: Vec = Vec::new(); - for r in self.db.relations.values() { - if r.destination_type == "entity" { - let condition1 = relative_type.hierarchy.contains(&r.source_type); - let condition2 = entity_type.hierarchy.contains(&r.source_type); - - if condition1 || condition2 { - poly_relations.push(r.clone()); - } - } - } - relative_relation = self.reduce_entity_relations(poly_relations, relative, relation_name)?; - } - - Ok(relative_relation) - } + // Helper Functions fn apply_entity_relation( source_entity: &mut serde_json::Map, diff --git a/src/queryer/compiler.rs b/src/queryer/compiler.rs index 82db9d1..7ab7ad4 100644 --- a/src/queryer/compiler.rs +++ b/src/queryer/compiler.rs @@ -47,7 +47,7 @@ impl SqlCompiler { // We expect the top level to typically be an Object or Array let is_stem_query = stem_path.is_some(); - let (sql, _) = self.walk_schema(target_schema, "t1", None, filter_keys, is_stem_query, 0, String::new())?; + let (sql, _) = self.walk_schema(target_schema, "t1", None, None, filter_keys, is_stem_query, 0, String::new())?; Ok(sql) } @@ -57,6 +57,7 @@ impl SqlCompiler { &self, schema: &crate::database::schema::Schema, parent_alias: &str, + parent_type_def: Option<&crate::database::r#type::Type>, prop_name_context: Option<&str>, filter_keys: &[String], is_stem_query: bool, @@ -80,6 +81,7 @@ impl SqlCompiler { items, type_def, parent_alias, + parent_type_def, prop_name_context, true, filter_keys, @@ -92,6 +94,7 @@ impl SqlCompiler { let (item_sql, _) = self.walk_schema( items, parent_alias, + parent_type_def, prop_name_context, filter_keys, is_stem_query, @@ -125,6 +128,7 @@ impl SqlCompiler { schema, type_def, parent_alias, + parent_type_def, prop_name_context, false, filter_keys, @@ -141,6 +145,7 @@ impl SqlCompiler { return self.walk_schema( target_schema, parent_alias, + parent_type_def, prop_name_context, filter_keys, is_stem_query, @@ -169,6 +174,7 @@ impl SqlCompiler { return self.compile_one_of( &family_schemas, parent_alias, + parent_type_def, prop_name_context, filter_keys, is_stem_query, @@ -182,6 +188,7 @@ impl SqlCompiler { return self.compile_one_of( one_of, parent_alias, + parent_type_def, prop_name_context, filter_keys, is_stem_query, @@ -195,6 +202,7 @@ impl SqlCompiler { return self.compile_inline_object( props, parent_alias, + parent_type_def, filter_keys, is_stem_query, depth, @@ -241,6 +249,7 @@ impl SqlCompiler { schema: &crate::database::schema::Schema, type_def: &crate::database::r#type::Type, parent_alias: &str, + parent_type_def: Option<&crate::database::r#type::Type>, prop_name: Option<&str>, is_array: bool, filter_keys: &[String], @@ -290,12 +299,12 @@ impl SqlCompiler { let base_alias = table_aliases.get(&type_def.name).cloned().unwrap_or_else(|| parent_alias.to_string()); select_args.push(format!("'id', {}.id", base_alias)); - let (case_sql, _) = self.compile_one_of(&family_schemas, &base_alias, None, filter_keys, is_stem_query, depth, current_path.clone())?; + let (case_sql, _) = self.compile_one_of(&family_schemas, &base_alias, parent_type_def, None, filter_keys, is_stem_query, depth, current_path.clone())?; select_args.push(format!("'type', {}", case_sql)); } else if let Some(one_of) = &schema.obj.one_of { let base_alias = table_aliases.get(&type_def.name).cloned().unwrap_or_else(|| parent_alias.to_string()); select_args.push(format!("'id', {}.id", base_alias)); - let (case_sql, _) = self.compile_one_of(one_of, &base_alias, None, filter_keys, is_stem_query, depth, current_path.clone())?; + let (case_sql, _) = self.compile_one_of(one_of, &base_alias, parent_type_def, None, filter_keys, is_stem_query, depth, current_path.clone())?; select_args.push(format!("'type', {}", case_sql)); } @@ -311,6 +320,7 @@ impl SqlCompiler { type_def, &table_aliases, parent_alias, + parent_type_def, prop_name, filter_keys, ¤t_path, @@ -428,6 +438,7 @@ impl SqlCompiler { let (val_sql, val_type) = self.walk_schema( prop_schema, &owner_alias, + Some(type_def), // Pass current type_def as parent_type_def for child properties Some(prop_key), filter_keys, is_stem_query, @@ -448,6 +459,7 @@ impl SqlCompiler { type_def: &crate::database::r#type::Type, table_aliases: &std::collections::HashMap, parent_alias: &str, + parent_type_def: Option<&crate::database::r#type::Type>, prop_name: Option<&str>, filter_keys: &[String], current_path: &str, @@ -596,10 +608,39 @@ impl SqlCompiler { } if let Some(prop) = prop_name { - if prop == "target" || prop == "source" { - where_clauses.push(format!("{}.id = {}.{}_id", base_alias, parent_alias, prop)); - } else { - where_clauses.push(format!("{}.parent_id = {}.id", base_alias, parent_alias)); + // Find what type the parent alias is actually mapping to + let mut relation_alias = parent_alias.to_string(); + + let mut relation_resolved = false; + if let Some(parent_type) = parent_type_def { + if let Some(relation) = self.db.get_relation(&parent_type.name, &type_def.name, prop, None) { + + let source_col = &relation.source_columns[0]; + let dest_col = &relation.destination_columns[0]; + + // Determine directionality based on the Relation metadata + if relation.source_type == parent_type.name || parent_type.hierarchy.contains(&relation.source_type) { + // Parent is the source + where_clauses.push(format!("{}.{} = {}.{}", parent_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) { + // Parent is the destination + where_clauses.push(format!("{}.{} = {}.{}", base_alias, source_col, parent_alias, dest_col)); + relation_resolved = true; + } + } + } + + if !relation_resolved { + // Fallback heuristics for unmapped polymorphism or abstract models + if prop == "target" || prop == "source" { + if parent_alias.ends_with("_t1") { + relation_alias = parent_alias.replace("_t1", "_t2"); + } + where_clauses.push(format!("{}.id = {}.{}_id", base_alias, relation_alias, prop)); + } else { + where_clauses.push(format!("{}.parent_id = {}.id", base_alias, relation_alias)); + } } } @@ -610,6 +651,7 @@ impl SqlCompiler { &self, props: &std::collections::BTreeMap>, parent_alias: &str, + parent_type_def: Option<&crate::database::r#type::Type>, filter_keys: &[String], is_stem_query: bool, depth: usize, @@ -626,6 +668,7 @@ impl SqlCompiler { let (child_sql, val_type) = self.walk_schema( v, parent_alias, + parent_type_def, Some(k), filter_keys, is_stem_query, @@ -645,6 +688,7 @@ impl SqlCompiler { &self, schemas: &[Arc], parent_alias: &str, + parent_type_def: Option<&crate::database::r#type::Type>, prop_name_context: Option<&str>, filter_keys: &[String], is_stem_query: bool, @@ -667,6 +711,7 @@ impl SqlCompiler { let (val_sql, _) = self.walk_schema( option_schema, parent_alias, + parent_type_def, prop_name_context, filter_keys, is_stem_query,