diff options
author | Paul J. Davis <paul.joseph.davis@gmail.com> | 2018-11-28 10:58:42 -0600 |
---|---|---|
committer | Paul J. Davis <paul.joseph.davis@gmail.com> | 2019-01-18 13:03:28 -0600 |
commit | 16e6af4cf8ec268f7f7eef0f78b3cf0ec3b6f3bd (patch) | |
tree | e2b0e67b8980c233c1981c2e0bb7d9a3528274cf | |
parent | 53d3515e32d866f72956fb1ec831da52357f1999 (diff) | |
download | couchdb-16e6af4cf8ec268f7f7eef0f78b3cf0ec3b6f3bd.tar.gz |
Add Elixir tests for 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.ex | 12 | ||||
-rw-r--r-- | test/elixir/test/partition_all_docs_test.exs | 118 | ||||
-rw-r--r-- | test/elixir/test/partition_crud_test.exs | 356 | ||||
-rw-r--r-- | test/elixir/test/partition_ddoc_test.exs | 171 | ||||
-rw-r--r-- | test/elixir/test/partition_design_docs_test.exs | 16 | ||||
-rw-r--r-- | test/elixir/test/partition_helpers.exs | 76 | ||||
-rw-r--r-- | test/elixir/test/partition_mango_test.exs | 591 | ||||
-rw-r--r-- | test/elixir/test/partition_size_test.exs | 357 | ||||
-rw-r--r-- | test/elixir/test/partition_view_test.exs | 299 | ||||
-rw-r--r-- | test/elixir/test/partition_view_update_test.exs | 155 | ||||
-rw-r--r-- | test/elixir/test/test_helper.exs | 1 |
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__) |