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 2019/06/05 19:03:19 UTC

[couchdb] 04/06: Initial test suite for the fabric2 implementation

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

davisp pushed a commit to branch prototype/fdb-layer
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 1db8a4385208b4459fafd9f1f3e84b276f4f58f7
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Wed Jun 5 13:33:55 2019 -0500

    Initial test suite for the fabric2 implementation
    
    This provides a good bit of code coverage for the new implementation.
    We'll want to expand this to include relevant tests from the previous
    fabric test suite along with reading through the various other tests and
    ensuring that we cover the API as deeply as is appropriate for this
    layer.
---
 src/fabric/test/fabric2_changes_fold_tests.erl     | 114 +++
 src/fabric/test/fabric2_db_crud_tests.erl          |  88 +++
 src/fabric/test/fabric2_db_misc_tests.erl          | 113 +++
 src/fabric/test/fabric2_db_security_tests.erl      | 162 +++++
 src/fabric/test/fabric2_doc_count_tests.erl        | 251 +++++++
 src/fabric/test/fabric2_doc_crud_tests.erl         | 770 +++++++++++++++++++++
 src/fabric/test/fabric2_doc_fold_tests.erl         | 209 ++++++
 src/fabric/test/fabric2_fdb_tx_retry_tests.erl     | 178 +++++
 src/fabric/test/fabric2_trace_db_create_tests.erl  |  46 ++
 src/fabric/test/fabric2_trace_db_delete_tests.erl  |  49 ++
 src/fabric/test/fabric2_trace_db_open_tests.erl    |  50 ++
 src/fabric/test/fabric2_trace_doc_create_tests.erl |  86 +++
 12 files changed, 2116 insertions(+)

