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

Polymorphic queries -> discriminated union types #1090

Merged
merged 19 commits into from
Sep 30, 2024

Conversation

CarsonF
Copy link
Collaborator

@CarsonF CarsonF commented Aug 31, 2024

I revived #508

Fixes #454

Probably a breaking change since the types are getting stricter.

@CarsonF
Copy link
Collaborator Author

CarsonF commented Sep 1, 2024

I restored some of the old functionality, and gated the stricter types behind an opt-in via declaration merging. Just looking for ways to get this out without a major. But open to reverting it or other suggestions.

@CarsonF

This comment was marked as resolved.

@CarsonF

This comment was marked as resolved.

@CarsonF CarsonF force-pushed the polymorphic-improvements branch 2 times, most recently from 92f8e59 to 50c8560 Compare September 4, 2024 03:53
Comment on lines 46 to 52
? ScalarType<
"std::str",
TypesystemOptions["future"]["strictTypeNames"] extends true
? Root[typeof typenameSymbol]
: string,
Root[typeof typenameSymbol]
>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not sure I understand the difference between Scalar's TsType & TsArgType. I was thinking that they were the output / input types respectively.

I don't understand why the input type would be called out here explicitly and set to the literal string - where/how would that be used be as an input?

I figured the output type remained as a string for BC reasons. So I created another future option to make it strict.
This is for this btw:

scope => ({ t: scope.__type__.name })

But then I was thinking about this case

scope => ({ __type__: { name: true } })

Which had already made the output type a strict string.

So I thought the other case was a miss, but there is some explicit code here.

I guess all that to say I could just do this:

Suggested change
? ScalarType<
"std::str",
TypesystemOptions["future"]["strictTypeNames"] extends true
? Root[typeof typenameSymbol]
: string,
Root[typeof typenameSymbol]
>
? ScalarType<"std::str", Root[typeof typenameSymbol]>

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@scotttrinh thoughts here? I don't think we need a flag for this & just use my suggestion

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good question. I'm not sure, but I'm fine with going with the suggested change for now and looking at what the fallout is.

