plpgsql jsonb_set for JSON array of objects with nested arrays - postgresql

Using PostgreSQL 14.0 PL/SQL (inside a do block). Attempting to:
query a certain key ('county') in a jsonb array of objects (which in turn has object + nested arrays) based on dynamic variable value (named cty.cty_name)
retrieve value and change it
update said jsonb to reflect the updated value in (2)
after executing (3) on multiple values, create new table with above jsonb as a row with one column
steps (1) and (2) execute properly. But, for the life of me, I can't figure (3) out.
jsonb object(res) -- may have 100s of array items at index root:
[ {"county": "x", "dpa": ["a", "b", "c"]},
{"county": "y", "dpa": ["d", "e", "f"]},
{"county": "z", "dpa": ["h", "i", "j"]},
...
]
code for (1) and (2) above:
execute format('select jsonb_path_query_array(''%s'', ''$[*]?(#.%s=="%s")'')',
res,'county',cty.cty_name) into s1;
execute format('select jsonb_array_elements(''%s'')->''%s''', s1,'dpa') into s2;
s2 := s2 || jsonb_build_array(r1.name);
where say:
cty.cty_name is y (which is created from a select in for loop)
r1.name is m
s2 holds the new value e.g. ["d", "e", "f", "m"]
Now, to execute (3) I need path to dpa for which key county matches value y in some index in res. Having tried (and failed miserably) at various permutations of jsonb_query_path with SQL/JSON Path, dollar-quoted strings, jsonb_path_to_array with double-quoted hell for format queries, other SO solutions which use idx or idx-1 (but I don't have JSON in table), I had to resort to soliciting the Borg collective's wisdom. Help please.

The problem you're running into with the current approach is twofold:
there's no way to "delete" the matching county object in vitro (via jsonb_set(), etc.)
there's no way to force uniqueness (to utilize the ON CONFLICT ... DO UPDATE mechanism) within the json document itself to accomplish the same
When we get to
Now, to execute (3) I need path to dpa for which key county matches value y in some index in res.
instead of updating the existing record in-place, why not just remove the matching record (with now-stale value for "dpa"), re-aggregate what remains (i.e. the non-matching objects), and then append the updated matching object to the aggregated jsonb array, similar to:
SELECT jsonb_agg(a) || jsonb_build_object('county', 'y', 'dpa', (jsonb_path_query_array(res, '$[*] ? (#.county=="y")')#>'{0,dpa}') || jsonb_build_array('m') )
FROM jsonb_array_elements(res) a
WHERE NOT a #> (jsonb_path_query_array(res, '$[*] ? (#.county=="y")')->0);
This gives a single jsonb value back per your specification in (4); you should be able to parameterize this into your EXECUTE invocation as necessary.
Worth noting on the output order, if you're looping over the initial "res" array, then the order of the objects within the array (with respect to the "county" values of the driving cursor) will be restored according to the order of the cursor you're iterating through for "county".
This is because a full cycle through said cursor will remove each of the old objects and replace them at the end of the resultant array, so defining an ORDER BY clause in this cursor will be important if this is relevant.

Related

JSONB Data Type Modification in Postgresql

I have a doubt with modification of jsonb data type in postgres
Basic setup:-
array=> ["1", "2", "3"]
and now I have a postgresql database with an id column and a jsonb datatype column named lets just say cards.
id cards
-----+---------
1 {"1": 3, "4": 2}
thats the data in the table named test
Question:
How do I convert the cards of id->1 FROM {"1": 3, "4": 2} TO {"1": 4, "4":2, "2": 1, "3": 1}
How I expect the changes to occur:
From the array, increment by 1 all elements present inside the array that exist in the cards jsonb as a key thus changing {"1": 3} to {"1": 4} and insert the values that don't exist as a key in the cards jsonb with a value of 1 thus changing {"1":4, "4":2} to {"1":4, "4":2, "2":1, "3":1}
purely through postgres.
Partial Solution
I asked a senior for support regarding my question and I was told this:-
Roughly (names may differ): object keys to explode cards, array_elements to explode the array, left join them, do the calculation, re-aggregate the object. There may be a more direct way to do this but the above brute-force approach will work.
So I tried to follow through it using these two functions json_each_text(), json_array_elements_text() but ended up stuck halfway into this as well as I was unable to understand what they meant by left joining two columns:-
SELECT jsonb_each_text(tester_cards) AS each_text, jsonb_array_elements_text('[["1", 1], ["2", 1], ["3", 1]]') AS array_elements FROM tester WHERE id=1;
TLDR;
Update statement that checks whether a range of keys from an array exist or not in the jsonb data and automatically increments by 1 or inserts respectively the keys into the jsonb with a value of 1
Now it might look like I'm asking to be spoonfed but I really haven't managed to find anyway to solve it so any assistance would be highly appreciated 🙇
The key insight is that with jsonb_each and jsonb_object_agg you can round-trip a JSON object in a subquery:
SELECT id, (
SELECT jsonb_object_agg(key, value)
FROM jsonb_each(cards)
) AS result
FROM test;
(online demo)
Now you can JOIN these key-value pairs against the jsonb_array_elements of your array input. Your colleague was close, but not quite right: it requires a full outer join, not just a left (or right) join to get all the desired object keys for your output, unless one of your inputs is a subset of the other.
SELECT id, (
SELECT jsonb_object_agg(COALESCE(obj_key, arr_value), …)
FROM jsonb_array_elements_text('["1", "2", "3"]') AS arr(arr_value)
FULL OUTER JOIN jsonb_each(cards) AS obj(obj_key, obj_value) ON obj_key = arr_value
) AS result
FROM test;
(online demo)
Now what's left is only the actual calculation and the conversion to an UPDATE statement:
UPDATE test
SET cards = (
SELECT jsonb_object_agg(
COALESCE(key, arr_value),
COALESCE(obj_value::int, 0) + (arr_value IS NOT NULL)::int
)
FROM jsonb_array_elements_text('["1", "2", "3"]') AS arr(arr_value)
FULL OUTER JOIN jsonb_each_text(cards) AS obj(key, obj_value) ON key = arr_value
);
(online demo)

What does `target` mean in `jsonb_set` of postgresql?

I am using jsonb type in a column in postgresql11. And I'd like to update one field in the json data and I see there is a function jsonb_set which can be used. (https://www.postgresql.org/docs/current/functions-json.html).
However, based on the document,
jsonb_set ( target jsonb, path text[], new_value jsonb [, create_if_missing boolean ] ) → jsonb
Returns target with the item designated by path replaced by new_value, or with new_value added
if create_if_missing is true (which is the default) and the item designated by path does not
exist. All earlier steps in the path must exist, or the target is returned unchanged. As with
the path oriented operators, negative integers that appear in the path count from the end of
JSON arrays. If the last path step is an array index that is out of range, and create_if_missing
is true, the new value is added at the beginning of the array if the index is negative, or at
the end of the array if it is positive.
The first argument is target. What does target mean here? Do I need to do a query to get existing value and put it as target?
I have tried below update statement:
my current data is:
# select "taxes" from "Sites" where "id" = '6daa9b5d-d5b2-4b0d-a8ee-5ad2cb141594';
taxes
--------------------------------------------------------------------------------------------------------------
{"feePercent": 0, "percent": 0}
And I tried below update:
# update "Sites" set "feePercent" = jsonb_set('{"feePercent": 0, "percent": 0}', '{feePercent}', 1) where "siteUuid"='6daa9b5d-d5b2-4b0d-a8ee-5ad2cb141594';
but I got below error:
ERROR: function jsonb_set(unknown, unknown, integer) does not exist
LINE 1: update "Sites" set "feePercent" = jsonb_set('{"feePerce...
jsonb_set() modifies a specific JSON object. So, your target is the JSON object (or JSON column) which you want to modify.
jsonb_set(my_jsonb_to_be_modified, ...)
So, if you had this JSON object;
{"my":"old", "json":"object"}
With the function you can turn it into:
{"my":"new", "json":"object"}
The code is:
demo:db<>fiddle
SELECT jsonb_set('{"my":"old", "json":"object"}', '{my}', '"new"')
The target is the original JSON object, the path points to the element you want to modify, and new_value is the new value for the element you specified in the path.
In that case my had the value old, which turns into new now.
From PostgreSQL v14 on, you can use subscripts to make this UPDATE statement look natural:
UPDATE "Sites"
SET taxes['feePercent'] = to_jsonb(1)
WHERE id = '6daa9b5d-d5b2-4b0d-a8ee-5ad2cb141594';
For earlier versions, you will have to use jsonb_set like this:
UPDATE "Sites"
SET taxes = jsonb_set(taxes, ARRAY['feePercent'], to_jsonb(1))
WHERE id = '6daa9b5d-d5b2-4b0d-a8ee-5ad2cb141594';
The effect is the same: the whole JSON is read, a new JSON is created and stored in a new version of the row.
All this becomes much simpler if you don't use JSON, but regular table columns.

Remove Key Value pair from jsonb nested array in postgresql

I have jsonb data as
{
"a":[
{"b":1,"c":2,"d":3},
{"b":4,"c":5,"d":6}
],
"g":[
{"b":1,"c":2,"d":3},
{"b":4,"c":5,"d":6}
]
}
I want to remove c key from nested array in both "a" and "g" keys. Is there a single query to perform this?
step-by-step demo: db<>fiddle
SELECT
jsonb_object_agg(key, a) -- 5
FROM (
SELECT
mydata,
key,
jsonb_agg(a_elems.value - 'c') as a -- 3/4
FROM
mytable,
jsonb_each(mydata) elems, -- 1
jsonb_array_elements(elems.value) AS a_elems -- 2
GROUP BY mydata, key -- 4
) s
GROUP BY mydata -- 5
Expand the JSON elements into one row each. This generates two columns: One for the key and one for the value (the JSON array)
Expand the JSON array into one row each (which separates the aggregated JSON object from which you want to remove the c element)
You can use the - operator to remove the element.
To reaggregate the original JSON object, you need to group it backwards. jsonb_agg() reaggregates the arrays
Finally you need to rebuild the original JSON object with jsonb_object_agg() using the previously generated key column and the new array column.

How can I update or insert multiple rows with ecto? (Upsert)

How can I either insert or update multiple rows with different values using Ecto with Postgres?
If I have a schema/struct: %Counter{key: String.t(), count: integer()}
How can I insert or update multiple entries? If the record does not exist I want to insert it, but if it does exist I want to increment the value.
[
%{key: "questions:asked", count: 1},
%{key: "questions:answered", count: 1},
%{key: "ads:viewed", count: 3}
]
Ecto.Repo.insert_all with :on_replace looks like it should work, but I want unique values for each row.
You can use Ecto.Repo.insert_all, but you must provide a query and take advantage of Postgresql's excluded table available in the conflict action.
upsert_query =
Counter
|> where([o], o.key == fragment("EXCLUDED.key"))
|> update(inc: [count: fragment("EXCLUDED.count")])
Repo.insert_all(Counter, records,
on_conflict: upsert_query,
conflict_target: [:key],
returning: false
)
The excluded values are the values that you passed in represented as a temporary table only available in the on conflict.
It should be noted that this can be used with set instead of inc if you wish to set a particular value.
Is there a better solution?

How to split array in json using json_query?

I've got a column in a table that's a json. It contains only values without keys like
Now I'm trying to split the data from the json and create new table using every index of each array as new entry like
I've already tried
SELECT JSON_QUERY(abc) as 'Type', Id as 'ValueId' from Table FOR JSON AUTO
Is there any way to handle splitting given that some arrays might be empty and look like
[]
?
A fairly simply approach would be to use outer apply with openjson.
First, create and populate sample table (Please save us this step in your future questions):
DECLARE #T AS TABLE
(
Id int,
Value nvarchar(20)
)
INSERT INTO #T VALUES
(1, '[10]'),
(2, '[20, 200]'),
(3, '[]'),
(4, '')
The query:
SELECT Id, JsonValues.Value
FROM #T As t
OUTER APPLY
OPENJSON( Value ) As JsonValues
WHERE ISJSON(t.Value) = 1
Results:
Id Value
1 10
2 20
2 200
3 NULL
Note the ISJSON condition in the where clause will prevent exceptions in case the Value column contains anything other than a valid json (an empty array [] is still considered valid for this purpose).
If you don't want to return a row where the json array is empty, use cross apply instead of outer apply.
Your own code calling for FOR JSON AUTO tries to create JSON out of tabular data. But what you really needs seems to be the opposite direction: You want to transform JSON to a result set, a derived table. This is done by OPENJSON.
Your JSON seems to be a very minimalistic array.
You can try something along this.
DECLARE #json NVARCHAR(MAX) =N'[1,2,3]';
SELECT * FROM OPENJSON(#json);
The result returns the zero-based ordinal position in key, the actual value in value and a (very limited) type-enum.
Hint: If you want to use this against a table's column you must use APPLY, something along
SELECT *
FROM YourTable t
OUTER APPLY OPENJSON(t.TheJsonColumn);