PostgreSQL: compare jsons [duplicate] - json

This question already has an answer here:
Operator does not exist: json = json
(1 answer)
Closed 3 years ago.
As known, at the moment PostgreSQL has no method to compare two json values. The comparison like json = json doesn't work. But what about casting json to text before?
Then
select ('{"x":"a", "y":"b"}')::json::text =
('{"x":"a", "y":"b"}')::json::text
returns true
while
select ('{"x":"a", "y":"b"}')::json::text =
('{"x":"a", "y":"d"}')::json::text
returns false
I tried several variants with more complex objects and it works as expected.
Are there any gotchas in this solution?
UPDATE:
The compatibility with v9.3 is needed

You can also use the #> operator. Let's say you have A and B, both JSONB objects, so A = B if:
A #> B AND A <# B
Read more here: https://www.postgresql.org/docs/current/functions-json.html

Yes there are multiple problem with your approach (i.e. converting to text). Consider the following example
select ('{"x":"a", "y":"b"}')::json::text = ('{"y":"b", "x":"a"}')::json::text;
This is like your first example example, except that I flipped the order of the x and y keys for the second object, and now it returns false, even thought the objects are equal.
Another issue is that json preserves white space, so
select ('{"x":"a", "y":"b"}')::json::text = ('{ "x":"a", "y":"b"}')::json::text;
returns false just because I added a space before the x in the second object.
A solution that works with v9.3 is to use the json_each_text function to expand the two JSON objects into tables, and then compare the two tables, e.g. like so:
SELECT NOT exists(
SELECT
FROM json_each_text(('{"x":"a", "y":"b"}')::json) t1
FULL OUTER JOIN json_each_text(('{"y":"b", "x":"a"}')::json) t2 USING (key)
WHERE t1.value<>t2.value OR t1.key IS NULL OR t2.key IS NULL
)
Note that this only works if the two JSON values are objects where for each key, the values are strings.
The key is in the query inside the exists: In that query we match all keys from the first JSON objects with the corresponding keys in the second JSON object. Then we keep only the rows that correspond to one of the following two cases:
a key exists in both JSON objects but the corresponding values are different
a key exists only in one of the two JSON objects and not the other
These are the only cases that "witness" the inequality of the two objects, hence we wrap everything with a NOT exists(...), i.e. the objects are equal if we didn't find any witnesses of inequality.
If you need to support other types of JSON values (e.g. arrays, nested objects, etc), you can write a plpgsql function based on the above idea.

