Using Postgres FK from jsonb with Hasura? - postgresql

We have foreign keys within a json blob in postgres. We join with these like so:
SELECT f.id, b.id FROM foo AS f
LEFT JOIN bar AS b ON f.data -> 'baz' ->> 'barId' = text(b.id)
I'm now trying out Hasura to do som graphql queries and I need these as object relationships. In the UI I can only try to manually add relationships with normal columns, not nested json data:
Is it at all possible to get a graphql relationship this way?

I got the answer in comments, thanks #iamnat. I'll just evolve here with my example for clarity since I still struggled a bit:
Super simple schema and data as such:
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE foo
(
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name text,
data jsonb NOT NULL
);
CREATE TABLE bar
(
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name text,
);
WITH bars AS (
INSERT INTO bar (name) VALUES ('bar') RETURNING id
)
INSERT INTO foo (name, data) VALUES ('foo', jsonb_build_object('barId', (SELECT id FROM bars)));
I then can create a function for the relationship:
CREATE FUNCTION foo_bar(foo_row foo)
RETURNS SETOF bar AS $$
SELECT *
FROM bar
WHERE text(id) = foo_row.data ->> 'barId'
$$ LANGUAGE sql STABLE;
This I can then use in Hasura as a computed field under "Data" -> foo -> Modify -> Computed fields -> "Add a new computed field". Just give it a name and reference the function in a dropdown:
I can then query:
query MyQuery {
foo {
name
foo_bar {
name
}
}
}
with expected result:
{
"data": {
"foo": [
{
"name": "foo",
"foo_bar": [
{
"name": "bar"
}
]
}
]
}
}

Related

Add a level an extra level of nesting to JSONB from columns using to_jsonb

In postgres you can convert a row to a JSONB using to_jsonb however I'd like to add an extra level of nesting. So given a table like
CREATE TABLE test (
foo text,
baz boolean
)
using to_jsonb produces
{
"foo":"bar",
"baz: false
}
but I'd like to transform the result to
{
"foo": {
"value" : "bar"
}
"baz": {
"value": false
}
}
for all top level fields without having to specify the field names.
#Bergi was super helpful. This is the answer (its json_build_object not jsonb_object)
SELECT
(SELECT jsonb_object_agg(key, json_build_object('value', value)) FROM jsonb_each(to_jsonb(t.*)) as x(key,value)) as attrs
FROM test AS t;

Querying a many:many relationship on PK of the related table (ie. filtering by related table column)

