diff options
author | Tom Lane <tgl@sss.pgh.pa.us> | 2020-12-21 13:11:29 -0500 |
---|---|---|
committer | Tom Lane <tgl@sss.pgh.pa.us> | 2020-12-21 13:11:29 -0500 |
commit | 75c8ef5ae56c5d9fad594b65eaa3bb8024f7f24f (patch) | |
tree | 9886c769b4b280d939c1aabfe4130b41c5dc4930 | |
parent | b6efd8a6daa54b3f94af83767aeaaaeff7c27879 (diff) | |
download | postgresql-75c8ef5ae56c5d9fad594b65eaa3bb8024f7f24f.tar.gz postgresql-75c8ef5ae56c5d9fad594b65eaa3bb8024f7f24f.zip |
Remove "invalid concatenation of jsonb objects" error case.
The jsonb || jsonb operator arbitrarily rejected certain combinations
of scalar and non-scalar inputs, while being willing to concatenate
other combinations. This was of course quite undocumented. Rather
than trying to document it, let's just remove the restriction,
creating a uniform rule that unless we are handling an object-to-object
concatenation, non-array inputs are converted to one-element arrays,
resulting in an array-to-array concatenation. (This does not change
the behavior for any case that didn't throw an error before.)
Per complaint from Joel Jacobson. Back-patch to all supported branches.
Discussion: https://postgr.es/m/163099.1608312033@sss.pgh.pa.us
-rw-r--r-- | doc/src/sgml/func.sgml | 11 | ||||
-rw-r--r-- | src/backend/utils/adt/jsonfuncs.c | 83 | ||||
-rw-r--r-- | src/test/regress/expected/jsonb.out | 36 | ||||
-rw-r--r-- | src/test/regress/sql/jsonb.sql | 5 |
4 files changed, 85 insertions, 50 deletions
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index c123b7fc828..b7289d01d31 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -11455,10 +11455,13 @@ table2-mapping <note> <para> - The <literal>||</literal> operator concatenates the elements at the top level of - each of its operands. It does not operate recursively. For example, if - both operands are objects with a common key field name, the value of the - field in the result will just be the value from the right hand operand. + The <literal>||</literal> operator concatenates two JSON objects by + generating an object containing the union of their keys, taking the + second object's value when there are duplicate keys. All other cases + produce a JSON array: first, any non-array input is converted into a + single-element array, and then the two arrays are concatenated. + It does not operate recursively; only the top-level array or object + structure is merged. </para> </note> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 787bc54a9ec..860c46abdf0 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4583,36 +4583,39 @@ IteratorConcat(JsonbIterator **it1, JsonbIterator **it2, rk1, rk2; - r1 = rk1 = JsonbIteratorNext(it1, &v1, false); - r2 = rk2 = JsonbIteratorNext(it2, &v2, false); + rk1 = JsonbIteratorNext(it1, &v1, false); + rk2 = JsonbIteratorNext(it2, &v2, false); /* - * Both elements are objects. + * JsonbIteratorNext reports raw scalars as if they were single-element + * arrays; hence we only need consider "object" and "array" cases here. */ if (rk1 == WJB_BEGIN_OBJECT && rk2 == WJB_BEGIN_OBJECT) { /* - * Append the all tokens from v1 to res, except last WJB_END_OBJECT + * Both inputs are objects. + * + * Append all the tokens from v1 to res, except last WJB_END_OBJECT * (because res will not be finished yet). */ - pushJsonbValue(state, r1, NULL); + pushJsonbValue(state, rk1, NULL); while ((r1 = JsonbIteratorNext(it1, &v1, true)) != WJB_END_OBJECT) pushJsonbValue(state, r1, &v1); /* - * Append the all tokens from v2 to res, include last WJB_END_OBJECT - * (the concatenation will be completed). + * Append all the tokens from v2 to res, including last WJB_END_OBJECT + * (the concatenation will be completed). Any duplicate keys will + * automatically override the value from the first object. */ while ((r2 = JsonbIteratorNext(it2, &v2, true)) != WJB_DONE) res = pushJsonbValue(state, r2, r2 != WJB_END_OBJECT ? &v2 : NULL); } - - /* - * Both elements are arrays (either can be scalar). - */ else if (rk1 == WJB_BEGIN_ARRAY && rk2 == WJB_BEGIN_ARRAY) { - pushJsonbValue(state, r1, NULL); + /* + * Both inputs are arrays. + */ + pushJsonbValue(state, rk1, NULL); while ((r1 = JsonbIteratorNext(it1, &v1, true)) != WJB_END_ARRAY) { @@ -4628,48 +4631,40 @@ IteratorConcat(JsonbIterator **it1, JsonbIterator **it2, res = pushJsonbValue(state, WJB_END_ARRAY, NULL /* signal to sort */ ); } - /* have we got array || object or object || array? */ - else if (((rk1 == WJB_BEGIN_ARRAY && !(*it1)->isScalar) && rk2 == WJB_BEGIN_OBJECT) || - (rk1 == WJB_BEGIN_OBJECT && (rk2 == WJB_BEGIN_ARRAY && !(*it2)->isScalar))) + else if (rk1 == WJB_BEGIN_OBJECT) { - - JsonbIterator **it_array = rk1 == WJB_BEGIN_ARRAY ? it1 : it2; - JsonbIterator **it_object = rk1 == WJB_BEGIN_OBJECT ? it1 : it2; - - bool prepend = (rk1 == WJB_BEGIN_OBJECT); + /* + * We have object || array. + */ + Assert(rk2 == WJB_BEGIN_ARRAY); pushJsonbValue(state, WJB_BEGIN_ARRAY, NULL); - if (prepend) - { - pushJsonbValue(state, WJB_BEGIN_OBJECT, NULL); - while ((r1 = JsonbIteratorNext(it_object, &v1, true)) != WJB_DONE) - pushJsonbValue(state, r1, r1 != WJB_END_OBJECT ? &v1 : NULL); - - while ((r2 = JsonbIteratorNext(it_array, &v2, true)) != WJB_DONE) - res = pushJsonbValue(state, r2, r2 != WJB_END_ARRAY ? &v2 : NULL); - } - else - { - while ((r1 = JsonbIteratorNext(it_array, &v1, true)) != WJB_END_ARRAY) - pushJsonbValue(state, r1, &v1); + pushJsonbValue(state, WJB_BEGIN_OBJECT, NULL); + while ((r1 = JsonbIteratorNext(it1, &v1, true)) != WJB_DONE) + pushJsonbValue(state, r1, r1 != WJB_END_OBJECT ? &v1 : NULL); - pushJsonbValue(state, WJB_BEGIN_OBJECT, NULL); - while ((r2 = JsonbIteratorNext(it_object, &v2, true)) != WJB_DONE) - pushJsonbValue(state, r2, r2 != WJB_END_OBJECT ? &v2 : NULL); - - res = pushJsonbValue(state, WJB_END_ARRAY, NULL); - } + while ((r2 = JsonbIteratorNext(it2, &v2, true)) != WJB_DONE) + res = pushJsonbValue(state, r2, r2 != WJB_END_ARRAY ? &v2 : NULL); } else { /* - * This must be scalar || object or object || scalar, as that's all - * that's left. Both of these make no sense, so error out. + * We have array || object. */ - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("invalid concatenation of jsonb objects"))); + Assert(rk1 == WJB_BEGIN_ARRAY); + Assert(rk2 == WJB_BEGIN_OBJECT); + + pushJsonbValue(state, WJB_BEGIN_ARRAY, NULL); + + while ((r1 = JsonbIteratorNext(it1, &v1, true)) != WJB_END_ARRAY) + pushJsonbValue(state, r1, &v1); + + pushJsonbValue(state, WJB_BEGIN_OBJECT, NULL); + while ((r2 = JsonbIteratorNext(it2, &v2, true)) != WJB_DONE) + pushJsonbValue(state, r2, r2 != WJB_END_OBJECT ? &v2 : NULL); + + res = pushJsonbValue(state, WJB_END_ARRAY, NULL); } return res; diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index bd6fc2e47dd..e9f22a4279d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -3654,9 +3654,41 @@ select '{"a":"b"}'::jsonb || '[]'::jsonb; (1 row) select '"a"'::jsonb || '{"a":1}'; -ERROR: invalid concatenation of jsonb objects + ?column? +----------------- + ["a", {"a": 1}] +(1 row) + select '{"a":1}' || '"a"'::jsonb; -ERROR: invalid concatenation of jsonb objects + ?column? +----------------- + [{"a": 1}, "a"] +(1 row) + +select '[3]'::jsonb || '{}'::jsonb; + ?column? +---------- + [3, {}] +(1 row) + +select '3'::jsonb || '[]'::jsonb; + ?column? +---------- + [3] +(1 row) + +select '3'::jsonb || '4'::jsonb; + ?column? +---------- + [3, 4] +(1 row) + +select '3'::jsonb || '{}'::jsonb; + ?column? +---------- + [3, {}] +(1 row) + select '["a", "b"]'::jsonb || '{"c":1}'; ?column? ---------------------- diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index a1831adf33d..06c29168938 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -973,6 +973,11 @@ select '{"a":"b"}'::jsonb || '[]'::jsonb; select '"a"'::jsonb || '{"a":1}'; select '{"a":1}' || '"a"'::jsonb; +select '[3]'::jsonb || '{}'::jsonb; +select '3'::jsonb || '[]'::jsonb; +select '3'::jsonb || '4'::jsonb; +select '3'::jsonb || '{}'::jsonb; + select '["a", "b"]'::jsonb || '{"c":1}'; select '{"c": 1}'::jsonb || '["a", "b"]'; |