graph-node icon indicating copy to clipboard operation
graph-node copied to clipboard

feat(graph,graphql): filter by child entity (interfaces)

Open kamilkisiela opened this issue 3 years ago • 2 comments

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

kamilkisiela avatar Jun 17 '22 16:06 kamilkisiela

@azf20 @lutter please let me know what other use cases I should cover in the tests.

kamilkisiela avatar Jul 15 '22 09:07 kamilkisiela

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.

kamilkisiela avatar Aug 08 '22 16:08 kamilkisiela

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 avatar Aug 12 '22 23:08 lutter

@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?

kamilkisiela avatar Aug 24 '22 08:08 kamilkisiela

@lutter the table says that Song type (not an interface) can have media: [Media!]! where Media is 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!

lutter avatar Sep 07 '22 23:09 lutter