Most notably A #> B AND B #> A will signify TRUE if they are both equal JSONB objects.
However, be careful when assuming that it works for all kinds of JSONB values, as demonstrated with the following query:
select
old,
new,
NOT(old #> new AND new #> old) as changed
from (
values
(
'{"a":"1", "b":"2", "c": {"d": 3}}'::jsonb,
'{"b":"2", "a":"1", "c": {"d": 3, "e": 4}}'::jsonb
),
(
'{"a":"1", "b":"2", "c": {"d": 3, "e": 4}}'::jsonb,
'{"b":"2", "a":"1", "c": {"d": 3}}'::jsonb
),
(
'[1, 2, 3]'::jsonb,
'[3, 2, 1]'::jsonb
),
(
'{"a": 1, "b": 2}'::jsonb,
'{"b":2, "a":1}'::jsonb
),
(
'{"a":[1, 2, 3]}'::jsonb,
'{"b":[3, 2, 1]}'::jsonb
)
) as t (old, new)
Problems with this approach are that JSONB arrays are not compared correctly, as in JSON [1, 2, 3] != [3, 2, 1] but Postgres returns TRUE nevertheless.
A correct solution will recursively iterate through the contents of the json and comparing arrays and objects differently. I have quickly built a set of functions that accomplishes just that.
Use them like SELECT jsonb_eql('[1, 2, 3]'::jsonb, '[3, 2, 1]'::jsonb) (the result is FALSE).
CREATE OR REPLACE FUNCTION jsonb_eql (a JSONB, b JSONB) RETURNS BOOLEAN AS $$
DECLARE
BEGIN
IF (jsonb_typeof(a) != jsonb_typeof(b)) THEN
RETURN FALSE;
ELSE
IF (jsonb_typeof(a) = 'object') THEN
RETURN jsonb_object_eql(a, b);
ELSIF (jsonb_typeof(a) = 'array') THEN
RETURN jsonb_array_eql(a, b);
ELSIF (COALESCE(jsonb_typeof(a), 'null') = 'null') THEN
RETURN COALESCE(a, 'null'::jsonb) = 'null'::jsonb AND COALESCE(b, 'null'::jsonb) = 'null'::jsonb;
ELSE
RETURN coalesce(a = b, FALSE);
END IF;
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION jsonb_object_eql (a JSONB, b JSONB) RETURNS BOOLEAN AS $$
DECLARE
_key_a text;
_val_a jsonb;
_key_b text;
_val_b jsonb;
BEGIN
IF (jsonb_typeof(a) != jsonb_typeof(b)) THEN
RETURN FALSE;
ELSIF (jsonb_typeof(a) != 'object') THEN
RETURN jsonb_eql(a, b);
ELSE
FOR _key_a, _val_a, _key_b, _val_b IN
SELECT t1.key, t1.value, t2.key, t2.value FROM jsonb_each(a) t1
LEFT OUTER JOIN (
SELECT * FROM jsonb_each(b)
) t2 ON (t1.key = t2.key)
LOOP
IF (_key_a != _key_b) THEN
RETURN FALSE;
ELSE
RETURN jsonb_eql(_val_a, _val_b);
END IF;
END LOOP;
RETURN a = b;
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION jsonb_array_eql (a JSONB, b JSONB) RETURNS BOOLEAN AS $$
DECLARE
_val_a jsonb;
_val_b jsonb;
BEGIN
IF (jsonb_typeof(a) != jsonb_typeof(b)) THEN
RETURN FALSE;
ELSIF (jsonb_typeof(a) != 'array') THEN
RETURN jsonb_eql(a, b);
ELSE
FOR _val_a, _val_b IN
SELECT jsonb_array_elements(a), jsonb_array_elements(b)
LOOP
IF (NOT(jsonb_eql(_val_a, _val_b))) THEN
RETURN FALSE;
END IF;
END LOOP;
RETURN TRUE;
END IF;
END;
$$ LANGUAGE plpgsql;

Related

Use varargin for multiple arguments with default values in MATLAB

Is there a way to supply arguments using varargin in MATLAB in the following manner?
Function
func myFunc(varargin)
if a not given as argument
a = 2;
if b not given as argument
b = 2;
if c not given as argument
c = a+b;
d = 2*c;
end
I want to call the above function once with b = 3 and another time while the previous one is running in the same command window with a = 3 and c = 3 and letting b take the default value in the function this time. How can it be done using varargin?
Here's the latest and greatest way to write the function (using arguments blocks from R2019b)
function out = someFcn(options)
arguments
options.A = 3;
options.B = 7;
options.C = [];
end
if isempty(options.C)
options.C = options.A + options.B;
end
out = options.A + options.B + options.C;
end
Note that this syntax does not allow you to say options.C = options.A + options.B directly in the arguments block.
In MATLAB < R2021a, you call this like so
someFcn('A', 3)
In MATLAB >= R2021a, you can use the new name=value syntax
someFcn(B = 7)
Here are two ways to do this which have been available since 2007a (i.e. a long time!). For a much newer approach, see Edric's answer.
Use nargin and ensure your inputs are always in order
Use name-value pairs and an input parser
nargin: slightly simpler but relies on consistent input order
function myFunc( a, b, c )
if nargin < 1 || isempty(a)
a = 2;
end
if nargin < 2 || isempty(b)
b = 2;
end
if nargin < 3 || isempty(c)
c = a + b;
end
end
Using the isempty check you can optionally provide just later arguments, for example myFunc( [], 4 ) would just set b=4 and use the defaults otherwise.
inputParser: more flexible but can't directly handle the c=a+b default
function myFunc( varargin )
p = inputParser;
p.addOptional( 'a', 2 );
p.addOptional( 'b', 2 );
p.addOptional( 'c', NaN ); % Can't default to a+b, default to NaN
p.parse( varargin{:} );
a = p.Results.a;
b = p.Results.b;
c = p.Results.c;
if isnan(c) % Handle the defaulted case
c = a + b;
end
end
This would get used like myFunc( 'b', 4 );. This approach is also agnostic to the input order because of the name-value pairs, so you can also do something like myFunc( 'c', 3, 'a', 1 );

How to truncate double precision value in PostgreSQL by keeping exactly first two decimals?

I'm trying to truncate double precision value when I'm build json using json_build_object() function in PostgreSQL 11.8 but with no luck. To be more precise I'm trying to truncate 19.9899999999999984 number to ONLY two decimals but making sure it DOES NOT round it to 20.00 (which is what it does), but to keep it at 19.98.
BTW, what I've tried so far was to use:
1) TRUNC(found_book.price::numeric, 2) and I get value 20.00
2) ROUND(found_book.price::numeric, 2) and I get value 19.99 -> so far this is closesest value but not what I need
3) ROUND(found_book.price::double precision, 2) and I get
[42883] ERROR: function round(double precision, integer) does not exist
Also here is whole code I'm using:
create or replace function public.get_book_by_book_id8(b_id bigint) returns json as
$BODY$
declare
found_book book;
book_authors json;
book_categories json;
book_price double precision;
begin
-- Load book data:
select * into found_book
from book b2
where b2.book_id = b_id;
-- Get assigned authors
select case when count(x) = 0 then '[]' else json_agg(x) end into book_authors
from (select aut.*
from book b
inner join author_book as ab on b.book_id = ab.book_id
inner join author as aut on ab.author_id = aut.author_id
where b.book_id = b_id) x;
-- Get assigned categories
select case when count(y) = 0 then '[]' else json_agg(y) end into book_categories
from (select cat.*
from book b
inner join category_book as cb on b.book_id = cb.book_id
inner join category as cat on cb.category_id = cat.category_id
where b.book_id = b_id) y;
book_price = trunc(found_book.price, 2);
-- Build the JSON response:
return (select json_build_object(
'book_id', found_book.book_id,
'title', found_book.title,
'price', book_price,
'amount', found_book.amount,
'is_deleted', found_book.is_deleted,
'authors', book_authors,
'categories', book_categories
));
end
$BODY$
language 'plpgsql';
select get_book_by_book_id8(186);
How do I achieve to keep EXACTLY ONLY two FIRST decimal digits 19.98 (any suggestion/help is greatly appreciated)?
P.S. PostgreSQL version is 11.8
In PostgreSQL 11.8 or 12.3 I cannot reproduce:
# select trunc('19.9899999999999984'::numeric, 2);
trunc
-------
19.98
(1 row)
# select trunc(19.9899999999999984::numeric, 2);
trunc
-------
19.98
(1 row)
# select trunc(19.9899999999999984, 2);
trunc
-------
19.98
(1 row)
Actually I can reproduce with the right type and a special setting:
# set extra_float_digits=0;
SET
# select trunc(19.9899999999999984::double precision::text::numeric, 2);
trunc
-------
19.99
(1 row)
And a possible solution:
# show extra_float_digits;
extra_float_digits
--------------------
3
(1 row)
select trunc(19.9899999999999984::double precision::text::numeric, 2);
trunc
-------
19.98
(1 row)
But note that:
Note: The extra_float_digits setting controls the number of extra
significant digits included when a floating point value is converted
to text for output. With the default value of 0, the output is the
same on every platform supported by PostgreSQL. Increasing it will
produce output that more accurately represents the stored value, but
may be unportable.
As #pifor suggested I've managed to get it done by directly passing trunc(found_book.price::double precision::text::numeric, 2) as value in json_build_object like this:
json_build_object(
'book_id', found_book.book_id,
'title', found_book.title,
'price', trunc(found_book.price::double precision::text::numeric, 2),
'amount', found_book.amount,
'is_deleted', found_book.is_deleted,
'authors', book_authors,
'categories', book_categories
)
Using book_price = trunc(found_book.price::double precision::text::numeric, 2); and passing it as value for 'price' key didn't work.
Thank you for your help. :)

