-
Notifications
You must be signed in to change notification settings - Fork 50
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
Schema rename multiple fields #828
Conversation
Should stay unchanged because we don't rename scalars
To have them fit a consistent format: first, with specific details, followed by whether it's a one-to-many rename or a suppression, and finally if it's a one-to-many rename, whther or not it includes the original field. If we have both a one-to-many rename and a suppression in the same rename, the one-to-many rename comes first (e.g. in test_one_to_many_rename_and_suppress).
Possibly non-obvious decisions about the changes that are already here
|
Non-obvious decisions that I haven't made yet and would like feedback on
type A {
Listb: [B]
}
type B {
c: C
}
enum C {
YES
NO
MAYBE
} Suppressing
union AOrBOrC = A | B | C If we suppress union AOrBOrC = B | C This has two drawbacks: the name may be misleading because it doesn't match the types, and may reveal information about the datatypes, which suppression is supposed to prevent. Is this union name issue because of the test input unions have names based on which types they could be, or is there something more fundamental we should be concerned about here? If the former, we may be able to just leave the union name as-is. One final note: if we also suppressed
For instance, if we start with this enum as part of the schema: enum Height {
TALL
SHORT
} And rename enum Height {
TALL
SHORT
}
enum NewHeight {
TALL
SHORT
} But if we also have a type that uses type Droid {
height: Height
} Neither of the following two options are good because now the schema has name conflicts: type Droid {
height: Height
}
type Droid {
height: NewHeight
} type Droid {
height: Height
height: NewHeight
} A plausible third option exists, involving unions: type Droid {
height: HeightOrNewHeight
}
union HeightOrNewHeight = Height | NewHeight However, this also fails if The same problem happens with non-enum fields.
|
Note: still no need to look at the code since I'll almost certainly split this into smaller PRs-- before diving into implementations or even writing out a bunch of tests that I'm not sure are correct, I wanted to iron out some uncertain and tricky parts conceptually in these two previous comments. |
Issue 1: Issue 2:
So I think it's best to solely omit the object type from the union type and do nothing more. Two ideas that I am thinking to improve on this would be to:
I would prioritize getting a basic version of schema renaming working before focusing on improvements like these though. For now simply omitting the object type seems good enough to me. Issue 3:
For unions, we need to have both the old and new type as part of the union field. (The old type to avoid breaking any existing queries, and the new type to allow users to start referring to solely the new type in queries). |
As you correctly point out, the operations happening here are complex and difficult to think about. Thank you for writing up all the edge cases you are considering, it was very useful. I'd like to make an observation here that might simplify things. We have two use cases that we are trying to cover here:
The latter only requires 1-1 renaming, which are conceptually simple. The former is solvable by 1-many renaming, but that's not the only viable solution and doesn't have to be the full story here. So I'd propose a hybrid approach for supporting migrations: allow 1-many renaming of fields, and use an alternative approach for types of all kinds: scalars, enums, interfaces, objects, unions, etc. That means I'm suggesting we reduce scope and only allow renaming types to 0 or 1 names, raising Also, I'd recommend we play it safe and don't implicitly cascade type hiding, since on a large schema (ours stretch into hundreds of MB!) it's going to be impractical to hand-review the changes resulting from a given operation. Instead, my preference would be to make the user explicitly state all the changes they'd like to perform, and have the code complain by raising errors if we end up in an illegal state, such as:
Importantly, such complaints must be clear ("input X said to do Y which caused illegal state Z") and actionable ("here are the actions that would resolve the problem") for the user. |
Thanks for the feedback! A few follow up questions:
type A {
Listb: [B]
}
type B {
c: C
}
enum C {
YES
NO
MAYBE
} and
|
Great points! My thoughts inline:
This is definitely a viable approach. I would argue it's slightly less desirable, though, because it doesn't help users be self-sufficient. Let me take a step back and explain what I mean. GraphQL has a built-in directive called Deprecating a type in GraphQL, however, is semantically strange. A type definition by itself doesn't do anything except for "special" types like the root query or mutation type: if no fields of that type are reachable, you can't do anything with it anyway — hence the special-casing of the root query/mutation types, since those are reachable by default. Correspondingly, Let's refer to having both old and new copies in the schema as a "soft" migration (both queries valid, one deprecated), and let's call only having the new copy in the schema + a fall-back path that accepts queries under the old schema a "hard" migration. Because of the On the basis of that, my recommendation was to support soft migrations for field definitions and enum values (because it's very good and not that hard), and only support hard migrations for object types/interfaces/unions/enums (because soft migrations are less good here anyway, and much harder to pull off).
After applying all the renaming, the generated schema would be used to write queries against, yes. It may be used through a tool like GraphiQL directly, or simply used by the compiler to compile a user's query against that schema. I wouldn't necessarily worry about
I'd pick the middle option. As you point out, the first one is counter to the user's expressed intent, and giving that message as feedback is akin to a "don't do that", which is unhelpful. The middle option is a nice balance of "reasonable to implement" and "works most of the time", because most real-world schemas wouldn't have these cascading situations, and if they do, they are unlikely to have many of them. Saving the user from needing to iterate 2-3 times, on a relative basis given our development bandwidth, is not worth the comparatively large amount of effort it would take to implement a system that would find and appropriately communicate the correct sequence of steps to take and the reasons for them to the user. |
As described in the discussion of PR #828, we actually don't need to support 1-many renaming for types, only for fields. That eliminates the need for a number of tests that were previously here. This commit isn't the end of the story though, since I plan to add in the 1-many argument for rename_schema after this.
Sounds good to me-- I'll aim to get the type hard migration/ suppression part for Just a few lingering issues:
suppresses
|
Good points. Open to adopting the API you suggest, mapping types to dicts of fields with their own remappings — I'd just suggest that we treat that info as overriding the default of "change nothing", so that we don't have to have lots of entries that simply communicate "don't change this field". To avoid describing the illegal intermediate state in the spec, perhaps we could specify the order of operations more fully — maybe something like "field suppressions happen before field renames, either 1-1 or 1-many". So long as the implementation's start-to-finish behavior is indistinguishable from the spec, whether the implementation actually follows that order shouldn't matter. |
Most of the work here has gotten merged, closing for the same reason as 834 to clean up |
Tests for
rename_schema
for #810. Don't look at this yet since I'll be adding comments explaining some of the trickier parts of what I've written so far and discussing other edge cases for which I haven't written tests yet.I'll probably split this into a number of smaller PRs as this grows (since this is already a pretty decent-sized PR), but for now I wanted to have things all in one place so i can keep track of things.