summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoan Touzet <wohali@users.noreply.github.com>2017-09-19 12:57:12 -0400
committerGitHub <noreply@github.com>2017-09-19 12:57:12 -0400
commit8d078844d373d8bde752ab6b325b067f267ef001 (patch)
tree5e81fcf93bd23bd619bbde4def37c247daa45ab2
parent297333f086a139dc2692a0e4f2bdac3d0dc3fde1 (diff)
parentc531a13b22cc6fcea6afb342eb3f5cb315db0313 (diff)
downloadcouchdb-8d078844d373d8bde752ab6b325b067f267ef001.tar.gz
Merge branch 'master' into run-mango-tests-with-check
-rwxr-xr-xdev/run8
-rw-r--r--rel/overlay/etc/default.ini15
-rw-r--r--src/chttpd/src/chttpd.erl2
-rw-r--r--src/chttpd/test/chttpd_db_attachment_size_tests.erl206
-rw-r--r--src/chttpd/test/chttpd_db_doc_size_tests.erl2
-rw-r--r--src/couch/src/couch_att.erl90
-rw-r--r--src/couch/src/couch_compaction_daemon.erl44
-rw-r--r--src/couch/src/couch_doc.erl26
-rw-r--r--src/couch/src/couch_ejson_size.erl99
-rw-r--r--src/couch/src/couch_httpd.erl4
-rw-r--r--src/couch/test/couch_doc_tests.erl1
-rw-r--r--src/couch/test/couch_ejson_size_tests.erl72
-rw-r--r--src/couch/test/couchdb_attachments_tests.erl132
-rw-r--r--src/couch_mrview/src/couch_mrview.erl8
-rw-r--r--src/couch_mrview/test/couch_mrview_ddoc_validation_tests.erl59
-rw-r--r--src/couch_replicator/src/couch_replicator_api_wrap.erl4
-rw-r--r--src/couch_replicator/src/couch_replicator_ids.erl16
-rw-r--r--src/couch_replicator/test/couch_replicator_attachments_too_large.erl104
-rw-r--r--src/fabric/src/fabric_doc_attachments.erl28
-rw-r--r--src/fabric/src/fabric_doc_update.erl4
-rw-r--r--src/mango/src/mango_cursor.erl31
-rw-r--r--src/mango/src/mango_cursor_text.erl3
-rw-r--r--src/mango/src/mango_cursor_view.erl3
-rw-r--r--src/mango/src/mango_error.erl6
-rw-r--r--src/mango/src/mango_httpd.erl30
-rw-r--r--src/mango/src/mango_idx.erl15
-rw-r--r--src/mango/src/mango_idx_view.erl6
-rw-r--r--src/mango/src/mango_native_proc.erl38
-rw-r--r--src/mango/test/03-operator-test.py72
-rw-r--r--src/mango/test/05-index-selection-test.py38
-rw-r--r--src/mango/test/12-use-correct-index.py19
-rw-r--r--src/mango/test/16-index-selectors.py156
-rw-r--r--src/mango/test/mango.py17
-rw-r--r--src/mango/test/user_docs.py28
-rw-r--r--test/javascript/tests/attachments.js18
35 files changed, 1256 insertions, 148 deletions
diff --git a/dev/run b/dev/run
index 2f25071a1..5693e1273 100755
--- a/dev/run
+++ b/dev/run
@@ -146,6 +146,7 @@ def setup_context(opts, args):
'haproxy': opts.haproxy,
'haproxy_port': opts.haproxy_port,
'config_overrides': opts.config_overrides,
+ 'reset_logs': True,
'procs': []}
@@ -400,8 +401,12 @@ def boot_node(ctx, node):
"-pa", os.path.join(erl_libs, "*"),
"-s", "boot_node"
]
+ if ctx['reset_logs']:
+ mode = "wb"
+ else:
+ mode = "r+b"
logfname = os.path.join(ctx['devdir'], "logs", "%s.log" % node)
- log = open(logfname, "wb")
+ log = open(logfname, mode)
cmd = [toposixpath(x) for x in cmd]
return sp.Popen(cmd, stdin=sp.PIPE, stdout=log, stderr=sp.STDOUT, env=env)
@@ -545,6 +550,7 @@ def run_command(ctx, cmd):
@log('Restart all nodes')
def reboot_nodes(ctx):
+ ctx['reset_logs'] = False
kill_processes(ctx)
boot_nodes(ctx)
ensure_all_nodes_alive(ctx)
diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index 30b2efa5c..653131e0c 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -40,9 +40,18 @@ default_security = admin_local
changes_doc_ids_optimization_threshold = 100
; Maximum document ID length. Can be set to an integer or 'infinity'.
;max_document_id_length = infinity
-; Single documents that exceed this value in a bulk request will receive a
-; too_large error. The max_http_request_size still takes precedence.
-;single_max_doc_size = 1048576
+;
+; Limit maximum document size. Requests to create / update documents with a body
+; size larger than this will fail with a 413 http error. This limit applies to
+; requests which update a single document as well as individual documents from
+; a _bulk_docs request. Since there is no canonical size of json encoded data,
+; due to variabiliy in what is escaped or how floats are encoded, this limit is
+; applied conservatively. For example 1.0e+16 could be encoded as 1e16, so 4 used
+; for size calculation instead of 7.
+;max_document_size = 4294967296 ; bytes
+
+; Maximum attachment size.
+; max_attachment_size = infinity
[cluster]
q=8
diff --git a/src/chttpd/src/chttpd.erl b/src/chttpd/src/chttpd.erl
index 425f95a6f..6be0d1848 100644
--- a/src/chttpd/src/chttpd.erl
+++ b/src/chttpd/src/chttpd.erl
@@ -897,6 +897,8 @@ error_info({missing_stub, Reason}) ->
{412, <<"missing_stub">>, Reason};
error_info(request_entity_too_large) ->
{413, <<"too_large">>, <<"the request entity is too large">>};
+error_info({request_entity_too_large, {attachment, AttName}}) ->
+ {413, <<"attachment_too_large">>, AttName};
error_info({request_entity_too_large, DocID}) ->
{413, <<"document_too_large">>, DocID};
error_info({error, security_migration_updates_disabled}) ->
diff --git a/src/chttpd/test/chttpd_db_attachment_size_tests.erl b/src/chttpd/test/chttpd_db_attachment_size_tests.erl
new file mode 100644
index 000000000..0ab08dd80
--- /dev/null
+++ b/src/chttpd/test/chttpd_db_attachment_size_tests.erl
@@ -0,0 +1,206 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(chttpd_db_attachment_size_tests).
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+
+-define(USER, "chttpd_db_att_test_admin").
+-define(PASS, "pass").
+-define(AUTH, {basic_auth, {?USER, ?PASS}}).
+-define(CONTENT_JSON, {"Content-Type", "application/json"}).
+-define(CONTENT_MULTI_RELATED, {"Content-Type",
+ "multipart/related;boundary=\"bound\""}).
+
+
+setup() ->
+ Hashed = couch_passwords:hash_admin_password(?PASS),
+ ok = config:set("admins", ?USER, ?b2l(Hashed), _Persist=false),
+ ok = config:set("couchdb", "max_attachment_size", "50", _Persist=false),
+ TmpDb = ?tempdb(),
+ Addr = config:get("chttpd", "bind_address", "127.0.0.1"),
+ Port = integer_to_list(mochiweb_socket_server:get(chttpd, port)),
+ Url = "http://" ++ Addr ++ ":" ++ Port ++ "/" ++ ?b2l(TmpDb),
+ create_db(Url),
+ add_doc(Url, "doc1"),
+ Url.
+
+
+teardown(Url) ->
+ delete_db(Url),
+ ok = config:delete("admins", ?USER, _Persist=false),
+ ok = config:delete("couchdb", "max_attachment_size").
+
+
+attachment_size_test_() ->
+ {
+ "chttpd max_attachment_size tests",
+ {
+ setup,
+ fun chttpd_test_util:start_couch/0,
+ fun chttpd_test_util:stop_couch/1,
+ {
+ foreach,
+ fun setup/0, fun teardown/1,
+ [
+ fun put_inline/1,
+ fun put_simple/1,
+ fun put_simple_chunked/1,
+ fun put_mp_related/1
+ ]
+ }
+ }
+ }.
+
+
+put_inline(Url) ->
+ ?_test(begin
+ Status = put_inline(Url, "doc2", 50),
+ ?assert(Status =:= 201 orelse Status =:= 202),
+ ?assertEqual(413, put_inline(Url, "doc3", 51))
+ end).
+
+
+put_simple(Url) ->
+ ?_test(begin
+ Headers = [{"Content-Type", "app/binary"}],
+ Rev1 = doc_rev(Url, "doc1"),
+ Data1 = data(50),
+ Status1 = put_req(Url ++ "/doc1/att2?rev=" ++ Rev1, Headers, Data1),
+ ?assert(Status1 =:= 201 orelse Status1 =:= 202),
+ Data2 = data(51),
+ Rev2 = doc_rev(Url, "doc1"),
+ Status2 = put_req(Url ++ "/doc1/att3?rev=" ++ Rev2, Headers, Data2),
+ ?assertEqual(413, Status2)
+ end).
+
+
+put_simple_chunked(Url) ->
+ ?_test(begin
+ Headers = [{"Content-Type", "app/binary"}],
+ Rev1 = doc_rev(Url, "doc1"),
+ DataFun1 = data_stream_fun(50),
+ Status1 = put_req_chunked(Url ++ "/doc1/att2?rev=" ++ Rev1, Headers, DataFun1),
+ ?assert(Status1 =:= 201 orelse Status1 =:= 202),
+ DataFun2 = data_stream_fun(51),
+ Rev2 = doc_rev(Url, "doc1"),
+ Status2 = put_req_chunked(Url ++ "/doc1/att3?rev=" ++ Rev2, Headers, DataFun2),
+ ?assertEqual(413, Status2)
+ end).
+
+
+put_mp_related(Url) ->
+ ?_test(begin
+ Headers = [?CONTENT_MULTI_RELATED],
+ Body1 = mp_body(50),
+ Status1 = put_req(Url ++ "/doc2", Headers, Body1),
+ ?assert(Status1 =:= 201 orelse Status1 =:= 202),
+ Body2 = mp_body(51),
+ Status2 = put_req(Url ++ "/doc3", Headers, Body2),
+ ?assertEqual(413, Status2)
+ end).
+
+
+% Helper functions
+
+create_db(Url) ->
+ Status = put_req(Url, "{}"),
+ ?assert(Status =:= 201 orelse Status =:= 202).
+
+
+add_doc(Url, DocId) ->
+ Status = put_req(Url ++ "/" ++ DocId, "{}"),
+ ?assert(Status =:= 201 orelse Status =:= 202).
+
+
+delete_db(Url) ->
+ {ok, 200, _, _} = test_request:delete(Url, [?AUTH]).
+
+
+put_inline(Url, DocId, Size) ->
+ Doc = "{\"_attachments\": {\"att1\":{"
+ "\"content_type\": \"app/binary\", "
+ "\"data\": \"" ++ data_b64(Size) ++ "\""
+ "}}}",
+ put_req(Url ++ "/" ++ DocId, Doc).
+
+
+mp_body(AttSize) ->
+ AttData = data(AttSize),
+ SizeStr = integer_to_list(AttSize),
+ string:join([
+ "--bound",
+
+ "Content-Type: application/json",
+
+ "",
+
+ "{\"_id\":\"doc2\", \"_attachments\":{\"att\":"
+ "{\"content_type\":\"app/binary\", \"length\":" ++ SizeStr ++ ","
+ "\"follows\":true}}}",
+
+ "--bound",
+
+ "Content-Disposition: attachment; filename=\"att\"",
+
+ "Content-Type: app/binary",
+
+ "",
+
+ AttData,
+
+ "--bound--"
+ ], "\r\n").
+
+
+doc_rev(Url, DocId) ->
+ {200, ResultProps} = get_req(Url ++ "/" ++ DocId),
+ {<<"_rev">>, BinRev} = lists:keyfind(<<"_rev">>, 1, ResultProps),
+ binary_to_list(BinRev).
+
+
+put_req(Url, Body) ->
+ put_req(Url, [], Body).
+
+
+put_req(Url, Headers, Body) ->
+ {ok, Status, _, _} = test_request:put(Url, Headers ++ [?AUTH], Body),
+ Status.
+
+
+put_req_chunked(Url, Headers, Body) ->
+ Opts = [{transfer_encoding, {chunked, 1}}],
+ {ok, Status, _, _} = test_request:put(Url, Headers ++ [?AUTH], Body, Opts),
+ Status.
+
+
+get_req(Url) ->
+ {ok, Status, _, ResultBody} = test_request:get(Url, [?CONTENT_JSON, ?AUTH]),
+ {[_ | _] = ResultProps} = ?JSON_DECODE(ResultBody),
+ {Status, ResultProps}.
+
+% Data streaming generator for ibrowse client. ibrowse will repeatedly call the
+% function with State and it should return {ok, Data, NewState} or eof at end.
+data_stream_fun(Size) ->
+ Fun = fun(0) -> eof; (BytesLeft) ->
+ {ok, <<"x">>, BytesLeft - 1}
+ end,
+ {Fun, Size}.
+
+
+data(Size) ->
+ string:copies("x", Size).
+
+
+data_b64(Size) ->
+ base64:encode_to_string(data(Size)).
diff --git a/src/chttpd/test/chttpd_db_doc_size_tests.erl b/src/chttpd/test/chttpd_db_doc_size_tests.erl
index 489a83c1b..88e2797a3 100644
--- a/src/chttpd/test/chttpd_db_doc_size_tests.erl
+++ b/src/chttpd/test/chttpd_db_doc_size_tests.erl
@@ -93,7 +93,7 @@ put_single_doc(Url) ->
bulk_doc(Url) ->
NewDoc = "{\"docs\": [{\"doc1\": 1}, {\"errordoc\":
- \"this_should_be_the_error_document\"}]}",
+ \"this_should_be_the_too_large_error_document\"}]}",
{ok, _, _, ResultBody} = test_request:post(Url ++ "/_bulk_docs/",
[?CONTENT_JSON, ?AUTH], NewDoc),
ResultJson = ?JSON_DECODE(ResultBody),
diff --git a/src/couch/src/couch_att.erl b/src/couch/src/couch_att.erl
index 9d38cfae2..5c040a8c4 100644
--- a/src/couch/src/couch_att.erl
+++ b/src/couch/src/couch_att.erl
@@ -50,6 +50,11 @@
downgrade/1
]).
+-export([
+ max_attachment_size/0,
+ validate_attachment_size/3
+]).
+
-compile(nowarn_deprecated_type).
-export_type([att/0]).
@@ -408,13 +413,21 @@ follow_from_json(Att, Props) ->
inline_from_json(Att, Props) ->
B64Data = couch_util:get_value(<<"data">>, Props),
- Data = base64:decode(B64Data),
- Length = size(Data),
- RevPos = couch_util:get_value(<<"revpos">>, Props, 0),
- store([
- {data, Data}, {revpos, RevPos}, {disk_len, Length},
- {att_len, Length}
- ], Att).
+ try base64:decode(B64Data) of
+ Data ->
+ Length = size(Data),
+ RevPos = couch_util:get_value(<<"revpos">>, Props, 0),
+ store([
+ {data, Data}, {revpos, RevPos}, {disk_len, Length},
+ {att_len, Length}
+ ], Att)
+ catch
+ _:_ ->
+ Name = fetch(name, Att),
+ ErrMsg = <<"Invalid attachment data for ", Name/binary>>,
+ throw({bad_request, ErrMsg})
+ end.
+
encoded_lengths_from_json(Props) ->
@@ -500,6 +513,8 @@ flush_data(Fd, Data, Att) when is_binary(Data) ->
couch_stream:write(OutputStream, Data)
end);
flush_data(Fd, Fun, Att) when is_function(Fun) ->
+ AttName = fetch(name, Att),
+ MaxAttSize = max_attachment_size(),
case fetch(att_len, Att) of
undefined ->
couch_db:with_stream(Fd, Att, fun(OutputStream) ->
@@ -510,7 +525,7 @@ flush_data(Fd, Fun, Att) when is_function(Fun) ->
% WriterFun({0, _Footers}, State)
% Called with Length == 0 on the last time.
% WriterFun returns NewState.
- fun({0, Footers}, _) ->
+ fun({0, Footers}, _Total) ->
F = mochiweb_headers:from_binary(Footers),
case mochiweb_headers:get_value("Content-MD5", F) of
undefined ->
@@ -518,11 +533,15 @@ flush_data(Fd, Fun, Att) when is_function(Fun) ->
Md5 ->
{md5, base64:decode(Md5)}
end;
- ({_Length, Chunk}, _) ->
- couch_stream:write(OutputStream, Chunk)
- end, ok)
+ ({Length, Chunk}, Total0) ->
+ Total = Total0 + Length,
+ validate_attachment_size(AttName, Total, MaxAttSize),
+ couch_stream:write(OutputStream, Chunk),
+ Total
+ end, 0)
end);
AttLen ->
+ validate_attachment_size(AttName, AttLen, MaxAttSize),
couch_db:with_stream(Fd, Att, fun(OutputStream) ->
write_streamed_attachment(OutputStream, Fun, AttLen)
end)
@@ -680,6 +699,22 @@ upgrade_encoding(false) -> identity;
upgrade_encoding(Encoding) -> Encoding.
+max_attachment_size() ->
+ case config:get("couchdb", "max_attachment_size", "infinity") of
+ "infinity" ->
+ infinity;
+ MaxAttSize ->
+ list_to_integer(MaxAttSize)
+ end.
+
+
+validate_attachment_size(AttName, AttSize, MaxAttSize)
+ when is_integer(AttSize), AttSize > MaxAttSize ->
+ throw({request_entity_too_large, {attachment, AttName}});
+validate_attachment_size(_AttName, _AttSize, _MAxAttSize) ->
+ ok.
+
+
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@@ -760,8 +795,37 @@ attachment_disk_term_test_() ->
attachment_json_term_test_() ->
- %% We need to create a few variations including stubs and inline data.
- {"JSON term tests", []}.
+ Props = [
+ {<<"content_type">>, <<"application/json">>},
+ {<<"digest">>, <<"md5-QCNtWUNXV0UzJnEjMk92YUk1JA==">>},
+ {<<"length">>, 14},
+ {<<"revpos">>, 1}
+ ],
+ PropsInline = [{<<"data">>, <<"eyJhbnN3ZXIiOiA0Mn0=">>}] ++ Props,
+ InvalidProps = [{<<"data">>, <<"!Base64Encoded$">>}] ++ Props,
+ Att = couch_att:new([
+ {name, <<"attachment.json">>},
+ {type, <<"application/json">>}
+ ]),
+ ResultStub = couch_att:new([
+ {name, <<"attachment.json">>},
+ {type, <<"application/json">>},
+ {att_len, 14},
+ {disk_len, 14},
+ {md5, <<"@#mYCWWE3&q#2OvaI5$">>},
+ {revpos, 1},
+ {data, stub},
+ {encoding, identity}
+ ]),
+ ResultFollows = ResultStub#att{data = follows},
+ ResultInline = ResultStub#att{md5 = <<>>, data = <<"{\"answer\": 42}">>},
+ {"JSON term tests", [
+ ?_assertEqual(ResultStub, stub_from_json(Att, Props)),
+ ?_assertEqual(ResultFollows, follow_from_json(Att, Props)),
+ ?_assertEqual(ResultInline, inline_from_json(Att, PropsInline)),
+ ?_assertThrow({bad_request, _}, inline_from_json(Att, Props)),
+ ?_assertThrow({bad_request, _}, inline_from_json(Att, InvalidProps))
+ ]}.
attachment_stub_merge_test_() ->
diff --git a/src/couch/src/couch_compaction_daemon.erl b/src/couch/src/couch_compaction_daemon.erl
index 8f95eb21e..024b867d0 100644
--- a/src/couch/src/couch_compaction_daemon.erl
+++ b/src/couch/src/couch_compaction_daemon.erl
@@ -509,34 +509,46 @@ free_space(Path) ->
length(filename:split(PathA)) > length(filename:split(PathB))
end,
disksup:get_disk_data()),
- free_space_rec(abs_path(Path), DiskData).
+ {ok, AbsPath} = abs_path(Path),
+ free_space_rec(AbsPath, DiskData).
free_space_rec(_Path, []) ->
undefined;
free_space_rec(Path, [{MountPoint0, Total, Usage} | Rest]) ->
- MountPoint = abs_path(MountPoint0),
- case MountPoint =:= string:substr(Path, 1, length(MountPoint)) of
- false ->
- free_space_rec(Path, Rest);
- true ->
- trunc(Total - (Total * (Usage / 100))) * 1024
+ case abs_path(MountPoint0) of
+ {ok, MountPoint} ->
+ case MountPoint =:= string:substr(Path, 1, length(MountPoint)) of
+ false ->
+ free_space_rec(Path, Rest);
+ true ->
+ trunc(Total - (Total * (Usage / 100))) * 1024
+ end;
+ {error, Reason} ->
+ couch_log:warning("Compaction daemon - unable to calculate free space"
+ " for `~s`: `~s`",
+ [MountPoint0, Reason]),
+ free_space_rec(Path, Rest)
end.
abs_path(Path0) ->
- {ok, Info} = file:read_link_info(Path0),
- case Info#file_info.type of
- symlink ->
- {ok, Path} = file:read_link(Path0),
- abs_path(Path);
- _ ->
- abs_path2(Path0)
+ case file:read_link_info(Path0) of
+ {ok, Info} ->
+ case Info#file_info.type of
+ symlink ->
+ {ok, Path} = file:read_link(Path0),
+ abs_path(Path);
+ _ ->
+ abs_path2(Path0)
+ end;
+ {error, Reason} ->
+ {error, Reason}
end.
abs_path2(Path0) ->
Path = filename:absname(Path0),
case lists:last(Path) of
$/ ->
- Path;
+ {ok, Path};
_ ->
- Path ++ "/"
+ {ok, Path ++ "/"}
end.
diff --git a/src/couch/src/couch_doc.erl b/src/couch/src/couch_doc.erl
index 381ad4b4f..eb96d44bb 100644
--- a/src/couch/src/couch_doc.erl
+++ b/src/couch/src/couch_doc.erl
@@ -16,6 +16,7 @@
-export([from_json_obj/1, from_json_obj_validate/1, to_json_obj/2,has_stubs/1, merge_stubs/2]).
-export([validate_docid/1, get_validate_doc_fun/1]).
-export([doc_from_multi_part_stream/2, doc_from_multi_part_stream/3]).
+-export([doc_from_multi_part_stream/4]).
-export([doc_to_multi_part_stream/5, len_doc_to_multi_part_stream/4]).
-export([restart_open_doc_revs/3]).
-export([to_path/1]).
@@ -127,13 +128,26 @@ doc_to_json_obj(#doc{id=Id,deleted=Del,body=Body,revs={Start, RevIds},
from_json_obj_validate(EJson) ->
MaxSize = config:get_integer("couchdb", "max_document_size", 4294967296),
Doc = from_json_obj(EJson),
- case erlang:external_size(Doc#doc.body) =< MaxSize of
+ case couch_ejson_size:encoded_size(Doc#doc.body) =< MaxSize of
true ->
+ validate_attachment_sizes(Doc#doc.atts),
Doc;
false ->
throw({request_entity_too_large, Doc#doc.id})
end.
+
+validate_attachment_sizes([]) ->
+ ok;
+validate_attachment_sizes(Atts) ->
+ MaxAttSize = couch_att:max_attachment_size(),
+ lists:foreach(fun(Att) ->
+ AttName = couch_att:fetch(name, Att),
+ AttSize = couch_att:fetch(att_len, Att),
+ couch_att:validate_attachment_size(AttName, AttSize, MaxAttSize)
+ end, Atts).
+
+
from_json_obj({Props}) ->
transfer_fields(Props, #doc{body=[]});
@@ -420,11 +434,19 @@ doc_from_multi_part_stream(ContentType, DataFun) ->
doc_from_multi_part_stream(ContentType, DataFun, make_ref()).
doc_from_multi_part_stream(ContentType, DataFun, Ref) ->
+ doc_from_multi_part_stream(ContentType, DataFun, Ref, true).
+
+doc_from_multi_part_stream(ContentType, DataFun, Ref, ValidateDocLimits) ->
case couch_httpd_multipart:decode_multipart_stream(ContentType, DataFun, Ref) of
{{started_open_doc_revs, NewRef}, Parser, _ParserRef} ->
restart_open_doc_revs(Parser, Ref, NewRef);
{{doc_bytes, Ref, DocBytes}, Parser, ParserRef} ->
- Doc = from_json_obj_validate(?JSON_DECODE(DocBytes)),
+ Doc = case ValidateDocLimits of
+ true ->
+ from_json_obj_validate(?JSON_DECODE(DocBytes));
+ false ->
+ from_json_obj(?JSON_DECODE(DocBytes))
+ end,
erlang:put(mochiweb_request_recv, true),
% we'll send the Parser process ID to the remote nodes so they can
% retrieve their own copies of the attachment data
diff --git a/src/couch/src/couch_ejson_size.erl b/src/couch/src/couch_ejson_size.erl
new file mode 100644
index 000000000..f5505680f
--- /dev/null
+++ b/src/couch/src/couch_ejson_size.erl
@@ -0,0 +1,99 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_ejson_size).
+
+-export([encoded_size/1]).
+
+
+%% Compound objects
+
+encoded_size({[]}) ->
+ 2; % opening { and closing }
+
+encoded_size({KVs}) ->
+ % Would add 2 because opening { and closing }, but then inside the LC
+ % would accumulate an extra , at the end so subtract 2 - 1
+ 1 + lists:sum([encoded_size(K) + encoded_size(V) + 2 || {K,V} <- KVs]);
+
+encoded_size([]) ->
+ 2; % opening [ and closing ]
+
+encoded_size(List) when is_list(List) ->
+ % 2 is for [ and ] but inside LC would accumulate an extra , so subtract
+ % 2 - 1
+ 1 + lists:sum([encoded_size(V) + 1 || V <- List]);
+
+%% Floats.
+
+encoded_size(0.0) ->
+ 3;
+
+encoded_size(1.0) ->
+ 3;
+
+encoded_size(Float) when is_float(Float), Float < 0.0 ->
+ encoded_size(-Float) + 1;
+
+encoded_size(Float) when is_float(Float), Float < 1.0 ->
+ if
+ Float =< 1.0e-300 -> 3; % close enough to 0.0
+ Float =< 1.0e-100 -> 6; % Xe-YYY
+ Float =< 1.0e-10 -> 5; % Xe-YY
+ Float =< 0.01 -> 4; % Xe-Y, 0.0X
+ true -> 3 % 0.X
+ end;
+
+encoded_size(Float) when is_float(Float) ->
+ if
+ Float >= 1.0e100 -> 5; % XeYYY
+ Float >= 1.0e10 -> 4; % XeYY
+ true -> 3 % XeY, X.Y
+ end;
+
+%% Integers
+
+encoded_size(0) ->
+ 1;
+
+encoded_size(Integer) when is_integer(Integer), Integer < 0 ->
+ encoded_size(-Integer) + 1;
+
+encoded_size(Integer) when is_integer(Integer) ->
+ if
+ Integer < 10 -> 1;
+ Integer < 100 -> 2;
+ Integer < 1000 -> 3;
+ Integer < 10000 -> 4;
+ true -> trunc(math:log10(Integer)) + 1
+ end;
+
+%% Strings
+
+encoded_size(Binary) when is_binary(Binary) ->
+ 2 + byte_size(Binary);
+
+%% Special terminal symbols as atoms
+
+encoded_size(null) ->
+ 4;
+
+encoded_size(true) ->
+ 4;
+
+encoded_size(false) ->
+ 5;
+
+%% Other atoms
+
+encoded_size(Atom) when is_atom(Atom) ->
+ encoded_size(atom_to_binary(Atom, utf8)).
diff --git a/src/couch/src/couch_httpd.erl b/src/couch/src/couch_httpd.erl
index faaf080d9..b3bbd5baa 100644
--- a/src/couch/src/couch_httpd.erl
+++ b/src/couch/src/couch_httpd.erl
@@ -885,6 +885,10 @@ error_info(file_exists) ->
"created, the file already exists.">>};
error_info(request_entity_too_large) ->
{413, <<"too_large">>, <<"the request entity is too large">>};
+error_info({request_entity_too_large, {attachment, AttName}}) ->
+ {413, <<"attachment_too_large">>, AttName};
+error_info({request_entity_too_large, DocID}) ->
+ {413, <<"document_too_large">>, DocID};
error_info(request_uri_too_long) ->
{414, <<"too_long">>, <<"the request uri is too long">>};
error_info({bad_ctype, Reason}) ->
diff --git a/src/couch/test/couch_doc_tests.erl b/src/couch/test/couch_doc_tests.erl
index d24cd67c0..5d0448a9e 100644
--- a/src/couch/test/couch_doc_tests.erl
+++ b/src/couch/test/couch_doc_tests.erl
@@ -131,6 +131,7 @@ mock_config_max_document_id_length() ->
ok = meck:new(config, [passthrough]),
meck:expect(config, get,
fun("couchdb", "max_document_id_length", "infinity") -> "1024";
+ ("couchdb", "max_attachment_size", "infinity") -> "infinity";
(Key, Val, Default) -> meck:passthrough([Key, Val, Default])
end
).
diff --git a/src/couch/test/couch_ejson_size_tests.erl b/src/couch/test/couch_ejson_size_tests.erl
new file mode 100644
index 000000000..df9168ed1
--- /dev/null
+++ b/src/couch/test/couch_ejson_size_tests.erl
@@ -0,0 +1,72 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_ejson_size_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+-define(HWAIR, $\x{10348}). % 4 byte utf8 encoding
+-define(EURO, $\x{20ac}). % 3 byte utf8 encoding
+-define(CENT, $\x{a2}). % 2 byte utf8 encoding
+
+
+ejson_size_test_() ->
+ [?_assertEqual(R, couch_ejson_size:encoded_size(Input)) || {R, Input} <- [
+ {1, 1}, {1, 1}, {2, -1}, {1, 9}, {2, 10}, {3, -10},
+ {2, 11}, {2, 99}, {3, 100}, {3, 999}, {4, 1000}, {4, 9999},
+ {5, 10000},
+
+ {3, 0.0}, {3, 0.1}, {3, 1.0}, {4, -1.0}, {3, 1.0e9},
+ {4, 1.0e10}, {5, 1.0e-10}, {5, 1.0e-99}, {6, 1.0e-100}, {3, 1.0e-323},
+
+ {2, arr_nested(0)}, {22, arr_nested(10)}, {2002, arr_nested(1000)},
+ {9, obj_nested(0)}, {69, obj_nested(10)}, {6009, obj_nested(1000)},
+
+ {4, null}, {4, true}, {5, false},
+
+ {3, str(1, $x)}, {4, str(1, ?CENT)}, {5, str(1, ?EURO)},
+ {6, str(1, ?HWAIR)}, {3, str(1, $\x{1})}, {12, str(10, $x)},
+ {22, str(10, ?CENT)}, {32, str(10, ?EURO)}, {42, str(10, ?HWAIR)},
+ {12, str(10, $\x{1})}
+ ]].
+
+
+%% Helper functions
+
+arr_nested(MaxDepth) ->
+ arr_nested(MaxDepth, 0).
+
+
+obj_nested(MaxDepth) ->
+ obj_nested(MaxDepth, 0).
+
+
+obj(N, K, V) ->
+ {[{K, V} || _ <- lists:seq(1, N)]}.
+
+
+str(N, C) ->
+ unicode:characters_to_binary([C || _ <- lists:seq(1, N)]).
+
+
+arr_nested(MaxDepth, MaxDepth) ->
+ [];
+
+arr_nested(MaxDepth, Depth) ->
+ [arr_nested(MaxDepth, Depth + 1)].
+
+
+obj_nested(MaxDepth, MaxDepth) ->
+ obj(1, <<"k">>, <<"v">>);
+
+obj_nested(MaxDepth, Depth) ->
+ {[{<<"k">>, obj_nested(MaxDepth, Depth + 1)}]}.
diff --git a/src/couch/test/couchdb_attachments_tests.erl b/src/couch/test/couchdb_attachments_tests.erl
index 88f53a189..4536ba6b2 100644
--- a/src/couch/test/couchdb_attachments_tests.erl
+++ b/src/couch/test/couchdb_attachments_tests.erl
@@ -14,6 +14,7 @@
-include_lib("couch/include/couch_eunit.hrl").
-include_lib("couch/include/couch_db.hrl").
+-include_lib("mem3/include/mem3.hrl").
-define(COMPRESSION_LEVEL, 8).
-define(ATT_BIN_NAME, <<"logo.png">>).
@@ -515,6 +516,137 @@ should_create_compressible_att_with_ctype_params({Host, DbName}) ->
end)}.
+compact_after_lowering_attachment_size_limit_test_() ->
+ {
+ "Compact after lowering attachment size limit",
+ {
+ foreach,
+ fun() ->
+ Ctx = test_util:start_couch(),
+ DbName = ?tempdb(),
+ {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]),
+ ok = couch_db:close(Db),
+ {Ctx, DbName}
+ end,
+ fun({Ctx, DbName}) ->
+ config:delete("couchdb", "max_attachment_size"),
+ ok = couch_server:delete(DbName, [?ADMIN_CTX]),
+ test_util:stop_couch(Ctx)
+ end,
+ [
+ fun should_compact_after_lowering_attachment_size_limit/1
+ ]
+ }
+ }.
+
+
+should_compact_after_lowering_attachment_size_limit({_Ctx, DbName}) ->
+ {timeout, ?TIMEOUT_EUNIT, ?_test(begin
+ {ok, Db1} = couch_db:open(DbName, [?ADMIN_CTX]),
+ Doc1 = #doc{id = <<"doc1">>, atts = att(1000)},
+ {ok, _} = couch_db:update_doc(Db1, Doc1, []),
+ couch_db:close(Db1),
+ config:set("couchdb", "max_attachment_size", "1", _Persist = false),
+ compact_db(DbName),
+ {ok, Db2} = couch_db:open_int(DbName, []),
+ {ok, Doc2} = couch_db:open_doc(Db2, <<"doc1">>),
+ couch_db:close(Db2),
+ [Att] = Doc2#doc.atts,
+ ?assertEqual(1000, couch_att:fetch(att_len, Att))
+ end)}.
+
+
+att(Size) when is_integer(Size), Size >= 1 ->
+ [couch_att:new([
+ {name, <<"att">>},
+ {type, <<"app/binary">>},
+ {att_len, Size},
+ {data, fun(_Bytes) ->
+ << <<"x">> || _ <- lists:seq(1, Size) >>
+ end}
+ ])].
+
+
+compact_db(DbName) ->
+ {ok, Db} = couch_db:open_int(DbName, []),
+ {ok, _CompactPid} = couch_db:start_compact(Db),
+ wait_compaction(DbName, "database", ?LINE),
+ ok = couch_db:close(Db).
+
+
+wait_compaction(DbName, Kind, Line) ->
+ WaitFun = fun() ->
+ case is_compaction_running(DbName) of
+ true -> wait;
+ false -> ok
+ end
+ end,
+ case test_util:wait(WaitFun, ?TIMEOUT) of
+ timeout ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, Line},
+ {reason, "Timeout waiting for "
+ ++ Kind
+ ++ " database compaction"}]});
+ _ ->
+ ok
+ end.
+
+
+is_compaction_running(DbName) ->
+ {ok, Db} = couch_db:open_int(DbName, []),
+ {ok, DbInfo} = couch_db:get_db_info(Db),
+ couch_db:close(Db),
+ couch_util:get_value(compact_running, DbInfo) =:= true.
+
+
+internal_replication_after_lowering_attachment_size_limit_test_() ->
+ {
+ "Internal replication after lowering max attachment size",
+ {
+ foreach,
+ fun() ->
+ Ctx = test_util:start_couch([mem3]),
+ SrcName = ?tempdb(),
+ {ok, SrcDb} = couch_db:create(SrcName, [?ADMIN_CTX]),
+ ok = couch_db:close(SrcDb),
+ TgtName = ?tempdb(),
+ {ok, TgtDb} = couch_db:create(TgtName, [?ADMIN_CTX]),
+ ok = couch_db:close(TgtDb),
+ {Ctx, SrcName, TgtName}
+ end,
+ fun({Ctx, SrcName, TgtName}) ->
+ config:delete("couchdb", "max_attachment_size"),
+ ok = couch_server:delete(SrcName, [?ADMIN_CTX]),
+ ok = couch_server:delete(TgtName, [?ADMIN_CTX]),
+ test_util:stop_couch(Ctx)
+ end,
+ [
+ fun should_replicate_after_lowering_attachment_size/1
+ ]
+ }
+ }.
+
+should_replicate_after_lowering_attachment_size({_Ctx, SrcName, TgtName}) ->
+ {timeout, ?TIMEOUT_EUNIT, ?_test(begin
+ {ok, SrcDb} = couch_db:open(SrcName, [?ADMIN_CTX]),
+ SrcDoc = #doc{id = <<"doc">>, atts = att(1000)},
+ {ok, _} = couch_db:update_doc(SrcDb, SrcDoc, []),
+ couch_db:close(SrcDb),
+ config:set("couchdb", "max_attachment_size", "1", _Persist = false),
+ % Create a pair of "fake" shards
+ SrcShard = #shard{name = SrcName, node = node()},
+ TgtShard = #shard{name = TgtName, node = node()},
+ mem3_rep:go(SrcShard, TgtShard, []),
+ {ok, TgtDb} = couch_db:open_int(TgtName, []),
+ {ok, TgtDoc} = couch_db:open_doc(TgtDb, <<"doc">>),
+ couch_db:close(TgtDb),
+ [Att] = TgtDoc#doc.atts,
+ ?assertEqual(1000, couch_att:fetch(att_len, Att))
+ end)}.
+
+
get_json(Json, Path) ->
couch_util:get_nested_json_value(Json, Path).
diff --git a/src/couch_mrview/src/couch_mrview.erl b/src/couch_mrview/src/couch_mrview.erl
index c44dd91f3..11c209b43 100644
--- a/src/couch_mrview/src/couch_mrview.erl
+++ b/src/couch_mrview/src/couch_mrview.erl
@@ -52,13 +52,13 @@ validate_ddoc_fields(DDoc) ->
lists:foreach(fun(Path) ->
validate_ddoc_fields(DDoc, Path)
end, [
- [{<<"filters">>, object}, {any, string}],
+ [{<<"filters">>, object}, {any, [object, string]}],
[{<<"language">>, string}],
- [{<<"lists">>, object}, {any, string}],
+ [{<<"lists">>, object}, {any, [object, string]}],
[{<<"options">>, object}],
[{<<"rewrites">>, [string, array]}],
- [{<<"shows">>, object}, {any, string}],
- [{<<"updates">>, object}, {any, string}],
+ [{<<"shows">>, object}, {any, [object, string]}],
+ [{<<"updates">>, object}, {any, [object, string]}],
[{<<"validate_doc_update">>, string}],
[{<<"views">>, object}, {<<"lib">>, object}],
[{<<"views">>, object}, {any, object}, {<<"map">>, MapFuncType}],
diff --git a/src/couch_mrview/test/couch_mrview_ddoc_validation_tests.erl b/src/couch_mrview/test/couch_mrview_ddoc_validation_tests.erl
index 028e0be11..c2038ddfb 100644
--- a/src/couch_mrview/test/couch_mrview_ddoc_validation_tests.erl
+++ b/src/couch_mrview/test/couch_mrview_ddoc_validation_tests.erl
@@ -15,6 +15,8 @@
-include_lib("couch/include/couch_eunit.hrl").
-include_lib("couch/include/couch_db.hrl").
+-define(LIB, {[{<<"mylib">>, {[{<<"lib1">>, <<"x=42">>}]}}]}).
+
setup() ->
{ok, Db} = couch_mrview_test_util:init_db(?tempdb(), map),
Db.
@@ -39,9 +41,13 @@ ddoc_validation_test_() ->
fun should_reject_invalid_builtin_reduce/1,
fun should_reject_non_object_options/1,
fun should_reject_non_object_filters/1,
+ fun should_accept_obj_in_filters/1,
fun should_reject_non_object_lists/1,
+ fun should_accept_obj_in_lists/1,
fun should_reject_non_object_shows/1,
+ fun should_accept_obj_in_shows/1,
fun should_reject_non_object_updates/1,
+ fun should_accept_obj_in_updates/1,
fun should_reject_non_object_views/1,
fun should_reject_non_string_language/1,
fun should_reject_non_string_validate_doc_update/1,
@@ -50,13 +56,13 @@ ddoc_validation_test_() ->
fun should_accept_option/1,
fun should_accept_any_option/1,
fun should_accept_filter/1,
- fun should_reject_non_string_filter_function/1,
+ fun should_reject_non_string_or_obj_filter_function/1,
fun should_accept_list/1,
- fun should_reject_non_string_list_function/1,
+ fun should_reject_non_string_or_obj_list_function/1,
fun should_accept_show/1,
- fun should_reject_non_string_show_function/1,
+ fun should_reject_non_string_or_obj_show_function/1,
fun should_accept_update/1,
- fun should_reject_non_string_update_function/1,
+ fun should_reject_non_string_or_obj_update_function/1,
fun should_accept_view/1,
fun should_accept_view_with_reduce/1,
fun should_accept_view_with_lib/1,
@@ -129,6 +135,13 @@ should_reject_non_object_filters(Db) ->
?_assertThrow({bad_request, invalid_design_doc, _},
couch_db:update_doc(Db, Doc, [])).
+should_accept_obj_in_filters(Db) ->
+ Doc = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"_design/should_accept_obj_in_filters">>},
+ {<<"filters">>, ?LIB}
+ ]}),
+ ?_assertMatch({ok, _}, couch_db:update_doc(Db, Doc, [])).
+
should_reject_non_object_lists(Db) ->
Doc = couch_doc:from_json_obj({[
{<<"_id">>, <<"_design/should_reject_non_object_lists">>},
@@ -145,6 +158,13 @@ should_reject_non_object_shows(Db) ->
?_assertThrow({bad_request, invalid_design_doc, _},
couch_db:update_doc(Db, Doc, [])).
+should_accept_obj_in_shows(Db) ->
+ Doc = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"_design/should_accept_obj_in_shows">>},
+ {<<"shows">>, ?LIB}
+ ]}),
+ ?_assertMatch({ok, _}, couch_db:update_doc(Db, Doc, [])).
+
should_reject_non_object_updates(Db) ->
Doc = couch_doc:from_json_obj({[
{<<"_id">>, <<"_design/should_reject_non_object_updates">>},
@@ -153,6 +173,13 @@ should_reject_non_object_updates(Db) ->
?_assertThrow({bad_request, invalid_design_doc, _},
couch_db:update_doc(Db, Doc, [])).
+should_accept_obj_in_updates(Db) ->
+ Doc = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"_design/should_accept_obj_in_updates">>},
+ {<<"updates">>, ?LIB}
+ ]}),
+ ?_assertMatch({ok, _}, couch_db:update_doc(Db, Doc, [])).
+
should_reject_non_object_views(Db) ->
Doc = couch_doc:from_json_obj({[
{<<"_id">>, <<"_design/should_reject_non_object_views">>},
@@ -213,9 +240,9 @@ should_accept_filter(Db) ->
]}),
?_assertMatch({ok,_}, couch_db:update_doc(Db, Doc, [])).
-should_reject_non_string_filter_function(Db) ->
+should_reject_non_string_or_obj_filter_function(Db) ->
Doc = couch_doc:from_json_obj({[
- {<<"_id">>, <<"_design/should_reject_non_string_filter_function">>},
+ {<<"_id">>, <<"_design/should_reject_non_string_or_obj_filter_function">>},
{<<"filters">>, {[ {<<"filter1">>, 1} ]}}
]}),
?_assertThrow({bad_request, invalid_design_doc, _},
@@ -228,14 +255,22 @@ should_accept_list(Db) ->
]}),
?_assertMatch({ok,_}, couch_db:update_doc(Db, Doc, [])).
-should_reject_non_string_list_function(Db) ->
+should_reject_non_string_or_obj_list_function(Db) ->
Doc = couch_doc:from_json_obj({[
- {<<"_id">>, <<"_design/should_reject_non_string_list_function">>},
+ {<<"_id">>, <<"_design/should_reject_non_string_or_obj_list_function">>},
{<<"lists">>, {[ {<<"list1">>, 1} ]}}
]}),
?_assertThrow({bad_request, invalid_design_doc, _},
couch_db:update_doc(Db, Doc, [])).
+should_accept_obj_in_lists(Db) ->
+ Doc = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"_design/should_accept_obj_in_lists">>},
+ {<<"lists">>, ?LIB}
+ ]}),
+ ?_assertMatch({ok, _}, couch_db:update_doc(Db, Doc, [])).
+
+
should_accept_show(Db) ->
Doc = couch_doc:from_json_obj({[
{<<"_id">>, <<"_design/should_accept_shows">>},
@@ -243,9 +278,9 @@ should_accept_show(Db) ->
]}),
?_assertMatch({ok,_}, couch_db:update_doc(Db, Doc, [])).
-should_reject_non_string_show_function(Db) ->
+should_reject_non_string_or_obj_show_function(Db) ->
Doc = couch_doc:from_json_obj({[
- {<<"_id">>, <<"_design/should_reject_non_string_show_function">>},
+ {<<"_id">>, <<"_design/should_reject_non_string_or_obj_show_function">>},
{<<"shows">>, {[ {<<"show1">>, 1} ]}}
]}),
?_assertThrow({bad_request, invalid_design_doc, _},
@@ -258,9 +293,9 @@ should_accept_update(Db) ->
]}),
?_assertMatch({ok,_}, couch_db:update_doc(Db, Doc, [])).
-should_reject_non_string_update_function(Db) ->
+should_reject_non_string_or_obj_update_function(Db) ->
Doc = couch_doc:from_json_obj({[
- {<<"_id">>, <<"_design/should_reject_non_string_update_function">>},
+ {<<"_id">>, <<"_design/should_reject_non_string_or_obj_update_function">>},
{<<"updates">>, {[ {<<"update1">>, 1} ]}}
]}),
?_assertThrow({bad_request, invalid_design_doc, _},
diff --git a/src/couch_replicator/src/couch_replicator_api_wrap.erl b/src/couch_replicator/src/couch_replicator_api_wrap.erl
index 91d7d7ae5..a2ef60fa3 100644
--- a/src/couch_replicator/src/couch_replicator_api_wrap.erl
+++ b/src/couch_replicator/src/couch_replicator_api_wrap.erl
@@ -711,10 +711,12 @@ receive_docs(Streamer, UserFun, Ref, UserAcc) ->
{headers, Ref, Headers} ->
case header_value("content-type", Headers) of
{"multipart/related", _} = ContentType ->
+ % Skip document body and attachment size limits validation here
+ % since these should be validated by the replication target
case couch_doc:doc_from_multi_part_stream(
ContentType,
fun() -> receive_doc_data(Streamer, Ref) end,
- Ref) of
+ Ref, _ValidateDocLimits = false) of
{ok, Doc, WaitFun, Parser} ->
case run_user_fun(UserFun, {ok, Doc}, UserAcc, Ref) of
{ok, UserAcc2} ->
diff --git a/src/couch_replicator/src/couch_replicator_ids.erl b/src/couch_replicator/src/couch_replicator_ids.erl
index 62cfdf267..e7067622b 100644
--- a/src/couch_replicator/src/couch_replicator_ids.erl
+++ b/src/couch_replicator/src/couch_replicator_ids.erl
@@ -78,7 +78,11 @@ replication_id(#rep{user_ctx = UserCtx} = Rep, 1) ->
-spec convert([_] | binary() | {string(), string()}) -> {string(), string()}.
convert(Id) when is_list(Id) ->
convert(?l2b(Id));
-convert(Id) when is_binary(Id) ->
+convert(Id0) when is_binary(Id0) ->
+ % Spaces can result from mochiweb incorrectly unquoting + characters from
+ % the URL path. So undo the incorrect parsing here to avoid forcing
+ % users to url encode + characters.
+ Id = binary:replace(Id0, <<" ">>, <<"+">>, [global]),
lists:splitwith(fun(Char) -> Char =/= $+ end, ?b2l(Id));
convert({BaseId, Ext} = Id) when is_list(BaseId), is_list(Ext) ->
Id.
@@ -222,6 +226,16 @@ get_non_default_port(_Schema, Port) ->
-include_lib("eunit/include/eunit.hrl").
+
+replication_id_convert_test_() ->
+ [?_assertEqual(Expected, convert(Id)) || {Expected, Id} <- [
+ {{"abc", ""}, "abc"},
+ {{"abc", ""}, <<"abc">>},
+ {{"abc", "+x+y"}, <<"abc+x+y">>},
+ {{"abc", "+x+y"}, {"abc", "+x+y"}},
+ {{"abc", "+x+y"}, <<"abc x y">>}
+ ]].
+
http_v4_endpoint_test_() ->
[?_assertMatch({remote, User, Host, Port, Path, HeadersNoAuth, undefined},
get_v4_endpoint(nil, #httpdb{url = Url, headers = Headers})) ||
diff --git a/src/couch_replicator/test/couch_replicator_attachments_too_large.erl b/src/couch_replicator/test/couch_replicator_attachments_too_large.erl
new file mode 100644
index 000000000..7fe84d2d9
--- /dev/null
+++ b/src/couch_replicator/test/couch_replicator_attachments_too_large.erl
@@ -0,0 +1,104 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_replicator_attachments_too_large).
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch_replicator/src/couch_replicator.hrl").
+
+
+setup(_) ->
+ Ctx = test_util:start_couch([couch_replicator]),
+ Source = create_db(),
+ create_doc_with_attachment(Source, <<"doc">>, 1000),
+ Target = create_db(),
+ {Ctx, {Source, Target}}.
+
+
+teardown(_, {Ctx, {Source, Target}}) ->
+ delete_db(Source),
+ delete_db(Target),
+ config:delete("couchdb", "max_attachment_size"),
+ ok = test_util:stop_couch(Ctx).
+
+
+attachment_too_large_replication_test_() ->
+ Pairs = [{local, remote}, {remote, local}, {remote, remote}],
+ {
+ "Attachment size too large replication tests",
+ {
+ foreachx,
+ fun setup/1, fun teardown/2,
+ [{Pair, fun should_succeed/2} || Pair <- Pairs] ++
+ [{Pair, fun should_fail/2} || Pair <- Pairs]
+ }
+ }.
+
+
+should_succeed({From, To}, {_Ctx, {Source, Target}}) ->
+ RepObject = {[
+ {<<"source">>, db_url(From, Source)},
+ {<<"target">>, db_url(To, Target)}
+ ]},
+ config:set("couchdb", "max_attachment_size", "1000", _Persist = false),
+ {ok, _} = couch_replicator:replicate(RepObject, ?ADMIN_USER),
+ ?_assertEqual(ok, couch_replicator_test_helper:compare_dbs(Source, Target)).
+
+
+should_fail({From, To}, {_Ctx, {Source, Target}}) ->
+ RepObject = {[
+ {<<"source">>, db_url(From, Source)},
+ {<<"target">>, db_url(To, Target)}
+ ]},
+ config:set("couchdb", "max_attachment_size", "999", _Persist = false),
+ {ok, _} = couch_replicator:replicate(RepObject, ?ADMIN_USER),
+ ?_assertError({badmatch, {not_found, missing}},
+ couch_replicator_test_helper:compare_dbs(Source, Target)).
+
+
+create_db() ->
+ DbName = ?tempdb(),
+ {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]),
+ ok = couch_db:close(Db),
+ DbName.
+
+
+create_doc_with_attachment(DbName, DocId, AttSize) ->
+ {ok, Db} = couch_db:open(DbName, [?ADMIN_CTX]),
+ Doc = #doc{id = DocId, atts = att(AttSize)},
+ {ok, _} = couch_db:update_doc(Db, Doc, []),
+ couch_db:close(Db),
+ ok.
+
+
+att(Size) when is_integer(Size), Size >= 1 ->
+ [couch_att:new([
+ {name, <<"att">>},
+ {type, <<"app/binary">>},
+ {att_len, Size},
+ {data, fun(_Bytes) ->
+ << <<"x">> || _ <- lists:seq(1, Size) >>
+ end}
+ ])].
+
+
+delete_db(DbName) ->
+ ok = couch_server:delete(DbName, [?ADMIN_CTX]).
+
+
+db_url(local, DbName) ->
+ DbName;
+db_url(remote, DbName) ->
+ Addr = config:get("httpd", "bind_address", "127.0.0.1"),
+ Port = mochiweb_socket_server:get(couch_httpd, port),
+ ?l2b(io_lib:format("http://~s:~b/~s", [Addr, Port, DbName])).
diff --git a/src/fabric/src/fabric_doc_attachments.erl b/src/fabric/src/fabric_doc_attachments.erl
index 8b8123fa9..47854d189 100644
--- a/src/fabric/src/fabric_doc_attachments.erl
+++ b/src/fabric/src/fabric_doc_attachments.erl
@@ -24,8 +24,8 @@ receiver(_Req, {unknown_transfer_encoding, Unknown}) ->
exit({unknown_transfer_encoding, Unknown});
receiver(Req, chunked) ->
MiddleMan = spawn(fun() -> middleman(Req, chunked) end),
- fun(4096, ChunkFun, ok) ->
- write_chunks(MiddleMan, ChunkFun)
+ fun(4096, ChunkFun, State) ->
+ write_chunks(MiddleMan, ChunkFun, State)
end;
receiver(_Req, 0) ->
<<"">>;
@@ -63,27 +63,29 @@ maybe_send_continue(#httpd{mochi_req = MochiReq} = Req) ->
end
end.
-write_chunks(MiddleMan, ChunkFun) ->
+write_chunks(MiddleMan, ChunkFun, State) ->
MiddleMan ! {self(), gimme_data},
Timeout = fabric_util:attachments_timeout(),
receive
{MiddleMan, ChunkRecordList} ->
rexi:reply(attachment_chunk_received),
- case flush_chunks(ChunkRecordList, ChunkFun) of
- continue -> write_chunks(MiddleMan, ChunkFun);
- done -> ok
+ case flush_chunks(ChunkRecordList, ChunkFun, State) of
+ {continue, NewState} ->
+ write_chunks(MiddleMan, ChunkFun, NewState);
+ {done, NewState} ->
+ NewState
end
after Timeout ->
exit(timeout)
end.
-flush_chunks([], _ChunkFun) ->
- continue;
-flush_chunks([{0, _}], _ChunkFun) ->
- done;
-flush_chunks([Chunk | Rest], ChunkFun) ->
- ChunkFun(Chunk, ok),
- flush_chunks(Rest, ChunkFun).
+flush_chunks([], _ChunkFun, State) ->
+ {continue, State};
+flush_chunks([{0, _}], _ChunkFun, State) ->
+ {done, State};
+flush_chunks([Chunk | Rest], ChunkFun, State) ->
+ NewState = ChunkFun(Chunk, State),
+ flush_chunks(Rest, ChunkFun, NewState).
receive_unchunked_attachment(_Req, 0) ->
ok;
diff --git a/src/fabric/src/fabric_doc_update.erl b/src/fabric/src/fabric_doc_update.erl
index 10e254ff5..214566d91 100644
--- a/src/fabric/src/fabric_doc_update.erl
+++ b/src/fabric/src/fabric_doc_update.erl
@@ -96,7 +96,9 @@ handle_message({not_found, no_db_file} = X, Worker, Acc0) ->
Docs = couch_util:get_value(Worker, GroupedDocs),
handle_message({ok, [X || _D <- Docs]}, Worker, Acc0);
handle_message({bad_request, Msg}, _, _) ->
- throw({bad_request, Msg}).
+ throw({bad_request, Msg});
+handle_message({request_entity_too_large, Entity}, _, _) ->
+ throw({request_entity_too_large, Entity}).
before_doc_update(DbName, Docs, Opts) ->
case {fabric_util:is_replicator_db(DbName), fabric_util:is_users_db(DbName)} of
diff --git a/src/mango/src/mango_cursor.erl b/src/mango/src/mango_cursor.erl
index cf7179079..f36febdfc 100644
--- a/src/mango/src/mango_cursor.erl
+++ b/src/mango/src/mango_cursor.erl
@@ -17,13 +17,15 @@
create/3,
explain/1,
execute/3,
- maybe_filter_indexes/2
+ maybe_filter_indexes_by_ddoc/2,
+ maybe_add_warning/3
]).
-include_lib("couch/include/couch_db.hrl").
-include("mango.hrl").
-include("mango_cursor.hrl").
+-include("mango_idx.hrl").
-ifdef(HAVE_DREYFUS).
@@ -85,10 +87,12 @@ execute(#cursor{index=Idx}=Cursor, UserFun, UserAcc) ->
Mod:execute(Cursor, UserFun, UserAcc).
-maybe_filter_indexes(Indexes, Opts) ->
+maybe_filter_indexes_by_ddoc(Indexes, Opts) ->
case lists:keyfind(use_index, 1, Opts) of
{use_index, []} ->
- Indexes;
+ %We remove any indexes that have a selector
+ % since they are only used when specified via use_index
+ remove_indexes_with_selector(Indexes);
{use_index, [DesignId]} ->
filter_indexes(Indexes, DesignId);
{use_index, [DesignId, ViewName]} ->
@@ -113,6 +117,16 @@ filter_indexes(Indexes0, DesignId, ViewName) ->
lists:filter(FiltFun, Indexes).
+remove_indexes_with_selector(Indexes) ->
+ FiltFun = fun(Idx) ->
+ case mango_idx:get_idx_selector(Idx) of
+ undefined -> true;
+ _ -> false
+ end
+ end,
+ lists:filter(FiltFun, Indexes).
+
+
create_cursor(Db, Indexes, Selector, Opts) ->
[{CursorMod, CursorModIndexes} | _] = group_indexes_by_type(Indexes),
CursorMod:create(Db, CursorModIndexes, Selector, Opts).
@@ -134,3 +148,14 @@ group_indexes_by_type(Indexes) ->
[]
end
end, ?CURSOR_MODULES).
+
+
+maybe_add_warning(UserFun, #idx{type = IndexType}, UserAcc) ->
+ case IndexType of
+ <<"special">> ->
+ Arg = {add_key, warning, <<"no matching index found, create an index to optimize query time">>},
+ {_Go, UserAcc0} = UserFun(Arg, UserAcc),
+ UserAcc0;
+ _ ->
+ UserAcc
+ end. \ No newline at end of file
diff --git a/src/mango/src/mango_cursor_text.erl b/src/mango/src/mango_cursor_text.erl
index ea62cd68c..70c911ac1 100644
--- a/src/mango/src/mango_cursor_text.erl
+++ b/src/mango/src/mango_cursor_text.erl
@@ -124,7 +124,8 @@ execute(Cursor, UserFun, UserAcc) ->
Arg = {add_key, bookmark, JsonBM},
{_Go, FinalUserAcc} = UserFun(Arg, LastUserAcc),
FinalUserAcc0 = mango_execution_stats:maybe_add_stats(Opts, UserFun, Stats0, FinalUserAcc),
- {ok, FinalUserAcc0}
+ FinalUserAcc1 = mango_cursor:maybe_add_warning(UserFun, Idx, FinalUserAcc0),
+ {ok, FinalUserAcc1}
end.
diff --git a/src/mango/src/mango_cursor_view.erl b/src/mango/src/mango_cursor_view.erl
index b292704c3..31e198fca 100644
--- a/src/mango/src/mango_cursor_view.erl
+++ b/src/mango/src/mango_cursor_view.erl
@@ -118,7 +118,8 @@ execute(#cursor{db = Db, index = Idx, execution_stats = Stats} = Cursor0, UserFu
{_Go, FinalUserAcc} = UserFun(Arg, LastCursor#cursor.user_acc),
Stats0 = LastCursor#cursor.execution_stats,
FinalUserAcc0 = mango_execution_stats:maybe_add_stats(Opts, UserFun, Stats0, FinalUserAcc),
- {ok, FinalUserAcc0};
+ FinalUserAcc1 = mango_cursor:maybe_add_warning(UserFun, Idx, FinalUserAcc0),
+ {ok, FinalUserAcc1};
{error, Reason} ->
{error, Reason}
end
diff --git a/src/mango/src/mango_error.erl b/src/mango/src/mango_error.erl
index 361016524..4f8ae204d 100644
--- a/src/mango/src/mango_error.erl
+++ b/src/mango/src/mango_error.erl
@@ -21,19 +21,19 @@
]).
-info(mango_cursor, {no_usable_index, no_indexes_defined}) ->
+info(mango_idx, {no_usable_index, no_indexes_defined}) ->
{
400,
<<"no_usable_index">>,
<<"There are no indexes defined in this database.">>
};
-info(mango_cursor, {no_usable_index, no_index_matching_name}) ->
+info(mango_idx, {no_usable_index, no_index_matching_name}) ->
{
400,
<<"no_usable_index">>,
<<"No index matches the index specified with \"use_index\"">>
};
-info(mango_cursor, {no_usable_index, missing_sort_index}) ->
+info(mango_idx, {no_usable_index, missing_sort_index}) ->
{
400,
<<"no_usable_index">>,
diff --git a/src/mango/src/mango_httpd.erl b/src/mango/src/mango_httpd.erl
index a99b0544d..d3ebf48c9 100644
--- a/src/mango/src/mango_httpd.erl
+++ b/src/mango/src/mango_httpd.erl
@@ -38,13 +38,13 @@ handle_req(#httpd{} = Req, Db0) ->
handle_req_int(Req, Db)
catch
throw:{mango_error, Module, Reason} ->
- %Stack = erlang:get_stacktrace(),
- {Code, ErrorStr, ReasonStr} = mango_error:info(Module, Reason),
- Resp = {[
- {<<"error">>, ErrorStr},
- {<<"reason">>, ReasonStr}
- ]},
- chttpd:send_json(Req, Code, Resp)
+ case mango_error:info(Module, Reason) of
+ {500, ErrorStr, ReasonStr} ->
+ Stack = erlang:get_stacktrace(),
+ chttpd:send_error(Req, 500, [], ErrorStr, ReasonStr, Stack);
+ {Code, ErrorStr, ReasonStr} ->
+ chttpd:send_error(Req, Code, ErrorStr, ReasonStr)
+ end
end.
@@ -183,7 +183,7 @@ handle_find_req(#httpd{method='POST'}=Req, Db) ->
chttpd:validate_ctype(Req, "application/json"),
{ok, Opts0} = mango_opts:validate_find(chttpd:json_body_obj(Req)),
{value, {selector, Sel}, Opts} = lists:keytake(selector, 1, Opts0),
- {ok, Resp0} = start_find_resp(Req, Db, Sel, Opts),
+ {ok, Resp0} = start_find_resp(Req),
{ok, AccOut} = run_find(Resp0, Db, Sel, Opts),
end_find_resp(AccOut);
@@ -230,18 +230,8 @@ convert_to_design_id(DDocId) ->
end.
-start_find_resp(Req, Db, Sel, Opts) ->
- chttpd:start_delayed_json_response(Req, 200, [], maybe_add_warning(Db, Sel, Opts)).
-
-
-maybe_add_warning(Db, Selector, Opts) ->
- UsableIndexes = mango_idx:get_usable_indexes(Db, Selector, Opts),
- case length(UsableIndexes) of
- 0 ->
- "{\"warning\":\"no matching index found, create an index to optimize query time\",\r\n\"docs\":[";
- _ ->
- "{\"docs\":["
- end.
+start_find_resp(Req) ->
+ chttpd:start_delayed_json_response(Req, 200, [], "{\"docs\":[").
end_find_resp(Acc0) ->
diff --git a/src/mango/src/mango_idx.erl b/src/mango/src/mango_idx.erl
index c330702b1..b8122517d 100644
--- a/src/mango/src/mango_idx.erl
+++ b/src/mango/src/mango_idx.erl
@@ -43,7 +43,8 @@
idx_mod/1,
to_json/1,
delete/4,
- get_usable_indexes/3
+ get_usable_indexes/3,
+ get_idx_selector/1
]).
@@ -64,7 +65,7 @@ get_usable_indexes(Db, Selector0, Opts) ->
?MANGO_ERROR({no_usable_index, no_indexes_defined})
end,
- FilteredIndexes = mango_cursor:maybe_filter_indexes(ExistingIndexes, Opts),
+ FilteredIndexes = mango_cursor:maybe_filter_indexes_by_ddoc(ExistingIndexes, Opts),
if FilteredIndexes /= [] -> ok; true ->
?MANGO_ERROR({no_usable_index, no_index_matching_name})
end,
@@ -367,3 +368,13 @@ filter_opts([Opt | Rest]) ->
[Opt | filter_opts(Rest)].
+get_idx_selector(#idx{def = Def}) when Def =:= all_docs; Def =:= undefined ->
+ undefined;
+get_idx_selector(#idx{def = {Def}}) ->
+ case proplists:get_value(<<"selector">>, Def) of
+ undefined -> undefined;
+ {[]} -> undefined;
+ Selector -> Selector
+ end.
+
+
diff --git a/src/mango/src/mango_idx_view.erl b/src/mango/src/mango_idx_view.erl
index 8bad34cca..d5dcd0c07 100644
--- a/src/mango/src/mango_idx_view.erl
+++ b/src/mango/src/mango_idx_view.erl
@@ -197,6 +197,12 @@ opts() ->
{<<"fields">>, [
{tag, fields},
{validator, fun mango_opts:validate_sort/1}
+ ]},
+ {<<"selector">>, [
+ {tag, selector},
+ {optional, true},
+ {default, {[]}},
+ {validator, fun mango_opts:validate_selector/1}
]}
].
diff --git a/src/mango/src/mango_native_proc.erl b/src/mango/src/mango_native_proc.erl
index ba17c4867..82081a976 100644
--- a/src/mango/src/mango_native_proc.erl
+++ b/src/mango/src/mango_native_proc.erl
@@ -135,26 +135,31 @@ index_doc(#st{indexes=Indexes}, Doc) ->
get_index_entries({IdxProps}, Doc) ->
{Fields} = couch_util:get_value(<<"fields">>, IdxProps),
- Values = lists:map(fun({Field, _Dir}) ->
+ Selector = get_index_selector(IdxProps),
+ case should_index(Selector, Doc) of
+ false ->
+ [];
+ true ->
+ Values = get_index_values(Fields, Doc),
+ case lists:member(not_found, Values) of
+ true -> [];
+ false -> [[Values, null]]
+ end
+ end.
+
+
+get_index_values(Fields, Doc) ->
+ lists:map(fun({Field, _Dir}) ->
case mango_doc:get_field(Doc, Field) of
not_found -> not_found;
bad_path -> not_found;
- Else -> Else
+ Value -> Value
end
- end, Fields),
- case lists:member(not_found, Values) of
- true ->
- [];
- false ->
- [[Values, null]]
- end.
+ end, Fields).
get_text_entries({IdxProps}, Doc) ->
- Selector = case couch_util:get_value(<<"selector">>, IdxProps) of
- [] -> {[]};
- Else -> Else
- end,
+ Selector = get_index_selector(IdxProps),
case should_index(Selector, Doc) of
true ->
get_text_entries0(IdxProps, Doc);
@@ -163,6 +168,13 @@ get_text_entries({IdxProps}, Doc) ->
end.
+get_index_selector(IdxProps) ->
+ case couch_util:get_value(<<"selector">>, IdxProps) of
+ [] -> {[]};
+ Else -> Else
+ end.
+
+
get_text_entries0(IdxProps, Doc) ->
DefaultEnabled = get_default_enabled(IdxProps),
IndexArrayLengths = get_index_array_lengths(IdxProps),
diff --git a/src/mango/test/03-operator-test.py b/src/mango/test/03-operator-test.py
index 76e00ccbe..863752682 100644
--- a/src/mango/test/03-operator-test.py
+++ b/src/mango/test/03-operator-test.py
@@ -11,15 +11,15 @@
# the License.
import mango
+import unittest
-
-class OperatorTests(mango.UserDocsTests):
+class OperatorTests:
def assertUserIds(self, user_ids, docs):
user_ids_returned = list(d["user_id"] for d in docs)
user_ids.sort()
user_ids_returned.sort()
- self.assertEqual(user_ids_returned, user_ids)
+ self.assertEqual(user_ids, user_ids_returned)
def test_all(self):
docs = self.db.find({
@@ -174,7 +174,16 @@ class OperatorTests(mango.UserDocsTests):
for d in docs:
self.assertIn("twitter", d)
- def test_range_includes_null_but_not_missing(self):
+ def test_lt_includes_null_but_not_missing(self):
+ docs = self.db.find({
+ "twitter": {"$lt": 1}
+ })
+ user_ids = [9]
+ self.assertUserIds(user_ids, docs)
+ for d in docs:
+ self.assertEqual(d["twitter"], None)
+
+ def test_lte_includes_null_but_not_missing(self):
docs = self.db.find({
"twitter": {"$lt": 1}
})
@@ -183,6 +192,27 @@ class OperatorTests(mango.UserDocsTests):
for d in docs:
self.assertEqual(d["twitter"], None)
+ def test_lte_null_includes_null_but_not_missing(self):
+ docs = self.db.find({
+ "twitter": {"$lte": None}
+ })
+ user_ids = [9]
+ self.assertUserIds(user_ids, docs)
+ for d in docs:
+ self.assertEqual(d["twitter"], None)
+
+ def test_lte_at_z_except_null_excludes_null_and_missing(self):
+ docs = self.db.find({
+ "twitter": {"$and": [
+ {"$lte": "@z"},
+ {"$ne": None}
+ ]}
+ })
+ user_ids = [0,1,4,13]
+ self.assertUserIds(user_ids, docs)
+ for d in docs:
+ self.assertNotEqual(d["twitter"], None)
+
def test_range_gte_null_includes_null_but_not_missing(self):
docs = self.db.find({
"twitter": {"$gte": None}
@@ -198,3 +228,37 @@ class OperatorTests(mango.UserDocsTests):
self.assertGreater(len(docs), 0)
for d in docs:
self.assertNotIn("twitter", d)
+
+ @unittest.skipUnless(not mango.has_text_service(),
+ "text indexes do not support range queries across type boundaries")
+ def test_lte_respsects_unicode_collation(self):
+ docs = self.db.find({
+ "ordered": {"$lte": "a"}
+ })
+ user_ids = [7,8,9,10,11,12]
+ self.assertUserIds(user_ids, docs)
+
+ @unittest.skipUnless(not mango.has_text_service(),
+ "text indexes do not support range queries across type boundaries")
+ def test_gte_respsects_unicode_collation(self):
+ docs = self.db.find({
+ "ordered": {"$gte": "a"}
+ })
+ user_ids = [12,13,14]
+ self.assertUserIds(user_ids, docs)
+
+
+
+class OperatorJSONTests(mango.UserDocsTests, OperatorTests):
+ pass
+
+
+@unittest.skipUnless(mango.has_text_service(), "requires text service")
+class OperatorTextTests(mango.UserDocsTextTests, OperatorTests):
+ pass
+
+
+class OperatorAllDocsTests(mango.UserDocsTestsNoIndexes, OperatorTests):
+ pass
+
+
diff --git a/src/mango/test/05-index-selection-test.py b/src/mango/test/05-index-selection-test.py
index bbd3aa7f2..2fb0a405b 100644
--- a/src/mango/test/05-index-selection-test.py
+++ b/src/mango/test/05-index-selection-test.py
@@ -24,14 +24,14 @@ class IndexSelectionTests(mango.UserDocsTests):
def test_basic(self):
resp = self.db.find({"name.last": "A last name"}, explain=True)
- assert resp["index"]["type"] == "json"
+ self.assertEqual(resp["index"]["type"], "json")
def test_with_and(self):
resp = self.db.find({
"name.first": "Stephanie",
"name.last": "This doesn't have to match anything."
}, explain=True)
- assert resp["index"]["type"] == "json"
+ self.assertEqual(resp["index"]["type"], "json")
@unittest.skipUnless(mango.has_text_service(), "requires text service")
def test_with_text(self):
@@ -40,12 +40,12 @@ class IndexSelectionTests(mango.UserDocsTests):
"name.first": "Stephanie",
"name.last": "This doesn't have to match anything."
}, explain=True)
- assert resp["index"]["type"] == "text"
+ self.assertEqual(resp["index"]["type"], "text")
@unittest.skipUnless(mango.has_text_service(), "requires text service")
def test_no_view_index(self):
resp = self.db.find({"name.first": "Ohai!"}, explain=True)
- assert resp["index"]["type"] == "text"
+ self.assertEqual(resp["index"]["type"], "text")
@unittest.skipUnless(mango.has_text_service(), "requires text service")
def test_with_or(self):
@@ -55,7 +55,7 @@ class IndexSelectionTests(mango.UserDocsTests):
{"name.last": "This doesn't have to match anything."}
]
}, explain=True)
- assert resp["index"]["type"] == "text"
+ self.assertEqual(resp["index"]["type"], "text")
def test_use_most_columns(self):
# ddoc id for the age index
@@ -65,22 +65,30 @@ class IndexSelectionTests(mango.UserDocsTests):
"name.last": "Something or other",
"age": {"$gt": 1}
}, explain=True)
- assert resp["index"]["ddoc"] != "_design/" + ddocid
+ self.assertNotEqual(resp["index"]["ddoc"], "_design/" + ddocid)
resp = self.db.find({
"name.first": "Stephanie",
"name.last": "Something or other",
"age": {"$gt": 1}
}, use_index=ddocid, explain=True)
- assert resp["index"]["ddoc"] == ddocid
+ self.assertEqual(resp["index"]["ddoc"], ddocid)
- def test_use_most_columns(self):
+ def test_no_valid_sort_index(self):
+ try:
+ self.db.find({"_id": {"$gt": None}}, sort=["name"], return_raw=True)
+ except Exception, e:
+ self.assertEqual(e.response.status_code, 400)
+ else:
+ raise AssertionError("bad find")
+
+ def test_invalid_use_index(self):
# ddoc id for the age index
ddocid = "_design/ad3d537c03cd7c6a43cf8dff66ef70ea54c2b40f"
try:
self.db.find({}, use_index=ddocid)
except Exception, e:
- assert e.response.status_code == 400
+ self.assertEqual(e.response.status_code, 400)
else:
raise AssertionError("bad find")
@@ -149,9 +157,9 @@ class IndexSelectionTests(mango.UserDocsTests):
}
self.db.save_doc(design_doc)
docs= self.db.find({"age" : 48})
- assert len(docs) == 1
- assert docs[0]["name"]["first"] == "Stephanie"
- assert docs[0]["age"] == 48
+ self.assertEqual(len(docs), 1)
+ self.assertEqual(docs[0]["name"]["first"], "Stephanie")
+ self.assertEqual(docs[0]["age"], 48)
@unittest.skipUnless(mango.has_text_service(), "requires text service")
@@ -165,14 +173,14 @@ class MultiTextIndexSelectionTests(mango.UserDocsTests):
def test_view_ok_with_multi_text(self):
resp = self.db.find({"name.last": "A last name"}, explain=True)
- assert resp["index"]["type"] == "json"
+ self.assertEqual(resp["index"]["type"], "json")
def test_multi_text_index_is_error(self):
try:
self.db.find({"$text": "a query"}, explain=True)
except Exception, e:
- assert e.response.status_code == 400
+ self.assertEqual(e.response.status_code, 400)
def test_use_index_works(self):
resp = self.db.find({"$text": "a query"}, use_index="foo", explain=True)
- assert resp["index"]["ddoc"] == "_design/foo"
+ self.assertEqual(resp["index"]["ddoc"], "_design/foo")
diff --git a/src/mango/test/12-use-correct-index.py b/src/mango/test/12-use-correct-index.py
index f1eaf5fe8..84b425343 100644
--- a/src/mango/test/12-use-correct-index.py
+++ b/src/mango/test/12-use-correct-index.py
@@ -58,35 +58,42 @@ class ChooseCorrectIndexForDocs(mango.DbPerClass):
self.db.create_index(["name", "age", "user_id"], ddoc="aaa")
self.db.create_index(["name"], ddoc="zzz")
explain = self.db.find({"name": "Eddie"}, explain=True)
- assert explain["index"]["ddoc"] == '_design/zzz'
+ self.assertEqual(explain["index"]["ddoc"], '_design/zzz')
def test_choose_index_with_two(self):
self.db.create_index(["name", "age", "user_id"], ddoc="aaa")
self.db.create_index(["name", "age"], ddoc="bbb")
self.db.create_index(["name"], ddoc="zzz")
explain = self.db.find({"name": "Eddie", "age":{"$gte": 12}}, explain=True)
- assert explain["index"]["ddoc"] == '_design/bbb'
+ self.assertEqual(explain["index"]["ddoc"], '_design/bbb')
def test_choose_index_alphabetically(self):
self.db.create_index(["name", "age", "user_id"], ddoc="aaa")
self.db.create_index(["name", "age", "location"], ddoc="bbb")
self.db.create_index(["name"], ddoc="zzz")
explain = self.db.find({"name": "Eddie", "age": {"$gte": 12}}, explain=True)
- assert explain["index"]["ddoc"] == '_design/aaa'
+ self.assertEqual(explain["index"]["ddoc"], '_design/aaa')
def test_choose_index_most_accurate(self):
self.db.create_index(["name", "location", "user_id"], ddoc="aaa")
self.db.create_index(["name", "age", "user_id"], ddoc="bbb")
self.db.create_index(["name"], ddoc="zzz")
explain = self.db.find({"name": "Eddie", "age": {"$gte": 12}}, explain=True)
- assert explain["index"]["ddoc"] == '_design/bbb'
+ self.assertEqual(explain["index"]["ddoc"], '_design/bbb')
def test_choose_index_most_accurate_in_memory_selector(self):
self.db.create_index(["name", "location", "user_id"], ddoc="aaa")
self.db.create_index(["name", "age", "user_id"], ddoc="bbb")
self.db.create_index(["name"], ddoc="zzz")
explain = self.db.find({"name": "Eddie", "number": {"$lte": 12}}, explain=True)
- assert explain["index"]["ddoc"] == '_design/zzz'
+ self.assertEqual(explain["index"]["ddoc"], '_design/zzz')
+
+ def test_warn_on_full_db_scan(self):
+ selector = {"not_indexed":"foo"}
+ explain_resp = self.db.find(selector, explain=True, return_raw=True)
+ self.assertEqual(explain_resp["index"]["type"], "special")
+ resp = self.db.find(selector, return_raw=True)
+ self.assertEqual(resp["warning"], "no matching index found, create an index to optimize query time")
def test_chooses_idxA(self):
DOCS2 = [
@@ -97,4 +104,4 @@ class ChooseCorrectIndexForDocs(mango.DbPerClass):
self.db.create_index(["a", "b", "c"])
self.db.create_index(["a", "d", "e"])
explain = self.db.find({"a": {"$gt": 0}, "b": {"$gt": 0}, "c": {"$gt": 0}}, explain=True)
- assert explain["index"]["def"]["fields"] == [{'a': 'asc'}, {'b': 'asc'}, {'c': 'asc'}]
+ self.assertEqual(explain["index"]["def"]["fields"], [{'a': 'asc'}, {'b': 'asc'}, {'c': 'asc'}])
diff --git a/src/mango/test/16-index-selectors.py b/src/mango/test/16-index-selectors.py
new file mode 100644
index 000000000..b18945609
--- /dev/null
+++ b/src/mango/test/16-index-selectors.py
@@ -0,0 +1,156 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import copy
+import mango
+import unittest
+
+DOCS = [
+ {
+ "_id": "100",
+ "name": "Jimi",
+ "location": "AUS",
+ "user_id": 1,
+ "same": "value"
+ },
+ {
+ "_id": "200",
+ "name": "Eddie",
+ "location": "BRA",
+ "user_id": 2,
+ "same": "value"
+ },
+ {
+ "_id": "300",
+ "name": "Harry",
+ "location": "CAN",
+ "user_id":3,
+ "same": "value"
+ },
+ {
+ "_id": "400",
+ "name": "Eddie",
+ "location": "DEN",
+ "user_id":4,
+ "same": "value"
+ },
+ {
+ "_id": "500",
+ "name": "Jones",
+ "location": "ETH",
+ "user_id":5,
+ "same": "value"
+ },
+ {
+ "_id": "600",
+ "name": "Winnifried",
+ "location": "FRA",
+ "user_id":6,
+ "same": "value"
+ },
+ {
+ "_id": "700",
+ "name": "Marilyn",
+ "location": "GHA",
+ "user_id":7,
+ "same": "value"
+ },
+ {
+ "_id": "800",
+ "name": "Sandra",
+ "location": "ZAR",
+ "user_id":8,
+ "same": "value"
+ },
+]
+
+class IndexSelectorJson(mango.DbPerClass):
+ def setUp(self):
+ self.db.recreate()
+ self.db.save_docs(copy.deepcopy(DOCS))
+
+ def test_saves_selector_in_index(self):
+ selector = {"location": {"$gte": "FRA"}}
+ self.db.create_index(["location"], selector=selector)
+ indexes = self.db.list_indexes()
+ self.assertEqual(indexes[1]["def"]["selector"], selector)
+
+ def test_uses_partial_index_for_query_selector(self):
+ selector = {"location": {"$gte": "FRA"}}
+ self.db.create_index(["location"], selector=selector, ddoc="Selected", name="Selected")
+ resp = self.db.find(selector, explain=True, use_index='Selected')
+ self.assertEqual(resp["index"]["name"], "Selected")
+ docs = self.db.find(selector, use_index='Selected')
+ self.assertEqual(len(docs), 3)
+
+ def test_uses_partial_index_with_different_selector(self):
+ selector = {"location": {"$gte": "FRA"}}
+ selector2 = {"location": {"$gte": "A"}}
+ self.db.create_index(["location"], selector=selector, ddoc="Selected", name="Selected")
+ resp = self.db.find(selector2, explain=True, use_index='Selected')
+ self.assertEqual(resp["index"]["name"], "Selected")
+ docs = self.db.find(selector2, use_index='Selected')
+ self.assertEqual(len(docs), 3)
+
+ def test_doesnot_use_selector_when_not_specified(self):
+ selector = {"location": {"$gte": "FRA"}}
+ self.db.create_index(["location"], selector=selector, ddoc="Selected", name="Selected")
+ resp = self.db.find(selector, explain=True)
+ self.assertEqual(resp["index"]["name"], "_all_docs")
+
+ def test_doesnot_use_selector_when_not_specified_with_index(self):
+ selector = {"location": {"$gte": "FRA"}}
+ self.db.create_index(["location"], selector=selector, ddoc="Selected", name="Selected")
+ self.db.create_index(["location"], name="NotSelected")
+ resp = self.db.find(selector, explain=True)
+ self.assertEqual(resp["index"]["name"], "NotSelected")
+
+ @unittest.skipUnless(mango.has_text_service(), "requires text service")
+ def test_text_saves_selector_in_index(self):
+ selector = {"location": {"$gte": "FRA"}}
+ self.db.create_text_index(fields=[{"name":"location", "type":"string"}], selector=selector)
+ indexes = self.db.list_indexes()
+ self.assertEqual(indexes[1]["def"]["selector"], selector)
+
+ @unittest.skipUnless(mango.has_text_service(), "requires text service")
+ def test_text_uses_partial_index_for_query_selector(self):
+ selector = {"location": {"$gte": "FRA"}}
+ self.db.create_text_index(fields=[{"name":"location", "type":"string"}], selector=selector, ddoc="Selected", name="Selected")
+ resp = self.db.find(selector, explain=True, use_index='Selected')
+ self.assertEqual(resp["index"]["name"], "Selected")
+ docs = self.db.find(selector, use_index='Selected', fields=['_id', 'location'])
+ self.assertEqual(len(docs), 3)
+
+ @unittest.skipUnless(mango.has_text_service(), "requires text service")
+ def test_text_uses_partial_index_with_different_selector(self):
+ selector = {"location": {"$gte": "FRA"}}
+ selector2 = {"location": {"$gte": "A"}}
+ self.db.create_text_index(fields=[{"name":"location", "type":"string"}], selector=selector, ddoc="Selected", name="Selected")
+ resp = self.db.find(selector2, explain=True, use_index='Selected')
+ self.assertEqual(resp["index"]["name"], "Selected")
+ docs = self.db.find(selector2, use_index='Selected')
+ self.assertEqual(len(docs), 3)
+
+ @unittest.skipUnless(mango.has_text_service(), "requires text service")
+ def test_text_doesnot_use_selector_when_not_specified(self):
+ selector = {"location": {"$gte": "FRA"}}
+ self.db.create_text_index(fields=[{"name":"location", "type":"string"}], selector=selector, ddoc="Selected", name="Selected")
+ resp = self.db.find(selector, explain=True)
+ self.assertEqual(resp["index"]["name"], "_all_docs")
+
+ @unittest.skipUnless(mango.has_text_service(), "requires text service")
+ def test_text_doesnot_use_selector_when_not_specified_with_index(self):
+ selector = {"location": {"$gte": "FRA"}}
+ self.db.create_text_index(fields=[{"name":"location", "type":"string"}], selector=selector, ddoc="Selected", name="Selected")
+ self.db.create_text_index(fields=[{"name":"location", "type":"string"}], name="NotSelected")
+ resp = self.db.find(selector, explain=True)
+ self.assertEqual(resp["index"]["name"], "NotSelected") \ No newline at end of file
diff --git a/src/mango/test/mango.py b/src/mango/test/mango.py
index dbe980e29..2c8971485 100644
--- a/src/mango/test/mango.py
+++ b/src/mango/test/mango.py
@@ -84,7 +84,7 @@ class Database(object):
r.raise_for_status()
return r.json()
- def create_index(self, fields, idx_type="json", name=None, ddoc=None):
+ def create_index(self, fields, idx_type="json", name=None, ddoc=None, selector=None):
body = {
"index": {
"fields": fields
@@ -96,6 +96,8 @@ class Database(object):
body["name"] = name
if ddoc is not None:
body["ddoc"] = ddoc
+ if selector is not None:
+ body["index"]["selector"] = selector
body = json.dumps(body)
r = self.sess.post(self.path("_index"), data=body)
r.raise_for_status()
@@ -120,7 +122,7 @@ class Database(object):
if index_array_lengths is not None:
body["index"]["index_array_lengths"] = index_array_lengths
if selector is not None:
- body["selector"] = selector
+ body["index"]["selector"] = selector
if fields is not None:
body["index"]["fields"] = fields
if ddoc is not None:
@@ -226,6 +228,17 @@ class UserDocsTests(DbPerClass):
user_docs.setup(klass.db)
+class UserDocsTestsNoIndexes(DbPerClass):
+
+ @classmethod
+ def setUpClass(klass):
+ super(UserDocsTestsNoIndexes, klass).setUpClass()
+ user_docs.setup(
+ klass.db,
+ index_type="_all_docs"
+ )
+
+
class UserDocsTextTests(DbPerClass):
DEFAULT_FIELD = None
diff --git a/src/mango/test/user_docs.py b/src/mango/test/user_docs.py
index 01105e2f0..9896e5596 100644
--- a/src/mango/test/user_docs.py
+++ b/src/mango/test/user_docs.py
@@ -83,7 +83,8 @@ def add_view_indexes(db, kwargs):
["manager"],
["favorites"],
["favorites.3"],
- ["twitter"]
+ ["twitter"],
+ ["ordered"]
]
for idx in indexes:
assert db.create_index(idx) is True
@@ -310,8 +311,8 @@ DOCS = [
"Ruby",
"Erlang"
],
- "exists_field" : "should_exist1"
-
+ "exists_field" : "should_exist1",
+ "ordered": None
},
{
"_id": "6c0afcf1-e57e-421d-a03d-0c0717ebf843",
@@ -333,7 +334,8 @@ DOCS = [
"email": "jamesmcdaniel@globoil.com",
"manager": True,
"favorites": None,
- "exists_field" : "should_exist2"
+ "exists_field" : "should_exist2",
+ "ordered": False
},
{
"_id": "954272af-d5ed-4039-a5eb-8ed57e9def01",
@@ -361,7 +363,8 @@ DOCS = [
"Python"
],
"exists_array" : ["should", "exist", "array1"],
- "complex_field_value" : "+-(){}[]^~&&*||\"\\/?:!"
+ "complex_field_value" : "+-(){}[]^~&&*||\"\\/?:!",
+ "ordered": True
},
{
"_id": "e900001d-bc48-48a6-9b1a-ac9a1f5d1a03",
@@ -386,7 +389,8 @@ DOCS = [
"Erlang",
"Erlang"
],
- "exists_array" : ["should", "exist", "array2"]
+ "exists_array" : ["should", "exist", "array2"],
+ "ordered": 9
},
{
"_id": "b06aadcf-cd0f-4ca6-9f7e-2c993e48d4c4",
@@ -414,7 +418,8 @@ DOCS = [
"C++",
"C++"
],
- "exists_object" : {"should": "object"}
+ "exists_object" : {"should": "object"},
+ "ordered": 10000
},
{
"_id": "5b61abc1-a3d3-4092-b9d7-ced90e675536",
@@ -440,7 +445,8 @@ DOCS = [
"Python",
"Lisp"
],
- "exists_object" : {"another": "object"}
+ "exists_object" : {"another": "object"},
+ "ordered": "a"
},
{
"_id": "b1e70402-8add-4068-af8f-b4f3d0feb049",
@@ -466,7 +472,8 @@ DOCS = [
"C",
"Ruby",
"Ruby"
- ]
+ ],
+ "ordered": "A"
},
{
"_id": "c78c529f-0b07-4947-90a6-d6b7ca81da62",
@@ -491,7 +498,8 @@ DOCS = [
"Erlang",
"Python",
"Lisp"
- ]
+ ],
+ "ordered": "aa"
}
]
diff --git a/test/javascript/tests/attachments.js b/test/javascript/tests/attachments.js
index cd6474d0f..2e831a731 100644
--- a/test/javascript/tests/attachments.js
+++ b/test/javascript/tests/attachments.js
@@ -33,6 +33,24 @@ couchTests.attachments= function(debug) {
var save_response = db.save(binAttDoc);
T(save_response.ok);
+ var badAttDoc = {
+ _id: "bad_doc",
+ _attachments: {
+ "foo.txt": {
+ content_type: "text/plain",
+ data: "notBase64Encoded="
+ }
+ }
+ };
+
+ try {
+ db.save(badAttDoc);
+ T(false && "Shouldn't get here!");
+ } catch (e) {
+ TEquals("bad_request", e.error);
+ TEquals("Invalid attachment data for foo.txt", e.message);
+ }
+
var xhr = CouchDB.request("GET", "/" + db_name + "/bin_doc/foo.txt");
T(xhr.responseText == "This is a base64 encoded text");
T(xhr.getResponseHeader("Content-Type") == "application/octet-stream");