DB2: Create Function - Scale

i have a table popo:
No|Id|Values.
1|X|321225.4775 -> Scale:4.
2|Y|321235.2115 -> Scale:4.
3|Z|12123.12321 -> Scale:5.
4|A| -> NULL.
5|B|12321 -> Scale:0.
i want to Flaging when column "Values" have a data where scale > 4.
below is my current function script.
CREATE OR REPLACE
FUNCTION "SCHEME"."VALIDATION01" (A VARCHAR(500)) RETURNS INT
LANGUAGE SQL
BEGIN
DECLARE ASD DEC;
DECLARE A DEC;
SET ASD = CAST(A AS DEC(20,4));
IF A = ASD THEN RETURN NULL;
ELSEIF A IS NULL THEN RETURN NULL;
ELSE RETURN 1;
END IF;
END
Output:
[null]
[null]
[null]
[null]
[null]
i want output is:
[null]
[null]
1
[null]
[null]
can you help me?
Try the following:
CREATE OR REPLACE
FUNCTION "SCHEME"."VALIDATION01" (A VARCHAR(500)) RETURNS INT
LANGUAGE SQL
RETURN CASE WHEN A <> DEC(A, 20, 4) THEN 1 END;
The CASE statement returns 1, if A IS NOT NULL AND the original value is not equal to this value casted to the DECIMAL(20, 4) data type. It returns NULL otherwise.
It's the user's responsibility to call this function with proper string parameter to avoid string to number data type conversion errors.

Does mysql have function for returning a number with ordinal suffix?

