updating postgres jsonb column - postgresql

I have below json string in my table column which is of type jsonb,
{
"abc": 1,
"def": 2
}
i want to remove the "abc" key from it and insert "mno" with some default value. i followed the below approcach for it.
UPDATE books SET books_desc = books_desc - 'abc';
UPDATE books SET books_desc = jsonb_set(books_desc, '{mno}', '5');
and it works.
Now i have another table with json as below,
{
"a": {
"abc": 1,
"def": 2
},
"b": {
"abc": 1,
"def": 2
}
}
Even in this json, i want to do the same thing. take out "abc" and introduce "mno" with some default value. Please help me to achieve this.
The keys "a" and "b" are dynamic and can change. But the values for "a" and "b" will always have same keys but values may change.
I need a generic logic.
Requirement 2:
abc:true should get converted to xyz:1.
abc:false should get converted to xyz:0.

demo:db<>fiddle
Because of a possible variety of your JSON keys it might be complicated to generate a common query. This is because you need to give the path within the json_set() function. But without actual values it would be hard.
A simple work-around is using the regexp_replace() function on the text representation of the JSON string to replace the relevant objects.
UPDATE my_table
SET my_data =
regexp_replace(my_data::text, '"abc"\s*:\s*\d+', '"mno":5', 'g')::jsonb

For added requirement 2:
I wrote the below query based on already given solution:
UPDATE books
SET book_info =
regexp_replace(book_info::text, '"abc"\s*:\s*true', '"xyz":1', 'g')::jsonb;
UPDATE books
SET book_info =
regexp_replace(book_info::text, '"abc"\s*:\s*false', '"xyz":0', 'g')::jsonb;

Related

Convert an object to array of size 1 in PostgreSQL jsonb and transform the json with nested arrays to rows

I have a two part question
We have a PostgreSQL table with a jsonb column. The values in jsonb are valid jsons, but they are such that for some rows a node would come in as an array whereas for others it will come as an object.
for example, the json we receive could either be like this ( node4 I just an object )
"node1": {
"node2": {
"node3": {
"node4": {
"attr1": "7d181b05-9c9b-4759-9368-aa7a38b0dc69",
"attr2": "S1",
"UserID": "WebServices",
"attr3": "S&P 500*",
"attr4": "EI",
"attr5": "0"
}
}
}
}
Or like this ( node4 is an array )
"node1": {
"node2": {
"node3": {
"node4": [
{
"attr1": "7d181b05-9c9b-4759-9368-aa7a38b0dc69",
"attr2": "S1",
"UserID": "WebServices",
"attr3": "S&P 500*",
"attr4": "EI",
"attr5": "0"
},
{
"attr1": "7d181b05-9c9b-4759-9368-aa7a38b0dc69",
"attr2": "S1",
"UserID": "WebServices",
"attr3": "S&P 500*",
"attr4": "EI",
"attr5": "0"
}
]
}
}
}
And I have to write a jsonpath query to extract, for example, attr1, for each PostgreSQL row containing this json. I would like to have just one jsonpath query that would always work irrespective of whether the node is object or array. So, I want to use a path like below, assuming, if it is an array, it will return the value for all indices in that array.
jsonb_path_query(payload, '$.node1.node2.node3.node4[*].attr1')#>> '{}' AS "ATTR1"
I would like to avoid checking whether the type in array or object and then run a separate query for each and do a union.
Is it possible?
A sub-question related to above - Since I needed the output as text without the quotes, and somewhere I saw to use #>> '{}' - so I tried that and it is working, but can someone explain, how that works?
The second part of the question is - the incoming json can have multiple sets of nested arrays and the json and the number of nodes is huge. So other part I would like to do is flatten the json into multiple rows. The examples I found were one has to identify each level and either use cross join or unnest. What I was hoping is there is a way to flatten a node that is an array, including all of the parent information, without knowing which, if any, if its parents are arrays or simple object. Is this possible as well?
Update
I tried to look at the documentation and tried to understand the #>> '{}' construct, and then I came to realise that '{}' is the right hand operand for the #>> operator which takes a path and in my case the path is the current attribute value hence {}. Looking at examples that had non-empty single attribute path helped me realise that.
Thank you
You can use a "recursive term" in the JSON path expression:
select t.some_column,
p.attr1
from the_table t
cross join jsonb_path_query(payload, 'strict $.**.attr1') as p(attr1)
Note that the strict modifier is required, otherwise, each value will be returned multiple times.
This will return one row for each key attr1 found in any level of the JSON structure.
For the given sample data, this would return:
attr1
--------------------------------------
"7d181b05-9c9b-4759-9368-aa7a38b0dc69"
"7d181b05-9c9b-4759-9368-aa7a38b0dc69"
"7d181b05-9c9b-4759-9368-aa7a38b0dc69"
"I would like to avoid checking whether the type in array or object and then run a separate query for each and do a union. Is it possible?"
Yes it is and your jsonpath query works fine in both cases either when node4 is a jsonb object or when it is a jsonb array because the jsonpath wildcard array accessor [*] also works with a jsonb object in the lax mode which is the default behavior (but not in the strict mode see the manual). See the test results in dbfiddle.
"I saw to use #>> '{}' - so I tried that and it is working, but can someone explain, how that works?"
The output of the jsonb_path_query function is of type jsonb, and when the result is a jsonb string, then it is automatically displayed with double quotes " in the query results. The operator #>> converts the output into the text type which is displayed without " in the query results and the associated text array '{}' just point at the root of the passed jsonb data.
" the incoming json can have multiple sets of nested arrays and the json and the number of nodes is huge. So other part I would like to do is flatten the json into multiple rows"
you can refer to the answer of a_horse_with_no_name using the recursive wildcard member accessor .**

