diff --git a/fixtures/queryer.json b/fixtures/queryer.json index d654694..1cc3dcb 100644 --- a/fixtures/queryer.json +++ b/fixtures/queryer.json @@ -1024,7 +1024,7 @@ " JOIN agreego.entity entity_6 ON entity_6.id = relationship_5.id", " WHERE", " NOT entity_6.archived", - " AND contact_4.parent_id = entity_3.id),", + " AND contact_4.source_id = entity_3.id),", " 'age', person_1.age,", " 'archived', entity_3.archived,", " 'contacts',", @@ -1085,7 +1085,7 @@ " JOIN agreego.entity entity_11 ON entity_11.id = relationship_10.id", " WHERE", " NOT entity_11.archived", - " AND contact_9.parent_id = entity_3.id),", + " AND contact_9.source_id = entity_3.id),", " 'created_at', entity_3.created_at,", " 'email_addresses',", " (SELECT COALESCE(jsonb_agg(jsonb_build_object(", @@ -1115,7 +1115,7 @@ " JOIN agreego.entity entity_20 ON entity_20.id = relationship_19.id", " WHERE", " NOT entity_20.archived", - " AND contact_18.parent_id = entity_3.id),", + " AND contact_18.source_id = entity_3.id),", " 'first_name', person_1.first_name,", " 'id', entity_3.id,", " 'last_name', person_1.last_name,", @@ -1148,7 +1148,7 @@ " JOIN agreego.entity entity_25 ON entity_25.id = relationship_24.id", " WHERE", " NOT entity_25.archived", - " AND contact_23.parent_id = entity_3.id),", + " AND contact_23.source_id = entity_3.id),", " 'type', entity_3.type", ")", "FROM agreego.person person_1", @@ -1261,7 +1261,7 @@ " JOIN agreego.entity entity_6 ON entity_6.id = relationship_5.id", " WHERE", " NOT entity_6.archived", - " AND contact_4.parent_id = entity_3.id),", + " AND contact_4.source_id = entity_3.id),", " 'age', person_1.age,", " 'archived', entity_3.archived,", " 'contacts',", @@ -1323,7 +1323,7 @@ " WHERE", " NOT entity_11.archived", " AND contact_9.is_primary = ($11#>>'{}')::boolean", - " AND contact_9.parent_id = entity_3.id),", + " AND contact_9.source_id = entity_3.id),", " 'created_at', entity_3.created_at,", " 'email_addresses',", " (SELECT COALESCE(jsonb_agg(jsonb_build_object(", @@ -1353,7 +1353,7 @@ " JOIN agreego.entity entity_20 ON entity_20.id = relationship_19.id", " WHERE", " NOT entity_20.archived", - " AND contact_18.parent_id = entity_3.id),", + " AND contact_18.source_id = entity_3.id),", " 'first_name', person_1.first_name,", " 'id', entity_3.id,", " 'last_name', person_1.last_name,", @@ -1387,7 +1387,7 @@ " JOIN agreego.entity entity_25 ON entity_25.id = relationship_24.id", " WHERE", " NOT entity_25.archived", - " AND contact_23.parent_id = entity_3.id),", + " AND contact_23.source_id = entity_3.id),", " 'type', entity_3.type", ")", "FROM agreego.person person_1", diff --git a/reorder.py b/reorder.py new file mode 100644 index 0000000..47cb7bc --- /dev/null +++ b/reorder.py @@ -0,0 +1,69 @@ +import sys + +with open("src/queryer/compiler.rs", "r") as f: + text = f.read() + +def find_function(text, name): + pos = text.find(f" fn {name}(") + if pos == -1: + pos = text.find(f" pub fn {name}(") + if pos == -1: + return None, None, None + + # Capture documentation comments immediately above the function + doc_pos = pos + while True: + prev_newline = text.rfind("\n", 0, doc_pos) + if prev_newline == -1: break + line = text[prev_newline+1:doc_pos].strip() + if line.startswith("///") or line == "": + doc_pos = prev_newline + else: + break + + start_brace = text.find("{", pos) + depth = 1 + i = start_brace + 1 + while depth > 0 and i < len(text): + if text[i] == '{': depth += 1 + elif text[i] == '}': depth -= 1 + i += 1 + + return doc_pos + 1, i, text[doc_pos+1:i] + +# Desired order +funcs = [ + "compile", + "compile_node", + "compile_entity", + "compile_object", + "compile_one_of", + "compile_from_clause", + "compile_select_clause", + "compile_where_clause", + "get_merged_properties" +] + +blocks = {} +for f in funcs: + s, e, block = find_function(text, f) + if block: + blocks[f] = block.strip() + else: + print(f"Failed to find {f}") + sys.exit(1) + +impl_start = text.find("impl<'a> Compiler<'a> {") +header = text[:impl_start] + "impl<'a> Compiler<'a> {\n" +footer = "}\n\n" + +new_text = header +for f in funcs: + new_text += " " + blocks[f].replace("\n", "\n ") + "\n\n" +new_text = new_text.rstrip() + "\n}\n" + +# Remove extra indents injected back to the root level +new_text = new_text.replace(" \n", "\n").replace("\n }\n", "\n}\n") + +with open("src/queryer/compiler.rs", "w") as f: + f.write(new_text) diff --git a/src/queryer/compiler.rs b/src/queryer/compiler.rs index e718656..42f1c50 100644 --- a/src/queryer/compiler.rs +++ b/src/queryer/compiler.rs @@ -1,15 +1,24 @@ use crate::database::Database; use std::sync::Arc; - -pub struct SqlCompiler { - pub db: Arc, +pub struct Compiler<'a> { + pub db: &'a Database, + pub filter_keys: &'a [String], + pub is_stem_query: bool, + pub alias_counter: usize, } -impl SqlCompiler { - pub fn new(db: Arc) -> Self { - Self { db } - } +#[derive(Clone, Debug)] +pub struct Node<'a> { + pub schema: std::sync::Arc, + pub parent_alias: String, + pub parent_type_aliases: Option>>, + pub parent_type: Option<&'a crate::database::r#type::Type>, + pub property_name: Option, + pub depth: usize, + pub stem_path: String, +} +impl<'a> Compiler<'a> { /// Compiles a JSON schema into a nested PostgreSQL query returning JSONB pub fn compile( &self, @@ -23,11 +32,10 @@ impl SqlCompiler { .get(schema_id) .ok_or_else(|| format!("Schema not found: {}", schema_id))?; - let resolved_arc; let target_schema = if let Some(path) = stem_path.filter(|p| !p.is_empty() && *p != "/") { if let Some(stems_map) = self.db.stems.get(schema_id) { if let Some(stem) = stems_map.get(path) { - resolved_arc = stem.schema.clone(); + stem.schema.clone() } else { return Err(format!( "Stem entity type '{}' not found in schema '{}'", @@ -40,358 +48,176 @@ impl SqlCompiler { path, schema_id )); } - resolved_arc.as_ref() } else { - schema + std::sync::Arc::new(schema.clone()) }; - // We expect the top level to typically be an Object or Array let is_stem_query = stem_path.is_some(); - let mut alias_counter: usize = 0; - let (sql, _) = self.walk_schema( - target_schema, - "t1", - None, - None, - None, + + let mut compiler = Compiler { + db: &self.db, filter_keys, is_stem_query, - 0, - String::new(), - &mut alias_counter, - )?; + alias_counter: 0, + }; + + let node = Node { + schema: target_schema, + parent_alias: "t1".to_string(), + parent_type_aliases: None, + parent_type: None, + property_name: None, + depth: 0, + stem_path: String::new(), + }; + + let (sql, _) = compiler.compile_node(node)?; Ok(sql) } /// Recursively walks the schema AST emitting native PostgreSQL jsonb mapping /// Returns a tuple of (SQL_String, Field_Type) - fn walk_schema( - &self, - schema: &crate::database::schema::Schema, - parent_alias: &str, - parent_table_aliases: Option<&std::collections::HashMap>, - parent_type_def: Option<&crate::database::r#type::Type>, - prop_name_context: Option<&str>, - filter_keys: &[String], - is_stem_query: bool, - depth: usize, - current_path: String, - alias_counter: &mut usize, - ) -> Result<(String, String), String> { + fn compile_node(&mut self, node: Node<'a>) -> Result<(String, String), String> { // Determine the base schema type (could be an array, object, or literal) - match &schema.obj.type_ { + match &node.schema.obj.type_ { Some(crate::database::schema::SchemaTypeOrArray::Single(t)) if t == "array" => { - // Handle Arrays: - if let Some(items) = &schema.obj.items { - let next_path = if current_path.is_empty() { - String::from("#") - } else { - format!("{}.#", current_path) - }; - - if let Some(ref_id) = &items.obj.r#ref { - if let Some(type_def) = self.db.types.get(ref_id) { - return self.compile_entity_node( - items, - type_def, - parent_alias, - parent_table_aliases, - parent_type_def, - prop_name_context, - true, - filter_keys, - is_stem_query, - depth, - next_path, - alias_counter, - ); - } - } - let (item_sql, _) = self.walk_schema( - items, - parent_alias, - parent_table_aliases, - parent_type_def, - prop_name_context, - filter_keys, - is_stem_query, - depth + 1, - next_path, - alias_counter, - )?; - return Ok(( - format!("(SELECT jsonb_agg({}) FROM TODO)", item_sql), - "array".to_string(), - )); - } - - Ok(( - "SELECT jsonb_agg(TODO) FROM TODO".to_string(), - "array".to_string(), - )) - } - _ => { - // Determine if this schema represents a Database Entity - let mut resolved_type = None; - - if let Some(family_target) = schema.obj.family.as_ref() { - resolved_type = self.db.types.get(family_target); - } else if let Some(lookup_key) = schema.obj.id.as_ref().or(schema.obj.r#ref.as_ref()) { - let base_type_name = lookup_key.split('.').next_back().unwrap_or("").to_string(); - resolved_type = self.db.types.get(&base_type_name); - } - - if let Some(type_def) = resolved_type { - return self.compile_entity_node( - schema, - type_def, - parent_alias, - parent_table_aliases, - parent_type_def, - prop_name_context, - false, - filter_keys, - is_stem_query, - depth, - current_path, - alias_counter, - ); - } - - // Handle Direct Refs - if let Some(ref_id) = &schema.obj.r#ref { - // If it's just an ad-hoc struct ref, we should resolve it - if let Some(target_schema) = self.db.schemas.get(ref_id) { - return self.walk_schema( - target_schema, - parent_alias, - parent_table_aliases, - parent_type_def, - prop_name_context, - filter_keys, - is_stem_query, - depth, - current_path, - alias_counter, - ); - } - return Err(format!("Unresolved $ref: {}", ref_id)); - } - // Handle $family Polymorphism fallbacks for relations - if let Some(family_target) = &schema.obj.family { - let base_type_name = family_target.split('.').next_back().unwrap_or(family_target).to_string(); - - if let Some(type_def) = self.db.types.get(&base_type_name) { - if type_def.variations.len() == 1 { - let mut bypass_schema = crate::database::schema::Schema::default(); - bypass_schema.obj.r#ref = Some(family_target.clone()); - return self.walk_schema( - &std::sync::Arc::new(bypass_schema), - parent_alias, - parent_table_aliases, - parent_type_def, - prop_name_context, - filter_keys, - is_stem_query, - depth, - current_path, - alias_counter, - ); - } - - let mut sorted_variations: Vec = type_def.variations.iter().cloned().collect(); - sorted_variations.sort(); - - let mut family_schemas = Vec::new(); - for variation in &sorted_variations { - let mut ref_schema = crate::database::schema::Schema::default(); - ref_schema.obj.r#ref = Some(variation.clone()); - family_schemas.push(std::sync::Arc::new(ref_schema)); - } - - return self.compile_one_of( - &family_schemas, - parent_alias, - parent_table_aliases, - parent_type_def, - prop_name_context, - filter_keys, - is_stem_query, - depth, - current_path, - alias_counter, - ); - } - } - - // Handle oneOf Polymorphism fallbacks for relations - if let Some(one_of) = &schema.obj.one_of { - return self.compile_one_of( - one_of, - parent_alias, - parent_table_aliases, - parent_type_def, - prop_name_context, - filter_keys, - is_stem_query, - depth, - current_path, - alias_counter, - ); - } - - // Just an inline object definition? - if let Some(props) = &schema.obj.properties { - return self.compile_inline_object( - props, - parent_alias, - parent_table_aliases, - parent_type_def, - filter_keys, - is_stem_query, - depth, - current_path, - alias_counter, - ); - } - - // Literal fallback - Ok(( - format!( - "{}.{}", - parent_alias, - prop_name_context.unwrap_or("unknown_prop") - ), - "string".to_string(), - )) + self.compile_array(node) } + _ => self.compile_reference(node), } } - fn get_merged_properties( - &self, - schema: &crate::database::schema::Schema, - ) -> std::collections::BTreeMap> { - let mut props = std::collections::BTreeMap::new(); + fn compile_array(&mut self, node: Node<'a>) -> Result<(String, String), String> { + if let Some(items) = &node.schema.obj.items { + let next_path = if node.stem_path.is_empty() { + String::from("#") + } else { + format!("{}.#", node.stem_path) + }; - if let Some(ref_id) = &schema.obj.r#ref { - if let Some(parent_schema) = self.db.schemas.get(ref_id) { - props.extend(self.get_merged_properties(parent_schema)); + if let Some(ref_id) = &items.obj.r#ref { + if let Some(type_def) = self.db.types.get(ref_id) { + let mut entity_noke = node.clone(); + entity_noke.stem_path = next_path; + entity_noke.schema = std::sync::Arc::clone(items); + return self.compile_entity(type_def, entity_noke, true); + } } + + let mut next_node = node.clone(); + next_node.depth += 1; + next_node.stem_path = next_path; + next_node.schema = std::sync::Arc::clone(items); + let (item_sql, _) = self.compile_node(next_node)?; + return Ok(( + format!("(SELECT jsonb_agg({}) FROM TODO)", item_sql), + "array".to_string(), + )); } - if let Some(local_props) = &schema.obj.properties { - for (k, v) in local_props { - props.insert(k.clone(), v.clone()); - } - } - - props + Ok(( + "SELECT jsonb_agg(TODO) FROM TODO".to_string(), + "array".to_string(), + )) } - fn compile_entity_node( - &self, - schema: &crate::database::schema::Schema, - type_def: &crate::database::r#type::Type, - parent_alias: &str, - parent_table_aliases: Option<&std::collections::HashMap>, - parent_type_def: Option<&crate::database::r#type::Type>, - prop_name: Option<&str>, + fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> { + // Determine if this schema represents a Database Entity + let mut resolved_type = None; + + if let Some(family_target) = node.schema.obj.family.as_ref() { + resolved_type = self.db.types.get(family_target); + } else if let Some(lookup_key) = node + .schema + .obj + .id + .as_ref() + .or(node.schema.obj.r#ref.as_ref()) + { + let base_type_name = lookup_key.split('.').next_back().unwrap_or("").to_string(); + resolved_type = self.db.types.get(&base_type_name); + } + + if let Some(type_def) = resolved_type { + return self.compile_entity(type_def, node.clone(), false); + } + + // Handle Direct Refs + if let Some(ref_id) = &node.schema.obj.r#ref { + // If it's just an ad-hoc struct ref, we should resolve it + if let Some(target_schema) = self.db.schemas.get(ref_id) { + let mut ref_node = node.clone(); + ref_node.schema = std::sync::Arc::new(target_schema.clone()); + return self.compile_node(ref_node); + } + return Err(format!("Unresolved $ref: {}", ref_id)); + } + // Handle $family Polymorphism fallbacks for relations + if let Some(family_target) = &node.schema.obj.family { + let base_type_name = family_target + .split('.') + .next_back() + .unwrap_or(family_target) + .to_string(); + + if let Some(type_def) = self.db.types.get(&base_type_name) { + if type_def.variations.len() == 1 { + let mut bypass_schema = crate::database::schema::Schema::default(); + bypass_schema.obj.r#ref = Some(family_target.clone()); + let mut bypass_node = node.clone(); + bypass_node.schema = std::sync::Arc::new(bypass_schema); + return self.compile_node(bypass_node); + } + + let mut sorted_variations: Vec = type_def.variations.iter().cloned().collect(); + sorted_variations.sort(); + + let mut family_schemas = Vec::new(); + for variation in &sorted_variations { + let mut ref_schema = crate::database::schema::Schema::default(); + ref_schema.obj.r#ref = Some(variation.clone()); + family_schemas.push(std::sync::Arc::new(ref_schema)); + } + + return self.compile_one_of(&family_schemas, node); + } + } + + // Handle oneOf Polymorphism fallbacks for relations + if let Some(one_of) = &node.schema.obj.one_of { + return self.compile_one_of(one_of, node.clone()); + } + + // Just an inline object definition? + if let Some(props) = &node.schema.obj.properties { + return self.compile_object(props, node.clone()); + } + + // Literal fallback + Ok(( + format!( + "{}.{}", + node.parent_alias, + node.property_name.as_deref().unwrap_or("unknown_prop") + ), + "string".to_string(), + )) + } + + fn compile_entity( + &mut self, + r#type: &'a crate::database::r#type::Type, + node: Node<'a>, is_array: bool, - filter_keys: &[String], - is_stem_query: bool, - depth: usize, - current_path: String, - alias_counter: &mut usize, ) -> Result<(String, String), String> { - // 1. Build FROM clauses and table aliases - let (table_aliases, from_clauses) = self.build_hierarchy_from_clauses(type_def, alias_counter); + let (table_aliases, from_clauses) = self.compile_from_clause(r#type); // 2. Map properties and build jsonb_build_object args - let mut select_args = self.map_properties_to_aliases( - schema, - type_def, - &table_aliases, - parent_alias, - filter_keys, - is_stem_query, - depth, - ¤t_path, - alias_counter, - )?; + let mut select_args = self.compile_select_clause(r#type, &table_aliases, node.clone())?; // 2.5 Inject polymorphism directly into the query object - if let Some(family_target) = &schema.obj.family { - let base_type_name = family_target.split('.').next_back().unwrap_or(family_target).to_string(); - - if let Some(fam_type_def) = self.db.types.get(&base_type_name) { - if fam_type_def.variations.len() == 1 { - let mut bypass_schema = crate::database::schema::Schema::default(); - bypass_schema.obj.r#ref = Some(family_target.clone()); - - let mut bypassed_args = self.map_properties_to_aliases( - &bypass_schema, - type_def, - &table_aliases, - parent_alias, - filter_keys, - is_stem_query, - depth, - ¤t_path, - alias_counter, - )?; - select_args.append(&mut bypassed_args); - } else { - let mut family_schemas = Vec::new(); - let mut sorted_fam_variations: Vec = fam_type_def.variations.iter().cloned().collect(); - sorted_fam_variations.sort(); - - for variation in &sorted_fam_variations { - let mut ref_schema = crate::database::schema::Schema::default(); - ref_schema.obj.r#ref = Some(variation.clone()); - family_schemas.push(std::sync::Arc::new(ref_schema)); - } - - 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, - Some(&table_aliases), - parent_type_def, - None, - filter_keys, - is_stem_query, - depth, - current_path.clone(), - alias_counter, - )?; - 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, - Some(&table_aliases), - parent_type_def, - None, - filter_keys, - is_stem_query, - depth, - current_path.clone(), - alias_counter, - )?; - select_args.push(format!("'type', {}", case_sql)); - } + let mut poly_args = self.compile_polymorphism_select(r#type, &table_aliases, node.clone())?; + select_args.append(&mut poly_args); let jsonb_obj_sql = if select_args.is_empty() { "jsonb_build_object()".to_string() @@ -400,17 +226,7 @@ impl SqlCompiler { }; // 3. Build WHERE clauses - let where_clauses = self.build_filter_where_clauses( - schema, - type_def, - &table_aliases, - parent_alias, - parent_table_aliases, - parent_type_def, - prop_name, - filter_keys, - ¤t_path, - )?; + let where_clauses = self.compile_where_clause(r#type, &table_aliases, node)?; let selection = if is_array { format!("COALESCE(jsonb_agg({}), '[]'::jsonb)", jsonb_obj_sql) @@ -435,23 +251,161 @@ impl SqlCompiler { )) } - fn build_hierarchy_from_clauses( - &self, - type_def: &crate::database::r#type::Type, - alias_counter: &mut usize, + fn compile_polymorphism_select( + &mut self, + r#type: &'a crate::database::r#type::Type, + table_aliases: &std::collections::HashMap, + node: Node<'a>, + ) -> Result, String> { + let mut select_args = Vec::new(); + + if let Some(family_target) = node.schema.obj.family.as_ref() { + let base_type_name = family_target + .split('.') + .next_back() + .unwrap_or(family_target) + .to_string(); + + if let Some(fam_type_def) = self.db.types.get(&base_type_name) { + if fam_type_def.variations.len() == 1 { + let mut bypass_schema = crate::database::schema::Schema::default(); + bypass_schema.obj.r#ref = Some(family_target.clone()); + + let mut bypass_node = node.clone(); + bypass_node.schema = std::sync::Arc::new(bypass_schema); + + let mut bypassed_args = + self.compile_select_clause(r#type, table_aliases, bypass_node)?; + select_args.append(&mut bypassed_args); + } else { + let mut family_schemas = Vec::new(); + let mut sorted_fam_variations: Vec = + fam_type_def.variations.iter().cloned().collect(); + sorted_fam_variations.sort(); + + for variation in &sorted_fam_variations { + let mut ref_schema = crate::database::schema::Schema::default(); + ref_schema.obj.r#ref = Some(variation.clone()); + family_schemas.push(std::sync::Arc::new(ref_schema)); + } + + let base_alias = table_aliases + .get(&r#type.name) + .cloned() + .unwrap_or_else(|| node.parent_alias.to_string()); + select_args.push(format!("'id', {}.id", base_alias)); + let mut case_node = node.clone(); + case_node.parent_alias = base_alias.clone(); + let arc_aliases = std::sync::Arc::new(table_aliases.clone()); + case_node.parent_type_aliases = Some(arc_aliases); + + let (case_sql, _) = self.compile_one_of(&family_schemas, case_node)?; + select_args.push(format!("'type', {}", case_sql)); + } + } + } else if let Some(one_of) = &node.schema.obj.one_of { + let base_alias = table_aliases + .get(&r#type.name) + .cloned() + .unwrap_or_else(|| node.parent_alias.to_string()); + select_args.push(format!("'id', {}.id", base_alias)); + let mut case_node = node.clone(); + case_node.parent_alias = base_alias.clone(); + let arc_aliases = std::sync::Arc::new(table_aliases.clone()); + case_node.parent_type_aliases = Some(arc_aliases); + + let (case_sql, _) = self.compile_one_of(one_of, case_node)?; + select_args.push(format!("'type', {}", case_sql)); + } + + Ok(select_args) + } + + fn compile_object( + &mut self, + props: &std::collections::BTreeMap>, + node: Node<'a>, + ) -> Result<(String, String), String> { + let mut build_args = Vec::new(); + for (k, v) in props { + let next_path = if node.stem_path.is_empty() { + k.clone() + } else { + format!("{}.{}", node.stem_path, k) + }; + + let mut child_node = node.clone(); + child_node.property_name = Some(k.clone()); + child_node.depth += 1; + child_node.stem_path = next_path; + child_node.schema = std::sync::Arc::clone(v); + + let (child_sql, val_type) = self.compile_node(child_node)?; + if val_type == "abort" { + continue; + } + build_args.push(format!("'{}', {}", k, child_sql)); + } + let combined = format!("jsonb_build_object({})", build_args.join(", ")); + Ok((combined, "object".to_string())) + } + + fn compile_one_of( + &mut self, + schemas: &[Arc], + node: Node<'a>, + ) -> Result<(String, String), String> { + let mut case_statements = Vec::new(); + let type_col = if let Some(prop) = &node.property_name { + format!("{}_type", prop) + } else { + "type".to_string() + }; + + for option_schema in schemas { + if let Some(ref_id) = &option_schema.obj.r#ref { + // Find the physical type this ref maps to + let base_type_name = ref_id.split('.').next_back().unwrap_or("").to_string(); + + // Generate the nested SQL for this specific target type + let mut child_node = node.clone(); + child_node.schema = std::sync::Arc::clone(option_schema); + let (val_sql, _) = self.compile_node(child_node)?; + + case_statements.push(format!( + "WHEN {}.{} = '{}' THEN ({})", + node.parent_alias, type_col, base_type_name, val_sql + )); + } + } + + if case_statements.is_empty() { + return Ok(("NULL".to_string(), "string".to_string())); + } + + case_statements.sort(); + + let sql = format!("CASE {} ELSE NULL END", case_statements.join(" ")); + + Ok((sql, "object".to_string())) + } + + fn compile_from_clause( + &mut self, + r#type: &crate::database::r#type::Type, ) -> (std::collections::HashMap, Vec) { let mut table_aliases = std::collections::HashMap::new(); let mut from_clauses = Vec::new(); - for (i, table_name) in type_def.hierarchy.iter().enumerate() { - *alias_counter += 1; - let alias = format!("{}_{}", table_name, alias_counter); + for (i, table_name) in r#type.hierarchy.iter().enumerate() { + self.alias_counter += 1; + let alias = format!("{}_{}", table_name, self.alias_counter); table_aliases.insert(table_name.clone(), alias.clone()); if i == 0 { from_clauses.push(format!("agreego.{} {}", table_name, alias)); } else { - let prev_alias = format!("{}_{}", type_def.hierarchy[i - 1], *alias_counter - 1); + let prev_alias = format!("{}_{}", r#type.hierarchy[i - 1], self.alias_counter - 1); from_clauses.push(format!( "JOIN agreego.{} {} ON {}.id = {}.id", table_name, alias, alias, prev_alias @@ -461,44 +415,20 @@ impl SqlCompiler { (table_aliases, from_clauses) } - fn map_properties_to_aliases( - &self, - schema: &crate::database::schema::Schema, - type_def: &crate::database::r#type::Type, + fn compile_select_clause( + &mut self, + r#type: &'a crate::database::r#type::Type, table_aliases: &std::collections::HashMap, - parent_alias: &str, - filter_keys: &[String], - is_stem_query: bool, - depth: usize, - current_path: &str, - alias_counter: &mut usize, + node: Node<'a>, ) -> Result, String> { let mut select_args = Vec::new(); - let grouped_fields = type_def.grouped_fields.as_ref().and_then(|v| v.as_object()); - let merged_props = self.get_merged_properties(schema); + let grouped_fields = r#type.grouped_fields.as_ref().and_then(|v| v.as_object()); + let merged_props = self.get_merged_properties(node.schema.as_ref()); let mut sorted_keys: Vec<&String> = merged_props.keys().collect(); sorted_keys.sort(); for prop_key in sorted_keys { let prop_schema = &merged_props[prop_key]; - let mut owner_alias = table_aliases - .get("entity") - .cloned() - .unwrap_or_else(|| format!("{}_t_err", parent_alias)); - - if let Some(gf) = grouped_fields { - for (t_name, fields_val) in gf { - if let Some(fields_arr) = fields_val.as_array() { - if fields_arr.iter().any(|v| v.as_str() == Some(prop_key)) { - owner_alias = table_aliases - .get(t_name) - .cloned() - .unwrap_or_else(|| parent_alias.to_string()); - break; - } - } - } - } let is_object_or_array = match &prop_schema.obj.type_ { Some(crate::database::schema::SchemaTypeOrArray::Single(s)) => { @@ -510,38 +440,55 @@ impl SqlCompiler { _ => false, }; - let is_primitive = prop_schema.obj.r#ref.is_none() - && prop_schema.obj.items.is_none() - && prop_schema.obj.properties.is_none() - && prop_schema.obj.one_of.is_none() - && !is_object_or_array; + let is_primitive = prop_schema.obj.r#ref.is_none() + && !is_object_or_array + && prop_schema.obj.family.is_none() + && prop_schema.obj.one_of.is_none(); if is_primitive { - if let Some(ft) = type_def.field_types.as_ref().and_then(|v| v.as_object()) { + if let Some(ft) = r#type.field_types.as_ref().and_then(|v| v.as_object()) { if !ft.contains_key(prop_key) { - continue; // Skip frontend virtual properties (e.g. `computer` fields, `created`) missing from physical table fields + continue; // Skip frontend virtual properties missing from physical table fields } } } - let next_path = if current_path.is_empty() { + let mut owner_alias = table_aliases + .get("entity") + .cloned() + .unwrap_or_else(|| format!("{}_t_err", node.parent_alias)); + + if let Some(gf) = grouped_fields { + for (t_name, fields_val) in gf { + if let Some(fields_arr) = fields_val.as_array() { + if fields_arr.iter().any(|v| v.as_str() == Some(prop_key)) { + owner_alias = table_aliases + .get(t_name) + .cloned() + .unwrap_or_else(|| node.parent_alias.to_string()); + break; + } + } + } + } + + let mut child_node = node.clone(); + child_node.parent_alias = owner_alias.clone(); + let arc_aliases = std::sync::Arc::new(table_aliases.clone()); + child_node.parent_type_aliases = Some(arc_aliases); + child_node.parent_type = Some(r#type); + child_node.property_name = Some(prop_key.clone()); + child_node.depth += 1; + let next_path = if node.stem_path.is_empty() { prop_key.clone() } else { - format!("{}.{}", current_path, prop_key) + format!("{}.{}", node.stem_path, prop_key) }; - let (val_sql, val_type) = self.walk_schema( - prop_schema, - &owner_alias, - Some(table_aliases), - Some(type_def), // Pass current type_def as parent_type_def for child properties - Some(prop_key), - filter_keys, - is_stem_query, - depth + 1, - next_path, - alias_counter, - )?; + child_node.stem_path = next_path; + child_node.schema = std::sync::Arc::clone(prop_schema); + + let (val_sql, val_type) = self.compile_node(child_node)?; if val_type != "abort" { select_args.push(format!("'{}', {}", prop_key, val_sql)); @@ -550,24 +497,18 @@ impl SqlCompiler { Ok(select_args) } - fn build_filter_where_clauses( + fn compile_where_clause( &self, - schema: &crate::database::schema::Schema, - type_def: &crate::database::r#type::Type, - table_aliases: &std::collections::HashMap, - parent_alias: &str, - parent_table_aliases: Option<&std::collections::HashMap>, - parent_type_def: Option<&crate::database::r#type::Type>, - prop_name: Option<&str>, - filter_keys: &[String], - current_path: &str, + r#type: &'a crate::database::r#type::Type, + type_aliases: &std::collections::HashMap, + node: Node<'a>, ) -> Result, String> { - let base_alias = table_aliases - .get(&type_def.name) + let base_alias = type_aliases + .get(&r#type.name) .cloned() .unwrap_or_else(|| "err".to_string()); - let entity_alias = table_aliases + let entity_alias = type_aliases .get("entity") .cloned() .unwrap_or_else(|| base_alias.clone()); @@ -575,18 +516,90 @@ impl SqlCompiler { let mut where_clauses = Vec::new(); where_clauses.push(format!("NOT {}.archived", entity_alias)); - for (i, filter_key) in filter_keys.iter().enumerate() { + 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)?; + + Ok(where_clauses) + } + + fn resolve_filter_alias( + r#type: &crate::database::r#type::Type, + type_aliases: &std::collections::HashMap, + base_alias: &str, + field_name: &str, + ) -> String { + if let Some(gf) = r#type.grouped_fields.as_ref().and_then(|v| v.as_object()) { + for (t_name, fields_val) in gf { + if let Some(fields_arr) = fields_val.as_array() { + if fields_arr.iter().any(|v| v.as_str() == Some(field_name)) { + return type_aliases.get(t_name).cloned().unwrap_or_else(|| base_alias.to_string()); + } + } + } + } + base_alias.to_string() + } + + fn determine_sql_cast_and_op( + r#type: &crate::database::r#type::Type, + node: &Node, + field_name: &str, + ) -> (&'static str, bool) { + let mut is_ilike = false; + let mut cast = ""; + + if let Some(field_types) = r#type.field_types.as_ref().and_then(|v| v.as_object()) { + if let Some(pg_type_val) = field_types.get(field_name) { + if let Some(pg_type) = pg_type_val.as_str() { + if pg_type == "uuid" { + cast = "::uuid"; + } else if pg_type == "boolean" || pg_type == "bool" { + cast = "::boolean"; + } else if pg_type.contains("timestamp") || pg_type == "timestamptz" || pg_type == "date" { + cast = "::timestamptz"; + } else if pg_type == "numeric" + || pg_type.contains("int") + || pg_type == "real" + || pg_type == "double precision" + { + cast = "::numeric"; + } else if pg_type == "text" || pg_type.contains("char") { + let mut is_enum = false; + if let Some(props) = &node.schema.obj.properties { + if let Some(ps) = props.get(field_name) { + is_enum = ps.obj.enum_.is_some(); + } + } + if !is_enum { + is_ilike = true; + } + } + } + } + } + (cast, is_ilike) + } + + fn compile_filter_conditions( + &self, + r#type: &crate::database::r#type::Type, + type_aliases: &std::collections::HashMap, + node: &Node, + base_alias: &str, + where_clauses: &mut Vec, + ) { + for (i, filter_key) in self.filter_keys.iter().enumerate() { let mut parts = filter_key.split(':'); let full_field_path = parts.next().unwrap_or(filter_key); let op = parts.next().unwrap_or("$eq"); - let field_name = if current_path.is_empty() { + let field_name = if node.stem_path.is_empty() { if full_field_path.contains('.') || full_field_path.contains('#') { continue; } full_field_path } else { - let prefix = format!("{}.", current_path); + let prefix = format!("{}.", node.stem_path); if full_field_path.starts_with(&prefix) { let remainder = &full_field_path[prefix.len()..]; if remainder.contains('.') || remainder.contains('#') { @@ -598,55 +611,8 @@ impl SqlCompiler { } }; - let mut filter_alias = base_alias.clone(); - - if let Some(gf) = type_def.grouped_fields.as_ref().and_then(|v| v.as_object()) { - for (t_name, fields_val) in gf { - if let Some(fields_arr) = fields_val.as_array() { - if fields_arr.iter().any(|v| v.as_str() == Some(field_name)) { - filter_alias = table_aliases - .get(t_name) - .cloned() - .unwrap_or_else(|| base_alias.clone()); - break; - } - } - } - } - - let mut is_ilike = false; - let mut cast = ""; - - if let Some(field_types) = type_def.field_types.as_ref().and_then(|v| v.as_object()) { - if let Some(pg_type_val) = field_types.get(field_name) { - if let Some(pg_type) = pg_type_val.as_str() { - if pg_type == "uuid" { - cast = "::uuid"; - } else if pg_type == "boolean" || pg_type == "bool" { - cast = "::boolean"; - } else if pg_type.contains("timestamp") || pg_type == "timestamptz" || pg_type == "date" - { - cast = "::timestamptz"; - } else if pg_type == "numeric" - || pg_type.contains("int") - || pg_type == "real" - || pg_type == "double precision" - { - cast = "::numeric"; - } else if pg_type == "text" || pg_type.contains("char") { - let mut is_enum = false; - if let Some(props) = &schema.obj.properties { - if let Some(ps) = props.get(field_name) { - is_enum = ps.obj.enum_.is_some(); - } - } - if !is_enum { - is_ilike = true; - } - } - } - } - } + let filter_alias = Self::resolve_filter_alias(r#type, type_aliases, base_alias, field_name); + let (cast, is_ilike) = Self::determine_sql_cast_and_op(r#type, node, field_name); let param_index = i + 1; let p_val = format!("${}#>>'{{}}'", param_index); @@ -663,31 +629,13 @@ impl SqlCompiler { )); } else { let sql_op = match op { - "$eq" => { - if is_ilike { - "ILIKE" - } else { - "=" - } - } - "$ne" => { - if is_ilike { - "NOT ILIKE" - } else { - "!=" - } - } + "$eq" => if is_ilike { "ILIKE" } else { "=" }, + "$ne" => if is_ilike { "NOT ILIKE" } else { "!=" }, "$gt" => ">", "$gte" => ">=", "$lt" => "<", "$lte" => "<=", - _ => { - if is_ilike { - "ILIKE" - } else { - "=" - } - } + _ => if is_ilike { "ILIKE" } else { "=" }, }; let param_sql = if is_ilike && (op == "$eq" || op == "$ne") { @@ -702,171 +650,72 @@ impl SqlCompiler { )); } } + } - if let Some(prop) = prop_name { - // Find what type the parent alias is actually mapping to - let mut relation_alias = parent_alias.to_string(); - + fn compile_relation_conditions( + &self, + r#type: &crate::database::r#type::Type, + 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; - if let Some(parent_type) = parent_type_def { - if let Some(relation) = self - .db - .get_relation(&parent_type.name, &type_def.name, prop, None) - { + + 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 mut possible_relation_alias = None; - if let Some(pta) = parent_table_aliases { + if let Some(pta) = &node.parent_type_aliases { if let Some(a) = pta.get(&relation.source_type) { - possible_relation_alias = Some(a.clone()); + relation_alias = a.clone(); } else if let Some(a) = pta.get(&relation.destination_type) { - possible_relation_alias = Some(a.clone()); + relation_alias = a.clone(); } } - if let Some(pa) = possible_relation_alias { - relation_alias = pa; - } - // 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!( - "{}.{} = {}.{}", - relation_alias, source_col, base_alias, dest_col - )); + 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) - { - // Parent is the destination - where_clauses.push(format!( - "{}.{} = {}.{}", - base_alias, source_col, relation_alias, dest_col - )); + } 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 !relation_resolved { - // Fallback heuristics for unmapped polymorphism or abstract models - if prop == "target" || prop == "source" { - if let Some(pta) = parent_table_aliases { - if let Some(a) = pta.get("relationship") { - relation_alias = a.clone(); - } - } - where_clauses.push(format!( - "{}.id = {}.{}_id", - base_alias, relation_alias, prop - )); - } else { - where_clauses.push(format!("{}.parent_id = {}.id", base_alias, relation_alias)); - } + 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)); } } - - Ok(where_clauses) + Ok(()) } - fn compile_inline_object( + fn get_merged_properties( &self, - props: &std::collections::BTreeMap>, - parent_alias: &str, - parent_table_aliases: Option<&std::collections::HashMap>, - parent_type_def: Option<&crate::database::r#type::Type>, - filter_keys: &[String], - is_stem_query: bool, - depth: usize, - current_path: String, - alias_counter: &mut usize, - ) -> Result<(String, String), String> { - let mut build_args = Vec::new(); - for (k, v) in props { - let next_path = if current_path.is_empty() { - k.clone() - } else { - format!("{}.{}", current_path, k) - }; + schema: &crate::database::schema::Schema, + ) -> std::collections::BTreeMap> { + let mut props = std::collections::BTreeMap::new(); - let (child_sql, val_type) = self.walk_schema( - v, - parent_alias, - parent_table_aliases, - parent_type_def, - Some(k), - filter_keys, - is_stem_query, - depth + 1, - next_path, - alias_counter, - )?; - if val_type == "abort" { - continue; - } - build_args.push(format!("'{}', {}", k, child_sql)); - } - let combined = format!("jsonb_build_object({})", build_args.join(", ")); - Ok((combined, "object".to_string())) - } - - fn compile_one_of( - &self, - schemas: &[Arc], - parent_alias: &str, - parent_table_aliases: Option<&std::collections::HashMap>, - parent_type_def: Option<&crate::database::r#type::Type>, - prop_name_context: Option<&str>, - filter_keys: &[String], - is_stem_query: bool, - depth: usize, - current_path: String, - alias_counter: &mut usize, - ) -> Result<(String, String), String> { - let mut case_statements = Vec::new(); - let type_col = if let Some(prop) = prop_name_context { - format!("{}_type", prop) - } else { - "type".to_string() - }; - - for option_schema in schemas { - if let Some(ref_id) = &option_schema.obj.r#ref { - // Find the physical type this ref maps to - let base_type_name = ref_id.split('.').next_back().unwrap_or("").to_string(); - - // Generate the nested SQL for this specific target type - let (val_sql, _) = self.walk_schema( - option_schema, - parent_alias, - parent_table_aliases, - parent_type_def, - prop_name_context, - filter_keys, - is_stem_query, - depth, - current_path.clone(), - alias_counter, - )?; - - case_statements.push(format!( - "WHEN {}.{} = '{}' THEN ({})", - parent_alias, type_col, base_type_name, val_sql - )); + if let Some(ref_id) = &schema.obj.r#ref { + if let Some(parent_schema) = self.db.schemas.get(ref_id) { + props.extend(self.get_merged_properties(parent_schema)); } } - if case_statements.is_empty() { - return Ok(("NULL".to_string(), "string".to_string())); + if let Some(local_props) = &schema.obj.properties { + for (k, v) in local_props { + props.insert(k.clone(), v.clone()); + } } - case_statements.sort(); - - let sql = format!("CASE {} ELSE NULL END", case_statements.join(" ")); - - Ok((sql, "object".to_string())) + props } } diff --git a/src/queryer/mod.rs b/src/queryer/mod.rs index 8082d6e..816073b 100644 --- a/src/queryer/mod.rs +++ b/src/queryer/mod.rs @@ -97,7 +97,13 @@ impl Queryer { return Ok(cached_sql.value().clone()); } - let compiler = compiler::SqlCompiler::new(self.db.clone()); + let compiler = compiler::Compiler { + db: &self.db, + filter_keys: filter_keys, + is_stem_query: stem_opt.is_some(), + alias_counter: 0, + }; + match compiler.compile(schema_id, stem_opt, filter_keys) { Ok(compiled_sql) => { self diff --git a/src/tests/types/expect/sql.rs b/src/tests/types/expect/sql.rs index 794e1dc..d1384b3 100644 --- a/src/tests/types/expect/sql.rs +++ b/src/tests/types/expect/sql.rs @@ -191,9 +191,9 @@ impl Expect { } Expr::Function(func) => { if let sqlparser::ast::FunctionArguments::List(args) = &func.args { - if let Some(sqlparser::ast::FunctionArg::Unnamed(sqlparser::ast::FunctionArgExpr::Expr( - e, - ))) = args.args.get(0) + if let Some(sqlparser::ast::FunctionArg::Unnamed( + sqlparser::ast::FunctionArgExpr::Expr(e), + )) = args.args.get(0) { Self::validate_expr(e, available_aliases, sql)?; }