You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ei...@apache.org on 2020/02/11 16:32:15 UTC

[couchdb] branch prototype/flattened-doc-storage created (now 1c97a07)

This is an automated email from the ASF dual-hosted git repository.

eiri pushed a change to branch prototype/flattened-doc-storage
in repository https://gitbox.apache.org/repos/asf/couchdb.git.


      at 1c97a07  Add migration from the old doc storage format

This branch includes the following new commits:

     new ba9a548  Add JSON explode/implode function
     new 3863f46  Fix invalid json object in fabric2_doc_crud test
     new 999686e  Use flattened JSON for store doc in fdb
     new 1c97a07  Add migration from the old doc storage format

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[couchdb] 01/04: Add JSON explode/implode function

Posted by ei...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

eiri pushed a commit to branch prototype/flattened-doc-storage
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit ba9a54836cf045588583a6c77445fb2c81eb9756
Author: Eric Avdey <ei...@eiri.ca>
AuthorDate: Fri Feb 7 12:00:45 2020 -0400

    Add JSON explode/implode function
---
 src/couch/src/couch_util.erl              | 71 ++++++++++++++++++++++
 src/couch/test/eunit/couch_util_tests.erl | 99 +++++++++++++++++++++++++++++++
 2 files changed, 170 insertions(+)

diff --git a/src/couch/src/couch_util.erl b/src/couch/src/couch_util.erl
index 260aac6..ad810a1 100644
--- a/src/couch/src/couch_util.erl
+++ b/src/couch/src/couch_util.erl
@@ -22,6 +22,7 @@
 -export([proplist_apply_field/2, json_apply_field/2]).
 -export([to_binary/1, to_integer/1, to_list/1, url_encode/1]).
 -export([json_encode/1, json_decode/1]).
+-export([json_explode/1, json_implode/1]).
 -export([verify/2,simple_call/2,shutdown_sync/1]).
 -export([get_value/2, get_value/3]).
 -export([reorder_results/2]).
@@ -515,6 +516,76 @@ json_decode(V) ->
             throw({invalid_json, Error})
     end.
 
+
+json_explode({[]}) ->
+    {};
+json_explode({V}) ->
+    json_explode(V);
+json_explode(V) when is_list(V), length(V) > 0 ->
+    json_explode(V, []);
+json_explode(V) ->
+    V.
+
+json_explode([], Acc) ->
+    lists:reverse(Acc);
+json_explode([{K, V} | Rest], Acc) ->
+    Acc1 = json_explode(V, [K], []),
+    json_explode(Rest, lists:append(Acc1, Acc));
+json_explode(L, Acc) when is_list(L) ->
+    {_, Acc1} = lists:foldl(fun(V, {K, A}) ->
+        A1 = json_explode(V, [K], []),
+        {K+1, lists:append(A1, A)}
+    end, {0, Acc}, L),
+    lists:reverse(Acc1).
+
+json_explode({[]}, KAcc, []) ->
+    [{lists:reverse(KAcc), {}}];
+json_explode({[]}, _KAcc, Acc) ->
+    Acc;
+json_explode({[{K, V} | Rest]}, KAcc, Acc) ->
+    Acc1 = json_explode(V, [K | KAcc], Acc),
+    json_explode({Rest}, KAcc, Acc1);
+json_explode(L, KAcc, Acc) when is_list(L), length(L) > 0 ->
+    {_, Acc1} = lists:foldl(fun(V, {K, A}) ->
+        A1 = json_explode(V, [K | KAcc], []),
+        {K+1, lists:append(A1, A)}
+    end, {0, Acc}, L),
+    Acc1;
+json_explode(V, KAcc, Acc) ->
+    V1 = {lists:reverse(KAcc), V},
+    [V1 | Acc].
+
+
+json_implode([{[K | _], _} | _] = L) ->
+    V = json_implode(L, []),
+    if is_number(K) -> V; true -> {V} end;
+json_implode({}) ->
+    {[]};
+json_implode(V) ->
+    V.
+
+json_implode([], Acc) ->
+    lists:reverse(Acc);
+json_implode([{[K1 | K2] = K, V} | Rest], Acc) ->
+    {V1, Rest1} = json_implode(K, V, Rest),
+    %% try to put it at the end of Rest1?
+    V2 = json_implode(V1),
+    V3 = if is_number(K1) -> V2; true -> {K1, V2} end,
+    json_implode(Rest1, [V3 | Acc]).
+
+json_implode([_K], V, Rest) ->
+    {V, Rest};
+json_implode([K1 | K2], V, Rest) ->
+    json_implode(K1, Rest, [{K2, V}], []).
+
+json_implode(_K, [], KAcc, RestAcc) ->
+    {lists:reverse(KAcc), lists:reverse(RestAcc)};
+json_implode(K, [{[K | K1], V} | Rest], KAcc, RestAcc) ->
+    json_implode(K, Rest, [{K1, V} | KAcc], RestAcc);
+json_implode(K, [V | Rest], KAcc, RestAcc) ->
+    json_implode(K, Rest, KAcc, [V | RestAcc]).
+
+
 verify([X|RestX], [Y|RestY], Result) ->
     verify(RestX, RestY, (X bxor Y) bor Result);
 verify([], [], Result) ->
diff --git a/src/couch/test/eunit/couch_util_tests.erl b/src/couch/test/eunit/couch_util_tests.erl
index b518028..97df15e 100644
--- a/src/couch/test/eunit/couch_util_tests.erl
+++ b/src/couch/test/eunit/couch_util_tests.erl
@@ -308,3 +308,102 @@ random_unicode_char() ->
         BC ->
             BC
     end.
+
+
+json_explode_test_() ->
+    {
+        setup,
+        fun explode_implode_setup/0,
+        fun(Cases) ->
+            lists:map(fun({Description, Before0, After}) ->
+                Before = couch_util:json_decode(Before0),
+                {Description,
+                ?_assertEqual(After, couch_util:json_explode(Before))}
+            end, Cases)
+        end
+    }.
+
+json_implode_test_() ->
+    {
+        setup,
+        fun explode_implode_setup/0,
+        fun(Cases) ->
+            lists:map(fun({Description, Before0, After}) ->
+                Before = couch_util:json_decode(Before0),
+                {Description,
+                ?_assertEqual(Before, couch_util:json_implode(After))}
+            end, Cases)
+        end
+    }.
+
+explode_implode_setup() ->
+    [
+        {"null", <<"null">>, null},
+        {"integer", <<"1">>, 1},
+        {"bool", <<"true">>, true},
+        {"string", <<"\"a\"">>, <<"a">>},
+        {"empty list", <<"[]">>, []},
+        {"one element list", <<"[\"a\"]">>, [{[0], <<"a">>}]},
+        {"two elements list", <<"[\"a\", 1]">>,
+            [{[0], <<"a">>}, {[1], 1}]},
+        {"list of lists", <<"[[\"a\", \"b\"], [1, 2, 3], [true]]">>,
+            [{[0, 0], <<"a">>}, {[0, 1], <<"b">>},
+            {[1, 0], 1}, {[1, 1], 2}, {[1, 2], 3}, {[2, 0], true}]},
+        {"deep list of lists", <<"[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]">>,
+            [{[0, 0, 0], 1}, {[0, 0, 1], 2}, {[0, 1, 0], 3}, {[0, 1, 1], 4},
+            {[1, 0, 0], 5}, {[1, 0, 1], 6}, {[1, 1, 0], 7}, {[1, 1, 1], 8}]},
+        {"empty object", <<"{}">>, {}},
+        {"one element simple object",
+            <<"{\"a\": 1}">>,
+            [{[<<"a">>], 1}]},
+        {"two elements simple object",
+            <<"{\"a\": 1, \"b\": 2}">>,
+            [{[<<"a">>], 1}, {[<<"b">>], 2}]},
+        {"one element deep object",
+            <<"{\"a\": {\"b\": {\"c\": 3, \"d\": 4}}}">>,
+            [{[<<"a">>, <<"b">>, <<"c">>], 3},
+                {[<<"a">>, <<"b">>, <<"d">>], 4}]},
+        {"two elements deep object",
+            <<"{\"a\": {\"b\": 2, \"c\": 3}, \"d\": {\"e\": 5, \"f\": 6}}">>,
+            [{[<<"a">>, <<"b">>], 2}, {[<<"a">>, <<"c">>], 3},
+                {[<<"d">>, <<"e">>], 5}, {[<<"d">>, <<"f">>], 6}]},
+        {"three elements mixed depth object",
+            <<"{\"a\": {\"b\": {\"c\": {\"d\": 4, \"e\": \"f\"}},"
+                "\"g\": 7}, \"h\": true, \"i\": {\"j\": null}}">>,
+            [{[<<"a">>, <<"b">>, <<"c">>, <<"d">>], 4},
+                {[<<"a">>, <<"b">>, <<"c">>, <<"e">>], <<"f">>},
+                {[<<"a">>, <<"g">>], 7}, {[<<"h">>], true},
+                {[<<"i">>, <<"j">>], null}]},
+        {"object of lists", <<"{\"a\": [1, 2], \"b\": [3, 4]}">>,
+            [{[<<"a">>, 0], 1}, {[<<"a">>, 1], 2},
+            {[<<"b">>, 0], 3}, {[<<"b">>, 1], 4}]},
+        {"list of objects",
+            <<"[{\"a\": 1, \"b\": 2}, {\"c\": 3, \"d\": 4}]">>,
+            [{[0, <<"a">>], 1}, {[0, <<"b">>], 2},
+            {[1, <<"c">>], 3}, {[1, <<"d">>], 4}]},
+        {"object of lists of objects",
+            <<"{\"a\": [{\"c\": 3, \"d\": 4}, {\"e\": 5, \"f\": 6}],"
+                "\"b\": [{\"g\": 7, \"h\": 8}, {\"i\": 9, \"j\": 10}]}">>,
+            [{[<<"a">>, 0, <<"c">>], 3}, {[<<"a">>, 0, <<"d">>], 4},
+            {[<<"a">>, 1, <<"e">>], 5}, {[<<"a">>, 1, <<"f">>], 6},
+            {[<<"b">>, 0, <<"g">>], 7}, {[<<"b">>, 0, <<"h">>], 8},
+            {[<<"b">>, 1, <<"i">>], 9}, {[<<"b">>, 1, <<"j">>], 10}]},
+        {"one element object with an empty list value",
+            <<"{\"a\": []}">>,
+            [{[<<"a">>], []}]},
+        {"two elements deep object with an empty list values",
+            <<"{\"a\": {\"b\": []}, \"c\": {\"d\": []}}">>,
+            [{[<<"a">>, <<"b">>], []}, {[<<"c">>, <<"d">>], []}]},
+        {"one element object with an empty object value",
+            <<"{\"a\": {}}">>,
+            [{[<<"a">>], {}}]},
+        {"two elements deep object with an empty object values",
+            <<"{\"a\": {\"b\": {}}, \"c\": {\"d\": {}}}">>,
+            [{[<<"a">>, <<"b">>], {}}, {[<<"c">>, <<"d">>], {}}]},
+        {"list with of empty lists", <<"[[], [], []]">>,
+            [{[0], []}, {[1], []}, {[2], []}]},
+        {"list with of empty objects", <<"[{}, {}, {}]">>,
+            [{[0], {}}, {[1], {}}, {[2], {}}]},
+        {"deep list of mixed values", <<"[1, [[], 2], [3, {}]]">>,
+            [{[0], 1}, {[1, 0], []}, {[1, 1], 2}, {[2, 0], 3}, {[2, 1], {}}]}
+    ].


[couchdb] 02/04: Fix invalid json object in fabric2_doc_crud test

Posted by ei...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

eiri pushed a commit to branch prototype/flattened-doc-storage
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 3863f4622a047e9eaccc580226f569c0b9445d28
Author: Eric Avdey <ei...@eiri.ca>
AuthorDate: Mon Feb 10 17:33:28 2020 -0400

    Fix invalid json object in fabric2_doc_crud test
---
 src/fabric/test/fabric2_doc_crud_tests.erl | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/fabric/test/fabric2_doc_crud_tests.erl b/src/fabric/test/fabric2_doc_crud_tests.erl
index 184eb4a..d50c554 100644
--- a/src/fabric/test/fabric2_doc_crud_tests.erl
+++ b/src/fabric/test/fabric2_doc_crud_tests.erl
@@ -894,7 +894,7 @@ local_doc_with_previous_encoding({Db, _}) ->
 before_doc_update_skips_local_docs({Db0, _}) ->
 
     BduFun = fun(Doc, _, _) ->
-        Doc#doc{body = {[<<"bdu_was_here">>, true]}}
+        Doc#doc{body = {[{<<"bdu_was_here">>, true}]}}
     end,
 
     Db = Db0#{before_doc_update := BduFun},
@@ -909,4 +909,4 @@ before_doc_update_skips_local_docs({Db0, _}) ->
     {ok, Doc2} = fabric2_db:open_doc(Db, Doc1#doc.id),
 
     ?assertEqual({[]}, LDoc2#doc.body),
-    ?assertEqual({[<<"bdu_was_here">>, true]}, Doc2#doc.body).
+    ?assertEqual({[{<<"bdu_was_here">>, true}]}, Doc2#doc.body).


[couchdb] 04/04: Add migration from the old doc storage format

Posted by ei...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

eiri pushed a commit to branch prototype/flattened-doc-storage
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 1c97a07d6f825e4ec894434a8d2831b4855be0af
Author: Eric Avdey <ei...@eiri.ca>
AuthorDate: Tue Feb 11 12:28:30 2020 -0400

    Add migration from the old doc storage format
---
 src/fabric/src/fabric2_fdb.erl | 45 +++++++++++++++++++++++++++++++++---------
 1 file changed, 36 insertions(+), 9 deletions(-)

diff --git a/src/fabric/src/fabric2_fdb.erl b/src/fabric/src/fabric2_fdb.erl
index cdd1b60..9c7edba 100644
--- a/src/fabric/src/fabric2_fdb.erl
+++ b/src/fabric/src/fabric2_fdb.erl
@@ -526,20 +526,30 @@ get_doc_body_wait(#{} = Db0, DocId, RevInfo, Future) ->
 
     DocPrefix = erlfdb_tuple:pack({?DB_DOCS, DocId}, DbPrefix),
 
-    MetaBodyAcc = erlfdb:fold_range_wait(Tx, Future, fun({K, V}, Acc) ->
+    DocAcc = erlfdb:fold_range_wait(Tx, Future, fun({K, V}, Acc) ->
+        {NewAcc, OldAcc} = Acc,
         Tuple = erlfdb_tuple:unpack(K, DocPrefix),
         case tuple_to_list(Tuple) of
             [Deleted, RevPos, Rev] ->
-                {[], V, Deleted};
+                {{[], V, Deleted}, OldAcc};
             [_Deleted, RevPos, Rev | KeyPath] ->
-                {Body, DiskAtts, Deleted} = Acc,
-                {[{KeyPath, V} | Body], DiskAtts, Deleted};
+                {Body, DiskAtts, Deleted} = NewAcc,
+                {{[{KeyPath, V} | Body], DiskAtts, Deleted}, OldAcc};
+            %% migration from old format
+            [RevPos, Rev, _ChunkId] ->
+                {NewAcc, [V | OldAcc]};
             _ ->
                 Acc
         end
-    end, []),
-
-    fdb_to_doc(Db, DocId, RevPos, [Rev | RevPath], MetaBodyAcc).
+    end, {[], []}),
+
+    case DocAcc of
+        {[], RevBodyRows} ->
+            BodyRows = lists:reverse(RevBodyRows),
+            fdb_to_doc(Db, DocId, RevPos, [Rev | RevPath], BodyRows);
+        {MetaBodyAcc, _} ->
+            fdb_to_doc(Db, DocId, RevPos, [Rev | RevPath], MetaBodyAcc)
+    end.
 
 
 get_local_doc(#{} = Db0, <<?LOCAL_DOC_PREFIX, _/binary>> = DocId) ->
@@ -1053,6 +1063,15 @@ clear_doc_body(#{} = Db, DocId, #{} = RevInfo) ->
     } = RevInfo,
 
     BaseKey = {?DB_DOCS, DocId, Deleted, RevPos, Rev},
+
+    maybe_clean_old_doc_body(Tx, BaseKey, DbPrefix),
+
+    {StartKey, EndKey} = erlfdb_tuple:range(BaseKey, DbPrefix),
+    ok = erlfdb:clear_range(Tx, StartKey, EndKey).
+
+
+maybe_clean_old_doc_body(Tx, BaseKey0, DbPrefix) ->
+    BaseKey = erlang:delete_element(3, BaseKey0),
     {StartKey, EndKey} = erlfdb_tuple:range(BaseKey, DbPrefix),
     ok = erlfdb:clear_range(Tx, StartKey, EndKey).
 
@@ -1209,14 +1228,22 @@ doc_to_fdb(Db, #doc{} = Doc) ->
 fdb_to_doc(_Db, _DocId, _Pos, _Path, []) ->
     {not_found, missing};
 
-fdb_to_doc(Db, DocId, Pos, Path, MetaBodyAcc) ->
-    {Body0, DiskAtts0, Deleted} = MetaBodyAcc,
+% This is an upgrade clause, the migration will happen on doc's update.
+fdb_to_doc(Db, DocId, Pos, Path, BinRows) when is_list(BinRows) ->
+    Bin = iolist_to_binary(BinRows),
+    {Body, DiskAtts, Deleted} = binary_to_term(Bin, [safe]),
+    fdb_to_doc(Db, DocId, Pos, Path, Body, DiskAtts, Deleted);
+
+fdb_to_doc(Db, DocId, Pos, Path, {Body0, DiskAtts0, Deleted}) ->
     BodyRows = [{K, decode_body_value(V)} || {K, V} <- lists:reverse(Body0)],
     Body = case BodyRows of
         [] -> {[]};
         _ -> couch_util:json_implode(BodyRows)
     end,
     DiskAtts = binary_to_term(DiskAtts0, [safe]),
+    fdb_to_doc(Db, DocId, Pos, Path, Body, DiskAtts, Deleted).
+
+fdb_to_doc(Db, DocId, Pos, Path, Body, DiskAtts, Deleted) ->
     Atts = lists:map(fun(Att) ->
         couch_att:from_disk_term(Db, DocId, Att)
     end, DiskAtts),


[couchdb] 03/04: Use flattened JSON for store doc in fdb

Posted by ei...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

eiri pushed a commit to branch prototype/flattened-doc-storage
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 999686e76b924c8a9f44bdcc528b6252c914ff92
Author: Eric Avdey <ei...@eiri.ca>
AuthorDate: Mon Feb 10 17:39:07 2020 -0400

    Use flattened JSON for store doc in fdb
---
 src/fabric/src/fabric2_fdb.erl | 74 ++++++++++++++++++++++++++++++------------
 1 file changed, 53 insertions(+), 21 deletions(-)

diff --git a/src/fabric/src/fabric2_fdb.erl b/src/fabric/src/fabric2_fdb.erl
index 6abe1f6..cdd1b60 100644
--- a/src/fabric/src/fabric2_fdb.erl
+++ b/src/fabric/src/fabric2_fdb.erl
@@ -508,18 +508,15 @@ get_doc_body_future(#{} = Db, DocId, RevInfo) ->
         db_prefix := DbPrefix
     } = ensure_current(Db),
 
-    #{
-        rev_id := {RevPos, Rev}
-    } = RevInfo,
-
-    Key = {?DB_DOCS, DocId, RevPos, Rev},
+    Key = {?DB_DOCS, DocId},
     {StartKey, EndKey} = erlfdb_tuple:range(Key, DbPrefix),
     erlfdb:fold_range_future(Tx, StartKey, EndKey, []).
 
 
 get_doc_body_wait(#{} = Db0, DocId, RevInfo, Future) ->
     #{
-        tx := Tx
+        tx := Tx,
+        db_prefix := DbPrefix
     } = Db = ensure_current(Db0),
 
     #{
@@ -527,12 +524,22 @@ get_doc_body_wait(#{} = Db0, DocId, RevInfo, Future) ->
         rev_path := RevPath
     } = RevInfo,
 
-    RevBodyRows = erlfdb:fold_range_wait(Tx, Future, fun({_K, V}, Acc) ->
-        [V | Acc]
+    DocPrefix = erlfdb_tuple:pack({?DB_DOCS, DocId}, DbPrefix),
+
+    MetaBodyAcc = erlfdb:fold_range_wait(Tx, Future, fun({K, V}, Acc) ->
+        Tuple = erlfdb_tuple:unpack(K, DocPrefix),
+        case tuple_to_list(Tuple) of
+            [Deleted, RevPos, Rev] ->
+                {[], V, Deleted};
+            [_Deleted, RevPos, Rev | KeyPath] ->
+                {Body, DiskAtts, Deleted} = Acc,
+                {[{KeyPath, V} | Body], DiskAtts, Deleted};
+            _ ->
+                Acc
+        end
     end, []),
-    BodyRows = lists:reverse(RevBodyRows),
 
-    fdb_to_doc(Db, DocId, RevPos, [Rev | RevPath], BodyRows).
+    fdb_to_doc(Db, DocId, RevPos, [Rev | RevPath], MetaBodyAcc).
 
 
 get_local_doc(#{} = Db0, <<?LOCAL_DOC_PREFIX, _/binary>> = DocId) ->
@@ -1041,10 +1048,11 @@ clear_doc_body(#{} = Db, DocId, #{} = RevInfo) ->
     } = Db,
 
     #{
-        rev_id := {RevPos, Rev}
+        rev_id := {RevPos, Rev},
+        deleted := Deleted
     } = RevInfo,
 
-    BaseKey = {?DB_DOCS, DocId, RevPos, Rev},
+    BaseKey = {?DB_DOCS, DocId, Deleted, RevPos, Rev},
     {StartKey, EndKey} = erlfdb_tuple:range(BaseKey, DbPrefix),
     ok = erlfdb:clear_range(Tx, StartKey, EndKey).
 
@@ -1182,21 +1190,33 @@ doc_to_fdb(Db, #doc{} = Doc) ->
 
     DiskAtts = lists:map(fun couch_att:to_disk_term/1, Atts),
 
-    Value = term_to_binary({Body, DiskAtts, Deleted}, [{minor_version, 1}]),
+    DocKey = {?DB_DOCS, Id, Deleted, Start, Rev},
+    DocPrefix = erlfdb_tuple:pack(DocKey, DbPrefix),
+    Meta = term_to_binary(DiskAtts, [{minor_version, 1}]),
+    Rows0 = [{DocPrefix, Meta}],
 
-    {Rows, _} = lists:mapfoldl(fun(Chunk, ChunkId) ->
-        Key = erlfdb_tuple:pack({?DB_DOCS, Id, Start, Rev, ChunkId}, DbPrefix),
-        {{Key, Chunk}, ChunkId + 1}
-    end, 0, chunkify_binary(Value)),
-    Rows.
+    Body1 = case Body of
+        {[]} -> [];
+        _ -> couch_util:json_explode(Body)
+    end,
+    lists:foldl(fun({K, V}, Acc) ->
+        Key = erlfdb_tuple:pack(list_to_tuple(K), DocPrefix),
+        Value = encode_body_value(V),
+        [{Key, Value} | Acc]
+    end, Rows0, Body1).
 
 
 fdb_to_doc(_Db, _DocId, _Pos, _Path, []) ->
     {not_found, missing};
 
-fdb_to_doc(Db, DocId, Pos, Path, BinRows) when is_list(BinRows) ->
-    Bin = iolist_to_binary(BinRows),
-    {Body, DiskAtts, Deleted} = binary_to_term(Bin, [safe]),
+fdb_to_doc(Db, DocId, Pos, Path, MetaBodyAcc) ->
+    {Body0, DiskAtts0, Deleted} = MetaBodyAcc,
+    BodyRows = [{K, decode_body_value(V)} || {K, V} <- lists:reverse(Body0)],
+    Body = case BodyRows of
+        [] -> {[]};
+        _ -> couch_util:json_implode(BodyRows)
+    end,
+    DiskAtts = binary_to_term(DiskAtts0, [safe]),
     Atts = lists:map(fun(Att) ->
         couch_att:from_disk_term(Db, DocId, Att)
     end, DiskAtts),
@@ -1214,6 +1234,18 @@ fdb_to_doc(Db, DocId, Pos, Path, BinRows) when is_list(BinRows) ->
     end.
 
 
+encode_body_value(V) when is_list(V); is_tuple(V) ->
+    term_to_binary(V, [{minor_version, 1}]);
+encode_body_value(V) ->
+    erlfdb_tuple:pack({V}).
+
+decode_body_value(<<131, _/binary>> = V) ->
+    binary_to_term(V, [safe]);
+decode_body_value(V) ->
+    {V1} = erlfdb_tuple:unpack(V),
+    V1.
+
+
 local_doc_to_fdb(Db, #doc{} = Doc) ->
     #{
         db_prefix := DbPrefix