summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul J. Davis <paul.joseph.davis@gmail.com>2019-01-18 13:07:13 -0600
committerGitHub <noreply@github.com>2019-01-18 13:07:13 -0600
commit0c591905e1a31db0e6c34f35dd0b66c72ad03db0 (patch)
tree90a7e0b4624c055f3b0326e37a08452fc0c6fccf
parent1bdca62752f03db145d14252d9c233ffcbce779c (diff)
parent389330120de5f5a652400cb66ce657b5110dfe67 (diff)
downloadcouchdb-0c591905e1a31db0e6c34f35dd0b66c72ad03db0.tar.gz
Merge pull request #34 from cloudant-labs/feature/database-partitions
Implement partitioned queries
-rw-r--r--.gitignore2
-rw-r--r--include/dreyfus.hrl1
-rw-r--r--src/clouseau_rpc.erl11
-rw-r--r--src/dreyfus_httpd.erl108
-rw-r--r--src/dreyfus_index.erl38
-rw-r--r--src/dreyfus_index_updater.erl36
-rw-r--r--src/dreyfus_util.erl80
-rw-r--r--test/elixir/mix.exs30
-rw-r--r--test/elixir/mix.lock5
-rwxr-xr-xtest/elixir/run4
-rw-r--r--test/elixir/test/partition_search_test.exs187
-rw-r--r--test/elixir/test/test_helper.exs4
12 files changed, 431 insertions, 75 deletions
diff --git a/.gitignore b/.gitignore
index 4598aa522..16fd00698 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
ebin/
.*.sw?
+test/elixir/_build
+test/elixir/deps
diff --git a/include/dreyfus.hrl b/include/dreyfus.hrl
index f86287c92..7c6a36945 100644
--- a/include/dreyfus.hrl
+++ b/include/dreyfus.hrl
@@ -32,6 +32,7 @@
-record(index_query_args, {
q,
+ partition=nil,
limit=25,
stale=false,
include_docs=false,
diff --git a/src/clouseau_rpc.erl b/src/clouseau_rpc.erl
index 345b499e6..38bf651d4 100644
--- a/src/clouseau_rpc.erl
+++ b/src/clouseau_rpc.erl
@@ -18,8 +18,8 @@
-include("dreyfus.hrl").
-export([open_index/3]).
--export([await/2, commit/2, get_update_seq/1, info/1, search/6, search/2]).
--export([group1/7, group2/8, group2/2]).
+-export([await/2, commit/2, get_update_seq/1, info/1, search/2]).
+-export([group1/7, group2/2]).
-export([delete/2, update/3, cleanup/1, cleanup/2, rename/1]).
-export([analyze/2, version/0, disk_size/1]).
-export([set_purge_seq/2, get_purge_seq/1, get_root_dir/0]).
@@ -50,10 +50,6 @@ set_purge_seq(Ref, Seq) ->
get_purge_seq(Ref) ->
rpc(Ref, get_purge_seq).
-%% @deprecated
-search(Ref, Query, Limit, Refresh, Bookmark, Sort) ->
- rpc(Ref, {search, Query, Limit, Refresh, Bookmark, Sort}).
-
search(Ref, Args) ->
case rpc(Ref, {search, Args}) of
{ok, Response} when is_list(Response) ->
@@ -71,9 +67,6 @@ search(Ref, Args) ->
group1(Ref, Query, GroupBy, Refresh, Sort, Offset, Limit) ->
rpc(Ref, {group1, Query, GroupBy, Refresh, Sort, Offset, Limit}).
-group2(Ref, Query, GroupBy, Refresh, Groups, GroupSort, DocSort, DocLimit) ->
- rpc(Ref, {group2, Query, GroupBy, Refresh, Groups, GroupSort, DocSort, DocLimit}).
-
group2(Ref, Args) ->
rpc(Ref, {group2, Args}).
diff --git a/src/dreyfus_httpd.erl b/src/dreyfus_httpd.erl
index 8db545466..a1a5cb398 100644
--- a/src/dreyfus_httpd.erl
+++ b/src/dreyfus_httpd.erl
@@ -17,6 +17,7 @@
-export([handle_search_req/3, handle_info_req/3, handle_disk_size_req/3,
handle_cleanup_req/2, handle_analyze_req/1]).
+
-include("dreyfus.hrl").
-include_lib("couch/include/couch_db.hrl").
-import(chttpd, [send_method_not_allowed/2, send_json/2, send_json/3,
@@ -31,17 +32,10 @@ handle_search_req(#httpd{method=Method, path_parts=[_, _, _, _, IndexName]}=Req
DbName = couch_db:name(Db),
Start = os:timestamp(),
QueryArgs = #index_query_args{
- q = Query,
include_docs = IncludeDocs,
grouping = Grouping
- } = parse_index_params(Req),
- case Query of
- undefined ->
- Msg = <<"Query must include a 'q' or 'query' argument">>,
- throw({query_parse_error, Msg});
- _ ->
- ok
- end,
+ } = parse_index_params(Req, Db),
+ validate_search_restrictions(Db, DDoc, QueryArgs),
Response = case Grouping#grouping.by of
nil ->
case dreyfus_fabric_search:go(DbName, DDoc, IndexName, QueryArgs) of
@@ -190,22 +184,30 @@ analyze(Req, Analyzer, Text) ->
send_error(Req, Reason)
end.
-parse_index_params(#httpd{method='GET'}=Req) ->
+parse_index_params(#httpd{method='GET'}=Req, Db) ->
IndexParams = lists:flatmap(fun({K, V}) -> parse_index_param(K, V) end,
chttpd:qs(Req)),
- parse_index_params(IndexParams);
-parse_index_params(#httpd{method='POST'}=Req) ->
+ parse_index_params(IndexParams, Db);
+parse_index_params(#httpd{method='POST'}=Req, Db) ->
IndexParams = lists:flatmap(fun({K, V}) -> parse_json_index_param(K, V) end,
element(1, chttpd:json_body_obj(Req))),
- parse_index_params(IndexParams);
-parse_index_params(IndexParams) ->
- Args = #index_query_args{},
+ parse_index_params(IndexParams, Db);
+parse_index_params(IndexParams, Db) ->
+ DefaultLimit = case fabric_util:is_partitioned(Db) of
+ true ->
+ list_to_integer(config:get("dreyfus", "limit_partitions", "2000"));
+ false ->
+ list_to_integer(config:get("dreyfus", "limit", "25"))
+ end,
+ Args = #index_query_args{limit=DefaultLimit},
lists:foldl(fun({K, V}, Args2) ->
validate_index_query(K, V, Args2)
end, Args, IndexParams).
validate_index_query(q, Value, Args) ->
Args#index_query_args{q=Value};
+validate_index_query(partition, Value, Args) ->
+ Args#index_query_args{partition=Value};
validate_index_query(stale, Value, Args) ->
Args#index_query_args{stale=Value};
validate_index_query(limit, Value, Args) ->
@@ -254,12 +256,14 @@ parse_index_param("q", Value) ->
[{q, ?l2b(Value)}];
parse_index_param("query", Value) ->
[{q, ?l2b(Value)}];
+parse_index_param("partition", Value) ->
+ [{partition, ?l2b(Value)}];
parse_index_param("bookmark", Value) ->
[{bookmark, ?l2b(Value)}];
parse_index_param("sort", Value) ->
[{sort, ?JSON_DECODE(Value)}];
parse_index_param("limit", Value) ->
- [{limit, parse_non_negative_int_param("limit", Value, "max_limit", "200")}];
+ [{limit, ?JSON_DECODE(Value)}];
parse_index_param("stale", "ok") ->
[{stale, ok}];
parse_index_param("stale", _Value) ->
@@ -301,12 +305,14 @@ parse_json_index_param(<<"q">>, Value) ->
[{q, Value}];
parse_json_index_param(<<"query">>, Value) ->
[{q, Value}];
+parse_json_index_param(<<"partition">>, Value) ->
+ [{partition, Value}];
parse_json_index_param(<<"bookmark">>, Value) ->
[{bookmark, Value}];
parse_json_index_param(<<"sort">>, Value) ->
[{sort, Value}];
parse_json_index_param(<<"limit">>, Value) ->
- [{limit, parse_non_negative_int_param("limit", Value, "max_limit", "200")}];
+ [{limit, ?JSON_DECODE(Value)}];
parse_json_index_param(<<"stale">>, <<"ok">>) ->
[{stale, ok}];
parse_json_index_param(<<"include_docs">>, Value) when is_boolean(Value) ->
@@ -418,6 +424,74 @@ parse_non_negative_int_param(Name, Val, Prop, Default) ->
end.
+validate_search_restrictions(Db, DDoc, Args) ->
+ #index_query_args{
+ q = Query,
+ partition = Partition,
+ grouping = Grouping,
+ limit = Limit
+ } = Args,
+ #grouping{
+ by = GroupBy
+ } = Grouping,
+
+ case Query of
+ undefined ->
+ Msg1 = <<"Query must include a 'q' or 'query' argument">>,
+ throw({query_parse_error, Msg1});
+ _ ->
+ ok
+ end,
+
+ DbPartitioned = fabric_util:is_partitioned(Db),
+ ViewPartitioned = get_view_partition_option(DDoc, DbPartitioned),
+
+ case not DbPartitioned andalso is_binary(Partition) of
+ true ->
+ Msg2 = <<"`partition` not supported on this index">>,
+ throw({bad_request, Msg2});
+ false ->
+ ok
+ end,
+
+ case {ViewPartitioned, is_binary(Partition)} of
+ {false, false} ->
+ ok;
+ {true, true} ->
+ ok;
+ {true, false} ->
+ Msg3 = <<"`partition` parameter is mandatory "
+ "for queries to this index.">>,
+ throw({bad_request, Msg3});
+ {false, true} ->
+ Msg4 = <<"`partition` not supported on this index">>,
+ throw({bad_request, Msg4})
+ end,
+
+ case DbPartitioned of
+ true ->
+ MaxLimit = config:get("dreyfus", "max_limit", "2000"),
+ parse_non_negative_int_param(
+ "limit", Limit, "max_limit_partitions", MaxLimit);
+ false ->
+ MaxLimit = config:get("dreyfus", "max_limit", "200"),
+ parse_non_negative_int_param("limit", Limit, "max_limit", MaxLimit)
+ end,
+
+ case GroupBy /= nil andalso is_binary(Partition) of
+ true ->
+ Msg5 = <<"`group_by` and `partition` are incompatible">>,
+ throw({bad_request, Msg5});
+ false ->
+ ok
+ end.
+
+
+get_view_partition_option(#doc{body = {Props}}, Default) ->
+ {Options} = couch_util:get_value(<<"options">>, Props, {[]}),
+ couch_util:get_value(<<"partitioned">>, Options, Default).
+
+
hits_to_json(DbName, IncludeDocs, Hits) ->
{Ids, HitData} = lists:unzip(lists:map(fun get_hit_data/1, Hits)),
if IncludeDocs ->
diff --git a/src/dreyfus_index.erl b/src/dreyfus_index.erl
index c6d4d856a..e33a208ee 100644
--- a/src/dreyfus_index.erl
+++ b/src/dreyfus_index.erl
@@ -305,6 +305,7 @@ index_name(#index{dbname=DbName,ddoc_id=DDocId,name=IndexName}) ->
args_to_proplist(#index_query_args{} = Args) ->
[
{'query', Args#index_query_args.q},
+ {partition, Args#index_query_args.partition},
{limit, Args#index_query_args.limit},
{refresh, Args#index_query_args.stale =:= false},
{'after', Args#index_query_args.bookmark},
@@ -339,20 +340,8 @@ args_to_proplist2(#index_query_args{} = Args) ->
search_int(Pid, QueryArgs0) ->
QueryArgs = dreyfus_util:upgrade(QueryArgs0),
- case QueryArgs of
- #index_query_args{counts=nil,ranges=nil,drilldown=[],include_fields=nil,
- highlight_fields=nil} ->
- clouseau_rpc:search(
- Pid,
- QueryArgs#index_query_args.q,
- QueryArgs#index_query_args.limit,
- QueryArgs#index_query_args.stale =:= false,
- QueryArgs#index_query_args.bookmark,
- QueryArgs#index_query_args.sort);
- _ ->
- Props = args_to_proplist(QueryArgs),
- clouseau_rpc:search(Pid, Props)
- end.
+ Props = args_to_proplist(QueryArgs),
+ clouseau_rpc:search(Pid, Props).
group1_int(Pid, QueryArgs0) ->
QueryArgs = dreyfus_util:upgrade(QueryArgs0),
@@ -371,25 +360,8 @@ group1_int(Pid, QueryArgs0) ->
group2_int(Pid, QueryArgs0) ->
QueryArgs = dreyfus_util:upgrade(QueryArgs0),
- case QueryArgs of
- #index_query_args{include_fields=nil, highlight_fields=nil} -> %remove after upgrade
- #index_query_args{
- q = Query,
- stale = Stale,
- sort = DocSort,
- limit = DocLimit,
- grouping = #grouping{
- by = GroupBy,
- groups = Groups,
- sort = GroupSort
- }
- } = QueryArgs,
- clouseau_rpc:group2(Pid, Query, GroupBy, Stale =:= false, Groups,
- GroupSort, DocSort, DocLimit);
- _ ->
- Props = args_to_proplist2(QueryArgs),
- clouseau_rpc:group2(Pid, Props)
- end.
+ Props = args_to_proplist2(QueryArgs),
+ clouseau_rpc:group2(Pid, Props).
info_int(Pid) ->
clouseau_rpc:info(Pid).
diff --git a/src/dreyfus_index_updater.erl b/src/dreyfus_index_updater.erl
index e2fbe2b05..40fd0c377 100644
--- a/src/dreyfus_index_updater.erl
+++ b/src/dreyfus_index_updater.erl
@@ -132,13 +132,19 @@ update_or_delete_index(IndexPid, Db, DI, Proc) ->
true ->
ok = clouseau_rpc:delete(IndexPid, Id);
false ->
- {ok, Doc} = couch_db:open_doc(Db, DI, []),
- Json = couch_doc:to_json_obj(Doc, []),
- [Fields|_] = proc_prompt(Proc, [<<"index_doc">>, Json]),
- Fields1 = [list_to_tuple(Field) || Field <- Fields],
- case Fields1 of
- [] -> ok = clouseau_rpc:delete(IndexPid, Id);
- _ -> ok = clouseau_rpc:update(IndexPid, Id, Fields1)
+ case maybe_skip_doc(Db, Id) of
+ true ->
+ ok;
+ false ->
+ {ok, Doc} = couch_db:open_doc(Db, DI, []),
+ Json = couch_doc:to_json_obj(Doc, []),
+ [Fields|_] = proc_prompt(Proc, [<<"index_doc">>, Json]),
+ Fields1 = [list_to_tuple(Field) || Field <- Fields],
+ Fields2 = maybe_add_partition(Db, Id, Fields1),
+ case Fields2 of
+ [] -> ok = clouseau_rpc:delete(IndexPid, Id);
+ _ -> ok = clouseau_rpc:update(IndexPid, Id, Fields2)
+ end
end
end.
@@ -157,3 +163,19 @@ update_task(NumChanges) ->
(Changes2 * 100) div Total
end,
couch_task_status:update([{progress, Progress}, {changes_done, Changes2}]).
+
+maybe_skip_doc(Db, <<"_design/", _/binary>>) ->
+ couch_db:is_partitioned(Db);
+maybe_skip_doc(_Db, _Id) ->
+ false.
+
+maybe_add_partition(_Db, _Id, []) ->
+ [];
+maybe_add_partition(Db, Id, Fields) ->
+ case couch_db:is_partitioned(Db) of
+ true ->
+ Partition = couch_partition:from_docid(Id),
+ [{<<"_partition">>, Partition, {[]}} | Fields];
+ false ->
+ Fields
+ end.
diff --git a/src/dreyfus_util.erl b/src/dreyfus_util.erl
index 3b3f4f955..ae3133e7d 100644
--- a/src/dreyfus_util.erl
+++ b/src/dreyfus_util.erl
@@ -33,15 +33,31 @@
verify_index_exists/2
]).
-get_shards(DbName, #index_query_args{stale=ok}) ->
- mem3:ushards(DbName);
-get_shards(DbName, #index_query_args{stable=true}) ->
- mem3:ushards(DbName);
-get_shards(DbName, #index_query_args{stale=false}) ->
- mem3:shards(DbName);
+
+get_shards(DbName, #index_query_args{partition = nil} = Args) ->
+ case use_ushards(Args) of
+ true ->
+ mem3:ushards(DbName);
+ false ->
+ mem3:shards(DbName)
+ end;
+get_shards(DbName, #index_query_args{partition = Partition} = Args) ->
+ PartitionId = couch_partition:shard_key(Partition),
+ case use_ushards(Args) of
+ true ->
+ mem3:ushards(DbName, PartitionId);
+ false ->
+ mem3:shards(DbName, PartitionId)
+ end;
get_shards(DbName, Args) ->
get_shards(DbName, upgrade(Args)).
+use_ushards(#index_query_args{stale=ok}) ->
+ true;
+use_ushards(#index_query_args{stable=true}) ->
+ true;
+use_ushards(#index_query_args{}) ->
+ false.
-spec sort(Order :: relevance | [any()], [#sortable{}]) -> [#sortable{}].
sort(Sort, List0) ->
@@ -136,10 +152,34 @@ upgrade({index_query_args, Query, Limit, Stale, IncludeDocs, Bookmark,
highlight_post_tag = HighlightPostTag,
highlight_number = HighlightNumber,
highlight_size = HighlightSize
+ };
+upgrade({index_query_args, Query, Limit, Stale, IncludeDocs, Bookmark,
+ Sort, Grouping, Stable, Counts, Ranges, Drilldown,
+ IncludeFields, HighlightFields, HighlightPreTag, HighlightPostTag,
+ HighlightNumber, HighlightSize, RawBookmark}) ->
+ #index_query_args{
+ q = Query,
+ limit = Limit,
+ stale = Stale,
+ include_docs = IncludeDocs,
+ bookmark = Bookmark,
+ sort = Sort,
+ grouping = Grouping,
+ stable = Stable,
+ counts = Counts,
+ ranges = Ranges,
+ drilldown = Drilldown,
+ include_fields = IncludeFields,
+ highlight_fields = HighlightFields,
+ highlight_pre_tag = HighlightPreTag,
+ highlight_post_tag = HighlightPostTag,
+ highlight_number = HighlightNumber,
+ highlight_size = HighlightSize,
+ raw_bookmark = RawBookmark
}.
-export(#index_query_args{counts = nil, ranges = nil, drilldown = [],
- include_fields = nil, highlight_fields = nil} = Args) ->
+export(#index_query_args{partition = nil, counts = nil, ranges = nil,
+ drilldown = [], include_fields = nil, highlight_fields = nil} = Args) ->
% Ensure existing searches work during the upgrade by creating an
% #index_query_args record in the old format
{index_query_args,
@@ -152,7 +192,8 @@ export(#index_query_args{counts = nil, ranges = nil, drilldown = [],
Args#index_query_args.grouping,
Args#index_query_args.stable
};
-export(#index_query_args{include_fields = nil, highlight_fields = nil} = Args) ->
+export(#index_query_args{partition = nil, include_fields = nil,
+ highlight_fields = nil} = Args) ->
{index_query_args,
Args#index_query_args.q,
Args#index_query_args.limit,
@@ -166,6 +207,27 @@ export(#index_query_args{include_fields = nil, highlight_fields = nil} = Args) -
Args#index_query_args.ranges,
Args#index_query_args.drilldown
};
+export(#index_query_args{partition = nil} = Args) ->
+ {index_query_args,
+ Args#index_query_args.q,
+ Args#index_query_args.limit,
+ Args#index_query_args.stale,
+ Args#index_query_args.include_docs,
+ Args#index_query_args.bookmark,
+ Args#index_query_args.sort,
+ Args#index_query_args.grouping,
+ Args#index_query_args.stable,
+ Args#index_query_args.counts,
+ Args#index_query_args.ranges,
+ Args#index_query_args.drilldown,
+ Args#index_query_args.include_fields,
+ Args#index_query_args.highlight_fields,
+ Args#index_query_args.highlight_pre_tag,
+ Args#index_query_args.highlight_post_tag,
+ Args#index_query_args.highlight_number,
+ Args#index_query_args.highlight_size,
+ Args#index_query_args.raw_bookmark
+ };
export(QueryArgs) ->
QueryArgs.
diff --git a/test/elixir/mix.exs b/test/elixir/mix.exs
new file mode 100644
index 000000000..9b0f642dd
--- /dev/null
+++ b/test/elixir/mix.exs
@@ -0,0 +1,30 @@
+defmodule Foo.Mixfile do
+ use Mix.Project
+
+ def project do
+ [
+ app: :foo,
+ version: "0.1.0",
+ elixir: "~> 1.5",
+ start_permanent: Mix.env == :prod,
+ deps: deps()
+ ]
+ end
+
+ # Run "mix help compile.app" to learn about applications.
+ def application do
+ [
+ extra_applications: [:logger]
+ ]
+ end
+
+ # Run "mix help deps" to learn about dependencies.
+ defp deps do
+ [
+ # {:dep_from_hexpm, "~> 0.3.0"},
+ {:httpotion, "~> 3.0"},
+ {:jiffy, "~> 0.14.11"}
+ # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
+ ]
+ end
+end
diff --git a/test/elixir/mix.lock b/test/elixir/mix.lock
new file mode 100644
index 000000000..ed51e5312
--- /dev/null
+++ b/test/elixir/mix.lock
@@ -0,0 +1,5 @@
+%{
+ "httpotion": {:hex, :httpotion, "3.1.0", "14d20d9b0ce4e86e253eb91e4af79e469ad949f57a5d23c0a51b2f86559f6589", [:mix], [{:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: false]}], "hexpm"},
+ "ibrowse": {:hex, :ibrowse, "4.4.1", "2b7d0637b0f8b9b4182de4bd0f2e826a4da2c9b04898b6e15659ba921a8d6ec2", [:rebar3], [], "hexpm"},
+ "jiffy": {:hex, :jiffy, "0.14.13", "225a9a35e26417832c611526567194b4d3adc4f0dfa5f2f7008f4684076f2a01", [:rebar3], [], "hexpm"},
+}
diff --git a/test/elixir/run b/test/elixir/run
new file mode 100755
index 000000000..66a5947b7
--- /dev/null
+++ b/test/elixir/run
@@ -0,0 +1,4 @@
+#!/bin/bash -e
+cd "$(dirname "$0")"
+mix deps.get
+mix test --trace
diff --git a/test/elixir/test/partition_search_test.exs b/test/elixir/test/partition_search_test.exs
new file mode 100644
index 000000000..98b23b508
--- /dev/null
+++ b/test/elixir/test/partition_search_test.exs
@@ -0,0 +1,187 @@
+defmodule PartitionSearchTest do
+ use CouchTestCase
+
+ @moduletag :search
+
+ @moduledoc """
+ Test Partition functionality with search
+ """
+
+ def create_search_docs(db_name, pk1 \\ "foo", pk2 \\ "bar") do
+ docs = for i <- 1..10 do
+ id = if rem(i, 2) == 0 do
+ "#{pk1}:#{i}"
+ else
+ "#{pk2}:#{i}"
+ end
+ %{
+ :_id => id,
+ :value => i,
+ :some => "field"
+ }
+ end
+
+ resp = Couch.post("/#{db_name}/_bulk_docs", body: %{:docs => docs} )
+ assert resp.status_code == 201
+ end
+
+ def create_ddoc(db_name, opts \\ %{}) do
+ indexFn = "function(doc) {\n if (doc.some) {\n index('some', doc.some);\n }\n}"
+ default_ddoc = %{
+ indexes: %{
+ books: %{
+ analyzer: %{name: "standard"},
+ index: indexFn
+ }
+ }
+ }
+
+ ddoc = Enum.into(opts, default_ddoc)
+
+ resp = Couch.put("/#{db_name}/_design/library", body: ddoc)
+ assert resp.status_code == 201
+ assert Map.has_key?(resp.body, "ok") == true
+ end
+
+ def get_ids (resp) do
+ %{:body => %{"rows" => rows}} = resp
+ Enum.map(rows, fn row -> row["id"] end)
+ end
+
+ @tag :with_partitioned_db
+ test "Simple query returns partitioned search results", context do
+ db_name = context[:db_name]
+ create_search_docs(db_name)
+ create_ddoc(db_name)
+
+ url = "/#{db_name}/_partition/foo/_design/library/_search/books"
+ resp = Couch.get(url, query: %{q: "some:field"})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert ids == ["foo:10", "foo:2", "foo:4", "foo:6", "foo:8"]
+
+ url = "/#{db_name}/_partition/bar/_design/library/_search/books"
+ resp = Couch.get(url, query: %{q: "some:field"})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert ids == ["bar:1", "bar:3", "bar:5", "bar:7", "bar:9"]
+ end
+
+ @tag :with_partitioned_db
+ test "Only returns docs in partition not those in shard", context do
+ db_name = context[:db_name]
+ create_search_docs(db_name, "foo", "bar42")
+ create_ddoc(db_name)
+
+ url = "/#{db_name}/_partition/foo/_design/library/_search/books"
+ resp = Couch.get(url, query: %{q: "some:field"})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert ids == ["foo:10", "foo:2", "foo:4", "foo:6", "foo:8"]
+ end
+
+ @tag :with_partitioned_db
+ test "Works with bookmarks and limit", context do
+ db_name = context[:db_name]
+ create_search_docs(db_name)
+ create_ddoc(db_name)
+
+ url = "/#{db_name}/_partition/foo/_design/library/_search/books"
+ resp = Couch.get(url, query: %{q: "some:field", limit: 3})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert ids == ["foo:10", "foo:2", "foo:4"]
+
+ %{:body => %{"bookmark" => bookmark}} = resp
+
+ resp = Couch.get(url, query: %{q: "some:field", limit: 3, bookmark: bookmark})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert ids == ["foo:6", "foo:8"]
+
+ resp = Couch.get(url, query: %{q: "some:field", limit: 2000, bookmark: bookmark})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert ids == ["foo:6", "foo:8"]
+
+ resp = Couch.get(url, query: %{q: "some:field", limit: 2001, bookmark: bookmark})
+ assert resp.status_code == 400
+ end
+
+ @tag :with_partitioned_db
+ test "Cannot do global query with partition view", context do
+ db_name = context[:db_name]
+ create_search_docs(db_name)
+ create_ddoc(db_name)
+
+ url = "/#{db_name}/_design/library/_search/books"
+ resp = Couch.get(url, query: %{q: "some:field"})
+ assert resp.status_code == 400
+ %{:body => %{"reason" => reason}} = resp
+ assert Regex.match?(~r/mandatory for queries to this index./, reason)
+ end
+
+ @tag :with_partitioned_db
+ test "Cannot do partition query with global search ddoc", context do
+ db_name = context[:db_name]
+ create_search_docs(db_name)
+ create_ddoc(db_name, options: %{partitioned: false})
+
+ url = "/#{db_name}/_partition/foo/_design/library/_search/books"
+ resp = Couch.get(url, query: %{q: "some:field"})
+ assert resp.status_code == 400
+ %{:body => %{"reason" => reason}} = resp
+ assert reason == "`partition` not supported on this index"
+ end
+
+ @tag :with_db
+ test "normal search on non-partitioned dbs still work", context do
+ db_name = context[:db_name]
+ create_search_docs(db_name)
+ create_ddoc(db_name)
+
+ url = "/#{db_name}/_design/library/_search/books"
+ resp = Couch.get(url, query: %{q: "some:field"})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert ids == ["bar:1", "bar:5", "bar:9", "foo:2", "bar:3", "foo:4", "foo:6", "bar:7", "foo:8", "foo:10"]
+ end
+
+ @tag :with_db
+ test "normal search on non-partitioned dbs without limit", context do
+ db_name = context[:db_name]
+ create_search_docs(db_name)
+ create_ddoc(db_name)
+
+ url = "/#{db_name}/_design/library/_search/books"
+ resp = Couch.get(url, query: %{q: "some:field"})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert ids == ["bar:1", "bar:5", "bar:9", "foo:2", "bar:3", "foo:4", "foo:6", "bar:7", "foo:8", "foo:10"]
+ end
+
+ @tag :with_db
+ test "normal search on non-partitioned dbs with limit", context do
+ db_name = context[:db_name]
+ create_search_docs(db_name)
+ create_ddoc(db_name)
+
+ url = "/#{db_name}/_design/library/_search/books"
+ resp = Couch.get(url, query: %{q: "some:field", limit: 3})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert ids == ["bar:1", "bar:5", "bar:9"]
+ end
+
+ @tag :with_db
+ test "normal search on non-partitioned dbs with over limit", context do
+ db_name = context[:db_name]
+ create_search_docs(db_name)
+ create_ddoc(db_name)
+
+ url = "/#{db_name}/_design/library/_search/books"
+ resp = Couch.get(url, query: %{q: "some:field", limit: 201})
+ assert resp.status_code == 400
+ end
+
+end
diff --git a/test/elixir/test/test_helper.exs b/test/elixir/test/test_helper.exs
new file mode 100644
index 000000000..6eb20e242
--- /dev/null
+++ b/test/elixir/test/test_helper.exs
@@ -0,0 +1,4 @@
+Code.require_file "../../../../couchdb/test/elixir/lib/couch.ex", __DIR__
+Code.require_file "../../../../couchdb/test/elixir/test/test_helper.exs", __DIR__
+Code.require_file "../../../../couchdb/test/elixir/test/support/couch_test_case.ex", __DIR__
+Code.require_file "../../../../couchdb/test/elixir/lib/couch/db_test.ex", __DIR__