diff --git a/src/fabric/test/fabric2_changes_fold_tests.erl b/src/fabric/test/fabric2_changes_fold_tests.erl
new file mode 100644
index 0000000..892b448
--- /dev/null
+++ b/src/fabric/test/fabric2_changes_fold_tests.erl
@@ -0,0 +1,114 @@
+% 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(fabric2_changes_fold_tests).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+-define(DOC_COUNT, 25).
+
+
+changes_fold_test_() ->
+    {
+        "Test changes fold operations",
+        {
+            setup,
+            fun setup/0,
+            fun cleanup/1,
+            {with, [
+                fun fold_changes_basic/1,
+                fun fold_changes_since_now/1,
+                fun fold_changes_since_seq/1,
+                fun fold_changes_basic_rev/1,
+                fun fold_changes_since_now_rev/1,
+                fun fold_changes_since_seq_rev/1
+            ]}
+        }
+    }.
+
+
+setup() ->
+    Ctx = test_util:start_couch([fabric]),
+    {ok, Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]),
+    Rows = lists:map(fun(Val) ->
+        DocId = fabric2_util:uuid(),
+        Doc = #doc{
+            id = DocId,
+            body = {[{<<"value">>, Val}]}
+        },
+        {ok, RevId} = fabric2_db:update_doc(Db, Doc, []),
+        UpdateSeq = fabric2_db:get_update_seq(Db),
+        #{
+            id => DocId,
+            sequence => UpdateSeq,
+            deleted => false,
+            rev_id => RevId
+        }
+    end, lists:seq(1, ?DOC_COUNT)),
+    {Db, Rows, Ctx}.
+
+
+cleanup({Db, _DocIdRevs, Ctx}) ->
+    ok = fabric2_db:delete(fabric2_db:name(Db), []),
+    test_util:stop_couch(Ctx).
+
+
+fold_changes_basic({Db, DocRows, _}) ->
+    {ok, Rows} = fabric2_db:fold_changes(Db, 0, fun fold_fun/2, []),
+    ?assertEqual(lists:reverse(DocRows), Rows).
+
+
+fold_changes_since_now({Db, _, _}) ->
+    {ok, Rows} = fabric2_db:fold_changes(Db, now, fun fold_fun/2, []),
+    ?assertEqual([], Rows).
+
+
+fold_changes_since_seq({_, [], _}) ->
+    ok;
+
+fold_changes_since_seq({Db, [Row | RestRows], _}) ->
+    #{sequence := Since} = Row,
+    {ok, Rows} = fabric2_db:fold_changes(Db, Since, fun fold_fun/2, []),
+    ?assertEqual(lists:reverse(RestRows), Rows),
+    fold_changes_since_seq({Db, RestRows, nil}).
+
+
+fold_changes_basic_rev({Db, _, _}) ->
+    Opts = [{dir, rev}],
+    {ok, Rows} = fabric2_db:fold_changes(Db, 0, fun fold_fun/2, [], Opts),
+    ?assertEqual([], Rows).
+
+
+fold_changes_since_now_rev({Db, DocRows, _}) ->
+    Opts = [{dir, rev}],
+    {ok, Rows} = fabric2_db:fold_changes(Db, now, fun fold_fun/2, [], Opts),
+    ?assertEqual(DocRows, Rows).
+
+
+fold_changes_since_seq_rev({_, [], _}) ->
+    ok;
+
+fold_changes_since_seq_rev({Db, DocRows, _}) ->
+    #{sequence := Since} = lists:last(DocRows),
+    Opts = [{dir, rev}],
+    {ok, Rows} = fabric2_db:fold_changes(Db, Since, fun fold_fun/2, [], Opts),
+    ?assertEqual(DocRows, Rows),
+    RestRows = lists:sublist(DocRows, length(DocRows) - 1),
+    fold_changes_since_seq_rev({Db, RestRows, nil}).
+
+
+fold_fun(#{} = Change, Acc) ->
+    {ok, [Change | Acc]}.
diff --git a/src/fabric/test/fabric2_db_crud_tests.erl b/src/fabric/test/fabric2_db_crud_tests.erl
new file mode 100644
index 0000000..24deeb2
--- /dev/null
+++ b/src/fabric/test/fabric2_db_crud_tests.erl
@@ -0,0 +1,88 @@
+% 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(fabric2_db_crud_tests).
+
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+-define(TDEF(A), {atom_to_list(A), fun A/0}).
+
+
+crud_test_() ->
+    {
+        "Test database CRUD operations",
+        {
+            setup,
+            fun() -> test_util:start_couch([fabric]) end,
+            fun test_util:stop_couch/1,
+            [
+                ?TDEF(create_db),
+                ?TDEF(open_db),
+                ?TDEF(delete_db),
+                ?TDEF(list_dbs)
+            ]
+        }
+    }.
+
+
+create_db() ->
+    DbName = ?tempdb(),
+    ?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
+    ?assertEqual(true, ets:member(fabric2_server, DbName)),
+    ?assertEqual({error, file_exists}, fabric2_db:create(DbName, [])).
+
+
+open_db() ->
+    DbName = ?tempdb(),
+    ?assertError(database_does_not_exist, fabric2_db:open(DbName, [])),
+
+    ?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
+    ?assertEqual(true, ets:member(fabric2_server, DbName)),
+
+    % Opening the cached version
+    ?assertMatch({ok, _}, fabric2_db:open(DbName, [])),
+
+    % Remove from cache and re-open
+    true = ets:delete(fabric2_server, DbName),
+    ?assertMatch({ok, _}, fabric2_db:open(DbName, [])).
+
+
+delete_db() ->
+    DbName = ?tempdb(),
+    ?assertError(database_does_not_exist, fabric2_db:delete(DbName, [])),
+
+    ?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
+    ?assertEqual(true, ets:member(fabric2_server, DbName)),
+
+    ?assertEqual(ok, fabric2_db:delete(DbName, [])),
+    ?assertEqual(false, ets:member(fabric2_server, DbName)),
+
+    ?assertError(database_does_not_exist, fabric2_db:open(DbName, [])).
+
+
+list_dbs() ->
+    DbName = ?tempdb(),
+    AllDbs1 = fabric2_db:list_dbs(),
+
+    ?assert(is_list(AllDbs1)),
+    ?assert(not lists:member(DbName, AllDbs1)),
+
+    ?assertMatch({ok, _}, fabric2_db:create(DbName, [])),
+    AllDbs2 = fabric2_db:list_dbs(),
+    ?assert(lists:member(DbName, AllDbs2)),
+
+    ?assertEqual(ok, fabric2_db:delete(DbName, [])),
+    AllDbs3 = fabric2_db:list_dbs(),
+    ?assert(not lists:member(DbName, AllDbs3)).
diff --git a/src/fabric/test/fabric2_db_misc_tests.erl b/src/fabric/test/fabric2_db_misc_tests.erl
new file mode 100644
index 0000000..8e64056
--- /dev/null
+++ b/src/fabric/test/fabric2_db_misc_tests.erl
@@ -0,0 +1,113 @@
+% 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(fabric2_db_misc_tests).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+-define(TDEF(A), {atom_to_list(A), fun A/1}).
+
+
+misc_test_() ->
+    {
+        "Test database miscellaney",
+        {
+            setup,
+            fun setup/0,
+            fun cleanup/1,
+            {with, [
+                fun empty_db_info/1,
+                fun accessors/1,
+                fun set_revs_limit/1,
+                fun set_security/1,
+                fun is_system_db/1,
+                fun ensure_full_commit/1
+            ]}
+        }
+    }.
+
+
+setup() ->
+    Ctx = test_util:start_couch([fabric]),
+    DbName = ?tempdb(),
+    {ok, Db} = fabric2_db:create(DbName, [{user_ctx, ?ADMIN_USER}]),
+    {DbName, Db, Ctx}.
+
+
+cleanup({_DbName, Db, Ctx}) ->
+    ok = fabric2_db:delete(fabric2_db:name(Db), []),
+    test_util:stop_couch(Ctx).
+
+
+empty_db_info({DbName, Db, _}) ->
+    {ok, Info} = fabric2_db:get_db_info(Db),
+    ?assertEqual(DbName, fabric2_util:get_value(db_name, Info)),
+    ?assertEqual(0, fabric2_util:get_value(doc_count, Info)),
+    ?assertEqual(0, fabric2_util:get_value(doc_del_count, Info)),
+    ?assert(is_binary(fabric2_util:get_value(update_seq, Info))).
+
+
+accessors({DbName, Db, _}) ->
+    SeqZero = fabric2_fdb:vs_to_seq(fabric2_util:seq_zero_vs()),
+    ?assertEqual(DbName, fabric2_db:name(Db)),
+    ?assertEqual(0, fabric2_db:get_instance_start_time(Db)),
+    ?assertEqual(nil, fabric2_db:get_pid(Db)),
+    ?assertEqual(undefined, fabric2_db:get_before_doc_update_fun(Db)),
+    ?assertEqual(undefined, fabric2_db:get_after_doc_read_fun(Db)),
+    ?assertEqual(SeqZero, fabric2_db:get_committed_update_seq(Db)),
+    ?assertEqual(SeqZero, fabric2_db:get_compacted_seq(Db)),
+    ?assertEqual(SeqZero, fabric2_db:get_update_seq(Db)),
+    ?assertEqual(nil, fabric2_db:get_compactor_pid(Db)),
+    ?assertEqual(1000, fabric2_db:get_revs_limit(Db)),
+    ?assertMatch(<<_:32/binary>>, fabric2_db:get_uuid(Db)),
+    ?assertEqual(true, fabric2_db:is_db(Db)),
+    ?assertEqual(false, fabric2_db:is_db(#{})),
+    ?assertEqual(false, fabric2_db:is_partitioned(Db)),
+    ?assertEqual(false, fabric2_db:is_clustered(Db)).
+
+
+set_revs_limit({DbName, Db, _}) ->
+    ?assertEqual(ok, fabric2_db:set_revs_limit(Db, 500)),
+    {ok, Db2} = fabric2_db:open(DbName, []),
+    ?assertEqual(500, fabric2_db:get_revs_limit(Db2)).
+
+
+set_security({DbName, Db, _}) ->
+    SecObj = {[
+        {<<"admins">>, {[
+            {<<"names">>, []},
+            {<<"roles">>, []}
+        ]}}
+    ]},
+    ?assertEqual(ok, fabric2_db:set_security(Db, SecObj)),
+    {ok, Db2} = fabric2_db:open(DbName, []),
+    ?assertEqual(SecObj, fabric2_db:get_security(Db2)).
+
+
+is_system_db({DbName, Db, _}) ->
+    ?assertEqual(false, fabric2_db:is_system_db(Db)),
+    ?assertEqual(false, fabric2_db:is_system_db_name("foo")),
+    ?assertEqual(false, fabric2_db:is_system_db_name(DbName)),
+    ?assertEqual(true, fabric2_db:is_system_db_name(<<"_replicator">>)),
+    ?assertEqual(true, fabric2_db:is_system_db_name("_replicator")),
+    ?assertEqual(true, fabric2_db:is_system_db_name(<<"foo/_replicator">>)),
+    ?assertEqual(false, fabric2_db:is_system_db_name(<<"f.o/_replicator">>)),
+    ?assertEqual(false, fabric2_db:is_system_db_name(<<"foo/bar">>)).
+
+
+ensure_full_commit({_, Db, _}) ->
+    ?assertEqual({ok, 0}, fabric2_db:ensure_full_commit(Db)),
+    ?assertEqual({ok, 0}, fabric2_db:ensure_full_commit(Db, 5)).
diff --git a/src/fabric/test/fabric2_db_security_tests.erl b/src/fabric/test/fabric2_db_security_tests.erl
new file mode 100644
index 0000000..9796011
--- /dev/null
+++ b/src/fabric/test/fabric2_db_security_tests.erl
@@ -0,0 +1,162 @@
+% 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(fabric2_db_security_tests).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+security_test_() ->
+    {
+        "Test database security operations",
+        {
+            setup,
+            fun setup/0,
+            fun cleanup/1,
+            {with, [
+                fun is_admin_name/1,
+                fun is_not_admin_name/1,
+                fun is_admin_role/1,
+                fun is_not_admin_role/1,
+                fun check_is_admin/1,
+                fun check_is_not_admin/1,
+                fun check_is_member_name/1,
+                fun check_is_not_member_name/1,
+                fun check_is_member_role/1,
+                fun check_is_not_member_role/1,
+                fun check_admin_is_member/1,
+                fun check_is_member_of_public_db/1,
+                fun check_set_user_ctx/1
+            ]}
+        }
+    }.
+
+
+setup() ->
+    Ctx = test_util:start_couch([fabric]),
+    DbName = ?tempdb(),
+    {ok, Db1} = fabric2_db:create(DbName, [{user_ctx, ?ADMIN_USER}]),
+    SecProps = {[
+        {<<"admins">>, {[
+            {<<"names">>, [<<"admin_name1">>, <<"admin_name2">>]},
+            {<<"roles">>, [<<"admin_role1">>, <<"admin_role2">>]}
+        ]}},
+        {<<"members">>, {[
+            {<<"names">>, [<<"member_name1">>, <<"member_name2">>]},
+            {<<"roles">>, [<<"member_role1">>, <<"member_role2">>]}
+        ]}}
+    ]},
+    ok = fabric2_db:set_security(Db1, SecProps),
+    {ok, Db2} = fabric2_db:open(DbName, []),
+    {Db2, Ctx}.
+
+
+cleanup({Db, Ctx}) ->
+    ok = fabric2_db:delete(fabric2_db:name(Db), []),
+    test_util:stop_couch(Ctx).
+
+
+is_admin_name({Db, _}) ->
+    UserCtx = #user_ctx{name = <<"admin_name1">>},
+    ?assertEqual(true, fabric2_db:is_admin(Db#{user_ctx := UserCtx})).
+
+
+is_not_admin_name({Db, _}) ->
+    UserCtx = #user_ctx{name = <<"member1">>},
+    ?assertEqual(false, fabric2_db:is_admin(Db#{user_ctx := UserCtx})).
+
+
+is_admin_role({Db, _}) ->
+    UserCtx = #user_ctx{roles = [<<"admin_role1">>]},
+    ?assertEqual(true, fabric2_db:is_admin(Db#{user_ctx := UserCtx})).
+
+
+is_not_admin_role({Db, _}) ->
+    UserCtx = #user_ctx{roles = [<<"member_role1">>]},
+    ?assertEqual(false, fabric2_db:is_admin(Db#{user_ctx := UserCtx})).
+
+
+check_is_admin({Db, _}) ->
+    UserCtx = #user_ctx{name = <<"admin_name1">>},
+    ?assertEqual(ok, fabric2_db:check_is_admin(Db#{user_ctx := UserCtx})).
+
+
+check_is_not_admin({Db, _}) ->
+    UserCtx = #user_ctx{name = <<"member_name1">>},
+    ?assertThrow(
+        {unauthorized, <<"You are not a db or server admin.">>},
+        fabric2_db:check_is_admin(Db#{user_ctx := #user_ctx{}})
+    ),
+    ?assertThrow(
+        {forbidden, <<"You are not a db or server admin.">>},
+        fabric2_db:check_is_admin(Db#{user_ctx := UserCtx})
+    ).
+
+
+check_is_member_name({Db, _}) ->
+    UserCtx = #user_ctx{name = <<"member_name1">>},
+    ?assertEqual(ok, fabric2_db:check_is_member(Db#{user_ctx := UserCtx})).
+
+
+check_is_not_member_name({Db, _}) ->
+    UserCtx = #user_ctx{name = <<"foo">>},
+    ?assertThrow(
+        {unauthorized, <<"You are not authorized", _/binary>>},
+        fabric2_db:check_is_member(Db#{user_ctx := #user_ctx{}})
+    ),
+    ?assertThrow(
+        {forbidden, <<"You are not allowed to access", _/binary>>},
+        fabric2_db:check_is_member(Db#{user_ctx := UserCtx})
+    ).
+
+
+check_is_member_role({Db, _}) ->
+    UserCtx = #user_ctx{name = <<"foo">>, roles = [<<"member_role1">>]},
+    ?assertEqual(ok, fabric2_db:check_is_member(Db#{user_ctx := UserCtx})).
+
+
+check_is_not_member_role({Db, _}) ->
+    UserCtx = #user_ctx{name = <<"foo">>, roles = [<<"bar">>]},
+    ?assertThrow(
+        {forbidden, <<"You are not allowed to access", _/binary>>},
+        fabric2_db:check_is_member(Db#{user_ctx := UserCtx})
+    ).
+
+
+check_admin_is_member({Db, _}) ->
+    UserCtx = #user_ctx{name = <<"admin_name1">>},
+    ?assertEqual(ok, fabric2_db:check_is_member(Db#{user_ctx := UserCtx})).
+
+
+check_is_member_of_public_db({Db, _}) ->
+    PublicDb = Db#{security_doc := {[]}},
+    UserCtx = #user_ctx{name = <<"foo">>, roles = [<<"bar">>]},
+    ?assertEqual(
+        ok,
+        fabric2_db:check_is_member(PublicDb#{user_ctx := #user_ctx{}})
+    ),
+    ?assertEqual(
+        ok,
+        fabric2_db:check_is_member(PublicDb#{user_ctx := UserCtx})
+    ).
+
+
+check_set_user_ctx({Db0, _}) ->
+    DbName = fabric2_db:name(Db0),
+    UserCtx = #user_ctx{name = <<"foo">>, roles = [<<"bar">>]},
+    {ok, Db1} = fabric2_db:open(DbName, [{user_ctx, UserCtx}]),
+    ?assertEqual(UserCtx, fabric2_db:get_user_ctx(Db1)).
+
+
diff --git a/src/fabric/test/fabric2_doc_count_tests.erl b/src/fabric/test/fabric2_doc_count_tests.erl
new file mode 100644
index 0000000..37d0840
--- /dev/null
+++ b/src/fabric/test/fabric2_doc_count_tests.erl
@@ -0,0 +1,251 @@
+% 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(fabric2_doc_count_tests).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+-define(DOC_COUNT, 10).
+
+
+doc_count_test_() ->
+    {
+        "Test document counting operations",
+        {
+            setup,
+            fun setup/0,
+            fun cleanup/1,
+            {with, [
+                fun normal_docs/1,
+                fun design_docs/1,
+                fun local_docs/1
+            ]}
+        }
+    }.
+
+
+setup() ->
+    Ctx = test_util:start_couch([fabric]),
+    {ok, Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]),
+    {Db, Ctx}.
+
+
+cleanup({Db, Ctx}) ->
+    ok = fabric2_db:delete(fabric2_db:name(Db), []),
+    test_util:stop_couch(Ctx).
+
+
+normal_docs({Db, _}) ->
+    {DocCount, DelDocCount, DDocCount, LDocCount} = get_doc_counts(Db),
+
+    Docs1 = lists:map(fun(Id) ->
+        Doc = #doc{
+            id = integer_to_binary(Id),
+            body = {[{<<"value">>, Id}]}
+        },
+        {ok, {RevPos, Rev}} = fabric2_db:update_doc(Db, Doc, []),
+        Doc#doc{revs = {RevPos, [Rev]}}
+    end, lists:seq(1, ?DOC_COUNT)),
+
+    check_doc_counts(
+            Db,
+            DocCount + ?DOC_COUNT,
+            DelDocCount,
+            DDocCount,
+            LDocCount
+        ),
+
+    Docs2 = lists:map(fun(Doc) ->
+        {[{<<"value">>, V}]} = Doc#doc.body,
+        NewDoc = case V rem 2 of
+            0 -> Doc#doc{deleted = true};
+            1 -> Doc
+        end,
+        {ok, {RevPos, Rev}} = fabric2_db:update_doc(Db, NewDoc, []),
+        NewDoc#doc{revs = {RevPos, [Rev]}}
+    end, Docs1),
+
+    check_doc_counts(
+            Db,
+            DocCount + ?DOC_COUNT div 2,
+            DelDocCount + ?DOC_COUNT div 2,
+            DDocCount,
+            LDocCount
+        ),
+
+    lists:map(fun(Doc) ->
+        case Doc#doc.deleted of
+            true ->
+                Undeleted = Doc#doc{
+                    revs = {0, []},
+                    deleted = false
+                },
+                {ok, {RevPos, Rev}} = fabric2_db:update_doc(Db, Undeleted, []),
+                Undeleted#doc{revs = {RevPos, [Rev]}};
+            false ->
+                Doc
+        end
+    end, Docs2),
+
+    check_doc_counts(
+            Db,
+            DocCount + ?DOC_COUNT,
+            DelDocCount,
+            DDocCount,
+            LDocCount
+        ).
+
+
+design_docs({Db, _}) ->
+    {DocCount, DelDocCount, DDocCount, LDocCount} = get_doc_counts(Db),
+
+    Docs1 = lists:map(fun(Id) ->
+        BinId = integer_to_binary(Id),
+        DDocId = <<?DESIGN_DOC_PREFIX, BinId/binary>>,
+        Doc = #doc{
+            id = DDocId,
+            body = {[{<<"value">>, Id}]}
+        },
+        {ok, {RevPos, Rev}} = fabric2_db:update_doc(Db, Doc, []),
+        Doc#doc{revs = {RevPos, [Rev]}}
+    end, lists:seq(1, ?DOC_COUNT)),
+
+    check_doc_counts(
+            Db,
+            DocCount + ?DOC_COUNT,
+            DelDocCount,
+            DDocCount + ?DOC_COUNT,
+            LDocCount
+        ),
+
+    Docs2 = lists:map(fun(Doc) ->
+        {[{<<"value">>, V}]} = Doc#doc.body,
+        NewDoc = case V rem 2 of
+            0 -> Doc#doc{deleted = true};
+            1 -> Doc
+        end,
+        {ok, {RevPos, Rev}} = fabric2_db:update_doc(Db, NewDoc, []),
+        NewDoc#doc{revs = {RevPos, [Rev]}}
+    end, Docs1),
+
+    check_doc_counts(
+            Db,
+            DocCount + ?DOC_COUNT div 2,
+            DelDocCount + ?DOC_COUNT div 2,
+            DDocCount + ?DOC_COUNT div 2,
+            LDocCount
+        ),
+
+    lists:map(fun(Doc) ->
+        case Doc#doc.deleted of
+            true ->
+                Undeleted = Doc#doc{
+                    revs = {0, []},
+                    deleted = false
+                },
+                {ok, {RevPos, Rev}} = fabric2_db:update_doc(Db, Undeleted, []),
+                Undeleted#doc{revs = {RevPos, [Rev]}};
+            false ->
+                Doc
+        end
+    end, Docs2),
+
+    check_doc_counts(
+            Db,
+            DocCount + ?DOC_COUNT,
+            DelDocCount,
+            DDocCount + ?DOC_COUNT,
+            LDocCount
+        ).
+
+
+local_docs({Db, _}) ->
+    {DocCount, DelDocCount, DDocCount, LDocCount} = get_doc_counts(Db),
+
+    Docs1 = lists:map(fun(Id) ->
+        BinId = integer_to_binary(Id),
+        LDocId = <<?LOCAL_DOC_PREFIX, BinId/binary>>,
+        Doc = #doc{
+            id = LDocId,
+            body = {[{<<"value">>, Id}]}
+        },
+        {ok, {RevPos, Rev}} = fabric2_db:update_doc(Db, Doc, []),
+        Doc#doc{revs = {RevPos, [Rev]}}
+    end, lists:seq(1, ?DOC_COUNT)),
+
+    check_doc_counts(
+            Db,
+            DocCount,
+            DelDocCount,
+            DDocCount,
+            LDocCount + ?DOC_COUNT
+        ),
+
+    Docs2 = lists:map(fun(Doc) ->
+        {[{<<"value">>, V}]} = Doc#doc.body,
+        NewDoc = case V rem 2 of
+            0 -> Doc#doc{deleted = true};
+            1 -> Doc
+        end,
+        {ok, {RevPos, Rev}} = fabric2_db:update_doc(Db, NewDoc, []),
+        NewDoc#doc{revs = {RevPos, [Rev]}}
+    end, Docs1),
+
+    check_doc_counts(
+            Db,
+            DocCount,
+            DelDocCount,
+            DDocCount,
+            LDocCount + ?DOC_COUNT div 2
+        ),
+
+    lists:map(fun(Doc) ->
+        case Doc#doc.deleted of
+            true ->
+                Undeleted = Doc#doc{
+                    revs = {0, []},
+                    deleted = false
+                },
+                {ok, {RevPos, Rev}} = fabric2_db:update_doc(Db, Undeleted, []),
+                Undeleted#doc{revs = {RevPos, [Rev]}};
+            false ->
+                Doc
+        end
+    end, Docs2),
+
+    check_doc_counts(
+            Db,
+            DocCount,
+            DelDocCount,
+            DDocCount,
+            LDocCount + ?DOC_COUNT
+        ).
+
+
+get_doc_counts(Db) ->
+    DocCount = fabric2_db:get_doc_count(Db),
+    DelDocCount = fabric2_db:get_del_doc_count(Db),
+    DDocCount = fabric2_db:get_doc_count(Db, <<"_design">>),
+    LDocCount = fabric2_db:get_doc_count(Db, <<"_local">>),
+    {DocCount, DelDocCount, DDocCount, LDocCount}.
+
+
+check_doc_counts(Db, DocCount, DelDocCount, DDocCount, LDocCount) ->
+    ?assertEqual(DocCount, fabric2_db:get_doc_count(Db)),
+    ?assertEqual(DelDocCount, fabric2_db:get_del_doc_count(Db)),
+    ?assertEqual(DocCount, fabric2_db:get_doc_count(Db, <<"_all_docs">>)),
+    ?assertEqual(DDocCount, fabric2_db:get_doc_count(Db, <<"_design">>)),
+    ?assertEqual(LDocCount, fabric2_db:get_doc_count(Db, <<"_local">>)).
diff --git a/src/fabric/test/fabric2_doc_crud_tests.erl b/src/fabric/test/fabric2_doc_crud_tests.erl
new file mode 100644
index 0000000..85b2766
--- /dev/null
+++ b/src/fabric/test/fabric2_doc_crud_tests.erl
@@ -0,0 +1,770 @@
+% 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(fabric2_doc_crud_tests).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+doc_crud_test_() ->
+    {
+        "Test document CRUD operations",
+        {
+            setup,
+            fun setup/0,
+            fun cleanup/1,
+            {with, [
+                fun open_missing_doc/1,
+                fun create_new_doc/1,
+                fun create_ddoc_basic/1,
+                fun create_ddoc_requires_admin/1,
+                fun create_ddoc_requires_validation/1,
+                fun create_ddoc_requires_compilation/1,
+                fun update_doc_basic/1,
+                fun update_ddoc_basic/1,
+                fun update_doc_replicated/1,
+                fun update_doc_replicated_add_conflict/1,
+                fun update_doc_replicated_changes_winner/1,
+                fun update_doc_replicated_extension/1,
+                fun update_doc_replicate_existing_rev/1,
+                fun update_winning_conflict_branch/1,
+                fun update_non_winning_conflict_branch/1,
+                fun delete_doc_basic/1,
+                fun delete_changes_winner/1,
+                fun recreate_doc_basic/1,
+                fun conflict_on_create_new_with_rev/1,
+                fun conflict_on_update_with_no_rev/1,
+                fun conflict_on_create_as_deleted/1,
+                fun conflict_on_recreate_as_deleted/1,
+                fun conflict_on_extend_deleted/1,
+                fun open_doc_revs_basic/1,
+                fun open_doc_revs_all/1,
+                fun open_doc_revs_latest/1,
+                fun get_missing_revs_basic/1,
+                fun get_missing_revs_on_missing_doc/1,
+                fun open_missing_local_doc/1,
+                fun create_local_doc_basic/1,
+                fun update_local_doc_basic/1,
+                fun delete_local_doc_basic/1,
+                fun recreate_local_doc/1,
+                fun create_local_doc_bad_rev/1,
+                fun create_local_doc_random_rev/1
+            ]}
+        }
+    }.
+
+
+setup() ->
+    Ctx = test_util:start_couch([fabric]),
+    {ok, Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]),
+    {Db, Ctx}.
+
+
+cleanup({Db, Ctx}) ->
+    ok = fabric2_db:delete(fabric2_db:name(Db), []),
+    test_util:stop_couch(Ctx).
+
+
+open_missing_doc({Db, _}) ->
+    ?assertEqual({not_found, missing}, fabric2_db:open_doc(Db, <<"foo">>)).
+
+
+create_new_doc({Db, _}) ->
+    Doc = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {RevPos, Rev}} = fabric2_db:update_doc(Db, Doc),
+    NewDoc = Doc#doc{revs = {RevPos, [Rev]}},
+    ?assertEqual({ok, NewDoc}, fabric2_db:open_doc(Db, Doc#doc.id)).
+
+
+create_ddoc_basic({Db, _}) ->
+    UUID = fabric2_util:uuid(),
+    DDocId = <<"_design/", UUID/binary>>,
+    Doc = #doc{
+        id = DDocId,
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {RevPos, Rev}} = fabric2_db:update_doc(Db, Doc),
+    NewDoc = Doc#doc{revs = {RevPos, [Rev]}},
+    ?assertEqual({ok, NewDoc}, fabric2_db:open_doc(Db, Doc#doc.id)).
+
+
+create_ddoc_requires_admin({Db, _}) ->
+    Db2 = fabric2_db:set_user_ctx(Db, #user_ctx{}),
+    UUID = fabric2_util:uuid(),
+    DDocId = <<"_design/", UUID/binary>>,
+    Doc = #doc{
+        id = DDocId,
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    ?assertThrow({unauthorized, _}, fabric2_db:update_doc(Db2, Doc)).
+
+
+create_ddoc_requires_validation({Db, _}) ->
+    UUID = fabric2_util:uuid(),
+    DDocId = <<"_design/", UUID/binary>>,
+    Doc = #doc{
+        id = DDocId,
+        body = {[
+            {<<"views">>, {[
+                {<<"foo">>, {[
+                    {<<"map">>, <<"function(doc) {}">>},
+                    {<<"reduce">>, <<"_not_a_builtin_reduce">>}
+                ]}}
+            ]}}
+        ]}
+    },
+    ?assertThrow(
+            {bad_request, invalid_design_doc, _},
+            fabric2_db:update_doc(Db, Doc)
+        ).
+
+
+create_ddoc_requires_compilation({Db, _}) ->
+    UUID = fabric2_util:uuid(),
+    DDocId = <<"_design/", UUID/binary>>,
+    Doc = #doc{
+        id = DDocId,
+        body = {[
+            {<<"language">>, <<"javascript">>},
+            {<<"views">>, {[
+                {<<"foo">>, {[
+                    {<<"map">>, <<"Hopefully this is invalid JavaScript">>}
+                ]}}
+            ]}}
+        ]}
+    },
+    ?assertThrow(
+            {bad_request, compilation_error, _},
+            fabric2_db:update_doc(Db, Doc)
+        ).
+
+
+update_doc_basic({Db, _}) ->
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"state">>, 1}]}
+    },
+    {ok, {Pos1, Rev1}} = fabric2_db:update_doc(Db, Doc1),
+    Doc2 = Doc1#doc{
+        revs = {Pos1, [Rev1]},
+        body = {[{<<"state">>, 2}]}
+    },
+    {ok, {Pos2, Rev2}} = fabric2_db:update_doc(Db, Doc2),
+    Doc3 = Doc2#doc{
+        revs = {Pos2, [Rev2, Rev1]}
+    },
+    ?assertEqual({ok, Doc3}, fabric2_db:open_doc(Db, Doc2#doc.id)).
+
+
+update_ddoc_basic({Db, _}) ->
+    UUID = fabric2_util:uuid(),
+    DDocId = <<"_design/", UUID/binary>>,
+    Doc1 = #doc{
+        id = DDocId,
+        body = {[{<<"state">>, 1}]}
+    },
+    {ok, {Pos1, Rev1}} = fabric2_db:update_doc(Db, Doc1),
+    Doc2 = Doc1#doc{
+        revs = {Pos1, [Rev1]},
+        body = {[{<<"state">>, 2}]}
+    },
+    {ok, {Pos2, Rev2}} = fabric2_db:update_doc(Db, Doc2),
+    Doc3 = Doc2#doc{
+        revs = {Pos2, [Rev2, Rev1]}
+    },
+    ?assertEqual({ok, Doc3}, fabric2_db:open_doc(Db, Doc2#doc.id)).
+
+
+update_doc_replicated({Db, _}) ->
+    Doc = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [fabric2_util:uuid(), fabric2_util:uuid()]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc, [replicated_changes]),
+    ?assertEqual({ok, Doc}, fabric2_db:open_doc(Db, Doc#doc.id)).
+
+
+update_doc_replicated_add_conflict({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    ?assertEqual({ok, Doc1}, fabric2_db:open_doc(Db, Doc1#doc.id)),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+    ?assertEqual({ok, Doc1}, fabric2_db:open_doc(Db, Doc2#doc.id)).
+
+
+update_doc_replicated_changes_winner({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    ?assertEqual({ok, Doc1}, fabric2_db:open_doc(Db, Doc1#doc.id)),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+    ?assertEqual({ok, Doc2}, fabric2_db:open_doc(Db, Doc2#doc.id)).
+
+
+update_doc_replicated_extension({Db, _}) ->
+    % No sort necessary and avoided on purpose to
+    % demonstrate that this is not sort dependent
+    Rev1 = fabric2_util:uuid(),
+    Rev2 = fabric2_util:uuid(),
+    Rev3 = fabric2_util:uuid(),
+    Rev4 = fabric2_util:uuid(),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    Doc2 = Doc1#doc{
+        revs = {4, [Rev4, Rev3, Rev2]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {4, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+    {ok, Doc3} = fabric2_db:open_doc(Db, Doc2#doc.id),
+    ?assertEqual({4, [Rev4, Rev3, Rev2, Rev1]}, Doc3#doc.revs),
+    ?assertEqual(Doc2#doc{revs = undefined}, Doc3#doc{revs = undefined}).
+
+
+update_doc_replicate_existing_rev({Db, _}) ->
+    Rev1 = fabric2_util:uuid(),
+    Rev2 = fabric2_util:uuid(),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    {ok, []} = fabric2_db:update_docs(Db, [Doc1], [replicated_changes]),
+    ?assertEqual({ok, Doc1}, fabric2_db:open_doc(Db, Doc1#doc.id)).
+
+
+update_winning_conflict_branch({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+    % Update the winning branch
+    Doc3 = Doc1#doc{
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"baz">>, 2}]}
+    },
+    {ok, {3, Rev4}} = fabric2_db:update_doc(Db, Doc3),
+    {ok, Doc4} = fabric2_db:open_doc(Db, Doc3#doc.id),
+    % Assert we've got the correct winner
+    ?assertEqual({3, [Rev4, Rev3, Rev1]}, Doc4#doc.revs),
+    ?assertEqual(Doc3#doc{revs = undefined}, Doc4#doc{revs = undefined}).
+
+
+update_non_winning_conflict_branch({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+    % Update the non winning branch
+    Doc3 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"baz">>, 2}]}
+    },
+    {ok, {3, Rev4}} = fabric2_db:update_doc(Db, Doc3),
+    {ok, Doc4} = fabric2_db:open_doc(Db, Doc3#doc.id),
+    % Assert we've got the correct winner
+    ?assertEqual({3, [Rev4, Rev2, Rev1]}, Doc4#doc.revs),
+    ?assertEqual(Doc3#doc{revs = undefined}, Doc4#doc{revs = undefined}).
+
+
+delete_doc_basic({Db, _}) ->
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"state">>, 1}]}
+    },
+    {ok, {Pos1, Rev1}} = fabric2_db:update_doc(Db, Doc1),
+    Doc2 = Doc1#doc{
+        revs = {Pos1, [Rev1]},
+        deleted = true,
+        body = {[{<<"state">>, 2}]}
+    },
+    {ok, {Pos2, Rev2}} = fabric2_db:update_doc(Db, Doc2),
+    Doc3 = Doc2#doc{revs = {Pos2, [Rev2, Rev1]}},
+    ?assertEqual({ok, Doc3}, fabric2_db:open_doc(Db, Doc2#doc.id, [deleted])).
+
+
+delete_changes_winner({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+    % Delete the winning branch
+    Doc3 = Doc1#doc{
+        revs = {2, [Rev3, Rev1]},
+        deleted = true,
+        body = {[]}
+    },
+    {ok, {3, _}} = fabric2_db:update_doc(Db, Doc3),
+    ?assertEqual({ok, Doc2}, fabric2_db:open_doc(Db, Doc3#doc.id)).
+
+
+recreate_doc_basic({Db, _}) ->
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"state">>, 1}]}
+    },
+    {ok, {1, Rev1}} = fabric2_db:update_doc(Db, Doc1),
+    Doc2 = Doc1#doc{
+        revs = {1, [Rev1]},
+        deleted = true,
+        body = {[{<<"state">>, 2}]}
+    },
+    {ok, {2, Rev2}} = fabric2_db:update_doc(Db, Doc2),
+    Doc3 = Doc1#doc{
+        revs = {0, []},
+        deleted = false,
+        body = {[{<<"state">>, 3}]}
+    },
+    {ok, {3, Rev3}} = fabric2_db:update_doc(Db, Doc3),
+    {ok, Doc4} = fabric2_db:open_doc(Db, Doc3#doc.id),
+    ?assertEqual({3, [Rev3, Rev2, Rev1]}, Doc4#doc.revs),
+    ?assertEqual(Doc3#doc{revs = undefined}, Doc4#doc{revs = undefined}).
+
+
+conflict_on_create_new_with_rev({Db, _}) ->
+    Doc = #doc{
+        id = fabric2_util:uuid(),
+        revs = {1, [fabric2_util:uuid()]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    ?assertThrow({error, conflict}, fabric2_db:update_doc(Db, Doc)).
+
+
+conflict_on_update_with_no_rev({Db, _}) ->
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"state">>, 1}]}
+    },
+    {ok, _} = fabric2_db:update_doc(Db, Doc1),
+    Doc2 = Doc1#doc{
+        revs = {0, []},
+        body = {[{<<"state">>, 2}]}
+    },
+    ?assertThrow({error, conflict}, fabric2_db:update_doc(Db, Doc2)).
+
+
+conflict_on_create_as_deleted({Db, _}) ->
+    Doc = #doc{
+        id = fabric2_util:uuid(),
+        deleted = true,
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    ?assertThrow({error, conflict}, fabric2_db:update_doc(Db, Doc)).
+
+
+conflict_on_recreate_as_deleted({Db, _}) ->
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"state">>, 1}]}
+    },
+    {ok, {Pos1, Rev1}} = fabric2_db:update_doc(Db, Doc1),
+    Doc2 = Doc1#doc{
+        revs = {Pos1, [Rev1]},
+        deleted = true,
+        body = {[{<<"state">>, 2}]}
+    },
+    {ok, _} = fabric2_db:update_doc(Db, Doc2),
+    Doc3 = Doc1#doc{
+        revs = {0, []},
+        deleted = true,
+        body = {[{<<"state">>, 3}]}
+    },
+    ?assertThrow({error, conflict}, fabric2_db:update_doc(Db, Doc3)).
+
+
+conflict_on_extend_deleted({Db, _}) ->
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"state">>, 1}]}
+    },
+    {ok, {Pos1, Rev1}} = fabric2_db:update_doc(Db, Doc1),
+    Doc2 = Doc1#doc{
+        revs = {Pos1, [Rev1]},
+        deleted = true,
+        body = {[{<<"state">>, 2}]}
+    },
+    {ok, {Pos2, Rev2}} = fabric2_db:update_doc(Db, Doc2),
+    Doc3 = Doc1#doc{
+        revs = {Pos2, [Rev2]},
+        deleted = false,
+        body = {[{<<"state">>, 3}]}
+    },
+    ?assertThrow({error, conflict}, fabric2_db:update_doc(Db, Doc3)).
+
+
+open_doc_revs_basic({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    DocId = fabric2_util:uuid(),
+    Doc1 = #doc{
+        id = DocId,
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+
+    {ok, [{ok, Doc3}]} = fabric2_db:open_doc_revs(Db, DocId, [{2, Rev3}], []),
+    ?assertEqual(Doc1, Doc3),
+
+    {ok, [{ok, Doc4}]} = fabric2_db:open_doc_revs(Db, DocId, [{2, Rev2}], []),
+    ?assertEqual(Doc2, Doc4),
+
+    Revs = [{2, Rev3}, {2, Rev2}, {1, Rev1}],
+    {ok, Docs} = fabric2_db:open_doc_revs(Db, DocId, Revs, []),
+    ?assert(length(Docs) == 3),
+    ?assert(lists:member({ok, Doc1}, Docs)),
+    ?assert(lists:member({ok, Doc2}, Docs)),
+    ?assert(lists:member({{not_found, missing}, {1, Rev1}}, Docs)),
+
+    % Make sure crazy madeup revisions are accepted
+    MissingRevs = [{5, fabric2_util:uuid()}, {1, fabric2_util:uuid()}],
+    {ok, NFMissing} = fabric2_db:open_doc_revs(Db, DocId, MissingRevs, []),
+    ?assertEqual(2, length(NFMissing)),
+    lists:foreach(fun(MR) ->
+        ?assert(lists:member({{not_found, missing}, MR}, NFMissing))
+    end, MissingRevs).
+
+
+open_doc_revs_all({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    DocId = fabric2_util:uuid(),
+    Doc1 = #doc{
+        id = DocId,
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+
+    {ok, Docs} = fabric2_db:open_doc_revs(Db, DocId, all, []),
+    ?assert(length(Docs) == 2),
+    ?assert(lists:member({ok, Doc1}, Docs)),
+    ?assert(lists:member({ok, Doc2}, Docs)).
+
+
+open_doc_revs_latest({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    DocId = fabric2_util:uuid(),
+    Doc1 = #doc{
+        id = DocId,
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+
+    Opts = [latest],
+    {ok, [{ok, Doc3}]} = fabric2_db:open_doc_revs(Db, DocId, [{2, Rev3}], Opts),
+    ?assertEqual(Doc1, Doc3),
+
+    {ok, Docs} = fabric2_db:open_doc_revs(Db, DocId, [{1, Rev1}], Opts),
+    ?assert(length(Docs) == 2),
+    ?assert(lists:member({ok, Doc1}, Docs)),
+    ?assert(lists:member({ok, Doc2}, Docs)).
+
+
+get_missing_revs_basic({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    DocId = fabric2_util:uuid(),
+    Doc1 = #doc{
+        id = DocId,
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+
+    % Check that we can find all revisions
+    AllRevs = [{1, Rev1}, {2, Rev2}, {2, Rev3}],
+    ?assertEqual(
+            {ok, []},
+            fabric2_db:get_missing_revs(Db, [{DocId, AllRevs}])
+        ),
+
+    % Check that a missing revision is found with no possible ancestors
+    MissingRev = {2, fabric2_util:uuid()},
+    ?assertEqual(
+            {ok, [{DocId, [MissingRev], []}]},
+            fabric2_db:get_missing_revs(Db, [{DocId, [MissingRev]}])
+        ),
+
+    % Check that only a missing rev is returned
+    ?assertEqual(
+            {ok, [{DocId, [MissingRev], []}]},
+            fabric2_db:get_missing_revs(Db, [{DocId, [MissingRev | AllRevs]}])
+        ),
+
+    % Check that we can find possible ancestors
+    MissingWithAncestors = {4, fabric2_util:uuid()},
+    PossibleAncestors = [{2, Rev2}, {2, Rev3}],
+    ?assertEqual(
+            {ok, [{DocId, [MissingWithAncestors], PossibleAncestors}]},
+            fabric2_db:get_missing_revs(Db, [{DocId, [MissingWithAncestors]}])
+        ).
+
+
+get_missing_revs_on_missing_doc({Db, _}) ->
+    Revs = lists:sort([
+            couch_doc:rev_to_str({1, fabric2_util:uuid()}),
+            couch_doc:rev_to_str({2, fabric2_util:uuid()}),
+            couch_doc:rev_to_str({800, fabric2_util:uuid()})
+        ]),
+    DocId = fabric2_util:uuid(),
+    {ok, Resp} = fabric2_db:get_missing_revs(Db, [{DocId, Revs}]),
+    ?assertMatch([{DocId, [_ | _], []}], Resp),
+    [{DocId, Missing, _}] = Resp,
+    MissingStrs = [couch_doc:rev_to_str(Rev) || Rev <- Missing],
+    ?assertEqual(Revs, lists:sort(MissingStrs)).
+
+
+open_missing_local_doc({Db, _}) ->
+    ?assertEqual(
+            {not_found, missing},
+            fabric2_db:open_doc(Db, <<"_local/foo">>, [])
+        ).
+
+
+create_local_doc_basic({Db, _}) ->
+    UUID = fabric2_util:uuid(),
+    LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
+    Doc1 = #doc{
+        id = LDocId,
+        revs = {0, []},
+        deleted = false,
+        body = {[{<<"ohai">>, <<"there">>}]}
+    },
+    ?assertEqual({ok, {0, <<"1">>}}, fabric2_db:update_doc(Db, Doc1)),
+    {ok, Doc2} = fabric2_db:open_doc(Db, Doc1#doc.id, []),
+    ?assertEqual(Doc1#doc{revs = {0, [<<"1">>]}}, Doc2).
+
+
+update_local_doc_basic({Db, _}) ->
+    UUID = fabric2_util:uuid(),
+    LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
+    Doc1 = #doc{
+        id = LDocId,
+        revs = {0, []},
+        deleted = false,
+        body = {[{<<"ohai">>, <<"there">>}]}
+    },
+    ?assertEqual({ok, {0, <<"1">>}}, fabric2_db:update_doc(Db, Doc1)),
+    Doc2 = Doc1#doc{
+        revs = {0, [<<"1">>]},
+        body = {[{<<"whiz">>, <<"bang">>}]}
+    },
+    ?assertEqual({ok, {0, <<"2">>}}, fabric2_db:update_doc(Db, Doc2)),
+    {ok, Doc3} = fabric2_db:open_doc(Db, Doc1#doc.id, []),
+    ?assertEqual(Doc2#doc{revs = {0, [<<"2">>]}}, Doc3).
+
+
+delete_local_doc_basic({Db, _}) ->
+    UUID = fabric2_util:uuid(),
+    LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
+    Doc1 = #doc{
+        id = LDocId,
+        revs = {0, []},
+        deleted = false,
+        body = {[{<<"ohai">>, <<"there">>}]}
+    },
+    ?assertEqual({ok, {0, <<"1">>}}, fabric2_db:update_doc(Db, Doc1)),
+    Doc2 = Doc1#doc{
+        revs = {0, [<<"1">>]},
+        deleted = true,
+        body = {[]}
+    },
+    ?assertEqual({ok, {0, <<"0">>}}, fabric2_db:update_doc(Db, Doc2)),
+    ?assertEqual(
+            {not_found, missing},
+            fabric2_db:open_doc(Db, LDocId)
+        ).
+
+
+recreate_local_doc({Db, _}) ->
+    UUID = fabric2_util:uuid(),
+    LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
+    Doc1 = #doc{
+        id = LDocId,
+        revs = {0, []},
+        deleted = false,
+        body = {[{<<"ohai">>, <<"there">>}]}
+    },
+    ?assertEqual({ok, {0, <<"1">>}}, fabric2_db:update_doc(Db, Doc1)),
+    Doc2 = Doc1#doc{
+        revs = {0, [<<"1">>]},
+        deleted = true,
+        body = {[]}
+    },
+    ?assertEqual({ok, {0, <<"0">>}}, fabric2_db:update_doc(Db, Doc2)),
+    ?assertEqual(
+            {not_found, missing},
+            fabric2_db:open_doc(Db, LDocId)
+        ),
+
+    ?assertEqual({ok, {0, <<"1">>}}, fabric2_db:update_doc(Db, Doc1)),
+    {ok, Doc3} = fabric2_db:open_doc(Db, LDocId),
+    ?assertEqual(Doc1#doc{revs = {0, [<<"1">>]}}, Doc3).
+
+
+create_local_doc_bad_rev({Db, _}) ->
+    UUID = fabric2_util:uuid(),
+    LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
+    Doc1 = #doc{
+        id = LDocId,
+        revs = {0, [<<"not a number">>]}
+    },
+    ?assertThrow(
+            {error, <<"Invalid rev format">>},
+            fabric2_db:update_doc(Db, Doc1)
+        ),
+
+    Doc2 = Doc1#doc{
+        revs = bad_bad_rev_roy_brown
+    },
+    ?assertThrow(
+            {error, <<"Invalid rev format">>},
+            fabric2_db:update_doc(Db, Doc2)
+        ).
+
+
+create_local_doc_random_rev({Db, _}) ->
+    % Local docs don't care what rev is passed as long
+    % as long as its a number.
+    UUID = fabric2_util:uuid(),
+    LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
+    Doc1 = #doc{
+        id = LDocId,
+        revs = {0, [<<"42">>]},
+        body = {[{<<"state">>, 1}]}
+    },
+    ?assertEqual({ok, {0, <<"43">>}}, fabric2_db:update_doc(Db, Doc1)),
+    {ok, Doc2} = fabric2_db:open_doc(Db, LDocId, []),
+    ?assertEqual(Doc1#doc{revs = {0, [<<"43">>]}}, Doc2),
+
+    Doc3 = Doc1#doc{
+        revs = {0, [<<"1234567890">>]},
+        body = {[{<<"state">>, 2}]}
+    },
+    ?assertEqual({ok, {0, <<"1234567891">>}}, fabric2_db:update_doc(Db, Doc3)),
+    {ok, Doc4} = fabric2_db:open_doc(Db, LDocId, []),
+    ?assertEqual(Doc3#doc{revs = {0, [<<"1234567891">>]}}, Doc4),
+
+    Doc5 = Doc1#doc{
+        revs = {0, [<<"1">>]},
+        body = {[{<<"state">>, 3}]}
+    },
+    ?assertEqual({ok, {0, <<"2">>}}, fabric2_db:update_doc(Db, Doc5)),
+    {ok, Doc6} = fabric2_db:open_doc(Db, LDocId, []),
+    ?assertEqual(Doc5#doc{revs = {0, [<<"2">>]}}, Doc6).
diff --git a/src/fabric/test/fabric2_doc_fold_tests.erl b/src/fabric/test/fabric2_doc_fold_tests.erl
new file mode 100644
index 0000000..caa5f92
--- /dev/null
+++ b/src/fabric/test/fabric2_doc_fold_tests.erl
@@ -0,0 +1,209 @@
+% 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(fabric2_doc_fold_tests).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+-define(DOC_COUNT, 50).
+
+
+doc_fold_test_() ->
+    {
+        "Test document fold operations",
+        {
+            setup,
+            fun setup/0,
+            fun cleanup/1,
+            {with, [
+                fun fold_docs_basic/1,
+                fun fold_docs_rev/1,
+                fun fold_docs_with_start_key/1,
+                fun fold_docs_with_end_key/1,
+                fun fold_docs_with_both_keys_the_same/1,
+                fun fold_docs_with_different_keys/1
+            ]}
+        }
+    }.
+
+
+setup() ->
+    Ctx = test_util:start_couch([fabric]),
+    {ok, Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]),
+    DocIdRevs = lists:map(fun(Val) ->
+        DocId = fabric2_util:uuid(),
+        Doc = #doc{
+            id = DocId,
+            body = {[{<<"value">>, Val}]}
+        },
+        {ok, Rev} = fabric2_db:update_doc(Db, Doc, []),
+        {DocId, couch_doc:rev_to_str(Rev)}
+    end, lists:seq(1, ?DOC_COUNT)),
+    {Db, lists:sort(DocIdRevs), Ctx}.
+
+
+cleanup({Db, _DocIdRevs, Ctx}) ->
+    ok = fabric2_db:delete(fabric2_db:name(Db), []),
+    test_util:stop_couch(Ctx).
+
+
+fold_docs_basic({Db, DocIdRevs, _}) ->
+    {ok, {?DOC_COUNT, Rows}} = fabric2_db:fold_docs(Db, fun fold_fun/2, []),
+    ?assertEqual(DocIdRevs, lists:reverse(Rows)).
+
+
+fold_docs_rev({Db, DocIdRevs, _}) ->
+    Opts = [{dir, rev}],
+    {ok, {?DOC_COUNT, Rows}} =
+            fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts),
+    ?assertEqual(DocIdRevs, Rows).
+
+
+fold_docs_with_start_key({Db, DocIdRevs, _}) ->
+    {StartKey, _} = hd(DocIdRevs),
+    Opts = [{start_key, StartKey}],
+    {ok, {?DOC_COUNT, Rows}}
+            = fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts),
+    ?assertEqual(DocIdRevs, lists:reverse(Rows)),
+    if length(DocIdRevs) == 1 -> ok; true ->
+        fold_docs_with_start_key({Db, tl(DocIdRevs), nil})
+    end.
+
+
+fold_docs_with_end_key({Db, DocIdRevs, _}) ->
+    RevDocIdRevs = lists:reverse(DocIdRevs),
+    {EndKey, _} = hd(RevDocIdRevs),
+    Opts = [{end_key, EndKey}],
+    {ok, {?DOC_COUNT, Rows}} =
+            fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts),
+    ?assertEqual(RevDocIdRevs, Rows),
+    if length(DocIdRevs) == 1 -> ok; true ->
+        fold_docs_with_end_key({Db, lists:reverse(tl(RevDocIdRevs)), nil})
+    end.
+
+
+fold_docs_with_both_keys_the_same({Db, DocIdRevs, _}) ->
+    lists:foreach(fun({DocId, _} = Row) ->
+        check_all_combos(Db, DocId, DocId, [Row])
+    end, DocIdRevs).
+
+
+fold_docs_with_different_keys({Db, DocIdRevs, _}) ->
+    lists:foreach(fun(_) ->
+        {StartKey, EndKey, Rows} = pick_range(DocIdRevs),
+        check_all_combos(Db, StartKey, EndKey, Rows)
+    end, lists:seq(1, 500)).
+
+
+check_all_combos(Db, StartKey, EndKey, Rows) ->
+    Opts1 = make_opts(fwd, StartKey, EndKey, true),
+    {ok, {?DOC_COUNT, Rows1}} =
+            fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts1),
+    ?assertEqual(lists:reverse(Rows), Rows1),
+
+    Opts2 = make_opts(fwd, StartKey, EndKey, false),
+    {ok, {?DOC_COUNT, Rows2}} =
+            fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts2),
+    Expect2 = if EndKey == undefined -> lists:reverse(Rows); true ->
+        lists:reverse(all_but_last(Rows))
+    end,
+    ?assertEqual(Expect2, Rows2),
+
+    Opts3 = make_opts(rev, StartKey, EndKey, true),
+    {ok, {?DOC_COUNT, Rows3}} =
+            fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts3),
+    ?assertEqual(Rows, Rows3),
+
+    Opts4 = make_opts(rev, StartKey, EndKey, false),
+    {ok, {?DOC_COUNT, Rows4}} =
+            fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts4),
+    Expect4 = if StartKey == undefined -> Rows; true ->
+        tl(Rows)
+    end,
+    ?assertEqual(Expect4, Rows4).
+
+
+
+make_opts(fwd, StartKey, EndKey, InclusiveEnd) ->
+    DirOpts = case rand:uniform() =< 0.50 of
+        true -> [{dir, fwd}];
+        false -> []
+    end,
+    StartOpts = case StartKey of
+        undefined -> [];
+        <<_/binary>> -> [{start_key, StartKey}]
+    end,
+    EndOpts = case EndKey of
+        undefined -> [];
+        <<_/binary>> when InclusiveEnd -> [{end_key, EndKey}];
+        <<_/binary>> -> [{end_key_gt, EndKey}]
+    end,
+    DirOpts ++ StartOpts ++ EndOpts;
+make_opts(rev, StartKey, EndKey, InclusiveEnd) ->
+    BaseOpts = make_opts(fwd, EndKey, StartKey, InclusiveEnd),
+    [{dir, rev}] ++ BaseOpts -- [{dir, fwd}].
+
+
+all_but_last([]) ->
+    [];
+all_but_last([_]) ->
+    [];
+all_but_last(Rows) ->
+    lists:sublist(Rows, length(Rows) - 1).
+
+
+pick_range(DocIdRevs) ->
+    {StartKey, StartRow, RestRows} = pick_start_key(DocIdRevs),
+    {EndKey, EndRow, RowsBetween} = pick_end_key(RestRows),
+    {StartKey, EndKey, StartRow ++ RowsBetween ++ EndRow}.
+
+
+pick_start_key(Rows) ->
+    case rand:uniform() =< 0.1 of
+        true ->
+            {undefined, [], Rows};
+        false ->
+            Idx = rand:uniform(length(Rows)),
+            {DocId, _} = Row = lists:nth(Idx, Rows),
+            {DocId, [Row], lists:nthtail(Idx, Rows)}
+    end.
+
+
+pick_end_key([]) ->
+    {undefined, [], []};
+
+pick_end_key(Rows) ->
+    case rand:uniform() =< 0.1 of
+        true ->
+            {undefined, [], Rows};
+        false ->
+            Idx = rand:uniform(length(Rows)),
+            {DocId, _} = Row = lists:nth(Idx, Rows),
+            Tail = lists:nthtail(Idx, Rows),
+            {DocId, [Row], Rows -- [Row | Tail]}
+    end.
+
+
+fold_fun({meta, Meta}, _Acc) ->
+    Total = fabric2_util:get_value(total, Meta),
+    {ok, {Total, []}};
+fold_fun({row, Row}, {Total, Rows}) ->
+    RowId = fabric2_util:get_value(id, Row),
+    RowId = fabric2_util:get_value(key, Row),
+    RowRev = fabric2_util:get_value(value, Row),
+    {ok, {Total, [{RowId, RowRev} | Rows]}};
+fold_fun(complete, Acc) ->
+    {ok, Acc}.
diff --git a/src/fabric/test/fabric2_fdb_tx_retry_tests.erl b/src/fabric/test/fabric2_fdb_tx_retry_tests.erl
new file mode 100644
index 0000000..c924ce5
--- /dev/null
+++ b/src/fabric/test/fabric2_fdb_tx_retry_tests.erl
@@ -0,0 +1,178 @@
+% 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(fabric2_fdb_tx_retry_tests).
+
+
+-include_lib("eunit/include/eunit.hrl").
+
+
+-define(TDEF(A), {atom_to_list(A), fun A/0}).
+
+
+meck_setup() ->
+    meck:new(erlfdb),
+    meck:new(fabric2_txids),
+    EnvSt = case application:get_env(fabric, db) of
+        {ok, Db} -> {ok, Db};
+        undefined -> undefined
+    end,
+    application:set_env(fabric, db, not_a_real_db),
+    EnvSt.
+
+
+meck_cleanup(EnvSt) ->
+    case EnvSt of
+        {ok, Db} -> application:set_env(fabric, db, Db);
+        undefined -> application:unset_env(fabric, db)
+    end,
+    meck:unload().
+
+
+retry_test_() ->
+    {
+        foreach,
+        fun meck_setup/0,
+        fun meck_cleanup/1,
+        [
+            ?TDEF(read_only_no_retry),
+            ?TDEF(read_only_commit_unknown_result),
+            ?TDEF(run_on_first_try),
+            ?TDEF(retry_when_commit_conflict),
+            ?TDEF(retry_when_txid_not_found),
+            ?TDEF(no_retry_when_txid_found)
+        ]
+    }.
+
+
+read_only_no_retry() ->
+    meck:expect(erlfdb, transactional, fun(_Db, UserFun) ->
+        UserFun(not_a_real_transaction)
+    end),
+    meck:expect(erlfdb, get_last_error, fun() -> 0 end),
+    meck:expect(erlfdb, get, fun(_, _) -> foo end),
+    meck:expect(erlfdb, is_read_only, fun(_) -> true end),
+    meck:expect(fabric2_txids, remove, fun(undefined) -> ok end),
+
+    Result = fabric2_fdb:transactional(fun(Tx) ->
+        ?assertEqual(foo, erlfdb:get(Tx, bar)),
+        did_run
+    end),
+
+    ?assertEqual(did_run, Result),
+    ?assert(meck:validate([erlfdb, fabric2_txids])).
+
+
+read_only_commit_unknown_result() ->
+    % Not 100% certain that this would ever actually
+    % happen in the wild but might as well test that
+    % we don't blow up if it does.
+    meck:expect(erlfdb, transactional, fun(_Db, UserFun) ->
+        UserFun(not_a_real_transaction)
+    end),
+    meck:expect(erlfdb, get_last_error, fun() -> 1021 end),
+    meck:expect(erlfdb, get, fun(_, _) -> foo end),
+    meck:expect(erlfdb, is_read_only, fun(_) -> true end),
+    meck:expect(fabric2_txids, remove, fun(undefined) -> ok end),
+
+    Result = fabric2_fdb:transactional(fun(Tx) ->
+        ?assertEqual(foo, erlfdb:get(Tx, bar)),
+        did_run
+    end),
+
+    ?assertEqual(did_run, Result),
+    ?assert(meck:validate([erlfdb, fabric2_txids])).
+
+
+run_on_first_try() ->
+    meck:expect(erlfdb, transactional, fun(_Db, UserFun) ->
+        UserFun(not_a_real_transaction)
+    end),
+    meck:expect(erlfdb, get_last_error, fun() -> undefined end),
+    meck:expect(erlfdb, clear, fun(_, _) -> ok end),
+    meck:expect(erlfdb, is_read_only, fun(_) -> false end),
+    meck:expect(fabric2_txids, create, fun(_, _) -> <<"a txid">> end),
+    meck:expect(erlfdb, set, fun(_, <<"a txid">>, <<>>) -> ok end),
+    meck:expect(fabric2_txids, remove, fun(<<"a txid">>) -> ok end),
+
+    Result = fabric2_fdb:transactional(fun(Tx) ->
+        ?assertEqual(ok, erlfdb:clear(Tx, bang)),
+        did_run
+    end),
+
+    ?assertEqual(did_run, Result),
+    ?assert(meck:validate([erlfdb, fabric2_txids])).
+
+
+retry_when_commit_conflict() ->
+    meck:expect(erlfdb, transactional, fun(_Db, UserFun) ->
+        UserFun(not_a_real_transaction)
+    end),
+    meck:expect(erlfdb, get_last_error, fun() -> 1020 end),
+    meck:expect(erlfdb, clear, fun(_, _) -> ok end),
+    meck:expect(erlfdb, is_read_only, fun(_) -> false end),
+    meck:expect(fabric2_txids, create, fun(_, _) -> <<"a txid">> end),
+    meck:expect(erlfdb, set, fun(_, <<"a txid">>, <<>>) -> ok end),
+    meck:expect(fabric2_txids, remove, fun(<<"a txid">>) -> ok end),
+
+    Result = fabric2_fdb:transactional(fun(Tx) ->
+        ?assertEqual(ok, erlfdb:clear(Tx, <<"foo">>)),
+        did_run
+    end),
+
+    ?assertEqual(did_run, Result),
+    ?assert(meck:validate([erlfdb, fabric2_txids])).
+
+
+retry_when_txid_not_found() ->
+    meck:expect(erlfdb, transactional, fun(_Db, UserFun) ->
+        UserFun(not_a_real_transaction)
+    end),
+    meck:expect(erlfdb, get_last_error, fun() -> 1021 end),
+    meck:expect(erlfdb, get, fun(_, <<"a txid">>) -> future end),
+    meck:expect(erlfdb, wait, fun(future) -> not_found end),
+    meck:expect(erlfdb, clear, fun(_, _) -> ok end),
+    meck:expect(erlfdb, is_read_only, fun(_) -> false end),
+    meck:expect(erlfdb, set, fun(_, <<"a txid">>, <<>>) -> ok end),
+    meck:expect(fabric2_txids, remove, fun(<<"a txid">>) -> ok end),
+
+    put('$fabric_tx_id', <<"a txid">>),
+    put('$fabric_tx_result', not_the_correct_result),
+
+    Result = fabric2_fdb:transactional(fun(Tx) ->
+        ?assertEqual(ok, erlfdb:clear(Tx, <<"foo">>)),
+        yay_not_skipped
+    end),
+
+    ?assertEqual(yay_not_skipped, Result),
+    ?assert(meck:validate([erlfdb, fabric2_txids])).
+
+
+no_retry_when_txid_found() ->
+    meck:expect(erlfdb, transactional, fun(_Db, UserFun) ->
+        UserFun(not_a_real_transaction)
+    end),
+    meck:expect(erlfdb, get_last_error, fun() -> 1021 end),
+    meck:expect(erlfdb, get, fun(_, <<"a txid">>) -> future end),
+    meck:expect(erlfdb, wait, fun(future) -> <<>> end),
+    meck:expect(fabric2_txids, remove, fun(<<"a txid">>) -> ok end),
+
+    put('$fabric_tx_id', <<"a txid">>),
+    put('$fabric_tx_result', did_not_run),
+
+    Result = fabric2_fdb:transactional(fun(_Tx) ->
+        ?assert(false),
+        did_run
+    end),
+
+    ?assertEqual(did_not_run, Result),
+    ?assert(meck:validate([erlfdb, fabric2_txids])).
\ No newline at end of file
diff --git a/src/fabric/test/fabric2_trace_db_create_tests.erl b/src/fabric/test/fabric2_trace_db_create_tests.erl
new file mode 100644
index 0000000..09cc863
--- /dev/null
+++ b/src/fabric/test/fabric2_trace_db_create_tests.erl
@@ -0,0 +1,46 @@
+% 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(fabric2_trace_db_create_tests).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+trace_test_() ->
+    {
+        "Trace operation",
+        {
+            setup,
+            fun setup/0,
+            fun cleanup/1,
+            [
+                fun create_db/0
+            ]
+        }
+    }.
+
+
+setup() ->
+    put(erlfdb_trace, "starting fabric"),
+    test_util:start_couch([fabric]).
+
+
+cleanup(Ctx) ->
+    test_util:stop_couch(Ctx).
+
+
+create_db() ->
+    put(erlfdb_trace, <<"create db">>),
+    {ok, _Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]).
diff --git a/src/fabric/test/fabric2_trace_db_delete_tests.erl b/src/fabric/test/fabric2_trace_db_delete_tests.erl
new file mode 100644
index 0000000..ddbb2c8
--- /dev/null
+++ b/src/fabric/test/fabric2_trace_db_delete_tests.erl
@@ -0,0 +1,49 @@
+% 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(fabric2_trace_db_delete_tests).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+trace_test_() ->
+    {
+        "Trace operation",
+        {
+            setup,
+            fun setup/0,
+            fun cleanup/1,
+            {with, [
+                fun delete_db/1
+            ]}
+        }
+    }.
+
+
+setup() ->
+    put(erlfdb_trace, "starting fabric"),
+    Ctx = test_util:start_couch([fabric]),
+    {ok, Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]),
+    {Db, Ctx}.
+
+
+cleanup({_Db, Ctx}) ->
+    test_util:stop_couch(Ctx).
+
+
+delete_db({Db, _}) ->
+    put(erlfdb_trace, <<"delete db">>),
+    fabric2_server:remove(fabric2_db:name(Db)),
+    ok = fabric2_db:delete(fabric2_db:name(Db), []).
diff --git a/src/fabric/test/fabric2_trace_db_open_tests.erl b/src/fabric/test/fabric2_trace_db_open_tests.erl
new file mode 100644
index 0000000..71e3301
--- /dev/null
+++ b/src/fabric/test/fabric2_trace_db_open_tests.erl
@@ -0,0 +1,50 @@
+% 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(fabric2_trace_db_open_tests).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+trace_test_() ->
+    {
+        "Trace operation",
+        {
+            setup,
+            fun setup/0,
+            fun cleanup/1,
+            {with, [
+                fun open_db/1
+            ]}
+        }
+    }.
+
+
+setup() ->
+    put(erlfdb_trace, "starting fabric"),
+    Ctx = test_util:start_couch([fabric]),
+    {ok, Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]),
+    {Db, Ctx}.
+
+
+cleanup({Db, Ctx}) ->
+    ok = fabric2_db:delete(fabric2_db:name(Db), []),
+    test_util:stop_couch(Ctx).
+
+
+open_db({Db, _}) ->
+    put(erlfdb_trace, <<"open db">>),
+    fabric2_server:remove(fabric2_db:name(Db)),
+    {ok, _Db} = fabric2_db:open(fabric2_db:name(Db), [{user_ctx, ?ADMIN_USER}]).
diff --git a/src/fabric/test/fabric2_trace_doc_create_tests.erl b/src/fabric/test/fabric2_trace_doc_create_tests.erl
new file mode 100644
index 0000000..1e0b47c
--- /dev/null
+++ b/src/fabric/test/fabric2_trace_doc_create_tests.erl
@@ -0,0 +1,86 @@
+% 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(fabric2_trace_doc_create_tests).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+doc_crud_test_() ->
+    {
+        "Test document CRUD operations",
+        {
+            setup,
+            fun setup/0,
+            fun cleanup/1,
+            {with, [
+                fun create_new_doc/1,
+                fun create_two_docs/1,
+                fun create_50_docs/1
+            ]}
+        }
+    }.
+
+
+setup() ->
+    Ctx = test_util:start_couch([fabric]),
+    {ok, Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]),
+    {Db, Ctx}.
+
+
+cleanup({Db, Ctx}) ->
+    ok = fabric2_db:delete(fabric2_db:name(Db), []),
+    test_util:stop_couch(Ctx).
+
+
+create_new_doc({Db, _}) ->
+    put(erlfdb_trace, <<"one doc">>),
+    Doc = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, _} = fabric2_db:update_doc(Db, Doc).
+
+
+create_two_docs({Db, _}) ->
+    put(erlfdb_trace, <<"two docs">>),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"bam">>, <<"baz">>}]}
+    },
+    Doc2 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"bang">>, <<"bargle">>}]}
+    },
+    {ok, _} = fabric2_db:update_docs(Db, [Doc1, Doc2]).
+
+
+create_50_docs({Db, _}) ->
+    lists:foreach(fun(_) ->
+        spawn_monitor(fun() ->
+            Name = io_lib:format("50 docs : ~w", [self()]),
+            put(erlfdb_trace, iolist_to_binary(Name)),
+            Docs = lists:map(fun(Val) ->
+                #doc{
+                    id = fabric2_util:uuid(),
+                    body = {[{<<"value">>, Val}]}
+                }
+            end, lists:seq(1, 50)),
+            {ok, _} = fabric2_db:update_docs(Db, Docs)
+        end)
+    end, lists:seq(1, 5)),
+    lists:foreach(fun(_) ->
+        receive {'DOWN', _, _, _, _} -> ok end
+    end, lists:seq(1, 5)).