Skip to content

Commit add26e7

Browse files
authored
Merge pull request #498 from supabase/merge_fields_new
Reimplement field merging
2 parents f28cd14 + 5f07ebe commit add26e7

11 files changed

+1330
-82
lines changed

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ uuid = "1"
2525
base64 = "0.13"
2626
lazy_static = "1"
2727
bimap = { version = "0.6.3", features = ["serde"] }
28+
indexmap = "2.2"
2829

2930
[dev-dependencies]
3031
pgrx-tests = "=0.11.2"

src/builder.rs

Lines changed: 81 additions & 65 deletions
Large diffs are not rendered by default.

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use serde_json::json;
88
mod builder;
99
mod graphql;
1010
mod gson;
11+
mod merge;
1112
mod omit;
1213
mod parser_util;
1314
mod resolve;

src/merge.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use std::{collections::HashMap, hash::Hash};
2+
3+
use graphql_parser::query::{Field, Text, Value};
4+
use indexmap::IndexMap;
5+
6+
use crate::parser_util::alias_or_name;
7+
8+
/// Merges duplicates in a vector of fields. The fields in the vector are added to a
9+
/// map from field name to field. If a field with the same name already exists in the
10+
/// map, the existing and new field's children are combined into the existing field's
11+
/// children. These children will be merged later when they are normalized.
12+
///
13+
/// The map is an `IndexMap` to ensure iteration order of the fields is preserved.
14+
/// This prevents tests from being flaky due to field order changing between test runs.
15+
pub fn merge<'a, 'b, T>(fields: Vec<Field<'a, T>>) -> Result<Vec<Field<'a, T>>, String>
16+
where
17+
T: Text<'a> + Eq + AsRef<str>,
18+
T::Value: Hash,
19+
{
20+
let mut merged: IndexMap<String, Field<'a, T>> = IndexMap::new();
21+
22+
for current_field in fields {
23+
let response_key = alias_or_name(&current_field);
24+
match merged.get_mut(&response_key) {
25+
Some(existing_field) => {
26+
if can_merge(&current_field, existing_field)? {
27+
existing_field
28+
.selection_set
29+
.items
30+
.extend(current_field.selection_set.items);
31+
}
32+
}
33+
None => {
34+
merged.insert(response_key, current_field);
35+
}
36+
}
37+
}
38+
39+
let fields = merged.into_iter().map(|(_, field)| field).collect();
40+
41+
Ok(fields)
42+
}
43+
44+
fn can_merge<'a, T>(field_a: &Field<'a, T>, field_b: &Field<'a, T>) -> Result<bool, String>
45+
where
46+
T: Text<'a> + Eq + AsRef<str>,
47+
T::Value: Hash,
48+
{
49+
if field_a.name != field_b.name {
50+
return Err(format!(
51+
"Fields `{}` and `{}` are different",
52+
field_a.name.as_ref(),
53+
field_b.name.as_ref(),
54+
));
55+
}
56+
if !same_arguments(&field_a.arguments, &field_b.arguments) {
57+
return Err(format!(
58+
"Two fields named `{}` have different arguments",
59+
field_a.name.as_ref(),
60+
));
61+
}
62+
63+
Ok(true)
64+
}
65+
66+
/// Compares two sets of arguments and returns true only if
67+
/// both are the same. `arguments_a` should not have two
68+
/// arguments with the same name. Similarly `arguments_b`
69+
/// should not have duplicates. It is assumed that [Argument
70+
/// Uniqueness] validation has already been run by the time
71+
/// this function is called.
72+
///
73+
/// [Argument Uniqueness]: https://spec.graphql.org/October2021/#sec-Argument-Uniqueness
74+
fn same_arguments<'a, 'b, T>(
75+
arguments_a: &[(T::Value, Value<'a, T>)],
76+
arguments_b: &[(T::Value, Value<'a, T>)],
77+
) -> bool
78+
where
79+
T: Text<'a> + Eq + AsRef<str>,
80+
T::Value: Hash,
81+
{
82+
if arguments_a.len() != arguments_b.len() {
83+
return false;
84+
}
85+
86+
let mut arguments_a_map = HashMap::new();
87+
for (arg_a_name, arg_a_val) in arguments_a {
88+
arguments_a_map.insert(arg_a_name, arg_a_val);
89+
}
90+
91+
for (arg_b_name, arg_b_val) in arguments_b {
92+
match arguments_a_map.get(arg_b_name) {
93+
Some(arg_a_val) => {
94+
if *arg_a_val != arg_b_val {
95+
return false;
96+
}
97+
}
98+
None => return false,
99+
}
100+
}
101+
102+
true
103+
}
104+
105+
#[cfg(test)]
106+
mod tests {
107+
108+
use super::same_arguments;
109+
use graphql_parser::query::Value;
110+
111+
#[test]
112+
fn same_args_test() {
113+
let arguments_a = vec![("a", Value::Int(1.into()))];
114+
let arguments_b = vec![("a", Value::Int(1.into()))];
115+
let result = same_arguments::<&str>(&arguments_a, &arguments_b);
116+
assert!(result);
117+
118+
let arguments_a = vec![("a", Value::Int(1.into())), ("b", Value::Int(2.into()))];
119+
let arguments_b = vec![("a", Value::Int(1.into())), ("b", Value::Int(2.into()))];
120+
let result = same_arguments::<&str>(&arguments_a, &arguments_b);
121+
assert!(result);
122+
123+
let arguments_a = vec![("a", Value::Int(1.into())), ("b", Value::Int(2.into()))];
124+
let arguments_b = vec![("b", Value::Int(2.into())), ("a", Value::Int(1.into()))];
125+
let result = same_arguments::<&str>(&arguments_a, &arguments_b);
126+
assert!(result);
127+
128+
let arguments_a = vec![("a", Value::Int(1.into()))];
129+
let arguments_b = vec![("a", Value::Int(2.into()))];
130+
let result = same_arguments::<&str>(&arguments_a, &arguments_b);
131+
assert!(!result);
132+
133+
let arguments_a = vec![("a", Value::Int(1.into())), ("b", Value::Int(1.into()))];
134+
let arguments_b = vec![("a", Value::Int(1.into()))];
135+
let result = same_arguments::<&str>(&arguments_a, &arguments_b);
136+
assert!(!result);
137+
138+
let arguments_a = vec![("a", Value::Int(1.into()))];
139+
let arguments_b = vec![("b", Value::Int(1.into()))];
140+
let result = same_arguments::<&str>(&arguments_a, &arguments_b);
141+
assert!(!result);
142+
}
143+
}

src/parser_util.rs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::graphql::{EnumSource, __InputValue, __Type, ___Type};
2-
use crate::gson;
2+
use crate::{gson, merge::merge};
33
use graphql_parser::query::*;
44
use std::collections::HashMap;
5+
use std::hash::Hash;
56

67
pub fn alias_or_name<'a, T>(query_field: &graphql_parser::query::Field<'a, T>) -> String
78
where
@@ -19,11 +20,12 @@ pub fn normalize_selection_set<'a, 'b, T>(
1920
fragment_definitions: &'b Vec<FragmentDefinition<'a, T>>,
2021
type_name: &String, // for inline fragments
2122
variables: &serde_json::Value, // for directives
22-
) -> Result<Vec<&'b Field<'a, T>>, String>
23+
) -> Result<Vec<Field<'a, T>>, String>
2324
where
24-
T: Text<'a> + Eq + AsRef<str>,
25+
T: Text<'a> + Eq + AsRef<str> + Clone,
26+
T::Value: Hash,
2527
{
26-
let mut selections: Vec<&'b Field<'a, T>> = vec![];
28+
let mut selections: Vec<Field<'a, T>> = vec![];
2729

2830
for selection in &selection_set.items {
2931
let sel = selection;
@@ -32,6 +34,7 @@ where
3234
Err(err) => return Err(err),
3335
}
3436
}
37+
let selections = merge(selections)?;
3538
Ok(selections)
3639
}
3740

@@ -135,19 +138,20 @@ pub fn normalize_selection<'a, 'b, T>(
135138
fragment_definitions: &'b Vec<FragmentDefinition<'a, T>>,
136139
type_name: &String, // for inline fragments
137140
variables: &serde_json::Value, // for directives
138-
) -> Result<Vec<&'b Field<'a, T>>, String>
141+
) -> Result<Vec<Field<'a, T>>, String>
139142
where
140-
T: Text<'a> + Eq + AsRef<str>,
143+
T: Text<'a> + Eq + AsRef<str> + Clone,
144+
T::Value: Hash,
141145
{
142-
let mut selections: Vec<&Field<'a, T>> = vec![];
146+
let mut selections: Vec<Field<'a, T>> = vec![];
143147

144148
if selection_is_skipped(query_selection, variables)? {
145149
return Ok(selections);
146150
}
147151

148152
match query_selection {
149153
Selection::Field(field) => {
150-
selections.push(field);
154+
selections.push(field.clone());
151155
}
152156
Selection::FragmentSpread(fragment_spread) => {
153157
let frag_name = &fragment_spread.fragment_name;
@@ -180,7 +184,7 @@ where
180184
variables,
181185
);
182186
match frag_selections {
183-
Ok(sels) => selections.extend(sels.iter()),
187+
Ok(sels) => selections.extend(sels),
184188
Err(err) => return Err(err),
185189
};
186190
}
@@ -199,7 +203,7 @@ where
199203
type_name,
200204
variables,
201205
)?;
202-
selections.extend(infrag_selections.iter());
206+
selections.extend(infrag_selections);
203207
}
204208
}
205209
}