Comment on lines 1401 to 1403
if (hasPolyEl && !seen.has("__typename")) {
addLine("__typename := .__type__.name");
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The new functionality implies a __typename field when needed. If the user specifies this and it doesn't match what is assumed then the types will be wrong. We could check the __typename value here if it is already present (from a user) and confirm it is .__type__.name or throw.
I couldn't figure out a clean way to do this because the actual generated EdgeQL explicitly references the scope, which I couldn't figure out how to access.

single __typename := __scope_1_defaultMovie.__type__.name,

Copy link
Collaborator

Choose a reason for hiding this comment

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

ok, we can leave that as a follow up investigation, thanks for the details!

Copy link
Collaborator

@scotttrinh scotttrinh left a comment

Choose a reason for hiding this comment

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

Very solid 👏 Thanks for the huge effort here. Love that we now have some feature-flagging/opt-in machinery set up in the typesystem, too, that seems generally very useful.

Comment on lines 1401 to 1403
if (hasPolyEl && !seen.has("__typename")) {
addLine("__typename := .__type__.name");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

ok, we can leave that as a follow up investigation, thanks for the details!

@scotttrinh
Copy link
Collaborator

One thing I want to think about is maybe having some tests with the legacy behavior just to catch any regressions we make between now and when we switch the new behavior to the default in a future major release. I can add those.

@@ -1299,6 +1301,7 @@ function shapeToEdgeQL(
if (val.__kind__ === ExpressionKind.PolyShapeElement) {
polyType = val.__polyType__;
val = val.__shapeElement__;
hasPolyEl = true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, we don't actually have a runtime representation of the type-level "opt in" for this behavior, so this ends up still being an arguably breaking change since we will be returning more. Definitely arguable as to whether or not returning strictly more data is breaking, but anyone who currently has tests for the existing behavior will get failures.

I wonder if we can make a runtime configuration, and actually infer the type-level config from that runtime config?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'd probably recommend a generator config flag in that case.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah, agreed. we can even have it generate/copy a different typesystem.ts file to avoid the overhead of being conditional in the types.

Copy link
Collaborator Author

@CarsonF CarsonF Sep 19, 2024

Choose a reason for hiding this comment

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

I probably wouldn't say that adding an additional field is a breaking change. User tests may not break. Only if they are asserting exactly or using snapshots.

But I can see how it's not black/white. IMO none of this stuff is. kinda like changes in TS releases lol

Copy link
Collaborator

Choose a reason for hiding this comment

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

User tests may not break. Only if they are asserting exactly or using snapshots.

I think this is pretty common: make a query (or use a data layer more likely) and assert that the returned data is precisely these objects. I agree in general that if you are introducing new properties/data in your own application, maybe you don't consider that a breaking change, but this might warrant a little more care to avoid tripping up people when upgrading point releases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Are you looking for a CLI flag for this as well?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, I think we should add it, like --enable-polymorphism-as-discriminated-unions as silly as that sounds. This is non-trivial, but I think it's worth it to avoid going with a major version bump here. I'm honestly open to the major version bump, but that will absolutely delay getting this released.

If you think you have the bandwidth to add the CLI flag, let me know. Otherwise, I'll stick it in the queue and try to get to it (or maybe have @diksipav pick this up) ASAP.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

--future-polymorphism-as-discriminated-unions

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah, that was my first thought, but then I thought maybe in the next major we could allow you to still opt-into the old behavior, but now I'm thinking there is no advantage to keeping the old behavior around in the next major.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I don't see a reason to keep the old behavior long term.

Comment on lines 15 to 41
/**
* Use declaration merging to set the {@link TypesystemOptions} via this
*/
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface SetTypesystemOptions {}

export type TypesystemOptions = {
future: {
/**
* Opt-in to the new discriminated union functionality for polymorphism.
*/
polymorphismAsDiscriminatedUnions: SetTypesystemOptions extends {
future: { polymorphismAsDiscriminatedUnions: true };
}
? true
: false;
/**
* Opt-in to strict __type__.name string literal unions.
*/
strictTypeNames: SetTypesystemOptions extends {
future: { strictTypeNames: true };
}
? true
: false;
};
};

Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we still need this or can we use import future from "./future"; and get the type from the import?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think it depends on what the DX goal is.

If the goal is to just use the cli flag and expect that future.ts is statically overridden, then no we don't need TypesystemOptions or the declaration merging on SetTypesystemOptions.

If the goal is to allow this to be set dynamically. Then yes we still need it.
In that case, devs would set the futures to true.

future.polymorphismAsDiscriminatedUnions = true;
future.strictTypeNames = true;

And then denote to TS that they intend to do that with the declaration merge.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think it makes sense for this to be dynamic in any way at all. You're either opting into this future behavior and will refactor all of your code to match it, or you're not. I also care about making sure the runtime behavior and types are aligned, so allowing (or really I guess encouraging) both seems dangerous.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Then yeah we don't need any of this. Just base it all of the futures object like you suggested

computeObjectShape<
Pointer["target"]["__pointers__"] & Pointer["properties"],
Element,
TypesystemOptions["future"]["strictTypeNames"] extends true
Copy link
Collaborator

Choose a reason for hiding this comment

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

So like,

Suggested change
TypesystemOptions["future"]["strictTypeNames"] extends true
(typeof future["strictTypeNames"]) extends true

etc

Comment on lines +241 to +242
case "--future":
options.future = true;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think this is sufficient. We need to be able to specify which futures you are opting into. I could be ok with future A and have updated my codebase to account for it. But then future B is added and I would be automatically opted into it as well which is a breaking change I wasn't ready for.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, @diksipav and I talked a bit about this yesterday and here are my thoughts:

  1. From the perspective of a general feature of the generator package, this future behavior is similar to TypeScript's own strict behavior, which if you opt into directly is breaking between versions. It stands for some set of more granular configuration values, so I think this is useful in general.
  2. From the perspective of this future behavior, I expect most (all?) folks to want to opt into both, but also would be fine exposing granular flags for each here, too. I don't want to add tests for each permutation since we plan on making this the only behavior in a future breaking change, but that does mean there might be bugs or poorly understood side-effects of having one or the other enabled, but not both.

I don't think we need to block on this, though, and I'm fine with trying to land this as-is now. I'll leave it up to @diksipav to decide if she wants to address this in this PR specifically, or as a follow-up PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

hey lets merge this (once it's ready) and I'll create right away a follow up PR with individual future flags

Copy link
Contributor

Choose a reason for hiding this comment

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

@scotttrinh I'll merge this and start working on the PR with individual flags, if u think we should "swap" tests will do it in the next PR

@diksipav diksipav merged commit bcf4a8d into edgedb:master Sep 30, 2024
10 checks passed
@CarsonF CarsonF deleted the polymorphic-improvements branch September 30, 2024 16:55
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.

Better return types for .__type__.name and polymorphic queries if possible
4 participants