graph-node
graph-node copied to clipboard
feat(graph,graphql): filter by child entity (interfaces)
Continuation of #3184, adds support for interfaces.
GraphQL Schema
type Musician @entity {
id: ID!
name: String!
mainBand: Band
bands: [Band!]!
writtenSongs: [Song]! @derivedFrom(field: "writtenBy")
}
type Band @entity {
id: ID!
name: String!
members: [Musician!]! @derivedFrom(field: "bands")
reviews: [BandReview] @derivedFrom(field: "band")
originalSongs: [Song!]!
}
type Song @entity {
id: ID!
title: String!
writtenBy: Musician!
publisher: Publisher!
band: Band @derivedFrom(field: "originalSongs")
reviews: [SongReview] @derivedFrom(field: "song")
}
type SongStat @entity {
id: ID!
song: Song @derivedFrom(field: "id")
played: Int!
}
type Publisher {
id: Bytes!
}
interface Review {
id: ID!
body: String!
author: User!
}
type SongReview implements Review @entity {
id: ID!
body: String!
song: Song
author: User!
}
type BandReview implements Review @entity {
id: ID!
body: String!
band: Band
author: User!
}
type User @entity {
id: ID!
name: String!
bandReviews: [BandReview] @derivedFrom(field: "author")
songReviews: [SongReview] @derivedFrom(field: "author")
reviews: [Review] @derivedFrom(field: "author")
}
GraphQL Query
query {
users(first: 5, orderBy: id, where: { reviews_: { body_starts_with: "Good" } }) {
name
reviews {
body
}
}
}
SQL statement
select
'User' as entity,
to_jsonb(c.*) as data
from
(
select
*
from
"sgd445"."user" c
where
c.block_range @> $1
and (
(
exists (
select
1
from
"sgd445"."song_review" as i
where
c."id" = i."author"
and i.block_range @> $2
and (i."body" like $3)
)
or exists (
select
1
from
"sgd445"."band_review" as i
where
c."id" = i."author"
and i.block_range @> $4
and (i."body" like $5)
)
)
)
order by "id", block_range
limit 100
) c
@azf20 @lutter please let me know what other use cases I should cover in the tests.
I don't know exactly how the interfaces are supposed to work, what is possible, and what is not with and without the @derivedFrom directive. I'm guessing here a bit. Waiting for a few actual use cases to match my implementation against some real and confirmed to be working examples/tests.
I just made a big table to get an overview of what combinations of parent and child types are possible and came up with this:
| parent intf | child intf | derived | list | parent | child |
|---|---|---|---|---|---|
| f | f | f | f | Musician | mainBand |
| f | f | f | t | Musician | bands |
| f | f | t | f | Song | band |
| f | f | t | t | Band | members |
| f | t | f | f | User | latestReview |
| f | t | f | t | Song | media |
| f | t | t | f | --- | --- |
| f | t | t | t | User | reviews |
| t | f | f | f | Review | author |
| t | f | f | t | --- | --- |
| t | f | t | f | Media | song |
| t | f | t | t | --- | --- |
| t | t | f | f | --- | --- |
| t | t | f | t | --- | --- |
| t | t | t | f | --- | --- |
| t | t | t | t | --- | --- |
They all are for a query along the lines of
{
parents(where: { child_: { someField_filter: "value" } }) { .. stuff .. }
}
The columns mean:
- parent intf: should the parent be an interface
- child intf: should the child be an interface
- derived: should the child be derived
- list: should the child be a list/array
- parent: the type of the parent to use
- child: the name of the child field to use
I tried to fill this in with examples that I see possible from the test schema and marked the ones where I think that's not possible in the schema with --- - since there's so many of them, I think we should try and make sure we cover all the cases that I marked as possible (they might all be covered already, haven't checked against the tests yet) and get them working. Once that's done, let's merge this PR and work on another one to cover more of the possible combinations here.
@lutter the table says that Song type (not an interface) can have media: [Media!]! where Media is an interface.
For this query:
query {
songs(
first: 100,
orderBy: id,
where: {
media_: {
title_starts_with: "Cheesy Tune"
}
}
) {
title
media {
title
}
}
}
This is the generated SQL query:
select 'Song' as entity, to_jsonb(c.*) as data from (select *
from "sgd52"."song" c
where c.block_range @> 1 and ((exists (select 1 from "sgd52"."photo" as i where i."id" = any(c."media") and i.block_range @> 1 and (i."title" like '%Cheesy Tune%')) or exists (select 1 from "sgd52"."video" as i where i."id" = any(c."media") and i.block_range @> 1 and (i."title" like '%Cheesy Tune%'))))
order by "id", block_range
limit 100) c
That query is valid and works.
The operator does not exist: bytea = text error comes from another SQL query that is not related to my code.
with matches as (select c.* from unnest($1::bytea[]) as q(id)
cross join lateral (select 'Photo' as entity, c.id, c.vid, p.id::text as g$parent_id, c.block_range
/* children_type_c */ from rows from (unnest($2), reduce_dim(array[array['0xf1', '0xf2']]::text[][])) as p(id, child_ids) cross join lateral (select * from "sgd54"."photo" c where c.block_range @> $3 and q.id = p.id and c.id = any(p.child_ids)) c
union all
select 'Video' as entity, c.id, c.vid, p.id::text as g$parent_id, c.block_range
/* children_type_c */ from rows from (unnest($4), reduce_dim(array[array['0xf1', '0xf2']]::text[][])) as p(id, child_ids) cross join lateral (select * from "sgd54"."video" c where c.block_range @> $5 and q.id = p.id and c.id = any(p.child_ids)) c
order by "id", block_range
limit 100) c)
select m.*, to_jsonb("c".*)|| jsonb_build_object('g$parent_id', m.g$parent_id) as data
from "sgd54"."photo" c, matches m
where c.vid = m.vid and m.entity = 'Photo'
union all
select m.*, to_jsonb("c".*)|| jsonb_build_object('g$parent_id', m.g$parent_id) as data
from "sgd54"."video" c, matches m
where c.vid = m.vid and m.entity = 'Video'
order by g$parent_id, "id"
Can you confirm it's not supported by graph-node or it's a bug?
@lutter the table says that
Songtype (not an interface) can havemedia: [Media!]!whereMediais an interface. ... Can you confirm it's not supported by graph-node or it's a bug?
Yes, that is indeed a pre-existing bug related to using Bytes for ids, caused by the media { title } part of the GraphQL query. I just added a commit to this PR that fixes that issue. You might want to move that commit before the commit that adds the test, I just didn't want to change too much in your PR.
I am glad that adding these tests actually uncovered that issue!