question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Filter On Single Relations

See original GitHub issue

I have

CREATE TABLE publisher (
    publisher_id serial PRIMARY KEY,
    name text
);

CREATE TABLE author (
    author_id serial PRIMARY KEY,
    name text
);

CREATE TABLE book (
    book_id serial PRIMARY KEY,
    title text
    publisher integer REFERENCES publisher(publisher_id),
    author integer REFERENCES author(author_id)
);

I display all the books in a table from the query:

query allBooks {
    allBooks {
        nodes {
            book_id
            title
            publisherByPublisher {
                name
            }
            authorByAuthor {
                name
            }
        }
    }
}

I want to be able to apply arbitrary filters such as all the books with publisher.name ilike ‘%foo%’ or author.name ilike ‘%bar%’ or both. Also, I would like to be able to construct a single query that accepts filters but will return all results if “unfiltered.”

This seems somewhat related to #26 but I decided to open a new issue because of how graphile has changed since that one was opened. Looking into how to implement this myself but any assistance you can provide would be greatly appreciated.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:20 (10 by maintainers)

github_iconTop GitHub Comments

5reactions
mattbretlcommented, Sep 12, 2018

I just published a forward-relations branch with this functionality included. I still need to add proper tests before merging.

For anyone curious as to how this actually works in PostGraphile, I’ll walk you through it…

Let’s start with a simple schema:

create table author (
    author_id serial PRIMARY KEY,
    name text
);

create table book (
    book_id serial PRIMARY KEY,
    title text,
    author integer REFERENCES author(author_id)
);

insert into author (author_id, name) values
  (1, 'Alice'),
  (2, 'Bob');

insert into book (book_id, title, author) values
  (1, 'The Alice Book', 1),
  (2, 'The Bob Book', 2);

and a simple GraphQL query with no filter argument:

query allBooks {
  allBooks {
    nodes {
      title
      authorByAuthor {
        name
      }
    }
  }
}

which PostGraphile transforms into the following SQL:

with __local_0__ as (
  select
    to_json((__local_1__."title")) as "title",
    to_json((
      select json_build_object('name'::text, (__local_2__."name")) as object
      from "public"."author" as __local_2__
      where (__local_1__."author" = __local_2__."author_id") and (TRUE) and (TRUE)
    )) as "@authorByAuthor"
  from "public"."book" as __local_1__
  where (TRUE) and (TRUE) -- ◀️◀️◀️ The filter plugin manipulates this where clause
  order by __local_1__."book_id" ASC
),
__local_3__ as (select json_agg(to_json(__local_0__)) as data from __local_0__)
select coalesce((select __local_3__.data from __local_3__), '[]'::json) as "data"

Now let’s run a GraphQL query with a simple filter:

query allBooksFilteredOnTitle {
  allBooks(filter: {
    title: { equalTo: "The Alice Book" }
  }) {
    nodes {
      title
      authorByAuthor {
        name
      }
    }
  }
}

which PostGraphile transforms into the following SQL:

with __local_0__ as (
  select
    to_json((__local_1__."title")) as "title",
    to_json((
      select json_build_object('name'::text, (__local_2__."name")) as object
      from "public"."author" as __local_2__
      where (__local_1__."author" = __local_2__."author_id") and (TRUE) and (TRUE)
    )) as "@authorByAuthor"
  from "public"."book" as __local_1__
  where (((__local_1__."title" = $1))) and (TRUE) and (TRUE)  -- ◀️◀️◀️
  order by __local_1__."book_id" ASC
),
__local_3__ as (select json_agg(to_json(__local_0__)) as data from __local_0__)
select coalesce((select __local_3__.data from __local_3__), '[]'::json) as "data"

Now let’s assume we want to filter on the name field in the author table using the following GraphQL query:

query allBooksFilteredOnAuthorName {
  allBooks(filter: {
    authorByAuthor: { name: { equalTo: "Alice" } }
  }) {
    nodes {
      title
      authorByAuthor {
        name
      }
    }
  }
}

If PostGraphile had generated the original SQL using joins, the __local_2__ alias would be accessible to the where clause where the filter plugin operates, and we could simply add another condition such as __local_2__."name" = $1. But since __local_2__ is buried in a subselect, we need to rebuild that link within the where clause.

