-
Notifications
You must be signed in to change notification settings - Fork 275
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
FED-191: [Operation Processing] port operation.optimize
#5207
Conversation
@duckki, please consider creating a changeset entry in |
f364f53
to
f7486ef
Compare
let Some(fragments) = fragments else { | ||
return Ok(()); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel it makes more sense to do this at the usage site, and require passing in a fragments: &NamedFragments
.
if fragments.is_empty() { | ||
return Ok(()); | ||
} | ||
assert!( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can avoid this assert either by taking a Option<NonZeroU32>
parameter, or maybe even removing it entirely and hardcoding the value to 2 (AFAIK JS only used it in tests)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. I'll remove the parameter altogether.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one is complicated too 😱 I think I'll have to do another pass on Monday to make sure I didn't miss anything. The biggest thing for me is more tests. I know the JS repo's got a ton of optimize tests that we can borrow.
pub(crate) fn output_ast_type(&self) -> Result<&ast::Type, FederationError> { | ||
Ok(&self.field_position.get(self.schema.schema())?.ty) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this probably also needs to live on an impl outside of this internal mod. perhaps on Field
?
impl Field { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it's no longer used. Let's make it private and then decide where to put it later.
@@ -2616,7 +2600,7 @@ impl SelectionSet { | |||
// version, but its Rust version doesn't use `lazy_map`. | |||
// PORT_NOTE #2: Taking ownership of `self` in this method was considered. However, calling | |||
// `Arc::make_mut` on the `Arc` fields of `self` didn't seem better than cloning Arc instances. | |||
fn lazy_map( | |||
pub(crate) fn lazy_map( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't particularly love that we are exposing more of these methods outside of the operation
file, but I suppose we will deal with once we start fixing up this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree.
/// Compute the reduced set of NamedFragments that are used in the selection set at least | ||
/// `min_usage_to_optimize` times. Also, computes the new selection set that uses only the | ||
/// reduced set of fragments by expanding the other ones. | ||
fn reduce_named_fragments( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just reduce
? we are already on the NamedFragments
impl. or just call it optimize
like on the other impls?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point.
self.named_fragments = final_fragments; | ||
Ok(()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you've got some tests below already. There are also some additional tests in the JS repo that specifically look at fragment optimization. It'd be good to add those here as well. This functionality is complicated, and we are not doing exactly the same thing as the JS QP, so it'd be really good to port these along as well. https://github.com/apollographql/federation/blob/28850ffba17d4201321dc6de0edc24cae48ec5e5/internals-js/src/__tests__/operations.test.ts#L37-L1033
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I didn't get to that today. I'll tackle this next week.
BTW, I was wondering if I would step on Simon's foot, porting tests. Let's synchronize on this next week. @SimonSapin
impl Operation { | ||
// PORT_NOTE: The JS version of `optimize` takes an optional `minUsagesToOptimize` argument. | ||
// However, it's only used in tests. So, it's removed in the Rust version. | ||
const DEFAULT_MIN_USAGES_TO_OPTIMIZE: u32 = 2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we just remove this const entirely then? or do you want to keep it the argument to pass to tests as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It didn't seem necessary to test different values. However, I think literals would better be declared as const for clarity. Otherwise, they can become magic numbers that no one knows what they are.
pub(crate) fn optimize_at_root( | ||
&self, | ||
fragments: &NamedFragments, | ||
) -> Result<SelectionSet, FederationError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering why we are not mutating the existing structs with all these optimize
functions, but are instead creating a brand new selection set/inline fragment selection/etc. I feel like we are doing a lot of cloning (and I know it's an Arc), only to use that cloned item to create the exact same modified type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea.
And I noticed a bug in this function and fixed it.
fn optimize( | ||
&self, | ||
fragments: &NamedFragments, | ||
validator: &mut FieldsConflictMultiBranchValidator, | ||
) -> Result<Selection, FederationError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it'll be easier to reason about if this returns a FieldSelection
instead? Something like this:
fn optimize( | |
&self, | |
fragments: &NamedFragments, | |
validator: &mut FieldsConflictMultiBranchValidator, | |
) -> Result<Selection, FederationError> { | |
fn optimize( | |
&mut self, | |
fragments: &NamedFragments, | |
validator: &mut FieldsConflictMultiBranchValidator, | |
) -> Result<Self, FederationError> { |
I am thinking we are walking down the tree from the SelectionSet
root, modifying and optimising it's potential children. It might be easier if the results bubble up as their own respective types, and we handle converting them into a Selection
at one of the top level nodes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potentially might not work due to borrow checker. But I guess what I am trying to say is that I am finding it difficult to follow that FieldSelection
/InlineFragmentSelection
/etc all optimize, theoretically their own values, but they don't actually always modify their own selection_set field and also don't return the selection_set
but a selection
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. In the JS version, optimze()
function was a virtual function with the same return type (Selection
). But, in Rust, we don't have do that. The wrinkle here is that InlineFragmentSelection
can be optimized into FragmentSpreadSelection
. So, it makes sense to add a new enum InlineOrFragmentSelection
, which helps to eliminate one unreachable!()
branch case.
impl InlineFragmentSelection { | ||
fn optimize( | ||
&self, | ||
fragments: &NamedFragments, | ||
validator: &mut FieldsConflictMultiBranchValidator, | ||
) -> Result<Selection, FederationError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I did a second pass, and nothing really jumps out at me. I think the main thing to do is to port those optimisation tests. It'll be a huge help here!
- Note: JS version caches the named fragments in FragmentSpreadSelection.
…ke a parent type argument
…ectly - also, ported several operation.optimization tests
I ported a few tests. That detected a bug in optimization and in a few other places. I'd like to merge this PR and hunt the rest (if any) together as we port tests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Glad you already found some bugs! Let's merge and find some more.
normalized_operation.named_fragments = Default::default(); | ||
insta::assert_snapshot!(normalized_operation, @r###" | ||
query NamedFragmentQuery { | ||
foo { | ||
id | ||
bar | ||
baz | ||
} | ||
} | ||
"###); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want to have snapshots inside mods? have we done this before? i don't want to us to introduce a new pattern for mod tests
There are some snapshot tests changes, and the new changes produced by these snapshots genuinely look incorrect - spacing for fragment definitions looks off 🤔 |
- Some have been fixed. Updated their annotations. - The rest are failing with different issues.
I fixed the spacing issue and a bug in |
Co-authored-by: Iryna Shestak <shestak.irina@gmail.com>
…hql#5207) Co-authored-by: Iryna Shestak <shestak.irina@gmail.com>
implemented the rest of
Operation::optimize
methodhttps://apollographql.atlassian.net/browse/FED-191
Depends on:
tryOptimizeSubselectionWithFragments
#5195Checklist
Complete the checklist (and note appropriate exceptions) before the PR is marked ready-for-review.
Exceptions
Have a few simple tests. More tests shall be done by poring existing federation tests.
Notes
Footnotes
It may be appropriate to bring upcoming changes to the attention of other (impacted) groups. Please endeavour to do this before seeking PR approval. The mechanism for doing this will vary considerably, so use your judgement as to how and when to do this. ↩
Configuration is an important part of many changes. Where applicable please try to document configuration examples. ↩
Tick whichever testing boxes are applicable. If you are adding Manual Tests, please document the manual testing (extensively) in the Exceptions. ↩