Basically I'm looking for something like
SELECT ordinal(my_number) FROM my_table
which would return
1st
11th
1071st
...
etc
but preferrably without the use of a stored procedure
I don't know of a built-in function but it's pretty easy to write:
SELECT
CONCAT(my_number, CASE
WHEN my_number%100 BETWEEN 11 AND 13 THEN "th"
WHEN my_number%10 = 1 THEN "st"
WHEN my_number%10 = 2 THEN "nd"
WHEN my_number%10 = 3 THEN "rd"
ELSE "th"
END)
FROM my_table;
mysql doesn't have support for this. You'll have to handle the strings in whichever language you are getting the mysql data from.
Based on Ken's code, a custom MySQL function would be as follows:
DELIMITER $$
CREATE FUNCTION ordinal(number BIGINT)
RETURNS VARCHAR(64)
DETERMINISTIC
BEGIN
DECLARE ord VARCHAR(64);
SET ord = (SELECT CONCAT(number, CASE
WHEN number%100 BETWEEN 11 AND 13 THEN "th"
WHEN number%10 = 1 THEN "st"
WHEN number%10 = 2 THEN "nd"
WHEN number%10 = 3 THEN "rd"
ELSE "th"
END));
RETURN ord;
END$$
DELIMITER ;
Then it can be used as:
SELECT ordinal(1) -- 1st
SELECT ordinal(11) -- 11th
SELECT ordinal(21) -- 21st
SELECT ordinal(my_number) FROM my_table
It is possible in MySQL using the string functions but it gets messy real fast. You'd better just do the suffix in the language you're using. For example, in PHP you could do something like this:
function ordSuffix($num) {
if(empty($num) || !is_numeric($num) || $num == 0) return $num;
$lastNum = substr($num, -1);
$suffix = 'th';
if($lastNum == 1 && $num != 11) { $suffix = 'st'; }
elseif($lastNum == 2 && $num != 12) { $suffix = 'nd'; }
elseif($lastNum == 3 && $num != 13) { $suffix = 'rd'; }
return $num.$suffix;
}
echo ordSuffix(4); // 4th
echo ordSuffix(1); // 1st
echo ordSuffix(12); // 12th
echo ordSuffix(1052); // 1052nd
I found a way that works for me but its a bit of a hack
DATE_FORMAT(CONCAT('2010-01-', my_number), '%D')
That works because currently the number I'm looking at never gets above 25. But it doesn't generalize well so someone might be entertained by this:
CONCAT(
IF(my_number % 100 BETWEEN 11 AND 13,
FLOOR(my_number / 100),
FLOOR(my_number / 10)),
DATE_FORMAT(
CONCAT('2010-01-',
IF(my_number % 100 BETWEEN 11 AND 13
my_number % 100,
my_number % 10)),
'%D'))
But that's a lot of work just to get at the DATE_FORMAT functionality when Ken's code is simpler.

MYSQL: self written string-manipulation function returns unexpected result

I'm trying to implement a MYSQL function MY_LEFT_STR(STRING x,INT position) in such a way that
MY_LEFT_STR('HELLO', 4) => returns 'HELL' (same as internal LEFT function)
MY_LEFT_STR('HELLO',-1) => returns 'HELL'
DROP FUNCTION IF EXISTS MY_LEFT_STR;
CREATE FUNCTION MY_LEFT_STR(
in_str VARCHAR(255),
pos INT
)
RETURNS VARCHAR(255)
BEGIN
IF (pos < 0) THEN
RETURN LEFT(in_str,LENGTH(in_str) - pos);
ELSE
RETURN LEFT(in_str,pos);
END IF;
END;
the result is
select left_str('HELLO', 4) as A
, left_str('HELLO',-1) as B
, left('HELLO',length('HELLO')-1) as C
from dual
+-----+-----+-----+
| A | B | C |
+-----+-----+-----+
|HELL |HELLO|HELL |
+-----+-----+-----+
QUESTION What is wrong with my function declaration? (Besides a generall lack of testing for bordercases like MY_LEFT_STR('A',-4) ...
ANSWER: so embarassing ... the answer lies in the double negative for pos=-1 in
RETURN LEFT(in_str,LENGTH(in_str) - pos);
this should be
RETURN LEFT(in_str,LENGTH(in_str) + pos);
Here's a clue: What's the result of LENGTH(in_str) - (-1)?
When pos is negative, then LENGTH(in_str) - pos yields a number longer than the length of the string. So LEFT() is bound to return the whole string, because you're asking for more characters than the total length of the string.
RETURN LEFT(in_str,LENGTH(in_str) - pos);
If pos is negative, won't LENGTH(in_str) - pos give you (for your example):
LENGTH(HELLO) - (-1) = 6?