I have a flat table with dot-notationed columns, similar to the following:
title.en | title.fr | category.name.en | category.name.fr | category.acronym.en | category.acronym.fr |
---|---|---|---|---|---|
English Title | French Title | English Category Name | French Category Name | English Category Acronym | French Category Acronym |
the dot notations are there to inidicate a nested object, so each dot makes a nesting level of json. According to this, I want to be able to query this result from the table in json(b):
{
"data": {
"title": {
"en": "English Title",
"fr": "French Title"
},
"category": {
"name": {
"en": "English Category Name",
"fr": "French Category Name"
},
"acronym": {
"en": "English Category Acronym",
"fr": "French Category Acronym"
}
}
}
}
I know it's possible to manually do this using nested "jsonb_build_object" functions, but want to know if it's possible to shortcut it using dot notations in column names.
Let's consider that data
is the table name. The solution below requires several steps:
1. Get the column names from the database schema:
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_name = 'data'
Result:
table_name | column_name |
---|---|
data | title.en |
data | title.fr |
data | category.name.en |
data | category.name.fr |
data | category.acronym.en |
data | category.acronym.fr |
2. Convert the dot-notationed column names into the target jsonb format:
SELECT ('{"' || table_name || '": {"' || replace(column_name, '.', '": {"') || '": "' || column_name || '"' || repeat('}', regexp_count(column_name, '\.')+2)) :: jsonb
FROM information_schema.columns
WHERE table_name = 'data'
Result:
jsonb |
---|
{"data": {"title": {"en": "title.en"}}} |
{"data": {"title": {"fr": "title.fr"}}} |
{"data": {"category": {"name": {"en": "category.name.en"}}}} |
{"data": {"category": {"name": {"fr": "category.name.fr"}}}} |
{"data": {"category": {"acronym": {"en": "category.acronym.en"}}}} |
{"data": {"category": {"acronym": {"fr": "category.acronym.fr"}}}} |
3. Merge the jsonb column names all together to build the final jsonb template:
In this step, we need to create a specific aggregate
function jsonb_merge_agg
:
CREATE OR REPLACE FUNCTION jsonb_merge(x jsonb, y jsonb) RETURNS jsonb LANGUAGE sql AS $$
SELECT jsonb_object_agg
( COALESCE(x1->>'key', y1->>'key')
, CASE
WHEN x1->>'key' IS NULL THEN y1->'value'
WHEN y1->>'key' IS NULL THEN x1->'value'
WHEN jsonb_typeof(x1->'value') = 'object' AND jsonb_typeof(y1->'value') = 'object' THEN jsonb_merge(x1->'value', y1->'value')
ELSE jsonb_build_array(x1->'value', y1->'value')
END
)
FROM jsonb_path_query(x,'$[*].keyvalue()') AS x1
FULL OUTER JOIN jsonb_path_query(y,'$[*].keyvalue()') AS y1
ON y1->>'key' = x1->>'key' ;
$$ ;
CREATE OR REPLACE AGGREGATE jsonb_merge_agg(x jsonb)
(stype = jsonb, sfunc = jsonb_merge) ;
SELECT jsonb_merge_agg(('{"' || table_name || '": {"' || replace(column_name, '.', '": {"') || '": "' || column_name || '"' || repeat('}', regexp_count(column_name, '\.')+2)) :: jsonb) AS jsonb_template
FROM information_schema.columns
WHERE table_name = 'data'
Result:
jsonb_template |
---|
{"data": {"title": {"en": "title.en", "fr": "title.fr"}, "category": {"name": {"en": "category.name.en", "fr": "category.name.fr"}, "acronym": {"en": "category.acronym.en", "fr": "category.acronym.fr"}}}} |
4. Replace in the jsonb_template every column name by the corresponding value:
In this step, we need to create another specific aggregate function replace_agg
based on the standard replace
function:
CREATE OR REPLACE FUNCTION replace(x text, y text, old text, new text) RETURNS text LANGUAGE sql AS $$
SELECT replace(COALESCE(x, y), old, new) ; $$ ;
CREATE OR REPLACE AGGREGATE replace_agg(x text, old text, new text)
(stype = text, sfunc = replace) ;
5. Build the final query:
WITH list AS (
SELECT jsonb_merge_agg(('{"' || table_name || '": {"' || replace(column_name, '.', '": {"') || '": "' || column_name || '"' || repeat('}', regexp_count(column_name, '\.')+2)) :: jsonb) AS jsonb_template
FROM information_schema.columns
WHERE table_name = 'data'
)
SELECT replace_agg( l.jsonb_template :: text
, content->>'key'
, content->>'value'
) AS final_result
FROM list AS l
CROSS JOIN data AS d
CROSS JOIN LATERAL jsonb_path_query(to_jsonb(d), '$.keyvalue()') AS content
GROUP BY d
Final Result:
final_result |
---|
{"data": {"title": {"en": "English Title", "fr": "French Title"}, "category": {"name": {"en": "English Category Name", "fr": "French Category Name"}, "acronym": {"en": "English Category Acronym", "fr": "French Category Acronym"}}}} |
all the test results in dbfiddle
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.