Update a mixed and nested object in Snowflake - sqlalchemy

I have a Snowflake table with one variant column (raw).
Every row in this table is complex (both dictionaries and arrays) and nested (multiple hierarchies).
What I want to do is to be able to update a specific item in some array.
It will be easier to understand it using an example so consider this as a row in the table:
{
"id": "1234"
"x_id": [
{
"y_id": "790437306684007491",
"y_state": "some_state"
}
],
"comments": {
"1": [
{
"comment_id": "bb288743-3b73-4423-b76b-f26b8c37f7d4",
"comment_timestamp": "2021-02-10 14:53:25.667564",
"comment_text": "Hey"
},
{
"comment_id": "7378f332-93c4-4522-9f73-3b6a8a9425ce",
"comment_text": "You",
"comment_timestamp": "2021-02-10 14:54:21.337046"
}
],
"2": [
{
"comment_id": "9dd0cbb0-df80-4b0f-b399-9ee153161462",
"comment_text": "Hello",
"comment_timestamp": "2021-02-09 09:26:17.987386"
},
{
"comment_id": "1a3bf1e8-82b5-4a9c-a959-a1da806ce7e3",
"comment_text": "World",
"comment_timestamp": "2021-02-09 09:28:32.144175"
}
]
}
}
And what I want is to update the comment text of a specific comment.
I know that I can update the whole JSON programmatically and update the whole object using PARSE_JSON, but this approach isn't sufficient because there could be other updates that will override other comments so this approach will fail (because these update will override each other).
So first, I've tried the naive approach (which I knew wouldn't work but I had to try):
update table1
set raw['comments']['1'][0]["comment_text"] = 'please work'
And not surprisingly I'm getting the following error:
SQL compilation error: syntax error line 2 at position 7 unexpected '['.
Next, I tried OBJECT_INSERT which should allow a way to update an object but it fails because of the nested key ('1'):
UPDATE table1
SET raw = OBJECT_INSERT(raw:comments:1, "comment_test", 'please work')
with the error
SQL compilation error: syntax error line 1 at position 99 unexpected '1'.
(I've also tried several permutations of this approach with raw:comments:"1" or raw:comments:1[0] or raw['comments']['1'] and some others)
I also tried to refactor the object so instead of having the comments as dictionary, to flat the comments into an array, something like:
{
"id": "1234"
"x_id": [
{
"y_id": "790437306684007491",
"y_state": "some_state"
}
],
"comments": [
{
"comment_id": "bb288743-3b73-4423-b76b-f26b8c37f7d4",
"comment_timestamp": "2021-02-10 14:53:25.667564",
"comment_text": "Hey"
"comment_key": "1"
},
{
"comment_id": "7378f332-93c4-4522-9f73-3b6a8a9425ce",
"comment_text": "You",
"comment_timestamp": "2021-02-10 14:54:21.337046"
"comment_key": "1"
}
{
"comment_id": "9dd0cbb0-df80-4b0f-b399-9ee153161462",
"comment_text": "Hello",
"comment_timestamp": "2021-02-09 09:26:17.987386",
"comment_key": "2"
},
{
"comment_id": "1a3bf1e8-82b5-4a9c-a959-a1da806ce7e3",
"comment_text": "World",
"comment_timestamp": "2021-02-09 09:28:32.144175",
"comment_key": "2"
}
]
}
But this doesn't get me any closer to a solution. I've looked for some ARRAY_REPLACE function that replace an item in array, but it doesn't look that such function exists (all semi-structured related functions)
I've also considered using JavaScript UDF's to do it, but I didn't find any source to UDF's that can actually update a row (they're all used to get data and not update it, as far from what I saw).
Is there any way to achieve what I want?
Thanks a lot!

You can update complex JSON structures using JavaScript UDFs. Here's a sample. Note that both of your JSON samples have errors. I used the second one and fixed the missing commas.
-- Create a temp table with a sigle variant. By convention, I uses "v" as the name of any
-- column in a single-column table. You can change to "raw" in your code.
create or replace temp table foo(v variant);
-- Create a UDF that updates the exact key you want to update.
-- Unfortunately, JavaScript treats the object path as a constant so you can't make this
-- a string that you pass in dynamically. There are ways around this possibly, but
-- library restrictions would require a raw JavaScript parser function. Just update the
-- path you need in the UDF.
create or replace function update_json("v" variant, "newValue" string)
returns variant
language javascript
as
$$
v.comments[0].comment_text = newValue;
return v;
$$;
-- Insert the corrected JSON into the variant field
insert into foo select parse_json('{
"id": "1234",
"x_id": [{
"y_id": "790437306684007491",
"y_state": "some_state"
}],
"comments": [{
"comment_id": "bb288743-3b73-4423-b76b-f26b8c37f7d4",
"comment_timestamp": "2021-02-10 14:53:25.667564",
"comment_text": "Hey",
"comment_key": "1"
},
{
"comment_id": "7378f332-93c4-4522-9f73-3b6a8a9425ce",
"comment_text": "You",
"comment_timestamp": "2021-02-10 14:54:21.337046",
"comment_key": "1"
},
{
"comment_id": "9dd0cbb0-df80-4b0f-b399-9ee153161462",
"comment_text": "Hello",
"comment_timestamp": "2021-02-09 09:26:17.987386",
"comment_key": "2"
},
{
"comment_id": "1a3bf1e8-82b5-4a9c-a959-a1da806ce7e3",
"comment_text": "World",
"comment_timestamp": "2021-02-09 09:28:32.144175",
"comment_key": "2"
}
]
}');
-- Show how the change works without updating the row
select update_json(v, 'please work') from foo;
-- Now update the row using the output. Note that this is updating the
-- whole variant field, not a portion of it.
update foo set v = update_json(v, 'please work');
-- Show the updated key
select v:comments[0].comment_text::string from foo;
Finally, if you want to modify a property that you have to look through the keys to find what you need first, you can do that in a loop in JavaScript. For example, if it's not the 1st comment you need but the one with a particular UUID or comment_text, etc., you can loop through to find it and update the comment_key on the same iteration of the loop.

Thanks, it works!
I've kinda managed to get it to work using built-in functions -
Assuming we know the position of the comment (in this example, position=3):
UPDATE table1 SET
raw = object_construct(
'id', raw:id,
'x_id', raw:x_id,
'comments', array_cat(array_append(array_slice(raw:comments ,0 ,2), parse_json('{"id": "3", "comment_text": "please work"}')) , ARRAY_SLICE(raw:comments,3,array_size(raw:comments)))
)
WHERE raw['id'] = 'some_id'
But I'm still thinking which approach will do the work better.
Anyway, thanks, helped a lot.

Related

How to update a specific JSON element inside a complex JSON document in postgresql 10

I have JSON document which is stored under single column of type jsonb inside postgresql which looks like below:
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"fullUrl": "urn:uuid:100",
"resource": {
"resourceType": "Encounter",
"id": "110",
"status": "planned",
"priority": {
"coding": [
{
"code": "ASAP"
}
]
},
"subject": {
"reference": "Patient/123"
},
"appointment": [
{
"reference": "Appointment/12213#42"
}
],
"diagnosis": [
{
"condition": {
"reference": "Condition/condReferenceValue"
},
"use": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/diagnosis-role",
"code": "AD"
},
{
"system": "http://terminology.hl7.org/CodeSystem/diagnosis-role",
"code": "DD"
}
]
}
}
],
"hospitalization": {
"preAdmissionIdentifier": {
"system": "https://system.html"
}
},
"location": [
{
"location": {
"display": "Mumbai"
},
"status": "active"
},
{
"status": "planned"
}
]
},
"request": {
"method": "POST",
"url": "Encounter"
}
}
]
}
Now, I want to update value for reference under subject attribute. So, I tried below way but it throws an error:
update fhir.testing set names = jsonb_set(names,'{"subject":{"reference"','"Patient/1"',true) where id = 10;
Error:
SQL Error [22P02]: ERROR: malformed array literal: "{"subject":{"reference""
Detail: Unexpected array element.
I referred this link but didn't work out for me. How can I do it?
I don't use Postgres that much but from what i read in the relative jsonb_set example in the documentation of JSON functions (and since you want to update) shouldn't it be
jsonb_set(names, '{entry,0,subject,reference}','Patient/1', false)
instead of
jsonb_set(names,'{"subject":{"reference"','"Patient/1"',true)
jsonb
jsonb_set(target jsonb, path text[], new_value jsonb [, create_missing
boolean])
Returns target with the section designated by path replaced by
new_value, or with new_value added if create_missing is true (default
is true) and the item designated by path does not exist. As with the
path oriented operators, negative integers that appear in path count
from the end of JSON arrays.
EDIT
To explain the path used in jsonb_set, check this example.
jsonb_set('[{"f1":1,"f2":null},2,null,3]', '{0,f1}','[2,3,4]', false)
returns
[{"f1":[2,3,4],"f2":null},2,null,3]
As i understand if a sub-element in a complex JSON document is an array, you need to specify it's index e.g. 0,1,2,...
EDIT
Always look very carefully the structure of the JSON document. I simply write this because i did not see that subject was a child of resource and that is causing you the error.
So the correct path is actually '{entry,0,resource,subject,reference}'
Correct Query for your requirement is:
update fhir.testing
set names= jsonb_set(names, '{entry,0,resource,subject,reference}', '"Patient/1"' , false)
where id = 10;
Explanation
json_set takes 4 parameter
target_json (jsonb) - which accept jsonb type data. In your case it is names field.
path (text[]) - which accepts a text array. in your case it is '{entry,0,resource,subject,reference}'.
new_value (jsonb) - in your case you want to change it to '"Patient/1"'.
create_missing (boolean) - in your case it should be false. as you want to replace the existing one. if you want to create the reference with given value in case of not found then just mark it true.
the value is not valid json, try this out:
update fhir.testing set names = jsonb_set(names, '{"entry": [{"resource": {"subject":{"reference":"Patient/1"} }}]}',true) where id = 10;
You have to create a valid json, closing every { and every ], yours was
'{"subject":{"reference"'

