JSONB fields and string keys - postgresql

I've a JSONB column with the following content:
{"ibd": true, "participant_id": "P016", "sample_participant_id": "B1"}
If I query the value without the double quotation mark I get an error:
ERROR: invalid input syntax for type json LINE 1: ...ERE
sample_metadata.metadata->'sample_participant_id' = 'B1'
for:
WHERE sample_metadata.metadata->'sample_participant_id' = 'B1'
But for
WHERE sample_metadata.metadata->'sample_participant_id' = '"B1"'
It works as expected. What I don't understand is why I need the double quotation mark. are they an actual value?
Thanks,
Eden

-> operator on a jsonb returns jsonb, so you can't compare a json with a string literal('B1') or text type.
WHERE sample_metadata.metadata->'sample_participant_id' = '"B1"'
works because element "B1" can be implicitly cast to jsonb for comparison
You should rather be using the ->> operator for your comparison. It returns text
WHERE metadata->>'sample_participant_id' = 'B1'
Demo

Related

SQLAlchemy IN_ - trouble with leading zeroes

In my sqlalchemy ( sqlalchemy = "^1.4.36" ) query I have a clause:
.filter( some_model.some_field[2].in_(['item1', 'item2']) )
where some_field is jsonb and the value in some_field value in the db formatted like this:
["something","something","123"]
or
["something","something","0123"]
note: some_field[2] is always digits-only double-quoted string, sometimes with leading zeroes and sometimes without them.
The query works fine for cases like this:
.filter( some_model.some_field[2].in_(['123', '345']) )
and fails when the values in the in_ clause have leading zeroes:
e.g. .filter( some_model.some_field[2].in_(['0123', '0345']) ) fails.
The error it gives:
cursor.execute(statement, parameters)\\npsycopg2.errors.InvalidTextRepresentation: invalid input syntax for type json\\nLINE 3: ...d_on) = 2 AND (app_cache.value_metadata -> 2) IN (\\'0123\\'\\n ^\\nDETAIL: Token \"0123\" is invalid.
Again, in the case of '123' (or any string of digits without leading zero) instead of '0123' the error is not thrown.
What is wrong with having leading zeroes for the strings in the list of in_ clause? Thanks.
UPDATE: basically, sqlachemy's IN_ assumes int input and fails accordingly. There must be some reasoning behind this behavior, can't tell what it is. I removed that filter fromm the query and did the filtering of the ouput in python code afterwards.
The problem here is that the values in the IN clause are being interpreted by PostgreSQL as JSON representations of integers, and an integer with a leading zero is not valid JSON.
The IN clause has a value of type jsonb on the left hand side. The values on the right hand side are not explicitly typed, so Postgres tries to find the best match that will allow them to be compared with a jsonb value. This type is jsonb, so Postgres attempts to cast the values to jsonb. This works for values without a leading zero, because digits in single quotes without leading zeroes are valid representations of integers in JSON:
test# select '123'::jsonb;
jsonb
═══════
123
(1 row)
but it doesn't work for values with leading zeroes, because they are not valid JSON:
test# select '0123'::jsonb;
ERROR: invalid input syntax for type json
LINE 1: select '0123'::jsonb;
^
DETAIL: Token "0123" is invalid.
CONTEXT: JSON data, line 1: 0123
Assuming that you expect some_field[2].in_(['123', '345']) and some_field[2].in_(['0123', '345']) to match ["something","something","123"] and ["something","something","123"] respectively, you can either serialise the values to JSON yourself:
some_field[2].in_([json.dumps(x) for x in ['0123', '345']])
or use the contained_by operator (<# in PostgreSQL), to test whether some_field[2] is present in the list of values:
some_field[2].contained_by(['0123', '345'])
or cast some_field[2] to text (that is, use the ->> operator) so that the values are compared as text, not JSON.
some_field[2].astext.in_(['0123', '345'])

Text and jsonb concatenation in a single postgresql query

How can I concatenate a string inside of a concatenated jsonb object in postgresql? In other words, I am using the JSONb concatenate operator as well as the text concatenate operator in the same query and running into trouble.
Or... if there is a totally different query I should be executing, I'd appreciate hearing suggestions. The goal is to update a row containing a jsonb column. We don't want to overwrite existing key value pairs in the jsonb column that are not provided in the query and we also want to update multiple rows at once.
My query:
update contacts as c set data = data || '{"geomatch": "MATCH","latitude":'||v.latitude||'}'
from (values (16247746,40.814140),
(16247747,20.900840),
(16247748,20.890570)) as v(contact_id,latitude) where c.contact_id = v.contact_id
The Error:
ERROR: invalid input syntax for type json
LINE 85: update contacts as c set data = data || '{"geomatch": "MATCH...
^
DETAIL: The input string ended unexpectedly.
CONTEXT: JSON data, line 1: {"geomatch": "MATCH","latitude":
SQL state: 22P02
Character: 4573
You might be looking for
SET data = data || ('{"geomatch": "MATCH","latitude":'||v.latitude||'}')::jsonb
-- ^^ jsonb ^^ text ^^ text
but that's not how one should build JSON objects - that v.latitude might not be a valid JSON literal, or even contain some injection like "", "otherKey": "oops". (Admittedly, in your example you control the values, and they're numbers so it might be fine, but it's still a bad practice). Instead, use jsonb_build_object:
SET data = data || jsonb_build_object('geomatch', 'MATCH', 'latitude', v.latitude)
There are two problems. The first is operator precedence preventing your concatenation of a jsonb object to what is read a text object. The second is that the concatenation of text pieces requires a cast to jsonb.
This should work:
update contacts as c
set data = data || ('{"geomatch": "MATCH","latitude":'||v.latitude||'}')::jsonb
from (values (16247746,40.814140),
(16247747,20.900840),
(16247748,20.890570)) as v(contact_id,latitude)
where c.contact_id = v.contact_id
;

How to use comparison where operator for JSON

I get this error when querying with a json column:
(psycopg2.ProgrammingError) operator does not exist: json = text
The column is defined as JSON with SQLAlchemy:
json_data = db.Column(db.JSON, nullable=False)
How do you compare with Postgres?
There is no equality (or inequality) operator for the data type json. If you need to test the value as a whole, you might cast to jsonb:
... WHERE json_data::jsonb = jsonb '{}';
Or cast to text for simple cases:
... WHERE json_data::text = '{}';
But there are many valid text representations for the same json value - which is the reason why Postgres does not implement equality / inequality operators for the type.
See:
How to query a json column for empty objects?

Postgres: create index on attribute of attribute in JSONB column?

I'm working in Postgres 9.6.5. I have the following table:
id | integer
data | jsonb
The data in the data column is nested, in the form:
{ 'identification': { 'registration_number': 'foo' }}
I'd like to index registration_number, so I can query on it. I've tried this (based on this answer):
CREATE INDEX ON mytable((data->>'identification'->>'registration_number'));
But got this:
ERROR: operator does not exist: text ->> unknown
LINE 1: CREATE INDEX ON psc((data->>'identification'->>'registration... ^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
What am I doing wrong?
You want:
CREATE INDEX ON mytable((data -> 'identification' ->> 'registration_number'));
The -> operator returns the jsonb object under the key, and the ->> operator returns the jsonb object under the key as text. The most notable difference between the two operators is that ->> will "unwrap" string values (i.e. remove double quotes from the TEXT representation).
The error you're seeing is reported because data ->> 'identification' returns text, and the subsequent ->> is not defined for the text type.
Since version 9.3 Postgres has the #> and #>> operators. This operators allow the user to specify a path (using an array of text) inside jsonb column to get the value.
You could use this operator to achieve your goal in a simpler way.
CREATE INDEX ON mytable((data #>> '{identification, registration_number}'));

How to update a text[] field with a string

I have a table with a field called tags which can contain any number of strings:
Table "public.page"
Column | Type | Modifiers
----------------------+--------------------------+----------------------------------
tags | text[] | not null default ARRAY[]::text[]
I want to add a string to the tags field - but I can't seem to get the concat function to work for me. I've tried:
update page set tags=concat('My New String',tags);
ERROR: function concat(unknown, text[]) does not exist
LINE 1: update page set tags=concat('My New String',tags) where ...
^
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
and
update page set tags=('My New String'||tags);
ERROR: operator is not unique: unknown || text[]
LINE 1: update page set tags = ('My New String' || tags) where w...
^
HINT: Could not choose a best candidate operator. You might need to add explicit type casts.
Any ideas?
In PostgreSQL's type system, the literal 'My New String' is not a varchar or text value, but a literal of type unknown, which can be processed as any type. (For instance, the literal for a date could be '2013-08-29'; this would not be processed as a varchar and then converted to date, it would be interpreted as a "date literal" at a very low level.)
Often, PostgreSQL can deduce the type automatically, but when it can't, you need to use one of the following to tell it that you want the literal to be treated as text:
text 'My New String' (SQL standard literal syntax)
Cast('My New String' as text) (SQL standard cast syntax, but not really a cast in this context)
'My New String'::text (PostgreSQL non-standard cast syntax, but quite readable)
In your case , the error message operator is not unique: unknown || text[] is saying that there are multiple types that Postgres could interpret the literal as, each with their own definition of the || operator.
You therefore need something like this (I've removed the unnecessary parentheses):
update page set tags = 'My New String'::text || tags;
Did you try || to concatenate?
select array['abc','def']::text[] || 'qwerty'::text;
http://www.postgresql.org/docs/current/static/functions-array.html#ARRAY-OPERATORS-TABLE
Note: this answer was in response to the OP's original (unedited) question. Other answers contain more detail relevant to the updated question.