Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplement field merging #498

Merged
merged 11 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
146 changes: 81 additions & 65 deletions src/builder.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use serde_json::json;
mod builder;
mod graphql;
mod gson;
mod merge;
mod omit;
mod parser_util;
mod resolve;
Expand Down
143 changes: 143 additions & 0 deletions src/merge.rs
Original file line number Diff line number Diff line change
@@ -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<Field<'a, T>>) -> Result<Vec<Field<'a, T>>, String>
where
T: Text<'a> + Eq + AsRef<str>,
T::Value: Hash,
{
let mut merged: IndexMap<String, Field<'a, T>> = IndexMap::new();

for current_field in fields {
let response_key = alias_or_name(&current_field);
match merged.get_mut(&response_key) {
Some(existing_field) => {
if can_merge(&current_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<bool, String>
where
T: Text<'a> + Eq + AsRef<str>,
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<str>,
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);
}
}
24 changes: 14 additions & 10 deletions src/parser_util.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,11 +20,12 @@ pub fn normalize_selection_set<'a, 'b, T>(
fragment_definitions: &'b Vec<FragmentDefinition<'a, T>>,
type_name: &String, // for inline fragments
variables: &serde_json::Value, // for directives
) -> Result<Vec<&'b Field<'a, T>>, String>
) -> Result<Vec<Field<'a, T>>, String>
where
T: Text<'a> + Eq + AsRef<str>,
T: Text<'a> + Eq + AsRef<str> + Clone,
T::Value: Hash,
{
let mut selections: Vec<&'b Field<'a, T>> = vec![];
let mut selections: Vec<Field<'a, T>> = vec![];

for selection in &selection_set.items {
let sel = selection;
Expand All @@ -32,6 +34,7 @@ where
Err(err) => return Err(err),
}
}
let selections = merge(selections)?;
Ok(selections)
}

Expand Down Expand Up @@ -135,19 +138,20 @@ pub fn normalize_selection<'a, 'b, T>(
fragment_definitions: &'b Vec<FragmentDefinition<'a, T>>,
type_name: &String, // for inline fragments
variables: &serde_json::Value, // for directives
) -> Result<Vec<&'b Field<'a, T>>, String>
) -> Result<Vec<Field<'a, T>>, String>
where
T: Text<'a> + Eq + AsRef<str>,
T: Text<'a> + Eq + AsRef<str> + Clone,
T::Value: Hash,
{
let mut selections: Vec<&Field<'a, T>> = vec![];
let mut selections: Vec<Field<'a, T>> = vec![];

if selection_is_skipped(query_selection, variables)? {
return Ok(selections);
}

match query_selection {
Selection::Field(field) => {
selections.push(field);
selections.push(field.clone());
}
Selection::FragmentSpread(fragment_spread) => {
let frag_name = &fragment_spread.fragment_name;
Expand Down Expand Up @@ -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),
};
}
Expand All @@ -199,7 +203,7 @@ where
type_name,
variables,
)?;
selections.extend(infrag_selections.iter());
selections.extend(infrag_selections);
}
}
}
Expand Down
16 changes: 11 additions & 5 deletions src/resolve.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::HashSet;
use std::hash::Hash;

use crate::builder::*;
use crate::graphql::*;
Expand All @@ -22,7 +23,8 @@ pub fn resolve_inner<'a, T>(
schema: &__Schema,
) -> GraphQLResponse
where
T: Text<'a> + Eq + AsRef<str>,
T: Text<'a> + Eq + AsRef<str> + Clone,
T::Value: Hash,
{
match variables {
serde_json::Value::Object(_) => (),
Expand Down Expand Up @@ -130,7 +132,8 @@ fn resolve_query<'a, 'b, T>(
fragment_definitions: Vec<FragmentDefinition<'a, T>>,
) -> GraphQLResponse
where
T: Text<'a> + Eq + AsRef<str>,
T: Text<'a> + Eq + AsRef<str> + Clone,
T::Value: Hash,
{
let variable_definitions = &query.variable_definitions;
resolve_selection_set(
Expand All @@ -150,7 +153,8 @@ fn resolve_selection_set<'a, 'b, T>(
variable_definitions: &Vec<VariableDefinition<'a, T>>,
) -> GraphQLResponse
where
T: Text<'a> + Eq + AsRef<str>,
T: Text<'a> + Eq + AsRef<str> + Clone,
T::Value: Hash,
{
use crate::graphql::*;

Expand Down Expand Up @@ -337,7 +341,8 @@ fn resolve_mutation<'a, 'b, T>(
fragment_definitions: Vec<FragmentDefinition<'a, T>>,
) -> GraphQLResponse
where
T: Text<'a> + Eq + AsRef<str>,
T: Text<'a> + Eq + AsRef<str> + Clone,
T::Value: Hash,
{
let variable_definitions = &query.variable_definitions;
resolve_mutation_selection_set(
Expand All @@ -357,7 +362,8 @@ fn resolve_mutation_selection_set<'a, 'b, T>(
variable_definitions: &Vec<VariableDefinition<'a, T>>,
) -> GraphQLResponse
where
T: Text<'a> + Eq + AsRef<str>,
T: Text<'a> + Eq + AsRef<str> + Clone,
T::Value: Hash,
{
use crate::graphql::*;

Expand Down
Loading
Loading