summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoan Touzet <wohali@users.noreply.github.com>2020-01-28 14:43:19 -0500
committerGitHub <noreply@github.com>2020-01-28 14:43:19 -0500
commitbdd3769b7fe137a3b64308ed92241847e59520af (patch)
treebee77b5868c4e64cc84600451772612cb0c32a44
parent82bd2d3e8f33efbf1eacc7ae7db9e4c704fd3ae6 (diff)
parent2aeae473d3fcf338097f5f2d46502ae0e866bc55 (diff)
downloadcouchdb-ini-singlenode-false.tar.gz
Merge branch 'master' into ini-singlenode-falseini-singlenode-false
-rw-r--r--.gitignore1
-rw-r--r--build-aux/Jenkinsfile.full70
-rw-r--r--build-aux/Jenkinsfile.pr10
-rwxr-xr-xbuild-aux/show-test-results.py2
-rw-r--r--mix.exs1
-rw-r--r--rebar.config.script8
-rw-r--r--rel/overlay/etc/default.ini29
-rw-r--r--src/chttpd/src/chttpd_auth.erl8
-rw-r--r--src/chttpd/src/chttpd_node.erl10
-rw-r--r--src/couch/include/couch_eunit_proper.hrl29
-rw-r--r--src/couch/priv/stats_descriptions.cfg28
-rw-r--r--src/couch/rebar.config.script7
-rw-r--r--src/couch/src/couch_httpd_auth.erl5
-rw-r--r--src/couch/src/couch_sup.erl2
-rw-r--r--src/couch/src/couch_util.erl2
-rw-r--r--src/couch/test/eunit/couch_key_tree_prop_tests.erl40
-rw-r--r--src/dreyfus/src/dreyfus_fabric_search.erl1
-rw-r--r--src/mango/src/mango_cursor.erl139
-rw-r--r--src/mango/src/mango_cursor_special.erl7
-rw-r--r--src/mango/src/mango_cursor_text.erl17
-rw-r--r--src/mango/src/mango_cursor_view.erl20
-rw-r--r--src/mango/src/mango_execution_stats.erl9
-rw-r--r--src/mango/src/mango_idx_text.erl2
-rw-r--r--src/mango/src/mango_selector.erl30
-rw-r--r--src/mango/src/mango_selector_text.erl32
-rw-r--r--src/mango/src/mango_util.erl2
-rw-r--r--src/mango/test/02-basic-find-test.py1
-rw-r--r--src/mango/test/05-index-selection-test.py8
-rw-r--r--src/mango/test/06-basic-text-test.py18
-rw-r--r--src/mango/test/12-use-correct-index-test.py4
-rw-r--r--src/mango/test/15-execution-stats-test.py5
-rw-r--r--src/mango/test/21-empty-selector-tests.py73
-rw-r--r--src/mango/test/mango.py10
-rw-r--r--src/mango/test/user_docs.py1
-rw-r--r--src/mem3/rebar.config.script22
-rw-r--r--src/mem3/test/eunit/mem3_ring_prop_tests.erl17
-rw-r--r--test/elixir/README.md4
-rw-r--r--test/elixir/lib/couch.ex22
-rw-r--r--test/elixir/test/config_test.exs7
-rw-r--r--test/elixir/test/etags_head_test.exs151
-rw-r--r--test/javascript/tests/etags_head.js2
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",
}
diff --git a/mix.exs b/mix.exs
index 4432b1c83..d717e4b4a 100644
--- a/mix.exs
+++ b/mix.exs
@@ -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();