-
-
Notifications
You must be signed in to change notification settings - Fork 576
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
Table inheritance / interfaces #387
Comments
Not currently, but we could do something with Postgres table inheritance. What is your use case? |
I think the search example from this post is quite good: As I understand it right now, I would need to write a postgres function which returns either a specific type of a table or declare a custom type which only contains the properties for that specific result set. Table inheritance might be an option, but in some cases (like the search example) this would lead to tables which are created, but do not contain any data and would even generate GraphQL queries/mutations. Maybe the generation could be avoided by placing the "abstract" tables into a schema that is not accessible to PostGraphQl |
The tables you inherit from you’d want to generate queries, but you wouldn’t want mutations. So just If we were to do this we would need to find a way to represent a union or an interface in Postgres. I’m not sure how that would be done. The way I generally design my schema is I have a single table which has a |
Maybe something like: create type postgraphql_union__search_result as (
"user" "user",
post post,
comment comment,
book book,
topic topic,
...
); -- Exactly one of these should be not-null at a time
create function search_results() returns setof postgraphql_union__search_result ... ? |
I think with that approach the make clear which types the union combines. |
I think @benjie’s proposal would be a fine thing to explore if anyone is interested in building out the implementation. That was what I imagined unions would probably have to look like. |
@EyMaddis The reason I didn't do that is because I wanted you to be able to extend the union later with the minimum of effort. |
But how can postgraphql know which type the union combines? |
@EyMaddis we can introspect the attributes on the type 😊 Every attribute represents a different type in the union. PostGraphQL would then only resolve the first non-null attribute it finds expecting all of the other attributes to be null. |
@calebmer ah, alright, got it. I did not pay close attention to the types of the attributes. I think I like that, but it would be kind of awkward when writing the functions, wouldn't it? something similar could be applied to interfaces, but postgraphql could let tables "inherit" an interface if the properties defined in the type matches the columns. |
I can currently somehow achieve what I want through inheritance into another schema. I created a Edit: by the way, multi-inheritance is possible |
Could you put together a quick demo so we can see what this looks like in practice? This is an interesting idea using table inheritance with an empty table. |
@calebmer what do you require exactly? Some SQL commands to set the tables as described above? CREATE SCHEMA interfaces; -- no access for PostGraphQL
CREATE SCHEMA postgraphql; -- access granted to PostGraphQL
CREATE TABLE interfaces.timestamped (
-- no id, because it would increase the serial with every document created by a child
"created_date" date DEFAULT now()
);
CREATE TABLE interfaces.other_interface (
"something_inherited" text
);
CREATE TABLE postgraphql.something(
"id" serial,
"something_specific" text,
PRIMARY KEY ("id")
) INHERITS (interfaces.timestamped, interfaces.other_interface); |
Do we have any updates on this, or if anyone's working on it? We're thinking about picking this up, interested in what people have tried or run into. |
I would love to see this feature, so I'm willing to try to implement this in a plugin. The first thing I want to do is adding a hard coded union type. I guess I have to use newWithHooks(), but looking at other plugins does not help. |
...or is this not even feasible in a plugin because there is no GraphQLUnionType implemented here ? https://github.com/graphile/graphile-build/blob/master/packages/graphile-build/src/SchemaBuilder.js#L159 Yes i'm kind of lost :) |
I have plans to add this to core, but they're not far along. My plan is: For every table However the complexity comes in the look-ahead logic. Currently it tries to resolve concretely which type will be returned ahead of time so it knows which SQL fields to request, this is going to need some thought. I've not got this far yet. I'm sure there's many other complexities I've not thought about yet as well. Since I'm not really sure what shape this solution should take yet, I don't think I can advise you how best to implement it. As for e.g. in graphql.js you'd do const MyInterfaceType = new GraphQLInterfaceType({
name: "MyInterface",
fields: () => ({id: {type: GraphQLString } }),
description: "My interface type",
}; In graphile you'd do:
The hooks themselves just take the object spec: {
name: "MyInterface",
fields: () => ({id: {type: GraphQLString } }),
description: "My interface type",
} and return a new version of the object spec: {
...oldObjectSpec,
description: "My much better description",
} |
That means you can't use A PR adding GraphQLUnionType support would be welcome; but it'd have to come with a lot of tests! |
Thank you for the detailed response @benjie. To be honest, this seems like a task that needs a deep knowledge of the API, and you seem to have thought this through a lot, so I guess it's hopeless for me to implement this the right way. Maybe I'll try to create simpler plugins first. So... let us know when there is progress with this, if there is :) It's a feature every graphql->db lacks from what I've seen, and whatever lib i try I'm stuck with this problem in my use case. |
Did not see your last comment. That could be a reasonable PR, but I'm not even sure. Maybe I'll give it a shot after having read the code a little more deeply... maybe not... I'll let you know how I feel about this. |
Yeah; it's definitely a non-trivial problem. I'll update this thread when progress is made, but this is not currently high on my TODO list. |
This simplifies frontend's Graphqelm code. In theory, we could also use a GraphQL interface for the two fields of FolderCount. But Postgraphile doesn't support GraphQL interfaces (yet): graphile/crystal#387
Hi @mathroc that seems like a nice idea. With While the functions bring great freedom into how a table implements an interface, the big downside is that there are now two ways to access a
In PostGraphile, these probably would be a collision of two fields with the same name. This might require you to |
if I recall correctly, the issue with |
We can circumvent that either using domains or smart comments. I believe We should also keep in mind user-defined interfaces, or interfaces where the columns may not match perfectly but are "fudged" via smart comments or plugins. |
Just for reference, here's how JoinMonster does (did - the library is basically dead) support unions & interfaces: https://join-monster.readthedocs.io/en/latest/unions/ |
Interesting, that means union types cannot share the names of columns with different types. E.g. if you had one table with |
From discord:
I thought of that idea briefly but dismissed it because I didn't see a good way to create multiple types with overlapping columns declaratively. Maybe declare them as composite types elsewhere and have a |
Think of the blocks in a Medium-style post:
Perhaps: type Block {
id: Int!
text: String
author: User
lastEdited: Datetime
}
type Image implements Block {
id: Int!
text: String
url: String
altText: String
}
type Title implements Block ... From: create table blocks (
id serial primary key,
type text default 'Paragraph',
text text,
author_id int references users,
last_edited timestamptz not null default now(),
url text,
alt_text text,
provider text,
identifier text
); or maybe a central block table with 1-to-1 relationships for the extra properties. create table block_images (
block_id int primary key references block,
url text,
alt_text text
}; Maybe the relationship would be the other way so you can use an XOR check constraint to ensure there's exactly one related type - this may mean there's no nead for I've not thought sufficiently deeply about this yet. |
If you have a concrete use case you should share it. We shouldn't build things for the sake of having them, we should build them because they're solving real problems people have. We should also not be too prescriptive about people's database models - if we can make a particular solution work with multiple different layouts of database then that's better. Using patterns that are in the wild already is better because then it can benefit more people out of the box (and is not controversial). |
We implement a content management system for movies., series and such. A movie, episode, series etc pretty much share all the fields except for some columns. Also relevant for a global search function that can query multiple types, as suggested at the start of this topic. |
Hello! I've already started hacking, I guess I should have a PR to look at (though probably nowhere near readiness for merge) this week. As @benjie said, Postgraphile will need to be able to deal with any database layouts that people might have used. My current plan is to support the following patterns:
Thanks for the examples you've come up with, I'll try to implement them in the test cases for demonstration. |
Having thought more about the queries for shared fields, I've come to the conclusion that it would be unnecessarily complicated trying to merge everything into a single object on the Postgres level already. There's no reason to do that, we're still using SELECT json_build_object(
'foo_x', foo.x,
'foo_bar', (SELECT json_build_object(
'shared', bar.shared,
'__concrete', (CASE bar_resolveType(bar)
WHEN 'user'::regclass::oid THEN (
SELECT json_build_object('__typename', 'User'::text, 'username', users.username)
FROM users WHERE users.id = bar.id)
WHEN 'posts'::regclass::oid THEN (
SELECT json_build_object('__typename', 'Blog'::text, 'title', posts.title)
FROM posts WHERE posts.id = bar.id)
ELSE pg_raise('no concrete type found')
END CASE)
)
FROM bar
WHERE bar.id = foo.bar_id)
)
FROM foo Now I only need to adopt |
Ah, found a fun problem: assuming we have concrete object types query {
allBars {
__typename
shared { a }
... on X {
shared { b d }
}
... on Y {
shared { c d }
}
}
} It could lead to a result such as { data: [
{ __typename: "X", shared: { a: 1, b: 2, d: 4 } }, // no c
{ __typename: "Y", shared: { a: 1, c: 3, d: 4 } }, // no b
{ __typename: "Z", shared: { a: 1 } }, // no b or c
] } The fields
I tend to favour the first approach (overfetching) because it's the simplest and requires the fewest code changes, then later implement the second approach (condition propagation) if anyone deems it necessary. |
Every entity in my system inherits from a create table if not exists internal.base (
entity_id uuid default uuid_generate_v1mc(),
valid_from timestamp with time zone not null default now(),
valid_until timestamp with time zone not null default 'infinity',
-- this is not inherited, and has to be copied
primary key (entity_id, valid_until)
unique (entity_id, valid_from)
); This is mostly for convenience - I would eventually like to move away from inheritance at the db level. However, dealing with the universally implemented interface at the postgraphile level would be a win for me even without table inheritance. I think an ideal extension that satisfies my use case would just take an interface definition, along with a matcher function by which to apply that definition. Essentially a generalized version of the approach used in determining Imagined usage would be something like: appendPlugins: [implementInterface({
interface: interfaceDefinition,
when: (type, table) =>
hasMyInterfaceFields(type) &&
hasMyInterfaceConstraints(table),
})] |
@micimize Ah, cool, thanks for the ideas! I guess this actually should be quite possible already by hooking onto |
I got my first (specific) union type with complete lookahead working, see #1210 (comment)! So it definitely seems possible, and I hope I can soon make the PR to cut the rough edges in the graphile core that would make this even easier. Then the step to a generic support for generating these types for arbitrary annotated tables won't be large any more… |
Removes the new inflectors added by 4.3.0 (which was never upgraded to `@latest`, so this is not a breaking change) and instead puts them all through a single function called `builtin`. Also upgrades `makeAddInflectorsPlugin` so that it accepts a function, allowing replacement inflectors to reference the previous inflectors.
Note that table inheritance is listed in the PostgreSQL wiki as a "Don't do this". |
Even without table inheritance, the possibility to return GraphQL union types in a resolver would be really nice to have! |
I just spent a bit of time trying to figure this out, but I am sort of stuck on trying to figure out the graphile code base. I would solve it the following, and would like to know if you think it's doable that way: We want a schema like this interface foobar
{
name: String
lowerCase: String
}
type foo implements foobar
{
name: String!
id: Int!
}
type bar implements foobar
{
name: String!
id: Int!
} Pure Interface TypesTo implement inheritances us the What we want is a schema that looks like this: interface foobar { name: String }
type foo implements foobar
{
name: String!
id: Int!
}
type bar implements foobar
{
name: String!
id: Int!
} If we just want the interface type, i.e. no query , then using a type like this should work: Here is an example of using a types, i.e. one that cannot be queried in create type p.foobar as(name text);
comment on type p.foobar is '@interface';
create table p.foo (id serial primary key, name text not null);
comment on table p.foo is '@implements foobar';
create table p.bar (id serial primary key, name text not null);
comment on table p.bar is '@implements foobar'; Another version would be to have a table that can represent two different types, and I want to represent them like this in the schema; create table p.foobar (id serial primary key, name text not null, is_foo boolean);
comment on table p.foobar is '@interface';
comment on column p.foobar.is_foo is '@omit';
create view p.foo as select id, name from p.foobar where is_foo;
comment on view p.foo is '@implements foobar';
create view p.bar as select id, name from p.foobar where not is_foo;
comment on view p.bar is '@implements foobar'; And as third option you could put two different tables in a common view, like so create table p.foo (id serial primary key, name text not null);
create table p.bar (id serial primary key, name text not null);
--tableoid is required to determine the type
create view p.foobar as
select id, name, tableoid from p.foo
union all
select id, name, tableoid from p.bar;
comment on view p.foobar is '@interface';
comment on table p.foo is '@implements foobar';
comment on table p.bar is '@implements foobar'; Any thoughts on the design idea? |
@klemens-morgenstern it’s nice to see alternatives, I like the third option. The second one does not allow implementing multiple interfaces, so I think that would be a problem. The first one could work but I like the third one better because it allows transforming data from the source table to match the interface properties. On your example, for Postgraphile to be able to generate the correct GraphQL interface, you would remove the I had a different suggestion that would like this in your example: create table p.foo (id serial primary key, name text not null);
create table p.bar (id serial primary key, name text not null);
create type p.foobar as(name text);
comment on type p.foobar is '@interface';
create function foobar_from(foo) returns p.foobar as $$
select row($1.name)::p.foobar
$$ language sql;
create function foobar_from(bar) returns p.foobar as $$
select row($1.name)::p.foobar
$$ language sql; from a maintenance point of view I like having multiple function instead of a global view but I don’t know if there’s performance implication from either implementation |
I would like to have both 2 & 3. If you break it down to something that is query-able, i.e. a view, matieralized view, table, and has a tableoid, you would have the most flexibility. You could also use table inheritance, should you so desire. In my example the I also tried to implement a prototype myself, but already failed on changing a |
I suggest you familiarise yourself with GraphQL.js before diving into the Graphile Engine codebase. We don’t have a way of changing the constructor of a type (GraphQLObjectType, GraphQLInterfaceType, etc), the engine is not designed to do that. |
Any updates on this PR? |
Polymorphism is now supported in PostGraphile V5 (and has been for a while, and is receiving production usage by some users); read more here: https://dev.to/graphile/intro-to-postgraphile-v5-part-5-polymorphism-40b4 |
Is there a way to model interfaces using PostgraphQl?
I guess that this would require to map any metadata to a table which could be a problem.
See potential solution: #387 (comment)
The text was updated successfully, but these errors were encountered: