diff options
author | Joan Touzet <wohali@users.noreply.github.com> | 2020-01-28 14:43:19 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-01-28 14:43:19 -0500 |
commit | bdd3769b7fe137a3b64308ed92241847e59520af (patch) | |
tree | bee77b5868c4e64cc84600451772612cb0c32a44 | |
parent | 82bd2d3e8f33efbf1eacc7ae7db9e4c704fd3ae6 (diff) | |
parent | 2aeae473d3fcf338097f5f2d46502ae0e866bc55 (diff) | |
download | couchdb-ini-singlenode-false.tar.gz |
Merge branch 'master' into ini-singlenode-falseini-singlenode-false
41 files changed, 707 insertions, 149 deletions
diff --git a/.gitignore b/.gitignore index 3fa860c59..e2f3df073 100644 --- a/.gitignore +++ b/.gitignore @@ -111,6 +111,7 @@ src/couch_tests/ebin/ src/global_changes/ebin/ src/mango/ebin/ src/mango/test/*.pyc +src/mango/nosetests.xml src/mango/venv/ test/javascript/junit.xml diff --git a/build-aux/Jenkinsfile.full b/build-aux/Jenkinsfile.full index f13be068d..b1d46e846 100644 --- a/build-aux/Jenkinsfile.full +++ b/build-aux/Jenkinsfile.full @@ -81,7 +81,7 @@ pipeline { agent { docker { label 'docker' - image 'couchdbdev/debian-stretch-erlang-20.3.8.24-1:latest' + image 'couchdbdev/debian-stretch-erlang-20.3.8.25-1:latest' args "${DOCKER_ARGS}" alwaysPull true } @@ -152,7 +152,8 @@ pipeline { junit '**/.eunit/*.xml, **/_build/*/lib/couchdbtest/*.xml, **/src/mango/nosetests.xml, **/test/javascript/junit.xml' } cleanup { - sh 'rm -rf $COUCHDB_IO_LOG_DIR' + sh 'killall -9 beam.smp || true' + sh 'rm -rf ${WORKSPACE}/* ${COUCHDB_IO_LOG_DIR} || true' } } // post } // stage FreeBSD @@ -160,7 +161,7 @@ pipeline { stage('CentOS 6') { agent { docker { - image 'couchdbdev/centos-6-erlang-20.3.8.24-1:latest' + image 'couchdbdev/centos-6-erlang-20.3.8.25-1:latest' label 'docker' args "${DOCKER_ARGS}" alwaysPull true @@ -204,7 +205,7 @@ pipeline { stage('CentOS 7') { agent { docker { - image 'couchdbdev/centos-7-erlang-20.3.8.24-1:latest' + image 'couchdbdev/centos-7-erlang-20.3.8.25-1:latest' label 'docker' args "${DOCKER_ARGS}" alwaysPull true @@ -249,7 +250,7 @@ pipeline { stage('CentOS 8') { agent { docker { - image 'couchdbdev/centos-8-erlang-20.3.8.24-1:latest' + image 'couchdbdev/centos-8-erlang-20.3.8.25-1:latest' label 'docker' args "${DOCKER_ARGS}" alwaysPull true @@ -294,7 +295,7 @@ pipeline { stage('Ubuntu Xenial') { agent { docker { - image 'couchdbdev/ubuntu-xenial-erlang-20.3.8.24-1:latest' + image 'couchdbdev/ubuntu-xenial-erlang-20.3.8.25-1:latest' label 'docker' args "${DOCKER_ARGS}" alwaysPull true @@ -338,7 +339,7 @@ pipeline { stage('Ubuntu Bionic') { agent { docker { - image 'couchdbdev/ubuntu-bionic-erlang-20.3.8.24-1:latest' + image 'couchdbdev/ubuntu-bionic-erlang-20.3.8.25-1:latest' label 'docker' alwaysPull true args "${DOCKER_ARGS}" @@ -382,7 +383,7 @@ pipeline { stage('Debian Stretch') { agent { docker { - image 'couchdbdev/debian-stretch-erlang-20.3.8.24-1:latest' + image 'couchdbdev/debian-stretch-erlang-20.3.8.25-1:latest' label 'docker' alwaysPull true args "${DOCKER_ARGS}" @@ -426,7 +427,7 @@ pipeline { stage('Debian Buster amd64') { agent { docker { - image 'couchdbdev/debian-buster-erlang-20.3.8.24-1:latest' + image 'couchdbdev/debian-buster-erlang-20.3.8.25-1:latest' label 'docker' alwaysPull true args "${DOCKER_ARGS}" @@ -470,7 +471,7 @@ pipeline { stage('Debian Buster arm64v8') { agent { docker { - image 'couchdbdev/arm64v8-debian-buster-erlang-20.3.8.24-1:latest' + image 'couchdbdev/arm64v8-debian-buster-erlang-20.3.8.25-1:latest' label 'arm64v8' alwaysPull true args "${DOCKER_ARGS}" @@ -511,6 +512,49 @@ pipeline { } // post } // stage + stage('Debian Buster ppc64le') { + agent { + docker { + image 'couchdbdev/ppc64le-debian-buster-erlang-20.3.8.25-1:latest' + label 'ppc64le' + alwaysPull true + args "${DOCKER_ARGS}" + } + } + environment { + platform = 'buster' + sm_ver = '60' + } + stages { + stage('Build from tarball & test') { + steps { + unstash 'tarball' + sh( script: build_and_test ) + } + post { + always { + junit '**/.eunit/*.xml, **/_build/*/lib/couchdbtest/*.xml, **/src/mango/nosetests.xml, **/test/javascript/junit.xml' + } + } + } + stage('Build CouchDB packages') { + steps { + sh( script: make_packages ) + sh( script: cleanup_and_save ) + } + post { + success { + archiveArtifacts artifacts: 'pkgs/**', fingerprint: true + } + } + } + } // stages + post { + cleanup { + sh 'rm -rf ${WORKSPACE}/*' + } + } // post + } // stage /* * Example of how to do a qemu-based run, please leave here @@ -540,12 +584,12 @@ pipeline { } stage('Pull latest docker image') { steps { - sh "docker pull couchdbdev/arm64v8-debian-buster-erlang-20.3.8.24-1:latest" + sh "docker pull couchdbdev/arm64v8-debian-buster-erlang-20.3.8.25-1:latest" } } stage('Build from tarball & test & packages') { steps { - withDockerContainer(image: "couchdbdev/arm64v8-debian-buster-erlang-20.3.8.24-1:latest", args: "${DOCKER_ARGS}") { + withDockerContainer(image: "couchdbdev/arm64v8-debian-buster-erlang-20.3.8.25-1:latest", args: "${DOCKER_ARGS}") { unstash 'tarball' withEnv(['MIX_HOME='+pwd(), 'HEX_HOME='+pwd()]) { sh( script: build_and_test ) @@ -585,7 +629,7 @@ pipeline { agent { docker { - image 'couchdbdev/debian-buster-erlang-20.3.8.24-1:latest' + image 'couchdbdev/debian-buster-erlang-20.3.8.25-1:latest' label 'docker' alwaysPull true args "${DOCKER_ARGS}" diff --git a/build-aux/Jenkinsfile.pr b/build-aux/Jenkinsfile.pr index cf287b225..8c9cbd930 100644 --- a/build-aux/Jenkinsfile.pr +++ b/build-aux/Jenkinsfile.pr @@ -12,7 +12,6 @@ // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. - build_and_test = ''' mkdir -p ${COUCHDB_IO_LOG_DIR} ${ERLANG_VERSION} cd ${ERLANG_VERSION} @@ -46,11 +45,12 @@ pipeline { // npm config cache below deals with /home/jenkins not mapping correctly // inside the image DOCKER_ARGS = '-e npm_config_cache=npm-cache -e HOME=. -v=/etc/passwd:/etc/passwd -v /etc/group:/etc/group' - // Also be sure to change these values in the matrix below... + // *** BE SURE TO CHANGE THE ERLANG VERSION FARTHER DOWN S WELL *** + // Search for ERLANG_VERSION // see https://issues.jenkins-ci.org/browse/JENKINS-40986 LOW_ERLANG_VER = '20.3.8.11' - MID_ERLANG_VER = '20.3.8.24' - HIGH_ERLANG_VER = '22.2' + MID_ERLANG_VER = '20.3.8.25' + HIGH_ERLANG_VER = '22.2.3' } options { @@ -106,7 +106,7 @@ pipeline { axes { axis { name 'ERLANG_VERSION' - values "20.3.8.11", "20.3.8.24", "22.2" + values "20.3.8.11", "20.3.8.25", "22.2.3" } } diff --git a/build-aux/show-test-results.py b/build-aux/show-test-results.py index c465fcf8a..c76a88409 100755 --- a/build-aux/show-test-results.py +++ b/build-aux/show-test-results.py @@ -12,7 +12,7 @@ TEST_COLLECTIONS = { "EUnit": "src/**/.eunit/*.xml", "EXUnit": "_build/integration/lib/couchdbtest/*.xml", "Mango": "src/mango/*.xml", - "JavaScript": "test/javascript/*.xml" + "JavaScript": "test/javascript/*.xml", } @@ -109,7 +109,6 @@ defmodule CouchDBTest.Mixfile do "bear", "mochiweb", "snappy", - "triq", "rebar", "proper", "mochiweb", diff --git a/rebar.config.script b/rebar.config.script index e39a08228..d356ac20e 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -142,10 +142,10 @@ SubDirs = [ DepDescs = [ %% Independent Apps -{config, "config", {tag, "2.1.6"}}, +{config, "config", {tag, "2.1.7"}}, {b64url, "b64url", {tag, "1.0.1"}}, -{ets_lru, "ets-lru", {tag, "1.0.0"}}, -{khash, "khash", {tag, "1.0.1"}}, +{ets_lru, "ets-lru", {tag, "1.1.0"}}, +{khash, "khash", {tag, "1.1.0"}}, {snappy, "snappy", {tag, "CouchDB-1.0.4"}}, %% Non-Erlang deps @@ -155,7 +155,7 @@ DepDescs = [ {tag, "v1.2.2"}, [raw]}, %% Third party deps {folsom, "folsom", {tag, "CouchDB-0.8.3"}}, -{hyper, "hyper", {tag, "CouchDB-2.2.0-4"}}, +{hyper, "hyper", {tag, "CouchDB-2.2.0-6"}}, {ibrowse, "ibrowse", {tag, "CouchDB-4.0.1-1"}}, {jiffy, "jiffy", {tag, "CouchDB-0.14.11-2"}}, {mochiweb, "mochiweb", {tag, "v2.20.0"}}, diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini index 2bd537b0c..599942d44 100644 --- a/rel/overlay/etc/default.ini +++ b/rel/overlay/etc/default.ini @@ -95,9 +95,11 @@ users_db_security_editable = false couch = couch_bt_engine [process_priority] -; Selectively disable altering process priorities -; for modules that request it. -; couch_server = true +; Selectively disable altering process priorities for modules that request it. +; * NOTE: couch_server priority has been shown to lead to CouchDB hangs and +; unexpected failures. Do not enable unless you're sure you can tolerate +; this possibility. +;couch_server = false [cluster] q=2 @@ -217,6 +219,12 @@ port = 6984 ; buffer_count = 2000 ; server_per_node = true ; stream_limit = 5 +; +; Use a single message to kill a group of remote workers This is +; mostly is an upgrade clause to allow operating in a mixed cluster of +; 2.x and 3.x nodes. After upgrading switch to true to save some +; network bandwidth +;use_kill_all = false ; [global_changes] ; max_event_delay = 25 @@ -318,6 +326,10 @@ os_process_limit = 100 ;index_all_disabled = false ; Default limit value for mango _find queries. ;default_limit = 25 +; Ratio between documents scanned and results matched that will +; generate a warning in the _find response. Setting this to 0 disables +; the warning. +;index_scan_warning_threshold = 10 [indexers] couch_mrview = true @@ -579,3 +591,14 @@ compaction = false ; CouchDB will use the value of `max_limit` instead. If neither is ; defined, the default is 2000 as stated here. ; max_limit_partitions = 2000 + +[reshard] +;max_jobs = 48 +;max_history = 20 +;max_retries = 1 +;retry_interval_sec = 10 +;delete_source = true +;update_shard_map_timeout_sec = 60 +;source_close_timeout_sec = 600 +;require_node_param = false +;require_range_param = false diff --git a/src/chttpd/src/chttpd_auth.erl b/src/chttpd/src/chttpd_auth.erl index 45e11905b..607f09a8a 100644 --- a/src/chttpd/src/chttpd_auth.erl +++ b/src/chttpd/src/chttpd_auth.erl @@ -55,10 +55,12 @@ party_mode_handler(#httpd{method='POST', path_parts=[<<"_session">>]} = Req) -> % See #1947 - users should always be able to attempt a login Req#httpd{user_ctx=#user_ctx{}}; party_mode_handler(Req) -> - case config:get("chttpd", "require_valid_user", "false") of - "true" -> + RequireValidUser = config:get_boolean("chttpd", "require_valid_user", false), + ExceptUp = config:get_boolean("chttpd", "require_valid_user_except_for_up", true), + case RequireValidUser andalso not ExceptUp of + true -> throw({unauthorized, <<"Authentication required.">>}); - "false" -> + false -> case config:get("admins") of [] -> Req#httpd{user_ctx = ?ADMIN_USER}; diff --git a/src/chttpd/src/chttpd_node.erl b/src/chttpd/src/chttpd_node.erl index 202070279..acd5affbd 100644 --- a/src/chttpd/src/chttpd_node.erl +++ b/src/chttpd/src/chttpd_node.erl @@ -46,6 +46,16 @@ handle_node_req(#httpd{method='GET', path_parts=[_, Node, <<"_config">>]}=Req) - send_json(Req, 200, {KVs}); handle_node_req(#httpd{path_parts=[_, _Node, <<"_config">>]}=Req) -> send_method_not_allowed(Req, "GET"); +% POST /_node/$node/_config/_reload - Flushes unpersisted config values from RAM +handle_node_req(#httpd{method='POST', path_parts=[_, Node, <<"_config">>, <<"_reload">>]}=Req) -> + case call_node(Node, config, reload, []) of + ok -> + send_json(Req, 200, {[{ok, true}]}); + {error, Reason} -> + chttpd:send_error(Req, {bad_request, Reason}) + end; +handle_node_req(#httpd{path_parts=[_, _Node, <<"_config">>, <<"_reload">>]}=Req) -> + send_method_not_allowed(Req, "POST"); % GET /_node/$node/_config/Section handle_node_req(#httpd{method='GET', path_parts=[_, Node, <<"_config">>, Section]}=Req) -> KVs = [{list_to_binary(Key), list_to_binary(Value)} diff --git a/src/couch/include/couch_eunit_proper.hrl b/src/couch/include/couch_eunit_proper.hrl new file mode 100644 index 000000000..31ae40e9d --- /dev/null +++ b/src/couch/include/couch_eunit_proper.hrl @@ -0,0 +1,29 @@ +% 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. + +-include_lib("proper/include/proper.hrl"). +-include_lib("eunit/include/eunit.hrl"). + + +-define(EUNIT_QUICKCHECK(QuickcheckTimeout), + [ + { + atom_to_list(F), + {timeout, QuickcheckTimeout, + ?_assert(proper:quickcheck(?MODULE:F(), [ + {to_file, user}, + {start_size, 2}, + long_result + ]))} + } + || {F, 0} <- ?MODULE:module_info(exports), F > 'prop_', F < 'prop`' + ]). diff --git a/src/couch/priv/stats_descriptions.cfg b/src/couch/priv/stats_descriptions.cfg index ae203bb21..7c8fd94cb 100644 --- a/src/couch/priv/stats_descriptions.cfg +++ b/src/couch/priv/stats_descriptions.cfg @@ -302,3 +302,31 @@ {type, counter}, {desc, <<"number of mango queries that could not use an index">>} ]}. +{[mango, query_invalid_index], [ + {type, counter}, + {desc, <<"number of mango queries that generated an invalid index warning">>} +]}. +{[mango, too_many_docs_scanned], [ + {type, counter}, + {desc, <<"number of mango queries that generated an index scan warning">>} +]}. +{[mango, docs_examined], [ + {type, counter}, + {desc, <<"number of documents examined by mango queries coordinated by this node">>} +]}. +{[mango, quorum_docs_examined], [ + {type, counter}, + {desc, <<"number of documents examined by mango queries, using cluster quorum">>} +]}. +{[mango, results_returned], [ + {type, counter}, + {desc, <<"number of rows returned by mango queries">>} +]}. +{[mango, query_time], [ + {type, histogram}, + {desc, <<"length of time processing a mango query">>} +]}. +{[mango, evaluate_selector], [ + {type, counter}, + {desc, <<"number of mango selector evaluations">>} +]}. diff --git a/src/couch/rebar.config.script b/src/couch/rebar.config.script index a6468612c..c0889ce75 100644 --- a/src/couch/rebar.config.script +++ b/src/couch/rebar.config.script @@ -92,6 +92,11 @@ MD5Config = case lists:keyfind(erlang_md5, 1, CouchConfig) of [] end, +ProperConfig = case code:lib_dir(proper) of + {error, bad_name} -> []; + _ -> [{d, 'WITH_PROPER'}] +end, + {JS_CFLAGS, JS_LDFLAGS} = case os:type() of {win32, _} when SMVsn == "1.8.5" -> { @@ -212,7 +217,7 @@ AddConfig = [ {d, 'COUCHDB_VERSION', Version}, {d, 'COUCHDB_GIT_SHA', GitSha}, {i, "../"} - ] ++ MD5Config}, + ] ++ MD5Config ++ ProperConfig}, {eunit_compile_opts, PlatformDefines} ]. diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl index 96de5bf3b..5e4450301 100644 --- a/src/couch/src/couch_httpd_auth.erl +++ b/src/couch/src/couch_httpd_auth.erl @@ -88,11 +88,6 @@ basic_name_pw(Req) -> default_authentication_handler(Req) -> default_authentication_handler(Req, couch_auth_cache). -default_authentication_handler(#httpd{path_parts=[<<"_up">>]}=Req, AuthModule) -> - case config:get_boolean("chttpd", "require_valid_user_except_for_up", false) of - true -> Req#httpd{user_ctx=?ADMIN_USER}; - _False -> default_authentication_handler(Req, AuthModule) - end; default_authentication_handler(Req, AuthModule) -> case basic_name_pw(Req) of {User, Pass} -> diff --git a/src/couch/src/couch_sup.erl b/src/couch/src/couch_sup.erl index c4a2e6303..6e7ef98b7 100644 --- a/src/couch/src/couch_sup.erl +++ b/src/couch/src/couch_sup.erl @@ -157,7 +157,7 @@ write_uris() -> get_uris() -> - Ip = config:get("httpd", "bind_address"), + Ip = config:get("chttpd", "bind_address"), lists:flatmap(fun(Uri) -> case get_uri(Uri, Ip) of undefined -> []; diff --git a/src/couch/src/couch_util.erl b/src/couch/src/couch_util.erl index b5c93ce51..180db9518 100644 --- a/src/couch/src/couch_util.erl +++ b/src/couch/src/couch_util.erl @@ -692,7 +692,7 @@ set_mqd_off_heap(Module) -> set_process_priority(Module, Level) -> - case config:get_boolean("process_priority", atom_to_list(Module), true) of + case config:get_boolean("process_priority", atom_to_list(Module), false) of true -> process_flag(priority, Level), ok; diff --git a/src/couch/test/eunit/couch_key_tree_prop_tests.erl b/src/couch/test/eunit/couch_key_tree_prop_tests.erl index f8146926a..9c09aace5 100644 --- a/src/couch/test/eunit/couch_key_tree_prop_tests.erl +++ b/src/couch/test/eunit/couch_key_tree_prop_tests.erl @@ -12,14 +12,21 @@ -module(couch_key_tree_prop_tests). --include_lib("triq/include/triq.hrl"). --triq(eunit). + +-ifdef(WITH_PROPER). + +-include_lib("couch/include/couch_eunit_proper.hrl"). + -define(SIZE_REDUCTION, 3). % How much to reduce size with tree depth. -define(MAX_BRANCHES, 4). % Maximum number of branches. -define(RAND_SIZE, 1 bsl 64). +property_test_() -> + ?EUNIT_QUICKCHECK(60). + + % % Properties % @@ -116,10 +123,10 @@ prop_stemming_results_in_same_or_less_total_revs() -> prop_stem_path_expect_size_to_get_smaller() -> ?FORALL({RevTree, StemDepth}, { - ?SIZED(Size, resize(Size * 10, g_revtree([], 1))), - choose(1,5) + ?SIZED(Size, g_revtree(Size * 10, [], 1)), + choose(1,3) }, - ?IMPLIES(real_depth(RevTree) > 5, + ?IMPLIES(real_depth(RevTree) > 3, begin Stemmed = couch_key_tree:stem(RevTree, StemDepth), StemmedKeys = lists:usort(keylist(Stemmed)), @@ -224,7 +231,7 @@ g_revtree(Size, ERevs, MaxBranches) -> g_treenode(0, Revs, _) -> {elements(Revs), x, []}; g_treenode(Size, Revs, MaxBranches) -> - ?DELAY(?LET(N, int(0, MaxBranches), + ?LAZY(?LET(N, choose(0, MaxBranches), begin [Rev | ChildRevs] = Revs, {Rev, x, g_nodes(Size div ?SIZE_REDUCTION, N, ChildRevs, MaxBranches)} @@ -261,17 +268,15 @@ g_stem_depth(Size) -> % Uses the shuffle/1 function to shuffle the input list. Unshuffled list is % used as the shrink value. % +g_shuffle([]) -> []; g_shuffle(L) when is_list(L) -> - triq_dom:domain(g_shuffle, - fun(Self, _Size) -> {Self, shuffle(L)} end, - fun(Self, _Val) -> {Self, L} end - ). + ?LET(X, elements(L), [X | g_shuffle(lists:delete(X,L))]). % Wrapper to make a list shuffling generator that doesn't shrink % g_shuffle_noshrink(L) when is_list(L) -> - triq_dom:noshrink(g_shuffle(L)). + proper_types:noshrink(g_shuffle(L)). % Generate shuffled sublists up to N items long from a list. @@ -297,7 +302,7 @@ g_revs(Size, Existing) when is_integer(Size), is_list(Existing) -> true -> % have extra, try various sublists g_shuffled_sublists(Revs, Expected); false -> - triq_dom:return(Revs) + proper_types:return(Revs) end. @@ -319,19 +324,12 @@ same_keys(RevTree1, RevTree2) -> all(L) -> lists:all(fun(E) -> E end, L). -% Shufle a list of items. Tag each item with a random number then sort -% the list and remove the tags. -% -shuffle(L) -> - Tagged = [{triq_rnd:uniform(), X} || X <- L], - [X || {_, X} <- lists:sort(Tagged)]. - % Generate list of relateively unique large random numbers rand_list(N) when N =< 0 -> []; rand_list(N) -> - [triq_rnd:uniform(?RAND_SIZE) || _ <- lists:seq(1, N)]. + [rand:uniform(?RAND_SIZE) || _ <- lists:seq(1, N)]. % Generate a list of revisions to be used as key in revision trees. Expected @@ -528,3 +526,5 @@ child_revs(ChildCount, Revs, Size, MaxBranches) -> false -> throw({not_enough_revisions, length(Revs), NeedKeys}) end. + +-endif. diff --git a/src/dreyfus/src/dreyfus_fabric_search.erl b/src/dreyfus/src/dreyfus_fabric_search.erl index c0ebde1d6..8edaa385a 100644 --- a/src/dreyfus/src/dreyfus_fabric_search.erl +++ b/src/dreyfus/src/dreyfus_fabric_search.erl @@ -56,7 +56,6 @@ go(DbName, DDoc, IndexName, #index_query_args{}=QueryArgs) -> Shards = dreyfus_util:get_shards(DbName, QueryArgs), LiveNodes = [node() | nodes()], LiveShards = [S || #shard{node=Node} = S <- Shards, lists:member(Node, LiveNodes)], - RingOpts = dreyful_util:get_ring_opts(QueryArgs, LiveShards), Bookmark1 = dreyfus_bookmark:add_missing_shards(Bookmark0, LiveShards), Counters0 = lists:flatmap(fun({#shard{name=Name, node=N} = Shard, After}) -> QueryArgs1 = dreyfus_util:export(QueryArgs#index_query_args{ diff --git a/src/mango/src/mango_cursor.erl b/src/mango/src/mango_cursor.erl index dc2ee74c7..29be49490 100644 --- a/src/mango/src/mango_cursor.erl +++ b/src/mango/src/mango_cursor.erl @@ -19,7 +19,8 @@ execute/3, maybe_filter_indexes_by_ddoc/2, remove_indexes_with_partial_filter_selector/1, - maybe_add_warning/3 + maybe_add_warning/4, + maybe_noop_range/2 ]). @@ -114,7 +115,7 @@ filter_indexes(Indexes0, DesignId, ViewName) -> remove_indexes_with_partial_filter_selector(Indexes) -> - FiltFun = fun(Idx) -> + FiltFun = fun(Idx) -> case mango_idx:get_partial_filter_selector(Idx) of undefined -> true; _ -> false @@ -123,6 +124,22 @@ remove_indexes_with_partial_filter_selector(Indexes) -> lists:filter(FiltFun, Indexes). +maybe_add_warning(UserFun, #cursor{index = Index, opts = Opts}, Stats, UserAcc) -> + W0 = invalid_index_warning(Index, Opts), + W1 = no_index_warning(Index), + W2 = index_scan_warning(Stats), + Warnings = lists:append([W0, W1, W2]), + case Warnings of + [] -> + UserAcc; + _ -> + WarningStr = lists:join(<<"\n">>, Warnings), + Arg = {add_key, warning, WarningStr}, + {_Go, UserAcc1} = UserFun(Arg, UserAcc), + UserAcc1 + end. + + create_cursor(Db, Indexes, Selector, Opts) -> [{CursorMod, CursorModIndexes} | _] = group_indexes_by_type(Indexes), CursorMod:create(Db, CursorModIndexes, Selector, Opts). @@ -146,46 +163,86 @@ group_indexes_by_type(Indexes) -> end, ?CURSOR_MODULES). -maybe_add_warning(UserFun, #cursor{index = Index, opts = Opts}, UserAcc) -> - NoIndexWarning = case Index#idx.type of - <<"special">> -> - <<"no matching index found, create an index to optimize query time">>; - _ -> - ok - end, - - UseIndexInvalidWarning = case lists:keyfind(use_index, 1, Opts) of - {use_index, []} -> - NoIndexWarning; - {use_index, [DesignId]} -> - case filter_indexes([Index], DesignId) of - [] -> - fmt("_design/~s was not used because it does not contain a valid index for this query.", - [ddoc_name(DesignId)]); - _ -> - NoIndexWarning - end; - {use_index, [DesignId, ViewName]} -> - case filter_indexes([Index], DesignId, ViewName) of - [] -> - fmt("_design/~s, ~s was not used because it is not a valid index for this query.", - [ddoc_name(DesignId), ViewName]); - _ -> - NoIndexWarning - end - end, - - maybe_add_warning_int(UseIndexInvalidWarning, UserFun, UserAcc). - - -maybe_add_warning_int(ok, _, UserAcc) -> - UserAcc; - -maybe_add_warning_int(Warning, UserFun, UserAcc) -> +% warn if the _all_docs index was used to fulfil a query +no_index_warning(#idx{type = Type}) when Type =:= <<"special">> -> couch_stats:increment_counter([mango, unindexed_queries]), - Arg = {add_key, warning, Warning}, - {_Go, UserAcc0} = UserFun(Arg, UserAcc), - UserAcc0. + [<<"No matching index found, create an index to optimize query time.">>]; + +no_index_warning(_) -> + []. + + +% warn if user specified an index which doesn't exist or isn't valid +% for the selector. +% In this scenario, Mango will ignore the index hint and auto-select an index. +invalid_index_warning(Index, Opts) -> + UseIndex = lists:keyfind(use_index, 1, Opts), + invalid_index_warning_int(Index, UseIndex). + + +invalid_index_warning_int(Index, {use_index, [DesignId]}) -> + Filtered = filter_indexes([Index], DesignId), + if Filtered /= [] -> []; true -> + couch_stats:increment_counter([mango, query_invalid_index]), + Reason = fmt("_design/~s was not used because it does not contain a valid index for this query.", + [ddoc_name(DesignId)]), + [Reason] + end; + +invalid_index_warning_int(Index, {use_index, [DesignId, ViewName]}) -> + Filtered = filter_indexes([Index], DesignId, ViewName), + if Filtered /= [] -> []; true -> + couch_stats:increment_counter([mango, query_invalid_index]), + Reason = fmt("_design/~s, ~s was not used because it is not a valid index for this query.", + [ddoc_name(DesignId), ViewName]), + [Reason] + end; + +invalid_index_warning_int(_, _) -> + []. + + +% warn if a large number of documents needed to be scanned per result +% returned, implying a lot of in-memory filtering +index_scan_warning(#execution_stats { + totalDocsExamined = Docs, + totalQuorumDocsExamined = DocsQuorum, + resultsReturned = ResultCount + }) -> + % Docs and DocsQuorum are mutually exclusive so it's safe to sum them + DocsScanned = Docs + DocsQuorum, + Ratio = calculate_index_scan_ratio(DocsScanned, ResultCount), + Threshold = config:get_integer("mango", "index_scan_warning_threshold", 10), + case Threshold > 0 andalso Ratio > Threshold of + true -> + couch_stats:increment_counter([mango, too_many_docs_scanned]), + Reason = <<"The number of documents examined is high in proportion to the number of results returned. Consider adding a more specific index to improve this.">>, + [Reason]; + false -> [] + end. + +% When there is an empty array for certain operators, we don't actually +% want to execute the query so we deny it by making the range [empty]. +% To clarify, we don't want this query to execute: {"$or": []}. Results should +% be empty. We do want this query to execute: {"age": 22, "$or": []}. It should +% return the same results as {"age": 22} +maybe_noop_range({[{Op, []}]}, IndexRanges) -> + Noops = [<<"$all">>, <<"$and">>, <<"$or">>, <<"$in">>], + case lists:member(Op, Noops) of + true -> + [empty]; + false -> + IndexRanges + end; +maybe_noop_range(_, IndexRanges) -> + IndexRanges. + + +calculate_index_scan_ratio(DocsScanned, 0) -> + DocsScanned; + +calculate_index_scan_ratio(DocsScanned, ResultCount) -> + DocsScanned / ResultCount. fmt(Format, Args) -> diff --git a/src/mango/src/mango_cursor_special.erl b/src/mango/src/mango_cursor_special.erl index f4a760d1c..df1f6d655 100644 --- a/src/mango/src/mango_cursor_special.erl +++ b/src/mango/src/mango_cursor_special.erl @@ -41,12 +41,14 @@ create(Db, Indexes, Selector, Opts) -> Limit = couch_util:get_value(limit, Opts, mango_opts:default_limit()), Skip = couch_util:get_value(skip, Opts, 0), Fields = couch_util:get_value(fields, Opts, all_fields), - Bookmark = couch_util:get_value(bookmark, Opts), + Bookmark = couch_util:get_value(bookmark, Opts), + + IndexRanges1 = mango_cursor:maybe_noop_range(Selector, IndexRanges), {ok, #cursor{ db = Db, index = Index, - ranges = IndexRanges, + ranges = IndexRanges1, selector = Selector, opts = Opts, limit = Limit, @@ -55,7 +57,6 @@ create(Db, Indexes, Selector, Opts) -> bookmark = Bookmark }}. - explain(Cursor) -> mango_cursor_view:explain(Cursor). diff --git a/src/mango/src/mango_cursor_text.erl b/src/mango/src/mango_cursor_text.erl index 8938f3557..43ef84e4c 100644 --- a/src/mango/src/mango_cursor_text.erl +++ b/src/mango/src/mango_cursor_text.erl @@ -92,8 +92,9 @@ execute(Cursor, UserFun, UserAcc) -> opts = Opts, execution_stats = Stats } = Cursor, + Query = mango_selector_text:convert(Selector), QueryArgs = #index_query_args{ - q = mango_selector_text:convert(Selector), + q = Query, partition = get_partition(Opts, nil), sort = sort_query(Opts, Selector), raw_bookmark = true @@ -113,7 +114,12 @@ execute(Cursor, UserFun, UserAcc) -> execution_stats = mango_execution_stats:log_start(Stats) }, try - execute(CAcc) + case Query of + <<>> -> + throw({stop, CAcc}); + _ -> + execute(CAcc) + end catch throw:{stop, FinalCAcc} -> #cacc{ @@ -126,7 +132,7 @@ 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), - FinalUserAcc1 = mango_cursor:maybe_add_warning(UserFun, Cursor, FinalUserAcc0), + FinalUserAcc1 = mango_cursor:maybe_add_warning(UserFun, Cursor, Stats0, FinalUserAcc0), {ok, FinalUserAcc1} end. @@ -170,6 +176,10 @@ handle_hits(CAcc0, [{Sort, Doc} | Rest]) -> handle_hits(CAcc1, Rest). +handle_hit(CAcc0, Sort, not_found) -> + CAcc1 = update_bookmark(CAcc0, Sort), + CAcc1; + handle_hit(CAcc0, Sort, Doc) -> #cacc{ limit = Limit, @@ -178,6 +188,7 @@ handle_hit(CAcc0, Sort, Doc) -> } = CAcc0, CAcc1 = update_bookmark(CAcc0, Sort), Stats1 = mango_execution_stats:incr_docs_examined(Stats), + couch_stats:increment_counter([mango, docs_examined]), CAcc2 = CAcc1#cacc{execution_stats = Stats1}, case mango_selector:match(CAcc2#cacc.selector, Doc) of true when Skip > 0 -> diff --git a/src/mango/src/mango_cursor_view.erl b/src/mango/src/mango_cursor_view.erl index f1b753bd7..240ef501d 100644 --- a/src/mango/src/mango_cursor_view.erl +++ b/src/mango/src/mango_cursor_view.erl @@ -46,10 +46,12 @@ create(Db, Indexes, Selector, Opts) -> Fields = couch_util:get_value(fields, Opts, all_fields), Bookmark = couch_util:get_value(bookmark, Opts), + IndexRanges1 = mango_cursor:maybe_noop_range(Selector, IndexRanges), + {ok, #cursor{ db = Db, index = Index, - ranges = IndexRanges, + ranges = IndexRanges1, selector = Selector, opts = Opts, limit = Limit, @@ -99,12 +101,20 @@ maybe_replace_max_json([H | T] = EndKey) when is_list(EndKey) -> maybe_replace_max_json(EndKey) -> EndKey. + base_args(#cursor{index = Idx, selector = Selector} = Cursor) -> + {StartKey, EndKey} = case Cursor#cursor.ranges of + [empty] -> + {null, null}; + _ -> + {mango_idx:start_key(Idx, Cursor#cursor.ranges), + mango_idx:end_key(Idx, Cursor#cursor.ranges)} + end, #mrargs{ view_type = map, reduce = false, - start_key = mango_idx:start_key(Idx, Cursor#cursor.ranges), - end_key = mango_idx:end_key(Idx, Cursor#cursor.ranges), + start_key = StartKey, + end_key = EndKey, include_docs = true, extra = [{callback, {?MODULE, view_cb}}, {selector, Selector}] }. @@ -145,7 +155,7 @@ 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), - FinalUserAcc1 = mango_cursor:maybe_add_warning(UserFun, Cursor, FinalUserAcc0), + FinalUserAcc1 = mango_cursor:maybe_add_warning(UserFun, Cursor, Stats0, FinalUserAcc0), {ok, FinalUserAcc1}; {error, Reason} -> {error, Reason} @@ -239,6 +249,7 @@ view_cb({row, Row}, #mrargs{extra = Options} = Acc) -> Doc -> put(mango_docs_examined, get(mango_docs_examined) + 1), Selector = couch_util:get_value(selector, Options), + couch_stats:increment_counter([mango, docs_examined]), case mango_selector:match(Selector, Doc) of true -> ok = rexi:stream2(ViewRow), @@ -423,6 +434,7 @@ doc_member(Cursor, RowProps) -> % an undefined doc was returned, indicating we should % perform a quorum fetch ExecutionStats1 = mango_execution_stats:incr_quorum_docs_examined(ExecutionStats), + couch_stats:increment_counter([mango, quorum_docs_examined]), Id = couch_util:get_value(id, RowProps), case mango_util:defer(fabric, open_doc, [Db, Id, Opts]) of {ok, #doc{}=DocProps} -> diff --git a/src/mango/src/mango_execution_stats.erl b/src/mango/src/mango_execution_stats.erl index 7e8afd782..5878a3190 100644 --- a/src/mango/src/mango_execution_stats.erl +++ b/src/mango/src/mango_execution_stats.erl @@ -62,6 +62,7 @@ incr_quorum_docs_examined(Stats) -> incr_results_returned(Stats) -> + couch_stats:increment_counter([mango, results_returned]), Stats#execution_stats { resultsReturned = Stats#execution_stats.resultsReturned + 1 }. @@ -81,11 +82,13 @@ log_end(Stats) -> }. -maybe_add_stats(Opts, UserFun, Stats, UserAcc) -> +maybe_add_stats(Opts, UserFun, Stats0, UserAcc) -> + Stats1 = log_end(Stats0), + couch_stats:update_histogram([mango, query_time], Stats1#execution_stats.executionTimeMs), + case couch_util:get_value(execution_stats, Opts) of true -> - Stats0 = log_end(Stats), - JSONValue = to_json(Stats0), + JSONValue = to_json(Stats1), Arg = {add_key, execution_stats, JSONValue}, {_Go, FinalUserAcc} = UserFun(Arg, UserAcc), FinalUserAcc; diff --git a/src/mango/src/mango_idx_text.erl b/src/mango/src/mango_idx_text.erl index 50f6cc866..1d4becfb3 100644 --- a/src/mango/src/mango_idx_text.erl +++ b/src/mango/src/mango_idx_text.erl @@ -126,6 +126,8 @@ columns(Idx) -> end. +is_usable(_, Selector, _) when Selector =:= {[]} -> + false; is_usable(Idx, Selector, _) -> case columns(Idx) of all_fields -> diff --git a/src/mango/src/mango_selector.erl b/src/mango/src/mango_selector.erl index fffadcd20..3ea83c220 100644 --- a/src/mango/src/mango_selector.erl +++ b/src/mango/src/mango_selector.erl @@ -52,15 +52,19 @@ normalize(Selector) -> % Match a selector against a #doc{} or EJSON value. % This assumes that the Selector has been normalized. % Returns true or false. +match(Selector, D) -> + couch_stats:increment_counter([mango, evaluate_selector]), + match_int(Selector, D). + % An empty selector matches any value. -match({[]}, _) -> +match_int({[]}, _) -> true; -match(Selector, #doc{body=Body}) -> +match_int(Selector, #doc{body=Body}) -> match(Selector, Body, fun mango_json:cmp/2); -match(Selector, {Props}) -> +match_int(Selector, {Props}) -> match(Selector, {Props}, fun mango_json:cmp/2). % Convert each operator into a normalized version as well @@ -399,10 +403,16 @@ negate({[{Field, Cond}]}) -> {[{Field, negate(Cond)}]}. +% We need to treat an empty array as always true. This will be applied +% for $or, $in, $all, $nin as well. +match({[{<<"$and">>, []}]}, _, _) -> + true; match({[{<<"$and">>, Args}]}, Value, Cmp) -> Pred = fun(SubSel) -> match(SubSel, Value, Cmp) end, lists:all(Pred, Args); +match({[{<<"$or">>, []}]}, _, _) -> + true; match({[{<<"$or">>, Args}]}, Value, Cmp) -> Pred = fun(SubSel) -> match(SubSel, Value, Cmp) end, lists:any(Pred, Args); @@ -410,6 +420,8 @@ match({[{<<"$or">>, Args}]}, Value, Cmp) -> match({[{<<"$not">>, Arg}]}, Value, Cmp) -> not match(Arg, Value, Cmp); +match({[{<<"$all">>, []}]}, _, _) -> + true; % All of the values in Args must exist in Values or % Values == hd(Args) if Args is a single element list % that contains a list. @@ -493,6 +505,8 @@ match({[{<<"$gte">>, Arg}]}, Value, Cmp) -> match({[{<<"$gt">>, Arg}]}, Value, Cmp) -> Cmp(Value, Arg) > 0; +match({[{<<"$in">>, []}]}, _, _) -> + true; match({[{<<"$in">>, Args}]}, Values, Cmp) when is_list(Values)-> Pred = fun(Arg) -> lists:foldl(fun(Value,Match) -> @@ -504,6 +518,8 @@ match({[{<<"$in">>, Args}]}, Value, Cmp) -> Pred = fun(Arg) -> Cmp(Value, Arg) == 0 end, lists:any(Pred, Args); +match({[{<<"$nin">>, []}]}, _, _) -> + true; match({[{<<"$nin">>, Args}]}, Values, Cmp) when is_list(Values)-> not match({[{<<"$in">>, Args}]}, Values, Cmp); match({[{<<"$nin">>, Args}]}, Value, Cmp) -> @@ -570,7 +586,7 @@ match({[_, _ | _] = _Props} = Sel, _Value, _Cmp) -> erlang:error({unnormalized_selector, Sel}). -% Returns true if Selector requires all +% Returns true if Selector requires all % fields in RequiredFields to exist in any matching documents. % For each condition in the selector, check @@ -600,13 +616,13 @@ has_required_fields_int(Selector, RequiredFields) when not is_list(Selector) -> % We can "see" through $and operator. Iterate % through the list of child operators. -has_required_fields_int([{[{<<"$and">>, Args}]}], RequiredFields) +has_required_fields_int([{[{<<"$and">>, Args}]}], RequiredFields) when is_list(Args) -> has_required_fields_int(Args, RequiredFields); % We can "see" through $or operator. Required fields % must be covered by all children. -has_required_fields_int([{[{<<"$or">>, Args}]} | Rest], RequiredFields) +has_required_fields_int([{[{<<"$or">>, Args}]} | Rest], RequiredFields) when is_list(Args) -> Remainder0 = lists:foldl(fun(Arg, Acc) -> % for each child test coverage against the full @@ -623,7 +639,7 @@ has_required_fields_int([{[{<<"$or">>, Args}]} | Rest], RequiredFields) % Handle $and operator where it has peers. Required fields % can be covered by any child. -has_required_fields_int([{[{<<"$and">>, Args}]} | Rest], RequiredFields) +has_required_fields_int([{[{<<"$and">>, Args}]} | Rest], RequiredFields) when is_list(Args) -> Remainder = has_required_fields_int(Args, RequiredFields), has_required_fields_int(Rest, Remainder); diff --git a/src/mango/src/mango_selector_text.erl b/src/mango/src/mango_selector_text.erl index cfa3baf6d..b3b61ff26 100644 --- a/src/mango/src/mango_selector_text.erl +++ b/src/mango/src/mango_selector_text.erl @@ -205,15 +205,36 @@ convert(_Path, {Props} = Sel) when length(Props) > 1 -> erlang:error({unnormalized_selector, Sel}). -to_query({op_and, Args}) when is_list(Args) -> +to_query_nested(Args) -> QueryArgs = lists:map(fun to_query/1, Args), - ["(", mango_util:join(<<" AND ">>, QueryArgs), ")"]; + % removes empty queries that result from selectors with empty arrays + FilterFun = fun(A) -> A =/= [] andalso A =/= "()" end, + lists:filter(FilterFun, QueryArgs). + + +to_query({op_and, []}) -> + []; + +to_query({op_and, Args}) when is_list(Args) -> + case to_query_nested(Args) of + [] -> []; + QueryArgs -> ["(", mango_util:join(<<" AND ">>, QueryArgs), ")"] + end; + +to_query({op_or, []}) -> + []; to_query({op_or, Args}) when is_list(Args) -> - ["(", mango_util:join(" OR ", lists:map(fun to_query/1, Args)), ")"]; + case to_query_nested(Args) of + [] -> []; + QueryArgs -> ["(", mango_util:join(" OR ", QueryArgs), ")"] + end; to_query({op_not, {ExistsQuery, Arg}}) when is_tuple(Arg) -> - ["(", to_query(ExistsQuery), " AND NOT (", to_query(Arg), "))"]; + case to_query(Arg) of + [] -> ["(", to_query(ExistsQuery), ")"]; + Query -> ["(", to_query(ExistsQuery), " AND NOT (", Query, "))"] + end; %% For $exists:false to_query({op_not, {ExistsQuery, false}}) -> @@ -345,7 +366,8 @@ value_str(Value) when is_binary(Value) -> true -> <<"\"", Value/binary, "\"">>; false -> - mango_util:lucene_escape_query_value(Value) + Escaped = mango_util:lucene_escape_query_value(Value), + <<"\"", Escaped/binary, "\"">> end; value_str(Value) when is_integer(Value) -> list_to_binary(integer_to_list(Value)); diff --git a/src/mango/src/mango_util.erl b/src/mango/src/mango_util.erl index a7347178e..0d31f15f9 100644 --- a/src/mango/src/mango_util.erl +++ b/src/mango/src/mango_util.erl @@ -344,6 +344,8 @@ has_suffix(Bin, Suffix) when is_binary(Bin), is_binary(Suffix) -> end. +join(_Sep, []) -> + []; join(_Sep, [Item]) -> [Item]; join(Sep, [Item | Rest]) -> diff --git a/src/mango/test/02-basic-find-test.py b/src/mango/test/02-basic-find-test.py index 0fc4248a8..afdba03a2 100644 --- a/src/mango/test/02-basic-find-test.py +++ b/src/mango/test/02-basic-find-test.py @@ -13,6 +13,7 @@ import mango +import user_docs class BasicFindTests(mango.UserDocsTests): diff --git a/src/mango/test/05-index-selection-test.py b/src/mango/test/05-index-selection-test.py index 3f7fb9f21..271e36176 100644 --- a/src/mango/test/05-index-selection-test.py +++ b/src/mango/test/05-index-selection-test.py @@ -84,7 +84,7 @@ class IndexSelectionTests: ddocid = "_design/age" r = self.db.find({}, use_index=ddocid, return_raw=True) self.assertEqual( - r["warning"], + r["warning"][0].lower(), "{0} was not used because it does not contain a valid index for this query.".format( ddocid ), @@ -107,7 +107,7 @@ class IndexSelectionTests: selector = {"company": "Pharmex"} r = self.db.find(selector, use_index=ddocid, return_raw=True) self.assertEqual( - r["warning"], + r["warning"][0].lower(), "{0} was not used because it does not contain a valid index for this query.".format( ddocid ), @@ -124,7 +124,7 @@ class IndexSelectionTests: resp = self.db.find(selector, use_index=[ddocid, name], return_raw=True) self.assertEqual( - resp["warning"], + resp["warning"][0].lower(), "{0}, {1} was not used because it is not a valid index for this query.".format( ddocid, name ), @@ -162,7 +162,7 @@ class IndexSelectionTests: selector, sort=["foo", "bar"], use_index=ddocid_invalid, return_raw=True ) self.assertEqual( - resp["warning"], + resp["warning"][0].lower(), "{0} was not used because it does not contain a valid index for this query.".format( ddocid_invalid ), diff --git a/src/mango/test/06-basic-text-test.py b/src/mango/test/06-basic-text-test.py index db7cf32cb..a3fe383d6 100644 --- a/src/mango/test/06-basic-text-test.py +++ b/src/mango/test/06-basic-text-test.py @@ -95,6 +95,9 @@ class BasicTextTests(mango.UserDocsTextTests): assert len(docs) == 1 assert docs[0]["company"] == "Affluex" + docs = self.db.find({"foo": {"$lt": "bar car apple"}}) + assert len(docs) == 0 + def test_lte(self): docs = self.db.find({"age": {"$lte": 21}}) assert len(docs) == 0 @@ -113,6 +116,10 @@ class BasicTextTests(mango.UserDocsTextTests): for d in docs: assert d["user_id"] in (0, 11) + docs = self.db.find({"foo": {"$lte": "bar car apple"}}) + assert len(docs) == 1 + assert docs[0]["user_id"] == 14 + def test_eq(self): docs = self.db.find({"age": 21}) assert len(docs) == 0 @@ -156,6 +163,13 @@ class BasicTextTests(mango.UserDocsTextTests): docs = self.db.find({"company": {"$gt": "Zialactic"}}) assert len(docs) == 0 + docs = self.db.find({"foo": {"$gt": "bar car apple"}}) + assert len(docs) == 0 + + docs = self.db.find({"foo": {"$gt": "bar car"}}) + assert len(docs) == 1 + assert docs[0]["user_id"] == 14 + def test_gte(self): docs = self.db.find({"age": {"$gte": 77}}) assert len(docs) == 2 @@ -178,6 +192,10 @@ class BasicTextTests(mango.UserDocsTextTests): assert len(docs) == 1 assert docs[0]["company"] == "Zialactic" + docs = self.db.find({"foo": {"$gte": "bar car apple"}}) + assert len(docs) == 1 + assert docs[0]["user_id"] == 14 + def test_and(self): docs = self.db.find({"age": 22, "manager": True}) assert len(docs) == 1 diff --git a/src/mango/test/12-use-correct-index-test.py b/src/mango/test/12-use-correct-index-test.py index 2de88a21a..3a2f60af8 100644 --- a/src/mango/test/12-use-correct-index-test.py +++ b/src/mango/test/12-use-correct-index-test.py @@ -93,8 +93,8 @@ class ChooseCorrectIndexForDocs(mango.DbPerClass): 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", + resp["warning"][0].lower(), + "no matching index found, create an index to optimize query time.", ) def test_chooses_idxA(self): diff --git a/src/mango/test/15-execution-stats-test.py b/src/mango/test/15-execution-stats-test.py index d3687f8c8..537a19add 100644 --- a/src/mango/test/15-execution-stats-test.py +++ b/src/mango/test/15-execution-stats-test.py @@ -54,10 +54,13 @@ class ExecutionStatsTests(mango.UserDocsTests): self.assertEqual(resp["execution_stats"]["results_returned"], len(resp["docs"])) def test_no_matches_index_scan(self): - resp = self.db.find({"age": {"$lt": 35}, "nomatch": "me"}, return_raw=True, executionStats=True) + resp = self.db.find( + {"age": {"$lt": 35}, "nomatch": "me"}, return_raw=True, executionStats=True + ) self.assertEqual(resp["execution_stats"]["total_docs_examined"], 3) self.assertEqual(resp["execution_stats"]["results_returned"], 0) + @unittest.skipUnless(mango.has_text_service(), "requires text service") class ExecutionStatsTests_Text(mango.UserDocsTextTests): def test_simple_text_index(self): diff --git a/src/mango/test/21-empty-selector-tests.py b/src/mango/test/21-empty-selector-tests.py new file mode 100644 index 000000000..beb222c85 --- /dev/null +++ b/src/mango/test/21-empty-selector-tests.py @@ -0,0 +1,73 @@ +# 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 json +import mango +import unittest +import user_docs +import math + + +def make_empty_selector_suite(klass): + class EmptySelectorTestCase(klass): + def test_empty(self): + resp = self.db.find({}, explain=True) + self.assertEqual(resp["index"]["type"], "special") + + def test_empty_array_or(self): + resp = self.db.find({"$or": []}, explain=True) + self.assertEqual(resp["index"]["type"], klass.INDEX_TYPE) + docs = self.db.find({"$or": []}) + assert len(docs) == 0 + + def test_empty_array_or_with_age(self): + resp = self.db.find({"age": 22, "$or": []}, explain=True) + self.assertEqual(resp["index"]["type"], klass.INDEX_TYPE) + docs = self.db.find({"age": 22, "$or": []}) + assert len(docs) == 1 + + def test_empty_array_and_with_age(self): + resp = self.db.find( + {"age": 22, "$and": [{"b": {"$all": []}}]}, explain=True + ) + self.assertEqual(resp["index"]["type"], klass.INDEX_TYPE) + docs = self.db.find({"age": 22, "$and": []}) + assert len(docs) == 1 + + def test_empty_arrays_complex(self): + resp = self.db.find({"$or": [], "a": {"$in": []}}, explain=True) + self.assertEqual(resp["index"]["type"], klass.INDEX_TYPE) + docs = self.db.find({"$or": [], "a": {"$in": []}}) + assert len(docs) == 0 + + def test_empty_nin(self): + resp = self.db.find({"favorites": {"$nin": []}}, explain=True) + self.assertEqual(resp["index"]["type"], klass.INDEX_TYPE) + docs = self.db.find({"favorites": {"$nin": []}}) + assert len(docs) == len(user_docs.DOCS) + + return EmptySelectorTestCase + + +class EmptySelectorNoIndexTests( + make_empty_selector_suite(mango.UserDocsTestsNoIndexes) +): + pass + + +@unittest.skipUnless(mango.has_text_service(), "requires text service") +class EmptySelectorTextTests(make_empty_selector_suite(mango.UserDocsTextTests)): + pass + + +class EmptySelectorUserDocTests(make_empty_selector_suite(mango.UserDocsTests)): + pass diff --git a/src/mango/test/mango.py b/src/mango/test/mango.py index de8a638a8..03cb85f48 100644 --- a/src/mango/test/mango.py +++ b/src/mango/test/mango.py @@ -314,6 +314,8 @@ class DbPerClass(unittest.TestCase): class UserDocsTests(DbPerClass): + INDEX_TYPE = "json" + @classmethod def setUpClass(klass): super(UserDocsTests, klass).setUpClass() @@ -321,14 +323,16 @@ class UserDocsTests(DbPerClass): class UserDocsTestsNoIndexes(DbPerClass): + INDEX_TYPE = "special" + @classmethod def setUpClass(klass): super(UserDocsTestsNoIndexes, klass).setUpClass() - user_docs.setup(klass.db, index_type="_all_docs") + user_docs.setup(klass.db, index_type=klass.INDEX_TYPE) class UserDocsTextTests(DbPerClass): - + INDEX_TYPE = "text" DEFAULT_FIELD = None FIELDS = None @@ -338,7 +342,7 @@ class UserDocsTextTests(DbPerClass): if has_text_service(): user_docs.setup( klass.db, - index_type="text", + index_type=klass.INDEX_TYPE, default_field=klass.DEFAULT_FIELD, fields=klass.FIELDS, ) diff --git a/src/mango/test/user_docs.py b/src/mango/test/user_docs.py index e0495353b..f6a33960e 100644 --- a/src/mango/test/user_docs.py +++ b/src/mango/test/user_docs.py @@ -343,6 +343,7 @@ DOCS = [ "city": "Axis", "address": {"street": "Brightwater Avenue", "number": 1106}, }, + "foo" : "bar car apple", "company": "Pharmex", "email": "faithhess@pharmex.com", "favorites": ["Erlang", "Python", "Lisp"], diff --git a/src/mem3/rebar.config.script b/src/mem3/rebar.config.script new file mode 100644 index 000000000..8f2deb4ae --- /dev/null +++ b/src/mem3/rebar.config.script @@ -0,0 +1,22 @@ +%% 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. + +WithProper = code:lib_dir(proper) /= {error, bad_name}. + +if not WithProper -> CONFIG; true -> + CurrOpts = case lists:keyfind(erl_opts, 1, CONFIG) of + {erl_opts, Opts} -> Opts; + false -> [] + end, + NewOpts = [{d, 'WITH_PROPER'} | CurrOpts], + lists:keystore(erl_opts, 1, CONFIG, {erl_opts, NewOpts}) +end. diff --git a/src/mem3/test/eunit/mem3_ring_prop_tests.erl b/src/mem3/test/eunit/mem3_ring_prop_tests.erl index 9f4f86f5f..51d8f10bf 100644 --- a/src/mem3/test/eunit/mem3_ring_prop_tests.erl +++ b/src/mem3/test/eunit/mem3_ring_prop_tests.erl @@ -13,8 +13,13 @@ -module(mem3_ring_prop_tests). --include_lib("triq/include/triq.hrl"). --triq(eunit). +-ifdef(WITH_PROPER). + +-include_lib("couch/include/couch_eunit_proper.hrl"). + + +property_test_() -> + ?EUNIT_QUICKCHECK(60). % Properties @@ -97,7 +102,7 @@ g_disconnected_intervals(Begin, End) -> g_disconnected_intervals(Begin, End, Split) when Begin =< End -> ?LET(Connected, g_non_trivial_connected_intervals(Begin, End, Split), begin - I = triq_rnd:uniform(length(Connected)) - 1, + I = rand:uniform(length(Connected)) - 1, {Before, [_ | After]} = lists:split(I, Connected), Before ++ After end). @@ -131,14 +136,16 @@ rand_range(B, B) -> B; rand_range(B, E) -> - B + triq_rnd:uniform(E - B). + B + rand:uniform(E - B). shuffle(L) -> - Tagged = [{triq_rnd:uniform(), X} || X <- L], + Tagged = [{rand:uniform(), X} || X <- L], [X || {_, X} <- lists:sort(Tagged)]. endpoints(Ranges) -> {Begins, Ends} = lists:unzip(Ranges), sets:from_list(Begins ++ Ends). + +-endif. diff --git a/test/elixir/README.md b/test/elixir/README.md index ef95e5f61..90b2fd601 100644 --- a/test/elixir/README.md +++ b/test/elixir/README.md @@ -46,8 +46,8 @@ X means done, - means partially - [ ] Port design_options.js - [ ] Port design_paths.js - [X] Port erlang_views.js - - [ ] Port etags_head.js - - [ ] Port etags_views.js + - [X] Port etags_head.js + - [ ] ~~Port etags_views.js~~ (skipped in js test suite) - [ ] Port form_submit.js - [ ] Port http.js - [X] Port invalid_docids.js diff --git a/test/elixir/lib/couch.ex b/test/elixir/lib/couch.ex index 6a63dffb0..3aef07f01 100644 --- a/test/elixir/lib/couch.ex +++ b/test/elixir/lib/couch.ex @@ -97,9 +97,9 @@ defmodule Couch do def process_options(options) do options - |> set_auth_options() - |> set_inactivity_timeout() - |> set_request_timeout() + |> set_auth_options() + |> set_inactivity_timeout() + |> set_request_timeout() end def process_request_body(body) do @@ -110,6 +110,10 @@ defmodule Couch do end end + def process_response_body(_headers, body) when body == [] do + "" + end + def process_response_body(headers, body) do content_type = headers[:"Content-Type"] @@ -137,9 +141,14 @@ defmodule Couch do end def set_inactivity_timeout(options) do - Keyword.update(options, :ibrowse, [{:inactivity_timeout, @inactivity_timeout}], fn(ibrowse) -> - Keyword.put_new(ibrowse, :inactivity_timeout, @inactivity_timeout) - end) + Keyword.update( + options, + :ibrowse, + [{:inactivity_timeout, @inactivity_timeout}], + fn ibrowse -> + Keyword.put_new(ibrowse, :inactivity_timeout, @inactivity_timeout) + end + ) end def set_request_timeout(options) do @@ -165,5 +174,4 @@ defmodule Couch do %Couch.Session{error: resp.body["error"]} end end - end diff --git a/test/elixir/test/config_test.exs b/test/elixir/test/config_test.exs index 2b2d71414..53c5bc82e 100644 --- a/test/elixir/test/config_test.exs +++ b/test/elixir/test/config_test.exs @@ -174,4 +174,11 @@ defmodule ConfigTest do set_config(context, section, "wohali", "rules", 403) end) end + + test "Reload config", context do + url = "#{context[:config_url]}/_reload" + resp = Couch.post(url) + + assert resp.status_code == 200 + end end diff --git a/test/elixir/test/etags_head_test.exs b/test/elixir/test/etags_head_test.exs new file mode 100644 index 000000000..9b9ff8bb0 --- /dev/null +++ b/test/elixir/test/etags_head_test.exs @@ -0,0 +1,151 @@ +defmodule EtagsHeadTest do + use CouchTestCase + + @moduletag :etags + + @tag :with_db + test "etag header on creation", context do + db_name = context[:db_name] + + resp = + Couch.put("/#{db_name}/1", + headers: ["Content-Type": "application/json"], + body: %{} + ) + + assert resp.status_code == 201 + assert Map.has_key?(resp.headers.hdrs, "etag") + end + + @tag :with_db + test "etag header on retrieval", context do + db_name = context[:db_name] + + resp = + Couch.put("/#{db_name}/1", + headers: ["Content-Type": "application/json"], + body: %{} + ) + + etag = resp.headers.hdrs["etag"] + + # get the doc and verify the headers match + resp = Couch.get("/#{db_name}/1") + assert etag == resp.headers.hdrs["etag"] + + # 'head' the doc and verify the headers match + resp = + Couch.head("/#{db_name}/1", + headers: ["if-none-match": "s"] + ) + + assert etag == resp.headers.hdrs["etag"] + end + + @tag :with_db + test "etag header on head", context do + db_name = context[:db_name] + + resp = + Couch.put("/#{db_name}/1", + headers: ["Content-Type": "application/json"], + body: %{} + ) + + etag = resp.headers.hdrs["etag"] + + # 'head' the doc and verify the headers match + resp = + Couch.head("/#{db_name}/1", + headers: ["if-none-match": "s"] + ) + + assert etag == resp.headers.hdrs["etag"] + end + + @tag :with_db + test "etags head", context do + db_name = context[:db_name] + + resp = + Couch.put("/#{db_name}/1", + headers: ["Content-Type": "application/json"], + body: %{} + ) + + assert resp.status_code == 201 + assert Map.has_key?(resp.headers.hdrs, "etag") + + etag = resp.headers.hdrs["etag"] + + # get the doc and verify the headers match + resp = Couch.get("/#{db_name}/1") + assert etag == resp.headers.hdrs["etag"] + + # 'head' the doc and verify the headers match + resp = + Couch.head("/#{db_name}/1", + headers: ["if-none-match": "s"] + ) + + assert etag == resp.headers.hdrs["etag"] + + # replace a doc + resp = + Couch.put("/#{db_name}/1", + headers: ["if-match": etag], + body: %{} + ) + + assert resp.status_code == 201 + + # extract the new ETag value + previous_etag = etag + etag = resp.headers.hdrs["etag"] + + # fail to replace a doc + resp = + Couch.put("/#{db_name}/1", + body: %{} + ) + + assert resp.status_code == 409 + + # verify get w/Etag + resp = + Couch.get("/#{db_name}/1", + headers: ["if-none-match": previous_etag] + ) + + assert resp.status_code == 200 + + resp = + Couch.get("/#{db_name}/1", + headers: ["if-none-match": etag] + ) + + assert resp.status_code == 304 + + resp = + Couch.get("/#{db_name}/1", + headers: ["if-none-match": "W/#{etag}"] + ) + + assert resp.status_code == 304 + + # fail to delete a doc + resp = + Couch.delete("/#{db_name}/1", + headers: ["if-match": previous_etag] + ) + + assert resp.status_code == 409 + + resp = + Couch.delete("/#{db_name}/1", + headers: ["if-match": etag] + ) + + assert resp.status_code == 200 + end +end diff --git a/test/javascript/tests/etags_head.js b/test/javascript/tests/etags_head.js index 9faca4af6..678479004 100644 --- a/test/javascript/tests/etags_head.js +++ b/test/javascript/tests/etags_head.js @@ -10,7 +10,9 @@ // License for the specific language governing permissions and limitations under // the License. +couchTests.elixir = true; couchTests.etags_head = function(debug) { + return console.log('done in test/elixir/test/etags_head_test.exs'); var db_name = get_random_db_name(); var db = new CouchDB(db_name, {"X-Couch-Full-Commit":"false"}); db.createDb(); |