(NOTE: Using a join here would almost certainly be more performant, but PostGraphile does not currently support joins; see https://github.com/graphile/graphile-engine/issues/2. If join support is added at some point, we should revisit this implementation.)

The SQL query we want to generate (credit to @benjie for the where exists(select 1 from ... ) tip) looks like this:

with __local_0__ as (
  select
    to_json((__local_1__."title")) as "title",
    to_json((
      select json_build_object('name'::text, (__local_2__."name")) as object
      from "public"."author" as __local_2__
      where (__local_1__."author" = __local_2__."author_id") and (TRUE) and (TRUE)
    )) as "@authorByAuthor"
  from "public"."book" as __local_1__
  where
    exists(                                                  -- ◀️◀️◀️
      select 1                                               -- ◀️◀️◀️
      from "public"."author" as __local_3__                  -- ◀️◀️◀️
      where (__local_1__."author" = __local_3__."author_id") -- ◀️◀️◀️
        and (((__local_3__."name" = $1)))                    -- ◀️◀️◀️
    )                                                        -- ◀️◀️◀️
    and (TRUE) and (TRUE)
  order by __local_1__."book_id" ASC
),
__local_4__ as (select json_agg(to_json(__local_0__)) as data from __local_0__)
select coalesce((select __local_4__.data from __local_4__), '[]'::json) as "data"

We just duplicated the logic from the subselect (with __local_2__ becoming __local_3__) and then tacked on __local_3__."name" = $1.

This approach can even be used to recursively resolve relations in the GraphQL query. To prove this, let’s start with a schema that includes an additional relation:

create table account (
    account_id serial PRIMARY KEY,
    email text
);

create table author (
    author_id serial PRIMARY KEY,
    name text,
    account integer REFERENCES account(account_id)
);

create table book (
    book_id serial PRIMARY KEY,
    title text,
    author integer REFERENCES author(author_id)
);

insert into account (account_id, email) values
  (1, 'alice@example.com'),
  (2, 'bob@example.com');

insert into author (author_id, name, account) values
  (1, 'Alice', 1),
  (2, 'Bob', 2);

insert into book (book_id, title, author) values
  (1, 'The Alice Book', 1),
  (2, 'The Bob Book', 2);

and then consider the following GraphQL query:

query allBooksFilteredOnAuthorAccountEmail {
  allBooks(filter: {
    authorByAuthor: {
      accountByAccount: {
        email: {
          equalTo:"alice@example.com"
        }
      }
    }
  }) {
    nodes {
      title
      authorByAuthor {
        name
      }
    }
  }
}

The SQL query we want to generate looks like this:

with __local_0__ as (
  select
    to_json((__local_1__."title")) as "title",
    to_json((
      select json_build_object('name'::text, (__local_2__."name")) as object
      from "public"."author" as __local_2__
      where (__local_1__."author" = __local_2__."author_id") and (TRUE) and (TRUE)
    )) as "@authorByAuthor"
  from "public"."book" as __local_1__
  where
    exists(
      select 1
      from "public"."author" as __local_3__
      where (__local_1__."author" = __local_3__."author_id")
        and exists(                                                -- ◀️◀️◀️
          select 1                                                 -- ◀️◀️◀️
          from "public"."account" as __local_4__                   -- ◀️◀️◀️
          where (__local_3__."account" = __local_4__."account_id") -- ◀️◀️◀️
            and (((__local_4__."email" = $1)))                     -- ◀️◀️◀️
        )                                                          -- ◀️◀️◀️
    )
    and (TRUE) and (TRUE)
  order by __local_1__."book_id" ASC
),
__local_5__ as (select json_agg(to_json(__local_0__)) as data from __local_0__)
select coalesce((select __local_5__.data from __local_5__), '[]'::json) as "data"

This is the approach I took on the forward-relations branch. Don’t expect it to be performant, but I believe it’s the best we can do until PostGraphile supports joins.

1reaction
barbalexcommented, Mar 28, 2019

@benjie Thanks a lot for this great help!!!

Read more comments on GitHub >

github_iconTop Results From Across the Web

Filter a relation using projection and restriction - Retrieve Data ...
Filter a relation using projection and restriction ... These are unary operations, which means that they are defined on a single relation.
Read more >
Filtering in relational data models
The {dm} package offers functions to work with relational data models in R. This document introduces you to filtering functions, and shows how ......
Read more >
Filtering on a one-to-many relationship in MySQL
I am assuming an active member is one who has a membership whose date range includes the current date. Then to select active...
Read more >
Filter Which Relations Appear? : r/Notion - Reddit
I have a GTD setup in Notion with two related databases: one for next actions (tasks) and another for projects. Each task gets...
Read more >
Model relationships in Power BI Desktop - Microsoft Learn
A model relationship propagates filters applied on the column of one model table to a ... As long as a filter is applied...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found