use crate::database::Database; use std::sync::Arc; pub struct Compiler<'a> { pub db: &'a Database, pub filter_keys: &'a [String], pub is_stem_query: bool, pub alias_counter: usize, } #[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, schema_id: &str, stem_path: Option<&str>, filter_keys: &[String], ) -> Result { let schema = self .db .schemas .get(schema_id) .ok_or_else(|| format!("Schema not found: {}", schema_id))?; 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) { stem.schema.clone() } else { return Err(format!( "Stem entity type '{}' not found in schema '{}'", path, schema_id )); } } else { return Err(format!( "Stem entity type '{}' not found in schema '{}'", path, schema_id )); } } else { std::sync::Arc::new(schema.clone()) }; let is_stem_query = stem_path.is_some(); let mut compiler = Compiler { db: &self.db, filter_keys, is_stem_query, 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 compile_node(&mut self, node: Node<'a>) -> Result<(String, String), String> { // Determine the base schema type (could be an array, object, or literal) match &node.schema.obj.type_ { Some(crate::database::schema::SchemaTypeOrArray::Single(t)) if t == "array" => { self.compile_array(node) } _ => self.compile_reference(node), } } 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) = &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(), )); } Ok(( "SELECT jsonb_agg(TODO) FROM TODO".to_string(), "array".to_string(), )) } 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, ) -> Result<(String, String), String> { 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.compile_select_clause(r#type, &table_aliases, node.clone())?; // 2.5 Inject polymorphism directly into the query object 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() } else { format!("jsonb_build_object({})", select_args.join(", ")) }; // 3. Build WHERE clauses 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) } else { jsonb_obj_sql }; let full_sql = format!( "(SELECT {} FROM {} WHERE {})", selection, from_clauses.join(" "), where_clauses.join(" AND ") ); Ok(( full_sql, if is_array { "array".to_string() } else { "object".to_string() }, )) } 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 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!("{}_{}", r#type.hierarchy[i - 1], self.alias_counter - 1); from_clauses.push(format!( "JOIN agreego.{} {} ON {}.id = {}.id", table_name, alias, alias, prev_alias )); } } (table_aliases, from_clauses) } fn compile_select_clause( &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(); 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 is_object_or_array = match &prop_schema.obj.type_ { Some(crate::database::schema::SchemaTypeOrArray::Single(s)) => { s == "object" || s == "array" } Some(crate::database::schema::SchemaTypeOrArray::Multiple(v)) => { v.contains(&"object".to_string()) || v.contains(&"array".to_string()) } _ => false, }; 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) = r#type.field_types.as_ref().and_then(|v| v.as_object()) { if !ft.contains_key(prop_key) { continue; // Skip frontend virtual properties missing from physical table fields } } } 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!("{}.{}", node.stem_path, prop_key) }; 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)); } } Ok(select_args) } fn compile_where_clause( &self, r#type: &'a crate::database::r#type::Type, type_aliases: &std::collections::HashMap, node: Node<'a>, ) -> Result, String> { let base_alias = type_aliases .get(&r#type.name) .cloned() .unwrap_or_else(|| "err".to_string()); let entity_alias = type_aliases .get("entity") .cloned() .unwrap_or_else(|| base_alias.clone()); let mut where_clauses = Vec::new(); 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, type_aliases, &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 node.stem_path.is_empty() { if full_field_path.contains('.') || full_field_path.contains('#') { continue; } full_field_path } else { 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('#') { continue; } remainder } else { continue; } }; 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); if op == "$in" || op == "$nin" { let sql_op = if op == "$in" { "IN" } else { "NOT IN" }; let subquery = format!( "(SELECT value{} FROM jsonb_array_elements_text(({})::jsonb))", cast, p_val ); where_clauses.push(format!( "{}.{} {} {}", filter_alias, field_name, sql_op, subquery )); } else { let sql_op = match op { "$eq" => if is_ilike { "ILIKE" } else { "=" }, "$ne" => if is_ilike { "NOT ILIKE" } else { "!=" }, "$gt" => ">", "$gte" => ">=", "$lt" => "<", "$lte" => "<=", _ => if is_ilike { "ILIKE" } else { "=" }, }; let param_sql = if is_ilike && (op == "$eq" || op == "$ne") { p_val } else { format!("({}){}", p_val, cast) }; where_clauses.push(format!( "{}.{} {} {}", filter_alias, field_name, sql_op, param_sql )); } } } 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 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(); 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 ) })?; let source_col = &relation.source_columns[0]; let dest_col = &relation.destination_columns[0]; 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(); } } 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(()) } fn get_merged_properties( &self, schema: &crate::database::schema::Schema, ) -> std::collections::BTreeMap> { let mut props = std::collections::BTreeMap::new(); 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(local_props) = &schema.obj.properties { for (k, v) in local_props { props.insert(k.clone(), v.clone()); } } props } }