src/resolve.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::HashSet;
2+
use std::hash::Hash;
23

34
use crate::builder::*;
45
use crate::graphql::*;
@@ -22,7 +23,8 @@ pub fn resolve_inner<'a, T>(
2223
schema: &__Schema,
2324
) -> GraphQLResponse
2425
where
25-
T: Text<'a> + Eq + AsRef<str>,
26+
T: Text<'a> + Eq + AsRef<str> + Clone,
27+
T::Value: Hash,
2628
{
2729
match variables {
2830
serde_json::Value::Object(_) => (),
@@ -130,7 +132,8 @@ fn resolve_query<'a, 'b, T>(
130132
fragment_definitions: Vec<FragmentDefinition<'a, T>>,
131133
) -> GraphQLResponse
132134
where
133-
T: Text<'a> + Eq + AsRef<str>,
135+
T: Text<'a> + Eq + AsRef<str> + Clone,
136+
T::Value: Hash,
134137
{
135138
let variable_definitions = &query.variable_definitions;
136139
resolve_selection_set(
@@ -150,7 +153,8 @@ fn resolve_selection_set<'a, 'b, T>(
150153
variable_definitions: &Vec<VariableDefinition<'a, T>>,
151154
) -> GraphQLResponse
152155
where
153-
T: Text<'a> + Eq + AsRef<str>,
156+
T: Text<'a> + Eq + AsRef<str> + Clone,
157+
T::Value: Hash,
154158
{
155159
use crate::graphql::*;
156160

@@ -337,7 +341,8 @@ fn resolve_mutation<'a, 'b, T>(
337341
fragment_definitions: Vec<FragmentDefinition<'a, T>>,
338342
) -> GraphQLResponse
339343
where
340-
T: Text<'a> + Eq + AsRef<str>,
344+
T: Text<'a> + Eq + AsRef<str> + Clone,
345+
T::Value: Hash,
341346
{
342347
let variable_definitions = &query.variable_definitions;
343348
resolve_mutation_selection_set(
@@ -357,7 +362,8 @@ fn resolve_mutation_selection_set<'a, 'b, T>(
357362
variable_definitions: &Vec<VariableDefinition<'a, T>>,
358363
) -> GraphQLResponse
359364
where
360-
T: Text<'a> + Eq + AsRef<str>,
365+
T: Text<'a> + Eq + AsRef<str> + Clone,
366+
T::Value: Hash,
361367
{
362368
use crate::graphql::*;
363369

0 commit comments

Comments
 (0)