Repository: jqlang/jq

Problem statement:
Recursive merge
The `+` operator performs merge on objects. However it merges only on top-level.

A more powerful recursive merge that merges any other same-named objects encountered would be handy, and impossible to replicate with different logics.



Hints:

Patch:
diff --git a/builtin.c b/builtin.c
index 61a9fedd31..9881dc1cd8 100644
--- a/builtin.c
+++ b/builtin.c
@@ -172,6 +172,8 @@ static jv f_multiply(jv input, jv a, jv b) {
       return jv_null();
     }
     return res;
+  } else if (jv_get_kind(a) == JV_KIND_OBJECT && jv_get_kind(b) == JV_KIND_OBJECT) {
+    return jv_object_merge_recursive(a, b);
   } else {
     return type_error2(a, b, "cannot be multiplied");
   }  
diff --git a/docs/content/3.manual/manual.yml b/docs/content/3.manual/manual.yml
index a175b052e7..b30ba6c49a 100644
--- a/docs/content/3.manual/manual.yml
+++ b/docs/content/3.manual/manual.yml
@@ -479,7 +479,8 @@ sections:
           - **Objects** are added by merging, that is, inserting all
               the key-value pairs from both objects into a single
               combined object. If both objects contain a value for the
-              same key, the object on the right of the `+` wins.
+              same key, the object on the right of the `+` wins. (For
+              recursive merge use the `*` operator.)
           
           `null` can be added to any value, and returns the other
           value unchanged.
@@ -527,6 +528,11 @@ sections:
           Dividing a string by another splits the first using the second
           as separators.
 
+          Multiplying two objects will merge them recursively: this works
+          like addition but if both objects contain a value for the
+          same key, and the values are objects, the two are merged with
+          the same strategy.
+
         examples:
           - program: '10 / . * 3'
             input: 5
@@ -534,6 +540,9 @@ sections:
           - program: '. / ", "'
             input: '"a, b,c,d, e"'
             output: ['["a","b,c,d","e"]']
+          - program: '{"k": {"a": 1, "b": 2}} * {"k": {"a": 0,"c": 3}}'
+            input: 'null'
+            output: ['{"k": {"a": 0, "b": 2, "c": 3}}']
 
       - title: `length`
         body: |
diff --git a/jv.c b/jv.c
index 2530d7fcd7..e969b120f2 100644
--- a/jv.c
+++ b/jv.c
@@ -1106,6 +1106,25 @@ jv jv_object_merge(jv a, jv b) {
   return a;
 }
 
+jv jv_object_merge_recursive(jv a, jv b) {
+  assert(jv_get_kind(a) == JV_KIND_OBJECT);
+  assert(jv_get_kind(b) == JV_KIND_OBJECT);
+
+  jv_object_foreach(b, k, v) {
+    jv elem = jv_object_get(jv_copy(a), jv_copy(k));
+    if (jv_is_valid(elem) &&
+        jv_get_kind(elem) == JV_KIND_OBJECT &&
+        jv_get_kind(v) == JV_KIND_OBJECT) {
+      a = jv_object_set(a, k, jv_object_merge_recursive(elem, v));
+    } else {
+      jv_free(elem);
+      a = jv_object_set(a, k, v);
+    }
+  }
+  jv_free(b);
+  return a;
+}
+
 int jv_object_contains(jv a, jv b) {
   assert(jv_get_kind(a) == JV_KIND_OBJECT);
   assert(jv_get_kind(b) == JV_KIND_OBJECT);
diff --git a/jv.h b/jv.h
index 6e82b4c1e5..f58690a750 100644
--- a/jv.h
+++ b/jv.h
@@ -104,6 +104,7 @@ jv jv_object_set(jv object, jv key, jv value);
 jv jv_object_delete(jv object, jv key);
 int jv_object_length(jv object);
 jv jv_object_merge(jv, jv);
+jv jv_object_merge_recursive(jv, jv);
 
 int jv_object_iter(jv);
 int jv_object_iter_next(jv, int);


Test patch:
diff --git a/tests/all.test b/tests/all.test
index e2b77ed4c7..ff7245be7b 100644
--- a/tests/all.test
+++ b/tests/all.test
@@ -779,3 +779,15 @@ null
 map([1,2][0:.])
 [-1, 1, 2, 3, 1000000000000000000]
 [[1], [1], [1,2], [1,2], [1,2]]
+
+{"k": {"a": 1, "b": 2}} * .
+{"k": {"a": 0,"c": 3}}
+{"k": {"a": 0, "b": 2, "c": 3}}
+
+{"k": {"a": 1, "b": 2}, "hello": {"x": 1}} * .
+{"k": {"a": 0,"c": 3}, "hello": 1}
+{"k": {"a": 0, "b": 2, "c": 3}, "hello": 1}
+
+{"k": {"a": 1, "b": 2}, "hello": 1} * .
+{"k": {"a": 0,"c": 3}, "hello": {"x": 1}}
+{"k": {"a": 0, "b": 2, "c": 3}, "hello": {"x": 1}}