8000 Feature/out of search in range (#11324) · arangodb/arangodb@e2bc145 · GitHub
[go: up one dir, main page]

Skip to content

Commit e2bc145

Browse files
authored
Feature/out of search in range (#11324)
* WIP * WIP * Added js tests * Cleanup and build fixes
1 parent 8d4dbc1 commit e2bc145

File tree

7 files changed

+337
-3
lines changed

7 files changed

+337
-3
lines changed

arangod/Aql/AqlFunctionFeature.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ void AqlFunctionFeature::addStringFunctions() {
223223
add({"NGRAM_MATCH", ".,.|.,.", flags, &Functions::NgramMatch}); // (attribute, target, [threshold, analyzer]) OR (attribute, target, [analyzer])
224224
add({"NGRAM_SIMILARITY", ".,.,.", flags, &Functions::NgramSimilarity}); // (attribute, target, ngram size)
225225
add({"NGRAM_POSITIONAL_SIMILARITY", ".,.,.", flags, &Functions::NgramPositionalSimilarity}); // (attribute, target, ngram size)
226+
add({"IN_RANGE", ".,.,.,.,.", flags, &Functions::InRange }); // (attribute, lower, upper, include lower, include upper)
226227
// special flags:
227228
add({"RANDOM_TOKEN", ".", Function::makeFlags(FF::CanRunOnDBServer),
228229
&Functions::RandomToken}); // not deterministic and not cacheable

arangod/Aql/Functions.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1834,6 +1834,54 @@ AqlValue Functions::LevenshteinMatch(ExpressionContext* ctx, transaction::Method
18341834
return AqlValue(AqlValueHintBool(dist <= unsignedMaxDistanceValue));
18351835
}
18361836

1837+
/// @brief function IN_RANGE
1838+
AqlValue Functions::InRange(ExpressionContext* ctx, transaction::Methods* trx,
1839+
VPackFunctionParameters const& args) {
1840+
static char const* AFN = "IN_RANGE";
1841+
1842+
auto const argc = args.size();
1843+
1844+
if (argc != 5) {
1845+
registerWarning(
1846+
ctx, AFN,
1847+
arangodb::Result{ TRI_ERROR_QUERY_FUNCTION_ARGUMENT_NUMBER_MISMATCH,
1848+
"5 arguments are expected." });
1849+
return AqlValue(AqlValueHintNull());
1850+
}
1851+
1852+
auto const& attributeVal = extractFunctionParameterValue(args, 0);
1853+
auto const& lowerVal = extractFunctionParameterValue(args, 1);
1854+
auto const& upperVal = extractFunctionParameterValue(args, 2);
1855+
auto const& includeLowerVal = extractFunctionParameterValue(args, 3);
1856+
auto const& includeUpperVal = extractFunctionParameterValue(args, 4);
1857+
1858+
if (ADB_UNLIKELY(!includeLowerVal.isBoolean())) {
1859+
arangodb::aql::registerInvalidArgumentWarning(ctx, AFN);
1860+
return arangodb::aql::AqlValue{ arangodb::aql::AqlValueHintNull{} };
1861+
}
1862+
1863+
if (ADB_UNLIKELY(!includeUpperVal.isBoolean())) {
1864+
arangodb::aql::registerInvalidArgumentWarning(ctx, AFN);
1865+
return arangodb::aql::AqlValue{ arangodb::aql::AqlValueHintNull{} };
1866+
}
1867+
1868+
auto const includeLower = includeLowerVal.toBoolean();
1869+
auto const includeUpper = includeUpperVal.toBoolean();
1870+
1871+
// first check lower bound
1872+
{
1873+
auto const compareLowerResult = AqlValue::Compare(trx, lowerVal, attributeVal, true);
1874+
if ((!includeLower && compareLowerResult >= 0) || (includeLower && compareLowerResult > 0)) {
1875+
return AqlValue(AqlValueHintBool(false));
1876+
}
1877+
}
1878+
1879+
// lower bound is fine, check upper
1880+
auto const compareUpperResult = AqlValue::Compare(trx, attributeVal, upperVal, true);
1881+
return AqlValue(AqlValueHintBool((includeUpper && compareUpperResult <= 0) ||
1882+
(!includeUpper && compareUpperResult < 0)));
1883+
}
1884+
18371885
/// @brief function TO_BOOL
18381886
AqlValue Functions::ToBool(ExpressionContext*, transaction::Methods* /*trx*/,
18391887
VPackFunctionParameters const& parameters) {

arangod/Aql/Functions.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ struct Functions {
157157
VPackFunctionParameters const&);
158158
static AqlValue NgramMatch(ExpressionContext*, transaction::Methods*,
159159
VPackFunctionParameters const&);
160+
static AqlValue InRange(ExpressionContext*, transaction::Methods*,
161+
VPackFunctionParameters const&);
160162
// Date
161163
static AqlValue DateNow(arangodb::aql::ExpressionContext*,
162164
transaction::Methods*, VPackFunctionParameters const&);

arangod/IResearch/IResearchFeature.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ arangodb::aql::AqlValue dummyFilterFunc(arangodb::aql::ExpressionContext*,
9696
arangodb::containers::SmallVector<arangodb::aql::AqlValue> const&) {
9797
THROW_ARANGO_EXCEPTION_MESSAGE(
9898
TRI_ERROR_NOT_IMPLEMENTED,
99-
"ArangoSearch filter functions EXISTS, IN_RANGE, PHRASE "
99+
"ArangoSearch filter functions EXISTS, PHRASE "
100100
" are designed to be used only within a corresponding SEARCH statement "
101101
"of ArangoSearch view."
102102
" Please ensure function signature is correct.");
@@ -403,7 +403,6 @@ void registerFilters(arangodb::aql::AqlFunctionFeature& functions) {
403403
addFunction(functions, { "EXISTS", ".|.,.", flags, &dummyFilterFunc }); // (attribute, [ // "analyzer"|"type"|"string"|"numeric"|"bool"|"null" // ])
404404
addFunction(functions, { "STARTS_WITH", ".,.|.", flags, &startsWithFunc }); // (attribute, prefix, scoring-limit)
405405
addFunction(functions, { "PHRASE", ".,.|.+", flags, &dummyFilterFunc }); // (attribute, input [, offset, input... ] [, analyzer])
406-
addFunction(functions, { "IN_RANGE", ".,.,.,.,.", flags, &dummyFilterFunc }); // (attribute, lower, upper, include lower, include upper)
407406
addFunction(functions, { "MIN_MATCH", ".,.|.+", flags, &minMatchFunc }); // (filter expression [, filter expression, ... ], min match count)
408407
addFunction(functions, { "BOOST", ".,.", flags, &contextFunc }); // (filter expression, boost)
409408
addFunction(functions, { "ANALYZER", ".,.", flags, &contextFunc }); // (filter expression, analyzer)
@@ -584,7 +583,8 @@ bool isFilter(arangodb::aql::Function const& func) noexcept {
584583
func.implementation == &startsWithFunc ||
585584
func.implementation == &aql::Functions::LevenshteinMatch ||
586585
func.implementation == &aql::Functions::Like ||
587-
func.implementation == &aql::Functions::NgramMatch;
586+
func.implementation == &aql::Functions::NgramMatch ||
587+
func.implementation == &aql::Functions::InRange;
588588
}
589589

590590
bool isScorer(arangodb::aql::Function const& func) noexcept {

tests/Aql/InRangeFunctionTest.cpp

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
////////////////////////////////////////////////////////////////////////////////
2+
/// DISCLAIMER
3+
///
4+
/// Copyright 2020 ArangoDB GmbH, Cologne, Germany
5+
///
6+
/// Licensed under the Apache License, Version 2.0 (the "License");
7+
/// you may not use this file except in compliance with the License.
8+
/// You may obtain a copy of the License at
9+
///
10+
/// http://www.apache.org/licenses/LICENSE-2.0
11+
///
12+
/// Unless required by applicable law or agreed to in writing, software
13+
/// distributed under the License is distributed on an "AS IS" BASIS,
14+
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
/// See the License for the specific language governing permissions and
16+
/// limitations under the License.
17+
///
18+
/// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
///
20+
/// @author Andrei Lobov
21+
////////////////////////////////////////////////////////////////////////////////
22+
23+
#include "gtest/gtest.h"
24+
25+
#include "fakeit.hpp"
26+
27+
#include "Aql/ExpressionContext.h"
28+
#include "Aql/Functions.h"
29+
#include "Containers/SmallVector.h"
30+
#include "Transaction/Context.h"
31+
#include "Transaction/Methods.h"
32+
#include "IResearch/common.h"
33+
#include "Mocks/Servers.h"
34+
35+
#include <velocypack/Builder.h>
36+
#include <velocypack/Iterator.h>
37+
#include <velocypack/Parser.h>
38+
#include <velocypack/Slice.h>
39+
#include <velocypack/velocypack-aliases.h>
40+
#include <set>
41+
42+
using namespace arangodb;
43+
using namespace arangodb::aql;
44+
using namespace arangodb::containers;
45+
46+
class InRangeFunctionTest : public ::testing::Test {
47+
public:
48+
InRangeFunctionTest() {
49+
arangodb::tests::init();
50+
}
51+
52+
protected:
53+
AqlValue evaluate(AqlValue const* attribute,
54+
AqlValue const* lower,
55+
AqlValue const* upper,
56+
AqlValue const* includeLower,
57+
AqlValue const* includeUpper,
58+
std::set<int>* warnings = nullptr) {
59+
fakeit::Mock<ExpressionContext> expressionContextMock;
60+
ExpressionContext& expressionContext = expressionContextMock.get();
61+
fakeit::When(Method(expressionContextMock, registerWarning)).AlwaysDo([warnings](int c, char const*) {
62+
if (warnings) {
63+
warnings->insert(c);
64+
}});
65+
fakeit::Mock<transaction::Context> trxCtxMock;
66+
fakeit::When(Method(trxCtxMock, getVPackOptions)).AlwaysDo([]() {
67+
static VPackOptions options;
68+
return &options;
69+
});
70+
TRI_vocbase_t mockVocbase(TRI_VOCBASE_TYPE_NORMAL, testDBInfo(server.server()));
71+
auto trx = server.createFakeTransaction();
72+
SmallVector<AqlValue>::allocator_type::arena_type arena;
73+
SmallVector<AqlValue> params{ arena };
74+
if (attribute) {
75+
params.emplace_back(*attribute);
76+
}
77+
if (lower) {
78+
params.emplace_back(*lower);
79+
}
80+
if (upper) {
81+
params.emplace_back(*upper);
82+
}
83+
if (includeLower) {
84+
params.emplace_back(*includeLower);
85+
}
86+
if (includeUpper) {
87+
params.emplace_back(*includeUpper);
88+
}
89+
return Functions::InRange(&expressionContext, trx.get(), params);
90+
}
91+
92+
void assertInRangeFail(size_t line,
93+
std::set<int> const& expected_warnings,
94+
AqlValue const* attribute,
95+
AqlValue const* lower,
96+
AqlValue const* upper,
97+
AqlValue const* includeLower,
98+
AqlValue const* includeUpper) {
99+
SCOPED_TRACE(testing::Message("assertInRangeFail failed on line:") << line);
100+
std::set<int> warnings;
101+
ASSERT_TRUE(evaluate(attribute, lower, upper, includeLower, includeUpper, &warnings).isNull(false));
102+
ASSERT_EQ(expected_warnings, warnings);
103+
}
104+
105+
void assertInRange(size_t line,
106+
bool expectedValue,
107+
AqlValue const* attribute,
108+
AqlValue const* lower,
109+
AqlValue const* upper,
110+
bool includeLower,
111+
bool includeUpper) {
112+
SCOPED_TRACE(testing::Message("assertInRange failed on line:") << line);
113+
std::set<int> warnings;
114+
AqlValue includeLowerAql{ AqlValueHintBool(includeLower) };
115+
AqlValue includeUpperAql{ AqlValueHintBool(includeUpper) };
116+
auto value = evaluate(attribute, lower, upper,
117+
&includeLowerAql,
118+
&includeUpperAql, &warnings);
119+
ASSERT_TRUE(warnings.empty());
120+
ASSERT_TRUE(value.isBoolean());
121+
ASSERT_EQ(expectedValue, value.toBoolean());
122+
}
123+
124+
private:
125+
arangodb::tests::mocks::MockAqlServer server;
126+
};
127+
128+
TEST_F(InRangeFunctionTest, testValidArgs) {
129+
// strings
130+
{
131+
AqlValue foo("foo");
132+
AqlValue boo("boo");
133+
AqlValue poo("poo");
134+
assertInRange(__LINE__, true, &foo, &boo, &poo, true, true);
135+
assertInRange(__LINE__, false, &foo, &poo, &boo, true, true);
136+
assertInRange(__LINE__, true, &foo, &foo, &poo, true, true);
137+
assertInRange(__LINE__, true, &foo, &foo, &poo, true, false);
138+
assertInRange(__LINE__, false, &foo, &foo, &poo, false, true);
139+
assertInRange(__LINE__, true, &foo, &boo, &foo, true, true);
140+
assertInRange(__LINE__, true, &foo, &boo, &foo, false, true);
141+
assertInRange(__LINE__, false, &foo, &boo, &foo, true, false);
142+
}
143+
// non ASCII
144+
{
145+
AqlValue foo("ПУИ");
146+
AqlValue boo("ПУЗ");
147+
AqlValue poo("ПУЙ");
148+
assertInRange(__LINE__, true, &foo, &boo, &poo, true, true);
149+
assertInRange(__LINE__, false, &foo, &poo, &boo, true, true);
150+
assertInRange(__LINE__, true, &foo, &foo, &poo, true, true);
151+
assertInRange(__LINE__, true, &foo, &foo, &poo, true, false);
152+
assertInRange(__LINE__, false, &foo, &foo, &poo, false, true);
153+
assertInRange(__LINE__, true, &foo, &boo, &foo, true, true);
154+
assertInRange(__LINE__, true, &foo, &boo, &foo, false, true);
155+
assertInRange(__LINE__, false, &foo, &boo, &foo, true, false);
156+
}
157+
// numbers
158+
{
159+
AqlValue foo{ AqlValueHintInt(5) };
160+
AqlValue boo{ AqlValueHintDouble(4.9999) };
161+
AqlValue poo{ AqlValueHintDouble(5.0001) };
162+
assertInRange(__LINE__, true, &foo, &boo, &poo, true, true);
163+
assertInRange(__LINE__, false, &foo, &poo, &boo, true, true);
164+
assertInRange(__LINE__, true, &foo, &foo, &poo, true, true);
165+
assertInRange(__LINE__, true, &foo, &foo, &poo, true, false);
166+
assertInRange(__LINE__, false, &foo, &foo, &poo, false, true);
167+
assertInRange(__LINE__, true, &foo, &boo, &foo, true, true);
168+
assertInRange(__LINE__, true, &foo, &boo, &foo, false, true);
169+
assertInRange(__LINE__, false, &foo, &boo, &foo, true, false);
170+
}
171+
// type mix
172+
{
173+
AqlValue const Int5{ AqlValueHintInt(5) };
174+
AqlValue const NullVal{ AqlValueHintNull{} };
175+
AqlValue const ArrayVal{ AqlValueHintEmptyArray{} };
176+
AqlValue const ObjectVal{ AqlValueHintEmptyObject{} };
177+
AqlValue const StringVal("foo");
178+
assertInRange(__LINE__, true, &StringVal, &NullVal, &ObjectVal, true, true);
179+
assertInRange(__LINE__, true, &StringVal, &NullVal, &ArrayVal, true, true);
180+
assertInRange(__LINE__, false, &StringVal, &ObjectVal, &NullVal, true, true);
181+
assertInRange(__LINE__, false, &StringVal, &ArrayVal, &NullVal, true, true);
182+
assertInRange(__LINE__, false, &StringVal, &ObjectVal, &ArrayVal, true, true);
183+
assertInRange(__LINE__, false, &StringVal, &ArrayVal, &ObjectVal, true, true);
184+
assertInRange(__LINE__, false, &StringVal, &NullVal, &Int5, true, true);
185+
assertInRange(__LINE__, true, &StringVal, &NullVal, &StringVal, true, true);
186+
assertInRange(__LINE__, false, &StringVal, &StringVal, &NullVal, true, true);
187+
assertInRange(__LINE__, false, &StringVal, &StringVal, &Int5, true, true);
188+
assertInRange(__LINE__, true, &Int5, &NullVal, &StringVal, true, true);
189+
assertInRange(__LINE__, false, &Int5, &ArrayVal, &StringVal, true, true);
190+
assertInRange(__LINE__, true, &Int5, &NullVal, &ArrayVal, true, true);
191+
assertInRange(__LINE__, true, &Int5, &NullVal, &ObjectVal, true, true);
192+
assertInRange(__LINE__, false, &ArrayVal, &NullVal, &StringVal, true, true);
193+
assertInRange(__LINE__, true, &ArrayVal, &NullVal, &ObjectVal, true, true);
194+
assertInRange(__LINE__, true, &ArrayVal, &StringVal, &ObjectVal, true, true);
195+
assertInRange(__LINE__, true, &ArrayVal, &Int5, &ObjectVal, true, true);
196+
assertInRange(__LINE__, true, &ObjectVal, &Int5, &ObjectVal, true, true);
197+
assertInRange(__LINE__, false, &ObjectVal, &Int5, &ObjectVal, true, false);
198+
}
199+
}
200+
201+
TEST_F(InRangeFunctionTest, testInvalidArgs) {
202+
const std::set<int> typeMismatchWarning{ TRI_ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH };
203+
const std::set<int> invalidArgsCount{ TRI_ERROR_QUERY_FUNCTION_ARGUMENT_NUMBER_MISMATCH };
204+
AqlValue const ValidString{ "ValidString" };
205+
AqlValue const ValidBool{ AqlValueHintBool{true} };
206+
assertInRangeFail(__LINE__, invalidArgsCount, &ValidString, &ValidString, &ValidString, &ValidBool, nullptr);
207+
assertInRangeFail(__LINE__, typeMismatchWarning, &ValidString, &ValidString, &ValidString, &ValidBool, &ValidString);
208+
assertInRangeFail(__LINE__, typeMismatchWarning, &ValidString, &ValidString, &ValidString, &ValidString, &ValidBool);
209+
}

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ set(ARANGODB_TESTS_SOURCES
3535
Aql/CalculationExecutorTest.cpp
3636
Aql/CountCollectExecutorTest.cpp
3737
Aql/DateFunctionsTest.cpp
38+
Aql/InRangeFunctionTest.cpp
3839
Aql/JaccardFunctionTest.cpp
3940
Aql/LevenshteinMatchFunctionTest.cpp
4041
Aql/DependencyProxyMock.cpp

tests/js/server/aql/aql-functions.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4123,6 +4123,79 @@ function ahuacatlFunctionsTestSuite () {
41234123
assertQueryWarningAndNull(errors.ERROR_QUERY_ARRAY_EXPECTED.code, "RETURN NOOPT(STDDEV_SAMPLE({ }))");
41244124
},
41254125

4126+
////////////////////////////////////////////////////////////////////////////////
4127+
/// @brief test IN_RANGE function
4128+
////////////////////////////////////////////////////////////////////////////////
4129+
testInRange: function () {
4130+
assertQueryError(errors.ERROR_QUERY_FUNCTION_ARGUMENT_NUMBER_MISMATCH.code, "RETURN IN_RANGE()");
4131+
assertQueryWarningAndNull(errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH.code,
4132+
"RETURN IN_RANGE(123, 0, 500, null, true)");
4133+
assertQueryWarningAndNull(errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH.code,
4134+
"RETURN IN_RANGE(123, 0, 500, false, null)");
4135+
assertQueryWarningAndNull(errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH.code,
4136+
"RETURN IN_RANGE(123, 0, 500, 1, true)");
4137+
assertQueryWarningAndNull(errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH.code,
4138+
"RETURN IN_RANGE(123, 0, 500, false, 0)");
4139+
assertQueryWarningAndNull(errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH.code,
4140+
"RETURN IN_RANGE(123, 0, 500, [1, 2, 3], true)");
4141+
assertQueryWarningAndNull(errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH.code,
4142+
"RETURN IN_RANGE(123, 0, 500, false, [1,2,3])");
4143+
assertQueryWarningAndNull(errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH.code,
4144+
"RETURN IN_RANGE(123, 0, 500, 'true', true)");
4145+
assertQueryWarningAndNull(errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH.code,
4146+
"RETURN IN_RANGE(123, 0, 500, false, 'false')");
4147+
{
4148+
let res = getQueryResults("RETURN IN_RANGE(5, 1, 10, false, false)");
4149+
assertEqual(1, res.length);
4150+
assertTrue(res[0]);
4151+
}
4152+
{
4153+
let res = getQueryResults("RETURN IN_RANGE(5+1, 1+1, 10-1, false, false)");
4154+
assertEqual(1, res.length);
4155+
assertTrue(res[0]);
4156+
}
4157+
{
4158+
let res = getQueryResults("RETURN IN_RANGE(MAX([5,6]), MIN([1,0]), MAX([0,6]), false, true)");
4159+
assertEqual(1, res.length);
4160+
assertTrue(res[0]);
4161+
}
4162+
{
4163+
let res = getQueryResults("RETURN IN_RANGE(MAX([5,6]), MIN([1,0]), MAX([0,6]), false, false)");
4164+
assertEqual(1, res.length);
4165+
assertFalse(res[0]);
4166+
}
4167+
{
4168+
let res = getQueryResults("RETURN IN_RANGE(NOOPT(MAX([5,6])), MIN([1,0]), MAX([0,6]), false, false)");
4169+
assertEqual(1, res.length);
4170+
assertFalse(res[0]);
4171+
}
4172+
{
4173+
let res = getQueryResults("RETURN IN_RANGE('foo', MIN([1,0]), 'poo', false, false)");
4174+
assertEqual(1, res.length);
4175+
assertTrue(res[0]);
4176+
}
4177+
{
4178+
let res = getQueryResults("RETURN IN_RANGE('foo', null, 'poo', false, false)");
4179+
assertEqual(1, res.length);
4180+
assertTrue(res[0]);
4181+
}
4182+
{
4183+
let res = getQueryResults("RETURN IN_RANGE(123, null, 'poo', false, false)");
4184+
assertEqual(1, res.length);
4185+
assertTrue(res[0]);
4186+
}
4187+
{
4188+
let res = getQueryResults("RETURN IN_RANGE({a:1}, null, 'poo', false, false)");
4189+
assertEqual(1, res.length);
4190+
assertFalse(res[0]);
4191+
}
4192+
{
4193+
let res = getQueryResults("RETURN IN_RANGE('foo', 'boo', 'poo', false, false)");
4194+
assertEqual(1, res.length);
4195+
assertTrue(res[0]);
4196+
}
4197+
},
4198+
41264199
////////////////////////////////////////////////////////////////////////////////
41274200
/// @brief test non-existing functions
41284201
////////////////////////////////////////////////////////////////////////////////

0 commit comments

Comments
 (0)
0