From eff6867483b16fc1ce71038f0c085566fb56325a Mon Sep 17 00:00:00 2001 From: reindexer-bot <@> Date: Fri, 18 Aug 2023 23:14:06 +0000 Subject: [PATCH] Merge branch 'qa/fix_proto_tests' into 'develop' #1523 fix connect to running server See merge request itv-backend/reindexer!1389 --- .../test/test_storage_compatibility.sh | 194 ++++++++ cpp_src/core/expressiontree.md | 31 +- cpp_src/core/index/indexstore.cc | 35 ++ cpp_src/core/nsselecter/explaincalc.cc | 222 ++++++--- cpp_src/core/nsselecter/explaincalc.h | 56 ++- cpp_src/core/nsselecter/joinedselector.cc | 4 +- cpp_src/core/nsselecter/nsselecter.cc | 15 +- cpp_src/core/nsselecter/nsselecter.h | 1 + cpp_src/core/nsselecter/querypreprocessor.cc | 252 ++++++++++- cpp_src/core/nsselecter/querypreprocessor.h | 18 +- cpp_src/core/nsselecter/selectiterator.cc | 5 +- cpp_src/core/nsselecter/selectiterator.h | 4 +- .../nsselecter/selectiteratorcontainer.cc | 33 +- cpp_src/core/query/dsl/dslencoder.cc | 1 + cpp_src/core/query/dsl/dslencoder.h | 3 +- cpp_src/core/query/dsl/dslparser.cc | 2 +- cpp_src/core/query/queryentry.cc | 55 +++ cpp_src/core/query/queryentry.h | 48 +- cpp_src/core/query/sql/sqlencoder.cc | 20 +- cpp_src/core/query/sql/sqlencoder.h | 5 - cpp_src/core/reindexerimpl.cc | 6 +- cpp_src/core/type_consts_helpers.cc | 18 + cpp_src/core/type_consts_helpers.h | 18 +- cpp_src/gtests/tests/fixtures/queries_api.h | 60 ++- cpp_src/server/contrib/server.yml | 67 ++- cpp_src/server/rpcserver.cc | 3 +- iterator.go | 46 ++ test/composite_indexes_test.go | 425 ++++++++++-------- test/eq_and_set_test.go | 39 +- test/huge_items_test.go | 73 +-- test/join_test.go | 114 +++++ test/uuid_test.go | 9 +- 32 files changed, 1440 insertions(+), 442 deletions(-) create mode 100644 cpp_src/cmd/reindexer_server/test/test_storage_compatibility.sh diff --git a/cpp_src/cmd/reindexer_server/test/test_storage_compatibility.sh b/cpp_src/cmd/reindexer_server/test/test_storage_compatibility.sh new file mode 100644 index 000000000..2c014c6a5 --- /dev/null +++ b/cpp_src/cmd/reindexer_server/test/test_storage_compatibility.sh @@ -0,0 +1,194 @@ +#!/bin/bash +# Task: https://github.com/restream/reindexer/-/issues/1188 +set -e + +function KillAndRemoveServer { + local pid=$1 + kill $pid + wait $pid + yum remove -y 'reindexer*' > /dev/null +} + +function WaitForDB { + # wait until DB is loaded + set +e # disable "exit on error" so the script won't stop when DB's not loaded yet + is_connected=$(reindexer_tool --dsn $ADDRESS --command '\databases list'); + while [[ $is_connected != "test" ]] + do + sleep 2 + is_connected=$(reindexer_tool --dsn $ADDRESS --command '\databases list'); + done + set -e +} + +function CompareNamespacesLists { + local ns_list_actual=$1 + local ns_list_expected=$2 + local pid=$3 + + diff=$(echo ${ns_list_actual[@]} ${ns_list_expected[@]} | tr ' ' '\n' | sort | uniq -u) # compare in any order + if [ "$diff" == "" ]; then + echo "## PASS: namespaces list not changed" + else + echo "##### FAIL: namespaces list was changed" + echo "expected: $ns_list_expected" + echo "actual: $ns_list_actual" + KillAndRemoveServer $pid; + exit 1 + fi +} + +function CompareMemstats { + local actual=$1 + local expected=$2 + local pid=$3 + diff=$(echo ${actual[@]} ${expected[@]} | tr ' ' '\n' | sed 's/\(.*\),$/\1/' | sort | uniq -u) # compare in any order + if [ "$diff" == "" ]; then + echo "## PASS: memstats not changed" + else + echo "##### FAIL: memstats was changed" + echo "expected: $expected" + echo "actual: $actual" + KillAndRemoveServer $pid; + exit 1 + fi +} + + +RX_SERVER_CURRENT_VERSION_RPM="$(basename build/reindexer-*server*.rpm)" +VERSION_FROM_RPM=$(echo "$RX_SERVER_CURRENT_VERSION_RPM" | grep -o '.*server-..') +VERSION=$(echo ${VERSION_FROM_RPM: -2:1}) # one-digit version + +if [ $VERSION == 3 ]; then + LATEST_RELEASE=$(curl -s http://repo.restream.ru/itv-api-ng/7/x86_64/ | grep -o '>reindexer-server-.*.rpm' | tail -n1 | cut -c 2-) + namespaces_list_expected=$'purchase_options_ext_dict\nchild_account_recommendations\n#config\n#activitystats\nradio_channels\ncollections\n#namespaces\nwp_imports_tasks\nepg_genres\nrecom_media_items_personal\nrecom_epg_archive_default\n#perfstats\nrecom_epg_live_default\nmedia_view_templates\nasset_video_servers\nwp_tasks_schedule\nadmin_roles\n#clientsstats\nrecom_epg_archive_personal\nrecom_media_items_similars\nmenu_items\naccount_recommendations\nkaraoke_items\nmedia_items\nbanners\n#queriesperfstats\nrecom_media_items_default\nrecom_epg_live_personal\nservices\n#memstats\nchannels\nmedia_item_recommendations\nwp_tasks_tasks\nepg' +elif [ $VERSION == 4 ]; then + LATEST_RELEASE=$(curl -s http://repo.restream.ru/itv-api-ng/7/x86_64/ | grep -o '>reindexer-4-server-.*.rpm' | tail -n1 | cut -c 2-) + # replicationstats ns added for v4 + namespaces_list_expected=$'purchase_options_ext_dict\nchild_account_recommendations\n#config\n#activitystats\n#replicationstats\nradio_channels\ncollections\n#namespaces\nwp_imports_tasks\nepg_genres\nrecom_media_items_personal\nrecom_epg_archive_default\n#perfstats\nrecom_epg_live_default\nmedia_view_templates\nasset_video_servers\nwp_tasks_schedule\nadmin_roles\n#clientsstats\nrecom_epg_archive_personal\nrecom_media_items_similars\nmenu_items\naccount_recommendations\nkaraoke_items\nmedia_items\nbanners\n#queriesperfstats\nrecom_media_items_default\nrecom_epg_live_personal\nservices\n#memstats\nchannels\nmedia_item_recommendations\nwp_tasks_tasks\nepg' +else + echo "Unknown version" + exit 1 +fi + +echo "## downloading latest release rpm file: $LATEST_RELEASE" +curl "http://repo.itv.restr.im/itv-api-ng/7/x86_64/$LATEST_RELEASE" --output $LATEST_RELEASE; +echo "## downloading example DB" +curl "https://git.restream.ru/MaksimKravchuk/reindexer_testdata/-/raw/master/big.zip" --output big.zip; +unzip -o big.zip # unzips into mydb_big.rxdump; + +ADDRESS="cproto://127.0.0.1:6534/" +DB_NAME="test" + +memstats_expected=$'[ +{"replication":{"data_hash":24651210926,"data_count":3}}, +{"replication":{"data_hash":6252344969,"data_count":1}}, +{"replication":{"data_hash":37734732881,"data_count":28}}, +{"replication":{"data_hash":0,"data_count":0}}, +{"replication":{"data_hash":1024095024522,"data_count":1145}}, +{"replication":{"data_hash":8373644068,"data_count":1315}}, +{"replication":{"data_hash":0,"data_count":0}}, +{"replication":{"data_hash":0,"data_count":0}}, +{"replication":{"data_hash":0,"data_count":0}}, +{"replication":{"data_hash":0,"data_count":0}}, +{"replication":{"data_hash":7404222244,"data_count":97}}, +{"replication":{"data_hash":94132837196,"data_count":4}}, +{"replication":{"data_hash":1896088071,"data_count":2}}, +{"replication":{"data_hash":0,"data_count":0}}, +{"replication":{"data_hash":-672103903,"data_count":33538}}, +{"replication":{"data_hash":0,"data_count":0}}, +{"replication":{"data_hash":6833710705,"data_count":1}}, +{"replication":{"data_hash":5858155773472,"data_count":4500}}, +{"replication":{"data_hash":-473221280268823592,"data_count":65448}}, +{"replication":{"data_hash":0,"data_count":0}}, +{"replication":{"data_hash":8288213744,"data_count":3}}, +{"replication":{"data_hash":0,"data_count":0}}, +{"replication":{"data_hash":0,"data_count":0}}, +{"replication":{"data_hash":354171024786967,"data_count":3941}}, +{"replication":{"data_hash":-6520334670,"data_count":35886}}, +{"replication":{"data_hash":112772074632,"data_count":281}}, +{"replication":{"data_hash":-12679568198538,"data_count":1623116}} +] +Returned 27 rows' + +echo "##### Forward compatibility test #####" + +DB_PATH=$(pwd)"/rx_db" + +echo "Database: "$DB_PATH + +echo "## installing latest release: $LATEST_RELEASE" +yum install -y $LATEST_RELEASE > /dev/null; +# run RX server with disabled logging +reindexer_server -l warning --httplog=none --rpclog=none --db $DB_PATH & +server_pid=$! +sleep 2; + +reindexer_tool --dsn $ADDRESS$DB_NAME -f mydb_big.rxdump --createdb; +sleep 1; + +namespaces_1=$(reindexer_tool --dsn $ADDRESS$DB_NAME --command '\namespaces list'); +echo $namespaces_1; +CompareNamespacesLists "${namespaces_1[@]}" "${namespaces_list_expected[@]}" $server_pid; + +memstats_1=$(reindexer_tool --dsn $ADDRESS$DB_NAME --command 'select replication.data_hash, replication.data_count from #memstats'); +CompareMemstats "${memstats_1[@]}" "${memstats_expected[@]}" $server_pid; + +KillAndRemoveServer $server_pid; + +echo "## installing current version: $RX_SERVER_CURRENT_VERSION_RPM" +yum install -y build/*.rpm > /dev/null; +reindexer_server -l0 --corelog=none --httplog=none --rpclog=none --db $DB_PATH & +server_pid=$! +sleep 2; + +WaitForDB + +namespaces_2=$(reindexer_tool --dsn $ADDRESS$DB_NAME --command '\namespaces list'); +echo $namespaces_2; +CompareNamespacesLists "${namespaces_2[@]}" "${namespaces_1[@]}" $server_pid; + +memstats_2=$(reindexer_tool --dsn $ADDRESS$DB_NAME --command 'select replication.data_hash, replication.data_count from #memstats'); +CompareMemstats "${memstats_2[@]}" "${memstats_1[@]}" $server_pid; + +KillAndRemoveServer $server_pid; +rm -rf $DB_PATH; +sleep 1; + +echo "##### Backward compatibility test #####" + +echo "## installing current version: $RX_SERVER_CURRENT_VERSION_RPM" +yum install -y build/*.rpm > /dev/null; +reindexer_server -l warning --httplog=none --rpclog=none --db $DB_PATH & +server_pid=$! +sleep 2; + +reindexer_tool --dsn $ADDRESS$DB_NAME -f mydb_big.rxdump --createdb; +sleep 1; + +namespaces_3=$(reindexer_tool --dsn $ADDRESS$DB_NAME --command '\namespaces list'); +echo $namespaces_3; +CompareNamespacesLists "${namespaces_3[@]}" "${namespaces_list_expected[@]}" $server_pid; + +memstats_3=$(reindexer_tool --dsn $ADDRESS$DB_NAME --command 'select replication.data_hash, replication.data_count from #memstats'); +CompareMemstats "${memstats_3[@]}" "${memstats_expected[@]}" $server_pid; + +KillAndRemoveServer $server_pid; + +echo "## installing latest release: $LATEST_RELEASE" +yum install -y $LATEST_RELEASE > /dev/null; +reindexer_server -l warning --httplog=none --rpclog=none --db $DB_PATH & +server_pid=$! +sleep 2; + +WaitForDB + +namespaces_4=$(reindexer_tool --dsn $ADDRESS$DB_NAME --command '\namespaces list'); +echo $namespaces_4; +CompareNamespacesLists "${namespaces_4[@]}" "${namespaces_3[@]}" $server_pid; + +memstats_4=$(reindexer_tool --dsn $ADDRESS$DB_NAME --command 'select replication.data_hash, replication.data_count from #memstats'); +CompareMemstats "${memstats_4[@]}" "${memstats_3[@]}" $server_pid; + +KillAndRemoveServer $server_pid; +rm -rf $DB_PATH; diff --git a/cpp_src/core/expressiontree.md b/cpp_src/core/expressiontree.md index 1700e7782..772fdea75 100644 --- a/cpp_src/core/expressiontree.md +++ b/cpp_src/core/expressiontree.md @@ -19,9 +19,12 @@ class ExpressionTree; `ExpressionTree` does not support operator precedence. You can support it manually as it done in `QueryEntries` and `SelectIteratorContainer`, or by enclosing higher priority operators in brackets as it done in `SortExpression`. -Here do not used traditional way for constructing of trees with inheritance of nodes, allocations of separate nodes and holding of pointers to they. +Here is not used the traditional way for constructing trees with inheritance of nodes, allocations of separate nodes and holding pointers to them. `ExpressionTree` holds all nodes by value in a vector (`container_`) sequentially in type `Node` based on `variant`. -In order to support lazy copying `Node` can hold a reference to payload of another `Node` by using `ExpressionTree::Ref` type. !Warning! lazy copy should not live over the original one. +In order to support lazy copying `Node` can hold a reference to payload of another `Node` by using `ExpressionTree::Ref` type. + +**Warning**: The lazy copy node shall not live longer than the original one. + Subtree is stored in `container_` just behind its head (`SubTree`) which holds occupied space. For details see examples. This architecture allows to reduce count of allocations and virtual functions calls. @@ -187,21 +190,23 @@ It contains operation (value of `OperationType`) and a value of one of the types - `void Node::Append()` increments size of subexpression if it is head of subexpression, fails otherwise. - `void Node::Erase(size_t)` reduces size of subexpression if it is head of subexpression, fails otherwise. - ```c++ -template -void Node::ExecuteAppropriate(const std::function&... funcs); -template -void Node::ExecuteAppropriate(const std::function&... funcs) const; -``` -invoke appropriate functor if the `Node` holds value of one of `Args...` types or `Ref` where `T` is one of `Args...` types, no functor will be invoked otherwise. + template + void Node::ExecuteAppropriate(const std::function&... funcs); + template + void Node::ExecuteAppropriate(const std::function&... funcs) const; + ``` + + invokes appropriate functor if the `Node` holds value of one of `Args...` types or `Ref`, where `T` is one of `Args...` types, no functor will be invoked otherwise. + - ```c++ -template -R Node::CalculateAppropriate(const std::function& f, const std::function&... funcs) const; -``` -invokes appropriate functor depending on type of value is holded by `Node` and provides returned value. + template + R Node::CalculateAppropriate(const std::function& f, const std::function&... funcs) const; + ``` + invokes appropriate functor depending on type of value is held by `Node` and provides returned value. - `Node Node::MakeLazyCopy()&` -!Warning! the copy should not live over the origin. * returns copy of origin one if it is head of subexpression or holds value of `Ref` type. * returns new `Node` that holds `Ref` which references to payload of origin one if it holds `T` (one of `Ts...`). + > **Warning** the copy shall not live longer than the origin. - `Node Node::MakeDeepCopy() const &` * returns copy of origin one if it is head of subexpression or holds value of one of `Ts...` types. * returns new `Node` which holds copy of value that `Ref` references to if origin one holds value of `Ref` type. diff --git a/cpp_src/core/index/indexstore.cc b/cpp_src/core/index/indexstore.cc index 43654c377..c978af8da 100644 --- a/cpp_src/core/index/indexstore.cc +++ b/cpp_src/core/index/indexstore.cc @@ -106,6 +106,41 @@ SelectKeyResults IndexStore::SelectKey(const VariantArray &keys, CondType con if (condition == CondAny && !this->opts_.IsArray() && !this->opts_.IsSparse() && !sopts.distinct) throw Error(errParams, "The 'NOT NULL' condition is suported only by 'sparse' or 'array' indexes"); + // TODO: it may be necessary to remove or change this switch after QueryEntry refactoring + switch (condition) { + case CondAny: + if (!this->opts_.IsArray() && !this->opts_.IsSparse() && !sopts.distinct) { + throw Error(errParams, "The 'NOT NULL' condition is suported only by 'sparse' or 'array' indexes"); + } + break; + case CondEmpty: + if (!this->opts_.IsArray() && !this->opts_.IsSparse()) { + throw Error(errParams, "The 'is NULL' condition is suported only by 'sparse' or 'array' indexes"); + } + break; + case CondAllSet: + case CondSet: + case CondEq: + break; + case CondRange: + case CondDWithin: + if (keys.size() != 2) { + throw Error(errParams, "For condition %s required exactly 2 arguments, but provided %d", CondTypeToStr(condition), + keys.size()); + } + break; + case CondLt: + case CondLe: + case CondGt: + case CondGe: + case CondLike: + if (keys.size() != 1) { + throw Error(errParams, "For condition %s required exactly 1 argument, but provided %d", CondTypeToStr(condition), + keys.size()); + } + break; + } + res.comparators_.push_back(Comparator(condition, KeyType(), keys, opts_.IsArray(), sopts.distinct, payloadType_, fields_, idx_data.size() ? idx_data.data() : nullptr, opts_.collateOpts_)); return SelectKeyResults(std::move(res)); diff --git a/cpp_src/core/nsselecter/explaincalc.cc b/cpp_src/core/nsselecter/explaincalc.cc index 86cd6c7d0..c7d9506de 100644 --- a/cpp_src/core/nsselecter/explaincalc.cc +++ b/cpp_src/core/nsselecter/explaincalc.cc @@ -1,9 +1,8 @@ #include "explaincalc.h" + #include -#include "core/cbinding/reindexer_ctypes.h" + #include "core/cjson/jsonbuilder.h" -#include "core/namespace/namespaceimpl.h" -#include "core/query/sql/sqlencoder.h" #include "nsselecter.h" #include "tools/logger.h" @@ -36,9 +35,9 @@ void ExplainCalc::LogDump(int logLevel) { if (jselectors_) { for (auto &js : *jselectors_) { if (js.Type() == JoinType::LeftJoin || js.Type() == JoinType::Merge) { - logPrintf(LogInfo, "%s %s: called %d", SQLEncoder::JoinTypeName(js.Type()), js.RightNsName(), js.Called()); + logPrintf(LogInfo, "%s %s: called %d", JoinTypeName(js.Type()), js.RightNsName(), js.Called()); } else { - logPrintf(LogInfo, "%s %s: called %d, matched %d", SQLEncoder::JoinTypeName(js.Type()), js.RightNsName(), js.Called(), + logPrintf(LogInfo, "%s %s: called %d, matched %d", JoinTypeName(js.Type()), js.RightNsName(), js.Called(), js.Matched()); } } @@ -70,17 +69,32 @@ constexpr inline const char *opName(OpType op, bool first = true) { case OpNot: return "not "; default: - throw Error(errLogic, "Unexpected op type: %d", int(op)); + throw Error(errLogic, "Unexpected op type %d", int(op)); + } +} + +constexpr std::string_view fieldKind(IteratorFieldKind fk) { + using namespace std::string_view_literals; + switch (fk) { + case IteratorFieldKind::NonIndexed: + return "non-indexed"sv; + case IteratorFieldKind::Indexed: + return "indexed"sv; + case IteratorFieldKind::None: + return ""sv; + default: + throw Error(errLogic, "Unexpected field type %d", int(fk)); } } static std::string addToJSON(JsonBuilder &builder, const JoinedSelector &js, OpType op = OpAnd) { + using namespace std::string_view_literals; auto jsonSel = builder.Object(); std::string name{joinTypeName(js.Type()) + js.RightNsName()}; - jsonSel.Put("field", opName(op) + name); - jsonSel.Put("matched", js.Matched()); - jsonSel.Put("selects_count", js.Called()); - jsonSel.Put("join_select_total", ExplainCalc::To_us(js.PreResult()->selectTime)); + jsonSel.Put("field"sv, opName(op) + name); + jsonSel.Put("matched"sv, js.Matched()); + jsonSel.Put("selects_count"sv, js.Called()); + jsonSel.Put("join_select_total"sv, ExplainCalc::To_us(js.PreResult()->selectTime)); switch (js.Type()) { case JoinType::InnerJoin: case JoinType::OrInnerJoin: @@ -88,25 +102,25 @@ static std::string addToJSON(JsonBuilder &builder, const JoinedSelector &js, OpT assertrx(js.PreResult()); switch (js.PreResult()->dataMode) { case JoinPreResult::ModeValues: - jsonSel.Put("method", "preselected_values"); - jsonSel.Put("keys", js.PreResult()->values.size()); + jsonSel.Put("method"sv, "preselected_values"sv); + jsonSel.Put("keys"sv, js.PreResult()->values.size()); break; case JoinPreResult::ModeIdSet: - jsonSel.Put("method", "preselected_rows"); - jsonSel.Put("keys", js.PreResult()->ids.size()); + jsonSel.Put("method"sv, "preselected_rows"sv); + jsonSel.Put("keys"sv, js.PreResult()->ids.size()); break; case JoinPreResult::ModeIterators: - jsonSel.Put("method", "no_preselect"); - jsonSel.Put("keys", js.PreResult()->iterators.Size()); + jsonSel.Put("method"sv, "no_preselect"sv); + jsonSel.Put("keys"sv, js.PreResult()->iterators.Size()); break; default: break; } if (!js.PreResult()->explainPreSelect.empty()) { - jsonSel.Raw("explain_preselect", js.PreResult()->explainPreSelect); + jsonSel.Raw("explain_preselect"sv, js.PreResult()->explainPreSelect); } if (!js.PreResult()->explainOneSelect.empty()) { - jsonSel.Raw("explain_select", js.PreResult()->explainOneSelect); + jsonSel.Raw("explain_select"sv, js.PreResult()->explainOneSelect); } break; case JoinType::Merge: @@ -115,31 +129,101 @@ static std::string addToJSON(JsonBuilder &builder, const JoinedSelector &js, OpT return name; } +static void addToJSON(JsonBuilder &builder, const ConditionInjection &injCond) { + auto jsonSel = builder.Object(); + using namespace std::string_view_literals; + using namespace std::string_literals; + + jsonSel.Put("condition"sv, injCond.initCond); + jsonSel.Put("total_time_us"sv, ExplainCalc::To_us(injCond.totalTime_)); + jsonSel.Put("success"sv, injCond.succeed); + if (!injCond.succeed) { + if (injCond.reason.empty()) { + if (injCond.orChainPart_) { + jsonSel.Put("reason"sv, "Skipped as Or-chain part."sv); + } else { + jsonSel.Put("reason"sv, "Unknown"sv); + } + } else { + std::string reason{injCond.reason}; + if (injCond.orChainPart_) { + reason += " Or-chain part."; + } + jsonSel.Put("reason"sv, reason); + } + } + + if (!injCond.explain.empty()) { + jsonSel.Raw("explain_select"sv, injCond.explain); + } + if (injCond.aggType != AggType::AggUnknown) { + jsonSel.Put("agg_type"sv, AggTypeToStr(injCond.aggType)); + } + jsonSel.Put("values_count"sv, injCond.valuesCount); + jsonSel.Put("new_condition"sv, injCond.newCond); +} + +static std::string addToJSON(JsonBuilder &builder, const JoinOnInjection &injCond) { + auto jsonSel = builder.Object(); + std::string name{injCond.rightNsName}; + using namespace std::string_view_literals; + + jsonSel.Put("namespace"sv, injCond.rightNsName); + jsonSel.Put("on_condition"sv, injCond.joinCond); + jsonSel.Put("type"sv, injCond.type == JoinOnInjection::ByValue ? "by_value"sv : "select"sv); + jsonSel.Put("total_time_us"sv, ExplainCalc::To_us(injCond.totalTime_)); + jsonSel.Put("success"sv, injCond.succeed); + if (!injCond.reason.empty()) { + jsonSel.Put("reason"sv, injCond.reason); + } + jsonSel.Put("injected_condition"sv, injCond.injectedCond.Slice()); + if (!injCond.conditions.empty()) { + auto jsonCondInjections = jsonSel.Array("conditions"sv); + for (const auto &cond : injCond.conditions) { + addToJSON(jsonCondInjections, cond); + } + } + + return name; +} + std::string ExplainCalc::GetJSON() { + using namespace std::string_view_literals; WrSerializer ser; { JsonBuilder json(ser); if (enabled_) { - json.Put("total_us", To_us(total_)); - json.Put("prepare_us", To_us(prepare_)); - json.Put("indexes_us", To_us(select_)); - json.Put("postprocess_us", To_us(postprocess_)); - json.Put("loop_us", To_us(loop_)); - json.Put("general_sort_us", To_us(sort_)); + json.Put("total_us"sv, To_us(total_)); + json.Put("preselect_us"sv, To_us(preselect_)); + json.Put("prepare_us"sv, To_us(prepare_)); + json.Put("indexes_us"sv, To_us(select_)); + json.Put("postprocess_us"sv, To_us(postprocess_)); + json.Put("loop_us"sv, To_us(loop_)); + json.Put("general_sort_us"sv, To_us(sort_)); } - json.Put("sort_index", sortIndex_); - json.Put("sort_by_uncommitted_index", sortOptimization_); + json.Put("sort_index"sv, sortIndex_); + json.Put("sort_by_uncommitted_index"sv, sortOptimization_); - auto jsonSelArr = json.Array("selectors"); + { + auto jsonSelArr = json.Array("selectors"sv); - if (selectors_) { - selectors_->ExplainJSON(iters_, jsonSelArr, jselectors_); + if (selectors_) { + selectors_->ExplainJSON(iters_, jsonSelArr, jselectors_); + } + + if (jselectors_) { + // adding explain for LeftJoin-s and Merge subqueries + for (const JoinedSelector &js : *jselectors_) { + if (js.Type() == JoinType::InnerJoin || js.Type() == JoinType::OrInnerJoin) continue; + addToJSON(jsonSelArr, js); + } + } } - if (jselectors_) { - for (JoinedSelector &js : *jselectors_) { - if (js.Type() == JoinType::InnerJoin || js.Type() == JoinType::OrInnerJoin) continue; - addToJSON(jsonSelArr, js); + if (onInjections_ && !onInjections_->empty()) { + auto jsonOnInjections = json.Array("on_conditions_injections"sv); + for (const JoinOnInjection &injCond : *onInjections_) { + addToJSON(jsonOnInjections, injCond); } } } @@ -150,6 +234,8 @@ std::string ExplainCalc::GetJSON() { std::string SelectIteratorContainer::explainJSON(const_iterator begin, const_iterator end, int iters, JsonBuilder &builder, const JoinedSelectors *jselectors) { using namespace std::string_literals; + using namespace std::string_view_literals; + std::stringstream name; name << '('; for (const_iterator it = begin; it != end; ++it) { @@ -157,26 +243,29 @@ std::string SelectIteratorContainer::explainJSON(const_iterator begin, const_ite it->InvokeAppropriate( [&](const SelectIteratorsBracket &) { auto jsonSel = builder.Object(); - auto jsonSelArr = jsonSel.Array("selectors"); + auto jsonSelArr = jsonSel.Array("selectors"sv); const std::string brName{explainJSON(it.cbegin(), it.cend(), iters, jsonSelArr, jselectors)}; jsonSelArr.End(); - jsonSel.Put("field", opName(it->operation) + brName); + jsonSel.Put("field"sv, opName(it->operation) + brName); name << opName(it->operation, it == begin) << brName; }, [&](const SelectIterator &siter) { auto jsonSel = builder.Object(); - const bool isScanIterator{std::string_view(siter.name) == "-scan"}; + const bool isScanIterator{std::string_view(siter.name) == "-scan"sv}; if (!isScanIterator) { - jsonSel.Put("keys", siter.size()); - jsonSel.Put("comparators", siter.comparators_.size()); - jsonSel.Put("cost", siter.Cost(iters)); + jsonSel.Put("keys"sv, siter.size()); + jsonSel.Put("comparators"sv, siter.comparators_.size()); + jsonSel.Put("cost"sv, siter.Cost(iters)); } else { - jsonSel.Put("items", siter.GetMaxIterations(iters)); + jsonSel.Put("items"sv, siter.GetMaxIterations(iters)); } - jsonSel.Put("field", opName(it->operation) + siter.name); - jsonSel.Put("matched", siter.GetMatchedCount()); - jsonSel.Put("method", isScanIterator || siter.comparators_.size() ? "scan" : "index"); - jsonSel.Put("type", siter.TypeName()); + jsonSel.Put("field"sv, opName(it->operation) + siter.name); + if (siter.fieldKind != IteratorFieldKind::None) { + jsonSel.Put("field_type"sv, fieldKind(siter.fieldKind)); + } + jsonSel.Put("matched"sv, siter.GetMatchedCount()); + jsonSel.Put("method"sv, isScanIterator || siter.comparators_.size() ? "scan"sv : "index"sv); + jsonSel.Put("type"sv, siter.TypeName()); name << opName(it->operation, it == begin) << siter.name; }, [&](const JoinSelectIterator &jiter) { @@ -186,21 +275,21 @@ std::string SelectIteratorContainer::explainJSON(const_iterator begin, const_ite }, [&](const FieldsComparator &c) { auto jsonSel = builder.Object(); - jsonSel.Put("comparators", 1); - jsonSel.Put("field", opName(it->operation) + c.Name()); - jsonSel.Put("cost", c.Cost(iters)); - jsonSel.Put("method", "scan"); - jsonSel.Put("items", iters); - jsonSel.Put("matched", c.GetMatchedCount()); - jsonSel.Put("type", "TwoFieldsComparison"); + jsonSel.Put("comparators"sv, 1); + jsonSel.Put("field"sv, opName(it->operation) + c.Name()); + jsonSel.Put("cost"sv, c.Cost(iters)); + jsonSel.Put("method"sv, "scan"sv); + jsonSel.Put("items"sv, iters); + jsonSel.Put("matched"sv, c.GetMatchedCount()); + jsonSel.Put("type"sv, "TwoFieldsComparison"sv); name << opName(it->operation, it == begin) << c.Name(); }, [&](const AlwaysFalse &) { auto jsonSkiped = builder.Object(); - jsonSkiped.Put("type", "Skipped"); - jsonSkiped.Put("description", "always "s + (it->operation == OpNot ? "true" : "false")); - name << opName(it->operation == OpNot ? OpAnd : it->operation, it == begin) << "Always" - << (it->operation == OpNot ? "True" : "False"); + jsonSkiped.Put("type"sv, "Skipped"sv); + jsonSkiped.Put("description"sv, "always "s + (it->operation == OpNot ? "true" : "false")); + name << opName(it->operation == OpNot ? OpAnd : it->operation, it == begin) << "Always"sv + << (it->operation == OpNot ? "True"sv : "False"sv); }); } name << ')'; @@ -216,41 +305,36 @@ ExplainCalc::Duration ExplainCalc::lap() noexcept { int ExplainCalc::To_us(const ExplainCalc::Duration &d) noexcept { return duration_cast(d).count(); } -void reindexer::ExplainCalc::StartTiming() noexcept { +void ExplainCalc::StartTiming() noexcept { if (enabled_) lap(); } -void reindexer::ExplainCalc::StopTiming() noexcept { - if (enabled_) total_ = prepare_ + select_ + postprocess_ + loop_; +void ExplainCalc::StopTiming() noexcept { + if (enabled_) total_ = preselect_ + prepare_ + select_ + postprocess_ + loop_; } -void reindexer::ExplainCalc::AddPrepareTime() noexcept { +void ExplainCalc::AddPrepareTime() noexcept { if (enabled_) prepare_ += lap(); } -void reindexer::ExplainCalc::AddSelectTime() noexcept { +void ExplainCalc::AddSelectTime() noexcept { if (enabled_) select_ += lap(); } -void reindexer::ExplainCalc::AddPostprocessTime() noexcept { +void ExplainCalc::AddPostprocessTime() noexcept { if (enabled_) postprocess_ += lap(); } -void reindexer::ExplainCalc::AddLoopTime() noexcept { +void ExplainCalc::AddLoopTime() noexcept { if (enabled_) loop_ += lap(); } -void reindexer::ExplainCalc::StartSort() noexcept { +void ExplainCalc::StartSort() noexcept { if (enabled_) sort_start_point_ = Clock::now(); } -void reindexer::ExplainCalc::StopSort() noexcept { +void ExplainCalc::StopSort() noexcept { if (enabled_) sort_ = Clock::now() - sort_start_point_; } -void reindexer::ExplainCalc::AddIterations(int iters) noexcept { iters_ += iters; } -void reindexer::ExplainCalc::PutSortIndex(std::string_view index) noexcept { sortIndex_ = index; } -void ExplainCalc::PutSelectors(SelectIteratorContainer *qres) noexcept { selectors_ = qres; } -void ExplainCalc::PutJoinedSelectors(JoinedSelectors *jselectors) noexcept { jselectors_ = jselectors; } - } // namespace reindexer diff --git a/cpp_src/core/nsselecter/explaincalc.h b/cpp_src/core/nsselecter/explaincalc.h index 34cccd5f9..837dfafde 100644 --- a/cpp_src/core/nsselecter/explaincalc.h +++ b/cpp_src/core/nsselecter/explaincalc.h @@ -5,13 +5,17 @@ #include #include "core/type_consts.h" -#include "estl/h_vector.h" +#include "tools/serializer.h" namespace reindexer { class SelectIteratorContainer; class JoinedSelector; +struct JoinOnInjection; +struct ConditionInjection; + typedef std::vector JoinedSelectors; +typedef std::vector OnConditionInjections; class ExplainCalc { public: @@ -32,14 +36,16 @@ class ExplainCalc { void AddSelectTime() noexcept; void AddPostprocessTime() noexcept; void AddLoopTime() noexcept; - void AddIterations(int iters) noexcept; + void AddIterations(int iters) noexcept { iters_ += iters; } void StartSort() noexcept; void StopSort() noexcept; void PutCount(int cnt) noexcept { count_ = cnt; } - void PutSortIndex(std::string_view index) noexcept; - void PutSelectors(SelectIteratorContainer *qres) noexcept; - void PutJoinedSelectors(JoinedSelectors *jselectors) noexcept; + void PutSortIndex(std::string_view index) noexcept { sortIndex_ = index; } + void PutSelectors(const SelectIteratorContainer *qres) noexcept { selectors_ = qres; } + void PutJoinedSelectors(const JoinedSelectors *jselectors) noexcept { jselectors_ = jselectors; } + void SetPreselectTime(Duration preselectTime) noexcept { preselect_ = preselectTime; } + void PutOnConditionInjections(const OnConditionInjections *onCondInjections) noexcept { onInjections_ = onCondInjections; } void SetSortOptimization(bool enable) noexcept { sortOptimization_ = enable; } void LogDump(int logLevel); @@ -58,22 +64,56 @@ class ExplainCalc { private: Duration lap() noexcept; - static const char *JoinTypeName(JoinType jtype); time_point last_point_, sort_start_point_; Duration total_, prepare_ = Duration::zero(); + Duration preselect_ = Duration::zero(); Duration select_ = Duration::zero(); Duration postprocess_ = Duration::zero(); Duration loop_ = Duration::zero(); Duration sort_ = Duration::zero(); std::string_view sortIndex_; - SelectIteratorContainer *selectors_ = nullptr; - JoinedSelectors *jselectors_ = nullptr; + const SelectIteratorContainer *selectors_ = nullptr; + const JoinedSelectors *jselectors_ = nullptr; + const OnConditionInjections *onInjections_ = nullptr; ///< Optional + int iters_ = 0; int count_ = 0; bool sortOptimization_ = false; bool enabled_ = false; }; +/** + * @brief Describes the process of a single JOIN-query ON-conditions injection into the Where clause of a main query + */ +struct JoinOnInjection { + std::string_view rightNsName; ///< joinable ns name + std::string joinCond; ///< original ON-conditions clause. SQL-like string + ExplainCalc::Duration totalTime_ = + ExplainCalc::Duration::zero(); ///< total amount of time spent on checking and substituting all conditions + bool succeed = false; ///< result of injection attempt + std::string_view reason; ///< optional{succeed==false}. Explains condition injection failure + enum { ByValue, Select } type = ByValue; ///< byValue or Select + WrSerializer injectedCond; ///< injected condition. SQL-like string + std::vector conditions; ///< individual conditions processing results +}; + +/** + * @brief Describes an injection attempt of a single condition from the ON-clause of a JOIN-query + */ +struct ConditionInjection { + std::string initCond; ///< single condition from Join ON section. SQL-like string + ExplainCalc::Duration totalTime_ = + ExplainCalc::Duration::zero(); ///< total time elapsed from injection attempt start till the end of substitution or rejection + std::string explain; ///< optoinal{JoinOnInjection.type == Select}. Explain raw string from Select subquery. + AggType aggType = AggType::AggUnknown; ///< aggregation type used in subquery + bool succeed = false; ///< result of injection attempt + std::string_view reason; ///< optional{succeed==false}. Explains condition injection failure + bool orChainPart_ = false; ///< additional failure reason flag. Used in case if condition field was filled before and + ///< also it does not fit because it is an OR chain part + std::string newCond; ///< substituted condition in QueryEntry. SQL-like string + size_t valuesCount = 0; ///< resulting size of query values set +}; + } // namespace reindexer diff --git a/cpp_src/core/nsselecter/joinedselector.cc b/cpp_src/core/nsselecter/joinedselector.cc index c276e25f6..84fbe4b8b 100644 --- a/cpp_src/core/nsselecter/joinedselector.cc +++ b/cpp_src/core/nsselecter/joinedselector.cc @@ -1,4 +1,5 @@ #include "joinedselector.h" + #include "core/namespace/namespaceimpl.h" #include "core/queryresults/joinresults.h" #include "nsselecter.h" @@ -229,7 +230,8 @@ void JoinedSelector::AppendSelectIteratorOfJoinIndexData(SelectIteratorContainer bool was = false; for (SelectKeyResult &res : leftIndex->SelectKey(values, CondSet, sortId, opts, ctx, rdxCtx)) { if (!res.comparators_.empty()) continue; - SelectIterator selIter{res, false, joinEntry.index_, false}; + SelectIterator selIter{res, false, joinEntry.index_, + (joinEntry.idxNo < 0 ? IteratorFieldKind::NonIndexed : IteratorFieldKind::Indexed), false}; selIter.Bind(leftNs_->payloadType_, joinEntry.idxNo); const int curIterations = selIter.GetMaxIterations(); if (curIterations && curIterations < *maxIterations) *maxIterations = curIterations; diff --git a/cpp_src/core/nsselecter/nsselecter.cc b/cpp_src/core/nsselecter/nsselecter.cc index bdb1cf571..91b546cf0 100644 --- a/cpp_src/core/nsselecter/nsselecter.cc +++ b/cpp_src/core/nsselecter/nsselecter.cc @@ -1,4 +1,6 @@ #include "nsselecter.h" + +#include "core/cjson/jsonbuilder.h" #include "core/namespace/namespaceimpl.h" #include "core/queryresults/joinresults.h" #include "crashqueryreporter.h" @@ -30,6 +32,7 @@ void NsSelecter::operator()(QueryResults &result, SelectCtx &ctx, const RdxConte explain = ExplainCalc(ctx.query.explain_ || ctx.query.debugLevel >= LogInfo); ActiveQueryScope queryScope(ctx, ns_->optimizationState_, explain, ns_->locker_.IsReadOnly(), ns_->strHolder_.get()); + explain.SetPreselectTime(ctx.preResultTimeTotal); explain.StartTiming(); auto containSomeAggCount = [&ctx](const AggType &type) { @@ -59,12 +62,14 @@ void NsSelecter::operator()(QueryResults &result, SelectCtx &ctx, const RdxConte } } + OnConditionInjections explainInjectedOnConditions; QueryPreprocessor qPreproc((ctx.preResult && ctx.preResult->executionMode == JoinPreResult::ModeExecute) ? const_cast(&ctx.query.entries)->MakeLazyCopy() : QueryEntries{ctx.query.entries}, ns_, ctx); if (ctx.joinedSelectors) { - qPreproc.InjectConditionsFromJoins(*ctx.joinedSelectors, rdxCtx); + qPreproc.InjectConditionsFromJoins(*ctx.joinedSelectors, explainInjectedOnConditions, rdxCtx); + explain.PutOnConditionInjections(&explainInjectedOnConditions); } auto aggregators = getAggregators(ctx.query); qPreproc.AddDistinctEntries(aggregators); @@ -170,7 +175,8 @@ void NsSelecter::operator()(QueryResults &result, SelectCtx &ctx, const RdxConte SelectKeyResult res; res.emplace_back(std::move(ctx.preResult->ids)); static const std::string pr = "-preresult"; - qres.Append(OpAnd, SelectIterator(std::move(res), false, pr)); + // Iterator Field Kind: Preselect IdSet -> None + qres.Append(OpAnd, SelectIterator(std::move(res), false, pr, IteratorFieldKind::None)); } break; case JoinPreResult::ModeIterators: qres.LazyAppend(ctx.preResult->iterators.begin(), ctx.preResult->iterators.end()); @@ -261,7 +267,8 @@ void NsSelecter::operator()(QueryResults &result, SelectCtx &ctx, const RdxConte scan.emplace_back(0, limit); maxIterations = limit; } - qres.AppendFront(OpAnd, SelectIterator{std::move(scan), false, "-scan", true}); + // Iterator Field Kind: -scan. Sorting Context! -> None + qres.AppendFront(OpAnd, SelectIterator{std::move(scan), false, "-scan", IteratorFieldKind::None, true}); } // Get maximum iterations count, for right calculation comparators costs qres.SortByCost(maxIterations); @@ -269,7 +276,7 @@ void NsSelecter::operator()(QueryResults &result, SelectCtx &ctx, const RdxConte // Check idset must be 1st qres.CheckFirstQuery(); - // Rewing all results iterators + // Rewind all results iterators qres.ExecuteAppropriateForEach(Skip{}, [reverse, maxIterations](SelectIterator &it) { it.Start(reverse, maxIterations); }); diff --git a/cpp_src/core/nsselecter/nsselecter.h b/cpp_src/core/nsselecter/nsselecter.h index 4b247ed6f..701a11185 100644 --- a/cpp_src/core/nsselecter/nsselecter.h +++ b/cpp_src/core/nsselecter/nsselecter.h @@ -17,6 +17,7 @@ struct SelectCtx { SelectFunctionsHolder *functions = nullptr; JoinPreResult::Ptr preResult; + ExplainCalc::Duration preResultTimeTotal = ExplainCalc::Duration::zero(); SortingContext sortingContext; uint8_t nsid = 0; bool isForceAll = false; diff --git a/cpp_src/core/nsselecter/querypreprocessor.cc b/cpp_src/core/nsselecter/querypreprocessor.cc index 19b14ba07..205eaf4c8 100644 --- a/cpp_src/core/nsselecter/querypreprocessor.cc +++ b/cpp_src/core/nsselecter/querypreprocessor.cc @@ -1,4 +1,5 @@ #include "querypreprocessor.h" + #include "core/index/index.h" #include "core/index/indextext/indextext.h" #include "core/namespace/namespaceimpl.h" @@ -6,6 +7,7 @@ #include "core/nsselecter/selectiteratorcontainer.h" #include "core/nsselecter/sortexpression.h" #include "core/payload/fieldsset.h" +#include "core/query/dsl/dslencoder.h" #include "core/query/queryentry.h" #include "estl/overloaded.h" #include "nsselecter.h" @@ -119,6 +121,19 @@ void QueryPreprocessor::checkStrictMode(const std::string &index, int idxNo) con } } +class JoinOnExplainEnabled; +class JoinOnExplainDisabled; + +void QueryPreprocessor::InjectConditionsFromJoins(JoinedSelectors &js, OnConditionInjections &expalainOnInjections, + const RdxContext &rdxCtx) { + bool needExplain = query_.explain_ || query_.debugLevel >= LogInfo; + if (needExplain) { + injectConditionsFromJoins(0, container_.size(), js, expalainOnInjections, rdxCtx); + } else { + injectConditionsFromJoins(0, container_.size(), js, expalainOnInjections, rdxCtx); + } +} + void QueryPreprocessor::Reduce(bool isFt) { bool changed; do { @@ -359,9 +374,7 @@ size_t QueryPreprocessor::substituteCompositeIndexes(const size_t from, const si if (rx_unlikely(!res.fields.contains(qe.idxNo))) { throw Error(errLogic, "Error during composite index's fields substitution (this should not happen)"); } - if (rx_unlikely(qe.condition == CondEq && qe.values.size() == 0)) { - throw Error(errParams, "Condition EQ must have at least 1 argument, but provided 0"); - } + maxSetSize = std::max(maxSetSize, qe.values.size()); resultSetSize = (resultSetSize == 0) ? qe.values.size() : (resultSetSize * qe.values.size()); } @@ -581,9 +594,9 @@ void QueryPreprocessor::AddDistinctEntries(const h_vector &aggreg } } -void QueryPreprocessor::fillQueryEntryFromOnCondition(QueryEntry &queryEntry, NamespaceImpl &rightNs, Query joinQuery, - std::string joinIndex, CondType condition, KeyValueType valuesType, - const RdxContext &rdxCtx) { +void QueryPreprocessor::fillQueryEntryFromOnCondition(QueryEntry &queryEntry, std::string &explainStr, AggType &oAggType, + NamespaceImpl &rightNs, Query joinQuery, std::string joinIndex, CondType condition, + KeyValueType valuesType, const RdxContext &rdxCtx) { size_t limit; const auto &rNsCfg = rightNs.Config(); if (rNsCfg.maxPreselectSize == 0) { @@ -594,6 +607,7 @@ void QueryPreprocessor::fillQueryEntryFromOnCondition(QueryEntry &queryEntry, Na limit = std::min(std::max(rNsCfg.minPreselectSize, rightNs.ItemsCount() * rNsCfg.maxPreselectPart), rNsCfg.maxPreselectSize); } + joinQuery.explain_ = query_.explain_; joinQuery.count = limit + 2; joinQuery.start = 0; joinQuery.sortingEntries_.clear(); @@ -603,14 +617,17 @@ void QueryPreprocessor::fillQueryEntryFromOnCondition(QueryEntry &queryEntry, Na case CondEq: case CondSet: joinQuery.Distinct(std::move(joinIndex)); + oAggType = AggType::AggDistinct; break; case CondLt: case CondLe: joinQuery.Aggregate(AggMax, {std::move(joinIndex)}); + oAggType = AggType::AggMax; break; case CondGt: case CondGe: joinQuery.Aggregate(AggMin, {std::move(joinIndex)}); + oAggType = AggType::AggMin; break; case CondAny: case CondRange: @@ -620,11 +637,13 @@ void QueryPreprocessor::fillQueryEntryFromOnCondition(QueryEntry &queryEntry, Na case CondDWithin: throw Error(errParams, "Unsupported condition in ON statment: %s", CondTypeToStr(condition)); } + SelectCtx ctx{joinQuery, nullptr}; QueryResults qr; rightNs.Select(qr, ctx, rdxCtx); if (qr.Count() > limit) return; assertrx(qr.aggregationResults.size() == 1); + explainStr = qr.explainResults; switch (condition) { case CondEq: case CondSet: { @@ -725,23 +744,70 @@ void QueryPreprocessor::fillQueryEntryFromOnCondition(QueryEntry &queryEntry, st } } -void QueryPreprocessor::injectConditionsFromJoins(size_t from, size_t to, JoinedSelectors &js, const RdxContext &rdxCtx) { +template +void QueryPreprocessor::briefDump(size_t from, size_t to, const std::vector &joinedSelectors, WrSerializer &ser) const { + { + for (auto it = from; it < to; it = Next(it)) { + if (it != from || container_[it].operation != OpAnd) { + ser << container_[it].operation << ' '; + } + container_[it].InvokeAppropriate( + [&](const QueryEntriesBracket &b) { + ser << "("; + briefDump(it + 1, Next(it), joinedSelectors, ser); + dumpEqualPositions(0, ser, b.equalPositions); + ser << ")"; + }, + [&ser](const QueryEntry &qe) { ser << qe.DumpBrief() << ' '; }, + [&joinedSelectors, &ser](const JoinQueryEntry &jqe) { ser << jqe.Dump(joinedSelectors) << ' '; }, + [&ser](const BetweenFieldsQueryEntry &qe) { ser << qe.Dump() << ' '; }, + [&ser](const AlwaysFalse &) { ser << "AlwaysFalse" << ' '; }); + } + } +} + +template +size_t QueryPreprocessor::injectConditionsFromJoins(size_t from, size_t to, JoinedSelectors &js, OnConditionInjections &explainOnInjections, + const RdxContext &rdxCtx) { + using namespace std::string_view_literals; + + size_t injectedCount = 0; for (size_t cur = from; cur < to; cur = Next(cur)) { container_[cur].InvokeAppropriate( Skip{}, - [&js, cur, this, &rdxCtx](const QueryEntriesBracket &) { injectConditionsFromJoins(cur + 1, Next(cur), js, rdxCtx); }, + [&](const QueryEntriesBracket &) { + size_t injCount = injectConditionsFromJoins(cur + 1, Next(cur), js, explainOnInjections, rdxCtx); + to += injCount; + injectedCount += injCount; + assertrx_throw(to <= container_.size()); + }, [&](const JoinQueryEntry &jqe) { assertrx(js.size() > jqe.joinIndex); + JoinedSelector &joinedSelector = js[jqe.joinIndex]; - const bool byValues = joinedSelector.PreResult()->dataMode == JoinPreResult::ModeValues; + const bool byValues = joinedSelector.PreResult() && joinedSelector.PreResult()->dataMode == JoinPreResult::ModeValues; + + auto explainJoinOn = ExplainPolicy::AppendJoinOnExplain(explainOnInjections); + explainJoinOn.Init(jqe, js, byValues); + + // Checking if we are able to preselect something from RightNs, or there are preselected results if (!byValues) { const auto &rNsCfg = joinedSelector.RightNs()->Config(); - if (rNsCfg.maxPreselectSize == 0 && rNsCfg.maxPreselectPart == 0.0) return; + if (rNsCfg.maxPreselectSize == 0 && rNsCfg.maxPreselectPart == 0.0) { + explainJoinOn.Skipped("maxPreselectSize and maxPreselectPart == 0"sv); + return; + } } else { - if (!joinedSelector.PreResult()->values.IsPreselectAllowed()) return; + if (!joinedSelector.PreResult()->values.IsPreselectAllowed()) { + explainJoinOn.Skipped("Preselect is not allowed"sv); + return; + } } - assertrx(joinedSelector.Type() == InnerJoin || joinedSelector.Type() == OrInnerJoin); + const auto &joinEntries = joinedSelector.joinQuery_.joinEntries_; + // LeftJoin-s shall not be in QueryEntries container_ by construction + assertrx(joinedSelector.Type() == InnerJoin || joinedSelector.Type() == OrInnerJoin); + // Checking if we have anything to inject into main Where clause bool foundANDOrOR = false; for (const auto &je : joinEntries) { if (je.op_ != OpNot) { @@ -749,21 +815,35 @@ void QueryPreprocessor::injectConditionsFromJoins(size_t from, size_t to, Joined break; } } - if (!foundANDOrOR) return; + if (!foundANDOrOR) { + explainJoinOn.Skipped("And or Or operators not found"sv); + return; + } + OpType op = GetOperation(cur); if (joinedSelector.Type() == OrInnerJoin) { if (op == OpNot) throw Error(errParams, "OR INNER JOIN with operation NOT"); op = OpOr; joinedSelector.SetType(InnerJoin); } + + // inserting Bracket for JoinQuery itself into ExpressionTree SetOperation(OpAnd, cur); + // !!!Warning jqe reference will be invalidated after EncloseInBracket EncloseInBracket(cur, cur + 1, op); ++cur; + + explainJoinOn.ReserveOnEntries(joinEntries.size()); + size_t count = 0; bool prevIsSkipped = false; size_t orChainLength = 0; for (size_t i = 0, s = joinEntries.size(); i < s; ++i) { const QueryJoinEntry &joinEntry = joinEntries[i]; + + auto explainEntry = explainJoinOn.AppendOnEntryExplain(); + explainEntry.InitialCondition(joinEntry, joinedSelector); + CondType condition = joinEntry.condition_; OpType operation = joinEntry.op_; switch (operation) { @@ -785,6 +865,7 @@ void QueryPreprocessor::injectConditionsFromJoins(size_t from, size_t to, Joined case CondEq: case CondSet: prevIsSkipped = true; + explainEntry.Skipped("Skipped due to condition Eq|Set with operation Not."sv); continue; case CondAny: case CondRange: @@ -797,13 +878,17 @@ void QueryPreprocessor::injectConditionsFromJoins(size_t from, size_t to, Joined operation = OpAnd; break; case OpOr: - if (prevIsSkipped) continue; + explainEntry.OrChainPart(true); + if (prevIsSkipped) { + continue; + } ++orChainLength; break; case OpAnd: orChainLength = 0; break; } + QueryEntry newEntry; newEntry.index = joinEntry.index_; newEntry.idxNo = IndexValueType::SetByJsonPath; @@ -814,6 +899,7 @@ void QueryPreprocessor::injectConditionsFromJoins(size_t from, size_t to, Joined valuesType = index.SelectKeyType(); collate = index.Opts().collateOpts_; } + if (byValues) { assertrx(joinedSelector.itemQuery_.entries.HoldsOrReferTo(i)); const QueryEntry &qe = joinedSelector.itemQuery_.entries.Get(i); @@ -835,6 +921,9 @@ void QueryPreprocessor::injectConditionsFromJoins(size_t from, size_t to, Joined case CondGe: { const QueryEntry &qe = joinedSelector.itemQuery_.entries.Get(i); skip = qe.idxNo != IndexValueType::SetByJsonPath && joinedSelector.RightNs()->indexes_[qe.idxNo]->IsUuid(); + if (skip) { + explainEntry.Skipped("Skipped due to condition Lt|Le|Gt|Ge with UUID index field."sv); + } break; } case CondEq: @@ -848,30 +937,157 @@ void QueryPreprocessor::injectConditionsFromJoins(size_t from, size_t to, Joined break; } if (!skip) { - fillQueryEntryFromOnCondition(newEntry, *joinedSelector.RightNs(), joinedSelector.JoinQuery(), - joinEntry.joinIndex_, condition, valuesType, rdxCtx); + std::string explainSelect; + AggType selectAggType; + fillQueryEntryFromOnCondition(newEntry, explainSelect, selectAggType, *joinedSelector.RightNs(), + joinedSelector.JoinQuery(), joinEntry.joinIndex_, condition, valuesType, rdxCtx); + explainEntry.ExplainSelect(std::move(explainSelect), selectAggType); } } + if (!newEntry.values.empty()) { + explainEntry.Succeed(newEntry); + Insert(cur, operation, std::move(newEntry)); ++cur; ++count; prevIsSkipped = false; } else { + explainEntry.Skipped("Skipped as cannot obtain values from right namespace."sv); if (operation == OpOr) { Erase(cur - orChainLength, cur); count -= orChainLength; + // Marking On-injections as fail for removed entries. + explainJoinOn.FailOnEntriesAsOrChain(orChainLength); } prevIsSkipped = true; } - } + } // end of entries processing + if (count > 0) { EncloseInBracket(cur - count, cur, OpAnd); + + explainJoinOn.Succeed( + [this, cur, count, &js](WrSerializer &ser) { briefDump(cur - count, Next(cur - count), js, ser); }); + ++cur; + injectedCount += count + 2; to += count + 2; } }); } + return injectedCount; } -} // namespace reindexer +class JoinOnExplainDisabled { + JoinOnExplainDisabled() noexcept = default; + struct OnEntryExplain { + OnEntryExplain() noexcept = default; + + RX_ALWAYS_INLINE void InitialCondition(const QueryJoinEntry &, const JoinedSelector &) const noexcept {} + RX_ALWAYS_INLINE void Succeed(const QueryEntry &) const noexcept {} + RX_ALWAYS_INLINE void Skipped(std::string_view) const noexcept {} + RX_ALWAYS_INLINE void OrChainPart(bool) const noexcept {} + RX_ALWAYS_INLINE void ExplainSelect(std::string &&, AggType) const noexcept {} + }; + +public: + [[nodiscard]] RX_ALWAYS_INLINE static JoinOnExplainDisabled AppendJoinOnExplain(OnConditionInjections &) noexcept { return {}; } + + RX_ALWAYS_INLINE void Init(const JoinQueryEntry &, const JoinedSelectors &, bool) const noexcept {} + RX_ALWAYS_INLINE void Succeed(const std::function &) const noexcept {} + RX_ALWAYS_INLINE void Skipped(std::string_view) const noexcept {} + RX_ALWAYS_INLINE void ReserveOnEntries(size_t) const noexcept {} + [[nodiscard]] RX_ALWAYS_INLINE OnEntryExplain AppendOnEntryExplain() const noexcept { return {}; } + + RX_ALWAYS_INLINE void FailOnEntriesAsOrChain(size_t) const noexcept {} +}; + +class JoinOnExplainEnabled { + using time_point_t = ExplainCalc::Clock::time_point; + struct OnEntryExplain { + OnEntryExplain(ConditionInjection &explainEntry) noexcept : startTime_(ExplainCalc::Clock::now()), explainEntry_(explainEntry) {} + ~OnEntryExplain() noexcept { explainEntry_.totalTime_ = ExplainCalc::Clock::now() - startTime_; } + OnEntryExplain(const OnEntryExplain &) = delete; + OnEntryExplain(OnEntryExplain &&) = delete; + OnEntryExplain &operator=(const OnEntryExplain &) = delete; + OnEntryExplain &operator=(OnEntryExplain &&) = delete; + + void InitialCondition(const QueryJoinEntry &joinEntry, const JoinedSelector &joinedSelector) { + explainEntry_.initCond = joinEntry.DumpCondition(joinedSelector); + } + void Succeed(const QueryEntry &newEntry) { + explainEntry_.succeed = true; + explainEntry_.reason = ""; + explainEntry_.newCond = newEntry.DumpBrief(); + explainEntry_.valuesCount = newEntry.values.size(); + } + + void Skipped(std::string_view reason) noexcept { + if (explainEntry_.reason.empty()) { + explainEntry_.reason = reason; + } + explainEntry_.succeed = false; + } + + void OrChainPart(bool orChainPart) noexcept { explainEntry_.orChainPart_ = orChainPart; } + void ExplainSelect(std::string &&explain, AggType aggType) noexcept { + explainEntry_.explain = std::move(explain); + explainEntry_.aggType = aggType; + } + + private: + time_point_t startTime_; + ConditionInjection &explainEntry_; + }; + + JoinOnExplainEnabled(const JoinOnExplainEnabled &) = delete; + JoinOnExplainEnabled(JoinOnExplainEnabled &&) = delete; + JoinOnExplainEnabled &operator=(const JoinOnExplainEnabled &) = delete; + JoinOnExplainEnabled &operator=(JoinOnExplainEnabled &&) = delete; + + JoinOnExplainEnabled(JoinOnInjection &joinOn) noexcept : explainJoinOn_(joinOn), startTime_(ExplainCalc::Clock::now()) {} + +public: + [[nodiscard]] static JoinOnExplainEnabled AppendJoinOnExplain(OnConditionInjections &explainOnInjections) { + return {explainOnInjections.emplace_back()}; + } + ~JoinOnExplainEnabled() noexcept { explainJoinOn_.totalTime_ = ExplainCalc::Clock::now() - startTime_; } + + void Init(const JoinQueryEntry &jqe, const JoinedSelectors &js, bool byValues) { + const JoinedSelector &joinedSelector = js[jqe.joinIndex]; + explainJoinOn_.rightNsName = joinedSelector.RightNsName(); + explainJoinOn_.joinCond = jqe.DumpOnCondition(js); + explainJoinOn_.type = byValues ? JoinOnInjection::ByValue : JoinOnInjection::Select; + } + void Succeed(const std::function &setInjectedCond) { + explainJoinOn_.succeed = true; + setInjectedCond(explainJoinOn_.injectedCond); + } + void Skipped(std::string_view reason) noexcept { + if (explainJoinOn_.reason.empty()) { + explainJoinOn_.reason = reason; + } + explainJoinOn_.succeed = false; + } + + void ReserveOnEntries(size_t count) { explainJoinOn_.conditions.reserve(count); } + [[nodiscard]] OnEntryExplain AppendOnEntryExplain() { return {explainJoinOn_.conditions.emplace_back()}; }; + + void FailOnEntriesAsOrChain(size_t orChainLength) { + using namespace std::string_view_literals; + auto &conditions = explainJoinOn_.conditions; + assertrx(conditions.size() >= orChainLength); + // Marking On-injections as fail for removed entries. + for (size_t jsz = conditions.size(), j = jsz - orChainLength; j < jsz; ++j) { + conditions[j].succeed = false; + conditions[j].orChainPart_ = true; + } + } + +private: + JoinOnInjection &explainJoinOn_; + time_point_t startTime_; +}; + +} // namespace reindexer \ No newline at end of file diff --git a/cpp_src/core/nsselecter/querypreprocessor.h b/cpp_src/core/nsselecter/querypreprocessor.h index 31d3dbd29..907636251 100644 --- a/cpp_src/core/nsselecter/querypreprocessor.h +++ b/cpp_src/core/nsselecter/querypreprocessor.h @@ -47,13 +47,12 @@ class QueryPreprocessor : private QueryEntries { unsigned Count() const noexcept { return count_; } bool MoreThanOneEvaluation() const noexcept { return queryEntryAddedByForcedSortOptimization_; } bool AvailableSelectBySortIndex() const noexcept { return !queryEntryAddedByForcedSortOptimization_ || !forcedStage(); } - void InjectConditionsFromJoins(JoinedSelectors &js, const RdxContext &rdxCtx) { - injectConditionsFromJoins(0, container_.size(), js, rdxCtx); - } + void InjectConditionsFromJoins(JoinedSelectors &js, OnConditionInjections &expalainOnInjections, const RdxContext &rdxCtx); void Reduce(bool isFt); void InitIndexNumbers(); using QueryEntries::Size; using QueryEntries::Dump; + using QueryEntries::ToDsl; [[nodiscard]] SortingEntries GetSortingEntries(const SelectCtx &ctx) const; bool IsFtExcluded() const noexcept { return ftEntry_.has_value(); } void ExcludeFtQuery(const RdxContext &); @@ -90,9 +89,13 @@ class QueryPreprocessor : private QueryEntries { [[nodiscard]] const Index *findMaxIndex(QueryEntries::const_iterator begin, QueryEntries::const_iterator end) const; void findMaxIndex(QueryEntries::const_iterator begin, QueryEntries::const_iterator end, h_vector &foundIndexes) const; - void injectConditionsFromJoins(size_t from, size_t to, JoinedSelectors &, const RdxContext &); - void fillQueryEntryFromOnCondition(QueryEntry &, NamespaceImpl &rightNs, Query joinQuery, std::string joinIndex, CondType condition, - KeyValueType, const RdxContext &); + /** @brief recurrently checks and injects Join ON conditions + * @returns injected conditions and EntryBrackets count + */ + template + size_t injectConditionsFromJoins(size_t from, size_t to, JoinedSelectors &, OnConditionInjections &, const RdxContext &); + void fillQueryEntryFromOnCondition(QueryEntry &, std::string &outExplainStr, AggType &, NamespaceImpl &rightNs, Query joinQuery, + std::string joinIndex, CondType condition, KeyValueType, const RdxContext &); template void fillQueryEntryFromOnCondition(QueryEntry &, std::string_view joinIndex, CondType condition, const JoinedSelector &, KeyValueType, int rightIdxNo, const CollateOpts &); @@ -101,6 +104,9 @@ class QueryPreprocessor : private QueryEntries { size_t removeBrackets(size_t begin, size_t end); bool canRemoveBracket(size_t i) const; + template + void briefDump(size_t from, size_t to, const std::vector &joinedSelectors, WrSerializer &ser) const; + NamespaceImpl &ns_; const Query &query_; StrictMode strictMode_; diff --git a/cpp_src/core/nsselecter/selectiterator.cc b/cpp_src/core/nsselecter/selectiterator.cc index 9bc60dd4d..05ab657be 100644 --- a/cpp_src/core/nsselecter/selectiterator.cc +++ b/cpp_src/core/nsselecter/selectiterator.cc @@ -1,13 +1,14 @@ #include "selectiterator.h" + #include #include #include "core/index/indexiterator.h" namespace reindexer { -SelectIterator::SelectIterator(SelectKeyResult res, bool dist, std::string n, bool forcedFirst) - : SelectKeyResult(std::move(res)), distinct(dist), name(std::move(n)), forcedFirst_(forcedFirst), type_(Forward) {} +SelectIterator::SelectIterator(SelectKeyResult res, bool dist, std::string n, IteratorFieldKind fKind, bool forcedFirst) + : SelectKeyResult(std::move(res)), distinct(dist), name(std::move(n)), fieldKind(fKind), forcedFirst_(forcedFirst), type_(Forward) {} void SelectIterator::Bind(const PayloadType &type, int field) { for (Comparator &cmp : comparators_) cmp.Bind(type, field); diff --git a/cpp_src/core/nsselecter/selectiterator.h b/cpp_src/core/nsselecter/selectiterator.h index 0bba0546c..55286e183 100644 --- a/cpp_src/core/nsselecter/selectiterator.h +++ b/cpp_src/core/nsselecter/selectiterator.h @@ -4,6 +4,7 @@ namespace reindexer { +enum class IteratorFieldKind { None, NonIndexed, Indexed }; /// Allows to iterate over a result of selecting /// data for one certain key. class SelectIterator : public SelectKeyResult { @@ -23,7 +24,7 @@ class SelectIterator : public SelectKeyResult { }; SelectIterator() = default; - SelectIterator(SelectKeyResult res, bool distinct, std::string name, bool forcedFirst = false); + SelectIterator(SelectKeyResult res, bool distinct, std::string name, IteratorFieldKind fieldKind, bool forcedFirst = false); /// Starts iteration process: prepares /// object for further work. @@ -135,6 +136,7 @@ class SelectIterator : public SelectKeyResult { bool distinct = false; std::string name; + IteratorFieldKind fieldKind; protected: // Iterates to a next item of result diff --git a/cpp_src/core/nsselecter/selectiteratorcontainer.cc b/cpp_src/core/nsselecter/selectiteratorcontainer.cc index 48ee368ab..a926d6fb9 100644 --- a/cpp_src/core/nsselecter/selectiteratorcontainer.cc +++ b/cpp_src/core/nsselecter/selectiteratorcontainer.cc @@ -1,4 +1,5 @@ #include "selectiteratorcontainer.h" + #include #include #include "core/namespace/namespaceimpl.h" @@ -189,6 +190,34 @@ SelectKeyResults SelectIteratorContainer::processQueryEntry(const QueryEntry &qe FieldsSet fields; TagsPath tagsPath = ns.tagsMatcher_.path2tag(qe.index); + + // TODO: it may be necessary to remove or change this switch after QueryEntry refactoring + switch (qe.condition) { + case CondAny: + case CondEmpty: + case CondAllSet: + case CondEq: + case CondSet: + break; + case CondRange: + case CondDWithin: + if (qe.values.size() != 2) { + throw Error(errParams, "For condition %s required exactly 2 arguments, but provided %d", CondTypeToStr(qe.condition), + qe.values.size()); + } + break; + case CondLt: + case CondLe: + case CondGt: + case CondGe: + case CondLike: + if (qe.values.size() != 1) { + throw Error(errParams, "For condition %s required exactly 1 argument, but provided %d", CondTypeToStr(qe.condition), + qe.values.size()); + } + break; + } + if (!tagsPath.empty()) { SelectKeyResult comparisonResult; fields.push_back(tagsPath); @@ -330,7 +359,9 @@ void SelectIteratorContainer::processQueryEntryResults(SelectKeyResults &selectR [[fallthrough]]; case OpNot: case OpAnd: - Append(op, SelectIterator(res, qe.distinct, qe.index, isIndexFt)); + // Iterator Field Kind: Query entry results. Field known. + Append(op, SelectIterator(res, qe.distinct, qe.index, + qe.idxNo < 0 ? IteratorFieldKind::NonIndexed : IteratorFieldKind::Indexed, isIndexFt)); if (!nonIndexField && !isIndexSparse) { // last appended is always a SelectIterator const auto lastAppendedIt = lastAppendedOrClosed(); diff --git a/cpp_src/core/query/dsl/dslencoder.cc b/cpp_src/core/query/dsl/dslencoder.cc index 3e0404158..3561d5b24 100644 --- a/cpp_src/core/query/dsl/dslencoder.cc +++ b/cpp_src/core/query/dsl/dslencoder.cc @@ -1,4 +1,5 @@ #include "dslencoder.h" + #include #include "core/cjson/jsonbuilder.h" #include "core/keyvalue/key_string.h" diff --git a/cpp_src/core/query/dsl/dslencoder.h b/cpp_src/core/query/dsl/dslencoder.h index 55928152b..b629d74b5 100644 --- a/cpp_src/core/query/dsl/dslencoder.h +++ b/cpp_src/core/query/dsl/dslencoder.h @@ -8,5 +8,6 @@ class Query; namespace dsl { std::string toDsl(const Query& query); -} +} // namespace dsl + } // namespace reindexer diff --git a/cpp_src/core/query/dsl/dslparser.cc b/cpp_src/core/query/dsl/dslparser.cc index d5c847fd4..08dbdc6b7 100644 --- a/cpp_src/core/query/dsl/dslparser.cc +++ b/cpp_src/core/query/dsl/dslparser.cc @@ -327,7 +327,6 @@ static void parseFilter(const JsonValue& filter, Query& q, std::vector #include +#include "core/nsselecter/joinedselector.h" +#include "core/nsselecter/joinedselectormock.h" #include "core/payload/payloadiface.h" #include "query.h" #include "tools/serializer.h" @@ -9,6 +12,44 @@ namespace reindexer { +template +std::string JoinQueryEntry::Dump(const std::vector &joinedSelectors) const { + WrSerializer ser; + const auto &js = joinedSelectors.at(joinIndex); + const auto &q = js.JoinQuery(); + ser << js.Type() << " (" << q.GetSQL() << ") ON "; + ser << '('; + for (const auto &jqe : q.joinEntries_) { + if (&jqe != &q.joinEntries_.front()) { + ser << ' ' << jqe.op_ << ' '; + } else { + assertrx(jqe.op_ == OpAnd); + } + ser << q._namespace << '.' << jqe.joinIndex_ << ' ' << InvertJoinCondition(jqe.condition_) << ' ' << jqe.index_; + } + ser << ')'; + return std::string{ser.Slice()}; +} +template std::string JoinQueryEntry::Dump(const JoinedSelectors &) const; +template std::string JoinQueryEntry::Dump(const std::vector &) const; + +template +std::string JoinQueryEntry::DumpOnCondition(const std::vector &joinedSelectors) const { + WrSerializer ser; + const auto &js = joinedSelectors.at(joinIndex); + const auto &q = js.JoinQuery(); + ser << js.Type() << " ON ("; + for (const auto &jqe : q.joinEntries_) { + if (&jqe != &q.joinEntries_.front()) { + ser << ' ' << jqe.op_ << ' '; + } + ser << q._namespace << '.' << jqe.joinIndex_ << ' ' << InvertJoinCondition(jqe.condition_) << ' ' << jqe.index_; + } + ser << ')'; + return std::string{ser.Slice()}; +} +template std::string JoinQueryEntry::DumpOnCondition(const JoinedSelectors &) const; + bool QueryEntry::operator==(const QueryEntry &obj) const { return condition == obj.condition && index == obj.index && idxNo == obj.idxNo && distinct == obj.distinct && values.RelaxCompare(obj.values) == 0; @@ -31,6 +72,20 @@ std::string QueryEntry::Dump() const { return std::string{ser.Slice()}; } +std::string QueryEntry::DumpBrief() const { + WrSerializer ser; + { + ser << index << ' ' << condition << ' '; + const bool severalValues = (values.size() > 1); + if (severalValues) { + ser << "(...)"; + } else { + ser << '\'' << values.front().As() << '\''; + } + } + return std::string(ser.Slice()); +} + AggregateEntry::AggregateEntry(AggType type, h_vector fields, SortingEntries sort, unsigned limit, unsigned offset) : type_(type), fields_(std::move(fields)), sortingEntries_{std::move(sort)}, limit_(limit), offset_(offset) { switch (type_) { diff --git a/cpp_src/core/query/queryentry.h b/cpp_src/core/query/queryentry.h index 9391493b1..d2b53ebf8 100644 --- a/cpp_src/core/query/queryentry.h +++ b/cpp_src/core/query/queryentry.h @@ -25,23 +25,10 @@ struct JoinQueryEntry { bool operator!=(const JoinQueryEntry &other) const noexcept { return !operator==(other); } template - std::string Dump(const std::vector &joinedSelectors) const { - WrSerializer ser; - const auto &js = joinedSelectors.at(joinIndex); - const auto &q = js.JoinQuery(); - ser << js.Type() << " (" << q.GetSQL() << ") ON "; - ser << '('; - for (const auto &jqe : q.joinEntries_) { - if (&jqe != &q.joinEntries_.front()) { - ser << ' ' << jqe.op_ << ' '; - } else { - assertrx(jqe.op_ == OpAnd); - } - ser << q._namespace << '.' << jqe.joinIndex_ << ' ' << InvertJoinCondition(jqe.condition_) << ' ' << jqe.index_; - } - ser << ')'; - return std::string{ser.Slice()}; - } + std::string Dump(const std::vector &joinedSelectors) const; + + template + std::string DumpOnCondition(const std::vector &joinedSelectors) const; }; struct QueryEntry { @@ -63,6 +50,7 @@ struct QueryEntry { VariantArray values; std::string Dump() const; + std::string DumpBrief() const; }; class BetweenFieldsQueryEntry { @@ -136,6 +124,8 @@ class QueryEntries static bool checkIfSatisfyCondition(const QueryEntry &, const ConstPayload &, TagsMatcher &); static bool checkIfSatisfyCondition(const BetweenFieldsQueryEntry &, const ConstPayload &, TagsMatcher &); static bool checkIfSatisfyCondition(const VariantArray &lValues, CondType, const VariantArray &rValues); + +protected: static void dumpEqualPositions(size_t level, WrSerializer &ser, const EqualPositions_t &equalPositions) { for (const auto &eq : equalPositions) { for (size_t i = 0; i < level; ++i) { @@ -149,6 +139,7 @@ class QueryEntries ser << ")\n"; } } + template static void dump(size_t level, const_iterator begin, const_iterator end, const std::vector &joinedSelectors, WrSerializer &ser) { for (const_iterator it = begin; it != end; ++it) { @@ -208,11 +199,24 @@ struct QueryJoinEntry { bool operator==(const QueryJoinEntry &) const noexcept; bool operator!=(const QueryJoinEntry &qje) const noexcept { return !operator==(qje); } OpType op_ = OpAnd; - CondType condition_ = CondEq; - std::string index_; - std::string joinIndex_; - int idxNo = -1; - bool reverseNamespacesOrder = false; + CondType condition_ = CondEq; ///< Condition applied to expression: index_ COND joinIndex_ + std::string index_; ///< main ns index field name + std::string joinIndex_; ///< joining ns index field name + int idxNo = -1; ///< index_ field Index number in main ns + bool reverseNamespacesOrder = false; ///< controls SQL encoding order + ///< false: mainNs.index Condition joinNs.joinIndex + ///< true: joinNs.joinIndex Invert(Condition) mainNs.index + + template + std::string DumpCondition(const JS &joinedSelector, bool needOp = false) const { + WrSerializer ser; + const auto &q = joinedSelector.JoinQuery(); + if (needOp) { + ser << ' ' << op_ << ' '; + } + ser << q._namespace << '.' << joinIndex_ << ' ' << InvertJoinCondition(condition_) << ' ' << index_; + return std::string{ser.Slice()}; + } }; struct SortingEntry { diff --git a/cpp_src/core/query/sql/sqlencoder.cc b/cpp_src/core/query/sql/sqlencoder.cc index 4ed0e0bd5..aa82c1110 100644 --- a/cpp_src/core/query/sql/sqlencoder.cc +++ b/cpp_src/core/query/sql/sqlencoder.cc @@ -1,4 +1,5 @@ #include "core/query/sql/sqlencoder.h" + #include "core/keyvalue/geometry.h" #include "core/nsselecter/sortexpression.h" #include "core/queryresults/aggregationresult.h" @@ -7,21 +8,6 @@ namespace reindexer { -const char *SQLEncoder::JoinTypeName(JoinType type) { - switch (type) { - case JoinType::InnerJoin: - return "INNER JOIN"; - case JoinType::OrInnerJoin: - return "OR INNER JOIN"; - case JoinType::LeftJoin: - return "LEFT JOIN"; - case JoinType::Merge: - return "MERGE"; - default: - return ""; - } -} - static void indexToSql(const std::string &index, WrSerializer &ser) { if (index.find('+') == std::string::npos) { ser << index; @@ -68,7 +54,7 @@ SQLEncoder::SQLEncoder(const Query &q) : query_(q) {} void SQLEncoder::DumpSingleJoinQuery(size_t idx, WrSerializer &ser, bool stripArgs) const { assertrx(idx < query_.joinQueries_.size()); const auto &jq = query_.joinQueries_[idx]; - ser << ' ' << JoinTypeName(jq.joinType); + ser << ' ' << jq.joinType; if (jq.entries.Empty() && jq.count == QueryEntry::kDefaultLimit && jq.sortingEntries_.empty()) { ser << ' ' << jq._namespace << " ON "; } else { @@ -101,7 +87,7 @@ void SQLEncoder::dumpJoined(WrSerializer &ser, bool stripArgs) const { void SQLEncoder::dumpMerged(WrSerializer &ser, bool stripArgs) const { for (auto &me : query_.mergeQueries_) { - ser << ' ' << JoinTypeName(me.joinType) << "( "; + ser << ' ' << me.joinType << "( "; me.GetSQL(ser, stripArgs); ser << ')'; } diff --git a/cpp_src/core/query/sql/sqlencoder.h b/cpp_src/core/query/sql/sqlencoder.h index 2e9e1cc5e..c1b217609 100644 --- a/cpp_src/core/query/sql/sqlencoder.h +++ b/cpp_src/core/query/sql/sqlencoder.h @@ -22,11 +22,6 @@ class SQLEncoder { /// @param stripArgs - replace condition values with '?'. void DumpSingleJoinQuery(size_t idx, WrSerializer &ser, bool stripArgs) const; - /// Get readaby Join Type - /// @param type - join tyoe - /// @return string with join type name - static const char *JoinTypeName(JoinType type); - protected: /// Builds print version of a query with join in sql format. /// @param ser - serializer to store SQL string diff --git a/cpp_src/core/reindexerimpl.cc b/cpp_src/core/reindexerimpl.cc index f26086305..2f65bed2e 100644 --- a/cpp_src/core/reindexerimpl.cc +++ b/cpp_src/core/reindexerimpl.cc @@ -1,4 +1,5 @@ #include "core/reindexerimpl.h" + #include #include #include @@ -1103,16 +1104,19 @@ void ReindexerImpl::doSelect(const Query& q, QueryResults& result, NsLocker& auto ns = locks.Get(q._namespace); assertrx(ns); if (!ns) { - throw Error(errParams, "Namespace '%s' is not exists", q._namespace); + throw Error(errParams, "Namespace '%s' does not exist", q._namespace); } std::vector joinQueryResultsContexts; + const auto preselectStartTime = ExplainCalc::Clock::now(); // should be destroyed after results.lockResults() JoinedSelectors mainJoinedSelectors = prepareJoinedSelectors(q, result, locks, func, joinQueryResultsContexts, ctx); prepareJoinResults(q, result); + const ExplainCalc::Duration preselectTimeTotal = ExplainCalc::Clock::now() - preselectStartTime; IsFTQuery isFtQuery{IsFTQuery::NotSet}; { SelectCtx selCtx(q, nullptr); selCtx.joinedSelectors = mainJoinedSelectors.size() ? &mainJoinedSelectors : nullptr; + selCtx.preResultTimeTotal = preselectTimeTotal; selCtx.contextCollectingMode = true; selCtx.functions = &func; selCtx.nsid = 0; diff --git a/cpp_src/core/type_consts_helpers.cc b/cpp_src/core/type_consts_helpers.cc index 323ddc829..26c1eedbf 100644 --- a/cpp_src/core/type_consts_helpers.cc +++ b/cpp_src/core/type_consts_helpers.cc @@ -1,4 +1,5 @@ #include "type_consts_helpers.h" + #include "tools/errors.h" namespace reindexer { @@ -111,4 +112,21 @@ namespace reindexer { } } +[[nodiscard]] std::string_view JoinTypeName(JoinType type) { + using namespace std::string_view_literals; + + switch (type) { + case JoinType::InnerJoin: + return "INNER JOIN"sv; + case JoinType::OrInnerJoin: + return "OR INNER JOIN"sv; + case JoinType::LeftJoin: + return "LEFT JOIN"sv; + case JoinType::Merge: + return "MERGE"sv; + } + assertrx(false); + return "unknown"sv; +} + } // namespace reindexer diff --git a/cpp_src/core/type_consts_helpers.h b/cpp_src/core/type_consts_helpers.h index c5aa63ce2..deef5df98 100644 --- a/cpp_src/core/type_consts_helpers.h +++ b/cpp_src/core/type_consts_helpers.h @@ -11,6 +11,11 @@ namespace reindexer { [[nodiscard]] std::string_view TagTypeToStr(TagType); [[nodiscard]] std::string_view AggTypeToStr(AggType t) noexcept; +/// Get readable Join Type +/// @param type - join type +/// @returns string with join type name +[[nodiscard]] std::string_view JoinTypeName(JoinType type); + template auto& operator<<(T& os, CondType cond) { switch (cond) { @@ -59,18 +64,7 @@ auto& operator<<(T& os, OpType op) { template auto& operator<<(T& os, JoinType jt) { - switch (jt) { - case LeftJoin: - return os << "LEFT JOIN"; - case InnerJoin: - return os << "INNER JOIN"; - case OrInnerJoin: - return os << "OR INNER JOIN"; - case Merge: - return os << "MERGE"; - default: - abort(); - } + return os << JoinTypeName(jt); } template diff --git a/cpp_src/gtests/tests/fixtures/queries_api.h b/cpp_src/gtests/tests/fixtures/queries_api.h index eea582552..9dceb1975 100644 --- a/cpp_src/gtests/tests/fixtures/queries_api.h +++ b/cpp_src/gtests/tests/fixtures/queries_api.h @@ -1461,6 +1461,7 @@ class QueriesApi : public ReindexerApi, public QueriesVerifier { .Distinct(distinct)); for (CondType cond : {CondEq, CondSet, CondLt, CondLe, CondGt, CondGe}) { + const bool multyArgCond = cond == CondEq || cond == CondSet; ExecuteAndVerify(Query(default_namespace) .Where(kFieldNameUuid, cond, randUuid()) .Distinct(distinct.c_str()) @@ -1471,12 +1472,13 @@ class QueriesApi : public ReindexerApi, public QueriesVerifier { .Distinct(distinct.c_str()) .Sort(sortIdx, sortOrder)); - ExecuteAndVerify( - Query(default_namespace) - .Where(kFieldNameUuid, cond, - VariantArray::Create(randUuid(), randStrUuid(), randUuid(), randStrUuid(), randUuid())) - .Distinct(distinct.c_str()) - .Sort(sortIdx, sortOrder)); + ExecuteAndVerify(Query(default_namespace) + .Where(kFieldNameUuid, cond, + multyArgCond ? VariantArray::Create(randUuid(), randStrUuid(), randUuid(), + randStrUuid(), randUuid()) + : VariantArray::Create(randUuid())) + .Distinct(distinct.c_str()) + .Sort(sortIdx, sortOrder)); ExecuteAndVerify(Query(default_namespace) .Where(kFieldNameUuidArr, cond, randUuid()) @@ -1490,27 +1492,37 @@ class QueriesApi : public ReindexerApi, public QueriesVerifier { ExecuteAndVerify(Query(default_namespace) .Where(kFieldNameUuidArr, cond, - VariantArray::Create(randUuid(), randStrUuid(), randUuid(), randStrUuid(), - randUuid(), randStrUuid())) + multyArgCond ? VariantArray::Create(randUuid(), randStrUuid(), randUuid(), + randStrUuid(), randUuid(), randStrUuid()) + : VariantArray::Create(randUuid())) .Distinct(distinct.c_str()) .Sort(sortIdx, sortOrder)); - ExecuteAndVerify( - Query(default_namespace) - .WhereComposite( - kFieldNameUuid + compositePlus + kFieldNameName, cond, - {VariantArray::Create(randUuid(), RandString()), VariantArray::Create(randStrUuid(), RandString()), - VariantArray::Create(randUuid(), RandString()), VariantArray::Create(randStrUuid(), RandString()), - VariantArray::Create(randUuid(), RandString()), VariantArray::Create(randStrUuid(), RandString()), - VariantArray::Create(randUuid(), RandString()), VariantArray::Create(randStrUuid(), RandString()), - VariantArray::Create(randUuid(), RandString()), VariantArray::Create(randStrUuid(), RandString()), - VariantArray::Create(randUuid(), RandString()), VariantArray::Create(randStrUuid(), RandString()), - VariantArray::Create(randUuid(), RandString()), VariantArray::Create(randStrUuid(), RandString()), - VariantArray::Create(randUuid(), RandString()), VariantArray::Create(randStrUuid(), RandString()), - VariantArray::Create(randUuid(), RandString()), VariantArray::Create(randStrUuid(), RandString()), - VariantArray::Create(randUuid(), RandString())}) - .Distinct(distinct.c_str()) - .Sort(sortIdx, sortOrder)); + ExecuteAndVerify(Query(default_namespace) + .WhereComposite(kFieldNameUuid + compositePlus + kFieldNameName, cond, + multyArgCond + ? std::vector{VariantArray::Create(randUuid(), RandString()), + VariantArray::Create(randStrUuid(), RandString()), + VariantArray::Create(randUuid(), RandString()), + VariantArray::Create(randStrUuid(), RandString()), + VariantArray::Create(randUuid(), RandString()), + VariantArray::Create(randStrUuid(), RandString()), + VariantArray::Create(randUuid(), RandString()), + VariantArray::Create(randStrUuid(), RandString()), + VariantArray::Create(randUuid(), RandString()), + VariantArray::Create(randStrUuid(), RandString()), + VariantArray::Create(randUuid(), RandString()), + VariantArray::Create(randStrUuid(), RandString()), + VariantArray::Create(randUuid(), RandString()), + VariantArray::Create(randStrUuid(), RandString()), + VariantArray::Create(randUuid(), RandString()), + VariantArray::Create(randStrUuid(), RandString()), + VariantArray::Create(randUuid(), RandString()), + VariantArray::Create(randStrUuid(), RandString()), + VariantArray::Create(randUuid(), RandString())} + : std::vector{VariantArray::Create(randUuid(), RandString())}) + .Distinct(distinct.c_str()) + .Sort(sortIdx, sortOrder)); } ExecuteAndVerify(Query(default_namespace) diff --git a/cpp_src/server/contrib/server.yml b/cpp_src/server/contrib/server.yml index 0b6b39ab3..e9bf9f7db 100644 --- a/cpp_src/server/contrib/server.yml +++ b/cpp_src/server/contrib/server.yml @@ -3077,6 +3077,9 @@ definitions: postprocess_us: type: integer description: "Query post process time" + preselect_us: + type: integer + description: "Query preselect processing time" prepare_us: type: integer description: "Query prepare and optimize time" @@ -3091,7 +3094,7 @@ definitions: description: "Optimization of sort by uncompleted index has been performed" selectors: type: array - description: "Filter selectos, used to proccess query conditions" + description: "Filter selectors, used to proccess query conditions" items: type: object properties: @@ -3127,6 +3130,68 @@ definitions: explain_select: description: "One of selects in joined namespace execution explainings" $ref: "#/definitions/ExplainDef" + join_on_conditions: + type: array + description: "Describes Join ON conditions injections" + items: + type: object + properties: + namespace: + type: string + description: "Joinable ns name" + on_condition: + type: string + description: "Original ON-conditions clause. SQL-like string" + total_time_us: + type: int + description: "Total amount of time spent on checking and substituting all conditions" + success: + type: boolean + description: "Result of injection attempt" + reason: + type: string + description: "Optional{succeed==false}. Explains condition injection failure" + type: + type: string + description: "Values source: preselect values(by_value) or additional select(select)" + injected_condition: + type: string + description: "Injected condition. SQL-like string" + conditions: + type: array + description: "Individual conditions processing results" + items: + type: object + properties: + condition: + type: string + description: single condition from Join ON section. SQL-like string + total_time_us: + type: integer + description: total time elapsed from injection attempt start till the end of substitution or rejection + explain_select: + description: Optional. Explain of Select subquery + $ref: "#/definitions/ExplainDef" + agg_type: + type: string + description: Optional. Aggregation type used in subquery + enum: + - "min" + - "max" + - "distinct" + success: + type: boolean + description: result of injection attempt + reason: + type: string + description: Optional. Explains condition injection failure + new_condition: + type: string + description: substituted injected condition. SQL-like string + values_count: + type: integer + description: resulting size of query values set + AggregationResDef: type: object diff --git a/cpp_src/server/rpcserver.cc b/cpp_src/server/rpcserver.cc index 10e8d3644..8e37cd945 100644 --- a/cpp_src/server/rpcserver.cc +++ b/cpp_src/server/rpcserver.cc @@ -593,7 +593,8 @@ Error RPCServer::DeleteQuery(cproto::Context &ctx, p_string queryBin, std::optio if (flagsOpts) { flags = *flagsOpts; } - ResultFetchOpts opts{.flags = flags, .ptVersions = {}, .fetchOffset = 0, .fetchLimit = INT_MAX, .withAggregations = true}; + int32_t ptVersion = -1; + ResultFetchOpts opts{.flags = flags, .ptVersions = {&ptVersion, 1}, .fetchOffset = 0, .fetchLimit = INT_MAX, .withAggregations = true}; return sendResults(ctx, qres, RPCQrId(), opts); } diff --git a/iterator.go b/iterator.go index 4d8e84a0f..5d46e1777 100644 --- a/iterator.go +++ b/iterator.go @@ -16,6 +16,8 @@ import ( type ExplainSelector struct { // Field or index name Field string `json:"field"` + // Field type enum: indexed, non-indexed + FieldType string `json:"field_type,omitempty"` // Method, used to process condition Method string `json:"method"` // Number of uniq keys, processed by this selector (may be incorrect, in case of internal query optimization/caching @@ -39,6 +41,8 @@ type ExplainSelector struct { type ExplainResults struct { // Total query execution time TotalUs int `json:"total_us"` + // Query preselect build and select time + PreselectUs int `json:"preselect_us"` // Query prepare and optimize time PrepareUs int `json:"prepare_us"` // Indexes keys selection time @@ -55,6 +59,48 @@ type ExplainResults struct { SortByUncommittedIndex bool `json:"sort_by_uncommitted_index"` // Filter selectors, used to proccess query conditions Selectors []ExplainSelector `json:"selectors"` + // Explaining attempts to inject Join queries ON-conditions into the Main Query WHERE clause + OnConditionsInjections []ExplainJoinOnInjections `json:"on_conditions_injections,omitempty"` +} + +// Describes the process of a single JOIN-query ON-conditions injection into the Where clause of a main query +type ExplainJoinOnInjections struct { + // joinable ns name + RightNsName string `json:"namespace"` + // original ON-conditions clause. SQL-like string + JoinOnCondition string `json:"on_condition"` + // total amount of time spent on checking and substituting all conditions + TotalTimeUs int `json:"total_time_us"` + // result of injection attempt + Succeed bool `json:"success"` + // optional{succeed==false}. Explains condition injection failure + Reason string `json:"reason,omitempty"` + // by_value or select + Type string `json:"type"` + // Injected condition. SQL-like string + InjectedCondition string `json:"injected_condition"` + // individual conditions processing results + Conditions []ExplainConditionInjection `json:"conditions,omitempty"` +} + +// Describes an injection attempt of a single condition from the ON-clause of a JOIN-query +type ExplainConditionInjection struct { + // single condition from Join ON section. SQL-like string + InitialCondition string `json:"condition"` + // total time elapsed from injection attempt start till the end of substitution or rejection + TotalTime int `json:"total_time_us"` + // optoinal{JoinOnInjection.type == Select}. Explain raw string from Select subquery + Explain *ExplainResults `json:"explain_select,omitempty"` + // Optional. Aggregation type used in subquery + AggType string `json:"agg_type,omitempty"` + // result of injection attempt + Succeed bool `json:"success"` + // optional{succeed==false}. Explains condition injection failure + Reason string `json:"reason,omitempty"` + // substituted condition in QueryEntry. SQL-like string + NewCondition string `json:"new_condition"` + // resulting size of query values set + ValuesCount int `json:"values_count"` } func errIterator(err error) *Iterator { diff --git a/test/composite_indexes_test.go b/test/composite_indexes_test.go index fd6bf7ba8..63182ac15 100644 --- a/test/composite_indexes_test.go +++ b/test/composite_indexes_test.go @@ -89,10 +89,11 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -112,10 +113,11 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -135,10 +137,11 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first1+third", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+third", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -158,10 +161,11 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -182,16 +186,18 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "id", - Method: "index", - Keys: 1, - Matched: 1, + Field: "id", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -212,16 +218,18 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "id", - Method: "index", - Keys: 1, - Matched: 1, + Field: "id", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -242,16 +250,18 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "id", - Method: "index", - Keys: 1, - Matched: 1, + Field: "id", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -273,16 +283,18 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "second1+second2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "second1+second2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -305,22 +317,25 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "id", - Method: "index", - Keys: 1, - Matched: 1, + Field: "id", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "second1+second2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "second1+second2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -343,22 +358,25 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "id", - Method: "index", - Keys: 1, - Matched: 1, + Field: "id", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "second1+second2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "second1+second2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -381,22 +399,25 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "second1+second2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "second1+second2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "id", - Method: "index", - Keys: 1, - Matched: 1, + Field: "id", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -421,22 +442,25 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "second1+second2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "second1+second2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "id", - Method: "index", - Keys: 1, - Matched: 1, + Field: "id", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -464,6 +488,7 @@ func TestCompositeIndexesSubstitution(t *testing.T) { }, { Field: "first2 or first1", + FieldType: "indexed", Method: "scan", Comparators: 2, Matched: 1, @@ -501,12 +526,14 @@ func TestCompositeIndexesSubstitution(t *testing.T) { Selectors: []expectedExplain{ { Field: "first2", + FieldType: "indexed", Method: "scan", Comparators: 1, Matched: 1, }, { Field: "second2", + FieldType: "indexed", Method: "scan", Comparators: 1, Matched: 1, @@ -518,12 +545,14 @@ func TestCompositeIndexesSubstitution(t *testing.T) { Selectors: []expectedExplain{ { Field: "second1", + FieldType: "indexed", Method: "scan", Comparators: 1, Matched: 0, }, { Field: "first1", + FieldType: "indexed", Method: "scan", Comparators: 1, Matched: 0, @@ -565,16 +594,18 @@ func TestCompositeIndexesSubstitution(t *testing.T) { Field: "(id and first1+first2)", Selectors: []expectedExplain{ { - Field: "id", - Method: "index", - Keys: 1, - Matched: 1, + Field: "id", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, }, @@ -582,16 +613,18 @@ func TestCompositeIndexesSubstitution(t *testing.T) { Field: "or (second1+second2 and id)", Selectors: []expectedExplain{ { - Field: "second1+second2", - Method: "index", - Keys: 1, - Matched: 0, + Field: "second1+second2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 0, }, { - Field: "id", - Method: "index", - Keys: 1, - Matched: 0, + Field: "id", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 0, }, }, }, @@ -623,18 +656,21 @@ func TestCompositeIndexesSubstitution(t *testing.T) { }, { Field: "first1", + FieldType: "indexed", Method: "scan", Comparators: 1, Matched: 1, }, { Field: "first2 or second2", + FieldType: "indexed", Method: "scan", Comparators: 2, Matched: 1, }, { Field: "second1", + FieldType: "indexed", Method: "scan", Comparators: 1, Matched: 1, @@ -660,13 +696,15 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { Field: "second2 or second1", + FieldType: "indexed", Method: "scan", Comparators: 2, Matched: 1, @@ -694,13 +732,15 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { Field: "second2 or second1", + FieldType: "indexed", Method: "scan", Comparators: 2, Matched: 1, @@ -731,16 +771,18 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "second1+second2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "second1+second2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -772,16 +814,18 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { - Field: "second1+second2", - Method: "index", - Keys: 1, - Matched: 1, + Field: "second1+second2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -813,16 +857,18 @@ func TestCompositeIndexesSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "second1+second2", - Method: "index", - Keys: 0, - Matched: 0, + Field: "second1+second2", + FieldType: "indexed", + Method: "index", + Keys: 0, + Matched: 0, }, { - Field: "first1+first2", - Method: "index", - Keys: 1, - Matched: 0, + Field: "first1+first2", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 0, }, }, "") }) @@ -854,10 +900,11 @@ func TestCompositeIndexesBestSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first+second+third", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first+second+third", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -883,10 +930,11 @@ func TestCompositeIndexesBestSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first+second+third+fourth", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first+second+third+fourth", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) @@ -910,13 +958,15 @@ func TestCompositeIndexesBestSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first+second+third", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first+second+third", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { Field: "fourth or id", + FieldType: "indexed", Method: "scan", Comparators: 1, Matched: 1, @@ -943,13 +993,15 @@ func TestCompositeIndexesBestSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first+second", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first+second", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { Field: "fourth or third", + FieldType: "indexed", Method: "scan", Comparators: 2, Matched: 1, @@ -975,13 +1027,15 @@ func TestCompositeIndexesBestSubstitution(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first+second", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first+second", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { Field: "fourth or third", + FieldType: "indexed", Method: "scan", Comparators: 2, Matched: 1, @@ -1013,18 +1067,21 @@ func TestCompositeIndexesBestSubstitution(t *testing.T) { }, { Field: "fourth", + FieldType: "indexed", Method: "scan", Comparators: 1, Matched: 1, }, { Field: "second or third", + FieldType: "indexed", Method: "scan", Comparators: 2, Matched: 1, }, { Field: "first", + FieldType: "indexed", Method: "scan", Comparators: 1, Matched: 1, @@ -1082,13 +1139,15 @@ func TestCompositeSubstitutionLimit(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first+second+fourth", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first+second+fourth", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { Field: "third", + FieldType: "indexed", Method: "scan", Comparators: 1, Matched: 1, @@ -1118,13 +1177,15 @@ func TestCompositeSubstitutionLimit(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first+second+third", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first+second+third", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { Field: "fourth", + FieldType: "indexed", Method: "scan", Comparators: 1, Matched: 1, @@ -1154,19 +1215,22 @@ func TestCompositeSubstitutionLimit(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first+second", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first+second", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, { Field: "third", + FieldType: "indexed", Method: "scan", Comparators: 1, Matched: 1, }, { Field: "fourth", + FieldType: "indexed", Method: "scan", Comparators: 1, Matched: 1, @@ -1194,10 +1258,11 @@ func TestCompositeSubstitutionLimit(t *testing.T) { printExplainRes(explainRes) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "first+second+third", - Method: "index", - Keys: 1, - Matched: 1, + Field: "first+second+third", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }) diff --git a/test/eq_and_set_test.go b/test/eq_and_set_test.go index 2fa7e6008..b1a9c7203 100644 --- a/test/eq_and_set_test.go +++ b/test/eq_and_set_test.go @@ -153,11 +153,11 @@ func TestEmptySetAndEqWithoutIdx(t *testing.T) { require.NoError(t, err) t.Run("empty eq in where condition", func(t *testing.T) { - t.Skip("1288") query := DBD.Query(ns). WhereString("name", reindexer.EQ) - _, err := query.Exec().FetchAll() - require.Error(t, err, "For condition required at least 1 argument, but provided 0") + result, err := query.Exec().FetchAll() + require.NoError(t, err) + assert.Nil(t, result) }) t.Run("empty set in where condition", func(t *testing.T) { @@ -178,21 +178,21 @@ func TestEmptySetAndEqWithoutIdx(t *testing.T) { }) t.Run("set and empty eq in where condition", func(t *testing.T) { - t.Skip("1288") query := DBD.Query(ns). WhereString("name", reindexer.EQ). WhereInt("year", reindexer.SET, testItem.Year) - _, err := query.Exec().FetchAll() - require.Error(t, err, "For condition required at least 1 argument, but provided 0") + result, err := query.Exec().FetchAll() + require.NoError(t, err) + assert.Nil(t, result) }) t.Run("empty set and eq in where condition", func(t *testing.T) { - t.Skip("1288") query := DBD.Query(ns). WhereString("name", reindexer.EQ). WhereInt("year", reindexer.SET) - _, err := query.Exec().FetchAll() - require.Error(t, err, "For condition required at least 1 argument, but provided 0") + result, err := query.Exec().FetchAll() + require.NoError(t, err) + assert.Nil(t, result) }) } @@ -206,11 +206,12 @@ func TestEmptySetAndEqWithCompositeIdx(t *testing.T) { require.NoError(t, err) t.Run("empty eq in where condition", func(t *testing.T) { - t.Skip("1288") query := DBD.Query(ns). WhereString("subid", reindexer.EQ) - _, err := query.Exec().FetchAll() - require.Error(t, err, "For condition required at least 1 argument, but provided 0") + result, err := query.Exec().FetchAll() + require.NoError(t, err) + assert.Nil(t, result) + }) t.Run("empty set in where condition", func(t *testing.T) { @@ -231,21 +232,23 @@ func TestEmptySetAndEqWithCompositeIdx(t *testing.T) { }) t.Run("set and empty eq in where condition", func(t *testing.T) { - t.Skip("1288") query := DBD.Query(ns). WhereString("subid", reindexer.EQ). WhereInt32("id", reindexer.SET, testItem.ID) - _, err := query.Exec().FetchAll() - require.Error(t, err, "For condition required at least 1 argument, but provided 0") + result, err := query.Exec().FetchAll() + require.NoError(t, err) + assert.Nil(t, result) + }) t.Run("empty set and eq in where condition", func(t *testing.T) { - t.Skip("1288") query := DBD.Query(ns). WhereString("subid", reindexer.EQ). WhereInt("id", reindexer.SET) - _, err := query.Exec().FetchAll() - require.Error(t, err, "For condition required at least 1 argument, but provided 0") + result, err := query.Exec().FetchAll() + require.NoError(t, err) + assert.Nil(t, result) + }) } diff --git a/test/huge_items_test.go b/test/huge_items_test.go index 451a537fc..3ba2179c2 100644 --- a/test/huge_items_test.go +++ b/test/huge_items_test.go @@ -72,66 +72,75 @@ func FillTestItemWide(nsName string, typ reflect.Type, start int, count int) { func buildWideItemsExpectedExplain(initialIndexes int) []expectedExplain { resExplain := []expectedExplain{ { - Field: "id", - Method: "index", - Keys: 1, - Matched: 1, + Field: "id", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, } // Part of the expected explain set manually and may be changed. Result depends on the inrernal core's substitutions ordering resExplain = append(resExplain, expectedExplain{ Field: "field_6", + FieldType: "indexed", Method: "scan", Keys: 0, Matched: 1, Comparators: 1, }) resExplain = append(resExplain, expectedExplain{ - Field: "field_0+field_1+field_2+field_3", - Method: "index", - Keys: 1, - Matched: 1, + Field: "field_0+field_1+field_2+field_3", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }) resExplain = append(resExplain, expectedExplain{ - Field: "field_4", - Method: "index", - Keys: 1, - Matched: 1, + Field: "field_4", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }) resExplain = append(resExplain, expectedExplain{ - Field: "field_5", - Method: "index", - Keys: 1, - Matched: 1, + Field: "field_5", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }) resExplain = append(resExplain, expectedExplain{ - Field: "field_7", - Method: "index", - Keys: 1, - Matched: 1, + Field: "field_7", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }) resExplain = append(resExplain, expectedExplain{ - Field: "field_8", - Method: "index", - Keys: 1, - Matched: 1, + Field: "field_8", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }) for i := 9; i < initialIndexes-1; { var explainField expectedExplain if i%3 == 0 { explainField = expectedExplain{ - Field: fmt.Sprintf("field_%d+field_%d+field_%d+field_%d", i, i+1, i+2, i+3), - Method: "index", - Keys: 1, - Matched: 1, + Field: fmt.Sprintf("field_%d+field_%d+field_%d+field_%d", i, i+1, i+2, i+3), + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, } i += 4 } else { explainField = expectedExplain{ - Field: fmt.Sprintf("field_%d", i), - Method: "index", - Keys: 1, - Matched: 1, + Field: fmt.Sprintf("field_%d", i), + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, } i += 1 } diff --git a/test/join_test.go b/test/join_test.go index 0425231f8..6b6f42695 100644 --- a/test/join_test.go +++ b/test/join_test.go @@ -467,6 +467,7 @@ func initNsForExplain(t *testing.T, ns string, count int) { type expectedExplain struct { Field string + FieldType string Method string Keys int Comparators int @@ -476,6 +477,26 @@ type expectedExplain struct { Selectors []expectedExplain } +type expectedExplainConditionInjection struct { + InitialCondition string + AggType string + Succeed bool + Reason string + NewCondition string + ValuesCount int + ConditionSelectors []expectedExplain +} + +type expectedExplainJoinOnInjections struct { + RightNsName string + JoinOnCondition string + Succeed bool + Reason string + Type string + InjectedCondition string + Conditions []expectedExplainConditionInjection +} + func checkExplain(t *testing.T, res []reindexer.ExplainSelector, expected []expectedExplain, fieldName string) { require.Equal(t, len(expected), len(res)) for i := 0; i < len(expected); i++ { @@ -486,6 +507,7 @@ func checkExplain(t *testing.T, res []reindexer.ExplainSelector, expected []expe } else { assert.Equalf(t, len(res[i].Selectors), 0, fieldName+expected[i].Field) assert.Equalf(t, expected[i].Field, res[i].Field, fieldName+expected[i].Field) + assert.Equalf(t, expected[i].FieldType, res[i].FieldType, fieldName+expected[i].Field) assert.Equalf(t, expected[i].Method, res[i].Method, fieldName+expected[i].Field) assert.Equalf(t, expected[i].Matched, res[i].Matched, fieldName+expected[i].Field) assert.Equalf(t, expected[i].Keys, res[i].Keys, fieldName+expected[i].Field) @@ -504,6 +526,39 @@ func checkExplain(t *testing.T, res []reindexer.ExplainSelector, expected []expe } } +func checkExplainConditionInjection(t *testing.T, resConditions []reindexer.ExplainConditionInjection, expectedConditions []expectedExplainConditionInjection) { + for i := 0; i < len(expectedConditions); i++ { + assert.Equal(t, expectedConditions[i].InitialCondition, resConditions[i].InitialCondition) + assert.Equal(t, expectedConditions[i].AggType, resConditions[i].AggType) + assert.Equal(t, expectedConditions[i].Succeed, resConditions[i].Succeed) + assert.Equal(t, expectedConditions[i].Reason, resConditions[i].Reason) + assert.Equal(t, expectedConditions[i].NewCondition, resConditions[i].NewCondition) + assert.Equal(t, expectedConditions[i].ValuesCount, resConditions[i].ValuesCount) + if len(expectedConditions[i].ConditionSelectors) == 0 { + assert.Nil(t, resConditions[i].Explain) + } else { + checkExplain(t, resConditions[i].Explain.Selectors, expectedConditions[i].ConditionSelectors, "") + } + } +} + +func checkExplainJoinOnInjections(t *testing.T, res []reindexer.ExplainJoinOnInjections, expected []expectedExplainJoinOnInjections) { + require.Equal(t, len(expected), len(res)) + for i := 0; i < len(expected); i++ { + assert.Equal(t, expected[i].RightNsName, res[i].RightNsName) + assert.Equal(t, expected[i].JoinOnCondition, res[i].JoinOnCondition) + assert.Equal(t, expected[i].Succeed, res[i].Succeed) + assert.Equal(t, expected[i].Reason, res[i].Reason) + assert.Equal(t, expected[i].Type, res[i].Type) + assert.Equal(t, expected[i].InjectedCondition, res[i].InjectedCondition) + if len(expected[i].Conditions) == 0 { + assert.Nil(t, res[i].Conditions) + } else { + checkExplainConditionInjection(t, res[i].Conditions, expected[i].Conditions) + } + } +} + func TestExplainJoin(t *testing.T) { nsMain := "test_explain_main" nsJoined := "test_explain_joined" @@ -535,6 +590,7 @@ func TestExplainJoin(t *testing.T) { }, { Field: "not data", + FieldType: "indexed", Method: "index", Keys: 1, Comparators: 0, @@ -545,6 +601,7 @@ func TestExplainJoin(t *testing.T) { Selectors: []expectedExplain{ { Field: "id", + FieldType: "indexed", Method: "scan", Keys: 0, Comparators: 1, @@ -560,6 +617,7 @@ func TestExplainJoin(t *testing.T) { JoinSelect: []expectedExplain{ { Field: "id", + FieldType: "indexed", Method: "index", Keys: 1, Comparators: 0, @@ -567,6 +625,7 @@ func TestExplainJoin(t *testing.T) { }, { Field: "data", + FieldType: "indexed", Method: "scan", Keys: 0, Comparators: 1, @@ -581,6 +640,7 @@ func TestExplainJoin(t *testing.T) { Selectors: []expectedExplain{ { Field: "id", + FieldType: "indexed", Method: "scan", Keys: 0, Comparators: 1, @@ -595,6 +655,7 @@ func TestExplainJoin(t *testing.T) { Preselect: []expectedExplain{ { Field: "data", + FieldType: "indexed", Method: "index", Keys: 3, Comparators: 0, @@ -607,6 +668,7 @@ func TestExplainJoin(t *testing.T) { }, { Field: "or id", + FieldType: "indexed", Method: "index", Keys: 1, Comparators: 0, @@ -621,6 +683,7 @@ func TestExplainJoin(t *testing.T) { Preselect: []expectedExplain{ { Field: "data", + FieldType: "indexed", Method: "index", Keys: 1, Comparators: 0, @@ -630,4 +693,55 @@ func TestExplainJoin(t *testing.T) { JoinSelect: nil, }, }, "") + checkExplainJoinOnInjections(t, explainRes.OnConditionsInjections, []expectedExplainJoinOnInjections{ + { + RightNsName: "test_explain_joined", + JoinOnCondition: "INNER JOIN ON (test_explain_joined.id = id)", + Succeed: true, + Type: "select", + InjectedCondition: "(id IN (...) )", + Conditions: []expectedExplainConditionInjection{ + { + InitialCondition: "test_explain_joined.id = id", + AggType: "distinct", + Succeed: true, + NewCondition: "id IN (...)", + ValuesCount: 19, + ConditionSelectors: []expectedExplain{ + { + Field: "id", + FieldType: "indexed", + Method: "index", + Keys: 20, + Comparators: 0, + Matched: 20, + }, + { + Field: "data", + FieldType: "indexed", + Method: "scan", + Keys: 0, + Comparators: 1, + Matched: 19, + }, + }, + }, + }, + }, + { + RightNsName: "test_explain_joined", + JoinOnCondition: "OR INNER JOIN ON (test_explain_joined.id = id)", + Succeed: true, + Type: "by_value", + InjectedCondition: "(id IN (...) )", + Conditions: []expectedExplainConditionInjection{ + { + InitialCondition: "test_explain_joined.id = id", + Succeed: true, + NewCondition: "id IN (...)", + ValuesCount: 3, + }, + }, + }, + }) } diff --git a/test/uuid_test.go b/test/uuid_test.go index e381d4497..8cb4e9194 100644 --- a/test/uuid_test.go +++ b/test/uuid_test.go @@ -106,10 +106,11 @@ func checkExplainSelect(t *testing.T, it reindexer.Iterator, item interface{}) { assert.NoError(t, err) checkExplain(t, explainRes.Selectors, []expectedExplain{ { - Field: "uuid", - Method: "index", - Keys: 1, - Matched: 1, + Field: "uuid", + FieldType: "indexed", + Method: "index", + Keys: 1, + Matched: 1, }, }, "") }