summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul J. Davis <paul.joseph.davis@gmail.com>2018-11-28 10:58:42 -0600
committerPaul J. Davis <paul.joseph.davis@gmail.com>2019-01-18 12:35:38 -0600
commit91af772d609381defdd0c9e4fe22ae9f40478da0 (patch)
treee2b0e67b8980c233c1981c2e0bb7d9a3528274cf
parent0541a3124a6196a0757d7693479ebc40c61e0f27 (diff)
downloadcouchdb-feature/database-partitions.tar.gz
Add Elixir tests for database partitionsfeature/database-partitions
Co-authored-by: Garren Smith <garren.smith@gmail.com> Co-authored-by: Robert Newson <rnewson@apache.org>
-rw-r--r--test/elixir/lib/couch/db_test.ex12
-rw-r--r--test/elixir/test/partition_all_docs_test.exs118
-rw-r--r--test/elixir/test/partition_crud_test.exs356
-rw-r--r--test/elixir/test/partition_ddoc_test.exs171
-rw-r--r--test/elixir/test/partition_design_docs_test.exs16
-rw-r--r--test/elixir/test/partition_helpers.exs76
-rw-r--r--test/elixir/test/partition_mango_test.exs591
-rw-r--r--test/elixir/test/partition_size_test.exs357
-rw-r--r--test/elixir/test/partition_view_test.exs299
-rw-r--r--test/elixir/test/partition_view_update_test.exs155
-rw-r--r--test/elixir/test/test_helper.exs1
11 files changed, 2149 insertions, 3 deletions
diff --git a/test/elixir/lib/couch/db_test.ex b/test/elixir/lib/couch/db_test.ex
index 899237635..ba65a6d4e 100644
--- a/test/elixir/lib/couch/db_test.ex
+++ b/test/elixir/lib/couch/db_test.ex
@@ -18,6 +18,12 @@ defmodule Couch.DBTest do
|> Map.put(:db_name, random_db_name(db_name))
|> Map.put(:with_db, true)
+ %{:with_partitioned_db => true} ->
+ context
+ |> Map.put(:db_name, random_db_name())
+ |> Map.put(:query, %{partitioned: true})
+ |> Map.put(:with_db, true)
+
%{:with_db => true} ->
Map.put(context, :db_name, random_db_name())
@@ -29,7 +35,7 @@ defmodule Couch.DBTest do
end
if Map.has_key?(context, :with_db) do
- {:ok, _} = create_db(context[:db_name])
+ {:ok, _} = create_db(context[:db_name], query: context[:query])
on_exit(fn -> delete_db(context[:db_name]) end)
end
@@ -154,8 +160,8 @@ defmodule Couch.DBTest do
Map.put(user_doc, "_rev", resp.body["rev"])
end
- def create_db(db_name) do
- resp = Couch.put("/#{db_name}")
+ def create_db(db_name, opts \\ []) do
+ resp = Couch.put("/#{db_name}", opts)
assert resp.status_code in [201, 202]
assert resp.body == %{"ok" => true}
{:ok, resp}
diff --git a/test/elixir/test/partition_all_docs_test.exs b/test/elixir/test/partition_all_docs_test.exs
new file mode 100644
index 000000000..0941daf59
--- /dev/null
+++ b/test/elixir/test/partition_all_docs_test.exs
@@ -0,0 +1,118 @@
+defmodule PartitionAllDocsTest do
+ use CouchTestCase
+ import PartitionHelpers
+
+ @moduledoc """
+ Test Partition functionality for for all_docs
+ """
+
+ setup_all do
+ db_name = random_db_name()
+ {:ok, _} = create_db(db_name, query: %{partitioned: true, q: 1})
+ on_exit(fn -> delete_db(db_name) end)
+
+ create_partition_docs(db_name)
+
+ {:ok, [db_name: db_name]}
+ end
+
+ test "all_docs with partitioned:true returns partitioned fields", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_all_docs"
+ resp = Couch.get(url)
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert Enum.dedup(partitions) == ["foo"]
+
+ url = "/#{db_name}/_partition/bar/_all_docs"
+ resp = Couch.get(url)
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert Enum.dedup(partitions) == ["bar"]
+ end
+
+ test "partition all_docs errors with incorrect partition supplied", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/_bar/_all_docs"
+ resp = Couch.get(url)
+ assert resp.status_code == 400
+
+ url = "/#{db_name}/_partition//_all_docs"
+ resp = Couch.get(url)
+ assert resp.status_code == 400
+ end
+
+ test "partitioned _all_docs works with startkey, endkey range", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_all_docs"
+ resp = Couch.get(url, query: %{start_key: "\"foo:12\"", end_key: "\"foo:2\""})
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 5
+ assert Enum.dedup(partitions) == ["foo"]
+ end
+
+ test "partitioned _all_docs works with keys", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_all_docs"
+ resp = Couch.post(url, body: %{keys: ["foo:2", "foo:4", "foo:6"]})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert length(ids) == 3
+ assert ids == ["foo:2", "foo:4", "foo:6"]
+ end
+
+ test "partition _all_docs works with limit", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_all_docs"
+ resp = Couch.get(url, query: %{limit: 5})
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 5
+ assert Enum.dedup(partitions) == ["foo"]
+ end
+
+ test "partition _all_docs with descending", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_all_docs"
+ resp = Couch.get(url, query: %{descending: true, limit: 5})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert length(ids) == 5
+ assert ids == ["foo:98", "foo:96", "foo:94", "foo:92", "foo:90"]
+
+ resp = Couch.get(url, query: %{descending: false, limit: 5})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert length(ids) == 5
+ assert ids == ["foo:10", "foo:100", "foo:12", "foo:14", "foo:16"]
+ end
+
+ test "partition _all_docs with skip", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_all_docs"
+ resp = Couch.get(url, query: %{skip: 5, limit: 5})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert length(ids) == 5
+ assert ids == ["foo:18", "foo:2", "foo:20", "foo:22", "foo:24"]
+ end
+
+ test "partition _all_docs with key", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_all_docs"
+ resp = Couch.get(url, query: %{key: "\"foo:22\""})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert length(ids) == 1
+ assert ids == ["foo:22"]
+ end
+end
diff --git a/test/elixir/test/partition_crud_test.exs b/test/elixir/test/partition_crud_test.exs
new file mode 100644
index 000000000..415dd49bf
--- /dev/null
+++ b/test/elixir/test/partition_crud_test.exs
@@ -0,0 +1,356 @@
+defmodule PartitionCrudTest do
+ use CouchTestCase
+
+ @tag :with_partitioned_db
+ test "Sets partition in db info", context do
+ db_name = context[:db_name]
+ resp = Couch.get("/#{db_name}")
+ %{body: body} = resp
+ assert body["props"] == %{"partitioned" => true}
+ end
+
+ @tag :with_partitioned_db
+ test "PUT and GET document", context do
+ db_name = context[:db_name]
+ id = "my-partition:doc"
+ url = "/#{db_name}/#{id}"
+
+ resp = Couch.put(url, body: %{partitioned_doc: true})
+ %{body: doc} = resp
+ assert resp.status_code == 201
+ assert doc["id"] == id
+
+ resp = Couch.get(url)
+ assert resp.status_code == 200
+
+ %{body: doc} = resp
+ assert doc["_id"] == id
+ end
+
+ @tag :with_partitioned_db
+ test "PUT fails if a partition key is not supplied", context do
+ db_name = context[:db_name]
+ id = "not-partitioned"
+ url = "/#{db_name}/#{id}"
+
+ resp = Couch.put(url, body: %{partitioned_doc: false})
+ assert resp.status_code == 400
+
+ error = %{
+ "error" => "illegal_docid",
+ "reason" => "Doc id must be of form partition:id"
+ }
+
+ assert Map.get(resp, :body) == error
+ end
+
+ @tag :with_partitioned_db
+ test "PUT fails for partitions with _", context do
+ db_name = context[:db_name]
+ id = "_bad:partitioned"
+ url = "/#{db_name}/#{id}"
+
+ resp = Couch.put(url, body: %{partitioned_doc: false})
+
+ error = %{
+ "error" => "illegal_docid",
+ "reason" => "Only reserved document ids may start with underscore."
+ }
+
+ assert resp.status_code == 400
+ assert Map.get(resp, :body) == error
+ end
+
+ @tag :with_partitioned_db
+ test "PUT fails for bad partitions", context do
+ db_name = context[:db_name]
+ id = "bad:"
+ url = "/#{db_name}/#{id}"
+
+ resp = Couch.put(url, body: %{partitioned_doc: false})
+
+ error = %{
+ "error" => "illegal_docid",
+ "reason" => "Document id must not be empty"
+ }
+
+ assert resp.status_code == 400
+ assert Map.get(resp, :body) == error
+ end
+
+ @tag :with_partitioned_db
+ test "POST and GET document", context do
+ db_name = context[:db_name]
+ id = "my-partition-post:doc"
+ url = "/#{db_name}"
+
+ resp = Couch.post(url, body: %{_id: id, partitioned_doc: true})
+ assert resp.status_code == 201
+
+ resp = Couch.get("#{url}/#{id}")
+ assert resp.status_code == 200
+
+ %{body: doc} = resp
+ assert doc["_id"] == id
+ end
+
+ @tag :with_partitioned_db
+ test "POST and _bulk_get document", context do
+ db_name = context[:db_name]
+ id = "my-partition-post:doc"
+ url = "/#{db_name}"
+
+ resp = Couch.post(url, body: %{_id: id, partitioned_doc: true})
+ assert resp.status_code == 201
+
+ resp = Couch.post("#{url}/_bulk_get", body: %{docs: [%{id: id}]})
+ assert resp.status_code == 200
+
+ %{body: body} = resp
+
+ assert %{
+ "results" => [
+ %{
+ "docs" => [
+ %{
+ "ok" => %{
+ "_id" => "my-partition-post:doc",
+ "_rev" => "1-43d86359741cb629c0953a2beb6e9d7a",
+ "partitioned_doc" => true
+ }
+ }
+ ],
+ "id" => "my-partition-post:doc"
+ }
+ ]
+ } == body
+ end
+
+ @tag :with_partitioned_db
+ test "_bulk_get bad partitioned document", context do
+ db_name = context[:db_name]
+ id = "my-partition-post"
+ url = "/#{db_name}"
+
+ resp = Couch.post("#{url}/_bulk_get", body: %{docs: [%{id: id}]})
+ assert resp.status_code == 200
+ %{:body => body} = resp
+
+ assert %{
+ "results" => [
+ %{
+ "docs" => [
+ %{
+ "error" => %{
+ "error" => "illegal_docid",
+ "id" => "my-partition-post",
+ "reason" => "Doc id must be of form partition:id",
+ "rev" => :null
+ }
+ }
+ ],
+ "id" => "my-partition-post"
+ }
+ ]
+ } == body
+ end
+
+ @tag :with_partitioned_db
+ test "POST fails if a partition key is not supplied", context do
+ db_name = context[:db_name]
+ id = "not-partitioned-post"
+ url = "/#{db_name}"
+
+ resp = Couch.post(url, body: %{_id: id, partitited_doc: false})
+ assert resp.status_code == 400
+ end
+
+ @tag :with_partitioned_db
+ test "_bulk_docs saves docs with partition key", context do
+ db_name = context[:db_name]
+
+ docs = [
+ %{_id: "foo:1"},
+ %{_id: "bar:1"}
+ ]
+
+ url = "/#{db_name}"
+ resp = Couch.post("#{url}/_bulk_docs", body: %{:docs => docs})
+ assert resp.status_code == 201
+
+ resp = Couch.get("#{url}/foo:1")
+ assert resp.status_code == 200
+
+ resp = Couch.get("#{url}/bar:1")
+ assert resp.status_code == 200
+ end
+
+ @tag :with_partitioned_db
+ test "_bulk_docs errors with missing partition key", context do
+ db_name = context[:db_name]
+
+ docs = [
+ %{_id: "foo1"}
+ ]
+
+ error = %{
+ "error" => "illegal_docid",
+ "reason" => "Doc id must be of form partition:id"
+ }
+
+ url = "/#{db_name}"
+ resp = Couch.post("#{url}/_bulk_docs", body: %{:docs => docs})
+ assert resp.status_code == 400
+ assert Map.get(resp, :body) == error
+ end
+
+ @tag :with_partitioned_db
+ test "_bulk_docs errors with bad partition key", context do
+ db_name = context[:db_name]
+
+ docs = [
+ %{_id: "_foo:1"}
+ ]
+
+ error = %{
+ "error" => "illegal_docid",
+ "reason" => "Only reserved document ids may start with underscore."
+ }
+
+ url = "/#{db_name}"
+ resp = Couch.post("#{url}/_bulk_docs", body: %{:docs => docs})
+ assert resp.status_code == 400
+ assert Map.get(resp, :body) == error
+ end
+
+ @tag :with_partitioned_db
+ test "_bulk_docs errors with bad doc key", context do
+ db_name = context[:db_name]
+
+ docs = [
+ %{_id: "foo:"}
+ ]
+
+ error = %{
+ "error" => "illegal_docid",
+ "reason" => "Document id must not be empty"
+ }
+
+ url = "/#{db_name}"
+ resp = Couch.post("#{url}/_bulk_docs", body: %{:docs => docs})
+ assert resp.status_code == 400
+ assert Map.get(resp, :body) == error
+ end
+
+ @tag :with_partitioned_db
+ test "saves attachment with partitioned doc", context do
+ db_name = context[:db_name]
+ id = "foo:doc-with-attachment"
+
+ doc = %{
+ _id: id,
+ _attachments: %{
+ "foo.txt": %{
+ content_type: "text/plain",
+ data: Base.encode64("This is a text document to save")
+ }
+ }
+ }
+
+ resp = Couch.put("/#{db_name}/#{id}", body: doc)
+
+ assert resp.status_code == 201
+
+ resp = Couch.get("/#{db_name}/#{id}")
+ assert resp.status_code == 200
+ body = Map.get(resp, :body)
+ rev = Map.get(body, "_rev")
+
+ assert body["_attachments"] == %{
+ "foo.txt" => %{
+ "content_type" => "text/plain",
+ "digest" => "md5-OW2BoZAtMqs1E+fAnLpNBw==",
+ "length" => 31,
+ "revpos" => 1,
+ "stub" => true
+ }
+ }
+
+ resp = Couch.get("/#{db_name}/#{id}/foo.txt")
+ assert Map.get(resp, :body) == "This is a text document to save"
+
+ resp =
+ Couch.put("/#{db_name}/#{id}/bar.txt?rev=#{rev}",
+ headers: ["Content-Type": "text/plain"],
+ body: "This is another document"
+ )
+
+ assert resp.status_code == 201
+ %{:body => body} = resp
+ assert body["ok"] == true
+ assert body["id"] == id
+ end
+
+ @tag :with_partitioned_db
+ test "can purge partitioned db docs", context do
+ db_name = context[:db_name]
+
+ doc = %{
+ _id: "foo:bar",
+ value: "some value"
+ }
+
+ resp = Couch.post("/#{db_name}", query: [w: 3], body: doc)
+ assert resp.status_code == 201
+ %{body: body} = resp
+ rev = body["rev"]
+
+ resp = Couch.get("/#{db_name}/foo:bar")
+ assert resp.status_code == 200
+
+ body = %{"foo:bar" => [rev]}
+ resp = Couch.post("/#{db_name}/_purge", query: [w: 3], body: body)
+ assert resp.status_code == 201
+
+ resp = Couch.get("/#{db_name}/foo:bar")
+ assert resp.status_code == 404
+ assert resp.body == %{"error" => "not_found", "reason" => "missing"}
+ end
+
+ @tag :with_partitioned_db
+ test "purge rejects unpartitioned docid", context do
+ db_name = context[:db_name]
+ body = %{"no_partition" => ["1-967a00dff5e02add41819138abb3284d"]}
+ resp = Couch.post("/#{db_name}/_purge", query: [w: 3], body: body)
+ assert resp.status_code == 400
+ %{body: body} = resp
+ assert body["error"] == "illegal_docid"
+ end
+
+ test "create database with bad `partitioned` value", _context do
+ resp = Couch.put("/bad-db?partitioned=tru")
+ assert resp.status_code == 400
+
+ assert Map.get(resp, :body) == %{
+ "error" => "bad_request",
+ "reason" => "Invalid `partitioned` parameter"
+ }
+ end
+
+ test "can create unpartitioned system db", _context do
+ Couch.delete("/_replicator")
+ resp = Couch.put("/_replicator")
+ assert resp.status_code == 201
+ assert resp.body == %{"ok" => true}
+ end
+
+ test "cannot create partitioned system db", _context do
+ Couch.delete("/_replicator")
+
+ resp = Couch.put("/_replicator?partitioned=true")
+ assert resp.status_code == 400
+
+ %{:body => %{"reason" => reason}} = resp
+ assert Regex.match?(~r/Cannot partition a system database/, reason)
+ end
+end
diff --git a/test/elixir/test/partition_ddoc_test.exs b/test/elixir/test/partition_ddoc_test.exs
new file mode 100644
index 000000000..4b1f00d8b
--- /dev/null
+++ b/test/elixir/test/partition_ddoc_test.exs
@@ -0,0 +1,171 @@
+defmodule PartitionDDocTest do
+ use CouchTestCase
+
+ @moduledoc """
+ Test partition design doc interactions
+ """
+
+ setup do
+ db_name = random_db_name()
+ {:ok, _} = create_db(db_name, query: %{partitioned: true, q: 1})
+ on_exit(fn -> delete_db(db_name) end)
+
+ {:ok, [db_name: db_name]}
+ end
+
+ test "PUT /dbname/_design/foo", context do
+ db_name = context[:db_name]
+ resp = Couch.put("/#{db_name}/_design/foo", body: %{stuff: "here"})
+ assert resp.status_code == 201
+ end
+
+ test "PUT /dbname/_design/foo to update", context do
+ db_name = context[:db_name]
+ ddoc_id = "_design/foo"
+
+ ddoc = %{
+ _id: ddoc_id,
+ stuff: "here"
+ }
+
+ resp = Couch.put("/#{db_name}/#{ddoc_id}", body: ddoc)
+ assert resp.status_code == 201
+ %{body: body} = resp
+
+ ddoc = Map.put(ddoc, :_rev, body["rev"])
+ ddoc = Map.put(ddoc, :other, "attribute")
+ resp = Couch.put("/#{db_name}/#{ddoc_id}", body: ddoc)
+ assert resp.status_code == 201
+ end
+
+ test "PUT /dbname/_design/foo/readme.txt", context do
+ db_name = context[:db_name]
+ ddoc_id = "_design/foo"
+
+ ddoc = %{
+ _id: ddoc_id,
+ stuff: "here"
+ }
+
+ resp = Couch.put("/#{db_name}/#{ddoc_id}", body: ddoc)
+ assert resp.status_code == 201
+ %{body: body} = resp
+
+ att = "This is a readme.txt"
+
+ opts = [
+ headers: [{:"Content-Type", "text/plain"}],
+ query: [rev: body["rev"]],
+ body: att
+ ]
+
+ resp = Couch.put("/#{db_name}/#{ddoc_id}/readme.txt", opts)
+ assert resp.status_code == 201
+ end
+
+ test "DELETE /dbname/_design/foo", context do
+ db_name = context[:db_name]
+ ddoc_id = "_design/foo"
+
+ ddoc = %{
+ _id: ddoc_id,
+ stuff: "here"
+ }
+
+ resp = Couch.put("/#{db_name}/#{ddoc_id}", body: ddoc)
+ assert resp.status_code == 201
+ %{body: body} = resp
+
+ resp = Couch.delete("/#{db_name}/#{ddoc_id}", query: [rev: body["rev"]])
+ assert resp.status_code == 200
+ end
+
+ test "POST /dbname with design doc", context do
+ db_name = context[:db_name]
+ body = %{_id: "_design/foo", stuff: "here"}
+ resp = Couch.post("/#{db_name}", body: body)
+ assert resp.status_code == 201
+ end
+
+ test "POST /dbname/_bulk_docs with design doc", context do
+ db_name = context[:db_name]
+ body = %{:docs => [%{_id: "_design/foo", stuff: "here"}]}
+ resp = Couch.post("/#{db_name}/_bulk_docs", body: body)
+ assert resp.status_code == 201
+ end
+
+ test "GET /dbname/_design/foo", context do
+ db_name = context[:db_name]
+ resp = Couch.put("/#{db_name}/_design/foo", body: %{stuff: "here"})
+ assert resp.status_code == 201
+
+ resp = Couch.get("/#{db_name}/_design/foo")
+ assert resp.status_code == 200
+ end
+
+ test "GET /dbname/_design/foo?rev=$rev", context do
+ db_name = context[:db_name]
+ resp = Couch.put("/#{db_name}/_design/foo", body: %{stuff: "here"})
+ assert resp.status_code == 201
+ %{body: body} = resp
+
+ resp = Couch.get("/#{db_name}/_design/foo", query: [rev: body["rev"]])
+ assert resp.status_code == 200
+ end
+
+ test "GET /dbname/_bulk_get", context do
+ db_name = context[:db_name]
+ resp = Couch.put("/#{db_name}/_design/foo", body: %{stuff: "here"})
+ assert resp.status_code == 201
+
+ body = %{docs: [%{id: "_design/foo"}]}
+ resp = Couch.post("/#{db_name}/_bulk_get", body: body)
+ assert resp.status_code == 200
+ %{body: body} = resp
+
+ assert length(body["results"]) == 1
+
+ %{"results" => [%{"id" => "_design/foo", "docs" => [%{"ok" => _}]}]} = body
+ end
+
+ test "GET /dbname/_bulk_get with rev", context do
+ db_name = context[:db_name]
+ resp = Couch.put("/#{db_name}/_design/foo", body: %{stuff: "here"})
+ assert resp.status_code == 201
+ %{body: body} = resp
+
+ body = %{docs: [%{id: "_design/foo", rev: body["rev"]}]}
+ resp = Couch.post("/#{db_name}/_bulk_get", body: body)
+ assert resp.status_code == 200
+ %{body: body} = resp
+
+ assert length(body["results"]) == 1
+ %{"results" => [%{"id" => "_design/foo", "docs" => [%{"ok" => _}]}]} = body
+ end
+
+ test "GET /dbname/_all_docs?key=$ddoc_id", context do
+ db_name = context[:db_name]
+ resp = Couch.put("/#{db_name}/_design/foo", body: %{stuff: "here"})
+ assert resp.status_code == 201
+
+ resp = Couch.get("/#{db_name}/_all_docs", query: [key: "\"_design/foo\""])
+ assert resp.status_code == 200
+ %{body: body} = resp
+
+ assert length(body["rows"]) == 1
+ %{"rows" => [%{"id" => "_design/foo"}]} = body
+ end
+
+ test "GET /dbname/_design_docs", context do
+ db_name = context[:db_name]
+ resp = Couch.put("/#{db_name}/_design/foo", body: %{stuff: "here"})
+ assert resp.status_code == 201
+
+ resp = Couch.get("/#{db_name}/_design_docs")
+ assert resp.status_code == 200
+ %{body: body} = resp
+
+ assert length(body["rows"]) == 1
+ %{"rows" => [%{"id" => "_design/foo"}]} = body
+ end
+end
diff --git a/test/elixir/test/partition_design_docs_test.exs b/test/elixir/test/partition_design_docs_test.exs
new file mode 100644
index 000000000..42a2ced77
--- /dev/null
+++ b/test/elixir/test/partition_design_docs_test.exs
@@ -0,0 +1,16 @@
+defmodule PartitionDesignDocsTest do
+ use CouchTestCase
+
+ @moduledoc """
+ Test Partition functionality for partition design docs
+ """
+
+ @tag :with_partitioned_db
+ test "/_partition/:pk/_design/doc 404", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/fake-key/_design/mrtest/"
+ resp = Couch.get(url)
+ assert resp.status_code == 404
+ end
+end
diff --git a/test/elixir/test/partition_helpers.exs b/test/elixir/test/partition_helpers.exs
new file mode 100644
index 000000000..6eac2b1a4
--- /dev/null
+++ b/test/elixir/test/partition_helpers.exs
@@ -0,0 +1,76 @@
+defmodule PartitionHelpers do
+ use ExUnit.Case
+
+ def create_partition_docs(db_name, pk1 \\ "foo", pk2 \\ "bar") do
+ docs =
+ for i <- 1..100 do
+ id =
+ if rem(i, 2) == 0 do
+ "#{pk1}:#{i}"
+ else
+ "#{pk2}:#{i}"
+ end
+
+ group =
+ if rem(i, 3) == 0 do
+ "one"
+ else
+ "two"
+ end
+
+ %{
+ :_id => id,
+ :value => i,
+ :some => "field",
+ :group => group
+ }
+ end
+
+ resp = Couch.post("/#{db_name}/_bulk_docs", body: %{:w => 3, :docs => docs})
+ assert resp.status_code == 201
+ end
+
+ def create_partition_ddoc(db_name, opts \\ %{}) do
+ map_fn = """
+ function(doc) {
+ if (doc.some) {
+ emit(doc.value, doc.some);
+ }
+ }
+ """
+
+ default_ddoc = %{
+ views: %{
+ some: %{
+ map: map_fn
+ }
+ }
+ }
+
+ ddoc = Enum.into(opts, default_ddoc)
+
+ resp = Couch.put("/#{db_name}/_design/mrtest", 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
+
+ def get_partitions(resp) do
+ %{:body => %{"rows" => rows}} = resp
+
+ Enum.map(rows, fn row ->
+ [partition, _] = String.split(row["id"], ":")
+ partition
+ end)
+ end
+
+ def assert_correct_partition(partitions, correct_partition) do
+ assert Enum.all?(partitions, fn partition ->
+ partition == correct_partition
+ end)
+ end
+end
diff --git a/test/elixir/test/partition_mango_test.exs b/test/elixir/test/partition_mango_test.exs
new file mode 100644
index 000000000..1471ddb0a
--- /dev/null
+++ b/test/elixir/test/partition_mango_test.exs
@@ -0,0 +1,591 @@
+defmodule PartitionMangoTest do
+ use CouchTestCase
+ import PartitionHelpers, except: [get_partitions: 1]
+
+ @moduledoc """
+ Test Partition functionality for mango
+ """
+ def create_index(db_name, fields \\ ["some"], opts \\ %{}) do
+ default_index = %{
+ index: %{
+ fields: fields
+ }
+ }
+
+ index = Enum.into(opts, default_index)
+ resp = Couch.post("/#{db_name}/_index", body: index)
+
+ assert resp.status_code == 200
+ assert resp.body["result"] == "created"
+ end
+
+ def get_partitions(resp) do
+ %{:body => %{"docs" => docs}} = resp
+
+ Enum.map(docs, fn doc ->
+ [partition, _] = String.split(doc["_id"], ":")
+ partition
+ end)
+ end
+
+ @tag :with_partitioned_db
+ test "query using _id and partition works", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+ create_index(db_name)
+
+ url = "/#{db_name}/_partition/foo/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ _id: %{
+ "$gt": "foo:"
+ }
+ },
+ limit: 20
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 20
+ assert_correct_partition(partitions, "foo")
+
+ url = "/#{db_name}/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ _id: %{
+ "$lt": "foo:"
+ }
+ },
+ limit: 20
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 20
+ assert_correct_partition(partitions, "bar")
+ end
+
+ @tag :with_partitioned_db
+ test "query using _id works for global and local query", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+ create_index(db_name)
+
+ url = "/#{db_name}/_partition/foo/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ _id: %{
+ "$gt": 0
+ }
+ },
+ limit: 20
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 20
+ assert_correct_partition(partitions, "foo")
+
+ url = "/#{db_name}/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ _id: %{
+ "$gt": 0
+ }
+ },
+ limit: 20
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 20
+ assert_correct_partition(partitions, "bar")
+ end
+
+ @tag :with_partitioned_db
+ test "query with partitioned:true using index and $eq", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+ create_index(db_name)
+
+ url = "/#{db_name}/_partition/foo/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ some: "field"
+ },
+ limit: 20
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 20
+ assert_correct_partition(partitions, "foo")
+
+ url = "/#{db_name}/_partition/bar/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ some: "field"
+ },
+ limit: 20
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 20
+ assert_correct_partition(partitions, "bar")
+ end
+
+ @tag :with_partitioned_db
+ test "partitioned query using _all_docs with $eq", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+
+ url = "/#{db_name}/_partition/foo/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ some: "field"
+ },
+ limit: 20
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 20
+ assert_correct_partition(partitions, "foo")
+
+ url = "/#{db_name}/_partition/bar/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ some: "field"
+ },
+ limit: 20
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 20
+ assert_correct_partition(partitions, "bar")
+ end
+
+ @tag :with_db
+ test "non-partitioned query using _all_docs and $eq", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+
+ url = "/#{db_name}/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ some: "field"
+ },
+ skip: 40,
+ limit: 5
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 5
+ assert partitions == ["bar", "bar", "bar", "bar", "bar"]
+
+ url = "/#{db_name}/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ some: "field"
+ },
+ skip: 50,
+ limit: 5
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 5
+ assert partitions == ["foo", "foo", "foo", "foo", "foo"]
+ end
+
+ @tag :with_partitioned_db
+ test "partitioned query using index and range scan", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name, "foo", "bar42")
+ create_index(db_name, ["value"])
+
+ url = "/#{db_name}/_partition/foo/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ value: %{
+ "$gte": 6,
+ "$lt": 16
+ }
+ }
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 5
+ assert_correct_partition(partitions, "foo")
+
+ url = "/#{db_name}/_partition/bar42/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ value: %{
+ "$gte": 6,
+ "$lt": 16
+ }
+ }
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 5
+ assert_correct_partition(partitions, "bar42")
+ end
+
+ @tag :with_partitioned_db
+ test "partitioned query using _all_docs and range scan", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+
+ url = "/#{db_name}/_partition/foo/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ value: %{
+ "$gte": 6,
+ "$lt": 16
+ }
+ }
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 5
+ assert_correct_partition(partitions, "foo")
+
+ url = "/#{db_name}/_partition/bar/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ value: %{
+ "$gte": 6,
+ "$lt": 16
+ }
+ }
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 5
+ assert_correct_partition(partitions, "bar")
+ end
+
+ @tag :with_partitioned_db
+ test "partitioned query using _all_docs", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name, "foo", "bar42")
+
+ url = "/#{db_name}/_partition/foo/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ value: %{
+ "$gte": 6,
+ "$lt": 16
+ }
+ }
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 5
+ assert_correct_partition(partitions, "foo")
+
+ url = "/#{db_name}/_partition/bar42/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ value: %{
+ "$gte": 6,
+ "$lt": 16
+ }
+ }
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 5
+ assert_correct_partition(partitions, "bar42")
+ end
+
+ @tag :with_partitioned_db
+ test "explain works with partitions", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+ create_index(db_name, ["some"])
+
+ url = "/#{db_name}/_partition/foo/_explain"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ value: %{
+ "$gte": 6,
+ "$lt": 16
+ }
+ }
+ }
+ )
+
+ %{:body => body} = resp
+
+ assert body["index"]["name"] == "_all_docs"
+ assert body["mrargs"]["partition"] == "foo"
+
+ url = "/#{db_name}/_partition/bar/_explain"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ some: "field"
+ }
+ }
+ )
+
+ %{:body => body} = resp
+
+ assert body["index"]["def"] == %{"fields" => [%{"some" => "asc"}]}
+ assert body["mrargs"]["partition"] == "bar"
+ end
+
+ @tag :with_db
+ test "explain works with non partitioned db", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+ create_index(db_name, ["some"])
+
+ url = "/#{db_name}/_explain"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ value: %{
+ "$gte": 6,
+ "$lt": 16
+ }
+ }
+ }
+ )
+
+ %{:body => body} = resp
+
+ assert body["index"]["name"] == "_all_docs"
+ assert body["mrargs"]["partition"] == :null
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ some: "field"
+ }
+ }
+ )
+
+ %{:body => body} = resp
+
+ assert body["index"]["def"] == %{"fields" => [%{"some" => "asc"}]}
+ assert body["mrargs"]["partition"] == :null
+ end
+
+ @tag :with_partitioned_db
+ test "partitioned query using bookmarks", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+ create_index(db_name, ["value"])
+
+ url = "/#{db_name}/_partition/foo/_find"
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ value: %{
+ "$gte": 6,
+ "$lt": 16
+ }
+ },
+ limit: 3
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 3
+ assert_correct_partition(partitions, "foo")
+
+ %{:body => %{"bookmark" => bookmark}} = resp
+
+ resp =
+ Couch.post(url,
+ body: %{
+ selector: %{
+ value: %{
+ "$gte": 6,
+ "$lt": 16
+ }
+ },
+ limit: 3,
+ bookmark: bookmark
+ }
+ )
+
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 2
+ assert_correct_partition(partitions, "foo")
+ end
+
+ @tag :with_partitioned_db
+ test "global query uses global index", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+ create_index(db_name, ["some"], %{partitioned: false})
+
+ url = "/#{db_name}/_explain"
+
+ selector = %{
+ selector: %{
+ some: "field"
+ },
+ limit: 100
+ }
+
+ resp = Couch.post(url, body: selector)
+ assert resp.status_code == 200
+ %{:body => body} = resp
+ assert body["index"]["def"] == %{"fields" => [%{"some" => "asc"}]}
+
+ url = "/#{db_name}/_find"
+ resp = Couch.post(url, body: selector)
+ assert resp.status_code == 200
+
+ partitions = get_partitions(resp)
+ assert length(partitions) == 100
+ end
+
+ @tag :with_partitioned_db
+ test "global query does not use partition index", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+ create_index(db_name, ["some"])
+
+ url = "/#{db_name}/_explain"
+
+ selector = %{
+ selector: %{
+ some: "field"
+ },
+ limit: 100
+ }
+
+ resp = Couch.post(url, body: selector)
+ %{:body => body} = resp
+ assert body["index"]["name"] == "_all_docs"
+
+ url = "/#{db_name}/_find"
+ resp = Couch.post(url, body: selector)
+
+ assert resp.status_code == 200
+
+ partitions = get_partitions(resp)
+ assert length(partitions) == 100
+ end
+
+ @tag :with_partitioned_db
+ test "partitioned query does not use global index", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+ create_index(db_name, ["some"], %{partitioned: false})
+
+ url = "/#{db_name}/_partition/foo/_explain"
+
+ selector = %{
+ selector: %{
+ some: "field"
+ },
+ limit: 50
+ }
+
+ resp = Couch.post(url, body: selector)
+ assert resp.status_code == 200
+ %{:body => body} = resp
+ assert body["index"]["name"] == "_all_docs"
+
+ url = "/#{db_name}/_partition/foo/_find"
+ resp = Couch.post(url, body: selector)
+ assert resp.status_code == 200
+
+ partitions = get_partitions(resp)
+ assert length(partitions) == 50
+ assert_correct_partition(partitions, "foo")
+ end
+end
diff --git a/test/elixir/test/partition_size_test.exs b/test/elixir/test/partition_size_test.exs
new file mode 100644
index 000000000..c4d235b77
--- /dev/null
+++ b/test/elixir/test/partition_size_test.exs
@@ -0,0 +1,357 @@
+defmodule PartitionSizeTest do
+ use CouchTestCase
+
+ @moduledoc """
+ Test Partition size functionality
+ """
+
+ setup do
+ db_name = random_db_name()
+ {:ok, _} = create_db(db_name, query: %{partitioned: true, q: 1})
+ on_exit(fn -> delete_db(db_name) end)
+
+ {:ok, [db_name: db_name]}
+ end
+
+ def get_db_info(dbname) do
+ resp = Couch.get("/#{dbname}")
+ assert resp.status_code == 200
+ %{:body => body} = resp
+ body
+ end
+
+ def get_partition_info(dbname, partition) do
+ resp = Couch.get("/#{dbname}/_partition/#{partition}")
+ assert resp.status_code == 200
+ %{:body => body} = resp
+ body
+ end
+
+ def mk_partition(i) do
+ i |> rem(10) |> Integer.to_string() |> String.pad_leading(3, "0")
+ end
+
+ def mk_docid(i) do
+ id = i |> Integer.to_string() |> String.pad_leading(4, "0")
+ "#{mk_partition(i)}:#{id}"
+ end
+
+ def mk_docs(db_name) do
+ docs =
+ for i <- 1..1000 do
+ group = Integer.to_string(rem(i, 3))
+
+ %{
+ :_id => mk_docid(i),
+ :value => i,
+ :some => "field",
+ :group => group
+ }
+ end
+
+ body = %{:w => 3, :docs => docs}
+ resp = Couch.post("/#{db_name}/_bulk_docs", body: body)
+ assert resp.status_code == 201
+ end
+
+ def save_doc(db_name, doc) do
+ resp = Couch.post("/#{db_name}", query: [w: 3], body: doc)
+ assert resp.status_code == 201
+ %{:body => body} = resp
+ body["rev"]
+ end
+
+ test "get empty partition", context do
+ db_name = context[:db_name]
+ partition = "non_existent_partition"
+
+ info = get_partition_info(db_name, partition)
+
+ assert info["doc_count"] == 0
+ assert info["doc_del_count"] == 0
+ assert info["partition"] == partition
+ assert info["sizes"]["external"] == 0
+ assert info["sizes"]["active"] == 0
+ end
+
+ test "unknown partition return's zero", context do
+ db_name = context[:db_name]
+ mk_docs(db_name)
+
+ info = get_partition_info(db_name, "unknown")
+ assert info["doc_count"] == 0
+ assert info["doc_del_count"] == 0
+ assert info["sizes"]["external"] == 0
+ assert info["sizes"]["active"] == 0
+ end
+
+ test "simple partition size", context do
+ db_name = context[:db_name]
+ save_doc(db_name, %{_id: "foo:bar", val: 42})
+
+ info = get_partition_info(db_name, "foo")
+ assert info["doc_count"] == 1
+ assert info["doc_del_count"] == 0
+ assert info["sizes"]["external"] > 0
+ assert info["sizes"]["active"] > 0
+ end
+
+ test "adding docs increases partition sizes", context do
+ db_name = context[:db_name]
+ save_doc(db_name, %{_id: "foo:bar", val: 42})
+ pre_info = get_partition_info(db_name, "foo")
+
+ save_doc(db_name, %{_id: "foo:baz", val: 24})
+ post_info = get_partition_info(db_name, "foo")
+
+ assert post_info["doc_count"] == 2
+ assert post_info["doc_del_count"] == 0
+ assert post_info["sizes"]["external"] > pre_info["sizes"]["external"]
+ assert post_info["sizes"]["active"] > pre_info["sizes"]["active"]
+ end
+
+ test "updating docs affects partition sizes", context do
+ db_name = context[:db_name]
+ rev1 = save_doc(db_name, %{_id: "foo:bar", val: ""})
+ info1 = get_partition_info(db_name, "foo")
+
+ rev2 =
+ save_doc(db_name, %{
+ _id: "foo:bar",
+ _rev: rev1,
+ val: "this is a very long string that is so super long its beyond long"
+ })
+
+ info2 = get_partition_info(db_name, "foo")
+
+ save_doc(db_name, %{
+ _id: "foo:bar",
+ _rev: rev2,
+ val: "this string is shorter"
+ })
+
+ info3 = get_partition_info(db_name, "foo")
+
+ assert info3["doc_count"] == 1
+ assert info3["doc_del_count"] == 0
+
+ assert info3["sizes"]["external"] > info1["sizes"]["external"]
+ assert info2["sizes"]["external"] > info3["sizes"]["external"]
+ end
+
+ test "deleting a doc affects partition sizes", context do
+ db_name = context[:db_name]
+ rev1 = save_doc(db_name, %{_id: "foo:bar", val: "some stuff here"})
+ info1 = get_partition_info(db_name, "foo")
+
+ save_doc(db_name, %{_id: "foo:bar", _rev: rev1, _deleted: true})
+ info2 = get_partition_info(db_name, "foo")
+
+ assert info1["doc_count"] == 1
+ assert info1["doc_del_count"] == 0
+
+ assert info2["doc_count"] == 0
+ assert info2["doc_del_count"] == 1
+
+ assert info2["sizes"]["external"] < info1["sizes"]["external"]
+ end
+
+ test "design docs do not affect partition sizes", context do
+ db_name = context[:db_name]
+ mk_docs(db_name)
+
+ pre_infos =
+ 0..9
+ |> Enum.map(fn i ->
+ get_partition_info(db_name, mk_partition(i))
+ end)
+
+ 0..5
+ |> Enum.map(fn i ->
+ base = i |> Integer.to_string() |> String.pad_leading(5, "0")
+ docid = "_design/#{base}"
+ save_doc(db_name, %{_id: docid, value: "some stuff here"})
+ end)
+
+ post_infos =
+ 0..9
+ |> Enum.map(fn i ->
+ get_partition_info(db_name, mk_partition(i))
+ end)
+
+ assert post_infos == pre_infos
+ end
+
+ test "get all partition sizes", context do
+ db_name = context[:db_name]
+ mk_docs(db_name)
+
+ {esum, asum} =
+ 0..9
+ |> Enum.reduce({0, 0}, fn i, {esize, asize} ->
+ partition = mk_partition(i)
+ info = get_partition_info(db_name, partition)
+ assert info["doc_count"] == 100
+ assert info["doc_del_count"] == 0
+ assert info["sizes"]["external"] > 0
+ assert info["sizes"]["active"] > 0
+ {esize + info["sizes"]["external"], asize + info["sizes"]["active"]}
+ end)
+
+ db_info = get_db_info(db_name)
+ assert db_info["sizes"]["external"] >= esum
+ assert db_info["sizes"]["active"] >= asum
+ end
+
+ test "get partition size with attachment", context do
+ db_name = context[:db_name]
+
+ doc = %{
+ _id: "foo:doc-with-attachment",
+ _attachments: %{
+ "foo.txt": %{
+ content_type: "text/plain",
+ data: Base.encode64("This is a text document to save")
+ }
+ }
+ }
+
+ save_doc(db_name, doc)
+
+ db_info = get_db_info(db_name)
+ foo_info = get_partition_info(db_name, "foo")
+
+ assert foo_info["doc_count"] == 1
+ assert foo_info["doc_del_count"] == 0
+ assert foo_info["sizes"]["active"] > 0
+ assert foo_info["sizes"]["external"] > 0
+
+ assert foo_info["sizes"]["active"] <= db_info["sizes"]["active"]
+ assert foo_info["sizes"]["external"] <= db_info["sizes"]["external"]
+ end
+
+ test "attachments don't affect other partitions", context do
+ db_name = context[:db_name]
+ mk_docs(db_name)
+
+ pre_infos =
+ 0..9
+ |> Enum.map(fn i ->
+ get_partition_info(db_name, mk_partition(i))
+ end)
+
+ doc = %{
+ _id: "foo:doc-with-attachment",
+ _attachments: %{
+ "foo.txt": %{
+ content_type: "text/plain",
+ data: Base.encode64("This is a text document to save")
+ }
+ }
+ }
+
+ save_doc(db_name, doc)
+
+ att_info = get_partition_info(db_name, "foo")
+ assert att_info["doc_count"] == 1
+ assert att_info["sizes"]["external"] > 0
+
+ post_infos =
+ 0..9
+ |> Enum.map(fn i ->
+ get_partition_info(db_name, mk_partition(i))
+ end)
+
+ assert post_infos == pre_infos
+
+ esize =
+ ([att_info] ++ post_infos)
+ |> Enum.reduce(0, fn info, acc ->
+ info["sizes"]["external"] + acc
+ end)
+
+ db_info = get_db_info(db_name)
+ assert esize == db_info["sizes"]["external"]
+ end
+
+ test "partition activity not affect other partition sizes", context do
+ db_name = context[:db_name]
+ mk_docs(db_name)
+
+ partition1 = "000"
+ partition2 = "001"
+
+ info2 = get_partition_info(db_name, partition2)
+
+ doc_id = "#{partition1}:doc-with-attachment"
+
+ doc = %{
+ _id: doc_id,
+ _attachments: %{
+ "foo.txt": %{
+ content_type: "text/plain",
+ data: Base.encode64("This is a text document to save")
+ }
+ }
+ }
+
+ doc_rev = save_doc(db_name, doc)
+
+ info2_attach = get_partition_info(db_name, partition2)
+ assert info2_attach == info2
+
+ doc =
+ Enum.into(
+ %{
+ another: "add another field",
+ _rev: doc_rev
+ },
+ doc
+ )
+
+ doc_rev = save_doc(db_name, doc)
+
+ info2_update = get_partition_info(db_name, partition2)
+ assert info2_update == info2
+
+ resp = Couch.delete("/#{db_name}/#{doc_id}", query: %{rev: doc_rev})
+ assert resp.status_code == 200
+
+ info2_delete = get_partition_info(db_name, partition2)
+ assert info2_delete == info2
+ end
+
+ test "purging docs decreases partition size", context do
+ db_name = context[:db_name]
+ mk_docs(db_name)
+
+ partition = "000"
+
+ query = [
+ start_key: "\"#{partition}:0000\"",
+ end_key: "\"#{partition}:9999\"",
+ limit: 50
+ ]
+
+ resp = Couch.get("/#{db_name}/_all_docs", query: query)
+ assert resp.status_code == 200
+ %{body: body} = resp
+
+ pre_info = get_partition_info(db_name, partition)
+
+ pbody =
+ body["rows"]
+ |> Enum.reduce(%{}, fn row, acc ->
+ Map.put(acc, row["id"], [row["value"]["rev"]])
+ end)
+
+ resp = Couch.post("/#{db_name}/_purge", query: [w: 3], body: pbody)
+ assert resp.status_code == 201
+
+ post_info = get_partition_info(db_name, partition)
+ assert post_info["doc_count"] == pre_info["doc_count"] - 50
+ assert post_info["doc_del_count"] == 0
+ assert post_info["sizes"]["active"] < pre_info["sizes"]["active"]
+ assert post_info["sizes"]["external"] < pre_info["sizes"]["external"]
+ end
+end
diff --git a/test/elixir/test/partition_view_test.exs b/test/elixir/test/partition_view_test.exs
new file mode 100644
index 000000000..a25539172
--- /dev/null
+++ b/test/elixir/test/partition_view_test.exs
@@ -0,0 +1,299 @@
+defmodule ViewPartitionTest do
+ use CouchTestCase
+ import PartitionHelpers
+
+ @moduledoc """
+ Test Partition functionality for views
+ """
+
+ setup_all do
+ db_name = random_db_name()
+ {:ok, _} = create_db(db_name, query: %{partitioned: true, q: 1})
+ on_exit(fn -> delete_db(db_name) end)
+
+ create_partition_docs(db_name)
+
+ map_fun1 = """
+ function(doc) {
+ if (doc.some) {
+ emit(doc.value, doc.some);
+ }
+ }
+ """
+
+ map_fun2 = """
+ function(doc) {
+ if (doc.group) {
+ emit([doc.some, doc.group], 1);
+ }
+ }
+ """
+
+ query = %{:w => 3}
+
+ body = %{
+ :docs => [
+ %{
+ _id: "_design/map",
+ views: %{some: %{map: map_fun1}}
+ },
+ %{
+ _id: "_design/map_some",
+ views: %{some: %{map: map_fun2}}
+ },
+ %{
+ _id: "_design/partitioned_true",
+ views: %{some: %{map: map_fun1}},
+ options: %{partitioned: true}
+ },
+ %{
+ _id: "_design/partitioned_false",
+ views: %{some: %{map: map_fun1}},
+ options: %{partitioned: false}
+ },
+ %{
+ _id: "_design/reduce",
+ views: %{some: %{map: map_fun2, reduce: "_count"}}
+ },
+ %{
+ _id: "_design/include_ddocs",
+ views: %{some: %{map: map_fun1}},
+ options: %{include_design: true}
+ }
+ ]
+ }
+
+ resp = Couch.post("/#{db_name}/_bulk_docs", query: query, body: body)
+ Enum.each(resp.body, &assert(&1["ok"]))
+
+ {:ok, [db_name: db_name]}
+ end
+
+ def get_reduce_result(resp) do
+ %{:body => %{"rows" => rows}} = resp
+ rows
+ end
+
+ test "query with partitioned:true returns partitioned fields", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_design/partitioned_true/_view/some"
+ resp = Couch.get(url)
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert Enum.dedup(partitions) == ["foo"]
+
+ url = "/#{db_name}/_partition/bar/_design/partitioned_true/_view/some"
+ resp = Couch.get(url)
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert Enum.dedup(partitions) == ["bar"]
+ end
+
+ test "default view query returns partitioned fields", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_design/map/_view/some"
+ resp = Couch.get(url)
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert Enum.dedup(partitions) == ["foo"]
+
+ url = "/#{db_name}/_partition/bar/_design/map/_view/some"
+ resp = Couch.get(url)
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert Enum.dedup(partitions) == ["bar"]
+ end
+
+ test "query will return zero results for wrong inputs", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_design/map/_view/some"
+ resp = Couch.get(url, query: %{start_key: "\"foo:12\""})
+ assert resp.status_code == 200
+ assert Map.get(resp, :body)["rows"] == []
+ end
+
+ test "partitioned ddoc cannot be used in global query", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_design/map/_view/some"
+ resp = Couch.get(url)
+ %{:body => %{"reason" => reason}} = resp
+ assert resp.status_code == 400
+ assert Regex.match?(~r/mandatory for queries to this view./, reason)
+ end
+
+ test "partitioned query cannot be used with global ddoc", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_design/partitioned_false/_view/some"
+ resp = Couch.get(url)
+ %{:body => %{"reason" => reason}} = resp
+ assert resp.status_code == 400
+ assert Regex.match?(~r/is not supported in this design doc/, reason)
+ end
+
+ test "view query returns all docs for global query", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_design/partitioned_false/_view/some"
+ resp = Couch.get(url)
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert length(ids) == 100
+ end
+
+ test "partition query errors with incorrect partition supplied", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/_bar/_design/map/_view/some"
+ resp = Couch.get(url)
+ assert resp.status_code == 400
+
+ url = "/#{db_name}/_partition//_design/map/_view/some"
+ resp = Couch.get(url)
+ assert resp.status_code == 400
+ end
+
+ test "partitioned query works with startkey, endkey range", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_design/map/_view/some"
+ resp = Couch.get(url, query: %{start_key: 12, end_key: 20})
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 5
+ assert Enum.dedup(partitions) == ["foo"]
+ end
+
+ test "partitioned query works with keys", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_design/map/_view/some"
+ resp = Couch.post(url, body: %{keys: [2, 4, 6]})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert length(ids) == 3
+ assert ids == ["foo:2", "foo:4", "foo:6"]
+ end
+
+ test "global query works with keys", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_design/partitioned_false/_view/some"
+ resp = Couch.post(url, body: %{keys: [2, 4, 6]})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert length(ids) == 3
+ assert ids == ["foo:2", "foo:4", "foo:6"]
+ end
+
+ test "partition query works with limit", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_design/map/_view/some"
+ resp = Couch.get(url, query: %{limit: 5})
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 5
+ assert Enum.dedup(partitions) == ["foo"]
+ end
+
+ test "partition query with descending", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_design/map/_view/some"
+ resp = Couch.get(url, query: %{descending: true, limit: 5})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert length(ids) == 5
+ assert ids == ["foo:100", "foo:98", "foo:96", "foo:94", "foo:92"]
+
+ resp = Couch.get(url, query: %{descending: false, limit: 5})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert length(ids) == 5
+ assert ids == ["foo:2", "foo:4", "foo:6", "foo:8", "foo:10"]
+ end
+
+ test "partition query with skip", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_design/map/_view/some"
+ resp = Couch.get(url, query: %{skip: 5, limit: 5})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert length(ids) == 5
+ assert ids == ["foo:12", "foo:14", "foo:16", "foo:18", "foo:20"]
+ end
+
+ test "partition query with key", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_design/map/_view/some"
+ resp = Couch.get(url, query: %{key: 22})
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert length(ids) == 1
+ assert ids == ["foo:22"]
+ end
+
+ test "partition query with startkey_docid and endkey_docid", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_design/map_some/_view/some"
+
+ resp =
+ Couch.get(url,
+ query: %{
+ startkey: "[\"field\",\"one\"]",
+ endkey: "[\"field\",\"one\"]",
+ startkey_docid: "foo:12",
+ endkey_docid: "foo:30"
+ }
+ )
+
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert ids == ["foo:12", "foo:18", "foo:24", "foo:30"]
+ end
+
+ test "query with reduce works", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_design/reduce/_view/some"
+ resp = Couch.get(url, query: %{reduce: true, group_level: 1})
+ assert resp.status_code == 200
+ results = get_reduce_result(resp)
+ assert results == [%{"key" => ["field"], "value" => 50}]
+
+ resp = Couch.get(url, query: %{reduce: true, group_level: 2})
+ results = get_reduce_result(resp)
+
+ assert results == [
+ %{"key" => ["field", "one"], "value" => 16},
+ %{"key" => ["field", "two"], "value" => 34}
+ ]
+
+ resp = Couch.get(url, query: %{reduce: true, group: true})
+ results = get_reduce_result(resp)
+
+ assert results == [
+ %{"key" => ["field", "one"], "value" => 16},
+ %{"key" => ["field", "two"], "value" => 34}
+ ]
+ end
+
+ test "include_design works correctly", context do
+ db_name = context[:db_name]
+
+ url = "/#{db_name}/_partition/foo/_design/include_ddocs/_view/some"
+ resp = Couch.get(url)
+ assert resp.status_code == 200
+ partitions = get_partitions(resp)
+ assert length(partitions) == 50
+ assert Enum.dedup(partitions) == ["foo"]
+ end
+end
diff --git a/test/elixir/test/partition_view_update_test.exs b/test/elixir/test/partition_view_update_test.exs
new file mode 100644
index 000000000..502d5fabe
--- /dev/null
+++ b/test/elixir/test/partition_view_update_test.exs
@@ -0,0 +1,155 @@
+defmodule PartitionViewUpdateTest do
+ use CouchTestCase
+ import PartitionHelpers
+
+ @moduledoc """
+ Test Partition view update functionality
+ """
+ @tag :with_partitioned_db
+ test "view updates properly remove old keys", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name, "foo", "bar")
+ create_partition_ddoc(db_name)
+
+ check_key = fn key, num_rows ->
+ url = "/#{db_name}/_partition/foo/_design/mrtest/_view/some"
+ resp = Couch.get(url, query: [key: key])
+ assert resp.status_code == 200
+ assert length(resp.body["rows"]) == num_rows
+ end
+
+ check_key.(2, 1)
+
+ resp = Couch.get("/#{db_name}/foo:2")
+ doc = Map.put(resp.body, "value", 4)
+ resp = Couch.put("/#{db_name}/foo:2", query: [w: 3], body: doc)
+ assert resp.status_code >= 201 and resp.status_code <= 202
+
+ check_key.(4, 2)
+ check_key.(2, 0)
+ end
+
+ @tag :with_partitioned_db
+ test "query with update=false works", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+ create_partition_ddoc(db_name)
+
+ url = "/#{db_name}/_partition/foo/_design/mrtest/_view/some"
+
+ resp =
+ Couch.get(url,
+ query: %{
+ update: "true",
+ limit: 3
+ }
+ )
+
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert ids == ["foo:2", "foo:4", "foo:6"]
+
+ # Avoid race conditions by attempting to get a full response
+ # from every shard before we do our update:false test
+ for _ <- 1..12 do
+ resp = Couch.get(url)
+ assert resp.status_code == 200
+ end
+
+ Couch.put("/#{db_name}/foo:1", body: %{some: "field"})
+
+ resp =
+ Couch.get(url,
+ query: %{
+ update: "false",
+ limit: 3
+ }
+ )
+
+ assert resp.status_code == 200
+ ids = get_ids(resp)
+ assert ids == ["foo:2", "foo:4", "foo:6"]
+ end
+
+ @tag :with_partitioned_db
+ test "purge removes view rows", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+ create_partition_ddoc(db_name)
+
+ url = "/#{db_name}/_partition/foo/_design/mrtest/_view/some"
+
+ resp = Couch.get(url)
+ assert resp.status_code == 200
+ %{body: body} = resp
+ assert length(body["rows"]) == 50
+
+ resp = Couch.get("/#{db_name}/foo:2")
+ assert resp.status_code == 200
+ %{body: body} = resp
+ rev = body["_rev"]
+
+ body = %{"foo:2" => [rev]}
+ resp = Couch.post("/#{db_name}/_purge", query: [w: 3], body: body)
+ assert resp.status_code == 201
+
+ resp = Couch.get(url)
+ assert resp.status_code == 200
+ %{body: body} = resp
+ assert length(body["rows"]) == 49
+ end
+
+ @tag :with_partitioned_db
+ test "purged conflict changes view rows", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+ create_partition_ddoc(db_name)
+
+ url = "/#{db_name}/_partition/foo/_design/mrtest/_view/some"
+
+ resp = Couch.get(url)
+ assert resp.status_code == 200
+ %{body: body} = resp
+ assert length(body["rows"]) == 50
+
+ # Create a conflict on foo:2. Since the 4096
+ # value is deeper than the conflict we can assert
+ # that's in the view before the purge and assert
+ # that 8192 is in the view after the purge.
+ resp = Couch.get("/#{db_name}/foo:2")
+ assert resp.status_code == 200
+ %{body: body} = resp
+ rev1 = body["_rev"]
+
+ doc = %{_id: "foo:2", _rev: rev1, value: 4096, some: "field"}
+ resp = Couch.post("/#{db_name}", query: [w: 3], body: doc)
+ assert resp.status_code == 201
+ %{body: body} = resp
+ rev2 = body["rev"]
+
+ query = [w: 3, new_edits: false]
+ conflict_rev = "1-4a75b4efa0804859b3dfd327cbc1c2f9"
+ doc = %{_id: "foo:2", _rev: conflict_rev, value: 8192, some: "field"}
+ resp = Couch.put("/#{db_name}/foo:2", query: query, body: doc)
+ assert resp.status_code == 201
+
+ # Check that our expected row exists
+ resp = Couch.get(url, query: [key: 4096])
+ assert resp.status_code == 200
+ %{body: body} = resp
+ [row] = body["rows"]
+ assert row["id"] == "foo:2"
+
+ # Remove the current row to be replaced with
+ # a row from the conflict
+ body = %{"foo:2" => [rev2]}
+ resp = Couch.post("/#{db_name}/_purge", query: [w: 3], body: body)
+ assert resp.status_code == 201
+
+ resp = Couch.get(url, query: [key: 8192])
+ assert resp.status_code == 200
+ %{body: body} = resp
+ [row] = body["rows"]
+ assert row["id"] == "foo:2"
+ end
+end
diff --git a/test/elixir/test/test_helper.exs b/test/elixir/test/test_helper.exs
index 33041fd02..d6843eb21 100644
--- a/test/elixir/test/test_helper.exs
+++ b/test/elixir/test/test_helper.exs
@@ -1,2 +1,3 @@
ExUnit.configure(exclude: [pending: true])
ExUnit.start()
+Code.require_file("partition_helpers.exs", __DIR__)