diff --git a/Cargo.lock b/Cargo.lock index fccd2c45..5a01fc51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -751,9 +751,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" dependencies = [ "equivalent", "hashbrown", @@ -1040,6 +1040,7 @@ dependencies = [ "bimap", "cached", "graphql-parser", + "indexmap", "itertools", "lazy_static", "pgrx", diff --git a/Cargo.toml b/Cargo.toml index dfa9ce5a..45b43807 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ uuid = "1" base64 = "0.13" lazy_static = "1" bimap = { version = "0.6.3", features = ["serde"] } +indexmap = "2.2" [dev-dependencies] pgrx-tests = "=0.11.2" diff --git a/src/builder.rs b/src/builder.rs index 3880550d..4ed5df54 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -5,6 +5,7 @@ use crate::sql_types::*; use graphql_parser::query::*; use serde::Serialize; use std::collections::HashMap; +use std::hash::Hash; use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; @@ -263,7 +264,8 @@ pub fn to_insert_builder<'a, T>( variable_definitions: &Vec>, ) -> Result where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let type_ = field.type_().unmodified_type(); let type_name = type_ @@ -294,12 +296,12 @@ where None => return Err("unknown field in insert".to_string()), Some(f) => builder_fields.push(match f.name().as_ref() { "affectedCount" => InsertSelection::AffectedCount { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), }, "records" => { let node_builder = to_node_builder( f, - selection_field, + &selection_field, fragment_definitions, variables, &[], @@ -308,7 +310,7 @@ where InsertSelection::Records(node_builder?) } "__typename" => InsertSelection::Typename { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), typename: xtype .name() .expect("insert response type should have a name"), @@ -428,7 +430,8 @@ pub fn to_update_builder<'a, T>( variable_definitions: &Vec>, ) -> Result where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let type_ = field.type_().unmodified_type(); let type_name = type_ @@ -463,12 +466,12 @@ where None => return Err("unknown field in update".to_string()), Some(f) => builder_fields.push(match f.name().as_ref() { "affectedCount" => UpdateSelection::AffectedCount { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), }, "records" => { let node_builder = to_node_builder( f, - selection_field, + &selection_field, fragment_definitions, variables, &[], @@ -477,7 +480,7 @@ where UpdateSelection::Records(node_builder?) } "__typename" => UpdateSelection::Typename { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), typename: xtype .name() .expect("update response type should have a name"), @@ -533,7 +536,8 @@ pub fn to_delete_builder<'a, T>( variable_definitions: &Vec>, ) -> Result where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let type_ = field.type_().unmodified_type(); let type_name = type_ @@ -566,12 +570,12 @@ where None => return Err("unknown field in delete".to_string()), Some(f) => builder_fields.push(match f.name().as_ref() { "affectedCount" => DeleteSelection::AffectedCount { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), }, "records" => { let node_builder = to_node_builder( f, - selection_field, + &selection_field, fragment_definitions, variables, &[], @@ -580,7 +584,7 @@ where DeleteSelection::Records(node_builder?) } "__typename" => DeleteSelection::Typename { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), typename: xtype .name() .expect("delete response type should have a name"), @@ -642,7 +646,8 @@ pub fn to_function_call_builder<'a, T>( variable_definitions: &Vec>, ) -> Result where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let type_ = field.type_().unmodified_type(); let alias = alias_or_name(query_field); @@ -1336,7 +1341,8 @@ pub fn to_connection_builder<'a, T>( variable_definitions: &Vec>, ) -> Result where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let type_ = field.type_().unmodified_type(); let type_ = type_.return_type(); @@ -1453,24 +1459,24 @@ where Some(f) => builder_fields.push(match &f.type_.unmodified_type() { __Type::Edge(_) => ConnectionSelection::Edge(to_edge_builder( f, - selection_field, + &selection_field, fragment_definitions, variables, variable_definitions, )?), __Type::PageInfo(_) => ConnectionSelection::PageInfo(to_page_info_builder( f, - selection_field, + &selection_field, fragment_definitions, variables, )?), _ => match f.name().as_ref() { "totalCount" => ConnectionSelection::TotalCount { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), }, "__typename" => ConnectionSelection::Typename { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), typename: xtype.name().expect("connection type should have a name"), }, _ => return Err("unexpected field type on connection".to_string()), @@ -1509,7 +1515,8 @@ fn to_page_info_builder<'a, T>( variables: &serde_json::Value, ) -> Result where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let type_ = field.type_().unmodified_type(); let type_name = type_.name().ok_or(format!( @@ -1535,19 +1542,19 @@ where None => return Err("unknown field in pageInfo".to_string()), Some(f) => builder_fields.push(match f.name().as_ref() { "startCursor" => PageInfoSelection::StartCursor { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), }, "endCursor" => PageInfoSelection::EndCursor { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), }, "hasPreviousPage" => PageInfoSelection::HasPreviousPage { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), }, "hasNextPage" => PageInfoSelection::HasNextPage { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), }, "__typename" => PageInfoSelection::Typename { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), typename: xtype.name().expect("page info type should have a name"), }, _ => return Err("unexpected field type on pageInfo".to_string()), @@ -1571,7 +1578,8 @@ fn to_edge_builder<'a, T>( variable_definitions: &Vec>, ) -> Result where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let type_ = field.type_().unmodified_type(); let type_name = type_.name().ok_or(format!( @@ -1599,7 +1607,7 @@ where __Type::Node(_) => { let node_builder = to_node_builder( f, - selection_field, + &selection_field, fragment_definitions, variables, &[], @@ -1609,10 +1617,10 @@ where } _ => match f.name().as_ref() { "cursor" => EdgeSelection::Cursor { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), }, "__typename" => EdgeSelection::Typename { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), typename: xtype.name().expect("edge type should have a name"), }, _ => return Err("unexpected field type on edge".to_string()), @@ -1638,7 +1646,8 @@ pub fn to_node_builder<'a, T>( variable_definitions: &Vec>, ) -> Result where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let type_ = field.type_().unmodified_type(); @@ -1722,7 +1731,7 @@ where )) } Some(f) => { - let alias = alias_or_name(selection_field); + let alias = alias_or_name(&selection_field); let node_selection = match &f.sql_type { Some(node_sql_type) => match node_sql_type { @@ -1737,7 +1746,7 @@ where __Type::Node(_) => { let node_builder = to_node_builder( f, - selection_field, + &selection_field, fragment_definitions, variables, &[], @@ -1749,7 +1758,7 @@ where __Type::Connection(_) => { let connection_builder = to_connection_builder( f, - selection_field, + &selection_field, fragment_definitions, variables, &[], // TODO need ref to fkey here @@ -1777,14 +1786,14 @@ where }, _ => match f.name().as_ref() { "__typename" => NodeSelection::Typename { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), typename: xtype.name().expect("node type should have a name"), }, _ => match f.type_().unmodified_type() { __Type::Connection(_) => { let con_builder = to_connection_builder( f, - selection_field, + &selection_field, fragment_definitions, variables, &[], @@ -1795,7 +1804,7 @@ where __Type::Node(_) => { let node_builder = to_node_builder( f, - selection_field, + &selection_field, fragment_definitions, variables, &[], @@ -1981,7 +1990,8 @@ impl __Schema { variables: &serde_json::Value, ) -> Result<__EnumValueBuilder, String> where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let selection_fields = normalize_selection_set( &query_field.selection_set, @@ -2001,7 +2011,7 @@ impl __Schema { "isDeprecated" => __EnumValueField::IsDeprecated, "deprecationReason" => __EnumValueField::DeprecationReason, "__typename" => __EnumValueField::Typename { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), typename: enum_value.name(), }, _ => { @@ -2013,7 +2023,7 @@ impl __Schema { }; builder_fields.push(__EnumValueSelection { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), selection: __enum_value_field, }); } @@ -2033,7 +2043,8 @@ impl __Schema { variable_definitions: &Vec>, ) -> Result<__InputValueBuilder, String> where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let selection_fields = normalize_selection_set( &query_field.selection_set, @@ -2055,7 +2066,7 @@ impl __Schema { let t_builder = self.to_type_builder_from_type( &t, - selection_field, + &selection_field, fragment_definitions, variables, variable_definitions, @@ -2066,7 +2077,7 @@ impl __Schema { "isDeprecated" => __InputValueField::IsDeprecated, "deprecationReason" => __InputValueField::DeprecationReason, "__typename" => __InputValueField::Typename { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), typename: input_value.name(), }, _ => { @@ -2078,7 +2089,7 @@ impl __Schema { }; builder_fields.push(__InputValueSelection { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), selection: __input_value_field, }); } @@ -2098,7 +2109,8 @@ impl __Schema { variable_definitions: &Vec>, ) -> Result<__FieldBuilder, String> where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let selection_fields = normalize_selection_set( &query_field.selection_set, @@ -2122,7 +2134,7 @@ impl __Schema { for arg in args { let f_builder = self.to_input_value_builder( &arg, - selection_field, + &selection_field, fragment_definitions, variables, variable_definitions, @@ -2136,7 +2148,7 @@ impl __Schema { let t_builder = self.to_type_builder_from_type( &t, - selection_field, + &selection_field, fragment_definitions, variables, variable_definitions, @@ -2146,14 +2158,14 @@ impl __Schema { "isDeprecated" => __FieldField::IsDeprecated, "deprecationReason" => __FieldField::DeprecationReason, "__typename" => __FieldField::Typename { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), typename: field.name(), }, _ => return Err(format!("unknown field in __Field {}", type_field_name)), }; builder_fields.push(__FieldSelection { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), selection: __field_field, }); } @@ -2174,7 +2186,8 @@ impl __Schema { variable_definitions: &Vec>, ) -> Result, String> where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { if field.type_.unmodified_type() != __Type::__Type(__TypeType {}) { return Err("can not build query for non-__type type".to_string()); @@ -2226,7 +2239,8 @@ impl __Schema { variable_definitions: &Vec>, ) -> Result<__TypeBuilder, String> where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let field_map = field_map(&__Type::__Type(__TypeType {})); @@ -2245,7 +2259,7 @@ impl __Schema { match field_map.get(type_field_name) { None => return Err(format!("unknown field on __Type: {}", type_field_name)), Some(f) => builder_fields.push(__TypeSelection { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), selection: match f.name().as_str() { "kind" => __TypeField::Kind, "name" => __TypeField::Name, @@ -2267,7 +2281,7 @@ impl __Schema { let f_builder = self.to_field_builder( &vec_field, - selection_field, + &selection_field, fragment_definitions, variables, variable_definitions, @@ -2288,7 +2302,7 @@ impl __Schema { for vec_field in vec_fields { let f_builder = self.to_input_value_builder( &vec_field, - selection_field, + &selection_field, fragment_definitions, variables, variable_definitions, @@ -2306,7 +2320,7 @@ impl __Schema { for interface in &interfaces { let interface_builder = self.to_type_builder_from_type( interface, - selection_field, + &selection_field, fragment_definitions, variables, variable_definitions, @@ -2328,7 +2342,7 @@ impl __Schema { for enum_value in &enum_values { let f_builder = self.to_enum_value_builder( enum_value, - selection_field, + &selection_field, fragment_definitions, variables, )?; @@ -2346,7 +2360,7 @@ impl __Schema { for ty in &types { let type_builder = self.to_type_builder_from_type( ty, - selection_field, + &selection_field, fragment_definitions, variables, variable_definitions, @@ -2370,7 +2384,7 @@ impl __Schema { let inner_type: __Type = (*(list_type.type_)).clone(); Some(self.to_type_builder_from_type( &inner_type, - selection_field, + &selection_field, fragment_definitions, variables, variable_definitions, @@ -2380,7 +2394,7 @@ impl __Schema { let inner_type = (*(non_null_type.type_)).clone(); Some(self.to_type_builder_from_type( &inner_type, - selection_field, + &selection_field, fragment_definitions, variables, variable_definitions, @@ -2391,7 +2405,7 @@ impl __Schema { __TypeField::OfType(unwrapped_type_builder) } "__typename" => __TypeField::Typename { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), typename: type_.name(), }, _ => { @@ -2420,7 +2434,8 @@ impl __Schema { variable_definitions: &Vec>, ) -> Result<__DirectiveBuilder, String> where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let selection_fields = normalize_selection_set( &query_field.selection_set, @@ -2445,7 +2460,7 @@ impl __Schema { for arg in args { let builder = self.to_input_value_builder( arg, - selection_field, + &selection_field, fragment_definitions, variables, variable_definitions, @@ -2456,7 +2471,7 @@ impl __Schema { } "isRepeatable" => __DirectiveField::IsRepeatable, "__typename" => __DirectiveField::Typename { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), typename: __Directive::TYPE.to_string(), }, _ => { @@ -2469,7 +2484,7 @@ impl __Schema { }; builder_fields.push(__DirectiveSelection { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), selection: directive_field, }); } @@ -2489,7 +2504,8 @@ impl __Schema { variable_definitions: &Vec>, ) -> Result<__SchemaBuilder, String> where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let type_ = field.type_.unmodified_type(); let type_name = type_ @@ -2515,7 +2531,7 @@ impl __Schema { None => return Err(format!("unknown field in __Schema: {}", field_name)), Some(f) => { builder_fields.push(__SchemaSelection { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), selection: match f.name().as_str() { "types" => { let builders = self @@ -2586,7 +2602,7 @@ impl __Schema { __SchemaField::Directives(builders) } "__typename" => __SchemaField::Typename { - alias: alias_or_name(selection_field), + alias: alias_or_name(&selection_field), typename: field.name(), }, _ => { diff --git a/src/lib.rs b/src/lib.rs index 9338cc00..8d99a9d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ use serde_json::json; mod builder; mod graphql; mod gson; +mod merge; mod omit; mod parser_util; mod resolve; diff --git a/src/merge.rs b/src/merge.rs new file mode 100644 index 00000000..fb7ca945 --- /dev/null +++ b/src/merge.rs @@ -0,0 +1,143 @@ +use std::{collections::HashMap, hash::Hash}; + +use graphql_parser::query::{Field, Text, Value}; +use indexmap::IndexMap; + +use crate::parser_util::alias_or_name; + +/// Merges duplicates in a vector of fields. The fields in the vector are added to a +/// map from field name to field. If a field with the same name already exists in the +/// map, the existing and new field's children are combined into the existing field's +/// children. These children will be merged later when they are normalized. +/// +/// The map is an `IndexMap` to ensure iteration order of the fields is preserved. +/// This prevents tests from being flaky due to field order changing between test runs. +pub fn merge<'a, 'b, T>(fields: Vec>) -> Result>, String> +where + T: Text<'a> + Eq + AsRef, + T::Value: Hash, +{ + let mut merged: IndexMap> = IndexMap::new(); + + for current_field in fields { + let response_key = alias_or_name(¤t_field); + match merged.get_mut(&response_key) { + Some(existing_field) => { + if can_merge(¤t_field, existing_field)? { + existing_field + .selection_set + .items + .extend(current_field.selection_set.items); + } + } + None => { + merged.insert(response_key, current_field); + } + } + } + + let fields = merged.into_iter().map(|(_, field)| field).collect(); + + Ok(fields) +} + +fn can_merge<'a, T>(field_a: &Field<'a, T>, field_b: &Field<'a, T>) -> Result +where + T: Text<'a> + Eq + AsRef, + T::Value: Hash, +{ + if field_a.name != field_b.name { + return Err(format!( + "Fields `{}` and `{}` are different", + field_a.name.as_ref(), + field_b.name.as_ref(), + )); + } + if !same_arguments(&field_a.arguments, &field_b.arguments) { + return Err(format!( + "Two fields named `{}` have different arguments", + field_a.name.as_ref(), + )); + } + + Ok(true) +} + +/// Compares two sets of arguments and returns true only if +/// both are the same. `arguments_a` should not have two +/// arguments with the same name. Similarly `arguments_b` +/// should not have duplicates. It is assumed that [Argument +/// Uniqueness] validation has already been run by the time +/// this function is called. +/// +/// [Argument Uniqueness]: https://spec.graphql.org/October2021/#sec-Argument-Uniqueness +fn same_arguments<'a, 'b, T>( + arguments_a: &[(T::Value, Value<'a, T>)], + arguments_b: &[(T::Value, Value<'a, T>)], +) -> bool +where + T: Text<'a> + Eq + AsRef, + T::Value: Hash, +{ + if arguments_a.len() != arguments_b.len() { + return false; + } + + let mut arguments_a_map = HashMap::new(); + for (arg_a_name, arg_a_val) in arguments_a { + arguments_a_map.insert(arg_a_name, arg_a_val); + } + + for (arg_b_name, arg_b_val) in arguments_b { + match arguments_a_map.get(arg_b_name) { + Some(arg_a_val) => { + if *arg_a_val != arg_b_val { + return false; + } + } + None => return false, + } + } + + true +} + +#[cfg(test)] +mod tests { + + use super::same_arguments; + use graphql_parser::query::Value; + + #[test] + fn same_args_test() { + let arguments_a = vec![("a", Value::Int(1.into()))]; + let arguments_b = vec![("a", Value::Int(1.into()))]; + let result = same_arguments::<&str>(&arguments_a, &arguments_b); + assert!(result); + + let arguments_a = vec![("a", Value::Int(1.into())), ("b", Value::Int(2.into()))]; + let arguments_b = vec![("a", Value::Int(1.into())), ("b", Value::Int(2.into()))]; + let result = same_arguments::<&str>(&arguments_a, &arguments_b); + assert!(result); + + let arguments_a = vec![("a", Value::Int(1.into())), ("b", Value::Int(2.into()))]; + let arguments_b = vec![("b", Value::Int(2.into())), ("a", Value::Int(1.into()))]; + let result = same_arguments::<&str>(&arguments_a, &arguments_b); + assert!(result); + + let arguments_a = vec![("a", Value::Int(1.into()))]; + let arguments_b = vec![("a", Value::Int(2.into()))]; + let result = same_arguments::<&str>(&arguments_a, &arguments_b); + assert!(!result); + + let arguments_a = vec![("a", Value::Int(1.into())), ("b", Value::Int(1.into()))]; + let arguments_b = vec![("a", Value::Int(1.into()))]; + let result = same_arguments::<&str>(&arguments_a, &arguments_b); + assert!(!result); + + let arguments_a = vec![("a", Value::Int(1.into()))]; + let arguments_b = vec![("b", Value::Int(1.into()))]; + let result = same_arguments::<&str>(&arguments_a, &arguments_b); + assert!(!result); + } +} diff --git a/src/parser_util.rs b/src/parser_util.rs index 046576da..35a9914a 100644 --- a/src/parser_util.rs +++ b/src/parser_util.rs @@ -1,7 +1,8 @@ use crate::graphql::{EnumSource, __InputValue, __Type, ___Type}; -use crate::gson; +use crate::{gson, merge::merge}; use graphql_parser::query::*; use std::collections::HashMap; +use std::hash::Hash; pub fn alias_or_name<'a, T>(query_field: &graphql_parser::query::Field<'a, T>) -> String where @@ -19,11 +20,12 @@ pub fn normalize_selection_set<'a, 'b, T>( fragment_definitions: &'b Vec>, type_name: &String, // for inline fragments variables: &serde_json::Value, // for directives -) -> Result>, String> +) -> Result>, String> where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { - let mut selections: Vec<&'b Field<'a, T>> = vec![]; + let mut selections: Vec> = vec![]; for selection in &selection_set.items { let sel = selection; @@ -32,6 +34,7 @@ where Err(err) => return Err(err), } } + let selections = merge(selections)?; Ok(selections) } @@ -135,11 +138,12 @@ pub fn normalize_selection<'a, 'b, T>( fragment_definitions: &'b Vec>, type_name: &String, // for inline fragments variables: &serde_json::Value, // for directives -) -> Result>, String> +) -> Result>, String> where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { - let mut selections: Vec<&Field<'a, T>> = vec![]; + let mut selections: Vec> = vec![]; if selection_is_skipped(query_selection, variables)? { return Ok(selections); @@ -147,7 +151,7 @@ where match query_selection { Selection::Field(field) => { - selections.push(field); + selections.push(field.clone()); } Selection::FragmentSpread(fragment_spread) => { let frag_name = &fragment_spread.fragment_name; @@ -180,7 +184,7 @@ where variables, ); match frag_selections { - Ok(sels) => selections.extend(sels.iter()), + Ok(sels) => selections.extend(sels), Err(err) => return Err(err), }; } @@ -199,7 +203,7 @@ where type_name, variables, )?; - selections.extend(infrag_selections.iter()); + selections.extend(infrag_selections); } } } diff --git a/src/resolve.rs b/src/resolve.rs index bc151db4..9a87e77e 100644 --- a/src/resolve.rs +++ b/src/resolve.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::hash::Hash; use crate::builder::*; use crate::graphql::*; @@ -22,7 +23,8 @@ pub fn resolve_inner<'a, T>( schema: &__Schema, ) -> GraphQLResponse where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { match variables { serde_json::Value::Object(_) => (), @@ -130,7 +132,8 @@ fn resolve_query<'a, 'b, T>( fragment_definitions: Vec>, ) -> GraphQLResponse where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let variable_definitions = &query.variable_definitions; resolve_selection_set( @@ -150,7 +153,8 @@ fn resolve_selection_set<'a, 'b, T>( variable_definitions: &Vec>, ) -> GraphQLResponse where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { use crate::graphql::*; @@ -337,7 +341,8 @@ fn resolve_mutation<'a, 'b, T>( fragment_definitions: Vec>, ) -> GraphQLResponse where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { let variable_definitions = &query.variable_definitions; resolve_mutation_selection_set( @@ -357,7 +362,8 @@ fn resolve_mutation_selection_set<'a, 'b, T>( variable_definitions: &Vec>, ) -> GraphQLResponse where - T: Text<'a> + Eq + AsRef, + T: Text<'a> + Eq + AsRef + Clone, + T::Value: Hash, { use crate::graphql::*; diff --git a/test/expected/issue_237_field_merging.out b/test/expected/issue_237_field_merging.out new file mode 100644 index 00000000..81276d4b --- /dev/null +++ b/test/expected/issue_237_field_merging.out @@ -0,0 +1,374 @@ +begin; + -- https://github.com/supabase/pg_graphql/issues/237 + savepoint a; + create table blog_post( + id int primary key, + a text, + b text, + c text, + d text, + e text, + f text + ); + insert into public.blog_post + values (1, 'a', 'b', 'c', 'd', 'e', 'f'); + select jsonb_pretty( + graphql.resolve($$ + query { + blogPostCollection { + edges { + node { + a + ...c_query + ... @include(if: true) { + e + } + } + } + } + blogPostCollection { + edges { + node { + b + ...d_query + ... @include(if: true) { + f + } + } + } + } + } + + fragment c_query on BlogPost { + c + } + + fragment d_query on BlogPost { + d + } + $$) + ); + jsonb_pretty +----------------------------------- + { + + "data": { + + "blogPostCollection": { + + "edges": [ + + { + + "node": { + + "a": "a",+ + "b": "b",+ + "c": "c",+ + "d": "d",+ + "e": "e",+ + "f": "f" + + } + + } + + ] + + } + + } + + } +(1 row) + + rollback to savepoint a; + create table account( + id serial primary key, + email varchar(255) not null + ); + insert into public.account(email) + values + ('aardvark@x.com'); + create table blog( + id serial primary key, + owner_id integer not null references account(id), + name varchar(255) not null + ); + insert into blog(owner_id, name) + values + (1, 'A: Blog 1'); + select jsonb_pretty(graphql.resolve($$ { + accountCollection { + edges { + node { + email + email + id_alias: id + id_alias: id + } + } + } + }$$)); + jsonb_pretty +---------------------------------------------------- + { + + "data": { + + "accountCollection": { + + "edges": [ + + { + + "node": { + + "email": "aardvark@x.com",+ + "id_alias": 1 + + } + + } + + ] + + } + + } + + } +(1 row) + + select jsonb_pretty(graphql.resolve($$ { + accountCollection(first: 1) { + edges { + node { + id + email + } + } + } + accountCollection(first: 1) { + edges { + node { + id + email + } + } + } + }$$)); + jsonb_pretty +--------------------------------------------------- + { + + "data": { + + "accountCollection": { + + "edges": [ + + { + + "node": { + + "id": 1, + + "email": "aardvark@x.com"+ + } + + } + + ] + + } + + } + + } +(1 row) + + select jsonb_pretty(graphql.resolve($$ { + accountCollection(first: $count) { + edges { + node { + id + email + } + } + } + accountCollection(first: $count) { + edges { + node { + id + email + } + } + } + }$$, + jsonb_build_object( + 'count', 1 + ))); + jsonb_pretty +--------------------------------------------------- + { + + "data": { + + "accountCollection": { + + "edges": [ + + { + + "node": { + + "id": 1, + + "email": "aardvark@x.com"+ + } + + } + + ] + + } + + } + + } +(1 row) + + select jsonb_pretty(graphql.resolve($$ { + accountCollection { + edges { + ... on AccountEdge { + cursor + cursor + node { + id + email + } + } + ... on AccountEdge { + cursor + cursor + node { + id + email + } + } + ... cursorsFragment + ... anotherCursorsFragment + cursor + cursor + node { + id + email + } + } + } + } + fragment cursorsFragment on AccountEdge { + cursor + cursor + node { + id + email + } + } + fragment anotherCursorsFragment on AccountEdge { + cursor + cursor + node { + id + email + } + } + $$)); + jsonb_pretty +--------------------------------------------------- + { + + "data": { + + "accountCollection": { + + "edges": [ + + { + + "node": { + + "id": 1, + + "email": "aardvark@x.com"+ + }, + + "cursor": "WzFd" + + } + + ] + + } + + } + + } +(1 row) + + select jsonb_pretty(graphql.resolve($$ { + accountCollection { + edges { + cursor + cursor + node { + id + email + } + node { + id + email + } + } + edges { + cursor + cursor + node { + id + email + } + node { + id + email + } + } + } + } + $$)); + jsonb_pretty +--------------------------------------------------- + { + + "data": { + + "accountCollection": { + + "edges": [ + + { + + "node": { + + "id": 1, + + "email": "aardvark@x.com"+ + }, + + "cursor": "WzFd" + + } + + ] + + } + + } + + } +(1 row) + + select graphql.encode('["public", "account", 1]'::jsonb); + encode +---------------------------------- + WyJwdWJsaWMiLCAiYWNjb3VudCIsIDFd +(1 row) + + select graphql.encode('["public", "blog", 1]'::jsonb); + encode +------------------------------ + WyJwdWJsaWMiLCAiYmxvZyIsIDFd +(1 row) + + select jsonb_pretty(graphql.resolve($$ { + node(nodeId: "WyJwdWJsaWMiLCAiYWNjb3VudCIsIDFd") { + nodeId + ... on Account { + id + str: email + } + ... on Blog { + id + str: name + } + } + } + $$)); + jsonb_pretty +---------------------------------------------------------- + { + + "data": { + + "node": { + + "id": 1, + + "str": "aardvark@x.com", + + "nodeId": "WyJwdWJsaWMiLCAiYWNjb3VudCIsIDFd"+ + } + + } + + } +(1 row) + + select jsonb_pretty(graphql.resolve($$ { + node(nodeId: "WyJwdWJsaWMiLCAiYmxvZyIsIDFd") { + nodeId + ... on Account { + id + str: email + } + ... on Blog { + id + str: name + } + } + } + $$)); + jsonb_pretty +------------------------------------------------------ + { + + "data": { + + "node": { + + "id": 1, + + "str": "A: Blog 1", + + "nodeId": "WyJwdWJsaWMiLCAiYmxvZyIsIDFd"+ + } + + } + + } +(1 row) + +rollback; diff --git a/test/expected/issue_237_field_merging_mismatched.out b/test/expected/issue_237_field_merging_mismatched.out new file mode 100644 index 00000000..c0822b1d --- /dev/null +++ b/test/expected/issue_237_field_merging_mismatched.out @@ -0,0 +1,272 @@ +begin; + -- https://github.com/supabase/pg_graphql/issues/237 + savepoint a; + create table blog_post( + id int primary key, + a text, + b text, + c text, + d text, + e text, + f text + ); + insert into public.blog_post + values (1, 'a', 'b', 'c', 'd', 'e', 'f'); + -- mismatched field names + select jsonb_pretty( + graphql.resolve($$ + query { + blogPostCollection { + edges { + node { + a + } + } + } + blogPostCollection { + edges { + node { + a: b + } + } + } + } + $$) + ); + jsonb_pretty +----------------------------------------------------------- + { + + "data": null, + + "errors": [ + + { + + "message": "Fields `b` and `a` are different"+ + } + + ] + + } +(1 row) + + -- mismatched arguments + select jsonb_pretty( + graphql.resolve($$ + query { + blogPostCollection(filter: { + id: { eq: 1 } + }) { + edges { + node { + a + } + } + } + blogPostCollection { + edges { + node { + b + } + } + } + } + $$) + ); + jsonb_pretty +----------------------------------------------------------------------------------------- + { + + "errors": [ + + { + + "message": "Two fields named `blogPostCollection` have different arguments"+ + } + + ] + + } +(1 row) + + -- mismatched list to node + select jsonb_pretty( + graphql.resolve($$ + query { + blogPostCollection { + a: edges { + cursor + } + } + blogPostCollection { + a: pageInfo { + cursor: endCursor + } + } + } + $$) + ); + jsonb_pretty +---------------------------------------------------------------------- + { + + "data": null, + + "errors": [ + + { + + "message": "Fields `pageInfo` and `edges` are different"+ + } + + ] + + } +(1 row) + + rollback to savepoint a; + create table account( + id serial primary key, + email varchar(255) not null + ); + insert into public.account(email) + values + ('aardvark@x.com'); + create table blog( + id serial primary key, + owner_id integer not null references account(id), + name varchar(255) not null + ); + insert into blog(owner_id, name) + values + (1, 'A: Blog 1'); + select jsonb_pretty(graphql.resolve($$ { + accountCollection { + edges { + node { + email: id + email + } + } + } + }$$)); + jsonb_pretty +---------------------------------------------------------------- + { + + "data": null, + + "errors": [ + + { + + "message": "Fields `email` and `id` are different"+ + } + + ] + + } +(1 row) + + select jsonb_pretty(graphql.resolve($$ { + accountCollection(first: 1) { + edges { + node { + id + email + } + } + } + accountCollection(first: 2) { + edges { + node { + id + email + } + } + } + }$$)); + jsonb_pretty +---------------------------------------------------------------------------------------- + { + + "errors": [ + + { + + "message": "Two fields named `accountCollection` have different arguments"+ + } + + ] + + } +(1 row) + + select jsonb_pretty(graphql.resolve($$ { + accountCollection(first: $count) { + edges { + node { + id + email + } + } + } + accountCollection(first: 1) { + edges { + node { + id + email + } + } + } + }$$, + jsonb_build_object( + 'count', 1 + ))); + jsonb_pretty +---------------------------------------------------------------------------------------- + { + + "errors": [ + + { + + "message": "Two fields named `accountCollection` have different arguments"+ + } + + ] + + } +(1 row) + + select jsonb_pretty(graphql.resolve($$ { + accountCollection(first: $count) { + edges { + node { + id + email + } + } + } + accountCollection(first: $num) { + edges { + node { + id + email + } + } + } + }$$, + jsonb_build_object( + 'count', 1, + 'num', 1 + ))); + jsonb_pretty +---------------------------------------------------------------------------------------- + { + + "errors": [ + + { + + "message": "Two fields named `accountCollection` have different arguments"+ + } + + ] + + } +(1 row) + + select jsonb_pretty(graphql.resolve($$ { + accountCollection(first: 1) { + edges { + node { + id + email + } + } + } + accountCollection { + edges { + node { + id + email + } + } + } + }$$)); + jsonb_pretty +---------------------------------------------------------------------------------------- + { + + "errors": [ + + { + + "message": "Two fields named `accountCollection` have different arguments"+ + } + + ] + + } +(1 row) + +rollback; diff --git a/test/sql/issue_237_field_merging.sql b/test/sql/issue_237_field_merging.sql new file mode 100644 index 00000000..af477309 --- /dev/null +++ b/test/sql/issue_237_field_merging.sql @@ -0,0 +1,238 @@ +begin; + -- https://github.com/supabase/pg_graphql/issues/237 + savepoint a; + create table blog_post( + id int primary key, + a text, + b text, + c text, + d text, + e text, + f text + ); + insert into public.blog_post + values (1, 'a', 'b', 'c', 'd', 'e', 'f'); + select jsonb_pretty( + graphql.resolve($$ + query { + blogPostCollection { + edges { + node { + a + ...c_query + ... @include(if: true) { + e + } + } + } + } + blogPostCollection { + edges { + node { + b + ...d_query + ... @include(if: true) { + f + } + } + } + } + } + + fragment c_query on BlogPost { + c + } + + fragment d_query on BlogPost { + d + } + $$) + ); + + rollback to savepoint a; + + create table account( + id serial primary key, + email varchar(255) not null + ); + + insert into public.account(email) + values + ('aardvark@x.com'); + + create table blog( + id serial primary key, + owner_id integer not null references account(id), + name varchar(255) not null + ); + + insert into blog(owner_id, name) + values + (1, 'A: Blog 1'); + + select jsonb_pretty(graphql.resolve($$ { + accountCollection { + edges { + node { + email + email + id_alias: id + id_alias: id + } + } + } + }$$)); + + select jsonb_pretty(graphql.resolve($$ { + accountCollection(first: 1) { + edges { + node { + id + email + } + } + } + accountCollection(first: 1) { + edges { + node { + id + email + } + } + } + }$$)); + + select jsonb_pretty(graphql.resolve($$ { + accountCollection(first: $count) { + edges { + node { + id + email + } + } + } + accountCollection(first: $count) { + edges { + node { + id + email + } + } + } + }$$, + jsonb_build_object( + 'count', 1 + ))); + + select jsonb_pretty(graphql.resolve($$ { + accountCollection { + edges { + ... on AccountEdge { + cursor + cursor + node { + id + email + } + } + ... on AccountEdge { + cursor + cursor + node { + id + email + } + } + ... cursorsFragment + ... anotherCursorsFragment + cursor + cursor + node { + id + email + } + } + } + } + fragment cursorsFragment on AccountEdge { + cursor + cursor + node { + id + email + } + } + fragment anotherCursorsFragment on AccountEdge { + cursor + cursor + node { + id + email + } + } + $$)); + + select jsonb_pretty(graphql.resolve($$ { + accountCollection { + edges { + cursor + cursor + node { + id + email + } + node { + id + email + } + } + edges { + cursor + cursor + node { + id + email + } + node { + id + email + } + } + } + } + $$)); + + select graphql.encode('["public", "account", 1]'::jsonb); + select graphql.encode('["public", "blog", 1]'::jsonb); + + select jsonb_pretty(graphql.resolve($$ { + node(nodeId: "WyJwdWJsaWMiLCAiYWNjb3VudCIsIDFd") { + nodeId + ... on Account { + id + str: email + } + ... on Blog { + id + str: name + } + } + } + $$)); + + select jsonb_pretty(graphql.resolve($$ { + node(nodeId: "WyJwdWJsaWMiLCAiYmxvZyIsIDFd") { + nodeId + ... on Account { + id + str: email + } + ... on Blog { + id + str: name + } + } + } + $$)); + +rollback; diff --git a/test/sql/issue_237_field_merging_mismatched.sql b/test/sql/issue_237_field_merging_mismatched.sql new file mode 100644 index 00000000..a3969b17 --- /dev/null +++ b/test/sql/issue_237_field_merging_mismatched.sql @@ -0,0 +1,192 @@ +begin; + -- https://github.com/supabase/pg_graphql/issues/237 + savepoint a; + create table blog_post( + id int primary key, + a text, + b text, + c text, + d text, + e text, + f text + ); + insert into public.blog_post + values (1, 'a', 'b', 'c', 'd', 'e', 'f'); + -- mismatched field names + select jsonb_pretty( + graphql.resolve($$ + query { + blogPostCollection { + edges { + node { + a + } + } + } + blogPostCollection { + edges { + node { + a: b + } + } + } + } + $$) + ); + -- mismatched arguments + select jsonb_pretty( + graphql.resolve($$ + query { + blogPostCollection(filter: { + id: { eq: 1 } + }) { + edges { + node { + a + } + } + } + blogPostCollection { + edges { + node { + b + } + } + } + } + $$) + ); + -- mismatched list to node + select jsonb_pretty( + graphql.resolve($$ + query { + blogPostCollection { + a: edges { + cursor + } + } + blogPostCollection { + a: pageInfo { + cursor: endCursor + } + } + } + $$) + ); + + rollback to savepoint a; + + create table account( + id serial primary key, + email varchar(255) not null + ); + + insert into public.account(email) + values + ('aardvark@x.com'); + + create table blog( + id serial primary key, + owner_id integer not null references account(id), + name varchar(255) not null + ); + + insert into blog(owner_id, name) + values + (1, 'A: Blog 1'); + + select jsonb_pretty(graphql.resolve($$ { + accountCollection { + edges { + node { + email: id + email + } + } + } + }$$)); + + select jsonb_pretty(graphql.resolve($$ { + accountCollection(first: 1) { + edges { + node { + id + email + } + } + } + accountCollection(first: 2) { + edges { + node { + id + email + } + } + } + }$$)); + + select jsonb_pretty(graphql.resolve($$ { + accountCollection(first: $count) { + edges { + node { + id + email + } + } + } + accountCollection(first: 1) { + edges { + node { + id + email + } + } + } + }$$, + jsonb_build_object( + 'count', 1 + ))); + + select jsonb_pretty(graphql.resolve($$ { + accountCollection(first: $count) { + edges { + node { + id + email + } + } + } + accountCollection(first: $num) { + edges { + node { + id + email + } + } + } + }$$, + jsonb_build_object( + 'count', 1, + 'num', 1 + ))); + + select jsonb_pretty(graphql.resolve($$ { + accountCollection(first: 1) { + edges { + node { + id + email + } + } + } + accountCollection { + edges { + node { + id + email + } + } + } + }$$)); + +rollback;