Swift 5 Firebase queryOrderedBy String value

is it possible to query after the child value if its a string? In alphabetically order?
Doesnt matter whether it is descending or ascending.
e.g. under the key, each reference has the assigned name of the follower, and I want to order all the followers alphabetically.
Only manage to query it ordered by an integer unfortunately. (INCLUDING PAGINATION)
If this doesnt work, is there a way to query ordered by key? e.g. I have key 1 "-edasMmaed" and key 2 "-deLkdnw" etc and that if do paginate I start after the last value?
I haven't found anything useful unfortunately.
Kind regards
Edit: This is for the first part of the question
EDIT 2:
var query = Ref().databaseFollowingForUser(uid: userId, type: type).queryOrderedByKey()
if let latestUserFollowers = uid, latestUserFollowers != 0 {
query = query.queryEnding(atValue: latestUserFollowers).queryLimited(toLast: limit)
} else {
query = query.queryLimited(toLast: limit)
}
query.observeSingleEvent(of: .value, with: { (snapshot) in
With this code I receive the first 10 results (limit is defined as 10)
everbody from ID: 276 through ID: 18. (starting at holgerhagerson and ending at manni85)
Now I want to paginate and load more which I am not able yet.
The passed uid is the uid of the latest fetched user which is "18", manni85
BIG EDIT: I managed to order it by keys. Reading your answers regarding keys are always saved as strings, I realized my mistake and are now able to do it properly.
Big thank you!
Keys in the Firebase Realtime Database are stored (and sorted) as strings. Even if they look like numbers to you, Firebase will store (and sort) them as strings.
This means that the 2, 3, 4, etc in your screenshot are actually "2", "3", "4" etc. This affects how they are ordered, as strings are ordered lexicographically and in that order these keys will show up as "3", "30", "4", "44", "5", etc.
If you want to use keys that you can sort numerically, take these steps:
Prefix keys with a short non-numeric prefix, to prevent Firebase interpreting them as an array.
Use a fixed length for the numbers in all keys, prefixing the value with zeroes or spaces as needed.
When combined, your keys would show up as:
"key_003": ...,
"key_004": ...,
...
"key_008": ...,
"key_016": ...,
"key_018": ...,
"key_030": ...,
"key_044": ...
Which you can now reliably sort when you query /FOLLOW/Follower/2 by calling queryOrderedByKey().

Is jsonb conversion to text deterministic?

This is the short version of an overly long question that sadly attracted no answers.
Is it possible, given two jsonb variables x and y, to have both
1. (x = y) yield true, and
2. (x::text = y::text) yield false
I ask this question because it appears there is no promised order in which a jsonb object will be unpacked into a string. I'd just like to be sure this is the case.
Thanks in advance for feedback!
Edit:
The original question covers the "why" for this question, but the skinny is that I hope to group data in different rows based upon a hash of many columns represented as text, some of which are jsonb.
I don't care which way the object comes in or which way it gets unpacked, but I do care if two jsonb fields which are equivalent as jsonb are not equivalent as text strings.
As it seems I cannot count on text representations to be presented in the same way, I've normalized out the jsonb field to a separate table with the jsonb field set as a unique index.
And if I write more here... this question will approach the length of the one it derives from!
Formally the order is not deterministic because of the JSON object definition:
An object is an unordered set of name/value pairs.
Practically it appears that objects are sorted by length of keys and then alphabetically:
with example(col) as (
values
('{"cc": 1, "ab": 1, "a": 1, "aa": 1, "b": 2, "abc": 1}'::jsonb)
)
select col::text
from example
col
-------------------------------------------------------
{"a": 1, "b": 2, "aa": 1, "ab": 1, "cc": 1, "abc": 1}
(1 row)
Note that this behavior is undocumented and may change in future releases (though it may seem unlikely).

Extracting all keys from a JSON object unless a certain key has a value

My Postgres jsonb-foo isn't that great but I'd appreciate some help with a query I am trying to put together.
I have this rudimentary query to extract the name of all keys in the _doc's 'answers' key. The jsonb data looks something like this
_doc = {
"answers": {
"baz": true,
"qux": true
"other": "How do i find this"
}
}
and a query might look this this:
SELECT ss.foo, count(DISTINCT (ss.bar)) FROM (
SELECT (_doc::jsonb -> 'bar')::text as bar,
jsonb_object_keys(_doc::jsonb -> 'answers' -> 'foo') as foo
FROM public."table_name"
) ss
WHERE ss.foo IS NOT NULL
GROUP BY ss.foo;
So really the output here would be the number of times each key of answers appears.
("baz" = 1, "qux" = 1, "other" = 1)
Here is my problem, I want to get the number of times each key appears, apart from in the case of other. In that case I want to get the number of times its contents appears. So I want the result to be
("baz" = 1, "qux" = 1, "How do i find this" = 1)
If possible I would love some help structuring this query.
Thank you
demo:db<>fiddle for several json records
demo:db<>fiddle for one json records which has the same key twice (strictly not recommended!)
Using the json_each_text() function to get the key/value pairs. After that take the keys or the value of other, selecting through a CASE clause
SELECT
CASE WHEN elems.key = 'other' THEN elems.value
ELSE elems.key
END AS key,
COUNT(*)
FROM data,
json_each_text(jsondata -> 'answers') AS elems
GROUP BY 1

postgres - syntax for updating a jsonb array

I'm struggling to find the right syntax for updating an array in a jsonb column in postgres 9.6.6
Given a column "comments", with this example:
[
{
"Comment": "A",
"LastModified": "1527579949"
},
{
"Comment": "B",
"LastModified": "1528579949"
},
{
"Comment": "C",
"LastModified": "1529579949"
}
]
If I wanted to append Z to each comment (giving AZ, BZ, CZ).
I know I need to use something like jsonb_set(comments, '{"Comment"}',
Any hints on finishing this off?
Thanks.
Try:
UPDATE elbat
SET comments = array_to_json(ARRAY(SELECT jsonb_set(x.original_comment,
'{Comment}',
concat('"',
x.original_comment->>'Comment',
'Z"')::jsonb)
FROM (SELECT jsonb_array_elements(elbat.comments) original_comment) x))::jsonb;
It uses jsonb_array_elements() to get the array elements as set, applies the changes on them using jsonb_set(), transforms this to an array and back to json with array_to_json().
But that's an awful lot of work. OK, maybe there is a more elegant solution, that I didn't find. But since your JSON seems to have a fixed schema anyway, I'd recommend a redesign to do it the relational way and have a simple table for the comments plus a linking table for the objects the comment is on. The change would have been very, very easy in such a model for sure.
Find a query returning the expected result:
select jsonb_agg(value || jsonb_build_object('Comment', value->>'Comment' || 'Z'))
from my_table
cross join jsonb_array_elements(comments);
jsonb_agg
-----------------------------------------------------------------------------------------------------------------------------------------------------
[{"Comment": "AZ", "LastModified": "1527579949"}, {"Comment": "BZ", "LastModified": "1528579949"}, {"Comment": "CZ", "LastModified": "1529579949"}]
(1 row)
Create a simple SQL function based of the above query:
create or replace function update_comments(jsonb)
returns jsonb language sql as $$
select jsonb_agg(value || jsonb_build_object('Comment', value->>'Comment' || 'Z'))
from jsonb_array_elements($1)
$$;
Use the function:
update my_table
set comments = update_comments(comments);
DbFiddle.