Skip to content

Commit 79161ef

Browse files
author
Tal Kitron
committed
Merge branch 'master' into add-not-function-support
# Conflicts: # test/Nest.OData.Tests/FilterFunctionTests.cs
2 parents e6dc250 + a7e7d9a commit 79161ef

File tree

2 files changed

+260
-3
lines changed

2 files changed

+260
-3
lines changed

src/Nest.OData/ODataFilterExtensions.cs

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ public static QueryContainer ToQueryContainer(this FilterQueryOption filter, ODa
5050

5151
internal static QueryContainer TranslateExpression(QueryNode node, ODataExpressionContext context = null)
5252
{
53+
if (ShouldOptimizeOrOperation(node))
54+
{
55+
return OptimizeIdenticalFunctionCalls(node as BinaryOperatorNode, context);
56+
}
57+
5358
return node.Kind switch
5459
{
5560
QueryNodeKind.Any => TranslateAnyNode(node as AnyNode, context),
@@ -65,6 +70,71 @@ internal static QueryContainer TranslateExpression(QueryNode node, ODataExpressi
6570
};
6671
}
6772

73+
private static bool ShouldOptimizeOrOperation(QueryNode node)
74+
{
75+
return node is BinaryOperatorNode binaryNode &&
76+
binaryNode.OperatorKind == BinaryOperatorKind.Or &&
77+
binaryNode.Left is SingleValueFunctionCallNode &&
78+
binaryNode.Right is SingleValueFunctionCallNode;
79+
}
80+
81+
private static QueryContainer OptimizeIdenticalFunctionCalls(BinaryOperatorNode node, ODataExpressionContext context)
82+
{
83+
var leftFunc = node.Left as SingleValueFunctionCallNode;
84+
var rightFunc = node.Right as SingleValueFunctionCallNode;
85+
86+
var functionCalls = new[]
87+
{
88+
(Node: leftFunc, IsNested: IsNestedFunctionCall(leftFunc)),
89+
(Node: rightFunc, IsNested: IsNestedFunctionCall(rightFunc))
90+
};
91+
92+
if (AreFunctionCallsIdentical(functionCalls[0], functionCalls[1]))
93+
{
94+
return TranslateExpression(node.Left, context);
95+
}
96+
97+
return TranslateExpression(node.Right, context);
98+
}
99+
100+
private static bool AreFunctionCallsIdentical((SingleValueFunctionCallNode Node, bool IsNested) first, (SingleValueFunctionCallNode Node, bool IsNested) other)
101+
{
102+
if (HasIdenticalFunctionSignature(first.Node, other.Node) == false)
103+
{
104+
return false;
105+
}
106+
107+
if (HasIdenticalParameters(first.Node, other.Node) == false)
108+
{
109+
return false;
110+
}
111+
112+
if (first.IsNested != other.IsNested)
113+
{
114+
return false;
115+
}
116+
117+
return true;
118+
}
119+
120+
private static bool HasIdenticalFunctionSignature(SingleValueFunctionCallNode first, SingleValueFunctionCallNode other)
121+
{
122+
return string.Equals(first.Name, other.Name, StringComparison.OrdinalIgnoreCase) && first.Parameters.Count() == other.Parameters.Count();
123+
}
124+
125+
private static bool HasIdenticalParameters(SingleValueFunctionCallNode first, SingleValueFunctionCallNode other)
126+
{
127+
if (first.Parameters.First().ToString() != other.Parameters.First().ToString())
128+
{
129+
return false;
130+
}
131+
132+
var firstValue = ExtractValue(first.Parameters.Last())?.ToString();
133+
var otherValue = ExtractValue(other.Parameters.Last())?.ToString();
134+
135+
return string.Equals(firstValue, otherValue);
136+
}
137+
68138
private static QueryContainer TranslateAnyNode(AnyNode node, ODataExpressionContext context = null)
69139
{
70140
var fullyQualifiedFieldName = ODataHelpers.ExtractFullyQualifiedFieldName(node.Source, context);
@@ -213,6 +283,7 @@ private static QueryContainer TranslateFunctionCallNode(SingleValueFunctionCallN
213283
private static QueryContainer TranslateOrOperations(BinaryOperatorNode node, ODataExpressionContext context = null)
214284
{
215285
var queries = new List<QueryContainer>();
286+
var functionCalls = new List<(SingleValueFunctionCallNode Node, bool IsNested)>();
216287

217288
void Collect(QueryNode queryNode)
218289
{
@@ -223,13 +294,56 @@ void Collect(QueryNode queryNode)
223294
}
224295
else
225296
{
226-
queries.Add(TranslateExpression(queryNode, context));
297+
if (queryNode is SingleValueFunctionCallNode funcNode)
298+
{
299+
var isNested = IsNestedFunctionCall(funcNode);
300+
functionCalls.Add((funcNode, isNested));
301+
}
302+
303+
var query = TranslateExpression(queryNode, context);
304+
if (query != null)
305+
{
306+
queries.Add(query);
307+
}
227308
}
228309
}
229310

230311
Collect(node);
231312

232-
return new BoolQuery { Should = queries, MinimumShouldMatch = 1 };
313+
if (queries.Any() == false)
314+
{
315+
return null;
316+
}
317+
318+
return OptimizeOrQueries(queries, functionCalls);
319+
}
320+
321+
private static bool IsNestedFunctionCall(SingleValueFunctionCallNode funcNode)
322+
{
323+
return funcNode.Parameters.First() is SingleValuePropertyAccessNode propNode && ODataHelpers.IsNavigationNode(propNode.Source.Kind);
324+
}
325+
326+
private static QueryContainer OptimizeOrQueries(List<QueryContainer> queries, List<(SingleValueFunctionCallNode Node, bool IsNested)> functionCalls)
327+
{
328+
if (CanOptimizeFunctionCalls(functionCalls, queries.Count) == false)
329+
{
330+
return new BoolQuery { Should = queries, MinimumShouldMatch = 1 };
331+
}
332+
333+
var first = functionCalls[0];
334+
335+
return AreAllFunctionCallsIdentical(functionCalls, first) ? queries[0] : new BoolQuery { Should = queries, MinimumShouldMatch = 1 };
336+
}
337+
338+
private static bool CanOptimizeFunctionCalls(List<(SingleValueFunctionCallNode Node, bool IsNested)> functionCalls, int queryCount)
339+
{
340+
return functionCalls.Count > 0 && functionCalls.Count == queryCount;
341+
}
342+
343+
private static bool AreAllFunctionCallsIdentical(List<(SingleValueFunctionCallNode Node, bool IsNested)> functionCalls,
344+
(SingleValueFunctionCallNode Node, bool IsNested) first)
345+
{
346+
return functionCalls.All(f => AreFunctionCallsIdentical(first, f));
233347
}
234348

235349
private static QueryContainer TranslateAndOperations(BinaryOperatorNode node, ODataExpressionContext context = null)
@@ -251,6 +365,17 @@ void Collect(QueryNode queryNode)
251365

252366
Collect(node);
253367

368+
if (queries.Any() == false)
369+
{
370+
return null;
371+
}
372+
373+
// If we have a single query from an AND operation to maintain the expected structure.
374+
if (queries.Count == 1)
375+
{
376+
return new BoolQuery { Must = queries };
377+
}
378+
254379
return new BoolQuery { Must = queries };
255380
}
256381

@@ -320,7 +445,7 @@ private static string ExtractStringValue(QueryNode node)
320445
{
321446
if (constantNode.Value is DateTime dateTime)
322447
{
323-
return dateTime.ToString("o");
448+
return dateTime.ToString("o");
324449
}
325450
else if (constantNode.Value is DateTimeOffset dateTimeOffset)
326451
{

test/Nest.OData.Tests/FilterFunctionTests.cs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,138 @@ public void ContainsToLowerFunction()
118118
Assert.True(JToken.DeepEquals(expectedJObject, actualJObject), "Expected and actual JSON do not match.");
119119
}
120120

121+
[Fact]
122+
public void MultipleIdenticalFunctionCalls()
123+
{
124+
var queryOptions = "$filter=(contains(Category,'Goods')) or (contains(Category,'Goods')) or (contains(Category,'Goods'))".GetODataQueryOptions<Product>();
125+
126+
var elasticQuery = queryOptions.ToElasticQuery();
127+
128+
Assert.NotNull(elasticQuery);
129+
130+
var queryJson = elasticQuery.ToJson();
131+
132+
var expectedJson = @"
133+
{
134+
""query"": {
135+
""wildcard"": {
136+
""Category"": {
137+
""value"": ""*goods*""
138+
}
139+
}
140+
}
141+
}";
142+
143+
var actualJObject = JObject.Parse(queryJson);
144+
var expectedJObject = JObject.Parse(expectedJson);
145+
146+
Assert.True(JToken.DeepEquals(expectedJObject, actualJObject), "Expected and actual JSON do not match.");
147+
}
148+
149+
[Fact]
150+
public void MultipleDifferentFunctionCalls()
151+
{
152+
var queryOptions = "$filter=(contains(Category,'Goods')) or (contains(Category,'Food')) or (contains(Name,'Merchandise'))".GetODataQueryOptions<Product>();
153+
154+
var elasticQuery = queryOptions.ToElasticQuery();
155+
156+
Assert.NotNull(elasticQuery);
157+
158+
var queryJson = elasticQuery.ToJson();
159+
160+
var expectedJson = @"
161+
{
162+
""query"": {
163+
""bool"": {
164+
""minimum_should_match"": 1,
165+
""should"": [
166+
{
167+
""wildcard"": {
168+
""Category"": {
169+
""value"": ""*goods*""
170+
}
171+
}
172+
},
173+
{
174+
""wildcard"": {
175+
""Category"": {
176+
""value"": ""*food*""
177+
}
178+
}
179+
},
180+
{
181+
""wildcard"": {
182+
""Name"": {
183+
""value"": ""*merchandise*""
184+
}
185+
}
186+
}
187+
]
188+
}
189+
}
190+
}";
191+
192+
var actualJObject = JObject.Parse(queryJson);
193+
var expectedJObject = JObject.Parse(expectedJson);
194+
195+
Assert.True(JToken.DeepEquals(expectedJObject, actualJObject), "Expected and actual JSON do not match.");
196+
}
197+
198+
[Fact]
199+
public void MultipleOperatorsFunctionCall()
200+
{
201+
var queryOptions = "$filter=(contains(Category,'Food') and contains(Category,'Goods')) or contains(Name,'Merchandise')".GetODataQueryOptions<Product>();
202+
203+
var elasticQuery = queryOptions.ToElasticQuery();
204+
205+
Assert.NotNull(elasticQuery);
206+
207+
var queryJson = elasticQuery.ToJson();
208+
209+
var expectedJson = @"
210+
{
211+
""query"": {
212+
""bool"": {
213+
""minimum_should_match"": 1,
214+
""should"": [
215+
{
216+
""bool"": {
217+
""must"": [
218+
{
219+
""wildcard"": {
220+
""Category"": {
221+
""value"": ""*food*""
222+
}
223+
}
224+
},
225+
{
226+
""wildcard"": {
227+
""Category"": {
228+
""value"": ""*goods*""
229+
}
230+
}
231+
}
232+
]
233+
}
234+
},
235+
{
236+
""wildcard"": {
237+
""Name"": {
238+
""value"": ""*merchandise*""
239+
}
240+
}
241+
}
242+
],
243+
}
244+
}
245+
}";
246+
247+
var actualJObject = JObject.Parse(queryJson);
248+
var expectedJObject = JObject.Parse(expectedJson);
249+
250+
Assert.True(JToken.DeepEquals(expectedJObject, actualJObject), "Expected and actual JSON do not match.");
251+
}
252+
121253
[Fact]
122254
public void NotFunction()
123255
{

0 commit comments

Comments
 (0)