Skip to content
This repository has been archived by the owner. It is now read-only.
Permalink
Browse files
Add optional fields to change feed selectors
When using selectors with `include_docs=true` can specify an optional fields
array in the POST request JSON body.

Each element in the array can be a json field (or even a key path
specified as field1.field2...). Resulting documents will contain only the
specified document fields.

For example:
`
http://.../d1/_changes?filter=_selector&include_docs=true

{
  "selector": {"z" : {"$gte" : 1} }, "fields": ["field1", "field2"]

}
`
Will first select only document with "z" value >= 1, then will return only field1 and field2 in documents.

{ "field1": "field1value", "field2": "field2value"}

(This requires a companion pr in fabric to work)

Jira: COUCHDB-2988
  • Loading branch information
nickva committed Nov 9, 2016
1 parent bd64fa1 commit b4cd6709e98ad3034f482b7c266e4533cce4b891
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 21 deletions.
@@ -197,7 +197,7 @@ get_callback_acc(Callback) when is_function(Callback, 2) ->
configure_filter("_doc_ids", Style, Req, _Db) ->
{doc_ids, Style, get_doc_ids(Req)};
configure_filter("_selector", Style, Req, _Db) ->
{selector, Style, get_selector(Req)};
{selector, Style, get_selector_and_fields(Req)};
configure_filter("_design", Style, _Req, _Db) ->
{design_docs, Style};
configure_filter("_view", Style, Req, Db) ->
@@ -269,7 +269,7 @@ filter(_Db, DocInfo, {doc_ids, Style, DocIds}) ->
false ->
[]
end;
filter(Db, DocInfo, {selector, Style, Selector}) ->
filter(Db, DocInfo, {selector, Style, {Selector, _Fields}}) ->
Docs = open_revs(Db, DocInfo, Style),
Passes = [mango_selector:match(Selector, couch_doc:to_json_obj(Doc, []))
|| Doc <- Docs],
@@ -344,12 +344,14 @@ get_doc_ids(_) ->
throw({bad_request, no_doc_ids_provided}).


get_selector({json_req, {Props}}) ->
check_selector(couch_util:get_value(<<"selector">>, Props));
get_selector(#httpd{method='POST'}=Req) ->
get_selector_and_fields({json_req, {Props}}) ->
Selector = check_selector(couch_util:get_value(<<"selector">>, Props)),
Fields = check_fields(couch_util:get_value(<<"fields">>, Props, nil)),
{Selector, Fields};
get_selector_and_fields(#httpd{method='POST'}=Req) ->
couch_httpd:validate_ctype(Req, "application/json"),
get_selector({json_req, couch_httpd:json_body_obj(Req)});
get_selector(_) ->
get_selector_and_fields({json_req, couch_httpd:json_body_obj(Req)});
get_selector_and_fields(_) ->
throw({bad_request, "Selector must be specified in POST payload"}).


@@ -378,6 +380,21 @@ check_selector(_Selector) ->
throw({bad_request, "Selector error: expected a JSON object"}).


check_fields(nil) ->
nil;
check_fields(Fields) when is_list(Fields) ->
try
{ok, Fields1} = mango_fields:new(Fields),
Fields1
catch
{mango_error, Mod, Reason0} ->
{_StatusCode, _Error, Reason} = mango_error:info(Mod, Reason0),
throw({bad_request, Reason})
end;
check_fields(_Fields) ->
throw({bad_request, "Selector error: fields must be JSON array"}).


open_ddoc(#db{name=DbName, id_tree=undefined}, DDocId) ->
case ddoc_cache:open_doc(mem3:dbname(DbName), DDocId) of
{ok, _} = Resp -> Resp;
@@ -806,23 +823,35 @@ maybe_get_changes_doc(Value, #changes_acc{include_docs=true}=Acc) ->
#changes_acc{
db = Db,
doc_options = DocOpts,
conflicts = Conflicts
conflicts = Conflicts,
filter = Filter
} = Acc,
Opts = case Conflicts of
true -> [deleted, conflicts];
false -> [deleted]
end,
Doc = couch_index_util:load_doc(Db, Value, Opts),
case Doc of
null ->
[{doc, null}];
_ ->
[{doc, couch_doc:to_json_obj(Doc, DocOpts)}]
end;
true -> [deleted, conflicts];
false -> [deleted]
end,
load_doc(Db, Value, Opts, DocOpts, Filter);

maybe_get_changes_doc(_Value, _Acc) ->
[].


load_doc(Db, Value, Opts, DocOpts, Filter) ->
case couch_index_util:load_doc(Db, Value, Opts) of
null ->
[{doc, null}];
Doc ->
[{doc, doc_to_json(Doc, DocOpts, Filter)}]
end.


doc_to_json(Doc, DocOpts, {selector, _Style, {_Selector, Fields}})
when Fields =/= nil ->
mango_fields:extract(couch_doc:to_json_obj(Doc, DocOpts), Fields);
doc_to_json(Doc, DocOpts, _Filter) ->
couch_doc:to_json_obj(Doc, DocOpts).


deleted_item(true) -> [{<<"deleted">>, true}];
deleted_item(_) -> [].

@@ -21,7 +21,8 @@
-record(row, {
id,
seq,
deleted = false
deleted = false,
doc = nil
}).

setup() ->
@@ -98,7 +99,9 @@ filter_by_selector() ->
fun should_select_when_no_result/1,
fun should_select_with_deleted_docs/1,
fun should_select_with_continuous/1,
fun should_stop_selector_when_db_deleted/1
fun should_stop_selector_when_db_deleted/1,
fun should_select_with_empty_fields/1,
fun should_select_with_fields/1
]
}
}.
@@ -500,6 +503,49 @@ should_stop_selector_when_db_deleted({DbName, _Revs}) ->
end).


