You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by da...@apache.org on 2020/09/18 20:51:15 UTC

[couchdb] 05/08: Update legacy views

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

davisp pushed a commit to branch feature-ebtree-views
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 0451f30b32a32eac1e1c7376ad0f3bb32e097241
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Fri Sep 18 11:05:00 2020 -0500

    Update legacy views
---
 src/couch_views/include/couch_views.hrl           |   4 +
 src/couch_views/src/couch_views_fdb.erl           | 179 ++++++++--
 src/couch_views/src/couch_views_indexer.erl       |   8 +-
 src/couch_views/test/couch_views_upgrade_test.erl | 400 ++++++++++++++++++++++
 4 files changed, 550 insertions(+), 41 deletions(-)

diff --git a/src/couch_views/include/couch_views.hrl b/src/couch_views/include/couch_views.hrl
index 3882191..92b8f46 100644
--- a/src/couch_views/include/couch_views.hrl
+++ b/src/couch_views/include/couch_views.hrl
@@ -10,6 +10,9 @@
 % License for the specific language governing permissions and limitations under
 % the License.
 
+% Current implementation version
+-define(CURRENT_VIEW_IMPL_VERSION, 1).
+
 % Index info/data subspaces
 -define(VIEW_INFO, 0).
 -define(VIEW_DATA, 1).
@@ -21,6 +24,7 @@
 -define(VIEW_KV_SIZE, 2).
 -define(VIEW_BUILD_STATUS, 3).
 -define(VIEW_CREATION_VS, 4).
+-define(VIEW_IMPL_VERSION, 5).
 
 % Data keys
 -define(VIEW_ID_RANGE, 0).