Use Doctrine to search into a json database column

I have a Symfony 3.2 project, and I need to filter data from a json column.
Given that we have an entity named "pack" with a json column named "settings" containing this kind of data:
{
"name": "My pack",
"blocks": [
{
"name": "Block 1",
"fields": [
{"label": "A", "value": "57"},
{"label": "B", "value": "100"}
]
},
{
"name": "Bock 2",
"fields": [
{"label": "C", "value": "80"}
]
}
]
}
I have to search packs with a field which has the label "B" and its value at "100", but each pack doesn't have same blocks and fields order.
So in my repository, using Doctrine\ORM\EntityRepository and opsway/doctrine-dbal-postgresql (for GET_JSON_FIELD and GET_JSON_OBJECT functions), this kind of condition works:
use Doctrine\ORM\EntityRepository;
class Packs extends EntityRepository
{
public function findFiltered(...)
{
return $this->createQueryBuilder('pack')
->andWhere("GET_JSON_FIELD(GET_JSON_OBJECT(pack.settings, '{blocks,0,fields,1}'), 'label') = :label")
->andWhere("GET_JSON_FIELD(GET_JSON_OBJECT(pack.settings, '{blocks,0,fields,1}'), 'value') = :value")
->setParameter('label', 'B')
->setParameter('value', '100')
;
}
}
But the problem is that I have to specify the precise block (the first block object), and the precise field (the second field object of the first block object). And my two condition aren't connected, it search if there is a label "B", then it search if there is a value "100". When I would like to have a research in all blocks and fields to find the good label for the good value. Any idea?
I found the good SQL request for my problem:
SELECT *
FROM pack p, json_array_elements(p.settings#>'{blocks}') blocks, json_array_elements(blocks#>'{fields}') fields
WHERE fields->>'label' = 'B' and fields->>'value' = '100';
But how I do that with doctrine?
Maybe this link can help you, it is a custom filter for a JSON type field, maybe it will serve as an example, but these functions with this bundle solved the problem for me. I hope this helps someone else too. Cheers!

Postgres - updating an array element in a json column

I have a json column in a postgres table.
The column contains the following json data:
{
"data": {
"id": "1234",
"sites": [
{
"site": {
"code": "1",
"display": "Site1"
}
},
{
"site": {
"code": "2",
"display": "Site2"
},
"externalSite": true
},
{
"site": {
"code": "3",
"display": "Site3"
}
}
]
}
}
I need to create an update query that adds another attribute ('newAttribute' in the sample below) to all array items that have '"externalSite": true', so, after running the update query the second array element will be:
{
"site": {
"code": "2",
"display": "Site2"
},
"externalSite": true,
"newAttribute": true
}
The following query returns the array elements that need to be updated:
select * from myTable, jsonb_array_elements(data -> 'sites') sites
where sites ->'externalSite' = 'true'
What is the syntax of the update query?
Thanks
Kobi
Assuming your table is called test and your column is called data, you can update it like so:
UPDATE test SET data =
(select jsonb_set(data::jsonb, '{"data","sites"}', sites)
FROM test
CROSS JOIN LATERAL (
SELECT jsonb_agg(CASE WHEN site ? 'externalSite' THEN site || '{"newAttribute":"true"}'::jsonb
ELSE site
END) AS sites
FROM jsonb_array_elements( (data#>'{"data","sites"}')::jsonb ) as ja(site)
) as sub
);
Note that I cast the data to jsonb data as there are more functions and operators available for manipulating jsonb than plain json.
You can run the SELECT statement alone to see what it is doing, but the basic idea is to re-create the sites object by expanding it with jsonb_array_elements and adding the newAttribute attribute if externalSite exists.
This array is then aggregated with jsonb_agg and, finally, in the outer select, the sites object is replaced entirely with this newly computed version.

How to edit a json dictionary in Robot Framework

I am currently implementing some test automation that uses a json POST to a REST API to initialize the test data in the SUT. Most of the fields I don't have an issue editing using information I found in another thread: Json handling in ROBOT
However, one of the sets of information I am editing is a dictionary of meta data.
{
"title": "Test Auotmation Post 2018-03-06T16:12:02Z",
"content": "dummy text",
"excerpt": "Post made by automation for testing purposes.",
"name": "QA User",
"status": "publish",
"date": "2018-03-06T16:12:02Z",
"primary_section": "Entertainment",
"taxonomy": {
"section": [
"Entertainment"
]
},
"coauthors": [
{
"name": "QA User - CoAuthor",
"meta": {
"Title": "QA Engineer",
"Organization": "That One Place"
}
}
],
"post_meta": [
{
"key": "credit",
"value": "QA Engineer"
},
{
"key": "pub_date",
"value": "2018-03-06T16:12:02Z"
},
{
"key": "last_update",
"value": "2018-03-06T16:12:02Z"
},
{
"key": "source",
"value": "wordpress"
}
]
}
Is it possible to use the Set to Dictionary Keyword on a dictionary inside a dictionary? I would like to be able to edit the value of the pub_date and last_update inside of post_meta, specifically.
The most straightforward way would be to use the Evaluate keyword, and set the sub-dict value in it. Presuming you are working with a dictionary that's called ${value}:
Evaluate $value['post_meta'][1]['pub_date'] = 'your new value here'
I won't get into how to find the index of the post_meta list that has the 'key' with value 'pub_date', as that's not part of your question.
Is it possible to use the Set to Dictionary Keyword on a dictionary inside a dictionary?
Yes, it's possible.
However, because post_meta is a list rather than a dictionary, you will have to write some code to iterate over all of the values of post_meta until you find one with the key you want to update.
You could do this in python quite simply. You could also write a keyword in robot to do that for you. Here's an example:
*** Keywords ***
Set list element by key
[Arguments] ${data} ${target_key} ${new_value}
:FOR ${item} IN #{data}
\ run keyword if '''${item['key']}''' == '''${target_key}'''
\ ... set to dictionary ${item} value=${new_value}
[return] ${data}
Assuming you have a variable named ${data} contains the original JSON data as a string, you could call this keyword like the following:
${JSON}= evaluate json.loads('''${data}''') json
set list element by key ${JSON['post_meta']} pub_date yesterday
set list element by key ${JSON['post_meta']} last_update today
You will then have a python object in ${JSON} with the modified values.

Freebase: working with "namespace-prefix key" Json Object

I have made a request to freebase API, and the query is:
{
"id" : "/m/01f8mn",
"key" : [{
"namespace" : "/wikipedia/en_id",
"value" : null
}],
"key" : [{
"namespace" : "/authority/mobygames/game",
"value" : null
}]
}​
And I got the answer from Freebase:
{
"code": "/api/status/ok",
"result": {
"id": "/m/01f8mn",
"key": [{
"namespace": "/wikipedia/en_id",
"value": "213502"
}],
"ns0:key": [{
"namespace": "/authority/mobygames/game",
"value": "prince-of-persia"
}],
"ns1:key": [{
"namespace": "/authority/giantbomb/game",
"value": "61-2561"
}]
},
"status": "200 OK",
"transaction_id": "cache;cache03.p01.sjc1:8101;2012-05-20T18:37:04Z;0060"
}
As you can notice, there is a "namespace prefix" key in the result ("ns0:key", "ns1:key")
The question is: how can I handle this with Javascript Object? Can anyone help?
When accessing properties with special characters in the name, access it as you would an associative array property:
yourObject.result["ns0:key"]
Since the namespace and value properties are stored inside of an object that is the first index of an array, to access the namespace and value, use the following syntax:
yourObject.result["ns0:key"][0].namespace
yourObject.result["ns0:key"][0].value // contains "prince-of-persia"
yourObject.result["ns1:key"][0].namespace
yourObject.result["ns1:key"][0].value // contains "61-2561"
Your initial query isn't valid JSON (the two "key" keys will overwrite each other), so I suspect there's an intermediary software layer involved (perhaps Freebase's query editor?).
If it would help to be able to control the prefixes (they're not really namespaces), you can make them up yourself to suit you. Use "key" and "other:key" or whatever scheme works for you. You could also re-write the query using the |= operator so you can specify both namespaces in a single key clause (e.g. "key":[{"namespace|=" :[ns1,ns2]}]).
for ( key in freebaseResult.result ) {
var i, resultArray = freebaseResult.result[key];
for ( i = 0; i < resultArray.length; i++ ) {
alert( resultArray[i].namespace + ": " + resultArray[i].value );
}
}