From 2022492f21ca65bd0eb7e73adb96648784bf2bd8 Mon Sep 17 00:00:00 2001
From: Michael Hoennig <michael@hoennig.de>
Date: Tue, 23 Aug 2022 09:36:39 +0200
Subject: [PATCH] implements jsonb-changes-delta

---
 .../db/changelog/004-jsonb-changes-delta.sql  | 134 ++++++++++++++++++
 .../db/changelog/db.changelog-master.yaml     |   2 +
 2 files changed, 136 insertions(+)
 create mode 100644 src/main/resources/db/changelog/004-jsonb-changes-delta.sql

diff --git a/src/main/resources/db/changelog/004-jsonb-changes-delta.sql b/src/main/resources/db/changelog/004-jsonb-changes-delta.sql
new file mode 100644
index 00000000..04ef1c18
--- /dev/null
+++ b/src/main/resources/db/changelog/004-jsonb-changes-delta.sql
@@ -0,0 +1,134 @@
+--liquibase formatted sql
+
+
+-- ============================================================================
+--changeset JSONB-CHANGES-DELTA:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+/*
+    Recursively compares two jsonb values and returns what has changed.
+    This is a kind of right sided json diff.
+ */
+
+create or replace function jsonb_changes_delta(oldJson jsonb, newJson jsonb)
+    returns jsonb
+    called on null input
+    language plpgsql as $$
+declare
+    diffJson       jsonb;
+    oldJsonElement record;
+begin
+    raise notice '>>> diffing: % % vs. % %', jsonb_typeof(oldJson), oldJson, jsonb_typeof(newJson), newJson;
+
+    if oldJson is null or jsonb_typeof(oldJson) = 'null' or
+        newJson is null or jsonb_typeof(newJson) = 'null' then
+        return newJson;
+    end if;
+
+    diffJson = newJson;
+    for oldJsonElement in select * from jsonb_each(oldJson)
+        loop
+            raise notice 'intermediate result: %', diffJson;
+            raise notice 'record: %', oldJsonElement;
+            if diffJson @> jsonb_build_object(oldJsonElement.key, oldJsonElement.value) then
+                raise notice 'ignoring equal: %', oldJsonElement.key;
+                diffJson = diffJson - oldJsonElement.key;
+            elsif diffJson ? oldJsonElement.key then
+                if jsonb_typeof(newJson -> (oldJsonElement.key)) = 'object' then
+                    raise notice 'diffing new: % -> %', oldJsonElement.key, newJson -> (oldJsonElement.key);
+                    diffJson = diffJson ||
+                               jsonb_build_object(oldJsonElement.key,
+                                                  jsonb_changes_delta(oldJsonElement.value, newJson -> (oldJsonElement.key)));
+                else
+                    raise notice 'not an object: %, leaving %', oldJsonElement.key, newJson -> (oldJsonElement.key);
+                end if;
+                continue;
+            else
+                raise notice 'nulling old: %', oldJsonElement.key;
+                diffJson = diffJson || jsonb_build_object(oldJsonElement.key, null);
+            end if;
+        end loop;
+    raise notice '<<< result: %', diffJson;
+    return diffJson;
+end; $$;
+
+/*
+    Tests jsonb_diff.
+ */
+do language plpgsql $$
+    declare
+        expected text;
+        actual   text;
+    begin
+
+        select jsonb_changes_delta(null::jsonb, null::jsonb) into actual;
+        if actual is not null then
+            raise exception 'jsonb_diff #1 failed:%    expected: %,%    actually:   %', E'\n', expected, E'\n', actual;
+        end if;
+
+        select jsonb_changes_delta(null::jsonb, '{"a":  "new"}'::jsonb) into actual;
+        expected := '{"a":  "new"}'::jsonb;
+        if actual <> expected then
+            raise exception 'jsonb_diff #2 failed:%    expected:   %,%    actual:  %', E'\n', expected, E'\n', actual;
+        end if;
+
+        select jsonb_changes_delta('{"a":  "old"}'::jsonb, '{"a":  "new"}'::jsonb) into actual;
+        expected := '{"a":  "new"}'::jsonb;
+        if actual <> expected then
+            raise exception 'jsonb_diff #3 failed:%    expected:   %,%    actual:  %', E'\n', expected, E'\n', actual;
+        end if;
+
+        select jsonb_changes_delta('{"a":  "old"}'::jsonb, '{"a":  "old"}'::jsonb) into actual;
+        expected := '{}'::jsonb;
+        if actual <> expected then
+            raise exception 'jsonb_diff #4 failed:%    expected:   %,%    actual:  %', E'\n', expected, E'\n', actual;
+        end if;
+
+        select jsonb_changes_delta(
+                       $json${
+                         "a": "same",
+                         "b": "old",
+                         "c": "set",
+                         "d": "set",
+                         "e": null,
+                         "i": {
+                           "x": "equal",
+                           "y": "old",
+                           "z": "old"
+                         },
+                         "j": {
+                           "k": "set"
+                         },
+                         "l": {
+                           "m": "equal",
+                           "n": null
+                         }
+                       }$json$::jsonb,
+                       $json${
+                         "a": "same",
+                         "b": "new",
+                         "c": null,
+                         "e": {
+                           "f": "set"
+                         },
+                         "g": "set",
+                         "i": {
+                           "x": "equal",
+                           "y": "new",
+                           "z": null
+                         },
+                         "j": null,
+                         "l": {
+                           "m": "equal",
+                           "n": null
+                         }
+                       }$json$::jsonb
+                   )
+            into actual;
+        expected :=
+                $json${"b": "new", "c": null, "d": null, "e": {"f": "set"}, "g": "set", "i": {"y": "new", "z": null}, "j": null}$json$;
+        if actual <> expected then
+            raise exception 'jsonb_diff #5 failed:%    expected: %,%    actual:   %', E'\n', expected, E'\n', actual;
+        end if;
+    end; $$;
+--//
+
diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml
index c7175eb0..929a6f33 100644
--- a/src/main/resources/db/changelog/db.changelog-master.yaml
+++ b/src/main/resources/db/changelog/db.changelog-master.yaml
@@ -5,6 +5,8 @@ databaseChangeLog:
         file: db/changelog/002-int-to-var.sql
     - include:
         file: db/changelog/003-random-in-range.sql
+    - include:
+        file: db/changelog/004-jsonb-changes-delta.sql
     - include:
         file: db/changelog/005-uuid-ossp-extension.sql
     - include: