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/07/23 21:54:46 UTC

[couchdb] 01/31: CouchDB map indexes on FDB

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

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

commit ed46cb750969d0c42f0fa91de7d68cc08ba013bd
Author: Garren Smith <ga...@gmail.com>
AuthorDate: Mon Jun 17 15:45:10 2019 +0200

    CouchDB map indexes on FDB
    
    This adds couch_views which builds map indexes and stores them in FDB.
---
 rebar.config.script                                |   1 +
 rel/overlay/etc/default.ini                        |   6 +
 rel/reltool.config                                 |   2 +
 src/chttpd/src/chttpd_db.erl                       |   3 +-
 src/chttpd/src/chttpd_view.erl                     |   2 +-
 src/couch_mrview/src/couch_mrview_util.erl         |   2 +-
 src/couch_views/.gitignore                         |  19 +
 src/couch_views/README.md                          |  16 +
 src/couch_views/include/couch_views.hrl            |  94 ++++
 src/couch_views/rebar.config                       |  14 +
 src/couch_views/src/couch_views.app.src            |  31 ++
 src/couch_views/src/couch_views.erl                | 115 +++++
 src/couch_views/src/couch_views_app.erl            |  31 ++
 src/couch_views/src/couch_views_encoding.erl       | 108 +++++
 src/couch_views/src/couch_views_fdb.erl            | 208 +++++++++
 src/couch_views/src/couch_views_indexer.erl        | 262 +++++++++++
 src/couch_views/src/couch_views_jobs.erl           | 122 ++++++
 src/couch_views/src/couch_views_reader.erl         | 204 +++++++++
 src/couch_views/src/couch_views_sup.erl            |  46 ++
 src/couch_views/src/couch_views_util.erl           |  83 ++++
 src/couch_views/src/couch_views_worker.erl         |  44 ++
 src/couch_views/src/couch_views_worker_server.erl  | 110 +++++
 src/couch_views/test/couch_views_encoding_test.erl |  73 ++++
 src/couch_views/test/couch_views_indexer_test.erl  | 258 +++++++++++
 src/couch_views/test/couch_views_map_test.erl      | 484 +++++++++++++++++++++
 src/fabric/src/fabric2.hrl                         |   1 +
 src/fabric/src/fabric2_view.erl                    |  81 ++++
 test/elixir/test/map_test.exs                      | 222 ++++++++++
 28 files changed, 2639 insertions(+), 3 deletions(-)

diff --git a/rebar.config.script b/rebar.config.script
index 116c040..ce79728 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -82,6 +82,7 @@ SubDirs = [
     "src/couch_stats",
     "src/couch_peruser",
     "src/couch_tests",
+    "src/couch_views",
     "src/ddoc_cache",
     "src/fabric",
     "src/couch_jobs",
diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index 8fd2261..59b7d57 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -223,6 +223,12 @@ iterations = 10 ; iterations for password hashing
 ; users_db_public = false
 ; cookie_domain = example.com
 
+; Settings for view indexing
+[couch_views]
+; type_check_period_msec = 500
+; type_check_max_jitter_msec = 500
+; change_limit = 100
+
 ; CSP (Content Security Policy) Support for _utils
 [csp]
 enable = true
diff --git a/rel/reltool.config b/rel/reltool.config
index 7b2159d..2b088be 100644
--- a/rel/reltool.config
+++ b/rel/reltool.config
@@ -42,6 +42,7 @@
         couch_stats,
         couch_event,
         couch_peruser,
+        couch_views,
         ddoc_cache,
         ets_lru,
         fabric,
@@ -100,6 +101,7 @@
     {app, couch_stats, [{incl_cond, include}]},
     {app, couch_event, [{incl_cond, include}]},
     {app, couch_peruser, [{incl_cond, include}]},
+    {app, couch_views, [{incl_cond, include}]},
     {app, ddoc_cache, [{incl_cond, include}]},
     {app, ets_lru, [{incl_cond, include}]},
     {app, fabric, [{incl_cond, include}]},
diff --git a/src/chttpd/src/chttpd_db.erl b/src/chttpd/src/chttpd_db.erl
index 0c7e4d5..785ca3f 100644
--- a/src/chttpd/src/chttpd_db.erl
+++ b/src/chttpd/src/chttpd_db.erl
@@ -334,7 +334,8 @@ handle_design_req(#httpd{
         path_parts=[_DbName, _Design, Name, <<"_",_/binary>> = Action | _Rest]
     }=Req, Db) ->
     DbName = fabric2_db:name(Db),
-    case ddoc_cache:open(DbName, <<"_design/", Name/binary>>) of
+%%    case ddoc_cache:open(DbName, <<"_design/", Name/binary>>) of
+    case fabric2_db:open_doc(Db, <<"_design/", Name/binary>>) of
     {ok, DDoc} ->
         Handler = chttpd_handlers:design_handler(Action, fun bad_action_req/3),
         Handler(Req, Db, DDoc);