should_select_with_empty_fields({DbName, _}) ->
?_test(
begin
ChArgs = #changes_args{filter = "_selector", include_docs=true},
Selector = {[{<<"_id">>, <<"doc3">>}]},
Req = {json_req, {[{<<"selector">>, Selector},
{<<"fields">>, []}]}},
Consumer = spawn_consumer(DbName, ChArgs, Req),
{Rows, LastSeq} = wait_finished(Consumer),
{ok, Db} = couch_db:open_int(DbName, []),
UpSeq = couch_db:get_update_seq(Db),
couch_db:close(Db),
stop_consumer(Consumer),
?assertEqual(1, length(Rows)),
[#row{seq = Seq, id = Id, doc = Doc}] = Rows,
?assertEqual(<<"doc3">>, Id),
?assertEqual(6, Seq),
?assertEqual(UpSeq, LastSeq),
?assertMatch({[{_K1, _V1}, {_K2, _V2}]}, Doc)
end).

should_select_with_fields({DbName, _}) ->
?_test(
begin
ChArgs = #changes_args{filter = "_selector", include_docs=true},
Selector = {[{<<"_id">>, <<"doc3">>}]},
Req = {json_req, {[{<<"selector">>, Selector},
{<<"fields">>, [<<"_id">>, <<"nope">>]}]}},
Consumer = spawn_consumer(DbName, ChArgs, Req),
{Rows, LastSeq} = wait_finished(Consumer),
{ok, Db} = couch_db:open_int(DbName, []),
UpSeq = couch_db:get_update_seq(Db),
couch_db:close(Db),
stop_consumer(Consumer),
?assertEqual(1, length(Rows)),
[#row{seq = Seq, id = Id, doc = Doc}] = Rows,
?assertEqual(<<"doc3">>, Id),
?assertEqual(6, Seq),
?assertEqual(UpSeq, LastSeq),
?assertMatch(Doc, {[{<<"_id">>, <<"doc3">>}]})
end).


should_emit_only_design_documents({DbName, Revs}) ->
?_test(
begin
@@ -793,7 +839,8 @@ spawn_consumer(DbName, ChangesArgs0, Req) ->
Id = couch_util:get_value(<<"id">>, Change),
Seq = couch_util:get_value(<<"seq">>, Change),
Del = couch_util:get_value(<<"deleted">>, Change, false),
[#row{id = Id, seq = Seq, deleted = Del} | Acc];
Doc = couch_util:get_value(doc, Change, nil),
[#row{id = Id, seq = Seq, deleted = Del, doc = Doc} | Acc];
({stop, LastSeq}, _, Acc) ->
Parent ! {consumer_finished, lists:reverse(Acc), LastSeq},
stop_loop(Parent, Acc);

0 comments on commit b4cd670

Please sign in to comment.