How to update PostgreSQL array of jsonb - arrays

I have a table like:
id: integer,
... other stuff...,
comments: array of jsonb
where the comments column has the following structure:
[{
"uid": "comment_1",
"message": "level 1 - comment 1",
"comments": [{
"uid": "subcomment_1",
"message": "level 2 - comment 1",
"comments": []
}, {
"uid": "subcomment_2",
"message": "level 1 - comment 2",
"comments": []
}]
},
{
"uid": "P7D1hbRq4",
"message": "level 1 - comment 2",
"comments": []
}
]
I need to update a particular field, for example:comments[1](with uid = comment_1) -> comments[2] (with uid = subcomment_2) -> message = 'comment edited'.
I'm brand new to postgresql and I can't figure it out how to do this, not even close. I manage to merge objects and change message for level 1 with:
UPDATE tasks
set comments[1] = comments[1]::jsonb || $$
{
"message": "something",
}$$::jsonb
where id = 20;
but that's as far as I could go.
Any hints towards the right direction?
LE:
I got this far:
UPDATE tasks
set comments[1] = jsonb_set(comments[1], '{comments,1, message}', '"test_new"')
where id = 20;
Sure, I can get this path from javascript but it's that a best practice? Not feeling comfortable using indexes from javascript arrays.
Should I try to write a sql function to get the array and use the 'uid' as key? Any other simpler way to search/select using the 'uid' ?
LLE
I can't get it to work using suggestion at:this question (which I read and tried)
Code bellow returns nothing:
-- get index for level 2
select pos as elem_index
from tasks,
jsonb_array_elements(comments[0]->'comments') with ordinality arr(elem, pos)
where tasks.id = 20 and
elem ->>'uid'='subcomment_1';
and I need it for several levels so it's not quite a duplicate.

First, you cannot update a part of a column (an element of an array) but only a column as a whole.
Next, you should understand what the path (the second argument of the jsonb_set() function) means.
Last, the third argument of the function is a valid json, so a simple text value must be enclosed in both single and double quotes.
update tasks
set comments = jsonb_set(comments, '{0, comments, 1, message}', '"comment edited"')
where id = 1;
Path:
0 - the first element of the outer array (elements are indexed from
0)
comments - an object with key comments
1 - the second element of
the comments array
message - an object message in the above
element.
See Db<>fiddle.

Related

Is there a way of reading from sub arrays?