diff --git a/src/couch_views/src/couch_views_fdb.erl b/src/couch_views/src/couch_views_fdb.erl
index 3116e61..f22277d 100644
--- a/src/couch_views/src/couch_views_fdb.erl
+++ b/src/couch_views/src/couch_views_fdb.erl
@@ -13,6 +13,8 @@
 -module(couch_views_fdb).
 
 -export([
+    get_view_state/2,
+
     new_interactive_index/3,
     new_creation_vs/3,
     get_creation_vs/2,
@@ -50,52 +52,89 @@
 -include_lib("fabric/include/fabric2.hrl").
 
 
-new_interactive_index(Db, Mrst, VS) ->
-    couch_views_fdb:new_creation_vs(Db, Mrst, VS),
-    couch_views_fdb:set_build_status(Db, Mrst, ?INDEX_BUILDING).
+get_view_state(Db, #mrst{} = Mrst) ->
+    get_view_state(Db, Mrst#mrst.sig);
+
+get_view_state(Db, Sig) when is_binary(Sig) ->
+    #{
+        tx := Tx
+    } = Db,
+
+    VersionF = erlfdb:get(Tx, version_key(Db, Sig)),
+    ViewSeqF = erlfdb:get(Tx, seq_key(Db, Sig)),
+    ViewVSF = erlfdb:get(Tx, creation_vs_key(Db, Sig)),
+    BuildStatusF = erlfdb:get(Tx, build_status_key(Db, Sig)),
+
+    Version = case erlfdb:wait(VersionF) of
+        not_found -> not_found;
+        VsnVal -> element(1, erlfdb_tuple:unpack(VsnVal))
+    end,
+
+    ViewSeq = case erlfdb:wait(ViewSeqF) of
+        not_found -> <<>>;
+        SeqVal -> SeqVal
+    end,
+
+    ViewVS = case erlfdb:wait(ViewVSF) of
+        not_found -> not_found;
+        VSVal -> element(1, erlfdb_tuple:unpack(VSVal))
+    end,
+
+    State = #{
+        version => Version,
+        view_seq => ViewSeq,
+        view_vs => ViewVS,
+        build_status => erlfdb:wait(BuildStatusF)
+    },
+
+    maybe_upgrade_view(Db, Sig, State).
+
+
+new_interactive_index(Db, #mrst{} = Mrst, VS) ->
+    new_interactive_index(Db, Mrst#mrst.sig, VS);
+
+new_interactive_index(Db, Sig, VS) ->
+    set_version(Db, Sig),
+    new_creation_vs(Db, Sig, VS),
+    set_build_status(Db, Sig, ?INDEX_BUILDING).
 
 
 %Interactive View Creation Versionstamp
 %(<db>, ?DB_VIEWS, ?VIEW_INFO, ?VIEW_CREATION_VS, Sig) = VS
 
 new_creation_vs(TxDb, #mrst{} = Mrst, VS) ->
+    new_creation_vs(TxDb, Mrst#mrst.sig, VS);
+
+new_creation_vs(TxDb, Sig, VS) ->
     #{
         tx := Tx
     } = TxDb,
-    Key = creation_vs_key(TxDb, Mrst#mrst.sig),
+    Key = creation_vs_key(TxDb, Sig),
     Value = erlfdb_tuple:pack_vs({VS}),
     ok = erlfdb:set_versionstamped_value(Tx, Key, Value).
 
 
-get_creation_vs(TxDb, #mrst{} = Mrst) ->
-    get_creation_vs(TxDb, Mrst#mrst.sig);
-
-get_creation_vs(TxDb, Sig) ->
+get_creation_vs(TxDb, MrstOrSig) ->
     #{
-        tx := Tx
-    } = TxDb,
-    Key = creation_vs_key(TxDb, Sig),
-    case erlfdb:wait(erlfdb:get(Tx, Key)) of
-        not_found ->
-            not_found;
-        EK ->
-            {VS} = erlfdb_tuple:unpack(EK),
-            VS
-    end.
+        view_vs := ViewVS
+    } = get_view_state(TxDb, MrstOrSig),
+    ViewVS.
 
 
 %Interactive View Build Status
 %(<db>, ?DB_VIEWS, ?VIEW_INFO, ?VIEW_BUILD_STATUS, Sig) = INDEX_BUILDING | INDEX_READY
 
-get_build_status(TxDb, #mrst{sig = Sig}) ->
+get_build_status(TxDb, MrstOrSig) ->
     #{
-        tx := Tx
-    } = TxDb,
-    Key = build_status_key(TxDb, Sig),
-    erlfdb:wait(erlfdb:get(Tx, Key)).
+        build_status := BuildStatus
+    } = get_view_state(TxDb, MrstOrSig),
+    BuildStatus.
+
 
+set_build_status(TxDb, #mrst{} = Mrst, State) ->
+    set_build_status(TxDb, Mrst#mrst.sig, State);
 
-set_build_status(TxDb, #mrst{sig = Sig}, State) ->
+set_build_status(TxDb, Sig, State) ->
     #{
         tx := Tx
     } = TxDb,
@@ -108,24 +147,18 @@ set_build_status(TxDb, #mrst{sig = Sig}, State) ->
 % (<db>, ?DB_VIEWS, Sig, ?VIEW_UPDATE_SEQ) = Sequence
 
 
-get_update_seq(TxDb, #mrst{sig = Sig}) ->
+get_update_seq(TxDb, MrstOrSig) ->
     #{
-        tx := Tx,
-        db_prefix := DbPrefix
-    } = TxDb,
-
-    case erlfdb:wait(erlfdb:get(Tx, seq_key(DbPrefix, Sig))) of
-        not_found -> <<>>;
-        UpdateSeq -> UpdateSeq
-    end.
+        view_seq := ViewSeq
+    } = get_view_state(TxDb, MrstOrSig),
+    ViewSeq.
 
 
 set_update_seq(TxDb, Sig, Seq) ->
     #{
-        tx := Tx,
-        db_prefix := DbPrefix
+        tx := Tx
     } = TxDb,
-    ok = erlfdb:set(Tx, seq_key(DbPrefix, Sig), Seq).
+    ok = erlfdb:set(Tx, seq_key(TxDb, Sig), Seq).
 
 
 set_trees(TxDb, Mrst) ->
@@ -293,7 +326,10 @@ clear_index(Db, Signature) ->
     % Get view size to remove from global counter
     SizeTuple = {?DB_VIEWS, ?VIEW_INFO, ?VIEW_KV_SIZE, Signature},
     SizeKey = erlfdb_tuple:pack(SizeTuple, DbPrefix),
-    ViewSize = ?bin2uint(erlfdb:wait(erlfdb:get(Tx, SizeKey))),
+    ViewSize = case erlfdb:wait(erlfdb:get(Tx, SizeKey)) of
+        not_found -> 0;
+        SizeVal -> ?bin2uint(SizeVal)
+    end,
 
     % Clear index info keys
     Keys = [
@@ -322,6 +358,62 @@ clear_index(Db, Signature) ->
     erlfdb:add(Tx, DbSizeKey, -ViewSize).
 
 
+maybe_upgrade_view(_Db, _Sig, #{version := ?CURRENT_VIEW_IMPL_VERSION} = St) ->
+    St;
+maybe_upgrade_view(Db, Sig, #{version := not_found, view_seq := <<>>} = St) ->
+    % If we haven't started building the view yet
+    % then we don't change view_vs and build_status
+    % as they're still correct.
+    set_version(Db, Sig),
+    St#{
+        version => ?CURRENT_VIEW_IMPL_VERSION,
+        view_seq => <<>>
+    };
+maybe_upgrade_view(Db, Sig, #{version := not_found} = St) ->
+    clear_index(Db, Sig),
+    set_version(Db, Sig),
+    {ViewVS, BuildStatus} = reset_interactive_index(Db, Sig, St),
+    #{
+        version => ?CURRENT_VIEW_IMPL_VERSION,
+        view_seq => <<>>,
+        view_vs => ViewVS,
+        build_status => BuildStatus
+    }.
+
+
+set_version(Db, Sig) ->
+    #{
+        tx := Tx
+    } = Db,
+    Key = version_key(Db, Sig),
+    Val = erlfdb_tuple:pack({?CURRENT_VIEW_IMPL_VERSION}),
+    erlfdb:set(Tx, Key, Val).
+
+
+reset_interactive_index(_Db, _Sig, #{view_vs := not_found}) ->
+    % Not an interactive index
+    {not_found, not_found};
+reset_interactive_index(Db, Sig, _St) ->
+    % We have to reset the creation versionstamp
+    % to the current update seq of the database
+    % or else we'll not have indexed any documents
+    % inserted since the creation of the interactive
+    % index.
+    #{
+        tx := Tx
+    } = Db,
+
+    DbSeq = fabric2_db:get_update_seq(Db),
+    VS = fabric2_fdb:seq_to_vs(DbSeq),
+    Key = creation_vs_key(Db, Sig),
+    Val = erlfdb_tuple:pack({VS}),
+    ok = erlfdb:set(Tx, Key, Val),
+
+    set_build_status(Db, Sig, ?INDEX_BUILDING),
+
+    {VS, ?INDEX_BUILDING}.
+
+
 open_id_tree(TxDb, Sig) ->
     #{
         tx := Tx,
@@ -600,7 +692,18 @@ view_tree_prefix(DbPrefix, Sig, ViewId) ->
     erlfdb_tuple:pack(Key, DbPrefix).
 
 
-seq_key(DbPrefix, Sig) ->
+version_key(Db, Sig) ->
+    #{
+        db_prefix := DbPrefix
+    } = Db,
+    Key = {?DB_VIEWS, ?VIEW_INFO, ?VIEW_IMPL_VERSION, Sig},
+    erlfdb_tuple:pack(Key, DbPrefix).
+
+
+seq_key(Db, Sig) ->
+    #{
+        db_prefix := DbPrefix
+    } = Db,
     Key = {?DB_VIEWS, ?VIEW_INFO, ?VIEW_UPDATE_SEQ, Sig},
     erlfdb_tuple:pack(Key, DbPrefix).
 
diff --git a/src/couch_views/src/couch_views_indexer.erl b/src/couch_views/src/couch_views_indexer.erl
index 1b1fc4a..858a988 100644
--- a/src/couch_views/src/couch_views_indexer.erl
+++ b/src/couch_views/src/couch_views_indexer.erl
@@ -202,8 +202,8 @@ do_update(Db, Mrst0, State0) ->
             tx := Tx
         } = TxDb,
 
-        Mrst1 = couch_views_fdb:set_trees(TxDb, Mrst0),
         State1 = get_update_start_state(TxDb, Mrst0, State0),
+        Mrst1 = couch_views_fdb:set_trees(TxDb, Mrst0),
 
         {ok, State2} = fold_changes(State1),
 
@@ -261,8 +261,10 @@ maybe_set_build_status(TxDb, Mrst1, _ViewVS, State) ->
 % In the first iteration of update we need
 % to populate our db and view sequences
 get_update_start_state(TxDb, Mrst, #{db_seq := undefined} = State) ->
-    ViewVS = couch_views_fdb:get_creation_vs(TxDb, Mrst),
-    ViewSeq = couch_views_fdb:get_update_seq(TxDb, Mrst),
+    #{
+        view_vs := ViewVS,
+        view_seq := ViewSeq
+    } = couch_views_fdb:get_view_state(TxDb, Mrst),
 
     State#{
         tx_db := TxDb,
diff --git a/src/couch_views/test/couch_views_upgrade_test.erl b/src/couch_views/test/couch_views_upgrade_test.erl
new file mode 100644
index 0000000..0766f53
--- /dev/null
+++ b/src/couch_views/test/couch_views_upgrade_test.erl
@@ -0,0 +1,400 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_views_upgrade_test).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch_mrview/include/couch_mrview.hrl").
+-include_lib("couch_views/include/couch_views.hrl").
+-include_lib("fabric/include/fabric2.hrl").
+-include_lib("fabric/test/fabric2_test.hrl").
+
+
+-define(MAP_FUN1, <<"map_fun1">>).
+-define(MAP_FUN2, <<"map_fun2">>).
+
+
+indexer_test_() ->
+    {
+        "Test view indexing",
+        {
+            setup,
+            fun setup/0,
+            fun cleanup/1,
+            {
+                foreach,
+                fun foreach_setup/0,
+                fun foreach_teardown/1,
+                [
+                    ?TDEF_FE(empty_state),
+                    ?TDEF_FE(indexed_state),
+                    ?TDEF_FE(upgrade_non_interactive),
+                    ?TDEF_FE(upgrade_unbuilt_interactive),
+                    ?TDEF_FE(upgrade_partially_built_interactive),
+                    ?TDEF_FE(upgrade_built_interactive)
+                ]
+            }
+        }
+    }.
+
+
+setup() ->
+    Ctx = test_util:start_couch([
+            fabric,
+            couch_jobs,
+            couch_js,
+            couch_views
+        ]),
+    Ctx.
+
+
+cleanup(Ctx) ->
+    test_util:stop_couch(Ctx).
+
+
+foreach_setup() ->
+    {ok, Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]),
+    Db.
+
+
+foreach_teardown(Db) ->
+    meck:unload(),
+    config:delete("couch_views", "change_limit"),
+    ok = fabric2_db:delete(fabric2_db:name(Db), []).
+
+
+empty_state(Db) ->
+    DDoc = create_ddoc(),
+    {ok, Mrst} = couch_views_util:ddoc_to_mrst(fabric2_db:name(Db), DDoc),
+    State = fabric2_fdb:transactional(Db, fun(TxDb) ->
+        couch_views_fdb:get_view_state(TxDb, Mrst)
+    end),
+
+    Expect = #{
+        version => ?CURRENT_VIEW_IMPL_VERSION,
+        view_seq => <<>>,
+        view_vs => not_found,
+        build_status => not_found
+    },
+    ?assertEqual(Expect, State),
+    assert_fdb_state(Db, Mrst, Expect).
+
+
+indexed_state(Db) ->
+    DDoc = create_ddoc(),
+    Doc1 = doc(0),
+
+    {ok, _} = fabric2_db:update_doc(Db, DDoc, []),
+    {ok, _} = fabric2_db:update_doc(Db, Doc1, []),
+
+    {ok, Out} = run_query(Db, DDoc, ?MAP_FUN1),
+    ?assertEqual([row(<<"0">>, 0, 0)], Out),
+
+    assert_fdb_state(Db, DDoc, #{
+        version => ?CURRENT_VIEW_IMPL_VERSION,
+        view_seq => fabric2_db:get_update_seq(Db),
+        view_vs => not_found,
+        build_status => not_found
+    }).
+
+
+upgrade_non_interactive(Db) ->
+    DDoc = create_ddoc(),
+    Doc1 = doc(0),
+
+    {ok, _} = fabric2_db:update_docs(Db, [DDoc, Doc1], []),
+    DbSeq = fabric2_db:get_update_seq(Db),
+
+    init_fdb_state(Db, DDoc, #{view_seq => DbSeq}),
+
+    {ok, Out} = run_query(Db, DDoc, ?MAP_FUN1),
+    ?assertEqual([row(<<"0">>, 0, 0)], Out),
+
+    assert_fdb_state(Db, DDoc, #{
+        version => ?CURRENT_VIEW_IMPL_VERSION,
+        view_seq => DbSeq,
+        view_vs => not_found,
+        build_status => not_found
+    }).
+
+
+upgrade_unbuilt_interactive(Db) ->
+    DDoc = create_ddoc(),
+    {ok, Mrst} = couch_views_util:ddoc_to_mrst(fabric2_db:name(Db), DDoc),
+    Doc1 = doc(0),
+
+    {ok, _} = fabric2_db:update_docs(Db, [DDoc, Doc1], []),
+    DbSeq = fabric2_db:get_update_seq(Db),
+
+    init_fdb_state(Db, DDoc, #{
+        view_vs => fabric2_fdb:seq_to_vs(DbSeq),
+        build_status => ?INDEX_BUILDING
+    }),
+
+    % Trigger an upgrade
+    fabric2_fdb:transactional(Db, fun(TxDb) ->
+        couch_views_fdb:get_view_state(TxDb, Mrst)
+    end),
+
+    assert_fdb_state(Db, DDoc, #{
+        version => ?CURRENT_VIEW_IMPL_VERSION,
+        view_seq => <<>>,
+        view_vs => fabric2_fdb:seq_to_vs(DbSeq),
+        build_status => ?INDEX_BUILDING
+    }),
+
+    % Build the view
+    {ok, Out} = run_query(Db, DDoc, ?MAP_FUN1),
+    ?assertEqual([row(<<"0">>, 0, 0)], Out),
+
+    assert_fdb_state(Db, DDoc, #{
+        version => ?CURRENT_VIEW_IMPL_VERSION,
+        view_seq => DbSeq,
+        view_vs => fabric2_fdb:seq_to_vs(DbSeq),
+        build_status => ?INDEX_READY
+    }).
+
+
+upgrade_partially_built_interactive(Db) ->
+    DDoc = create_ddoc(),
+    {ok, Mrst} = couch_views_util:ddoc_to_mrst(fabric2_db:name(Db), DDoc),
+    {ok, _} = fabric2_db:update_doc(Db, DDoc, []),
+
+    MidSeq = fabric2_db:get_update_seq(Db),
+
+    Doc1 = doc(0),
+    {ok, _} = fabric2_db:update_doc(Db, Doc1, []),
+
+    DbSeq = fabric2_db:get_update_seq(Db),
+
+    init_fdb_state(Db, DDoc, #{
+        view_seq => MidSeq,
+        view_vs => fabric2_fdb:seq_to_vs(DbSeq),
+        build_status => ?INDEX_BUILDING
+    }),
+
+    % Trigger an upgrade
+    fabric2_fdb:transactional(Db, fun(TxDb) ->
+        couch_views_fdb:get_view_state(TxDb, Mrst)
+    end),
+
+    assert_fdb_state(Db, DDoc, #{
+        version => ?CURRENT_VIEW_IMPL_VERSION,
+        view_seq => <<>>,
+        view_vs => fabric2_fdb:seq_to_vs(DbSeq),
+        build_status => ?INDEX_BUILDING
+    }),
+
+    % Build the view
+    {ok, Out} = run_query(Db, DDoc, ?MAP_FUN1),
+    ?assertEqual([row(<<"0">>, 0, 0)], Out),
+
+    assert_fdb_state(Db, DDoc, #{
+        version => ?CURRENT_VIEW_IMPL_VERSION,
+        view_seq => DbSeq,
+        view_vs => fabric2_fdb:seq_to_vs(DbSeq),
+        build_status => ?INDEX_READY
+    }).
+
+
+upgrade_built_interactive(Db) ->
+    DDoc = create_ddoc(),
+    Doc1 = doc(0),
+
+    {ok, Mrst} = couch_views_util:ddoc_to_mrst(fabric2_db:name(Db), DDoc),
+    {ok, _} = fabric2_db:update_doc(Db, DDoc, []),
+    {ok, _} = fabric2_db:update_doc(Db, Doc1, []),
+
+    DbSeq = fabric2_db:get_update_seq(Db),
+
+    init_fdb_state(Db, DDoc, #{
+        view_seq => DbSeq,
+        view_vs => fabric2_fdb:seq_to_vs(DbSeq),
+        build_status => ?INDEX_READY
+    }),
+
+    % Trigger an upgrade
+    fabric2_fdb:transactional(Db, fun(TxDb) ->
+        couch_views_fdb:get_view_state(TxDb, Mrst)
+    end),
+
+    assert_fdb_state(Db, DDoc, #{
+        version => ?CURRENT_VIEW_IMPL_VERSION,
+        view_seq => <<>>,
+        view_vs => fabric2_fdb:seq_to_vs(DbSeq),
+        build_status => ?INDEX_BUILDING
+    }),
+
+    % Build the view
+    {ok, Out} = run_query(Db, DDoc, ?MAP_FUN1),
+    ?assertEqual([row(<<"0">>, 0, 0)], Out),
+
+    assert_fdb_state(Db, DDoc, #{
+        version => ?CURRENT_VIEW_IMPL_VERSION,
+        view_seq => DbSeq,
+        view_vs => fabric2_fdb:seq_to_vs(DbSeq),
+        build_status => ?INDEX_READY
+    }).
+
+
+init_fdb_state(Db, #doc{} = DDoc, Values) ->
+    {ok, Mrst} = couch_views_util:ddoc_to_mrst(fabric2_db:name(Db), DDoc),
+    init_fdb_state(Db, Mrst, Values);
+init_fdb_state(Db, #mrst{sig = Sig}, Values) ->
+    init_fdb_state(Db, Sig, Values);
+init_fdb_state(Db, Sig, Values) ->
+    VersionRow = case maps:get(version, Values, undefined) of
+        undefined -> [];
+        Version -> [{pack(Db, key(version, Sig)), pack({Version})}]
+    end,
+
+    SeqRow = case maps:get(view_seq, Values, undefined) of
+        undefined -> [];
+        Seq -> [{pack(Db, key(seq, Sig)), Seq}]
+    end,
+
+    VSRow = case maps:get(view_vs, Values, undefined) of
+        undefined -> [];
+        VS -> [{pack(Db, key(vs, Sig)), pack({VS})}]
+    end,
+
+    BSRow = case maps:get(build_status, Values, undefined) of
+        undefined -> [];
+        BS -> [{pack(Db, key(bs, Sig)), BS}]
+    end,
+
+    Rows = VersionRow ++ SeqRow ++ VSRow ++ BSRow,
+
+    fabric2_fdb:transactional(Db, fun(TxDb) ->
+        #{
+            tx := Tx
+        } = TxDb,
+        lists:foreach(fun({K, V}) ->
+            erlfdb:set(Tx, K, V)
+        end, Rows)
+    end).
+
+
+assert_fdb_state(Db, #doc{} = DDoc, Expect) ->
+    {ok, Mrst} = couch_views_util:ddoc_to_mrst(fabric2_db:name(Db), DDoc),
+    assert_fdb_state(Db, Mrst, Expect);
+assert_fdb_state(Db, #mrst{sig = Sig}, Expect) ->
+    assert_fdb_state(Db, Sig, Expect);
+assert_fdb_state(Db, Sig, Expect) ->
+    #{
+        version := Version,
+        view_seq := ViewSeq,
+        view_vs := ViewVS,
+        build_status := BuildStatus
+    } = Expect,
+
+    VersionRow = case Version of
+        not_found -> [];
+        _ -> [{pack(Db, key(version, Sig)), pack({Version})}]
+    end,
+
+    SeqRow = case ViewSeq of
+        <<>> -> [];
+        _ -> [{pack(Db, key(seq, Sig)), ViewSeq}]
+    end,
+
+    VSRow = case ViewVS of
+        not_found -> [];
+        _ -> [{pack(Db, key(vs, Sig)), pack({ViewVS})}]
+    end,
+
+    BSRow = case BuildStatus of
+        not_found -> [];
+        _ -> [{pack(Db, key(bs, Sig)), BuildStatus}]
+    end,
+
+    ExpectRows = lists:sort(VersionRow ++ SeqRow ++ VSRow ++ BSRow),
+
+    RawExistingRows = fabric2_fdb:transactional(Db, fun(TxDb) ->
+        #{
+            tx := Tx,
+            db_prefix := DbPrefix
+        } = TxDb,
+        RangePrefix = erlfdb_tuple:pack({?DB_VIEWS, ?VIEW_INFO}, DbPrefix),
+        erlfdb:wait(erlfdb:get_range_startswith(Tx, RangePrefix))
+    end),
+
+    % Ignore the KV size key in the view info rows
+    KVSizeKey = pack(Db, key(kv_size, Sig)),
+    ExistingRows = lists:keydelete(KVSizeKey, 1, RawExistingRows),
+
+    ?assertEqual(ExpectRows, ExistingRows).
+
+
+key(version, Sig) -> {?DB_VIEWS, ?VIEW_INFO, ?VIEW_IMPL_VERSION, Sig};
+key(seq, Sig) -> {?DB_VIEWS, ?VIEW_INFO, ?VIEW_UPDATE_SEQ, Sig};
+key(kv_size, Sig) -> {?DB_VIEWS, ?VIEW_INFO, ?VIEW_KV_SIZE, Sig};
+key(vs, Sig) -> {?DB_VIEWS, ?VIEW_INFO, ?VIEW_CREATION_VS, Sig};
+key(bs, Sig) -> {?DB_VIEWS, ?VIEW_INFO, ?VIEW_BUILD_STATUS, Sig}.
+
+
+pack(Db, Key) ->
+    #{
+        db_prefix := DbPrefix
+    } = Db,
+    erlfdb_tuple:pack(Key, DbPrefix).
+
+
+pack(Value) ->
+    erlfdb_tuple:pack(Value).
+
+
+row(Id, Key, Value) ->
+    {row, [
+        {id, Id},
+        {key, Key},
+        {value, Value}
+    ]}.
+
+
+fold_fun({meta, _Meta}, Acc) ->
+    {ok, Acc};
+fold_fun({row, _} = Row, Acc) ->
+    {ok, [Row | Acc]};
+fold_fun(complete, Acc) ->
+    {ok, lists:reverse(Acc)}.
+
+
+create_ddoc() ->
+    couch_doc:from_json_obj({[
+        {<<"_id">>, <<"_design/bar">>},
+        {<<"views">>, {[
+            {?MAP_FUN1, {[
+                {<<"map">>, <<"function(doc) {emit(doc.val, doc.val);}">>}
+            ]}},
+            {?MAP_FUN2, {[
+                {<<"map">>, <<"function(doc) {}">>}
+            ]}}
+        ]}}
+    ]}).
+
+
+doc(Id) ->
+    doc(Id, Id).
+
+
+doc(Id, Val) ->
+    couch_doc:from_json_obj({[
+        {<<"_id">>, list_to_binary(integer_to_list(Id))},
+        {<<"val">>, Val}
+    ]}).
+
+
+run_query(#{} = Db, DDoc, <<_/binary>> = View) ->
+    couch_views:query(Db, DDoc, View, fun fold_fun/2, [], #mrargs{}).
\ No newline at end of file