diff --git a/packages/fuel-indexer-graphql/src/arguments.rs b/packages/fuel-indexer-graphql/src/arguments.rs index 28ffb27b4..1ab1b9994 100644 --- a/packages/fuel-indexer-graphql/src/arguments.rs +++ b/packages/fuel-indexer-graphql/src/arguments.rs @@ -90,6 +90,7 @@ impl QueryParams { query_clause } + // TODO: Allow for non-id field to be used for pagination. pub(crate) fn parse_pagination(&mut self, db_type: &DbType) { match db_type { DbType::Postgres => { diff --git a/packages/fuel-indexer-graphql/src/lib.rs b/packages/fuel-indexer-graphql/src/lib.rs index 174711bfd..80c7d0c55 100644 --- a/packages/fuel-indexer-graphql/src/lib.rs +++ b/packages/fuel-indexer-graphql/src/lib.rs @@ -53,9 +53,10 @@ pub enum GraphqlError { } #[derive(Debug, Clone)] -pub enum ParsedQueryKind { +pub enum QueryKind { Object, Connection, + Cte, } /// The type of selection that can be present in a user's operation. @@ -86,7 +87,7 @@ pub enum ParsedSelection { alias: Option>, fields: Vec, arguments: Vec, - kind: ParsedQueryKind, + kind: QueryKind, root_entity_type: String, }, PageInfo { diff --git a/packages/fuel-indexer-graphql/src/query/parse.rs b/packages/fuel-indexer-graphql/src/query/parse.rs index 977711e60..a61fc4696 100644 --- a/packages/fuel-indexer-graphql/src/query/parse.rs +++ b/packages/fuel-indexer-graphql/src/query/parse.rs @@ -12,7 +12,7 @@ use fuel_indexer_lib::graphql::{parser::InternalType, ParsedGraphQLSchema}; use crate::{ arguments::{parse_argument_into_param, ParamType}, - GraphqlError, GraphqlResult, ParsedQueryKind, ParsedSelection, + GraphqlError, GraphqlResult, ParsedSelection, QueryKind, }; /// Contains information about a successfully-parsed user operation. @@ -265,15 +265,12 @@ fn parse_selections( Some(&query_type), )?; let (kind, query_type) = if query_type.contains("Connection") { - ( - ParsedQueryKind::Connection, - query_type.replace("Connection", ""), - ) + (QueryKind::Connection, query_type.replace("Connection", "")) } else { - (ParsedQueryKind::Object, query_type) + (QueryKind::Object, query_type) }; - if let ParsedQueryKind::Object = kind { + if let QueryKind::Object = kind { if !arguments.iter().any(|a| { matches!( a, diff --git a/packages/fuel-indexer-graphql/src/query/prepare.rs b/packages/fuel-indexer-graphql/src/query/prepare.rs index 5e69384a0..bdcf74675 100644 --- a/packages/fuel-indexer-graphql/src/query/prepare.rs +++ b/packages/fuel-indexer-graphql/src/query/prepare.rs @@ -7,7 +7,7 @@ use indexmap::{IndexMap, IndexSet}; use petgraph::graph::{Graph, NodeIndex}; use crate::{ - arguments::QueryParams, GraphqlError, GraphqlResult, ParsedQueryKind, ParsedSelection, + arguments::QueryParams, GraphqlError, GraphqlResult, ParsedSelection, QueryKind, }; use super::parse::ParsedOperation; @@ -20,8 +20,9 @@ pub struct CommonTable { pub dependency_graph: DependencyGraph, pub fully_qualified_namespace: String, pub group_by_fields: Vec, - pub aggregate_func_used: bool, pub connecting_reference_column: Option, + pub query_params: QueryParams, + pub db_type: DbType, } impl std::fmt::Display for CommonTable { @@ -47,11 +48,16 @@ impl std::fmt::Display for CommonTable { .unwrap() .to_string(), ); + fragments.push(self.query_params.get_filtering_expression(&self.db_type)); - if self.aggregate_func_used && !self.group_by_fields.is_empty() { + if !self.group_by_fields.is_empty() { fragments .push(format!("\nGROUP BY {}", self.group_by_fields.join(",\n"))); } + fragments.append(&mut vec![ + self.query_params.get_ordering_modififer(&self.db_type), + self.query_params.get_limit(&self.db_type), + ]); fragments.push(")".to_string()); @@ -251,13 +257,11 @@ impl DependencyGraph { pub struct PreparedOperation { pub selection_set: PreparedSelection, pub ctes: Vec, - pub group_by_fields: Vec, pub fully_qualified_namespace: String, pub root_object_name: String, pub joins: Joins, pub query_parameters: QueryParams, pub db_type: DbType, - pub aggregate_func_used: bool, } impl std::fmt::Display for PreparedOperation { @@ -297,15 +301,6 @@ impl std::fmt::Display for PreparedOperation { .get_filtering_expression(&self.db_type), ); - if self.aggregate_func_used { - let mut strs = vec![format!( - "{}.{}.id", - self.fully_qualified_namespace, self.root_object_name - )]; - strs.append(&mut self.group_by_fields.clone()); - fragments.push(format!("GROUP BY {}", strs.join(",\n"))); - } - fragments.append(&mut vec![ self.query_parameters.get_ordering_modififer(&self.db_type), self.query_parameters.get_limit(&self.db_type), @@ -318,36 +313,31 @@ impl std::fmt::Display for PreparedOperation { } /// Iterates through fields of a selection to get fields that are not part of a list object. -fn get_fields_from_selection( - prepared_selection: &PreparedSelection, -) -> (bool, Vec) { +fn get_fields_from_selection(prepared_selection: &PreparedSelection) -> Vec { + // If a query has a list field, then the resultant SQL query string + // will use an aggrate JSON function. Any field that is not included + // in this aggregate function needs to be included in a GROUP BY statement. match prepared_selection { - PreparedSelection::Field(f) => (false, vec![f.path.clone()]), - PreparedSelection::IdReference { path, .. } => (false, vec![path.clone()]), - PreparedSelection::List(_) => (true, vec![]), + PreparedSelection::Field(f) => vec![f.path.clone()], + PreparedSelection::IdReference { path, .. } => vec![path.clone()], + PreparedSelection::List(_) => vec![], PreparedSelection::Object(o) => { let mut v = vec![]; - let mut list_exists = false; for f in o.fields.iter() { - let (list_exists_in_subselections, mut fields) = - get_fields_from_selection(f); + let mut fields = get_fields_from_selection(f); v.append(&mut fields); - list_exists |= list_exists_in_subselections; } - (list_exists, v) + v } PreparedSelection::Root(r) => { let mut v = vec![]; - let mut list_exists = false; for f in r.fields.iter() { - let (list_exists_in_subselections, mut fields) = - get_fields_from_selection(f); + let mut fields = get_fields_from_selection(f); v.append(&mut fields); - list_exists |= list_exists_in_subselections; } - (list_exists, v) + v } } } @@ -374,24 +364,16 @@ pub fn prepare_operation( let root_object_name = selection_set.root_name()?; - // If a query has a list field, then the resultant SQL query string - // will use an aggrate JSON function. Any field that is not included - // in this aggregate function needs to be included in a GROUP BY statement. - let (aggregate_func_used, group_by_fields) = - get_fields_from_selection(&selection_set); - query_parameters.parse_pagination(db_type); Ok(PreparedOperation { selection_set, ctes: common_tables, - group_by_fields, fully_qualified_namespace: schema.fully_qualified_namespace(), root_object_name, joins: dependency_graph.get_sorted_joins()?, query_parameters, db_type: db_type.to_owned(), - aggregate_func_used, }) } }, @@ -421,7 +403,6 @@ impl std::fmt::Display for Field { pub struct Object { name: Option, fields: Vec, - _inside_cte: bool, } impl std::fmt::Display for Object { @@ -445,20 +426,44 @@ impl std::fmt::Display for Object { /// Root object field in `PreparedSelection`. #[derive(Debug, Clone)] pub struct Root { + name: String, root_entity: String, fields: Vec, + kind: QueryKind, } impl std::fmt::Display for Root { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let fields = self - .fields - .iter() - .map(|f| f.to_string()) - .collect::>() - .join(", "); + match &self.kind { + QueryKind::Object | QueryKind::Cte => { + let fields = self + .fields + .iter() + .map(|f| f.to_string()) + .collect::>() + .join(", "); - write!(f, "json_build_object({fields})") + write!(f, "json_build_object({fields})") + } + QueryKind::Connection => { + let fragments = self + .fields + .iter() + .map(|selection| match selection { + PreparedSelection::Field(f) => { + format!("{}, json_agg({})", f.name, f.path) + } + _ => unreachable!(), + }) + .collect::>(); + write!( + f, + "json_build_object('{}', json_build_object({}))", + self.name, + fragments.join(", ") + ) + } + } } } @@ -467,20 +472,26 @@ impl std::fmt::Display for Root { pub struct List { name: String, selection: Box, - inside_cte: bool, + kind: QueryKind, } impl std::fmt::Display for List { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.inside_cte { - write!( - f, - "json_agg({}) AS {}", - self.selection, - self.name.replace('\'', "") - ) - } else { - write!(f, "{}, json_agg({})", self.name, self.selection) + match &self.kind { + QueryKind::Object => { + write!(f, "{}, json_agg({})", self.name, self.selection) + } + QueryKind::Connection => { + write!(f, "{} AS {}", self.selection, self.name.replace('\'', "")) + } + QueryKind::Cte => { + write!( + f, + "json_agg({}) AS {}", + self.selection, + self.name.replace('\'', "") + ) + } } } } @@ -510,6 +521,21 @@ impl std::fmt::Display for PreparedSelection { } impl PreparedSelection { + fn name(&self) -> String { + match &self { + PreparedSelection::Field(f) => f.name.clone(), + PreparedSelection::List(l) => l.name.clone(), + PreparedSelection::Object(o) => { + if let Some(n) = &o.name { + n.to_owned() + } else { + todo!() + } + } + PreparedSelection::Root(r) => r.root_entity.clone(), + PreparedSelection::IdReference { name, .. } => name.to_owned(), + } + } fn root_name(&self) -> GraphqlResult { match &self { PreparedSelection::Root(r) => Ok(r.root_entity.to_lowercase()), @@ -536,14 +562,14 @@ fn prepare_query_selections( // TODO: This probably needs to be an iterator let prepared_query_selections = match parsed_selections[0].clone() { - root @ ParsedSelection::QueryRoot { .. } => prepare_selection( - root, + ref root @ ParsedSelection::QueryRoot { ref kind, .. } => prepare_selection( + root.clone(), schema, db_type, &mut dependency_graph, &mut query_parameters, &mut common_tables, - false, + kind, )?, _ => { return Err(GraphqlError::QueryError( @@ -569,7 +595,7 @@ pub fn prepare_selection( dependency_graph: &mut DependencyGraph, query_parameters: &mut QueryParams, common_tables: &mut Vec, - inside_cte: bool, + query_kind: &QueryKind, ) -> GraphqlResult { match db_type { DbType::Postgres => { @@ -690,7 +716,7 @@ pub fn prepare_selection( dependency_graph, query_parameters, common_tables, - inside_cte, + query_kind, )?; obj_fields.push(prepared_selection); } @@ -706,7 +732,6 @@ pub fn prepare_selection( None }, fields: obj_fields, - _inside_cte: inside_cte, }; Ok(PreparedSelection::Object(object)) @@ -736,197 +761,206 @@ pub fn prepare_selection( dependency_graph, query_parameters, common_tables, - inside_cte, + &QueryKind::Cte, )?), - inside_cte, + kind: query_kind.clone(), }; Ok(PreparedSelection::List(list)) } ParsedSelection::QueryRoot { name, - alias, + alias: _, fields, arguments, kind, root_entity_type, - } => { - let mut obj_fields: Vec = vec![]; - match kind { - ParsedQueryKind::Object => { - query_parameters.add_params( - arguments.to_owned(), - format!( - "{}.{}", - schema.fully_qualified_namespace(), - name - ), - ); - - for selection_node in fields { - if let ParsedSelection::List { - name: list_name, - obj_type, - .. - } = &selection_node + } => match kind { + QueryKind::Object => { + let mut obj_fields: Vec = vec![]; + query_parameters.add_params( + arguments.to_owned(), + format!("{}.{}", schema.fully_qualified_namespace(), name), + ); + + for selection_node in fields { + if let ParsedSelection::List { + name: list_name, + obj_type, + .. + } = &selection_node + { + if let Some(fk_map) = schema + .foreign_key_mappings() + .get(&root_entity_type.to_lowercase()) { - if let Some(fk_map) = schema - .foreign_key_mappings() - .get(&root_entity_type.to_lowercase()) + if let Some((_, fk_field)) = + fk_map.get(&list_name.to_string()) { - if let Some((_, fk_field)) = - fk_map.get(&list_name.to_string()) - { - let outer_obj_node = dependency_graph - .add_node(format!( - "{}.{}", - schema.fully_qualified_namespace(), - root_entity_type.to_lowercase() - )); - let inner_obj_node = dependency_graph - .add_node(format!( - "{}.{}", - schema.fully_qualified_namespace(), - obj_type.to_lowercase() - )); - let connecting_node = dependency_graph - .add_node(format!( - "{}.{}s_{}s", - schema.fully_qualified_namespace(), - root_entity_type.to_lowercase(), - obj_type.to_lowercase(), - )); - - dependency_graph.add_edge( - outer_obj_node, - connecting_node, - fk_field.clone(), - format!( - "{}_{fk_field}", - root_entity_type.to_lowercase() - ), - ); - dependency_graph.add_edge( - connecting_node, - inner_obj_node, - format!( - "{}_{fk_field}", - obj_type.to_lowercase() - ), - fk_field.clone(), - ); - } + let outer_obj_node = + dependency_graph.add_node(format!( + "{}.{}", + schema.fully_qualified_namespace(), + root_entity_type.to_lowercase() + )); + let inner_obj_node = + dependency_graph.add_node(format!( + "{}.{}", + schema.fully_qualified_namespace(), + obj_type.to_lowercase() + )); + let connecting_node = + dependency_graph.add_node(format!( + "{}.{}s_{}s", + schema.fully_qualified_namespace(), + root_entity_type.to_lowercase(), + obj_type.to_lowercase(), + )); + + dependency_graph.add_edge( + outer_obj_node, + connecting_node, + fk_field.clone(), + format!( + "{}_{fk_field}", + root_entity_type.to_lowercase() + ), + ); + dependency_graph.add_edge( + connecting_node, + inner_obj_node, + format!( + "{}_{fk_field}", + obj_type.to_lowercase() + ), + fk_field.clone(), + ); } } - let prepared_selection = prepare_selection( - selection_node, - schema, - db_type, - dependency_graph, - query_parameters, - common_tables, - inside_cte, - )?; - obj_fields.push(prepared_selection); } + let prepared_selection = prepare_selection( + selection_node, + schema, + db_type, + dependency_graph, + query_parameters, + common_tables, + &kind, + )?; + obj_fields.push(prepared_selection); + } - let object = Root { - fields: obj_fields, - root_entity: root_entity_type.to_string(), - }; + let object = Root { + name: name.to_string(), + fields: obj_fields, + root_entity: root_entity_type.to_string(), + kind: QueryKind::Object, + }; - Ok(PreparedSelection::Root(object)) + Ok(PreparedSelection::Root(object)) + } + QueryKind::Connection => { + let mut cte_dep_graph = DependencyGraph { + fully_qualified_namespace: schema.fully_qualified_namespace(), + ..Default::default() + }; + + let mut obj_fields: Vec = vec![]; + for sn in fields { + let prepared_selection = prepare_selection( + sn, + schema, + db_type, + &mut cte_dep_graph, + query_parameters, + common_tables, + &QueryKind::Connection, + )?; + obj_fields.push(prepared_selection); } - ParsedQueryKind::Connection => { - let mut cte_dep_graph = DependencyGraph { - fully_qualified_namespace: schema - .fully_qualified_namespace(), - ..Default::default() - }; - let field_name = - alias.clone().map_or(format!("'{}'", name), |a| { - format!("'{}'", a.node) - }); + let field_keys = + obj_fields.iter().map(|f| f.name()).collect::>(); - query_parameters.add_params( - arguments.to_owned(), - format!( + for ct in common_tables.iter() { + if let Some(connecting_reference_column) = + &ct.connecting_reference_column + { + let referring_node = cte_dep_graph.add_node(format!( "{}.{}", schema.fully_qualified_namespace(), - root_entity_type - ), - ); - - let mut obj_fields: Vec = vec![]; - for sn in fields { - let prepared_selection = prepare_selection( - sn, - schema, - db_type, - &mut cte_dep_graph, - query_parameters, - common_tables, - true, - )?; - obj_fields.push(prepared_selection); - } - - for ct in common_tables.iter() { - if let Some(connecting_reference_column) = - &ct.connecting_reference_column - { - let referring_node = cte_dep_graph.add_node(format!( - "{}.{}", - schema.fully_qualified_namespace(), - root_entity_type.to_lowercase() - )); - let primary_node = - cte_dep_graph.add_node(ct.name.clone()); - cte_dep_graph.add_edge( - referring_node, - primary_node, - "id".to_string(), - connecting_reference_column.clone(), - ); - } + root_entity_type.to_lowercase() + )); + let primary_node = + cte_dep_graph.add_node(ct.name.clone()); + cte_dep_graph.add_edge( + referring_node, + primary_node, + "id".to_string(), + connecting_reference_column.clone(), + ); } + } - let prepared_cte_query_root = PreparedSelection::Root(Root { - root_entity: root_entity_type.to_string(), - fields: obj_fields, - }); - - let (aggregate_func_used, group_by_fields) = - get_fields_from_selection(&prepared_cte_query_root); - - let cte = CommonTable { - name: name.to_string(), - table_root: prepared_cte_query_root.clone(), - root_entity_name: root_entity_type.to_string(), - dependency_graph: cte_dep_graph.clone(), - fully_qualified_namespace: schema - .fully_qualified_namespace(), - group_by_fields, - aggregate_func_used, - connecting_reference_column: None, - }; - - let selection = PreparedSelection::Field(Field { - name: field_name, - path: cte.name.to_string(), - }); - common_tables.push(cte); - - let query_root = PreparedSelection::Root(Root { - root_entity: root_entity_type.to_string(), - fields: vec![selection], - }); + let prepared_cte_query_root = PreparedSelection::Root(Root { + name: name.to_string(), + root_entity: root_entity_type.to_string(), + fields: obj_fields, + kind: QueryKind::Cte, + }); + + let group_by_fields = + get_fields_from_selection(&prepared_cte_query_root); + + let mut cte_query_params = QueryParams::default(); + cte_query_params.add_params( + arguments.to_owned(), + format!( + "{}.{}", + schema.fully_qualified_namespace(), + root_entity_type + ), + ); + + let cte = CommonTable { + name: name.to_string(), + table_root: prepared_cte_query_root.clone(), + root_entity_name: root_entity_type.to_string(), + dependency_graph: cte_dep_graph.clone(), + fully_qualified_namespace: schema.fully_qualified_namespace(), + group_by_fields, + connecting_reference_column: None, + query_params: cte_query_params, + db_type: db_type.clone(), + }; - Ok(query_root) - } + let selections = field_keys + .into_iter() + .map(|key| { + PreparedSelection::Field(Field { + name: key.clone(), + path: format!( + "{}.{}", + name.replace('\'', ""), + key.replace('\'', "") + ), + }) + }) + .collect::>(); + + common_tables.push(cte); + + let query_root = PreparedSelection::Root(Root { + name: name.to_string(), + root_entity: root_entity_type.to_string(), + fields: selections, + kind: QueryKind::Connection, + }); + + Ok(query_root) } - } + _ => unimplemented!(), + }, ParsedSelection::PageInfo { .. } => unimplemented!(), ParsedSelection::Edge { name: _, @@ -1019,10 +1053,6 @@ pub fn prepare_selection( if let ParsedSelection::Object { name, parent_entity, - // alias, - // fields, - // is_part_of_list, - // arguments, entity_type, .. } = *inner_obj.clone() @@ -1049,20 +1079,21 @@ pub fn prepare_selection( &mut cte_dep_graph, query_parameters, common_tables, - true, + query_kind, )?; inner_obj_fields.push(prepared_selection); let prepared_cte_query_root = PreparedSelection::Root(Root { + name: name.to_string(), root_entity: entity_type.clone(), fields: inner_obj_fields.clone(), + kind: QueryKind::Cte, }); - let (aggregate_func_used, group_by_fields) = - get_fields_from_selection( - &prepared_cte_query_root, - ); + let group_by_fields = get_fields_from_selection( + &prepared_cte_query_root, + ); let cte = CommonTable { name: name.to_string(), @@ -1072,10 +1103,11 @@ pub fn prepare_selection( fully_qualified_namespace: schema .fully_qualified_namespace(), group_by_fields, - aggregate_func_used, connecting_reference_column: Some( reference_col_name, ), + query_params: QueryParams::default(), + db_type: db_type.clone(), }; common_tables.push(cte); @@ -1098,14 +1130,13 @@ pub fn prepare_selection( dependency_graph, query_parameters, common_tables, - inside_cte, + query_kind, )?); } } obj_fields.push(PreparedSelection::Object(Object { name: Some(format!("'{name}'")), fields: node_obj_fields, - _inside_cte: inside_cte, })); } } @@ -1114,7 +1145,6 @@ pub fn prepare_selection( let object = Object { name: None, fields: obj_fields, - _inside_cte: inside_cte, }; Ok(PreparedSelection::Object(object))