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.
