10

I am using Postgres 9.5, and I have the following tables:

Users

  • id UUID
  • name TEXT

Images

  • id UUID
  • key TEXT
  • width INTEGER
  • height INTEGER

Posts

  • id UUID
  • title TEXT
  • author_id UUID
  • content JSONB

The posts' content is like:

[
  { "type": "text", "text": "learning pg" },
  { "type": "image", "image_id": "8f4422b4-3936-49f5-ab02-50aea5e6755f" },
  { "type": "image", "image_id": "57efc97c-b9b4-4cd5-b1e1-3539f5853835" },
  { "type": "text", "text": "pg is awesome" }
]

Now I want to join the image type of content, and populate them with image_id, like:

{
  "id": "cb1267ca-b1ac-4daa-8c7e-72d4c000e9fa",
  "title": "Learning join jsonb in Postgres",
  "author_id": "deba01b7-ec58-4cc2-b3ae-7dc42e582767",
  "content": [
    { "type": "text", "text": "learning pg" },
    {
       "type": "image",
       "image": {
         "id": "8f4422b4-3936-49f5-ab02-50aea5e6755f",
         "key": "/upload/test1.jpg",
         "width": 800,
         "height": 600
       }
    },
    {
       "type": "image",
       "image": {
         "id": "57efc97c-b9b4-4cd5-b1e1-3539f5853835",
         "key": "/upload/test2.jpg",
         "width": 1280,
         "height": 720
       }
    },
    { "type": "text", "text": "pg is awesome" }
  ]
}

Here is my test sql file:

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

DROP TABLE IF EXISTS Users;
DROP TABLE IF EXISTS Images;
DROP TABLE IF EXISTS Posts;

CREATE TABLE Users (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  name text NOT NULL
);

CREATE TABLE Images (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  key TEXT,
  width INTEGER,
  height INTEGER,
  creator_id UUID
);

CREATE TABLE Posts (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  title TEXT,
  author_id UUID,
  content JSONB
);


DO $$
DECLARE user_id UUID;
DECLARE image1_id UUID;
DECLARE image2_id UUID;

BEGIN

INSERT INTO Users (name) VALUES ('test user') RETURNING id INTO user_id;
INSERT INTO Images (key, width, height, creator_id) VALUES ('upload/test1.jpg', 800, 600, user_id) RETURNING id INTO image1_id;
INSERT INTO Images (key, width, height, creator_id) VALUES ('upload/test2.jpg', 600, 400, user_id) RETURNING id INTO image2_id;
INSERT INTO Posts (title, author_id, content) VALUES (
  'test post',
  user_id,
  ('[ { "type": "text", "text": "learning pg" }, { "type": "image", "image_id": "' || image1_id || '" }, { "type": "image", "image_id": "' || image2_id || '" }, { "type": "text", "text": "pg is awesome" } ]') :: JSONB
);

END $$;

Is there any way to implement this requirement?

user2331095
  • 6,577
  • 8
  • 36
  • 56
  • 1
    It would be more useful if you posted actual table definitions (`CREATE TABLE` statements) which we can use to reproduce the case instead of the pseudo code. And *always* your version of Postgres. – Erwin Brandstetter Nov 04 '16 at 02:10
  • 1
    @ErwinBrandstetter Thanks for you suggestion, I've add the sql statements to the question. – user2331095 Nov 04 '16 at 03:07

1 Answers1

10
SELECT jsonb_pretty(to_jsonb(p)) AS post_row_as_json
FROM  (
   SELECT id, title, author_id, c.content
   FROM   posts p
   LEFT   JOIN LATERAL (
      SELECT jsonb_agg(
               CASE WHEN c.elem->>'type' = 'image' AND i.id IS NOT NULL
                    THEN elem - 'image_id' || jsonb_build_object('image', i)
                    ELSE c.elem END) AS content
      FROM   jsonb_array_elements(p.content) AS c(elem)
      LEFT   JOIN images i ON c.elem->>'type' = 'image'
                          AND i.id = (elem->>'image_id')::uuid
      ) c ON true
   ) p;

How?

  1. Unnest the jsonb array, producing 1 row per array element:

    jsonb_array_elements(p.content) AS c(elem)
    
  2. For each element LEFT JOIN to images on the conditions that

    • ... the key 'type' has the value 'image': c.elem->>'type' = 'image'
    • ... the UUID in image_id matches: i.id = (elem->>'image_id')::uuid
      An invalid UUID in content would raise an exception.
  3. For image types, where a matching image was found

    c.elem->>'type' = 'image' AND i.id IS NOT NULL
    

    remove the key 'image_id' and add the related image row as jsonb value:

    elem - 'image_id' || jsonb_build_object('image', i)
    

    Else keep the original element.

  4. Re-aggregate the modified elements to a new content column with jsonb_agg().
    Would work with a plain ARRAY constructor as well.

  5. Unconditionally LEFT JOIN LATERAL the result to posts and select all columns, only replace p.content with the generated replacement c.content

  6. In the outer SELECT, convert the whole row to jsonb with a simple to_jsonb().
    jsonb_pretty() is only for human-readable representation and totally optional.

All jsonb functions are documented in the manual.

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • 1
    Big up for calling out the usage of `jsonb_array_elements` - it helped me find a solution to our only-slightly-related problem – shawmanz32na May 25 '17 at 20:04