I have a many:many relationship between 2 tables: note and tag, and want to be able to search all notes by their tagId. Because of the many:many I have a junction table note_tag.
My goal is to expose a computed field on my Postgraphile-generated Graphql schema that I can query against, along with the other properties of the note table.
I'm playing around with postgraphile-plugin-connection-filter. This plugin makes it possible to filter by things like authorId (which would be 1:many), but I'm unable to figure out how to filter by a many:many. I have a computed column on my note table called tags, which is JSON. Is there a way to "look into" this json and pick out where id = 1?
Here is my computed column tags:
create or replace function note_tags(note note, tagid text)
returns jsonb as $$
select
json_strip_nulls(
json_agg(
json_build_object(
'title', tag.title,
'id', tag.id,
)
)
)::jsonb
from note
inner join note_tag on note_tag.tag_id = tagid and note_tag.note_id = note.id
left join note_tag nt on note.id = nt.note_id
left join tag on nt.tag_id = tag.id
where note.account_id = '1'
group by note.id, note.title;
$$ language sql stable;
as I understand the function above, I am returning jsonb, based on the tagid that was given (to the function): inner join note_tag on note_tag.tag_id = tagid. So why is the json not being filtered by id when the column gets computed?
I am trying to make a query like this:
query notesByTagId {
notes {
edges {
node {
title
id
tags(tagid: "1")
}
}
}
}
but right now when I execute this query, I get back stringified JSON in the tags field. However, all tags are included in the json, whether or not the note actually belongs to that tag or not.
For instance, this note with id = 1 should only have tags with id = 1 and id = 2. Right now it returns every tag in the database
{
"data": {
"notes": {
"edges": [
{
"node": {
"id": "1",
"tags": "[{\"id\":\"1\",\"title\":\"Psychology\"},{\"id\":\"2\",\"title\":\"Logic\"},{\"id\":\"3\",\"title\":\"Charisma\"}]",
...
The key factor with this computed column is that the JSON must include all tags that the note belongs to, even though we are searching for notes on a single tagid
here are my simplified tables...
note:
create table notes(
id text,
title text
)
tag:
create table tag(
id text,
title text
)
note_tag:
create table note_tag(
note_id text FK references note.id
tag_id text FK references tag.id
)
Update
I am changing up the approach a bit, and am toying with the following function:
create or replace function note_tags(n note)
returns setof tag as $$
select tag.*
from tag
inner join note_tag on (note_tag.tag_id = tag.id)
where note_tag.note_id = n.id;
$$ language sql stable;
I am able to retrieve all notes with the tags field populated, but now I need to be able to filter out the notes that don't belong to a particular tag, while still retaining all of the tags that belong to a given note.
So the question remains the same as above: how do we filter a table based on a related table's PK?
After a while of digging, I think I've come across a good approach. Based on this response, I have made a function that returns all notes by a given tagid.
Here it is:
create or replace function all_notes_with_tag_id(tagid text)
returns setof note as $$
select distinct note.*
from tag
inner join note_tag on (note_tag.tag_id = tag.id)
inner join note on (note_tag.note_id = note.id)
where tag.id = tagid;
$$ language sql stable;
The error in approach was to expect the computed column to do all of the work, whereas its only job should be to get all of the data. This function all_nuggets_with_bucket_id can now be called directly in graphql like so:
query MyQuery($tagid: String!) {
allNotesWithTagId(tagid: $tagid) {
edges {
node {
id
title
tags {
edges {
node {
id
title
}
}
}
}
}
}
}

Insert element of UUID type in nested array of Object through Postgresql query

Current JSON
{
"layout":"dynamicReport1",
"templateType":"DYNAMIC_REPORTS",
"containers":{
"fieldsContainer":[
{
"fieldName":"role_id",
"displayName":"Role",
"fieldType":"text",
"isHidden":true,
"index":0,
"queryForParam":"select name as \"role_id\" from um_role_master where id=#role_id#",
"queryIdForParam":476
},
{
"fieldName":"course_id",
"displayName":"Course",
"fieldType":"text",
"isHidden":true,
"index":1,
"queryForParam":"select course_name as course_id from tr_course_master where course_id=#course_id#",
"queryIdForParam":477
},
{
"fieldName":"location_id",
"displayName":"Location",
"fieldType":"text",
"isHidden":true,
"index":2,
"queryForParam":"select name as location_id from location_master where id = #location_id#",
"queryIdForParam":478
}
]
}
}
Hierarchy is like
containers -> fieldContainer -> object
Above is my json config and i want to add queryUUIDForParam: random UUID to each Object through query.
How i can insert ?
I tried to get updated config by this query but it throws error:
select config::jsonb || ('{"queryUUIDForParam":' || cast(uuid as text) || '}')::jsonb
error :
SQL Error [22P02]: ERROR: invalid input syntax for type json Detail:
Token "5574ff23" is invalid. Where: JSON data, line 1:
{"queryUUIDForParam":5574ff23...
My expected output is to add "queryUUIDForParam" element in Object Like.
I want an element with UUID value to be appended.This UUId value is generated using random function.
{
"layout":"dynamicReport1",
"templateType":"DYNAMIC_REPORTS",
"containers":{
"fieldsContainer":[
{
"fieldName":"role_id",
"displayName":"Role",
"fieldType":"text",
"isHidden":true,
"index":0,
"queryForParam":"select name as \"role_id\" from um_role_master where id=#role_id#",
"queryIdForParam":476,
"queryUUIDForParam":"1ea99f17-6965-4a0d-8d31-22b8777b9c62"
},
{
"fieldName":"course_id",
"displayName":"Course",
"fieldType":"text",
"isHidden":true,
"index":1,
"queryForParam":"select course_name as course_id from tr_course_master where course_id=#course_id#",
"queryIdForParam":477,
"queryUUIDForParam":"3ea99f17-6965-4a0d-8d31-22b8777b9c62"
},
{
"fieldName":"location_id",
"displayName":"Location",
"fieldType":"text",
"isHidden":true,
"index":2,
"queryForParam":"select name as location_id from location_master where id = #location_id#",
"queryIdForParam":478,
"queryUUIDForParam":"9ea99f17-6965-4a0d-8d31-22b8777b9c62"
}
]
}
}
Thanks in advance :)
Try This:
with cte as (select jsonb_array_elements(jsonb_extract_path(config, 'containers','fieldsContainer')::jsonb) "objects" from example),
final_array as (
select jsonb_build_array(d) "array_data" from (select array_agg(objects::jsonb || jsonb_build_object('queryUUIDForParam',(select uuid_generate_v4()))) "fieldsContainer" from cte )d)
select jsonb_set(
config::jsonb,
'{containers,fieldsContainer}', (f.array_data),false)
from example, final_array f;
in case you want different uuid for each object
with cte as (select uuid_generate_v4() "uuid_",jsonb_array_elements(jsonb_extract_path(config, 'containers','fieldsContainer')::jsonb) "objects" from example),
final_array as (
select jsonb_build_array(d) "array_data" from (select array_agg(objects::jsonb || jsonb_build_object('queryUUIDForParam',uuid_)) "fieldsContainer" from cte )d)
select jsonb_set(
config::jsonb,
'{containers,fieldsContainer}', (f.array_data),false)
from example, final_array f;
Note: I have used Inbuilt function of Postgres to generate the UUID. Please run following statement before using it
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
DEMO on DB-Fiddle

Postgresql json select from values in second layer of containment of arrays

I have a jsonb column 'data' that contains a tree like json, example:
{
"libraries":[
{
"books":[
{
"name":"mybook",
"type":"fiction"
},
{
"name":"yourbook",
"type":"comedy"
}
{
"name":"hisbook",
"type":"fiction"
}
]
}
]
}
I want to be able to do a index using query that selects a value from the indented "book" jsons according to the type.
so all book names that are fiction.
I was able to do this using jsonb_array_elements a join query, but as i understand this would not be optimized with using the GIN index.
my query is
select books->'name'
from data,
jsonb_array_elements(data->'libraries') libraries,
jsonb_array_elements(libraries->'books') books,
where books->>'type'='grading'
If the example data you are showing is the type of data that is common in your JSON, I would suggest that you may be setting things up wrong.
Why not make a library table and a book table and not use JSON at all, it seems JSON is not the right choice here.
CREATE TABLE library
(
id serial,
name text
);
CREATE TABLE book
(
isbn BIGINT,
name text,
book_type text
);
CREATE TABLE library_books
(
library_id integer,
isbn BIGINT
)
select book.* from library_books where library_id = 1;

Passing CAST(foo AS type) as a relationship condition in DBIx::Class

For historical reasons, we have a table at work that has integer values in a text field that correspond to the ID's in another table. Example:
CREATE TABLE things (
id INTEGER,
name VARCHAR,
thingy VARCHAR
);
CREATE TABLE other_things (
id INTEGER,
name VARCHAR,
);
So a "thing" has-one "other thing", but rather than being set up sensibly, the join field is a varchar, and called "thingy".
So in Postgres, I can do this to join the two tables:
SELECT t.id, t.name, ot.name FROM things t
JOIN other_things ot ON CAST(t.thingy AS int) = ot.id
How can I represent this relationship in DBIx::Class? Here's an example of one thing I've tried:
package MySchema::Thing;
__PACKAGE__->has_one(
'other_thing',
'MySchema::OtherThing',
{ 'foreign.id' => 'CAST(self.thingy AS int)' },
);
nwellnhof was close, but to get the literal SQL to SQL::Abstract, I had to do a coderef like so:
__PACKAGE__->has_one(
'other_thing',
'MySchema::OtherThing',
sub {
my $args = shift;
return {
qq{$args->{'foreign_alias'}.id} => { q{=} => \qq{CAST($args->{'self_alias'}.dept AS int)} },
};
},
);
Using Literal SQL should do the trick:
__PACKAGE__->has_one(
'other_thing',
'MySchema::OtherThing',
{ 'foreign.id' => { '=', \'CAST(self.thingy AS int)' } },
);
I'd change the datatype of the field.
If that's not possible you could add another field of type int and a trigger that casts the varchar to an int and stores it in the int field that you then use for the joins to improve performance.