I am currently building an iOS application that stores user added products using Google Firestore. Each product that is added is concatenated into a single, user specific "products" array (as shown below - despite having separate numbers they are part of the same array but separated in the UI by Google to show each individual sub-array more clearly)
I use the following syntax to return the data from the first sub-array of the "products" field in the database
let group_array = document["product"] as? [String] ?? [""]
if (group_array.count) == 1 {
let productName1 = group_array.first ?? "No data to display :("`
self.tableViewData =
[cellData(opened: false, title: "Item 1", sectionData: [productName1])]
}
It is returned in the following format:
Product Name: 1, Listing Price: 3, A brief description: 4, Product URL: 2, Listing active until: 21/04/2021 10:22:17
However I am trying to query each of the individual sections of this sub array, so for example, I can return "Product Name: 1" instead of the whole sub-array. As let productName1 = group_array.first is used to return the first sub-array, I have tried let productName1 = group_array.first[0] to try and return the first value in this sub-array however I receive the following error:
Cannot infer contextual base in reference to member 'first'
So my question is, referring to the image from my database (at the top of my question), if I wanted to just return "Product Name: 1" from the example sub-array, is this possible and if so, how would I extract it?
I would reconsider storing the products as long strings that need to be parsed out because I suspect there are more efficient, and less error-prone, patterns. However, this pattern is how JSON works so if this is how you want to organize product data, let's go with it and solve your problem.
let productRaw = "Product Name: 1, Listing Price: 3, A brief description: 4, Product URL: 2, Listing active until: 21/04/2021 10:22:17"
First thing you can do is parse the string into an array of components:
let componentsRaw = productRaw.components(separatedBy: ", ")
The result:
["Product Name: 1", "Listing Price: 3", "A brief description: 4", "Product URL: 2", "Listing active until: 21/04/2021 10:22:17"]
Then you can search this array using substrings but for efficiency, let's translate it into a dictionary for easier access:
var product = [String: String]()
for component in componentsRaw {
let keyVal = component.components(separatedBy: ": ")
product[keyVal[0]] = keyVal[1]
}
The result:
["Listing active until": "21/04/2021 10:22:17", "A brief description": "4", "Product Name": "1", "Product URL": "2", "Listing Price": "3"]
And then simply find the product by its key:
if let productName = product["Product Name"] {
print(productName)
} else {
print("not found")
}
There are lots of caveats here. The product string must always be uniform in that commas and colons must always adhere to this strict formatting. If product names have colons and commas, this will not work. You can modify this to handle those cases but it could turn into a bowl of spaghetti pretty quickly, which is also why I suggest going with a different data pattern altogether. You can also explore other methods of translating the array into a dictionary such as with reduce or grouping but there are big-O performance warnings. But this would be a good starting point if this is the road you want to go down.
All that said, if you truly want to use this data pattern, consider adding a delimiter to the product string. For example, a custom delimiter would greatly reduce the need for handling edge cases:
let productRaw = "Product Name: 1**Listing Price: 3**A brief description: 4**Product URL: 2**Listing active until: 21/04/2021 10:22:17"
With a delimiter like **, the values can contain commas without worry. But for complete safety (and efficiency), I would add a second delimiter so that values can contain commas or colons:
let productRaw = "name$$1**price$$3**description$$4**url$$2**expy$$21/04/2021 10:22:17"
With this string, you can much more safely parse the components by ** and the value from the key by $$. And it would look something like this:
let productRaw = "name$$1**price$$3**description$$4**url$$2**expy$$21/04/2021 10:22:17"
let componentsRaw = productRaw.components(separatedBy: "**")
var product = [String: String]()
for component in componentsRaw {
let keyVal = component.components(separatedBy: "$$")
product[keyVal[0]] = keyVal[1]
}
if let productName = product["name"] {
print(productName)
} else {
print("not found")
}

Getting values from json array using an array of object and keys in Python

I'm a Python newbie and I'm trying to write a script to extract json keys by passing the keys dinamically, reading them from a csv.
First of all this is my first post and I'm sorry if my questions are banals and if the code is incomplete but it's just a pseudo code to understand the problem (I hope not to complicate it...)
The following partial code retrieves the values from three key (group, user and id or username) but I'd like to load the objects and key from a csv to make them dinamicals.
Input json
{
"fullname": "The Full Name",
"group": {
"user": {
"id": 1,
"username": "John Doe"
},
"location": {
"x": "1234567",
"y": "9876543"
}
},
"color": {
"code": "ffffff",
"type" : "plastic"
}
}
Python code...
...
url = urlopen(jsonFile)
data = json.loads(url.read())
id = (data["group"]["user"]["id"])
username = (data["group"]["user"]["username"])
...
File.csv loaded into an array. Each line contains one or more keys.
fullname;
group,user,id;
group,user,username;
group,location,x;
group,location,y;
color,code;
The questions are: can I use a variable containing the object or key to be extract?
And how can I specify how many keys there are in the keys array to put them into the data([ ][ ]...) using only one line?
Something like this pseudo code:
...
url = urlopen(jsonFile)
data = json.loads(url.read())
...
keys = line.split(',')
...
# using keys[] to identify the objects and keys
value = (data[keys[0]][keys[1]][keys[2]])
...
But the line value = (data[keys[0]][keys[1]][keys[2]]) should have the exact number of the keys per line read from the csv.
Or I must to make some "if" lines like these?:
...
if len(keys) == 3:
value = (data[keys[0]][keys[1]][keys[2]])
if len(keys) == 2:
value = (data[keys[0]][keys[1]])
...
Many thanks!
I'm not sure I completely understand your question, but I would suggest you to try and play with pandas. It might be as easy as this:
import pandas as pd
df = pd.read_json(<yourJsonFile>, orient='columns')
name = df.fullname[0]
group_user = df.group.user
group_location = df.group.location
color_type = df.color.type
color_code = df.color.code
(Where group_user and group_location will be python dictionaries).

How to transform a JSON array nested inside an object inside another array in Postgres?

I'm using Postgres 9.6 and have a JSON field called credits with the following structure; A list of credits, each with a position and multiple people that can be in that position.
[
{
"position": "Set Designers",
people: [
"Joe Blow",
"Tom Thumb"
]
}
]
I need to transform the nested people array, which are currently just strings representing their names, into objects that have a name and image_url field, like this
[
{
"position": "Set Designers",
people: [
{ "name": "Joe Blow", "image_url": "" },
{ "name": "Tom Thumb", "image_url": "" }
]
}
]
So far I've only been able to find decent examples of doing this on either the parent JSON array or on an array field nested inside a single JSON object.
So far this is all I've been able to manage and even it is mangling the result.
UPDATE campaigns
SET credits = (
SELECT jsonb_build_array(el)
FROM jsonb_array_elements(credits::jsonb) AS el
)::jsonb
;
Create an auxiliary function to simplify the rather complex operation:
create or replace function transform_my_array(arr jsonb)
returns jsonb language sql as $$
select case when coalesce(arr, '[]') = '[]' then '[]'
else jsonb_agg(jsonb_build_object('name', value, 'image_url', '')) end
from jsonb_array_elements(arr)
$$;
With the function the update is not so horrible:
update campaigns
set credits = (
select jsonb_agg(jsonb_set(el, '{people}', transform_my_array(el->'people')))
from jsonb_array_elements(credits::jsonb) as el
)::jsonb
;
Working example in rextester.

Watson Conversation get the first key of every element of an array

I am currently working on a chatbot based on IBM Watson conversations.
And I'm struggling getting the key out of the enginePower in the filter.
I need every key to be displayed in the chat. So a user can select one.
The structure looks like that:
"filter": {
"enginePower": [{
"key": "55",
"text": "55 kW (75 PS)",
"selectable": false
},
{
"key": "65",
"text": "65 kW (88 PS)",
"selectable": false
},
{
"key": "66",
"text": "66 kW (90 PS)",
"selectable": false
},
{
"key": "81",
"text": "81 kW (110 PS)",
"selectable": false
}]
}
Thanks in advance.
Short answer:
Your key values are: <? T(String).join( ",", $filter.get('enginePower').![key] ) ?>
Broken down
$filter.get('enginePower')
This will return the enginePower as an array containing all your objects.
.![key]
This will return the "Key" field values as an array.
T(String).join( ",", XXX )
This will convert your array XXX into a comma delimited list.
So your final output will say:
Your key values are: 55,65,66,81
Just to add to this. You can only get a single key/value list. If you want to capture a value of a different attribute using a key lookup, then you need to loop.
So for example, start by setting a counter to 0. Then have your node check for $filter.enginePower.size() > $counter.
Within that node, you set "multiple responses" on. Then for the first condition you set $filter.enginePower[$counter].selectable == true. This will allow you to take action if that field is true.
After this you need to create a child node, and have the parent node jump to it. Within the child node response put <? $counter = $counter + 1 ?>. Lastly have the child node jump back to the parent node.
This will loop through the array. Warning! You can only loop 50 times before the loop will end. This is to stop potential endless loops.
Realistically though, you can easily solve all this by formatting the data correctly at the application layer.

How can I sort based on children?

Using Vapor and Fluent (PostgreSQL if that matters) I have entity B that has aID: Node (A is B's parent) to reference A and A has a one-to-many relationship with B. How can I make a query to fetch all A's sorted by the count of B's?
I want the result to look something like this:
All A's in DB
[
{
"id": 4,
"name": "Hi",
"bCount": 1000
},
{
"id": 3,
"name": "Another",
"bCount": 800
},
{
"id": 5,
"name": "Test",
"bCount": 30
}
]
Firstly,
Create a modal for A
Turn that JSON string to array of A
if you do this your sorting becomes as easy as -
array.sort { $0.bCount < $1.bCount }
This is going to be tricky to implement entirely in Fluent using Entity. Firstly, you will need to use raw SQL to get your bCount. Secondly, you will need to change your init(node:) to accept bCount, though it shouldn't be in your makeNode() because we don't want to create a stored database field for it.
Try this for your raw SQL (untested):
SELECT
A.*,
(
SELECT COUNT(*)
FROM B
WHERE B.aID = A.id
) AS bCount
FROM A
ORDER BY bCount
Then, run that query to get your A models.
var models: [A] = []
if let driver = drop.database?.driver as? PostgreSQLDriver {
if case .array(let array) = try driver.raw(sql) {
for result in array {
do {
var model = try A(node: result)
models.append(model)
}
}
}
}
As I said before, your init method on A will be receiving bCount so you will need to store it there.

Resources