Repository: edgedb/edgedb

Issue:
Consider implementing ++ operator for JSON
Possible behaviour:

```
db> SELECT to_json('[1,2]') ++ to_json('[3]') = to_json('[1,2,3]');
db> SELECT to_json('{"a": 1}') ++ to_json('{"b":2}') = to_json('{"a":1, "b":2}');
db> SELECT to_json('"123"') ++ to_json('"456"') = to_json('"123456"');
```


Discussion:
@elprans green light for this?
Yes.  This should be easy to implement since there is an equivalent operator in Postgres.
@ambv @RobertoPrevato this is a delightful feature to work on!
The SQL operator `||` doesn't quite match the desired behavior, and has pretty zany behavior in general:
 * If both arguments are objects, it merges them
 * Otherwise it does a list concatenation, treating any non-list as a singleton list

I think we probably want to error on types that don't make sense. So I think our algorithm is:
 * If both types are list, use `||`
 * If both types are object use `||`
 * If both types are string, extract the string values, concatenate them, convert back to json
 * Otherwise error

We have a `jsonb_extract_scalar` function that can convert json to a string, though probably the type checking could be elided in this case, since there would be type dispatch on the outside.

Probably the best approach would be to implement a function in SQL in `metaschema.py` that performs the operation.
@msullivan thanks for these advices! I'm just curious, why is it better to implement this operator as a function in the `metaschema` module instead of using `USING SQL` approach directly? I defined this operator like this and it works, is there something wrong with this implementation?
```
CREATE INFIX OPERATOR
std::`++` (l: std::json, r: std::json) -> std::json {
    CREATE ANNOTATION std::identifier := 'concatenate';
    CREATE ANNOTATION std::description := 'Concatenate two JSON values into a new JSON value.';
    SET volatility := 'Stable';
    USING SQL $$
    SELECT (
        CASE WHEN jsonb_typeof("l") = 'array' AND jsonb_typeof("r") = 'array' THEN
            "l" || "r"
        WHEN jsonb_typeof("l") = 'object' AND jsonb_typeof("r") = 'object' THEN
            "l" || "r"
        WHEN jsonb_typeof("l") = 'string' AND jsonb_typeof("r") = 'string' THEN
            to_jsonb(("l"#>>'{}') || ("r"#>>'{}'))
        ELSE
            edgedb.raise(
                NULL::jsonb,
                'invalid_parameter_value',
                msg => 'invalid JSON values for ++ operator',
                detail => (
                    '{"hint":"Supported JSON types for concatenation: '
                    || 'array ++ array, object ++ object, string ++ string."}'
                )
            )
        END
    )
    $$;
};
```

Results:
```
edgedb> SELECT to_json('[1, 2]') ++ to_json('[3]');
{"[1, 2, 3]"}
edgedb> SELECT to_json('{"a": 1}') ++ to_json('{"b": 2}');
{"{\"a\": 1, \"b\": 2}"}
edgedb> SELECT to_json('{"a": 1, "b": 2}') ++ to_json('{"b": 3}');
{"{\"a\": 1, \"b\": 3}"}
edgedb> SELECT to_json('"123"') ++ to_json('"456"');
{"\"123456\""}
edgedb> SELECT to_json('"123"') ++ to_json('42');
InvalidValueError: invalid JSON values for ++ operator
```
@nsidnev I was wrong, there's no reason to put it in metaschema. I think I had been thinking that we inlined the bodies of operators defined with USING SQL, but we don't. That looks great.

Resolving patch:
diff --git a/edb/buildmeta.py b/edb/buildmeta.py
--- a/edb/buildmeta.py
+++ b/edb/buildmeta.py
@@ -38,7 +38,7 @@
 
 
 # Increment this whenever the database layout or stdlib changes.
-EDGEDB_CATALOG_VERSION = 2021_10_19_00_00
+EDGEDB_CATALOG_VERSION = 2021_10_24_00_00
 
 
 class MetadataError(Exception):


Test patch:
diff --git a/tests/test_edgeql_json.py b/tests/test_edgeql_json.py
--- a/tests/test_edgeql_json.py
+++ b/tests/test_edgeql_json.py
@@ -1659,3 +1659,32 @@ async def test_edgeql_json_long_shape(self):
             ''',
             {f'{{{", ".join(result)}}}'},
         )
+
+    async def test_edgeql_json_concatenate_01(self):
+        await self.assert_query_result(
+            r'''SELECT to_str(to_json('[1, 2]') ++ to_json('[3]'));''',
+            {'[1, 2, 3]'}
+        )
+
+        await self.assert_query_result(
+            r'''SELECT to_str(to_json('{"a": 1}') ++ to_json('{"b": 2}'));''',
+            {'{"a": 1, "b": 2}'}
+        )
+
+        await self.assert_query_result(
+            r'''SELECT to_str(to_json('{"a": 1}') ++ to_json('{"a": 2}'));''',
+            {'{"a": 2}'}
+        )
+
+        await self.assert_query_result(
+            r'''SELECT to_str(to_json('"123"') ++ to_json('"456"'));''',
+            {'"123456"'}
+        )
+
+    async def test_edgeql_json_concatenate_02(self):
+        async with self.assertRaisesRegexTx(
+                edgedb.InvalidValueError,
+                r"invalid JSON values for \+\+ operator"):
+            await self.con.query_json(
+                r'''SELECT to_str(to_json('"123"') ++ to_json('42'));'''
+            )