diff options
author | Joan Touzet <wohali@users.noreply.github.com> | 2017-09-19 12:57:12 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-09-19 12:57:12 -0400 |
commit | 8d078844d373d8bde752ab6b325b067f267ef001 (patch) | |
tree | 5e81fcf93bd23bd619bbde4def37c247daa45ab2 | |
parent | 297333f086a139dc2692a0e4f2bdac3d0dc3fde1 (diff) | |
parent | c531a13b22cc6fcea6afb342eb3f5cb315db0313 (diff) | |
download | couchdb-8d078844d373d8bde752ab6b325b067f267ef001.tar.gz |
Merge branch 'master' into run-mango-tests-with-check
35 files changed, 1256 insertions, 148 deletions
@@ -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"); |