From 4b55e976d3972b79c8f85da84ca21f5b6023733c Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 2 Dec 2023 16:08:38 -0500 Subject: [PATCH] Break things out for readability --- packages/fuel-indexer-graphql/src/dynamic.rs | 17 +- packages/fuel-indexer-graphql/src/lib.rs | 68 + packages/fuel-indexer-graphql/src/query.rs | 1289 ----------------- .../fuel-indexer-graphql/src/query/mod.rs | 2 + .../fuel-indexer-graphql/src/query/parse.rs | 327 +++++ .../fuel-indexer-graphql/src/query/prepare.rs | 920 ++++++++++++ 6 files changed, 1331 insertions(+), 1292 deletions(-) delete mode 100644 packages/fuel-indexer-graphql/src/query.rs create mode 100644 packages/fuel-indexer-graphql/src/query/mod.rs create mode 100644 packages/fuel-indexer-graphql/src/query/parse.rs create mode 100644 packages/fuel-indexer-graphql/src/query/prepare.rs diff --git a/packages/fuel-indexer-graphql/src/dynamic.rs b/packages/fuel-indexer-graphql/src/dynamic.rs index e703d557d..916b73c42 100644 --- a/packages/fuel-indexer-graphql/src/dynamic.rs +++ b/packages/fuel-indexer-graphql/src/dynamic.rs @@ -16,7 +16,10 @@ use fuel_indexer_schema::db::tables::IndexerSchema; use lazy_static::lazy_static; use serde_json::Value; -use crate::{query::ParsedOperation, GraphqlError, GraphqlResult}; +use crate::{ + query::{parse::ParsedOperation, prepare::prepare_operation}, + GraphqlError, GraphqlResult, +}; lazy_static! { /// Scalar types supported by the Fuel indexer. These should always stay up-to-date @@ -140,7 +143,11 @@ pub async fn execute_query( None, )?; - let query = parsed.prepare(schema.parsed(), &pool.database_type())?; + let query = prepare_operation( + &parsed, + schema.parsed(), + &pool.database_type(), + )?; vec![query.to_string()] } DocumentOperations::Multiple(op_defs) => { @@ -152,7 +159,11 @@ pub async fn execute_query( &exec_doc.fragments, Some(name.to_string()), )?; - let s = parsed.prepare(schema.parsed(), &pool.database_type())?; + let s = prepare_operation( + &parsed, + schema.parsed(), + &pool.database_type(), + )?; v.push(s.to_string()); } diff --git a/packages/fuel-indexer-graphql/src/lib.rs b/packages/fuel-indexer-graphql/src/lib.rs index 539854b58..174711bfd 100644 --- a/packages/fuel-indexer-graphql/src/lib.rs +++ b/packages/fuel-indexer-graphql/src/lib.rs @@ -2,6 +2,9 @@ pub mod arguments; pub mod dynamic; pub mod query; +use arguments::ParamType; +use async_graphql_parser::Positioned; +use async_graphql_value::Name; use thiserror::Error; pub type GraphqlResult = Result; @@ -48,3 +51,68 @@ pub enum GraphqlError { #[error("Could not get base entity type for field: {0:?}")] CouldNotGetBaseEntityType(String), } + +#[derive(Debug, Clone)] +pub enum ParsedQueryKind { + Object, + Connection, +} + +/// The type of selection that can be present in a user's operation. +#[derive(Debug, Clone)] +pub enum ParsedSelection { + Scalar { + name: Name, + parent_entity: String, + alias: Option>, + }, + Object { + name: Name, + parent_entity: String, + alias: Option>, + fields: Vec, + is_part_of_list: bool, + arguments: Vec, + entity_type: String, + }, + List { + name: Name, + alias: Option>, + node: Box, + obj_type: String, + }, + QueryRoot { + name: Name, + alias: Option>, + fields: Vec, + arguments: Vec, + kind: ParsedQueryKind, + root_entity_type: String, + }, + PageInfo { + name: Name, + alias: Option>, + fields: Vec, + parent_entity: String, + }, + Edge { + name: Name, + cursor: Box>, + node: Box>, + entity: String, + }, +} + +impl ParsedSelection { + /// Return name for a `ParsedSelection`. + pub fn name(&self) -> String { + match &self { + ParsedSelection::Scalar { name, .. } => name.to_string(), + ParsedSelection::Object { name, .. } => name.to_string(), + ParsedSelection::List { name, .. } => name.to_string(), + ParsedSelection::QueryRoot { name, .. } => name.to_string(), + ParsedSelection::PageInfo { name, .. } => name.to_string(), + ParsedSelection::Edge { name, .. } => name.to_string(), + } + } +} diff --git a/packages/fuel-indexer-graphql/src/query.rs b/packages/fuel-indexer-graphql/src/query.rs deleted file mode 100644 index 0570409e9..000000000 --- a/packages/fuel-indexer-graphql/src/query.rs +++ /dev/null @@ -1,1289 +0,0 @@ -use std::collections::{HashMap, VecDeque}; - -use async_graphql_parser::{ - types::{ - BaseType, Directive, FragmentDefinition, OperationDefinition, OperationType, - Selection, VariableDefinition, - }, - Positioned, -}; -use async_graphql_value::Name; -use fuel_indexer_database::DbType; -use fuel_indexer_lib::graphql::{parser::InternalType, ParsedGraphQLSchema}; -use indexmap::{IndexMap, IndexSet}; -use petgraph::graph::{Graph, NodeIndex}; - -use crate::{ - arguments::{parse_argument_into_param, ParamType, QueryParams}, - GraphqlError, GraphqlResult, -}; - -#[derive(Debug, Clone)] -pub struct CommonTable { - pub name: String, - pub prepared_operation: PreparedOperation, -} - -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) - } -} - -/// Contains information about a successfully-parsed user operation. -#[derive(Debug)] -pub struct ParsedOperation { - pub name: Option, - pub selections: Vec, - pub ty: OperationType, - pub variable_definitions: Vec>, - pub directives: Vec>, -} - -/// Contains necessary information for generating joins between database tables. -#[derive(Debug, Default)] -pub struct DependencyGraph { - pub table_node_idx_map: HashMap, - pub graph: Graph, - pub fully_qualified_namespace: String, -} - -/// Contains information about the database joins that will be needed -/// to successfully execute a user's operation. -#[derive(Debug, Default, Clone)] -pub struct Joins { - pub join_map: IndexMap>, - pub fully_qualified_namespace: String, -} - -impl std::fmt::Display for Joins { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - #[allow(clippy::type_complexity)] - let (singular_cond_joins, multi_cond_joins): ( - Vec<(&String, &IndexSet)>, - Vec<(&String, &IndexSet)>, - ) = self - .join_map - .iter() - .partition(|(_, join_set)| join_set.len() == 1); - - let mut joins = singular_cond_joins - .into_iter() - .map(|(_, j)| { - if let Some(join) = j.first() { - join.to_string() - } else { - "".to_string() - } - }) - .collect::>(); - - let mut combination_joins = multi_cond_joins - .iter() - .map(|(primary_table, join_set)| { - let conditions = join_set - .iter() - .map(|j| { - format!( - "{}.{} = {}.{}", - j.referring_table, - j.referring_field, - j.primary_table, - j.fk_field - ) - }) - .collect::>() - .join(" AND "); - format!("INNER JOIN {primary_table} ON {conditions}") - }) - .collect::>(); - joins.append(&mut combination_joins); - - write!(f, "{}", joins.join("\n")) - } -} - -/// Contains information necessary for generating database joins. -#[derive(Debug, Default, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct Join { - pub primary_table: String, - pub referring_table: String, - pub referring_field: String, - pub fk_field: String, - pub join_type: String, -} - -impl std::fmt::Display for Join { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "INNER JOIN {} ON {}.{} = {}.{}", - self.primary_table, - self.referring_table, - self.referring_field, - self.primary_table, - self.fk_field - ) - } -} - -impl DependencyGraph { - /// Add a new node to dependency graph. - fn add_node(&mut self, table: String) -> NodeIndex { - if let Some(existing_node_idx) = self.table_node_idx_map.get(&table) { - *existing_node_idx - } else { - let new_node_idx = self.graph.add_node(table.clone()); - self.table_node_idx_map.insert(table, new_node_idx); - new_node_idx - } - } - - /// Add an edge between two existing nodes. - fn add_edge( - &mut self, - parent: NodeIndex, - child: NodeIndex, - referring_field: String, - foreign_key_field: String, - ) { - self.graph - .add_edge(parent, child, (referring_field, foreign_key_field)); - } - - /// Returns database joins in topologically sorted order. - fn get_sorted_joins(&self) -> GraphqlResult { - let toposorted_nodes = - if let Ok(sorted) = petgraph::algo::toposort(&self.graph, None) { - sorted - } else { - return Err(GraphqlError::QueryError( - "Cannot have cycles in query".to_string(), - )); - }; - - if toposorted_nodes.is_empty() { - return Ok(Joins::default()); - } - - let mut joins = Joins { - fully_qualified_namespace: self.fully_qualified_namespace.clone(), - ..Default::default() - }; - - let mut seen = vec![false; self.graph.node_count()]; - - let mut stack = VecDeque::from(toposorted_nodes); - - while let Some(node_idx) = stack.pop_front() { - if seen[node_idx.index()] { - continue; - } - - let mut neighbors = self - .graph - .neighbors_directed(node_idx, petgraph::Direction::Outgoing) - .detach(); - - while let Some(e) = neighbors.next_edge(&self.graph) { - if let ( - Some((referring_node, primary_node)), - Some((referring_field, fk_field)), - ) = (self.graph.edge_endpoints(e), self.graph.edge_weight(e)) - { - if let (Some(referring_table), Some(primary_table)) = ( - self.graph.node_weight(referring_node), - self.graph.node_weight(primary_node), - ) { - let join = Join { - primary_table: primary_table.to_owned(), - referring_table: referring_table.to_owned(), - referring_field: referring_field.to_owned(), - fk_field: fk_field.to_owned(), - join_type: "INNER".to_string(), - }; - if let Some(join_set) = joins.join_map.get_mut(primary_table) { - join_set.insert(join); - } else { - let mut join_set = IndexSet::new(); - join_set.insert(join); - joins.join_map.insert(primary_table.to_owned(), join_set); - } - } - - stack.push_front(primary_node); - } - } - - seen[node_idx.index()] = true; - } - - Ok(joins) - } -} - -/// 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")) - } - } - } -} - -impl ParsedOperation { - /// Creates a `ParsedOperation` from a user's operation. - pub fn generate( - schema: &ParsedGraphQLSchema, - operation_def: &OperationDefinition, - fragments: &HashMap>, - name: Option, - ) -> GraphqlResult { - match operation_def.ty { - OperationType::Query => Ok(Self { - name, - ty: operation_def.ty, - variable_definitions: operation_def.variable_definitions.clone(), - directives: operation_def.directives.clone(), - selections: parse_selections( - &operation_def.selection_set.node.items, - fragments, - schema, - None, - )?, - }), - OperationType::Mutation => { - Err(GraphqlError::OperationNotSupported("Mutation".to_string())) - } - OperationType::Subscription => Err(GraphqlError::OperationNotSupported( - "Subscription".to_string(), - )), - } - } - - /// Prepares a string for a `ParsedOperation` for use in a database query. - pub fn prepare( - &self, - schema: &ParsedGraphQLSchema, - db_type: &DbType, - ) -> GraphqlResult { - match self.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 = self.selections[0].prepare( - schema, - db_type, - &mut dependency_graph, - &mut query_parameters, - &mut common_tables, - )?; - - 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, - }) - } - }, - OperationType::Mutation => { - Err(GraphqlError::OperationNotSupported("Mutation".to_string())) - } - OperationType::Subscription => Err(GraphqlError::OperationNotSupported( - "Subscription".to_string(), - )), - } - } -} - -/// 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) - } - } -} - -/// Scalar field in `PreparedSelection`. -#[derive(Debug, Clone)] -pub struct Field { - name: String, - path: String, -} - -impl std::fmt::Display for Field { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}, {}", self.name, self.path) - } -} - -/// Object field in `PreparedSelection`. -#[derive(Debug, Clone)] -pub struct Object { - name: Option, - fields: Vec, -} - -impl std::fmt::Display for Object { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let frag = if let Some(name) = self.name.clone() { - format!("{name}, ") - } else { - "".to_string() - }; - let fields = self - .fields - .iter() - .map(|f| f.to_string()) - .collect::>() - .join(", "); - - write!(f, "{frag}json_build_object({fields})") - } -} - -/// Root object field in `PreparedSelection`. -#[derive(Debug, Clone)] -pub struct Root { - root_entity: String, - fields: Vec, -} - -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(", "); - - write!(f, "json_build_object({fields})") - } -} - -/// List field in `PreparedSelection`. -#[derive(Debug, Clone)] -pub struct List { - name: String, - selection: Box, -} - -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) - } -} - -/// Representation of fields and objects to be selected as part of a user's operation. -#[derive(Debug, Clone)] -pub enum PreparedSelection { - Field(Field), - List(List), - Object(Object), - Root(Root), -} - -impl std::fmt::Display for PreparedSelection { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self { - PreparedSelection::Object(o) => write!(f, "{o}"), - PreparedSelection::Field(field) => write!(f, "{field}"), - PreparedSelection::List(l) => write!(f, "{l}"), - PreparedSelection::Root(r) => write!(f, "{r}"), - } - } -} - -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()), - } - } -} - -#[derive(Debug, Clone)] -pub enum QueryKind { - Object, - Connection, -} - -/// The type of selection that can be present in a user's operation. -#[derive(Debug, Clone)] -pub enum SelectionNode { - Scalar { - name: Name, - parent_entity: String, - alias: Option>, - }, - Object { - name: Name, - parent_entity: String, - alias: Option>, - fields: Vec, - is_part_of_list: bool, - arguments: Vec, - entity_type: String, - }, - List { - name: Name, - alias: Option>, - node: Box, - obj_type: String, - }, - QueryRoot { - name: Name, - alias: Option>, - fields: Vec, - arguments: Vec, - kind: QueryKind, - root_entity_type: String, - }, - PageInfo { - name: Name, - alias: Option>, - fields: Vec, - parent_entity: String, - }, - Edge { - name: Name, - cursor: Box>, - node: Box>, - entity: String, - }, -} - -impl SelectionNode { - /// Return name for a `SelectionNode`. - pub fn name(&self) -> String { - match &self { - SelectionNode::Scalar { name, .. } => name.to_string(), - SelectionNode::Object { name, .. } => name.to_string(), - SelectionNode::List { name, .. } => name.to_string(), - SelectionNode::QueryRoot { name, .. } => name.to_string(), - SelectionNode::PageInfo { name, .. } => name.to_string(), - SelectionNode::Edge { name, .. } => name.to_string(), - } - } - - /// Parses a `SelectionNode` into a collection of strings that will - /// be used for generating a database query. - pub fn prepare( - &self, - schema: &ParsedGraphQLSchema, - db_type: &DbType, - dependency_graph: &mut DependencyGraph, - query_parameters: &mut QueryParams, - common_tables: &mut Vec, - ) -> GraphqlResult { - match db_type { - DbType::Postgres => { - let fqn = schema.fully_qualified_namespace(); - match self { - SelectionNode::Scalar { - name, - parent_entity, - alias, - } => { - let field_name = alias - .clone() - .map_or(format!("'{}'", name), |a| format!("'{}'", a.node)); - let table_path = - format!("{fqn}.{}.{name}", parent_entity.to_lowercase()); - let field = Field { - name: field_name, - path: table_path, - }; - - Ok(PreparedSelection::Field(field)) - } - SelectionNode::Object { - name, - alias, - fields, - is_part_of_list, - arguments, - 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(), - format!( - "{}.{}", - schema.fully_qualified_namespace(), - entity_type.to_lowercase() - ), - ); - - for sn in fields { - if let SelectionNode::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_table, fk_field)) = - fk_map.get(&list_name.to_string()) - { - let outer_obj_node = - dependency_graph.add_node(format!( - "{}.{}", - schema.fully_qualified_namespace(), - 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(), - entity_type.to_lowercase(), - obj_type.to_lowercase(), - )); - - dependency_graph.add_edge( - outer_obj_node, - connecting_node, - fk_field.clone(), - format!( - "{}_{fk_field}", - 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 = sn.prepare( - schema, - db_type, - dependency_graph, - query_parameters, - common_tables, - )?; - obj_fields.push(prepared_selection); - } - - let object = Object { - name: if !*is_part_of_list { - let field_name = - alias.clone().map_or(format!("'{}'", name), |a| { - format!("'{}'", a.node) - }); - Some(field_name) - } else { - None - }, - fields: obj_fields, - }; - - Ok(PreparedSelection::Object(object)) - } - SelectionNode::List { - name, - alias, - node, - obj_type: _, - } => { - if let SelectionNode::List { .. } = **node { - return Err(GraphqlError::QueryError( - "Lists of lists are not supported".to_string(), - )); - } - - let field_name = alias - .clone() - .map_or(format!("'{}'", name), |a| format!("'{}'", a.node)); - - let list = List { - name: field_name, - selection: Box::new(node.prepare( - schema, - db_type, - dependency_graph, - query_parameters, - common_tables, - )?), - }; - - Ok(PreparedSelection::List(list)) - } - SelectionNode::QueryRoot { - name, - alias, - fields, - arguments, - kind, - root_entity_type, - } => { - let mut obj_fields: Vec = vec![]; - match kind { - QueryKind::Object => { - query_parameters.add_params( - arguments.to_owned(), - format!( - "{}.{}", - schema.fully_qualified_namespace(), - name - ), - ); - - for selection_node in fields { - if let SelectionNode::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_table, 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 prepared_selection = selection_node.prepare( - schema, - db_type, - dependency_graph, - query_parameters, - common_tables, - )?; - obj_fields.push(prepared_selection); - } - - let object = Root { - fields: obj_fields, - root_entity: root_entity_type.to_string(), - }; - - Ok(PreparedSelection::Root(object)) - } - QueryKind::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) - }); - - query_parameters.add_params( - arguments.to_owned(), - format!( - "{}.{}", - schema.fully_qualified_namespace(), - root_entity_type - ), - ); - - let mut obj_fields: Vec = vec![]; - for sn in fields { - let prepared_selection = sn.prepare( - schema, - db_type, - &mut cte_dep_graph, - query_parameters, - common_tables, - )?; - obj_fields.push(prepared_selection); - } - - 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_op = PreparedOperation { - selection_set: prepared_cte_query_root, - ctes: common_tables.clone(), - group_by_fields, - 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(), - aggregate_func_used, - }; - - let cte = CommonTable { - name: name.to_string(), - prepared_operation: cte_op, - }; - - let selection = PreparedSelection::Field(Field { - name: field_name, - path: format!( - "{}.{}", - cte.name.clone(), - cte.name.clone() - ), - }); - - let query_root = PreparedSelection::Root(Root { - root_entity: root_entity_type.to_string(), - fields: vec![selection], - }); - - common_tables.push(cte); - - Ok(query_root) - } - } - } - SelectionNode::PageInfo { .. } => unimplemented!(), - SelectionNode::Edge { - name: _, - cursor, - entity, - node, - } => { - let mut obj_fields = vec![]; - if cursor.is_some() { - let cursor_field = Field { - name: "'cursor'".to_string(), - path: format!( - "{}.{}.id", - schema.fully_qualified_namespace(), - entity.to_lowercase() - ), - }; - 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(SelectionNode::Object { - name, - parent_entity, - alias, - fields, - is_part_of_list, - arguments, - entity_type, - }) = *node.clone() - { - for selection_node in fields { - if let SelectionNode::List { - name, - alias, - node: list_node, - obj_type, - } = selection_node - { - if let SelectionNode::Object { - name, - parent_entity, - alias, - fields, - is_part_of_list, - arguments, - entity_type, - } = *list_node - { - // TODO: - // This is a list of objects inside an Edge type, - // we need to make a CTE for this nested object - } else { - let prepared_selection = list_node.prepare( - schema, - db_type, - dependency_graph, - query_parameters, - common_tables, - )?; - obj_fields.push(prepared_selection); - } - // manually parse it and do something similar to CTE work above - } else { - let prepared_selection = selection_node.prepare( - schema, - db_type, - dependency_graph, - query_parameters, - common_tables, - )?; - obj_fields.push(prepared_selection); - } - } - } - - // TODO: Alias? - let object = Object { - name: None, - fields: obj_fields, - }; - - Ok(PreparedSelection::Object(object)) - } - } - } - } - } -} - -/// Parses selections from an `OperationDefinition` into a list of `SelectionNode`s. -fn parse_selections( - selections: &[Positioned], - fragments: &HashMap>, - schema: &ParsedGraphQLSchema, - parent_obj: Option<&String>, -) -> GraphqlResult> { - // We're using a fold operation here in order to collect nodes from both field selections - // as well as selections from fragment defintions. - let parsed_selections = selections.iter().try_fold(vec![], |mut v, selection| { - Ok(match &selection.node { - Selection::Field(f) => { - let field_type = schema.graphql_type(parent_obj, &f.node.name.node); - let arguments = f - .node - .arguments - .iter() - .map(|(arg, value)| { - parse_argument_into_param( - field_type, - &arg.to_string(), - value.node.clone(), - schema, - ) - }) - .collect::>>()?; - - // If this function was called with a parent object, then the SelectionNode - // will NOT be a root level object. Thus, it needs to be parsed into the - // correct type of SelectionNode. - if let Some(parent) = parent_obj { - let has_no_subselections = f.node.selection_set.node.items.is_empty(); - - // List fields require a different function than the one used for objects, - // and internal types (e.g. pagination helper types) don't have tables in the database. - let (is_list_field, internal_type) = if let Some(parent) = parent_obj - { - let key = format!("{}.{}", parent, f.node.name.node); - if let Some((f_def, _)) = schema.field_defs().get(&key) { - match &f_def.ty.node.base { - BaseType::Named(t) => { - if let Some(internal) = - schema.internal_types().get(&t.to_string()) - { - (false, Some(internal)) - } else { - (false, None) - } - } - BaseType::List(inner_type) => { - if let BaseType::Named(t) = &inner_type.base { - if let Some(internal) = - schema.internal_types().get(&t.to_string()) - { - (true, Some(internal)) - } else { - (true, None) - } - } else { - return Err(GraphqlError::ListsOfLists); - } - } - } - } else { - (false, None) - } - } else { - (false, None) - }; - - let selection_node = if let Some(t) = internal_type { - let mut fields = parse_selections( - &f.node.selection_set.node.items, - fragments, - schema, - field_type, - )?; - let key = format!("{}.{}", parent, f.node.name.node); - let entity = if let Some(pagination_type) = - schema.field_type_mappings().get(&key) - { - if let Some(underlying_obj) = - schema.pagination_types().get(pagination_type) - { - underlying_obj.clone() - } else { - return Err(GraphqlError::CouldNotGetBaseEntityType( - f.node.name.node.to_string(), - )); - } - } else { - return Err(GraphqlError::CouldNotGetBaseEntityType( - f.node.name.node.to_string(), - )); - }; - - match t { - InternalType::Edge => { - let cursor = if let Some(idx) = - fields.iter().position(|f| f.name() == *"cursor") - { - let c = fields.swap_remove(idx); - Box::new(Some(c)) - } else { - Box::new(None) - }; - - let node = if let Some(idx) = - fields.iter().position(|f| f.name() == *"node") - { - let n = fields.swap_remove(idx); - Box::new(Some(n)) - } else { - Box::new(None) - }; - - SelectionNode::Edge { - name: f.node.name.node.clone(), - // alias: f.node.alias.clone(), - // parent_entity: parent.to_string(), - // fields, - cursor, - entity, - node, - } - } - InternalType::PageInfo => { - let backing_obj = if let Some(underlying_obj) = - schema.pagination_types().get(parent) - { - underlying_obj.clone() - } else { - return Err(GraphqlError::CouldNotGetBaseEntityType( - parent.to_owned(), - )); - }; - - SelectionNode::PageInfo { - name: f.node.name.node.clone(), - alias: f.node.alias.clone(), - fields, - parent_entity: backing_obj, - } - } - _ => { - return Err(GraphqlError::QueryError("FIXME".to_string())) - } - } - } else if has_no_subselections { - SelectionNode::Scalar { - name: f.node.name.node.clone(), - alias: f.node.alias.clone(), - parent_entity: parent.clone(), - } - } else { - let fields = parse_selections( - &f.node.selection_set.node.items, - fragments, - schema, - field_type, - )?; - let entity_type = if let Some(ty) = field_type { - ty.clone() - } else { - return Err(GraphqlError::CouldNotGetBaseEntityType( - f.node.name.node.to_string(), - )); - }; - - SelectionNode::Object { - name: f.node.name.node.clone(), - alias: f.node.alias.clone(), - fields, - parent_entity: parent.to_string(), - is_part_of_list: is_list_field, - arguments, - entity_type, - } - }; - - if is_list_field { - let entity_type = if let Some(ty) = field_type { - ty.clone() - } else { - return Err(GraphqlError::CouldNotGetBaseEntityType( - f.node.name.node.to_string(), - )); - }; - - v.push(SelectionNode::List { - node: Box::new(selection_node), - name: f.node.name.node.clone(), - alias: f.node.alias.clone(), - obj_type: entity_type, - }); - v - } else { - v.push(selection_node); - v - } - } else { - if let Some(query_type) = - schema.query_response_type(&f.node.name.node) - { - let fields = parse_selections( - &f.node.selection_set.node.items, - fragments, - schema, - Some(&query_type), - )?; - let (kind, query_type) = if query_type.contains("Connection") { - (QueryKind::Connection, query_type.replace("Connection", "")) - } else { - (QueryKind::Object, query_type) - }; - - v.push(SelectionNode::QueryRoot { - name: f.node.name.node.clone(), - alias: f.node.alias.clone(), - fields, - arguments, - root_entity_type: query_type, - kind, - }); - return Ok(v); - } else { - return Err(GraphqlError::QueryError( - "Root level must be a query".to_string(), - )); - } - } - } - Selection::FragmentSpread(frag_spread) => { - if let Some(definition) = - fragments.get(&frag_spread.node.fragment_name.node) - { - let selections = &definition.node.selection_set.node.items; - let mut sub_selections = - parse_selections(selections, fragments, schema, parent_obj)?; - v.append(&mut sub_selections); - v - } else { - return Err(GraphqlError::FragmentResolverFailed); - } - } - // TODO: Figure out what to do with this - Selection::InlineFragment(_) => todo!(), - }) - }); - - parsed_selections -} diff --git a/packages/fuel-indexer-graphql/src/query/mod.rs b/packages/fuel-indexer-graphql/src/query/mod.rs new file mode 100644 index 000000000..7d2f78984 --- /dev/null +++ b/packages/fuel-indexer-graphql/src/query/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod parse; +pub(crate) mod prepare; diff --git a/packages/fuel-indexer-graphql/src/query/parse.rs b/packages/fuel-indexer-graphql/src/query/parse.rs new file mode 100644 index 000000000..977711e60 --- /dev/null +++ b/packages/fuel-indexer-graphql/src/query/parse.rs @@ -0,0 +1,327 @@ +use std::collections::HashMap; + +use async_graphql_parser::{ + types::{ + BaseType, Directive, FragmentDefinition, OperationDefinition, OperationType, + Selection, VariableDefinition, + }, + Positioned, +}; +use async_graphql_value::Name; +use fuel_indexer_lib::graphql::{parser::InternalType, ParsedGraphQLSchema}; + +use crate::{ + arguments::{parse_argument_into_param, ParamType}, + GraphqlError, GraphqlResult, ParsedQueryKind, ParsedSelection, +}; + +/// Contains information about a successfully-parsed user operation. +#[derive(Debug)] +pub struct ParsedOperation { + pub name: Option, + pub selections: Vec, + pub ty: OperationType, + pub variable_definitions: Vec>, + pub directives: Vec>, +} +impl ParsedOperation { + /// Creates a `ParsedOperation` from a user's operation. + pub fn generate( + schema: &ParsedGraphQLSchema, + operation_def: &OperationDefinition, + fragments: &HashMap>, + name: Option, + ) -> GraphqlResult { + match operation_def.ty { + OperationType::Query => Ok(Self { + name, + ty: operation_def.ty, + variable_definitions: operation_def.variable_definitions.clone(), + directives: operation_def.directives.clone(), + selections: parse_selections( + &operation_def.selection_set.node.items, + fragments, + schema, + None, + )?, + }), + OperationType::Mutation => { + Err(GraphqlError::OperationNotSupported("Mutation".to_string())) + } + OperationType::Subscription => Err(GraphqlError::OperationNotSupported( + "Subscription".to_string(), + )), + } + } +} + +/// Parses selections from an `OperationDefinition` into a list of `ParsedSelection`s. +fn parse_selections( + selections: &[Positioned], + fragments: &HashMap>, + schema: &ParsedGraphQLSchema, + parent_obj: Option<&String>, +) -> GraphqlResult> { + // We're using a fold operation here in order to collect nodes from both field selections + // as well as selections from fragment defintions. + let parsed_selections = selections.iter().try_fold(vec![], |mut v, selection| { + Ok(match &selection.node { + Selection::Field(f) => { + let field_type = schema.graphql_type(parent_obj, &f.node.name.node); + let mut arguments = f + .node + .arguments + .iter() + .map(|(arg, value)| { + parse_argument_into_param( + field_type, + &arg.to_string(), + value.node.clone(), + schema, + ) + }) + .collect::>>()?; + + // If this function was called with a parent object, then the ParsedSelection + // will NOT be a root level object. Thus, it needs to be parsed into the + // correct type of ParsedSelection. + if let Some(parent) = parent_obj { + let has_no_subselections = f.node.selection_set.node.items.is_empty(); + + // List fields require a different function than the one used for objects, + // and internal types (e.g. pagination helper types) don't have tables in the database. + let (is_list_field, internal_type) = if let Some(parent) = parent_obj + { + let key = format!("{}.{}", parent, f.node.name.node); + if let Some((f_def, _)) = schema.field_defs().get(&key) { + match &f_def.ty.node.base { + BaseType::Named(t) => { + if let Some(internal) = + schema.internal_types().get(&t.to_string()) + { + (false, Some(internal)) + } else { + (false, None) + } + } + BaseType::List(inner_type) => { + if let BaseType::Named(t) = &inner_type.base { + if let Some(internal) = + schema.internal_types().get(&t.to_string()) + { + (true, Some(internal)) + } else { + (true, None) + } + } else { + return Err(GraphqlError::ListsOfLists); + } + } + } + } else { + (false, None) + } + } else { + (false, None) + }; + + let selection_node = if let Some(t) = internal_type { + let mut fields = parse_selections( + &f.node.selection_set.node.items, + fragments, + schema, + field_type, + )?; + let key = format!("{}.{}", parent, f.node.name.node); + let entity = if let Some(pagination_type) = + schema.field_type_mappings().get(&key) + { + if let Some(underlying_obj) = + schema.pagination_types().get(pagination_type) + { + underlying_obj.clone() + } else { + return Err(GraphqlError::CouldNotGetBaseEntityType( + f.node.name.node.to_string(), + )); + } + } else { + return Err(GraphqlError::CouldNotGetBaseEntityType( + f.node.name.node.to_string(), + )); + }; + + match t { + InternalType::Edge => { + let cursor = if let Some(idx) = + fields.iter().position(|f| f.name() == *"cursor") + { + let c = fields.swap_remove(idx); + Box::new(Some(c)) + } else { + Box::new(None) + }; + + let node = if let Some(idx) = + fields.iter().position(|f| f.name() == *"node") + { + let n = fields.swap_remove(idx); + Box::new(Some(n)) + } else { + Box::new(None) + }; + + ParsedSelection::Edge { + name: f.node.name.node.clone(), + // alias: f.node.alias.clone(), + // parent_entity: parent.to_string(), + // fields, + cursor, + entity, + node, + } + } + InternalType::PageInfo => { + let backing_obj = if let Some(underlying_obj) = + schema.pagination_types().get(parent) + { + underlying_obj.clone() + } else { + return Err(GraphqlError::CouldNotGetBaseEntityType( + parent.to_owned(), + )); + }; + + ParsedSelection::PageInfo { + name: f.node.name.node.clone(), + alias: f.node.alias.clone(), + fields, + parent_entity: backing_obj, + } + } + _ => { + return Err(GraphqlError::QueryError("FIXME".to_string())) + } + } + } else if has_no_subselections { + ParsedSelection::Scalar { + name: f.node.name.node.clone(), + alias: f.node.alias.clone(), + parent_entity: parent.clone(), + } + } else { + let fields = parse_selections( + &f.node.selection_set.node.items, + fragments, + schema, + field_type, + )?; + let entity_type = if let Some(ty) = field_type { + ty.clone() + } else { + return Err(GraphqlError::CouldNotGetBaseEntityType( + f.node.name.node.to_string(), + )); + }; + + ParsedSelection::Object { + name: f.node.name.node.clone(), + alias: f.node.alias.clone(), + fields, + parent_entity: parent.to_string(), + is_part_of_list: is_list_field, + arguments, + entity_type, + } + }; + + if is_list_field { + let entity_type = if let Some(ty) = field_type { + ty.clone() + } else { + return Err(GraphqlError::CouldNotGetBaseEntityType( + f.node.name.node.to_string(), + )); + }; + + v.push(ParsedSelection::List { + node: Box::new(selection_node), + name: f.node.name.node.clone(), + alias: f.node.alias.clone(), + obj_type: entity_type, + }); + v + } else { + v.push(selection_node); + v + } + } else if let Some(query_type) = + schema.query_response_type(&f.node.name.node) + { + let fields = parse_selections( + &f.node.selection_set.node.items, + fragments, + schema, + Some(&query_type), + )?; + let (kind, query_type) = if query_type.contains("Connection") { + ( + ParsedQueryKind::Connection, + query_type.replace("Connection", ""), + ) + } else { + (ParsedQueryKind::Object, query_type) + }; + + if let ParsedQueryKind::Object = kind { + if !arguments.iter().any(|a| { + matches!( + a, + ParamType::Filter( + crate::arguments::FilterType::IdSelection(_) + ) + ) + }) { + return Err(GraphqlError::QueryError( + "Query needs ID argument".to_string(), + )); + } else { + arguments.push(ParamType::Limit(1)); + } + } + + v.push(ParsedSelection::QueryRoot { + name: f.node.name.node.clone(), + alias: f.node.alias.clone(), + fields, + arguments, + root_entity_type: query_type, + kind, + }); + return Ok(v); + } else { + return Err(GraphqlError::QueryError( + "Root level must be a query".to_string(), + )); + } + } + Selection::FragmentSpread(frag_spread) => { + if let Some(definition) = + fragments.get(&frag_spread.node.fragment_name.node) + { + let selections = &definition.node.selection_set.node.items; + let mut sub_selections = + parse_selections(selections, fragments, schema, parent_obj)?; + v.append(&mut sub_selections); + v + } else { + return Err(GraphqlError::FragmentResolverFailed); + } + } + // TODO: Figure out what to do with this + Selection::InlineFragment(_) => todo!(), + }) + }); + + parsed_selections +} diff --git a/packages/fuel-indexer-graphql/src/query/prepare.rs b/packages/fuel-indexer-graphql/src/query/prepare.rs new file mode 100644 index 000000000..5cccf9026 --- /dev/null +++ b/packages/fuel-indexer-graphql/src/query/prepare.rs @@ -0,0 +1,920 @@ +use std::collections::{HashMap, VecDeque}; + +use async_graphql_parser::types::OperationType; +use fuel_indexer_database_types::DbType; +use fuel_indexer_lib::graphql::ParsedGraphQLSchema; +use indexmap::{IndexMap, IndexSet}; +use petgraph::graph::{Graph, NodeIndex}; + +use crate::{ + arguments::QueryParams, GraphqlError, GraphqlResult, ParsedQueryKind, ParsedSelection, +}; + +use super::parse::ParsedOperation; + +#[derive(Debug, Clone)] +pub struct CommonTable { + pub name: String, + pub prepared_operation: PreparedOperation, +} + +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) + } +} + +/// Contains necessary information for generating joins between database tables. +#[derive(Debug, Default)] +pub struct DependencyGraph { + pub table_node_idx_map: HashMap, + pub graph: Graph, + pub fully_qualified_namespace: String, +} + +/// Contains information about the database joins that will be needed +/// to successfully execute a user's operation. +#[derive(Debug, Default, Clone)] +pub struct Joins { + pub join_map: IndexMap>, + pub fully_qualified_namespace: String, +} + +impl std::fmt::Display for Joins { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + #[allow(clippy::type_complexity)] + let (singular_cond_joins, multi_cond_joins): ( + Vec<(&String, &IndexSet)>, + Vec<(&String, &IndexSet)>, + ) = self + .join_map + .iter() + .partition(|(_, join_set)| join_set.len() == 1); + + let mut joins = singular_cond_joins + .into_iter() + .map(|(_, j)| { + if let Some(join) = j.first() { + join.to_string() + } else { + "".to_string() + } + }) + .collect::>(); + + let mut combination_joins = multi_cond_joins + .iter() + .map(|(primary_table, join_set)| { + let conditions = join_set + .iter() + .map(|j| { + format!( + "{}.{} = {}.{}", + j.referring_table, + j.referring_field, + j.primary_table, + j.fk_field + ) + }) + .collect::>() + .join(" AND "); + format!("INNER JOIN {primary_table} ON {conditions}") + }) + .collect::>(); + joins.append(&mut combination_joins); + + write!(f, "{}", joins.join("\n")) + } +} + +/// Contains information necessary for generating database joins. +#[derive(Debug, Default, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct Join { + pub primary_table: String, + pub referring_table: String, + pub referring_field: String, + pub fk_field: String, + pub join_type: String, +} + +impl std::fmt::Display for Join { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "INNER JOIN {} ON {}.{} = {}.{}", + self.primary_table, + self.referring_table, + self.referring_field, + self.primary_table, + self.fk_field + ) + } +} + +impl DependencyGraph { + /// Add a new node to dependency graph. + fn add_node(&mut self, table: String) -> NodeIndex { + if let Some(existing_node_idx) = self.table_node_idx_map.get(&table) { + *existing_node_idx + } else { + let new_node_idx = self.graph.add_node(table.clone()); + self.table_node_idx_map.insert(table, new_node_idx); + new_node_idx + } + } + + /// Add an edge between two existing nodes. + fn add_edge( + &mut self, + parent: NodeIndex, + child: NodeIndex, + referring_field: String, + foreign_key_field: String, + ) { + self.graph + .add_edge(parent, child, (referring_field, foreign_key_field)); + } + + /// Returns database joins in topologically sorted order. + fn get_sorted_joins(&self) -> GraphqlResult { + let toposorted_nodes = + if let Ok(sorted) = petgraph::algo::toposort(&self.graph, None) { + sorted + } else { + return Err(GraphqlError::QueryError( + "Cannot have cycles in query".to_string(), + )); + }; + + if toposorted_nodes.is_empty() { + return Ok(Joins::default()); + } + + let mut joins = Joins { + fully_qualified_namespace: self.fully_qualified_namespace.clone(), + ..Default::default() + }; + + let mut seen = vec![false; self.graph.node_count()]; + + let mut stack = VecDeque::from(toposorted_nodes); + + while let Some(node_idx) = stack.pop_front() { + if seen[node_idx.index()] { + continue; + } + + let mut neighbors = self + .graph + .neighbors_directed(node_idx, petgraph::Direction::Outgoing) + .detach(); + + while let Some(e) = neighbors.next_edge(&self.graph) { + if let ( + Some((referring_node, primary_node)), + Some((referring_field, fk_field)), + ) = (self.graph.edge_endpoints(e), self.graph.edge_weight(e)) + { + if let (Some(referring_table), Some(primary_table)) = ( + self.graph.node_weight(referring_node), + self.graph.node_weight(primary_node), + ) { + let join = Join { + primary_table: primary_table.to_owned(), + referring_table: referring_table.to_owned(), + referring_field: referring_field.to_owned(), + fk_field: fk_field.to_owned(), + join_type: "INNER".to_string(), + }; + if let Some(join_set) = joins.join_map.get_mut(primary_table) { + join_set.insert(join); + } else { + let mut join_set = IndexSet::new(); + join_set.insert(join); + joins.join_map.insert(primary_table.to_owned(), join_set); + } + } + + stack.push_front(primary_node); + } + } + + seen[node_idx.index()] = true; + } + + Ok(joins) + } +} + +/// Prepares a string for a `ParsedOperation` for use in a database query. +pub fn prepare_operation( + parsed_operation: &ParsedOperation, + schema: &ParsedGraphQLSchema, + db_type: &DbType, +) -> GraphqlResult { + 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(), + schema, + db_type, + &mut dependency_graph, + &mut query_parameters, + &mut common_tables, + )?; + + 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, + }) + } + }, + OperationType::Mutation => { + Err(GraphqlError::OperationNotSupported("Mutation".to_string())) + } + OperationType::Subscription => Err(GraphqlError::OperationNotSupported( + "Subscription".to_string(), + )), + } +} +/// Scalar field in `PreparedSelection`. +#[derive(Debug, Clone)] +pub struct Field { + name: String, + path: String, +} + +impl std::fmt::Display for Field { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}, {}", self.name, self.path) + } +} + +/// Object field in `PreparedSelection`. +#[derive(Debug, Clone)] +pub struct Object { + name: Option, + fields: Vec, +} + +impl std::fmt::Display for Object { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let frag = if let Some(name) = self.name.clone() { + format!("{name}, ") + } else { + "".to_string() + }; + let fields = self + .fields + .iter() + .map(|f| f.to_string()) + .collect::>() + .join(", "); + + write!(f, "{frag}json_build_object({fields})") + } +} + +/// Root object field in `PreparedSelection`. +#[derive(Debug, Clone)] +pub struct Root { + root_entity: String, + fields: Vec, +} + +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(", "); + + write!(f, "json_build_object({fields})") + } +} + +/// List field in `PreparedSelection`. +#[derive(Debug, Clone)] +pub struct List { + name: String, + selection: Box, +} + +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) + } +} + +/// Representation of fields and objects to be selected as part of a user's operation. +#[derive(Debug, Clone)] +pub enum PreparedSelection { + Field(Field), + List(List), + Object(Object), + Root(Root), +} + +impl std::fmt::Display for PreparedSelection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + PreparedSelection::Object(o) => write!(f, "{o}"), + PreparedSelection::Field(field) => write!(f, "{field}"), + PreparedSelection::List(l) => write!(f, "{l}"), + PreparedSelection::Root(r) => write!(f, "{r}"), + } + } +} + +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()), + } + } +} +/// 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) + } + } +} + +/// Parses a `ParsedSelection` into a collection of strings that will +/// be used for generating a database query. +pub fn prepare_selection( + parsed_selection: ParsedSelection, + schema: &ParsedGraphQLSchema, + db_type: &DbType, + dependency_graph: &mut DependencyGraph, + query_parameters: &mut QueryParams, + common_tables: &mut Vec, +) -> GraphqlResult { + match db_type { + DbType::Postgres => { + let fqn = schema.fully_qualified_namespace(); + match parsed_selection { + ParsedSelection::Scalar { + name, + parent_entity, + alias, + } => { + let field_name = alias + .clone() + .map_or(format!("'{}'", name), |a| format!("'{}'", a.node)); + let table_path = + format!("{fqn}.{}.{name}", parent_entity.to_lowercase()); + let field = Field { + name: field_name, + path: table_path, + }; + + Ok(PreparedSelection::Field(field)) + } + ParsedSelection::Object { + name, + alias, + fields, + is_part_of_list, + arguments, + 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(), + format!( + "{}.{}", + schema.fully_qualified_namespace(), + entity_type.to_lowercase() + ), + ); + + 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_table, fk_field)) = + fk_map.get(&list_name.to_string()) + { + let outer_obj_node = + dependency_graph.add_node(format!( + "{}.{}", + schema.fully_qualified_namespace(), + 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(), + entity_type.to_lowercase(), + obj_type.to_lowercase(), + )); + + dependency_graph.add_edge( + outer_obj_node, + connecting_node, + fk_field.clone(), + format!( + "{}_{fk_field}", + 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( + sn, + schema, + db_type, + dependency_graph, + query_parameters, + common_tables, + )?; + obj_fields.push(prepared_selection); + } + + let object = Object { + name: if !is_part_of_list { + let field_name = + alias.clone().map_or(format!("'{}'", name), |a| { + format!("'{}'", a.node) + }); + Some(field_name) + } else { + None + }, + fields: obj_fields, + }; + + Ok(PreparedSelection::Object(object)) + } + ParsedSelection::List { + name, + alias, + node, + obj_type: _, + } => { + if let ParsedSelection::List { .. } = *node { + return Err(GraphqlError::QueryError( + "Lists of lists are not supported".to_string(), + )); + } + + let field_name = alias + .clone() + .map_or(format!("'{}'", name), |a| format!("'{}'", a.node)); + + let list = List { + name: field_name, + selection: Box::new(prepare_selection( + *node, + schema, + db_type, + dependency_graph, + query_parameters, + common_tables, + )?), + }; + + Ok(PreparedSelection::List(list)) + } + ParsedSelection::QueryRoot { + name, + 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 + { + if let Some(fk_map) = schema + .foreign_key_mappings() + .get(&root_entity_type.to_lowercase()) + { + if let Some((_fk_table, 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 prepared_selection = prepare_selection( + selection_node, + schema, + db_type, + dependency_graph, + query_parameters, + common_tables, + )?; + obj_fields.push(prepared_selection); + } + + let object = Root { + fields: obj_fields, + root_entity: root_entity_type.to_string(), + }; + + Ok(PreparedSelection::Root(object)) + } + 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) + }); + + query_parameters.add_params( + arguments.to_owned(), + 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, + )?; + obj_fields.push(prepared_selection); + } + + 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_op = PreparedOperation { + selection_set: prepared_cte_query_root, + ctes: common_tables.clone(), + group_by_fields, + 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(), + aggregate_func_used, + }; + + let cte = CommonTable { + name: name.to_string(), + prepared_operation: cte_op, + }; + + let selection = PreparedSelection::Field(Field { + name: field_name, + path: format!( + "{}.{}", + cte.name.clone(), + cte.name.clone() + ), + }); + + let query_root = PreparedSelection::Root(Root { + root_entity: root_entity_type.to_string(), + fields: vec![selection], + }); + + common_tables.push(cte); + + Ok(query_root) + } + } + } + ParsedSelection::PageInfo { .. } => unimplemented!(), + ParsedSelection::Edge { + name: _, + cursor, + entity, + node, + } => { + let mut obj_fields = vec![]; + if cursor.is_some() { + let cursor_field = Field { + name: "'cursor'".to_string(), + path: format!( + "{}.{}.id", + schema.fully_qualified_namespace(), + entity.to_lowercase() + ), + }; + 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 + { + // TODO: + // This is a list of objects inside an Edge type, + // we need to make a CTE for this nested object + } else { + let prepared_selection = prepare_selection( + *list_node, + schema, + db_type, + dependency_graph, + query_parameters, + common_tables, + )?; + obj_fields.push(prepared_selection); + } + // 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); + } + } + } + + // TODO: Alias? + let object = Object { + name: None, + fields: obj_fields, + }; + + Ok(PreparedSelection::Object(object)) + } + } + } + } +}