Repository: edgedb/edgedb

Issue:
Provide a way to construct arbitrary json objects
The only way to produce json objects is by casting from objects and named tuples and converting a string from `to_json`. 

I think we want something like
`function json_object_pack(els: array<tuple<str, json>>) -> json` (though it could also be `SET OF` instead?)

Annoyingly, postgres's json construction functions aren't great from an "arbitrary input" perspective, but it should still be doable?


Discussion:
`json_object_agg` for consistency with `array_agg`, and have a `set of` input.
`json_object_pack` is better for consistency with our own JSON API: https://www.edgedb.com/docs/stdlib/json#function::std::json_object_unpack
Hmm. Not really.  We have `array_agg` and `array_unpack`
I produced this mess of an implementation:
```
create or replace function json_merge(objs jsonb[], lo int8, hi int8)
returns jsonb
as $function$
select (
    case when (lo + 1 = hi)
    then objs[lo]
    else json_merge(objs, lo, (lo+hi)/2) || json_merge(objs, (lo+hi)/2, hi)
    end
)
$function$
language sql;

create or replace function json_object_pack(
    els edgedbpub."79d8ede8-30f1-a805-fbc3-503ece3c9205_t"[])
returns jsonb
as $function$
select json_merge(body.args, 1, array_length(body.args, 1) + 1)
from (
    select array_agg(jsonb_build_object(x."0", x."1")) as args
    from unnest(els) as x
) as body
$function$
language sql;
```

... before I realized that `jsonb_object_agg` existed, but was listed on the aggregates doc page instead of the json one.

Which gives:
```
create or replace function json_object_pack(
    els edgedbpub."79d8ede8-30f1-a805-fbc3-503ece3c9205_t"[])
returns jsonb
as $function$
select jsonb_object_agg(x."0", x."1") as args
from unnest(els) as x
$function$
language sql;
```
I'd still prefer `json_object_pack` to be an aggregate (as opposed to array-taking).
Yeah, final signature is `function json_object_pack(els: SET OF tuple<str, json>) -> json`.

Doing it as an aggregate function will mean needing to special case it in the compiler, which is fine.
(See all the functions marked with `@_special_case` for various examples of special cased functions/operators.)

Resolving patch:
diff --git a/edb/pgsql/compiler/relgen.py b/edb/pgsql/compiler/relgen.py
--- a/edb/pgsql/compiler/relgen.py
+++ b/edb/pgsql/compiler/relgen.py
@@ -3483,6 +3483,49 @@ def process_set_as_exists_expr(
     return new_stmt_set_rvar(ir_set, stmt, ctx=ctx)
 
 
+@_special_case('std::json_object_pack')
+def process_set_as_json_object_pack(
+        ir_set: irast.Set, stmt: pgast.SelectStmt, *,
+        ctx: context.CompilerContextLevel) -> SetRVars:
+    expr = ir_set.expr
+    assert isinstance(expr, irast.FunctionCall)
+
+    ir_arg = expr.args[0].expr
+
+    # compile IR to pg AST
+    dispatch.visit(ir_arg, ctx=ctx)
+    arg_val = pathctx.get_path_value_var(ctx.rel, ir_arg.path_id, env=ctx.env)
+
+    # get first and the second fields of the tuple
+    if isinstance(arg_val, pgast.TupleVar):
+        keys = arg_val.elements[0].val
+        values = arg_val.elements[1].val
+    else:
+        keys = astutils.tuple_getattr(arg_val, ir_arg.typeref, "0")
+        values = astutils.tuple_getattr(arg_val, ir_arg.typeref, "1")
+
+    # construct the function call
+    set_expr = pgast.FuncCall(
+        name=("coalesce",),
+        args=[
+            pgast.FuncCall(name=("jsonb_object_agg",), args=[keys, values]),
+            pgast.TypeCast(
+                arg=pgast.StringConstant(val="{}"),
+                type_name=pgast.TypeName(name=('jsonb',)),
+            ),
+        ],
+    )
+
+    # declare that the 'aspect=value' of ir_set (original set)
+    # can be found by in ctx.rel, by using set_expr
+    pathctx.put_path_value_var_if_not_exists(
+        ctx.rel, ir_set.path_id, set_expr, env=ctx.env
+    )
+
+    # return subquery as set_rvar
+    return new_stmt_set_rvar(ir_set, stmt, ctx=ctx)
+
+
 def build_array_expr(
         ir_expr: irast.Base,
         elements: List[pgast.BaseExpr], *,
diff --git a/edb/tools/pygments/edgeql/meta.py b/edb/tools/pygments/edgeql/meta.py
--- a/edb/tools/pygments/edgeql/meta.py
+++ b/edb/tools/pygments/edgeql/meta.py
@@ -299,6 +299,7 @@ class EdgeQL:
         "json_array_unpack",
         "json_get",
         "json_object_unpack",
+        "json_object_pack",
         "json_typeof",
         "len",
         "lg",


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
@@ -832,6 +832,43 @@ async def test_edgeql_json_object_unpack_05(self):
             [1, 2]
         )
 
+    async def test_edgeql_json_object_pack(self):
+        await self.assert_query_result(
+            r'''
+                select std::json_object_pack({
+                    ("foo", to_json("1")),
+                    ("bar", to_json("null")),
+                    ("baz", to_json("[]"))
+                })
+            ''',
+            [{'bar': None, 'baz': [], 'foo': 1}],
+            ['{"bar": null, "baz": [], "foo": 1}'],
+        )
+
+        await self.assert_query_result(
+            r"""
+                select std::json_object_pack(
+                    <tuple<str, json>>{}
+                )
+            """,
+            [{}],
+            ["{}"],
+        )
+
+        await self.assert_query_result(
+            r"""
+                select std::json_object_pack(
+                    array_unpack([
+                        ('foo', <json>1),
+                        ('bar', <json>2),
+                        ('baz', <json>3)
+                    ])
+                )
+            """,
+            [{"bar": 2, "baz": 3, "foo": 1}],
+            ['{"bar": 2, "baz": 3, "foo": 1}'],
+        )
+
     async def test_edgeql_json_get_01(self):
         await self.assert_query_result(
             r'''