Remove "invalid concatenation of jsonb objects" error case.
authorTom Lane <[email protected]>
Mon, 21 Dec 2020 18:11:29 +0000 (13:11 -0500)
committerTom Lane <[email protected]>
Mon, 21 Dec 2020 18:11:50 +0000 (13:11 -0500)
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- to all supported branches.

Discussion: https://postgr.es/m/163099.1608312033@sss.pgh.pa.us

doc/src/sgml/func.sgml
src/backend/utils/adt/jsonfuncs.c
src/test/regress/expected/jsonb.out
src/test/regress/sql/jsonb.sql

index d5cd705eebb7f0562ab1a4f67739e8223aca45c2..2707e757ca08f91af9faac31b3e86983543c0287 100644 (file)
@@ -14715,8 +14715,12 @@ table2-mapping
        </para>
        <para>
         Concatenates two <type>jsonb</type> values.
-        Concatenating two objects generates an object with the union of their
+        Concatenating two arrays generates an array containing all the
+        elements of each input.  Concatenating two objects generates an
+        object containing the union of their
         keys, taking the second object's value when there are duplicate keys.
+        All other cases are treated by converting a non-array input into a
+        single-element array, and then proceeding as for two arrays.
         Does not operate recursively: only the top-level array or object
         structure is merged.
        </para>
@@ -14727,6 +14731,22 @@ table2-mapping
        <para>
         <literal>'{"a": "b"}'::jsonb || '{"c": "d"}'::jsonb</literal>
         <returnvalue>{"a": "b", "c": "d"}</returnvalue>
+       </para>
+       <para>
+        <literal>'[1, 2]'::jsonb || '3'::jsonb</literal>
+        <returnvalue>[1, 2, 3]</returnvalue>
+       </para>
+       <para>
+        <literal>'{"a": "b"}'::jsonb || '42'::jsonb</literal>
+        <returnvalue>[{"a": "b"}, 42]</returnvalue>
+       </para>
+       <para>
+        To append an array to another array as a single entry, wrap it
+        in an additional layer of array, for example:
+       </para>
+       <para>
+        <literal>'[1, 2]'::jsonb || jsonb_build_array('[3, 4]'::jsonb)</literal>
+        <returnvalue>[1, 2, [3, 4]]</returnvalue>
        </para></entry>
       </row>
 
index 7a2541507800660cbb20484fbdfe60cd1d6ada94..69100feab7c1c4a95329a1667ff0f4a4e9fc4001 100644 (file)
@@ -4690,11 +4690,14 @@ IteratorConcat(JsonbIterator **it1, JsonbIterator **it2,
    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)
    {
        /*
+        * Both inputs are objects.
+        *
         * Append all the tokens from v1 to res, except last WJB_END_OBJECT
         * (because res will not be finished yet).
         */
@@ -4703,18 +4706,18 @@ IteratorConcat(JsonbIterator **it1, JsonbIterator **it2,
            pushJsonbValue(state, r1, &v1);
 
        /*
-        * Append all the 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)
    {
+       /*
+        * Both inputs are arrays.
+        */
        pushJsonbValue(state, rk1, NULL);
 
        while ((r1 = JsonbIteratorNext(it1, &v1, true)) != WJB_END_ARRAY)
@@ -4731,46 +4734,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 ((r2 = JsonbIteratorNext(it_object, &v2, true)) != WJB_DONE)
-               pushJsonbValue(state, r2, r2 != WJB_END_OBJECT ? &v2 : NULL);
+       pushJsonbValue(state, WJB_BEGIN_OBJECT, NULL);
+       while ((r1 = JsonbIteratorNext(it1, &v1, true)) != WJB_DONE)
+           pushJsonbValue(state, r1, r1 != WJB_END_OBJECT ? &v1 : 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;
index a70cd0b7c13d853dcccf7c257931d2ac1f9f3e12..1e6c6ef200aaa1a2089bedf680c90ad2341b2057 100644 (file)
@@ -4111,9 +4111,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?       
 ----------------------
index 3e2b8f66df2b20b78d29f45b260d476989ca9acd..b6409767f6d73feb0bd9b79ed31c22c766766da6 100644 (file)
@@ -1056,6 +1056,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"]';