From 0367eca57ed5e151e92c4bbad3c87c536bd19960 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 3 Dec 2023 21:33:08 -0500 Subject: [PATCH] Querying finally works; don't ever ask me to do this again --- .../fuel-indexer-graphql/src/query/prepare.rs | 653 ++++++++++++------ 1 file changed, 429 insertions(+), 224 deletions(-) diff --git a/packages/fuel-indexer-graphql/src/query/prepare.rs b/packages/fuel-indexer-graphql/src/query/prepare.rs index 5cccf9026..5e69384a0 100644 --- a/packages/fuel-indexer-graphql/src/query/prepare.rs +++ b/packages/fuel-indexer-graphql/src/query/prepare.rs @@ -15,17 +15,56 @@ use super::parse::ParsedOperation; #[derive(Debug, Clone)] pub struct CommonTable { pub name: String, - pub prepared_operation: PreparedOperation, + pub table_root: PreparedSelection, + pub root_entity_name: String, + pub dependency_graph: DependencyGraph, + pub fully_qualified_namespace: String, + pub group_by_fields: Vec, + pub aggregate_func_used: bool, + pub connecting_reference_column: Option, } impl std::fmt::Display for CommonTable { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{} AS ({})", self.name, self.prepared_operation) + if let PreparedSelection::Root(root) = &self.table_root { + let mut fragments = vec![self.name.clone(), "AS (SELECT".to_string()]; + let selection_str = root + .fields + .iter() + .map(|f| f.to_string()) + .collect::>() + .join(", "); + + fragments.push(selection_str); + fragments.push(format!( + "\nFROM {}.{}\n", + self.fully_qualified_namespace, + self.root_entity_name.to_lowercase() + )); + fragments.push( + self.dependency_graph + .get_sorted_joins() + .unwrap() + .to_string(), + ); + + if self.aggregate_func_used && !self.group_by_fields.is_empty() { + fragments + .push(format!("\nGROUP BY {}", self.group_by_fields.join(",\n"))); + } + + fragments.push(")".to_string()); + + write!(f, "{}", fragments.join(" ")) + } else { + // TODO: This arm shouldn't be possible, but we should put guardrails here. + write!(f, "") + } } } /// Contains necessary information for generating joins between database tables. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct DependencyGraph { pub table_node_idx_map: HashMap, pub graph: Graph, @@ -206,6 +245,113 @@ impl DependencyGraph { } } +/// PreparedOperation contains all of the necessary operation information to +/// generate a correct SQL query. +#[derive(Debug, Clone)] +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 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.db_type { + DbType::Postgres => { + let mut fragments = vec![]; + + if !self.ctes.is_empty() { + let cte_fragment = format!( + "WITH {}", + self.ctes + .iter() + .map(|cte| cte.to_string()) + .collect::>() + .join(",\n") + ); + fragments.append(&mut vec![ + cte_fragment, + format!("SELECT {}", self.selection_set), + format!("FROM {}s", self.root_object_name), + self.joins.to_string(), + ]); + } else { + fragments.append(&mut vec![ + format!("SELECT {}", self.selection_set), + format!( + "FROM {}.{}", + self.fully_qualified_namespace, self.root_object_name + ), + self.joins.to_string(), + ]); + } + + fragments.push( + self.query_parameters + .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), + ]); + + write!(f, "{}", fragments.join("\n")) + } + } + } +} + +/// 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) { + match prepared_selection { + PreparedSelection::Field(f) => (false, vec![f.path.clone()]), + PreparedSelection::IdReference { path, .. } => (false, vec![path.clone()]), + PreparedSelection::List(_) => (true, 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); + v.append(&mut fields); + list_exists |= list_exists_in_subselections; + } + + (list_exists, 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); + v.append(&mut fields); + list_exists |= list_exists_in_subselections; + } + + (list_exists, v) + } + } +} + /// Prepares a string for a `ParsedOperation` for use in a database query. pub fn prepare_operation( parsed_operation: &ParsedOperation, @@ -215,17 +361,15 @@ pub fn prepare_operation( match parsed_operation.ty { OperationType::Query => match db_type { DbType::Postgres => { - let mut query_parameters = QueryParams::default(); - let mut dependency_graph = DependencyGraph::default(); - let mut common_tables: Vec = vec![]; - - let selection_set = prepare_selection( - parsed_operation.selections[0].clone(), + let ( + selection_set, + dependency_graph, + common_tables, + mut query_parameters, + ) = prepare_query_selections( schema, + parsed_operation.selections.clone(), db_type, - &mut dependency_graph, - &mut query_parameters, - &mut common_tables, )?; let root_object_name = selection_set.root_name()?; @@ -277,6 +421,7 @@ impl std::fmt::Display for Field { pub struct Object { name: Option, fields: Vec, + _inside_cte: bool, } impl std::fmt::Display for Object { @@ -322,11 +467,21 @@ impl std::fmt::Display for Root { pub struct List { name: String, selection: Box, + inside_cte: bool, } impl std::fmt::Display for List { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}, json_agg({})", self.name, self.selection) + if self.inside_cte { + write!( + f, + "json_agg({}) AS {}", + self.selection, + self.name.replace('\'', "") + ) + } else { + write!(f, "{}, json_agg({})", self.name, self.selection) + } } } @@ -337,6 +492,7 @@ pub enum PreparedSelection { List(List), Object(Object), Root(Root), + IdReference { name: String, path: String }, } impl std::fmt::Display for PreparedSelection { @@ -346,6 +502,9 @@ impl std::fmt::Display for PreparedSelection { PreparedSelection::Field(field) => write!(f, "{field}"), PreparedSelection::List(l) => write!(f, "{l}"), PreparedSelection::Root(r) => write!(f, "{r}"), + PreparedSelection::IdReference { name, path } => { + write!(f, "{path} AS {name}") + } } } } @@ -353,119 +512,52 @@ impl std::fmt::Display for PreparedSelection { impl PreparedSelection { fn root_name(&self) -> GraphqlResult { match &self { - PreparedSelection::Field(_) => Err(GraphqlError::QueryError( - "Field cannot have a root name".to_string(), - )), - PreparedSelection::List(_) => Err(GraphqlError::QueryError( - "List cannot have a root name".to_string(), - )), - PreparedSelection::Object(_) => Err(GraphqlError::QueryError( - "Object cannot have a root name".to_string(), - )), PreparedSelection::Root(r) => Ok(r.root_entity.to_lowercase()), + _ => Err(GraphqlError::QueryError( + "Non-root type cannot have a root name".to_string(), + )), } } } -/// PreparedOperation contains all of the necessary operation information to -/// generate a correct SQL query. -#[derive(Debug, Clone)] -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 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // TODO: Get limit for single object response - match self.db_type { - DbType::Postgres => { - let mut fragments = vec![]; - - if !self.ctes.is_empty() { - let cte_fragment = format!( - "WITH {}", - self.ctes - .iter() - .map(|cte| cte.to_string()) - .collect::>() - .join(",") - ); - fragments.push(cte_fragment); - } - - fragments.append(&mut vec![ - format!("SELECT {}", self.selection_set), - format!( - "FROM {}.{}", - self.fully_qualified_namespace, self.root_object_name - ), - self.joins.to_string(), - ]); - - fragments.push( - self.query_parameters - .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), - ]); - - write!(f, "{}", fragments.join("\n")) - } - } - } -} - -/// 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) { - match prepared_selection { - PreparedSelection::Field(f) => (false, vec![f.path.clone()]), - PreparedSelection::List(_) => (true, 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); - v.append(&mut fields); - list_exists |= list_exists_in_subselections; - } - - (list_exists, 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); - v.append(&mut fields); - list_exists |= list_exists_in_subselections; - } - (list_exists, v) +fn prepare_query_selections( + schema: &ParsedGraphQLSchema, + parsed_selections: Vec, + db_type: &DbType, +) -> GraphqlResult<( + PreparedSelection, + DependencyGraph, + Vec, + QueryParams, +)> { + let mut query_parameters = QueryParams::default(); + let mut dependency_graph = DependencyGraph::default(); + let mut common_tables: Vec = vec![]; + + // TODO: This probably needs to be an iterator + let prepared_query_selections = match parsed_selections[0].clone() { + root @ ParsedSelection::QueryRoot { .. } => prepare_selection( + root, + schema, + db_type, + &mut dependency_graph, + &mut query_parameters, + &mut common_tables, + false, + )?, + _ => { + return Err(GraphqlError::QueryError( + "Root level node needs to be a root object".to_string(), + )) } - } + }; + + Ok(( + prepared_query_selections, + dependency_graph, + common_tables, + query_parameters, + )) } /// Parses a `ParsedSelection` into a collection of strings that will @@ -477,6 +569,7 @@ pub fn prepare_selection( dependency_graph: &mut DependencyGraph, query_parameters: &mut QueryParams, common_tables: &mut Vec, + inside_cte: bool, ) -> GraphqlResult { match db_type { DbType::Postgres => { @@ -508,34 +601,6 @@ pub fn prepare_selection( entity_type, .. } => { - if !is_part_of_list { - if let Some(fk_map) = schema - .foreign_key_mappings() - .get(&entity_type.to_lowercase()) - { - if let Some((fk_table, fk_field)) = - fk_map.get(&name.to_string()) - { - let referring_node = dependency_graph.add_node(format!( - "{}.{}", - schema.fully_qualified_namespace(), - entity_type.to_lowercase() - )); - let primary_node = dependency_graph.add_node(format!( - "{}.{}", - schema.fully_qualified_namespace(), - fk_table.clone() - )); - dependency_graph.add_edge( - referring_node, - primary_node, - name.clone().to_string(), - fk_field.clone(), - ); - } - } - } - let mut obj_fields: Vec = vec![]; query_parameters.add_params( arguments.to_owned(), @@ -547,17 +612,17 @@ pub fn prepare_selection( ); for sn in fields { - if let ParsedSelection::List { - name: list_name, - obj_type, - .. - } = &sn + if let Some(fk_map) = schema + .foreign_key_mappings() + .get(&entity_type.to_lowercase()) { - if let Some(fk_map) = schema - .foreign_key_mappings() - .get(&entity_type.to_lowercase()) + if let ParsedSelection::List { + name: list_name, + obj_type, + .. + } = &sn { - if let Some((_fk_table, fk_field)) = + if let Some((_, fk_field)) = fk_map.get(&list_name.to_string()) { let outer_obj_node = @@ -596,6 +661,25 @@ pub fn prepare_selection( fk_field.clone(), ); } + } else if let Some((fk_table, fk_field)) = + fk_map.get(&name.to_string()) + { + let referring_node = dependency_graph.add_node(format!( + "{}.{}", + schema.fully_qualified_namespace(), + entity_type.to_lowercase() + )); + let primary_node = dependency_graph.add_node(format!( + "{}.{}", + schema.fully_qualified_namespace(), + fk_table.clone() + )); + dependency_graph.add_edge( + referring_node, + primary_node, + name.clone().to_string(), + fk_field.clone(), + ); } } @@ -606,6 +690,7 @@ pub fn prepare_selection( dependency_graph, query_parameters, common_tables, + inside_cte, )?; obj_fields.push(prepared_selection); } @@ -621,6 +706,7 @@ pub fn prepare_selection( None }, fields: obj_fields, + _inside_cte: inside_cte, }; Ok(PreparedSelection::Object(object)) @@ -650,7 +736,9 @@ pub fn prepare_selection( dependency_graph, query_parameters, common_tables, + inside_cte, )?), + inside_cte, }; Ok(PreparedSelection::List(list)) @@ -686,7 +774,7 @@ pub fn prepare_selection( .foreign_key_mappings() .get(&root_entity_type.to_lowercase()) { - if let Some((_fk_table, fk_field)) = + if let Some((_, fk_field)) = fk_map.get(&list_name.to_string()) { let outer_obj_node = dependency_graph @@ -737,6 +825,7 @@ pub fn prepare_selection( dependency_graph, query_parameters, common_tables, + inside_cte, )?; obj_fields.push(prepared_selection); } @@ -778,10 +867,31 @@ pub fn prepare_selection( &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(), + ); + } + } + let prepared_cte_query_root = PreparedSelection::Root(Root { root_entity: root_entity_type.to_string(), fields: obj_fields, @@ -790,40 +900,29 @@ pub fn prepare_selection( let (aggregate_func_used, group_by_fields) = get_fields_from_selection(&prepared_cte_query_root); - let cte_op = PreparedOperation { - selection_set: prepared_cte_query_root, - ctes: common_tables.clone(), - group_by_fields, + 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(), - root_object_name: root_entity_type.to_lowercase(), - joins: cte_dep_graph.get_sorted_joins()?, - query_parameters: query_parameters.clone(), - db_type: db_type.clone(), + group_by_fields, aggregate_func_used, - }; - - let cte = CommonTable { - name: name.to_string(), - prepared_operation: cte_op, + connecting_reference_column: None, }; let selection = PreparedSelection::Field(Field { name: field_name, - path: format!( - "{}.{}", - cte.name.clone(), - cte.name.clone() - ), + 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], }); - common_tables.push(cte); - Ok(query_root) } } @@ -848,61 +947,166 @@ pub fn prepare_selection( obj_fields.push(PreparedSelection::Field(cursor_field)); } - // TODO: Replace this with an object/node struct so we don't have to do if/let - if let Some(ParsedSelection::Object { - name, - parent_entity, - alias, - fields, - is_part_of_list, - arguments, - entity_type, - }) = *node.clone() - { - for selection_node in fields { - if let ParsedSelection::List { - name, - alias, - node: list_node, - obj_type, - } = selection_node - { - if let ParsedSelection::Object { - name, - parent_entity, - alias, - fields, - is_part_of_list, - arguments, - entity_type, - } = *list_node + if let Some(sn) = *node.clone() { + if let ParsedSelection::Object { + name, + fields, + entity_type, + .. + } = sn.clone() + { + let mut node_obj_fields = vec![]; + let mut cte_dep_graph = DependencyGraph { + fully_qualified_namespace: schema + .fully_qualified_namespace(), + ..Default::default() + }; + for f in fields { + if let ParsedSelection::List { + name: list_name, + obj_type, + node: inner_obj, + .. + } = &f { - // TODO: - // This is a list of objects inside an Edge type, - // we need to make a CTE for this nested object + if let Some(fk_map) = schema + .foreign_key_mappings() + .get(&entity_type.to_lowercase()) + { + if let Some((_, fk_field)) = + fk_map.get(&list_name.to_string()) + { + let outer_obj_node = + cte_dep_graph.add_node(format!( + "{}.{}", + schema.fully_qualified_namespace(), + entity_type.to_lowercase() + )); + let inner_obj_node = + cte_dep_graph.add_node(format!( + "{}.{}", + schema.fully_qualified_namespace(), + obj_type.to_lowercase() + )); + let connecting_node = + cte_dep_graph.add_node(format!( + "{}.{}s_{}s", + schema.fully_qualified_namespace(), + entity_type.to_lowercase(), + obj_type.to_lowercase(), + )); + + cte_dep_graph.add_edge( + outer_obj_node, + connecting_node, + fk_field.clone(), + format!( + "{}_{fk_field}", + entity_type.to_lowercase() + ), + ); + cte_dep_graph.add_edge( + connecting_node, + inner_obj_node, + format!( + "{}_{fk_field}", + obj_type.to_lowercase() + ), + fk_field.clone(), + ); + } + } + if let ParsedSelection::Object { + name, + parent_entity, + // alias, + // fields, + // is_part_of_list, + // arguments, + entity_type, + .. + } = *inner_obj.clone() + { + let reference_col_name = format!( + "{}_id", + parent_entity.to_lowercase() + ); + let reference_col = + PreparedSelection::IdReference { + name: reference_col_name.clone(), + path: format!( + "{}.{}.id", + schema.fully_qualified_namespace(), + parent_entity.to_lowercase() + ), + }; + let mut inner_obj_fields = + vec![reference_col.clone()]; + let prepared_selection = prepare_selection( + f.clone(), + schema, + db_type, + &mut cte_dep_graph, + query_parameters, + common_tables, + true, + )?; + inner_obj_fields.push(prepared_selection); + + let prepared_cte_query_root = + PreparedSelection::Root(Root { + root_entity: entity_type.clone(), + fields: inner_obj_fields.clone(), + }); + + 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: parent_entity.to_string(), + dependency_graph: cte_dep_graph.clone(), + fully_qualified_namespace: schema + .fully_qualified_namespace(), + group_by_fields, + aggregate_func_used, + connecting_reference_column: Some( + reference_col_name, + ), + }; + common_tables.push(cte); + + node_obj_fields.push(PreparedSelection::Field( + Field { + name: format!("'{}'", name.clone()), + path: format!( + "{}.{}", + name.clone(), + name + ), + }, + )); + } } else { - let prepared_selection = prepare_selection( - *list_node, + node_obj_fields.push(prepare_selection( + f.clone(), schema, db_type, dependency_graph, query_parameters, common_tables, - )?; - obj_fields.push(prepared_selection); + inside_cte, + )?); } - // manually parse it and do something similar to CTE work above - } else { - let prepared_selection = prepare_selection( - selection_node, - schema, - db_type, - dependency_graph, - query_parameters, - common_tables, - )?; - obj_fields.push(prepared_selection); } + obj_fields.push(PreparedSelection::Object(Object { + name: Some(format!("'{name}'")), + fields: node_obj_fields, + _inside_cte: inside_cte, + })); } } @@ -910,6 +1114,7 @@ pub fn prepare_selection( let object = Object { name: None, fields: obj_fields, + _inside_cte: inside_cte, }; Ok(PreparedSelection::Object(object))