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

Implement field merging #472

Merged
merged 17 commits into from
Dec 26, 2023
Merged

Implement field merging #472

merged 17 commits into from
Dec 26, 2023

Conversation

bryanmylee
Copy link
Contributor

@bryanmylee bryanmylee commented Dec 17, 2023

What kind of change does this PR introduce?

Introduces field merging to properly combine multiple selections of the same object fields. This allows queries like:

query {
  blogPostCollection {
    edges {
      node {
        id
      }
    }
  }
  blogPostCollection {
    pageInfo {
      hasNextPage
    }
  }
}

... to resolve correctly to:

{
  "data": {
    "blogPostCollection": {
      "edges": [],
      "pageInfo": {
        "hasNextPage": false
      }
    }
  }
}

This PR also handles the case where two overlapping fields have different arguments, returning an error in the case.

query {
  blogPostCollection {
    edges {
      node {
        id
      }
    }
  }
  blogPostCollection(filter: {
    id: { eq: 1 }
  }) {
    pageInfo {
      hasNextPage
    }
  }
}
{
  "errors": [
    {
      "message": "Fields \"blogPostCollection\" conflict because they have differing arguments"
    }
  ]
}

What is the current behavior?

Object fields that appear more than once will override each other, resulting in only the last-seen field being returned.

{
  "data": {
    "blogPostCollection": {
      "pageInfo": {
        "hasNextPage": false
      }
    }
  }
}

This also affects #237 since overlapping fragment subqueries behave like overlapping fields.

What is missing?

Overlapping fields are merged correctly.

However, I still need to figure out how to determine the return type of a given field at the parser level so that I can return an error if two conflicting fields with different data types / shapes are merged with aliases.

There is also the issue of updating the positions and spans of a merged field. It doesn't affect the final output, but it may be useful in the future to define either a minimally containing span for all fields that make up a merged field, or store a vector of positions and spans.

@bryanmylee
Copy link
Contributor Author

bryanmylee commented Dec 17, 2023

For additional context, I've added std::fmt::Debug traits to the T generic type that's passed around for easier debugging, but this can be removed if preferred.

Additionally, I've converted selections: Vec<&'b Field<'a, T>> into selections: Vec<Field<'a, T>> to more easily consume and mutate fields when merging. Specifically, using any sort of reference posed issues when I tried to consume the selections of a "from" field to be added to the selections of a mutable "to" field.

However, this does increase the heap allocation size and I'm probably not doing the best thing. Any suggestions to improve this would be appreciated.

take_mut is a zero-dependency crate that allows us to consume and replace a vector element in place with zero cost as far as I'm aware.

@bryanmylee bryanmylee marked this pull request as ready for review December 19, 2023 09:52
@olirice olirice requested review from imor and olirice December 19, 2023 22:12
@olirice
Copy link
Contributor

olirice commented Dec 19, 2023

incredible stuff @bryanmylee!

I'll work through this tomorrow and follow up with any feedback

I'd like to get @imor 's take on this too since he's a much stronger rust developer. he's currently out sick so it might be a few days before he's able to get to it. just keeping you in the loop

Comment on lines 136 to 192
pub fn has_same_type_shape(
field_name: &str,
type_name: &str,
type_a: &__Type,
type_b: &__Type,
) -> Result<(), String> {
let mut type_a = type_a;
let mut type_b = type_b;

if matches!(type_a, __Type::NonNull(_)) || matches!(type_b, __Type::NonNull(_)) {
if let (__Type::NonNull(nullable_type_a), __Type::NonNull(nullable_type_b)) =
(type_a, type_b)
{
type_a = nullable_type_a.type_.as_ref();
type_b = nullable_type_b.type_.as_ref();
} else {
return Err(format!(
"Fields '{}' conflict because only one is non nullable",
field_name,
));
}
}

if matches!(type_a, __Type::List(_)) || matches!(type_b, __Type::List(_)) {
if let (__Type::List(list_type_a), __Type::List(list_type_b)) = (type_a, type_b) {
type_a = list_type_a.type_.as_ref();
type_b = list_type_b.type_.as_ref();
} else {
return Err(format!(
"Fields '{}' conflict because only one is a list type",
field_name,
));
}

return has_same_type_shape(field_name, type_name, type_a, type_b);
}

if matches!(type_a, __Type::Enum(_))
|| matches!(type_b, __Type::Enum(_))
|| matches!(type_a, __Type::Scalar(_))
|| matches!(type_b, __Type::Scalar(_))
{
return if type_a == type_b {
Ok(())
} else {
Err(format!(
"Fields '{}' conflict due to mismatched types",
field_name,
))
};
}

// TODO handle composite types?

// Subfield type shapes will be checked on a later pass.
Ok(())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is confirming that the __Type of an and b are the same. As I understand, they need to be the exact same __Type in order to merge, correct?

If so, could we do something like make __Type hashable or equatable so they can be directly compared without this logic? The reason I prefer that kind of solution is because it would fail loudly if we introduce new __Types vs quietly not working with merging

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is a check to make sure both types have the same shape following the GQL specification linked in the discussion thread.

I did this explicitly because it gives me the ability to return specific errors for the different type conflicts, and keep the system close to spec.

I'm not sure if we should consider all __Type variants in the sameness check since it's not in spec, but an Eq trait would imply that the values are equal across across all variants.

However, I do agree it should be co-located closer to the __Type definition.

Another thing I just noticed was that I forgot to include type_name in the error messages so it's an unused variable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should consider all __Type variants in the sameness check since it's not in spec, but an Eq trait would imply that the values are equal across across all variants.

That's interesting, I didn't quite follow. Could I hassle you to give an example that highlights the issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My guess was if a __Type::Edge and __Type::Node are compared, they should not fail the shape sameness check yet, as the __Type::Node might have a compatible set of fields for a selection to be merged with the __Type::Edge.

Of course, I just realized this case would rarely be encountered since field selections have to have the same field name anyways, so I don't see where this issue might occur.

With that in mind, we can definitely just use a Eq check, unless we want to provide better error messages?

Cargo.toml Outdated Show resolved Hide resolved
@olirice olirice merged commit eecd43e into supabase:master Dec 26, 2023
4 checks passed
imor added a commit that referenced this pull request Feb 21, 2024
This reverts commit eecd43e, reversing
changes made to b6a3458.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants