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

vatamane pushed a commit to branch allow-downgrades-option
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 677a08617d10447a1b36402d98d7fb01509d7481
Author: Nick Vatamaniuc <[email protected]>
AuthorDate: Wed Jul 30 15:37:48 2025 -0400

    Implement the ability to downgrade CouchDB versions
    
    Currently, CouchDB on-disk header structure allows appening new fields to 
the
    header record in such a way that newer versions can upgrade themselves from 
the
    old versions easily. However, it was not possible to perform a downgrade 
back
    to an old version in case something went wrong.
    
    While in general it may not be safe to downgrade, it may be possible for 
some
    features so allow for such an option and make it configurable.
    
    The release notes of future releases may indicate which version are dowgrade
    safe. Then, to perform a downgrade users would enable the downgrade flag,
    downgrade, and then reset it back to default = false.
    
    Implementation-wise, it's pretty basic, if the size of the new tuple is 
larger
    than the old one, only use as many tuple fields as that (now old version) 
knows
    about, everything else is discarded.
---
 rel/overlay/etc/default.ini              |  9 ++++
 src/couch/src/couch_bt_engine_header.erl | 82 +++++++++++++++++++++++++++-----
 2 files changed, 80 insertions(+), 11 deletions(-)

diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index dfefa62dc..5f74360be 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -120,6 +120,15 @@ view_index_dir = {{view_index_dir}}
 ; this setting.
 ;cfile_skip_ioq = false
 
+; CouchDB may release some features that touch the on-disk data structures.
+; Some of them may make it possible to downgrade to previous versions and some
+; won't. If it is possible, the documentation of the new release will indicate
+; that, and will indicate which downgrade version it's safe to downgrade to.
+; Then, in order to downgrade, set allow_header_downgrade option to true,
+; perform the downgrade to the old version, then reset the config value to
+; false.
+;allow_header_downgrade = false
+
 [purge]
 ; Allowed maximum number of documents in one purge request
 ;max_document_id_number = 100
diff --git a/src/couch/src/couch_bt_engine_header.erl 
b/src/couch/src/couch_bt_engine_header.erl
index 3581b1e39..185364eba 100644
--- a/src/couch/src/couch_bt_engine_header.erl
+++ b/src/couch/src/couch_bt_engine_header.erl
@@ -204,17 +204,20 @@ upgrade_tuple(Old) when is_record(Old, db_header) ->
     Old;
 upgrade_tuple(Old) when is_tuple(Old) ->
     NewSize = record_info(size, db_header),
-    if
-        tuple_size(Old) < NewSize -> ok;
-        true -> erlang:error({invalid_header_size, Old})
-    end,
-    {_, New} = lists:foldl(
-        fun(Val, {Idx, Hdr}) ->
-            {Idx + 1, setelement(Idx, Hdr, Val)}
+    OldKVs =
+        case tuple_size(Old) < NewSize of
+            true ->
+                % Upgrade: old header has less fields than current
+                tuple_to_list(Old);
+            false ->
+                % A potential downgrade. Allow it only if configured to do so
+                case config:get_boolean("couchdb", "allow_header_downgrade", 
false) of
+                    true -> lists:sublist(tuple_to_list(Old), NewSize);
+                    false -> erlang:error({invalid_header_size, Old})
+                end
         end,
-        {1, #db_header{}},
-        tuple_to_list(Old)
-    ),
+    FoldFun = fun(Val, {Idx, Hdr}) -> {Idx + 1, setelement(Idx, Hdr, Val)} end,
+    {_, New} = lists:foldl(FoldFun, {1, #db_header{}}, OldKVs),
     if
         is_record(New, db_header) -> ok;
         true -> erlang:error({invalid_header_extension, {Old, New}})
@@ -338,7 +341,7 @@ latest(_Else) ->
     undefined.
 
 -ifdef(TEST).
--include_lib("eunit/include/eunit.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
 
 mk_header(Vsn) ->
     {
@@ -478,4 +481,61 @@ get_epochs_from_old_header_test() ->
     Vsn5Header = mk_header(5),
     ?assertEqual(undefined, epochs(Vsn5Header)).
 
+tuple_uprade_test_() ->
+    {
+        foreach,
+        fun() ->
+            Ctx = test_util:start_couch(),
+            config:set("couchdb", "allow_header_downgrade", "false", false),
+            Ctx
+        end,
+        fun(Ctx) ->
+            config:delete("couchdb", "allow_header_downgrade", false),
+            test_util:stop_couch(Ctx)
+        end,
+        [
+            ?TDEF_FE(t_upgrade_tuple_same_size),
+            ?TDEF_FE(t_upgrade_tuple),
+            ?TDEF_FE(t_downgrade_default),
+            ?TDEF_FE(t_downgrade_allowed)
+        ]
+    }.
+
+t_upgrade_tuple_same_size(_) ->
+    Hdr = #db_header{disk_version = ?LATEST_DISK_VERSION},
+    Hdr1 = upgrade_tuple(Hdr),
+    ?assertEqual(Hdr, Hdr1).
+
+t_upgrade_tuple(_) ->
+    Hdr = {db_header, ?LATEST_DISK_VERSION, 101},
+    Hdr1 = upgrade_tuple(Hdr),
+    ?assertMatch(
+        #db_header{
+            disk_version = ?LATEST_DISK_VERSION,
+            update_seq = 101,
+            purge_infos_limit = 1000
+        },
+        Hdr1
+    ).
+
+t_downgrade_default(_) ->
+    Junk = lists:duplicate(50, x),
+    Hdr = list_to_tuple([db_header, ?LATEST_DISK_VERSION] ++ Junk),
+    % Not allowed by default
+    ?assertError({invalid_header_size, _}, upgrade_tuple(Hdr)).
+
+t_downgrade_allowed(_) ->
+    Junk = lists:duplicate(50, x),
+    Hdr = list_to_tuple([db_header, ?LATEST_DISK_VERSION, 42] ++ Junk),
+    config:set("couchdb", "allow_header_downgrade", "true", false),
+    Hdr1 = upgrade_tuple(Hdr),
+    ?assert(is_record(Hdr1, db_header)),
+    ?assertMatch(
+        #db_header{
+            disk_version = ?LATEST_DISK_VERSION + 1,
+            update_seq = 42
+        },
+        Hdr1
+    ).
+
 -endif.

Reply via email to