diff --git a/src/chttpd/src/chttpd_view.erl b/src/chttpd/src/chttpd_view.erl
index 26107d7..849d870 100644
--- a/src/chttpd/src/chttpd_view.erl
+++ b/src/chttpd/src/chttpd_view.erl
@@ -45,7 +45,7 @@ design_doc_view(Req, Db, DDoc, ViewName, Keys) ->
     Max = chttpd:chunked_response_buffer_size(),
     VAcc = #vacc{db=Db, req=Req, threshold=Max},
     Options = [{user_ctx, Req#httpd.user_ctx}],
-    {ok, Resp} = fabric:query_view(Db, Options, DDoc, ViewName,
+    {ok, Resp} = fabric2_view:query(Db, Options, DDoc, ViewName,
             fun view_cb/2, VAcc, Args),
     {ok, Resp#vacc.resp}.
 
diff --git a/src/couch_mrview/src/couch_mrview_util.erl b/src/couch_mrview/src/couch_mrview_util.erl
index eb68124..18a4be1 100644
--- a/src/couch_mrview/src/couch_mrview_util.erl
+++ b/src/couch_mrview/src/couch_mrview_util.erl
@@ -497,7 +497,7 @@ fold_reduce({NthRed, Lang, View}, Fun,  Acc, Options) ->
 
 
 validate_args(Db, DDoc, Args0) ->
-    {ok, State} = couch_mrview_index:init(Db, DDoc),
+    {ok, State} = couch_mrview_util:ddoc_to_mrst(fabric2_db:name(Db), DDoc),
     Args1 = apply_limit(State#mrst.partitioned, Args0),
     validate_args(State, Args1).
 
diff --git a/src/couch_views/.gitignore b/src/couch_views/.gitignore
new file mode 100644
index 0000000..f1c4554
--- /dev/null
+++ b/src/couch_views/.gitignore
@@ -0,0 +1,19 @@
+.rebar3
+_*
+.eunit
+*.o
+*.beam
+*.plt
+*.swp
+*.swo
+.erlang.cookie
+ebin
+log
+erl_crash.dump
+.rebar
+logs
+_build
+.idea
+*.iml
+rebar3.crashdump
+*~
diff --git a/src/couch_views/README.md b/src/couch_views/README.md
new file mode 100644
index 0000000..dba0fcf2
--- /dev/null
+++ b/src/couch_views/README.md
@@ -0,0 +1,16 @@
+CouchDB Views
+=====
+
+This is the new application that builds and runs Map/reduce views against FoundationDB.
+Currently only map indexes are supported and it will always return the full index.
+
+Code layout:
+
+* `couch_views` - Main entry point to query a view
+* `couch_views_reader` - Reads from the index.
+* `couch_views_indexer` - Queries the changes feed from the last sequence and updates the index
+* `couch_views_fdb` - a wrapper around erlfdb
+* `couch_views_encoding` - Emitted key encoding to keep CouchDB sorting rules
+* `couch_views_worker_server` - checks for indexing jobs and spawns a worker to build it
+* `couch_views_worker` - runs couch_views_indexer and builds index along with sending updates back to jobs
+* `couch_views_jobs` - a wrapper around couch_jobs
diff --git a/src/couch_views/include/couch_views.hrl b/src/couch_views/include/couch_views.hrl
new file mode 100644
index 0000000..f5a9c8b
--- /dev/null
+++ b/src/couch_views/include/couch_views.hrl
@@ -0,0 +1,94 @@
+% 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.
+
+% indexing
+-define(VIEW_UPDATE_SEQ, 1).
+-define(VIEW_ID_RANGE,   2).
+-define(VIEW_MAP_RANGE,  3).
+-define(VIEW_BUILDS,     4).
+-define(VIEW_STATUS,     5).
+-define(VIEW_WATCH,      6).
+-define(VIEW_ROW_KEY,    7).
+-define(VIEW_ROW_VALUE,  8).
+
+% jobs api
+-define(INDEX_JOB_TYPE, <<"views">>).
+
+
+-record(mrst, {
+    sig=nil,
+    fd=nil,
+    db_name,
+    idx_name,
+    language,
+    design_opts=[],
+    seq_indexed=false,
+    keyseq_indexed=false,
+    partitioned=false,
+    lib,
+    views,
+    % update_seq=0,
+    % purge_seq=0,
+    % first_build,
+    % partial_resp_pid,
+    % doc_acc,
+    % doc_queue,
+    % write_queue,
+    qserver=nil
+}).
+
+
+-record(mrview, {
+    id_num,
+    % update_seq=0,
+    % purge_seq=0,
+    map_names=[],
+    reduce_funs=[],
+    def,
+    seq_indexed=false,
+    keyseq_indexed=false,
+    options=[]
+}).
+
+
+-define(MAX_VIEW_LIMIT, 16#10000000).
+
+
+-record(mrargs, {
+    view_type,
+    % reduce,
+
+    % preflight_fun,
+
+    start_key,
+    start_key_docid,
+    end_key,
+    end_key_docid,
+    keys,
+
+    direction = fwd,
+    limit = ?MAX_VIEW_LIMIT,
+    skip = 0,
+    % group_level = 0,
+    % group = undefined,
+    stable = false,
+    update = true,
+    multi_get = false,
+    inclusive_end = true,
+    include_docs = false,
+    doc_options = [],
+    update_seq=false,
+    conflicts,
+    % callback,
+    sorted = true
+    % extra = []
+}).
diff --git a/src/couch_views/rebar.config b/src/couch_views/rebar.config
new file mode 100644
index 0000000..362c878
--- /dev/null
+++ b/src/couch_views/rebar.config
@@ -0,0 +1,14 @@
+% 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.
+
+{cover_enabled, true}.
+{cover_print_enabled, true}.
diff --git a/src/couch_views/src/couch_views.app.src b/src/couch_views/src/couch_views.app.src
new file mode 100644
index 0000000..9e1bbe7
--- /dev/null
+++ b/src/couch_views/src/couch_views.app.src
@@ -0,0 +1,31 @@
+% 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.
+
+{application, couch_views,
+ [{description, "CouchDB Views on FDB"},
+  {vsn, git},
+  {mod, {couch_views_app, []}},
+  {registered, [
+    couch_views_sup,
+    couch_views_worker_server
+  ]},
+  {applications, [
+    kernel,
+    stdlib,
+    erlfdb,
+    couch_log,
+    config,
+    couch_stats,
+    fabric,
+    couch_jobs
+   ]}
+ ]}.
diff --git a/src/couch_views/src/couch_views.erl b/src/couch_views/src/couch_views.erl
new file mode 100644
index 0000000..4ccf0fa
--- /dev/null
+++ b/src/couch_views/src/couch_views.erl
@@ -0,0 +1,115 @@
+% 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).
+
+-export([
+    map_query/6
+]).
+
+-include("couch_views.hrl").
+
+
+map_query(Db, DDoc, ViewName, Callback, Acc0, Args0) ->
+    Args = process_args(Args0),
+    #{name := DbName} = Db,
+    {ok, Mrst} = couch_views_util:ddoc_to_mrst(DbName, DDoc),
+    maybe_build_index(Db, Mrst, Args),
+    Resp = couch_views_reader:read(Db, DDoc, ViewName, Callback, Acc0, Args),
+
+    UpdateAfter = maps:get(update, Args) == lazy,
+    if UpdateAfter == false -> ok; true ->
+        maybe_add_couch_job(Db, Mrst)
+    end,
+    Resp.
+
+
+process_args(#{} = Args) ->
+    Args1 = maps:filter(fun (_, V) -> V /= undefined end, Args),
+
+    maps:merge(#{
+        direction => fwd,
+        inclusive_end => true,
+        update => true,
+        skip => 0,
+        limit => ?MAX_VIEW_LIMIT
+    }, Args1).
+
+
+maybe_build_index(_Db, _Mrst, #{update := false}) ->
+    false;
+
+maybe_build_index(_Db, _Mrst, #{update := lazy}) ->
+    false;
+
+maybe_build_index(Db, Mrst, _Args) ->
+    {Status, Seq} = fabric2_fdb:transactional(Db, fun(TxDb) ->
+        case view_up_to_date(TxDb, Mrst) of
+            {true, UpdateSeq} ->
+                {ready, UpdateSeq};
+            {false, LatestSeq} ->
+                maybe_add_couch_job(TxDb, Mrst),
+                {false, LatestSeq}
+        end
+    end),
+
+    if Status == ready -> true; true ->
+        subscribe_and_wait_for_index(Db, Mrst, Seq)
+    end.
+
+
+view_up_to_date(Db, Mrst) ->
+    fabric2_fdb:transactional(Db, fun(TxDb) ->
+        UpdateSeq = couch_views_fdb:get_update_seq(TxDb, Mrst),
+        LastChange = fabric2_fdb:get_last_change(TxDb),
+        {UpdateSeq == LastChange, LastChange}
+    end).
+
+
+maybe_add_couch_job(TxDb, Mrst) ->
+    case couch_views_jobs:status(TxDb, Mrst) of
+        running ->
+            ok;
+        pending ->
+            ok;
+        Status when Status == finished orelse Status == not_found ->
+            couch_views_jobs:add(TxDb, Mrst)
+    end.
+
+
+subscribe_and_wait_for_index(Db, Mrst, Seq) ->
+    case couch_views_jobs:subscribe(Db, Mrst) of
+        {error, Error} ->
+            throw({error, Error});
+        {ok, finished, _} ->
+            ready;
+        {ok, Subscription, _JobState, _} ->
+            wait_for_index_ready(Subscription, Db, Mrst, Seq)
+    end.
+
+
+wait_for_index_ready(Subscription, Db, Mrst, Seq) ->
+    Out = couch_views_jobs:wait(Subscription),
+    case Out of
+        {finished, _JobData} ->
+            ready;
+        {pending, _JobData} ->
+            wait_for_index_ready(Subscription, Db, Mrst, Seq);
+        {running, #{last_seq := LastSeq}} ->
+            if LastSeq =< Seq -> ready; true ->
+                wait_for_index_ready(Subscription, Db, Mrst, Seq)
+            end;
+        {running, _JobData} ->
+            wait_for_index_ready(Subscription, Db, Mrst, Seq);
+        {error, Error} ->
+            throw({error, Error})
+    end.
diff --git a/src/couch_views/src/couch_views_app.erl b/src/couch_views/src/couch_views_app.erl
new file mode 100644
index 0000000..5ede5ef
--- /dev/null
+++ b/src/couch_views/src/couch_views_app.erl
@@ -0,0 +1,31 @@
+% 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_app).
+
+
+-behaviour(application).
+
+
+-export([
+    start/2,
+    stop/1
+]).
+
+
+start(_StartType, StartArgs) ->
+    couch_views_sup:start_link(StartArgs).
+
+
+stop(_State) ->
+    ok.
diff --git a/src/couch_views/src/couch_views_encoding.erl b/src/couch_views/src/couch_views_encoding.erl
new file mode 100644
index 0000000..3af6d7f
--- /dev/null
+++ b/src/couch_views/src/couch_views_encoding.erl
@@ -0,0 +1,108 @@
+% 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_encoding).
+
+
+-export([
+    encode/1,
+    decode/1
+]).
+
+
+-define(NULL, 16#00).
+-define(FALSE, 16#26).
+-define(TRUE, 16#27).
+-define(NUMBER, 16#40).
+-define(STRING, 16#41).
+-define(LIST, 16#42).
+-define(OBJECT, 16#43).
+
+
+encode(X) ->
+    Encoded = encode_int(X),
+    erlfdb_tuple:pack(Encoded).
+
+
+decode(EncodedVal) ->
+    Val = erlfdb_tuple:unpack(EncodedVal),
+    decode_int(Val).
+
+
+encode_int(X) when is_atom(X) -> encode_atom(X);
+encode_int(X) when is_number(X) -> encode_number(X);
+encode_int(X) when is_binary(X) -> encode_binary(X);
+encode_int(X) when is_list(X) -> encode_list(X);
+encode_int(X) when is_tuple(X) -> encode_object(X).
+
+
+encode_atom(null) ->
+    {?NULL};
+
+encode_atom(false) ->
+    {?FALSE};
+
+encode_atom(true) ->
+    {?TRUE}.
+
+
+encode_number(Val) ->
+    {?NUMBER, float(Val)}.
+
+
+encode_binary(Val) ->
+    % TODO add sort strings
+    {?STRING, Val}.
+
+
+encode_list(List) ->
+    EncodedItems = lists:map(fun encode_int/1, List),
+    {?LIST, list_to_tuple(EncodedItems)}.
+
+
+encode_object({Props}) ->
+    EncodedProps = lists:map(fun({K, V}) -> 
+        EncodedK = encode_int(K),
+        EncodedV = encode_int(V),
+        {EncodedK, EncodedV}
+    end, Props),
+    {?OBJECT, list_to_tuple(EncodedProps)}.
+
+
+decode_int({?NULL}) ->
+    null;
+
+decode_int({?FALSE}) ->
+    false;
+
+decode_int({?TRUE}) ->
+    true;
+
+decode_int({?STRING, String}) ->
+    String;
+
+decode_int({?NUMBER, Number}) ->
+    case Number - trunc(Number) of
+        0 -> trunc(Number); % convert to integer
+        _ -> Number
+    end;
+
+decode_int({?LIST, List}) ->
+    lists:map(fun decode_int/1, tuple_to_list(List));
+
+decode_int({?OBJECT, Object}) ->
+    Props = lists:map(fun({EncodedK, EncodedV}) ->
+        K = decode_int(EncodedK),
+        V = decode_int(EncodedV),
+        {K, V}
+    end, tuple_to_list(Object)),
+    {Props}.
diff --git a/src/couch_views/src/couch_views_fdb.erl b/src/couch_views/src/couch_views_fdb.erl
new file mode 100644
index 0000000..0791ffa
--- /dev/null
+++ b/src/couch_views/src/couch_views_fdb.erl
@@ -0,0 +1,208 @@
+% 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_fdb).
+
+-export([
+    get_update_seq/2,
+    update_view_seq/3,
+    get_seq_key/2,
+
+    clear_id_index/4,
+    set_id_index/5,
+    get_id_index/4,
+    create_id_index_key/4,
+
+    clear_map_index/5,
+    set_map_index_results/5,
+    get_map_index_key/4,
+    get_map_range_keys/3,
+    get_map_range/4,
+    unpack_map_row/3
+]).
+
+
+-define(LIST_VALUE, 0).
+-define(JSON_VALUE, 1).
+-define(VALUE, 2).
+
+
+-include_lib("fabric/src/fabric2.hrl").
+-include("couch_views.hrl").
+
+% View Build Sequence Access
+% (<db>, ?DB_VIEWS, Sig, ?VIEW_UPDATE_SEQ) = Sequence
+
+get_update_seq(Db, #mrst{sig = Sig}) ->
+    #{
+        db_prefix := DbPrefix
+    } = Db,
+
+    fabric2_fdb:transactional(Db, fun(TxDb) ->
+        Key = get_seq_key(Sig, DbPrefix),
+        Tx = maps:get(tx, TxDb),
+        case erlfdb:wait(erlfdb:get(Tx, Key)) of
+            not_found -> 0;
+            UpdateSeq -> UpdateSeq
+        end
+    end).
+
+
+update_view_seq(Db, Sig, Seq) ->
+    fabric2_fdb:transactional(Db, fun(TxDb) ->
+        #{
+            db_prefix := DbPrefix,
+            tx := Tx
+        } = TxDb,
+        SeqKey = get_seq_key(Sig, DbPrefix),
+        erlfdb:set(Tx, SeqKey, Seq)
+    end).
+
+
+get_seq_key(Sig, DbPrefix) ->
+    erlfdb_tuple:pack({?DB_VIEWS, Sig, ?VIEW_UPDATE_SEQ}, DbPrefix).
+
+
+% Id Index access
+
+% (<db>, ?VIEWS, <sig>, ?VIEW_ID_INDEX, <_id>, <view_id>) -> [emitted keys]
+
+clear_id_index(TxDb, Sig, DocId, IdxName) ->
+    #{
+        db_prefix := DbPrefix,
+        tx := Tx
+    } = TxDb,
+    IdKey = create_id_index_key(DbPrefix, Sig, DocId, IdxName),
+    ok = erlfdb:clear(Tx, IdKey).
+
+
+set_id_index(TxDb, Sig, IdxName, DocId, IdxKey) ->
+    #{
+        db_prefix := DbPrefix,
+        tx := Tx
+    } = TxDb,
+    IdKey = create_id_index_key(DbPrefix, Sig, DocId, IdxName),
+    erlfdb:set(Tx, IdKey, couch_views_encoding:encode(IdxKey)).
+
+
+get_id_index(TxDb, Sig, Id, IdxName) ->
+    #{
+        db_prefix := DbPrefix,
+        tx := Tx
+    } = TxDb,
+    IdKey = create_id_index_key(DbPrefix, Sig, Id, IdxName),
+    case erlfdb:wait(erlfdb:get(Tx, IdKey)) of
+        not_found -> not_found;
+        IdxKey -> couch_views_encoding:decode(IdxKey)
+    end.
+
+
+create_id_index_key(DbPrefix, Sig, DocId, IdxName) ->
+    BaseIdKey = {?DB_VIEWS, Sig, ?VIEW_ID_RANGE, DocId, IdxName},
+    erlfdb_tuple:pack(BaseIdKey, DbPrefix).
+
+
+% Map Index Access
+% {<db>, ?DB_VIEWS, Sig, ?VIEW_MAP_RANGE, Idx, Key, DocId,
+%   RowType, Counter} = Values
+% RowType = Emitted Keys or Emitted Value
+
+
+clear_map_index(TxDb, Sig, IdxName, DocId, IdxKeys) when is_list(IdxKeys) ->
+    lists:foreach(fun (IdxKey) ->
+        clear_map_index(TxDb, Sig, IdxName, DocId, IdxKey)
+    end, IdxKeys);
+
+clear_map_index(TxDb, Sig, IdxName, DocId, IdxKey) ->
+    #{db_prefix := DbPrefix, tx := Tx} = TxDb,
+    Key = couch_views_encoding:encode(IdxKey),
+    BaseKey = {?DB_VIEWS, Sig, ?VIEW_MAP_RANGE, IdxName, Key, DocId},
+    {StartKey, EndKey} = erlfdb_tuple:range(BaseKey, DbPrefix),
+    ok = erlfdb:clear_range(Tx, StartKey, EndKey).
+
+
+set_map_index_results(TxDb, Sig, IdxName, DocId, Results) ->
+    #{db_prefix := DbPrefix, tx := Tx} = TxDb,
+    lists:foldl(fun ({IdxKey, IdxValue}, Counter) ->
+        RowKey = create_map_key(DbPrefix, Sig, IdxName, IdxKey, DocId,
+            ?VIEW_ROW_KEY, Counter),
+        RowValue = create_map_key(DbPrefix, Sig, IdxName, IdxKey, DocId,
+            ?VIEW_ROW_VALUE, Counter),
+
+        EncodedKey = pack_value(IdxKey),
+        EncodedValue = pack_value(IdxValue),
+
+        ok = erlfdb:set(Tx, RowKey, EncodedKey),
+        ok = erlfdb:set(Tx, RowValue, EncodedValue),
+        Counter + 1
+    end, 0, Results).
+
+
+get_map_index_key(#{db_prefix := DbPrefix}, Sig, IdxName, Key) ->
+    EncKey = couch_views_encoding:encode(Key),
+    erlfdb_tuple:pack({?DB_VIEWS, Sig, ?VIEW_MAP_RANGE,
+            IdxName, EncKey}, DbPrefix).
+
+
+get_map_range_keys(#{db_prefix := DbPrefix}, Sig, IdxName) ->
+    erlfdb_tuple:range({?DB_VIEWS, Sig, ?VIEW_MAP_RANGE, IdxName}, DbPrefix).
+
+
+get_map_range(TxDb, Start, End, Opts) ->
+    #{tx := Tx} = TxDb,
+    erlfdb:get_range(Tx, Start, End, Opts).
+
+
+unpack_map_row(#{db_prefix := DbPrefix}, Key, Value) ->
+    case erlfdb_tuple:unpack(Key, DbPrefix) of
+        {?DB_VIEWS, _Sig, ?VIEW_MAP_RANGE, _Idx, _RowKey, Id,
+            ?VIEW_ROW_KEY, _Counter} ->
+            RowKey = unpack_value(Value),
+            {key, Id, RowKey};
+
+        {?DB_VIEWS, _Sig, ?VIEW_MAP_RANGE, _Idx, _RowValue, Id,
+            ?VIEW_ROW_VALUE, _Counter} ->
+            RowValue = unpack_value(Value),
+            {value, Id, RowValue}
+    end.
+
+
+create_map_key(DbPrefix, Sig, IdxName, IdxKey, DocId, RowType, Counter) ->
+    Key = couch_views_encoding:encode(IdxKey),
+    BaseKey = {?DB_VIEWS, Sig, ?VIEW_MAP_RANGE,
+        IdxName, Key, DocId, RowType, Counter},
+    erlfdb_tuple:pack(BaseKey, DbPrefix).
+
+
+% Internal used to packed and unpack Values
+
+
+pack_value(Val) when is_list(Val) ->
+    erlfdb_tuple:pack({?LIST_VALUE, list_to_tuple(Val)});
+
+pack_value(Val) when is_tuple(Val) ->
+    {Props} = Val,
+    erlfdb_tuple:pack({?JSON_VALUE, list_to_tuple(Props)});
+
+pack_value(Val) ->
+    erlfdb_tuple:pack({?VALUE, Val}).
+
+
+unpack_value(Bin) ->
+    case erlfdb_tuple:unpack(Bin) of
+        {?LIST_VALUE, Val} ->
+            tuple_to_list(Val);
+        {?JSON_VALUE, Val} ->
+            {tuple_to_list(Val)};
+        {?VALUE, Val} ->
+            Val
+    end.
diff --git a/src/couch_views/src/couch_views_indexer.erl b/src/couch_views/src/couch_views_indexer.erl
new file mode 100644
index 0000000..e9f0b41
--- /dev/null
+++ b/src/couch_views/src/couch_views_indexer.erl
@@ -0,0 +1,262 @@
+% 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_indexer).
+
+-export([
+    update/2,
+    update/4,
+
+    % For tests
+    map_docs/2,
+    write_doc/4
+]).
+
+
+-include("couch_views.hrl").
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("fabric/src/fabric2.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+% TODO: 
+%  * Handle timeouts of transaction and other errors
+
+update(Db, Mrst) ->
+    Noop = fun (_) -> ok end,
+    update(Db, Mrst, Noop, []).
+
+
+update(#{} = Db, Mrst, ProgressCallback, ProgressArgs)
+        when is_function(ProgressCallback, 6) ->
+    try
+        Seq = couch_views_fdb:get_update_seq(Db, Mrst),
+        State = #{
+            since_seq => Seq,
+            count => 0,
+            limit => config:get_integer("couch_views", "change_limit", 100),
+            doc_acc => [],
+            last_seq => Seq,
+            callback => ProgressCallback,
+            callback_args => ProgressArgs,
+            mrst => Mrst
+        },
+        update_int(Db, State)
+    catch error:database_does_not_exist ->
+        #{db_prefix := DbPrefix} = Db,
+        couch_log:notice("couch_views_indexer stopped"
+        "- ~p database does not exist", [DbPrefix])
+    end.
+
+
+update_int(#{} = Db, State) ->
+    {ok, FinalState} = fabric2_fdb:transactional(Db, fun(TxDb) ->
+        State1 = maps:put(tx_db, TxDb, State),
+        fold_changes(State1)
+    end),
+
+    #{
+        count := Count,
+        limit := Limit,
+        doc_acc := DocAcc,
+        last_seq := LastSeq,
+        callback := Cb,
+        callback_args := CallbackArgs,
+        mrst := Mrst
+    } = FinalState,
+
+    {MappedResults, Mrst1} = map_docs(Mrst, DocAcc),
+    write_docs(Db, Mrst1, MappedResults, FinalState),
+
+    case Count < Limit of
+        true ->
+            Cb(undefined, finished, CallbackArgs, Db, Mrst, LastSeq);
+        false ->
+            NextState = maps:merge(FinalState, #{
+                limit => Limit,
+                count => 0,
+                doc_acc => [],
+                since_seq => LastSeq,
+                last_seq => 0,
+                mrst => Mrst1
+            }),
+            update_int(Db, NextState)
+    end.
+
+
+fold_changes(State) ->
+    #{
+        since_seq := SinceSeq,
+        limit := Limit,
+        tx_db := TxDb
+    } = State,
+
+    fabric2_db:fold_changes(TxDb, SinceSeq,
+        fun process_changes/2, State, [{limit, Limit}]).
+
+
+process_changes(Change, Acc) ->
+    #{
+        doc_acc := DocAcc,
+        count := Count,
+        tx_db := TxDb,
+        mrst := Mrst
+    } = Acc,
+
+    #{
+        id := Id,
+        sequence := LastSeq,
+        deleted := Deleted
+    } = Change,
+
+    IncludeDesign = lists:keymember(<<"include_design">>, 1,
+        Mrst#mrst.design_opts),
+
+    Acc1 = case {Id, IncludeDesign} of
+        {<<"_design/", _/binary>>, false} ->
+            % {ok, Doc} = fabric2_db:open_doc(Db, Id),
+            maps:merge(Acc, #{
+                count => Count + 1,
+                last_seq => LastSeq
+                });
+        _ ->
+
+            % Making a note here that we should make fetching all the docs
+            % a parallel fdb operation
+            Doc = if Deleted -> []; true ->
+                case fabric2_db:open_doc(TxDb, Id) of
+                    {ok, Doc0} -> Doc0;
+                    {not_found, _} -> []
+                end
+            end,
+
+            Change1 = maps:put(doc, Doc, Change),
+            maps:merge(Acc, #{
+                doc_acc => DocAcc ++ [Change1],
+                count => Count + 1,
+                last_seq => LastSeq
+            })
+    end,
+    {ok, Acc1}.
+
+
+map_docs(Mrst, Docs) ->
+    % Run all the non deleted docs through the view engine and
+    Mrst1 = get_query_server(Mrst),
+    QServer = Mrst1#mrst.qserver,
+
+    MapFun = fun
+        (#{deleted := true} = Change) ->
+            maps:put(results, [], Change);
+
+        (Change) ->
+            #{doc := Doc} = Change,
+            couch_stats:increment_counter([couchdb, mrview, map_doc]),
+            {ok, RawResults} = couch_query_servers:map_doc_raw(QServer, Doc),
+            JsonResults = couch_query_servers:raw_to_ejson(RawResults),
+            ListResults = [[list_to_tuple(Res) || Res <- FunRs]
+                || FunRs <- JsonResults],
+            maps:put(results, ListResults, Change)
+    end,
+    MappedResults = lists:map(MapFun, Docs),
+    {MappedResults, Mrst1}.
+
+
+start_query_server(#mrst{} = Mrst) ->
+    #mrst{
+        language=Language,
+        lib=Lib,
+        views=Views
+    } = Mrst,
+    Defs = [View#mrview.def || View <- Views],
+    {ok, QServer} = couch_query_servers:start_doc_map(Language, Defs, Lib),
+    Mrst#mrst{qserver=QServer}.
+
+
+get_query_server(#mrst{} = Mrst) ->
+    case Mrst#mrst.qserver of
+        nil -> start_query_server(Mrst);
+        _ -> Mrst
+    end.
+
+
+write_docs(Db, Mrst, Docs, State) ->
+    #mrst{
+        views = Views,
+        sig = Sig
+    } = Mrst,
+
+    #{
+        callback := Cb,
+        callback_args := CallbackArgs
+    } = State,
+
+    IdxNames = lists:map(fun (View) ->
+        View#mrview.id_num
+    end, Views),
+
+    lists:foreach(fun (Doc) ->
+        #{sequence := Seq} = Doc,
+        fabric2_fdb:transactional(Db, fun(TxDb) ->
+            couch_views_fdb:update_view_seq(TxDb, Sig, Seq),
+            Cb(TxDb, update, CallbackArgs, Db, Mrst, Seq),
+            write_doc(TxDb, Sig, Doc, IdxNames)
+        end)
+    end, Docs).
+
+
+write_doc(TxDb, Sig, #{deleted := true} = Doc, ViewIds) ->
+    #{id := DocId} = Doc,
+    lists:foreach(fun (IdxName) ->
+        maybe_clear_id_and_map_index(TxDb, Sig, DocId, IdxName)
+    end, ViewIds);
+
+write_doc(TxDb, Sig, Doc, ViewIds) ->
+    #{id := DocId, results := Results} = Doc,
+    lists:foreach(fun
+        ({IdxName, []}) ->
+            maybe_clear_id_and_map_index(TxDb, Sig, DocId, IdxName);
+        ({IdxName, IdxResults}) ->
+            lists:foldl(fun (IdxResult, DocIdsCleared) ->
+                {IdxKey, _} = IdxResult,
+                OldIdxKey = couch_views_fdb:get_id_index(TxDb, Sig,
+                    DocId, IdxName),
+                IsAlreadyCleared = lists:member(DocId, DocIdsCleared),
+                case OldIdxKey == not_found orelse IsAlreadyCleared == true of
+                    true ->
+                        couch_views_fdb:set_id_index(TxDb, Sig, IdxName,
+                            DocId, IdxKey),
+                        couch_views_fdb:set_map_index_results(TxDb, Sig,
+                            IdxName, DocId, IdxResults);
+                    false ->
+                        couch_views_fdb:clear_id_index(TxDb, Sig,
+                            DocId, IdxName),
+                        couch_views_fdb:clear_map_index(TxDb, Sig, IdxName,
+                            DocId, OldIdxKey),
+                        couch_views_fdb:set_id_index(TxDb, Sig, DocId,
+                            IdxName, IdxKey),
+                        couch_views_fdb:set_map_index_results(TxDb, Sig,
+                            IdxName, DocId, IdxResults)
+                end,
+                [DocId | DocIdsCleared]
+            end, [], IdxResults)
+    end, lists:zip(ViewIds, Results)).
+
+
+maybe_clear_id_and_map_index(TxDb, Sig, DocId, IdxName) ->
+    OldIdxKey = couch_views_fdb:get_id_index(TxDb, Sig,
+        DocId, IdxName),
+    if OldIdxKey == not_found -> ok; true ->
+        couch_views_fdb:clear_id_index(TxDb, Sig,
+            DocId, IdxName),
+        couch_views_fdb:clear_map_index(TxDb, Sig, IdxName,
+            DocId, OldIdxKey)
+    end.
diff --git a/src/couch_views/src/couch_views_jobs.erl b/src/couch_views/src/couch_views_jobs.erl
new file mode 100644
index 0000000..ff99475
--- /dev/null
+++ b/src/couch_views/src/couch_views_jobs.erl
@@ -0,0 +1,122 @@
+% 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_jobs).
+
+-export([
+    status/2,
+    add/2,
+
+    accept/0,
+    get_job_data/1,
+    update/5,
+    finish/5,
+    set_timeout/0,
+
+    subscribe/2,
+    wait/1,
+    unsubscribe/1,
+
+    create_job_id/2
+]).
+
+
+-include("couch_views.hrl").
+
+
+% Query request usage of jobs
+
+
+status(TxDb, Mrst) ->
+    JobId = create_job_id(TxDb, Mrst),
+
+    case couch_jobs:get_job_state(TxDb, ?INDEX_JOB_TYPE, JobId) of
+        {ok, State} -> State;
+        {error, not_found} -> not_found;
+        Error -> Error
+    end.
+
+
+add(TxDb, Mrst) ->
+    JobData = create_job_data(TxDb, Mrst, 0),
+
+    JobId = create_job_id(TxDb, Mrst),
+    JTx = couch_jobs_fdb:get_jtx(),
+    couch_jobs:add(JTx, ?INDEX_JOB_TYPE, JobId, JobData).
+
+
+% couch_views_worker api
+
+
+accept() ->
+    couch_jobs:accept(?INDEX_JOB_TYPE).
+
+
+get_job_data(JobId) ->
+    couch_jobs:get_job_data(undefined, ?INDEX_JOB_TYPE, JobId).
+
+
+update(JTx, Job, Db, Mrst, LastSeq) ->
+    JobData = create_job_data(Db, Mrst, LastSeq),
+    couch_jobs:update(JTx, Job, JobData).
+
+
+finish(JTx, Job, Db, Mrst, LastSeq) ->
+    JobData = create_job_data(Db, Mrst, LastSeq),
+    couch_jobs:finish(JTx, Job, JobData).
+
+
+set_timeout() ->
+    couch_jobs:set_type_timeout(?INDEX_JOB_TYPE, 6 * 1000).
+
+
+% Watcher Job api
+
+
+subscribe(Db, Mrst) ->
+    JobId = create_job_id(Db, Mrst),
+    couch_jobs:subscribe(?INDEX_JOB_TYPE, JobId).
+
+
+wait(JobSubscription) ->
+    case couch_jobs:wait(JobSubscription, infinity) of
+        {?INDEX_JOB_TYPE, _JobId, JobState, JobData} -> {JobState, JobData};
+        {timeout} -> {error, timeout}
+    end.
+
+
+unsubscribe(JobSubscription) ->
+    couch_jobs:unsubscribe(JobSubscription).
+
+
+% Internal
+
+
+create_job_id(#{name := DbName}, #mrst{sig = Sig}) ->
+    create_job_id(DbName, Sig);
+
+create_job_id(DbName, Sig) ->
+    <<DbName/binary, Sig/binary>>.
+
+
+create_job_data(Db, Mrst, LastSeq) ->
+    #{name := DbName} = Db,
+
+    #mrst{
+        idx_name = DDocId
+    } = Mrst,
+
+    #{
+        db_name => DbName,
+        ddoc_id => DDocId,
+        last_seq => LastSeq
+    }.
diff --git a/src/couch_views/src/couch_views_reader.erl b/src/couch_views/src/couch_views_reader.erl
new file mode 100644
index 0000000..2ddb5b6
--- /dev/null
+++ b/src/couch_views/src/couch_views_reader.erl
@@ -0,0 +1,204 @@
+% 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_reader).
+
+-export([
+    read/6
+]).
+
+
+-include("couch_views.hrl").
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("fabric/src/fabric2.hrl").
+
+
+read(Db, DDoc, ViewName, Callback, Acc0, Args) ->
+    #{name := DbName} = Db,
+
+    {ok, Mrst} = couch_views_util:ddoc_to_mrst(DbName, DDoc),
+    #mrst{
+        sig = Sig,
+        views = Views
+    } = Mrst,
+
+    IdxName = get_idx_name(ViewName, Views),
+    State0 = #{
+        acc => Acc0,
+        skip => maps:get(skip, Args, 0),
+        include_docs => maps:get(include_docs, Args, false),
+        db => Db
+    },
+
+    DefaultOpts = [{streaming_mode, want_all}],
+    {Start, End, QueryOpts} = convert_args_to_fdb(Db, Sig, IdxName, Args,
+        DefaultOpts),
+    Opts = QueryOpts ++ DefaultOpts,
+
+    fabric2_fdb:transactional(Db, fun(TxDb) ->
+        Future = couch_views_fdb:get_map_range(TxDb, Start, End, Opts),
+
+        UnPack = get_unpack_fun(TxDb, Opts, Callback),
+        State1 = lists:foldl(UnPack, State0, erlfdb:wait(Future)),
+
+        #{acc := Acc1} = State1,
+        Callback(complete, Acc1)
+    end).
+
+
+get_idx_name(ViewName, Views) ->
+    {value, View} = lists:search(fun (View) ->
+        lists:member(ViewName, View#mrview.map_names)
+    end, Views),
+    View#mrview.id_num.
+
+
+convert_args_to_fdb(Db, Sig, IdxName, Args, Opts) ->
+    #{
+        direction := Direction
+    } = Args,
+
+    {Start1, End1} = get_range_keys(Db, Sig, IdxName, Args),
+
+    Opts1 = case maps:is_key(limit, Args) of
+        false ->
+            Opts;
+        true ->
+            Skip = maps:get(skip, Args, 0),
+            Limit = maps:get(limit, Args),
+            % Limit is multiplied by two because there are two rows per key
+            % value.
+            % Skip is added because that is done in the fold so we need
+            % to fetch the number of documents
+            % along with the docs we would skip.
+            % Limit = (Doc limit + Skip) * Num of Rows per Map KV
+            [{limit, (Limit + Skip) * 2} | Opts]
+    end,
+
+    Opts2 = case Direction of
+        fwd ->
+            Opts1;
+        rev ->
+            [{reverse, true} | Opts1]
+    end,
+    {Start1, End1, Opts2}.
+
+
+get_range_keys(Db, Sig, IdxName, Args) ->
+    #{
+        inclusive_end := InclusiveEnd,
+        direction := Direction
+    } = Args,
+
+    {MapStartKey, MapEndKey} = case Direction of
+        fwd -> {start_key, end_key};
+        rev -> {end_key, start_key}
+    end,
+
+    {Start0, End0} = couch_views_fdb:get_map_range_keys(Db, Sig, IdxName),
+
+    Start1 = case maps:is_key(MapStartKey, Args) of
+        false ->
+            Start0;
+        true ->
+            StartKey = maps:get(MapStartKey, Args),
+            Start = couch_views_fdb:get_map_index_key(Db, Sig, IdxName,
+                StartKey),
+            erlfdb_key:first_greater_or_equal(Start)
+    end,
+
+    End1 = case maps:is_key(MapEndKey, Args) of
+        false ->
+            End0;
+        true ->
+            EndKey = maps:get(MapEndKey, Args),
+            EndBin = couch_views_fdb:get_map_index_key(Db, Sig, IdxName,
+                EndKey),
+            EndBin1 = case InclusiveEnd of
+                true -> <<EndBin/binary, 16#FF>>;
+                false -> EndBin
+            end,
+            erlfdb_key:first_greater_than(EndBin1)
+    end,
+    {Start1, End1}.
+
+
+get_unpack_fun(TxDb, Opts, Callback) ->
+    UnPackFwd = fun({K, V}, State) ->
+        case couch_views_fdb:unpack_map_row(TxDb, K, V) of
+            {key, _Id, RowKey} ->
+                maps:put(current_key, RowKey, State);
+            {value, Id, RowValue} ->
+                #{
+                    current_key := RowKey,
+                    acc := Acc,
+                    skip := Skip,
+                    db := Db
+                } = State,
+
+                case Skip > 0 of
+                    true ->
+                        maps:put(skip, Skip - 1, State);
+                    false ->
+                        Row = [{id, Id}, {key, RowKey}, {value, RowValue}],
+
+                        IncludeDoc = maps:get(include_docs, State, false),
+                        Row1 = maybe_include_doc(Db, Id, Row, IncludeDoc),
+
+                        {ok, AccNext} = Callback({row, Row1}, Acc),
+                        maps:put(acc, AccNext, State)
+                end
+        end
+    end,
+
+    UnPackRev = fun({K, V}, State) ->
+        case couch_views_fdb:unpack_map_row(TxDb, K, V) of
+            {key, Id, RowKey} ->
+                #{
+                    current_value := RowValue,
+                    acc := Acc,
+                    skip := Skip,
+                    db := Db
+                } = State,
+
+                case Skip > 0 of
+                    true ->
+                        maps:put(skip, Skip - 1, State);
+                    false ->
+                        Row = [{id, Id}, {key, RowKey}, {value, RowValue}],
+
+                        IncludeDoc = maps:get(include_docs, State, false),
+                        Row1 = maybe_include_doc(Db, Id, Row, IncludeDoc),
+
+                        {ok, AccNext} = Callback({row, Row1}, Acc),
+                        maps:put(acc, AccNext, State)
+                end;
+            {value, _Id, RowValue} ->
+                maps:put(current_value, RowValue, State)
+        end
+    end,
+
+    case lists:keyfind(reverse, 1, Opts) of
+        {reverse, true} -> UnPackRev;
+        _ -> UnPackFwd
+    end.
+
+
+maybe_include_doc(_Db, _Id, Row, false) ->
+    Row;
+
+maybe_include_doc(Db, Id, Row, true) ->
+    Doc1 = case fabric2_db:open_doc(Db, Id) of
+        {ok, Doc} -> couch_doc:to_json_obj(Doc, []);
+        {not_found, _} -> []
+    end,
+    Row ++ [{doc, Doc1}].
diff --git a/src/couch_views/src/couch_views_sup.erl b/src/couch_views/src/couch_views_sup.erl
new file mode 100644
index 0000000..da7d796
--- /dev/null
+++ b/src/couch_views/src/couch_views_sup.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(couch_views_sup).
+
+
+-behaviour(supervisor).
+
+
+-export([
+    start_link/1
+]).
+
+
+-export([
+    init/1
+]).
+
+
+start_link(Args) ->
+    supervisor:start_link({local, ?MODULE}, ?MODULE, Args).
+
+
+init([]) ->
+    Flags = #{
+        strategy => one_for_one,
+        intensity => 1,
+        period => 5
+    },
+    Children = [
+        #{
+            id => couch_views_worker_server,
+            start => {couch_views_worker_server, start_link, []}
+        }
+    ],
+    {ok, {Flags, Children}}.
diff --git a/src/couch_views/src/couch_views_util.erl b/src/couch_views/src/couch_views_util.erl
new file mode 100644
index 0000000..d7ed29f
--- /dev/null
+++ b/src/couch_views/src/couch_views_util.erl
@@ -0,0 +1,83 @@
+% 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_util).
+
+
+-export([
+    ddoc_to_mrst/2
+]).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include("couch_views.hrl").
+
+
+ddoc_to_mrst(DbName, #doc{id=Id, body={Fields}}) ->
+    MakeDict = fun({Name, {MRFuns}}, DictBySrcAcc) ->
+        case couch_util:get_value(<<"map">>, MRFuns) of
+            MapSrc when MapSrc /= undefined ->
+                RedSrc = couch_util:get_value(<<"reduce">>, MRFuns, null),
+                {ViewOpts} = couch_util:get_value(<<"options">>, MRFuns, {[]}),
+                View = case dict:find({MapSrc, ViewOpts}, DictBySrcAcc) of
+                    {ok, View0} -> View0;
+                    error -> #mrview{def=MapSrc, options=ViewOpts}
+                end,
+                {MapNames, RedSrcs} = case RedSrc of
+                    null ->
+                        MNames = [Name | View#mrview.map_names],
+                        {MNames, View#mrview.reduce_funs};
+                    _ ->
+                        RedFuns = [{Name, RedSrc} | View#mrview.reduce_funs],
+                        {View#mrview.map_names, RedFuns}
+                end,
+                View2 = View#mrview{map_names=MapNames, reduce_funs=RedSrcs},
+                dict:store({MapSrc, ViewOpts}, View2, DictBySrcAcc);
+            undefined ->
+                DictBySrcAcc
+        end;
+        ({Name, Else}, DictBySrcAcc) ->
+            couch_log:error("design_doc_to_view_group ~s views ~p",
+                [Name, Else]),
+            DictBySrcAcc
+    end,
+    {DesignOpts} = proplists:get_value(<<"options">>, Fields, {[]}),
+    SeqIndexed = proplists:get_value(<<"seq_indexed">>, DesignOpts, false),
+    KeySeqIndexed = proplists:get_value(<<"keyseq_indexed">>,
+        DesignOpts, false),
+    Partitioned = proplists:get_value(<<"partitioned">>, DesignOpts, false),
+
+    {RawViews} = couch_util:get_value(<<"views">>, Fields, {[]}),
+    BySrc = lists:foldl(MakeDict, dict:new(), RawViews),
+
+    NumViews = fun({_, View}, N) ->
+            {View#mrview{id_num=N, seq_indexed=SeqIndexed,
+                keyseq_indexed=KeySeqIndexed}, N+1}
+    end,
+    {Views, _} = lists:mapfoldl(NumViews, 0, lists:sort(dict:to_list(BySrc))),
+
+    Language = couch_util:get_value(<<"language">>, Fields, <<"javascript">>),
+    Lib = couch_util:get_value(<<"lib">>, RawViews, {[]}),
+
+    IdxState = #mrst{
+        db_name=DbName,
+        idx_name=Id,
+        lib=Lib,
+        views=Views,
+        language=Language,
+        design_opts=DesignOpts,
+        seq_indexed=SeqIndexed,
+        keyseq_indexed=KeySeqIndexed,
+        partitioned=Partitioned
+    },
+    SigInfo = {Views, Language, DesignOpts, couch_index_util:sort_lib(Lib)},
+    {ok, IdxState#mrst{sig=couch_hash:md5_hash(term_to_binary(SigInfo))}}.
diff --git a/src/couch_views/src/couch_views_worker.erl b/src/couch_views/src/couch_views_worker.erl
new file mode 100644
index 0000000..fa641d5
--- /dev/null
+++ b/src/couch_views/src/couch_views_worker.erl
@@ -0,0 +1,44 @@
+% 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_worker).
+
+-export([
+    start/2,
+    job_progress/6
+]).
+
+
+start(Job, JobData) ->
+    {ok, Db, Mrst} = get_indexing_info(JobData),
+    % maybe we should spawn here
+    couch_views_indexer:update(Db, Mrst, fun job_progress/6, Job).
+
+
+job_progress(Tx, Progress, Job, Db, Mrst, LastSeq) ->
+    case Progress of
+        update ->
+            couch_views_jobs:update(Tx, Job, Db, Mrst, LastSeq);
+        finished ->
+            couch_views_jobs:finish(Tx, Job, Db, Mrst, LastSeq)
+    end.
+
+
+get_indexing_info(JobData) ->
+    #{
+        <<"db_name">> := DbName,
+        <<"ddoc_id">> := DDocId
+    } = JobData,
+    {ok, Db} = fabric2_db:open(DbName, []),
+    {ok, DDoc} = fabric2_db:open_doc(Db, DDocId),
+    {ok, Mrst} = couch_views_util:ddoc_to_mrst(DbName, DDoc),
+    {ok, Db, Mrst}.
diff --git a/src/couch_views/src/couch_views_worker_server.erl b/src/couch_views/src/couch_views_worker_server.erl
new file mode 100644
index 0000000..1c815e5
--- /dev/null
+++ b/src/couch_views/src/couch_views_worker_server.erl
@@ -0,0 +1,110 @@
+% 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_worker_server).
+
+
+-behaviour(gen_server).
+
+
+-export([
+    start_link/0
+]).
+
+
+-export([
+    init/1,
+    terminate/2,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3
+]).
+
+
+-define(TYPE_CHECK_PERIOD_DEFAULT, 500).
+-define(MAX_JITTER_DEFAULT, 100).
+
+
+start_link() ->
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+
+init(_) ->
+    couch_views_jobs:set_timeout(),
+    schedule_check(),
+    {ok, #{}}.
+
+
+terminate(_, _St) ->
+    ok.
+
+
+handle_call(Msg, _From, St) ->
+    {stop, {bad_call, Msg}, {bad_call, Msg}, St}.
+
+
+handle_cast(Msg, St) ->
+    {stop, {bad_cast, Msg}, St}.
+
+
+handle_info(check_for_jobs, State) ->
+    accept_jobs(),
+    schedule_check(),
+    {noreply, State};
+
+handle_info({'DOWN', _Ref, process, Pid, Reason}, St) ->
+    LogMsg = "~p : process ~p exited with ~p",
+    couch_log:error(LogMsg, [?MODULE, Pid, Reason]),
+    {noreply, St};
+
+handle_info(Msg, St) ->
+    couch_log:notice("~s ignoring info ~w", [?MODULE, Msg]),
+    {noreply, St}.
+
+
+code_change(_OldVsn, St, _Extra) ->
+    {ok, St}.
+
+
+accept_jobs() ->
+    case couch_views_jobs:accept() of
+        not_found ->
+            ok;
+        {ok, Job, JobData} ->
+            start_worker(Job, JobData),
+            % keep accepting jobs until not_found
+            accept_jobs()
+    end.
+
+
+start_worker(Job, JobData) ->
+    % TODO Should I monitor it, or let jobs do that?
+    spawn_monitor(fun () -> couch_views_worker:start(Job, JobData) end),
+    ok.
+
+
+schedule_check() ->
+    Timeout = get_period_msec(),
+    MaxJitter = max(Timeout div 2, get_max_jitter_msec()),
+    Wait = Timeout + rand:uniform(max(1, MaxJitter)),
+    timer:send_after(Wait, self(), check_for_jobs).
+
+
+get_period_msec() ->
+    config:get_integer("couch_views", "type_check_period_msec",
+        ?TYPE_CHECK_PERIOD_DEFAULT).
+
+
+get_max_jitter_msec() ->
+    config:get_integer("couch_views", "type_check_max_jitter_msec",
+        ?MAX_JITTER_DEFAULT).
diff --git a/src/couch_views/test/couch_views_encoding_test.erl b/src/couch_views/test/couch_views_encoding_test.erl
new file mode 100644
index 0000000..a73cb42
--- /dev/null
+++ b/src/couch_views/test/couch_views_encoding_test.erl
@@ -0,0 +1,73 @@
+% 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_encoding_test).
+
+-include_lib("eunit/include/eunit.hrl").
+
+val_encoding_test() ->
+    Values = [
+        null,
+        true,
+        1.0,
+        <<"a">>,
+        {[{<<"a">>, 1.0}, {<<"b">>, <<"hello">>}]}
+    ],
+    lists:foreach(fun (Val) ->
+        EncVal = couch_views_encoding:encode(Val),
+        ?assertEqual(Val, couch_views_encoding:decode(EncVal))
+    end, Values).
+
+
+correct_ordering_test() ->
+    Ordered = [
+        %  Special values sort before all other types
+        null,
+        false,
+        true,
+
+        %  Then numbers
+        % 1,
+        % 2,
+        % 3.0,
+        % 4,
+
+        1.0,
+        2.0,
+        3.0,
+        4.0,
+
+        [<<"a">>],
+        [<<"b">>],
+        [<<"b">>, <<"c">>],
+        [<<"b">>, <<"c">>, <<"a">>],
+        [<<"b">>, <<"d">>],
+        [<<"b">>, <<"d">>, <<"e">>],
+
+        % Then objects, compared each key value in the list until different.
+        % Larger objects sort after their subset objects
+        {[{<<"a">>, 1.0}]},
+        {[{<<"a">>, 2.0}]},
+        {[{<<"b">>, 1.0}]},
+        {[{<<"b">>, 2.0}]},
+
+        % Member order does matter for collation
+        {[{<<"b">>, 2.0}, {<<"a">>, 1.0}]},
+        {[{<<"b">>, 2.0}, {<<"c">>, 2.0}]}
+
+    ],
+
+    BinList = lists:map(fun couch_views_encoding:encode/1, Ordered),
+    SortedBinList = lists:sort(BinList),
+    DecodedBinList = lists:map(fun couch_views_encoding:decode/1,
+        SortedBinList),
+    ?assertEqual(Ordered, DecodedBinList).
diff --git a/src/couch_views/test/couch_views_indexer_test.erl b/src/couch_views/test/couch_views_indexer_test.erl
new file mode 100644
index 0000000..2d192a6
--- /dev/null
+++ b/src/couch_views/test/couch_views_indexer_test.erl
@@ -0,0 +1,258 @@
+% 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_indexer_test).
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("couch_mrview/include/couch_mrview.hrl").
+
+
+-define(TDEF(A), {atom_to_list(A), fun A/0}).
+
+setup() ->
+    test_util:start_couch([fabric]).
+
+
+teardown(State) ->
+    test_util:stop_couch(State).
+
+
+foreach_setup() ->
+    ok.
+
+
+foreach_teardown(_) ->
+    meck:unload().
+
+
+index_server_test_() ->
+    {
+        "Test Couch Views indexer",
+        {
+            setup,
+            fun setup/0,
+            fun teardown/1,
+            {
+                foreach,
+                fun foreach_setup/0, fun foreach_teardown/1,
+                [
+                    ?TDEF(map_docs_no_results_for_deleted),
+                    ?TDEF(map_docs_returns_sorted_results),
+                    ?TDEF(write_doc_clears_for_deleted_doc),
+                    ?TDEF(write_doc_adds_for_new_doc),
+                    ?TDEF(write_doc_clears_and_sets_for_update),
+                    ?TDEF(write_doc_clears_for_no_new_update),
+                    ?TDEF(write_doc_clears_and_updates_duplicates)
+                ]
+            }
+
+        }
+    }.
+
+
+map_docs_no_results_for_deleted() ->
+    DbName = ?tempdb,
+
+    DDoc = create_ddoc(),
+    {ok, Mrst} = couch_views_util:ddoc_to_mrst(DbName, DDoc),
+
+    Doc = #{
+        id => <<"id">>,
+        sequence => <<1111>>,
+        rev_id => <<"1-123">>,
+        deleted => true
+    },
+
+    meck:expect(couch_query_servers, start_doc_map, fun(_, _, _) ->
+        {ok, fake}
+    end),
+
+    {Results, _} = couch_views_indexer:map_docs(Mrst, [Doc]),
+
+    [#{results := DocResult}] = Results,
+    ?assertEqual([], DocResult).
+
+
+map_docs_returns_sorted_results() ->
+    DbName = ?tempdb,
+    Doc = #{
+        id => <<"id">>,
+        sequence => <<1111>>,
+        rev_id => <<"1-123">>,
+        doc => doc(1)
+    },
+
+    CompleteResult = [[{1, 1}], []],
+
+    DDoc = create_ddoc(),
+    {ok, Mrst} = couch_views_util:ddoc_to_mrst(DbName, DDoc),
+
+
+    {Results, _} = couch_views_indexer:map_docs(Mrst, [Doc]),
+    [#{results := DocResult}] = Results,
+    ?assertEqual(CompleteResult, DocResult).
+
+
+write_doc_clears_for_deleted_doc() ->
+    TxDb = #{},
+    Sig = <<123>>,
+    Doc = #{deleted => true, id => 1},
+    ViewIds = [1],
+    OldIdxKey = old_key,
+
+    meck:expect(couch_views_fdb, get_id_index, 4, old_key),
+    meck:expect(couch_views_fdb, clear_id_index, 4, ok),
+    meck:expect(couch_views_fdb, clear_map_index, 5, ok),
+
+    couch_views_indexer:write_doc(TxDb, Sig, Doc, ViewIds),
+    ?assert(meck:called(couch_views_fdb, get_id_index, [TxDb, Sig, 1, 1])),
+    ?assert(meck:called(couch_views_fdb, clear_id_index, [TxDb, Sig, 1, 1])),
+    ?assert(meck:called(couch_views_fdb, clear_map_index,
+        [TxDb, Sig, 1, 1, OldIdxKey])),
+    ?assertEqual(length(meck:history(couch_views_fdb)), 3).
+
+
+write_doc_adds_for_new_doc() ->
+    TxDb = #{},
+    Sig = <<123>>,
+    Key = <<"key">>,
+    Value = 1,
+    Results = [{Key, Value}],
+    Doc = #{
+        deleted => false,
+        id => 1,
+        results => [Results]
+    },
+    ViewIds = [1],
+
+    meck:expect(couch_views_fdb, get_id_index, 4, not_found),
+    meck:expect(couch_views_fdb, set_id_index, 5, ok),
+    meck:expect(couch_views_fdb, set_map_index_results, 5, ok),
+
+    couch_views_indexer:write_doc(TxDb, Sig, Doc, ViewIds),
+    ?assert(meck:called(couch_views_fdb, get_id_index, [TxDb, Sig, 1, 1])),
+    ?assert(meck:called(couch_views_fdb, set_id_index,
+        [TxDb, Sig, 1, 1, Key])),
+    ?assert(meck:called(couch_views_fdb, set_map_index_results,
+        [TxDb, Sig, 1, 1, Results])),
+    ?assertEqual(length(meck:history(couch_views_fdb)), 3).
+
+
+write_doc_clears_and_sets_for_update() ->
+    TxDb = #{},
+    Sig = <<123>>,
+    Key = <<"key">>,
+    Value = 1,
+    Results = [{Key, Value}],
+    Doc = #{
+        deleted => false,
+        id => 1,
+        results => [Results]
+    },
+    ViewIds = [1],
+    OldKey = oldkey,
+
+    meck:expect(couch_views_fdb, get_id_index, 4, OldKey),
+    meck:expect(couch_views_fdb, clear_id_index, 4, ok),
+    meck:expect(couch_views_fdb, clear_map_index, 5, ok),
+    meck:expect(couch_views_fdb, set_id_index, 5, ok),
+    meck:expect(couch_views_fdb, set_map_index_results, 5, ok),
+
+    couch_views_indexer:write_doc(TxDb, Sig, Doc, ViewIds),
+    ?assert(meck:called(couch_views_fdb, get_id_index, [TxDb, Sig, 1, 1])),
+    ?assert(meck:called(couch_views_fdb, clear_id_index, [TxDb, Sig, 1, 1])),
+    ?assert(meck:called(couch_views_fdb, clear_map_index,
+        [TxDb, Sig, 1, 1, OldKey])),
+    ?assert(meck:called(couch_views_fdb, set_id_index,
+        [TxDb, Sig, 1, 1, Key])),
+    ?assert(meck:called(couch_views_fdb, set_map_index_results,
+        [TxDb, Sig, 1, 1, Results])),
+    ?assertEqual(length(meck:history(couch_views_fdb)), 5).
+
+
+write_doc_clears_for_no_new_update() ->
+    TxDb = #{},
+    Sig = <<123>>,
+    Results = [],
+    Doc = #{
+        deleted => false,
+        id => 1,
+        results => [Results]
+    },
+    ViewIds = [1],
+    OldKey = oldkey,
+
+    meck:expect(couch_views_fdb, get_id_index, 4, OldKey),
+    meck:expect(couch_views_fdb, clear_id_index, 4, ok),
+    meck:expect(couch_views_fdb, clear_map_index, 5, ok),
+
+    couch_views_indexer:write_doc(TxDb, Sig, Doc, ViewIds),
+    ?assert(meck:called(couch_views_fdb, get_id_index, [TxDb, Sig, 1, 1])),
+    ?assert(meck:called(couch_views_fdb, clear_id_index, [TxDb, Sig, 1, 1])),
+    ?assert(meck:called(couch_views_fdb, clear_map_index,
+        [TxDb, Sig, 1, 1, OldKey])),
+    ?assertEqual(length(meck:history(couch_views_fdb)), 3).
+
+
+write_doc_clears_and_updates_duplicates() ->
+    TxDb = #{},
+    Sig = <<123>>,
+    Key = <<"key">>,
+    Results = [{Key, 1}, {Key, 2}],
+    Doc = #{
+        deleted => false,
+        id => 1,
+        results => [Results]
+    },
+    ViewIds = [1],
+    OldKey = oldkey,
+
+    meck:expect(couch_views_fdb, get_id_index, 4, OldKey),
+    meck:expect(couch_views_fdb, clear_id_index, 4, ok),
+    meck:expect(couch_views_fdb, clear_map_index, 5, ok),
+    meck:expect(couch_views_fdb, set_id_index, 5, ok),
+    meck:expect(couch_views_fdb, set_map_index_results, 5, ok),
+
+    couch_views_indexer:write_doc(TxDb, Sig, Doc, ViewIds),
+    ?assertEqual(meck:num_calls(couch_views_fdb, get_id_index,
+        [TxDb, Sig, 1, 1]), 2),
+    ?assertEqual(meck:num_calls(couch_views_fdb, clear_id_index,
+        [TxDb, Sig, 1, 1]), 1),
+    ?assertEqual(meck:num_calls(couch_views_fdb, set_id_index,
+        [TxDb, Sig, 1, 1, Key]), 2),
+    ?assertEqual(meck:num_calls(couch_views_fdb, clear_map_index,
+        [TxDb, Sig, 1, 1, OldKey]), 1),
+    ?assertEqual(meck:num_calls(couch_views_fdb, set_map_index_results,
+        [TxDb, Sig, 1, 1, Results]), 2),
+    ?assertEqual(length(meck:history(couch_views_fdb)), 8).
+
+
+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) ->
+    couch_doc:from_json_obj({[
+        {<<"_id">>, list_to_binary(integer_to_list(Id))},
+        {<<"val">>, Id}
+    ]}).
diff --git a/src/couch_views/test/couch_views_map_test.erl b/src/couch_views/test/couch_views_map_test.erl
new file mode 100644
index 0000000..bbad93f
--- /dev/null
+++ b/src/couch_views/test/couch_views_map_test.erl
@@ -0,0 +1,484 @@
+% 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_map_test).
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+
+
+-define(TDEF(A), {atom_to_list(A), fun A/0}).
+
+
+setup() ->
+    test_util:start_couch([fabric, couch_jobs, couch_views]).
+
+
+teardown(State) ->
+    test_util:stop_couch(State).
+
+
+map_views_test_() ->
+    {
+        "Map views",
+        {
+            setup,
+            fun setup/0,
+            fun teardown/1,
+            [
+                ?TDEF(should_map),
+                ?TDEF(should_map_with_startkey),
+                ?TDEF(should_map_with_endkey),
+                ?TDEF(should_map_with_endkey_not_inclusive),
+                ?TDEF(should_map_reverse_and_limit),
+                ?TDEF(should_map_with_range_reverse),
+                ?TDEF(should_map_with_limit_and_skip),
+                ?TDEF(should_map_with_limit_and_skip_reverse),
+                ?TDEF(should_map_with_include_docs),
+                ?TDEF(should_map_with_include_docs_reverse),
+                ?TDEF(should_map_with_startkey_with_key_array),
+                ?TDEF(should_map_with_startkey_and_endkey_with_key_array),
+                ?TDEF(should_map_empty_views),
+                ?TDEF(should_map_duplicate_keys),
+                ?TDEF(should_map_with_doc_emit),
+                ?TDEF(should_map_update_is_false),
+                ?TDEF(should_map_update_is_lazy)
+                % fun should_give_ext_size_seq_indexed_test/1
+            ]
+        }
+    }.
+
+
+should_map() ->
+    Result = run_query(<<"baz">>, #{}),
+    Expect = {ok, [
+        {row, [{id, <<"1">>}, {key, 1}, {value, 1}]},
+        {row, [{id, <<"2">>}, {key, 2}, {value, 2}]},
+        {row, [{id, <<"3">>}, {key, 3}, {value, 3}]},
+        {row, [{id, <<"4">>}, {key, 4}, {value, 4}]},
+        {row, [{id, <<"5">>}, {key, 5}, {value, 5}]},
+        {row, [{id, <<"6">>}, {key, 6}, {value, 6}]},
+        {row, [{id, <<"7">>}, {key, 7}, {value, 7}]},
+        {row, [{id, <<"8">>}, {key, 8}, {value, 8}]},
+        {row, [{id, <<"9">>}, {key, 9}, {value, 9}]},
+        {row, [{id, <<"10">>}, {key, 10}, {value, 10}]}
+    ]},
+    ?assertEqual(Expect, Result).
+
+
+should_map_with_startkey() ->
+    Result = run_query(<<"baz">>, #{start_key => 4}),
+    Expect = {ok, [
+        {row, [{id, <<"4">>}, {key, 4}, {value, 4}]},
+        {row, [{id, <<"5">>}, {key, 5}, {value, 5}]},
+        {row, [{id, <<"6">>}, {key, 6}, {value, 6}]},
+        {row, [{id, <<"7">>}, {key, 7}, {value, 7}]},
+        {row, [{id, <<"8">>}, {key, 8}, {value, 8}]},
+        {row, [{id, <<"9">>}, {key, 9}, {value, 9}]},
+        {row, [{id, <<"10">>}, {key, 10}, {value, 10}]}
+    ]},
+    ?assertEqual(Expect, Result).
+
+
+should_map_with_endkey() ->
+    Result = run_query(<<"baz">>, #{end_key => 5}),
+    Expect = {ok, [
+        {row, [{id, <<"1">>}, {key, 1}, {value, 1}]},
+        {row, [{id, <<"2">>}, {key, 2}, {value, 2}]},
+        {row, [{id, <<"3">>}, {key, 3}, {value, 3}]},
+        {row, [{id, <<"4">>}, {key, 4}, {value, 4}]},
+        {row, [{id, <<"5">>}, {key, 5}, {value, 5}]}
+    ]},
+    ?assertEqual(Expect, Result).
+
+
+should_map_with_endkey_not_inclusive() ->
+    Result = run_query(<<"baz">>, #{
+        end_key => 5,
+        inclusive_end => false
+    }),
+    Expect = {ok, [
+        {row, [{id, <<"1">>}, {key, 1}, {value, 1}]},
+        {row, [{id, <<"2">>}, {key, 2}, {value, 2}]},
+        {row, [{id, <<"3">>}, {key, 3}, {value, 3}]},
+        {row, [{id, <<"4">>}, {key, 4}, {value, 4}]}
+    ]},
+    ?assertEqual(Expect, Result).
+
+
+should_map_reverse_and_limit() ->
+    Result = run_query(<<"baz">>, #{
+        direction => rev,
+        limit => 3
+    }),
+    Expect = {ok, [
+        {row, [{id, <<"10">>}, {key, 10}, {value, 10}]},
+        {row, [{id, <<"9">>}, {key, 9}, {value, 9}]},
+        {row, [{id, <<"8">>}, {key, 8}, {value, 8}]}
+    ]},
+    ?assertEqual(Expect, Result).
+
+
+should_map_with_range_reverse() ->
+    Result = run_query(<<"baz">>, #{
+        direction => rev,
+        start_key => 5,
+        end_key => 3,
+        inclusive_end => true
+    }),
+    Expect = {ok, [
+        {row, [{id, <<"5">>}, {key, 5}, {value, 5}]},
+        {row, [{id, <<"4">>}, {key, 4}, {value, 4}]},
+        {row, [{id, <<"3">>}, {key, 3}, {value, 3}]}
+    ]},
+    ?assertEqual(Expect, Result).
+
+
+should_map_with_limit_and_skip() ->
+    Result = run_query(<<"baz">>, #{
+        start_key => 2,
+        limit => 3,
+        skip => 3
+    }),
+    Expect = {ok, [
+        {row, [{id, <<"5">>}, {key, 5}, {value, 5}]},
+        {row, [{id, <<"6">>}, {key, 6}, {value, 6}]},
+        {row, [{id, <<"7">>}, {key, 7}, {value, 7}]}
+    ]},
+    ?assertEqual(Expect, Result).
+
+
+should_map_with_limit_and_skip_reverse() ->
+    Result = run_query(<<"baz">>, #{
+        start_key => 10,
+        limit => 3,
+        skip => 3,
+        direction => rev
+    }),
+    Expect = {ok, [
+        {row, [{id, <<"7">>}, {key, 7}, {value, 7}]},
+        {row, [{id, <<"6">>}, {key, 6}, {value, 6}]},
+        {row, [{id, <<"5">>}, {key, 5}, {value, 5}]}
+    ]},
+    ?assertEqual(Expect, Result).
+
+
+should_map_with_include_docs() ->
+    Result = run_query(<<"baz">>, #{
+        start_key => 8,
+        end_key => 8,
+        include_docs => true
+    }),
+    Doc = {[
+        {<<"_id">>, <<"8">>},
+        {<<"_rev">>, <<"1-55b9a29311341e07ec0a7ca13bc1b59f">>},
+        {<<"val">>, 8}
+    ]},
+    Expect = {ok, [
+        {row, [{id, <<"8">>}, {key, 8}, {value, 8}, {doc, Doc}]}
+    ]},
+    ?assertEqual(Expect, Result).
+
+
+should_map_with_include_docs_reverse() ->
+    Result = run_query(<<"baz">>, #{
+        start_key => 8,
+        end_key => 8,
+        include_docs => true,
+        direction => rev
+    }),
+    Doc = {[
+        {<<"_id">>, <<"8">>},
+        {<<"_rev">>, <<"1-55b9a29311341e07ec0a7ca13bc1b59f">>},
+        {<<"val">>, 8}
+    ]},
+    Expect = {ok, [
+        {row, [{id, <<"8">>}, {key, 8}, {value, 8}, {doc, Doc}]}
+    ]},
+    ?assertEqual(Expect, Result).
+
+
+should_map_with_startkey_with_key_array() ->
+    Rows = [
+        {row, [{id, <<"4">>}, {key, [<<"4">>, 4]}, {value, 4}]},
+        {row, [{id, <<"5">>}, {key, [<<"5">>, 5]}, {value, 5}]},
+        {row, [{id, <<"6">>}, {key, [<<"6">>, 6]}, {value, 6}]},
+        {row, [{id, <<"7">>}, {key, [<<"7">>, 7]}, {value, 7}]},
+        {row, [{id, <<"8">>}, {key, [<<"8">>, 8]}, {value, 8}]},
+        {row, [{id, <<"9">>}, {key, [<<"9">>, 9]}, {value, 9}]}
+    ],
+
+    Result = run_query(<<"boom">>, #{
+        start_key => [<<"4">>]
+    }),
+
+    ?assertEqual({ok, Rows}, Result),
+
+    ResultRev = run_query(<<"boom">>, #{
+        start_key => [<<"9">>, 9],
+        direction => rev,
+        limit => 6
+    }),
+
+    ?assertEqual({ok, lists:reverse(Rows)}, ResultRev).
+
+
+should_map_with_startkey_and_endkey_with_key_array() ->
+    Rows = [
+        {row, [{id, <<"4">>}, {key, [<<"4">>, 4]}, {value, 4}]},
+        {row, [{id, <<"5">>}, {key, [<<"5">>, 5]}, {value, 5}]},
+        {row, [{id, <<"6">>}, {key, [<<"6">>, 6]}, {value, 6}]},
+        {row, [{id, <<"7">>}, {key, [<<"7">>, 7]}, {value, 7}]},
+        {row, [{id, <<"8">>}, {key, [<<"8">>, 8]}, {value, 8}]}
+    ],
+
+    Result = run_query(<<"boom">>, #{
+        start_key => [<<"4">>],
+        end_key => [<<"8">>, []]
+    }),
+
+    ?assertEqual({ok, Rows}, Result),
+
+    ResultRev = run_query(<<"boom">>, #{
+        start_key => [<<"8">>, []],
+        end_key => [<<"4">>],
+        direction => rev
+    }),
+
+    ?assertEqual({ok, lists:reverse(Rows)}, ResultRev),
+
+    ResultRev2 = run_query(<<"boom">>, #{
+        start_key => [<<"9">>, 9],
+        end_key => [<<"4">>],
+        direction => rev,
+        inclusive_end => false
+    }),
+
+    ?assertEqual({ok, lists:reverse(Rows)}, ResultRev2).
+
+
+should_map_empty_views() ->
+    Result = run_query(<<"bing">>, #{}),
+    Expect = {ok, []},
+    ?assertEqual(Expect, Result).
+
+
+should_map_with_doc_emit() ->
+    Result = run_query(<<"doc_emit">>, #{
+        start_key => 8,
+        limit => 1
+    }),
+    Doc = {[
+        {<<"_id">>, <<"8">>},
+        {<<"_rev">>, <<"1-55b9a29311341e07ec0a7ca13bc1b59f">>},
+        {<<"val">>, 8}
+    ]},
+    Expect = {ok, [
+        {row, [{id, <<"8">>}, {key, 8}, {value, Doc}]}
+    ]},
+    ?assertEqual(Expect, Result).
+
+
+should_map_duplicate_keys() ->
+    Result = run_query(<<"duplicate_keys">>, #{
+        limit => 6
+    }),
+    Expect = {ok, [
+        {row, [{id, <<"1">>}, {key, <<"1">>}, {value, 1}]},
+        {row, [{id, <<"1">>}, {key, <<"1">>}, {value, 2}]},
+        {row, [{id, <<"10">>}, {key, <<"10">>}, {value, 10}]},
+        {row, [{id, <<"10">>}, {key, <<"10">>}, {value, 11}]},
+        {row, [{id, <<"2">>}, {key, <<"2">>}, {value, 2}]},
+        {row, [{id, <<"2">>}, {key, <<"2">>}, {value, 3}]}
+    ]},
+    ?debugFmt("EXPE ~p ~n", [Expect]),
+    ?assertEqual(Expect, Result).
+
+
+should_map_update_is_false() ->
+    Expect = {ok, [
+        {row, [{id, <<"8">>},  {key, 8},  {value, 8}]},
+        {row, [{id, <<"9">>},  {key, 9},  {value, 9}]},
+        {row, [{id, <<"10">>}, {key, 10}, {value, 10}]}
+    ]},
+
+    Expect1 = {ok, [
+        {row, [{id, <<"8">>},  {key, 8},  {value, 8}]},
+        {row, [{id, <<"9">>},  {key, 9},  {value, 9}]},
+        {row, [{id, <<"10">>}, {key, 10}, {value, 10}]},
+        {row, [{id, <<"11">>}, {key, 11}, {value, 11}]}
+    ]},
+
+    Idx = <<"baz">>,
+    DbName = ?tempdb(),
+
+    {ok, Db} = fabric2_db:create(DbName, [{user_ctx, ?ADMIN_USER}]),
+
+    DDoc = create_ddoc(),
+    Docs = make_docs(10),
+    fabric2_db:update_docs(Db, [DDoc | Docs]),
+
+    Args1 = #{
+        start_key => 8
+    },
+
+    Result1 = couch_views:map_query(Db, DDoc, Idx, fun default_cb/2,
+        [], Args1),
+    ?assertEqual(Expect, Result1),
+
+    Doc = doc(11),
+    fabric2_db:update_doc(Db, Doc),
+
+    Args2 = #{
+        start_key => 8,
+        update => false
+    },
+
+    Result2 = couch_views:map_query(Db, DDoc, Idx, fun default_cb/2,
+        [], Args2),
+    ?assertEqual(Expect, Result2),
+
+    Result3 = couch_views:map_query(Db, DDoc, Idx, fun default_cb/2,
+        [], Args1),
+    ?assertEqual(Expect1, Result3).
+
+
+should_map_update_is_lazy() ->
+    Expect = {ok, [
+        {row, [{id, <<"8">>},  {key, 8},  {value, 8}]},
+        {row, [{id, <<"9">>},  {key, 9},  {value, 9}]},
+        {row, [{id, <<"10">>}, {key, 10}, {value, 10}]}
+    ]},
+
+    Idx = <<"baz">>,
+    DbName = ?tempdb(),
+
+    {ok, Db} = fabric2_db:create(DbName, [{user_ctx, ?ADMIN_USER}]),
+
+    DDoc = create_ddoc(),
+    Docs = make_docs(10),
+
+    fabric2_db:update_docs(Db, [DDoc | Docs]),
+
+    Args1 = #{
+        start_key => 8,
+        update => lazy
+    },
+
+    Result1 = couch_views:map_query(Db, DDoc, Idx, fun default_cb/2,
+        [], Args1),
+    ?assertEqual({ok, []}, Result1),
+
+    {ok, Mrst} = couch_views_util:ddoc_to_mrst(DbName, DDoc),
+    {ok, Subscription, _, _} = couch_views_jobs:subscribe(Db, Mrst),
+    couch_jobs:wait(Subscription, finished, 1000),
+
+    Args2 = #{
+        start_key => 8,
+        update => false
+    },
+
+    Result2 = couch_views:map_query(Db, DDoc, Idx, fun default_cb/2,
+        [], Args2),
+    ?assertEqual(Expect, Result2).
+
+
+% should_give_ext_size_seq_indexed_test(Db) ->
+%     DDoc = couch_doc:from_json_obj({[
+%         {<<"_id">>, <<"_design/seqdoc">>},
+%         {<<"options">>, {[{<<"seq_indexed">>, true}]}},
+%         {<<"views">>, {[
+%                 {<<"view1">>, {[
+%                     {<<"map">>, <<"function(doc){emit(doc._id, doc._id);}">>}
+%                 ]}}
+%             ]}
+%         }
+%     ]}),
+%     {ok, _} = couch_db:update_doc(Db, DDoc, []),
+%     {ok, Db1} = couch_db:open_int(couch_db:name(Db), []),
+%     {ok, DDoc1} = couch_db:open_doc(Db1, <<"_design/seqdoc">>, [ejson_body]),
+%     couch_mrview:query_view(Db1, DDoc1, <<"view1">>, [{update, true}]),
+%     {ok, Info} = couch_mrview:get_info(Db1, DDoc),
+%     Size = couch_util:get_nested_json_value({Info}, [sizes, external]),
+%     ok = couch_db:close(Db1),
+%     ?assert(is_number(Size)).
+
+
+run_query(Idx, Args) ->
+    DbName = ?tempdb(),
+    {ok, Db} = fabric2_db:create(DbName, [{user_ctx, ?ADMIN_USER}]),
+    DDoc = create_ddoc(),
+    Docs = make_docs(10),
+    fabric2_db:update_docs(Db, [DDoc | Docs]),
+    couch_views:map_query(Db, DDoc, Idx, fun default_cb/2, [], Args).
+
+
+default_cb(complete, Acc) ->
+    {ok, lists:reverse(Acc)};
+default_cb({final, Info}, []) ->
+    {ok, [Info]};
+default_cb({final, _}, Acc) ->
+    {ok, Acc};
+default_cb(ok, ddoc_updated) ->
+    {ok, ddoc_updated};
+default_cb(Row, Acc) ->
+    {ok, [Row | Acc]}.
+
+
+create_ddoc() ->
+    couch_doc:from_json_obj({[
+        {<<"_id">>, <<"_design/bar">>},
+        {<<"views">>, {[
+            {<<"baz">>, {[
+                {<<"map">>, <<"function(doc) {emit(doc.val, doc.val);}">>}
+            ]}},
+            {<<"boom">>, {[
+                {<<"map">>, <<
+                    "function(doc) {\n"
+                    "   emit([doc.val.toString(), doc.val], doc.val);\n"
+                    "}"
+                >>}
+            ]}},
+            {<<"bing">>, {[
+                {<<"map">>, <<"function(doc) {}">>}
+            ]}},
+            {<<"doc_emit">>, {[
+                {<<"map">>, <<"function(doc) {emit(doc.val, doc)}">>}
+            ]}},
+            {<<"duplicate_keys">>, {[
+                {<<"map">>, <<
+                    "function(doc) {\n"
+                    "   emit(doc._id, doc.val);\n"
+                    "   emit(doc._id, doc.val + 1);\n"
+                    "}">>}
+            ]}},
+            {<<"zing">>, {[
+                {<<"map">>, <<
+                    "function(doc) {\n"
+                    "  if(doc.foo !== undefined)\n"
+                    "    emit(doc.foo, 0);\n"
+                    "}"
+                >>}
+            ]}}
+        ]}}
+    ]}).
+
+
+make_docs(Count) ->
+    [doc(I) || I <- lists:seq(1, Count)].
+
+
+doc(Id) ->
+    couch_doc:from_json_obj({[
+        {<<"_id">>, list_to_binary(integer_to_list(Id))},
+        {<<"val">>, Id}
+    ]}).
diff --git a/src/fabric/src/fabric2.hrl b/src/fabric/src/fabric2.hrl
index de1d3d1..6392d12 100644
--- a/src/fabric/src/fabric2.hrl
+++ b/src/fabric/src/fabric2.hrl
@@ -46,6 +46,7 @@
 -define(DB_DOCS, 21).
 -define(DB_LOCAL_DOCS, 22).
 -define(DB_ATTS, 23).
+-define(DB_VIEWS, 24).
 
 
 % Versions
diff --git a/src/fabric/src/fabric2_view.erl b/src/fabric/src/fabric2_view.erl
new file mode 100644
index 0000000..01c9ab0
--- /dev/null
+++ b/src/fabric/src/fabric2_view.erl
@@ -0,0 +1,81 @@
+% 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_view).
+
+-export([
+    query/7
+]).
+
+-include_lib("couch_mrview/include/couch_mrview.hrl").
+
+%% @doc execute a given view.
+%%      There are many additional query args that can be passed to a view,
+%%      see <a href="http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options">
+%%      query args</a> for details.
+% -spec query(db(), [{atom(), any()}] | [],
+%         #doc{} | binary(), iodata(), callback(), any(), #mrargs{}) -> any().
+query(Db, Options, DDoc, ViewName, Callback, Acc0, QueryArgs0) ->
+    DbName = fabric2_db:name(Db),
+%%    View = name(ViewName),
+    case fabric_util:is_users_db(DbName) of
+        true ->
+            FakeDb = fabric_util:open_cluster_db(DbName, Options),
+            couch_users_db:after_doc_read(DDoc, FakeDb);
+        false ->
+            ok
+    end,
+%%    {ok, #mrst{views=Views, language=Lang}} =
+%%        couch_views_util:ddoc_to_mrst(DbName, DDoc),
+%%    QueryArgs1 = couch_mrview_util:set_view_type(QueryArgs0, View, Views),
+%%    QueryArgs1 = fabric_util:validate_args(Db, DDoc, QueryArgs0),
+    QueryArgs1 = couch_mrview_util:validate_args(Db, DDoc, QueryArgs0),
+%%    VInfo = couch_mrview_util:extract_view(Lang, QueryArgs1, View, Views),
+    case is_reduce_view(QueryArgs1) of
+        true ->
+            throw({not_implemented});
+        false ->
+            MapQueryArgs = mrargs_to_map((QueryArgs1)),
+            couch_views:map_query(Db, DDoc, ViewName, Callback,
+                Acc0, MapQueryArgs)
+    end.
+
+
+is_reduce_view(_) ->
+    false.
+
+
+name(Thing) ->
+    couch_util:to_binary(Thing).
+
+
+mrargs_to_map(#mrargs{} = Args) ->
+    #{
+        start_key => Args#mrargs.start_key,
+        start_key_docid => Args#mrargs.start_key_docid,
+        end_key => Args#mrargs.end_key,
+        end_key_docid => Args#mrargs.end_key_docid,
+        keys => Args#mrargs.keys,
+        direction => Args#mrargs.direction,
+        limit => Args#mrargs.limit,
+        skip => Args#mrargs.skip,
+        update => Args#mrargs.update,
+        multi_get => Args#mrargs.multi_get,
+        inclusive_end => Args#mrargs.inclusive_end,
+        include_docs => Args#mrargs.include_docs,
+        doc_options => Args#mrargs.doc_options,
+        update_seq => Args#mrargs.update_seq,
+        conflicts => Args#mrargs.conflicts,
+        sorted => Args#mrargs.sorted
+    }.
+
+
diff --git a/test/elixir/test/map_test.exs b/test/elixir/test/map_test.exs
new file mode 100644
index 0000000..b7a809d
--- /dev/null
+++ b/test/elixir/test/map_test.exs
@@ -0,0 +1,222 @@
+defmodule ViewMapTest do
+  use CouchTestCase
+
+  @moduledoc """
+  Test Map functionality for views
+  """
+  def get_ids(resp) do
+    %{:body => %{"rows" => rows}} = resp
+    Enum.map(rows, fn row -> row["id"] end)
+  end
+
+  defp create_map_docs(db_name) do
+    docs =
+      for i <- 1..10 do
+        group =
+          if rem(i, 3) == 0 do
+            "one"
+          else
+            "two"
+          end
+
+        doc = %{
+          :_id => "doc-id-#{i}",
+          :value => i,
+          :some => "field",
+          :group => group
+        }
+      end
+
+    resp = Couch.post("/#{db_name}/_bulk_docs", body: %{:docs => docs})
+    assert resp.status_code == 201
+  end
+
+  setup do
+    db_name = random_db_name()
+    {:ok, _} = create_db(db_name)
+    on_exit(fn -> delete_db(db_name) end)
+
+    create_map_docs(db_name)
+
+    map_fun1 = """
+      function(doc) {
+        if (doc.some) {
+            emit(doc.value , doc.value);
+        }
+
+        if (doc._id.indexOf("_design") > -1) {
+            emit(0, "ddoc")
+        }
+      }
+    """
+
+    map_fun2 = """
+      function(doc) {
+        if (doc.group) {
+          emit([doc.some, doc.group], 1);
+        }
+      }
+    """
+
+    body = %{
+      :docs => [
+        %{
+          _id: "_design/map",
+          views: %{
+            some: %{map: map_fun1},
+            map_some: %{map: map_fun2}
+          }
+        },
+        %{
+          _id: "_design/include_ddocs",
+          views: %{some: %{map: map_fun1}},
+          options: %{include_design: true}
+        }
+      ]
+    }
+
+    resp = Couch.post("/#{db_name}/_bulk_docs", body: body)
+    Enum.each(resp.body, &assert(&1["ok"]))
+
+    #        ddoc = %{
+    #            :_id => "_design/map",
+    #            views: %{
+    #                some: %{map: map_fun1},
+    #                map_some: %{map: map_fun2}
+    #            }
+    #        }
+    #        resp = Couch.put("/#{db_name}/#{ddoc._id}", body: ddoc)
+    #        IO.inspect resp
+    #        assert resp.status_code == 201
+
+    {:ok, [db_name: db_name]}
+  end
+
+  def get_reduce_result(resp) do
+    %{:body => %{"rows" => rows}} = resp
+    rows
+  end
+
+  test "query returns docs", context do
+    db_name = context[:db_name]
+
+    url = "/#{db_name}/_design/map/_view/some"
+    resp = Couch.get(url)
+    assert resp.status_code == 200
+
+    ids = get_ids(resp)
+
+    assert ids == [
+             "doc-id-1",
+             "doc-id-2",
+             "doc-id-3",
+             "doc-id-4",
+             "doc-id-5",
+             "doc-id-6",
+             "doc-id-7",
+             "doc-id-8",
+             "doc-id-9",
+             "doc-id-10"
+           ]
+
+    url = "/#{db_name}/_design/map/_view/map_some"
+    resp = Couch.get(url)
+    assert resp.status_code == 200
+
+    ids = get_ids(resp)
+
+    assert ids == [
+             "doc-id-3",
+             "doc-id-6",
+             "doc-id-9",
+             "doc-id-1",
+             "doc-id-10",
+             "doc-id-2",
+             "doc-id-4",
+             "doc-id-5",
+             "doc-id-7",
+             "doc-id-8"
+           ]
+  end
+
+  test "updated docs rebuilds index", context do
+    db_name = context[:db_name]
+
+    url = "/#{db_name}/_design/map/_view/some"
+    resp = Couch.get(url)
+    assert resp.status_code == 200
+    ids = get_ids(resp)
+
+    assert ids == [
+             "doc-id-1",
+             "doc-id-2",
+             "doc-id-3",
+             "doc-id-4",
+             "doc-id-5",
+             "doc-id-6",
+             "doc-id-7",
+             "doc-id-8",
+             "doc-id-9",
+             "doc-id-10"
+           ]
+
+    update_doc_value(db_name, "doc-id-5", 0)
+    update_doc_value(db_name, "doc-id-6", 100)
+
+    resp = Couch.get("/#{db_name}/doc-id-3")
+    doc3 = convert(resp.body)
+    resp = Couch.delete("/#{db_name}/#{doc3["_id"]}", query: %{rev: doc3["_rev"]})
+    assert resp.status_code == 200
+    #
+    resp = Couch.get("/#{db_name}/doc-id-4")
+    doc4 = convert(resp.body)
+    doc4 = Map.delete(doc4, "some")
+    resp = Couch.put("/#{db_name}/#{doc4["_id"]}", body: doc4)
+    assert resp.status_code == 201
+    #
+    resp = Couch.get("/#{db_name}/doc-id-1")
+    doc1 = convert(resp.body)
+    doc1 = Map.put(doc1, "another", "value")
+    resp = Couch.put("/#{db_name}/#{doc1["_id"]}", body: doc1)
+    assert resp.status_code == 201
+
+    url = "/#{db_name}/_design/map/_view/some"
+    resp = Couch.get(url)
+    assert resp.status_code == 200
+    ids = get_ids(resp)
+
+    assert ids == [
+             "doc-id-5",
+             "doc-id-1",
+             "doc-id-2",
+             "doc-id-7",
+             "doc-id-8",
+             "doc-id-9",
+             "doc-id-10",
+             "doc-id-6"
+           ]
+  end
+
+  test "can index design docs", context do
+    db_name = context[:db_name]
+
+    url = "/#{db_name}/_design/include_ddocs/_view/some"
+    resp = Couch.get(url, query: %{limit: 3})
+    assert resp.status_code == 200
+    ids = get_ids(resp)
+
+    assert ids == ["_design/include_ddocs", "_design/map", "doc-id-1"]
+  end
+
+  def update_doc_value(db_name, id, value) do
+    resp = Couch.get("/#{db_name}/#{id}")
+    doc = convert(resp.body)
+    doc = Map.put(doc, "value", value)
+    resp = Couch.put("/#{db_name}/#{id}", body: doc)
+    assert resp.status_code == 201
+  end
+
+  def convert(value) do
+    :jiffy.decode(:jiffy.encode(value), [:return_maps])
+  end
+end