Skip to content

Commit bb968c2

Browse files
author
Harmen Weber
committed
#1623 Fix the newly added tests regarding nested elements matching multiple conditions in the MongoPersonTest
1 parent bd37261 commit bb968c2

1 file changed

Lines changed: 159 additions & 57 deletions

File tree

criteria/mongo/src/org/immutables/criteria/mongo/FindVisitor.java

Lines changed: 159 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -116,62 +116,19 @@ public Bson visit(Call call) {
116116
rightCall = (Call) rightCall.arguments().get(1);
117117
}
118118

119-
// Handle AND/OR operators within ANY (for compound conditions on array elements)
120-
if (rightCall.operator() == Operators.AND || rightCall.operator() == Operators.OR) {
119+
// Handle AND/OR/NOT operators within ANY (for compound conditions on array elements)
120+
if (rightCall.operator() == Operators.AND ||
121+
rightCall.operator() == Operators.OR ||
122+
rightCall.operator() == Operators.NOT) {
121123
final String arrayField = naming.name(path);
122-
final List<Bson> conditions = new ArrayList<>();
123-
124-
for (Expression expr : rightCall.arguments()) {
125-
Preconditions.checkArgument(expr instanceof Call, "Expected Call but got %s", expr);
126-
Call conditionCall = (Call) expr;
127-
128-
// Handle NOT operator
129-
Operator operator;
130-
if (conditionCall.operator() == Operators.NOT) {
131-
Preconditions.checkArgument(conditionCall.arguments().get(0) instanceof Call, "%s is not a call", conditionCall.arguments().get(0));
132-
conditionCall = (Call) conditionCall.arguments().get(0);
133-
operator = negateOperator(conditionCall.operator());
134-
} else {
135-
operator = conditionCall.operator();
136-
}
137-
138-
Preconditions.checkArgument(conditionCall.arguments().get(0) instanceof Path, "%s is not a path", conditionCall.arguments().get(0));
139-
140-
Path currentPath = Visitors.toPath(conditionCall.arguments().get(0));
141-
Path fullPath = null;
142-
List<Path> paths = new ArrayList<>();
143-
while (currentPath.parent().isPresent()) {
144-
paths.add(currentPath);
145-
currentPath = currentPath.parent().get();
146-
}
147-
Collections.reverse(paths);
148-
for (Path tmpPath : paths) {
149-
fullPath = Path.combine(fullPath, tmpPath);
150-
}
151-
152-
if (conditionCall.operator().arity() == Operator.Arity.UNARY) {
153-
conditions.add(visit(Expressions.unaryCall(conditionCall.operator(), fullPath)));
154-
} else {
155-
conditions.add(binaryCall(Expressions.binaryCall(operator, fullPath, conditionCall.arguments().get(1))));
156-
}
157-
}
158-
159-
Bson combinedCondition = rightCall.operator() == Operators.AND ? Filters.and(conditions) : Filters.or(conditions);
160-
return Filters.elemMatch(arrayField, combinedCondition);
161-
}
162-
163-
Operator operator;
164-
if (rightCall.operator() == Operators.NOT) {
165-
Preconditions.checkArgument(rightCall.arguments().get(0) instanceof Call, "%s is not a call", rightCall.arguments().get(0));
166-
167-
rightCall = (Call) rightCall.arguments().get(0);
168-
operator = negateOperator(rightCall.operator());
169-
} else {
170-
operator = rightCall.operator();
124+
Bson condition = buildElemMatchCondition(rightCall);
125+
return Filters.elemMatch(arrayField, condition);
171126
}
172127

128+
// Handle simple comparison operators within ANY
173129
Preconditions.checkArgument(rightCall.arguments().get(0) instanceof Path,"%s is not a path", rightCall.arguments().get(0));
174130

131+
// Reconstruct the full path by combining the array path with the condition path
175132
Path currentPath = Visitors.toPath(rightCall.arguments().get(0));
176133
List<Path> paths = new ArrayList<>();
177134
while (currentPath.parent().isPresent()) {
@@ -183,11 +140,7 @@ public Bson visit(Call call) {
183140
path = Path.combine(path, tmpPath);
184141
}
185142

186-
if (rightCall.operator().arity() == Operator.Arity.UNARY) {
187-
return visit(Expressions.unaryCall(rightCall.operator(), path));
188-
} else {
189-
return binaryCall(Expressions.binaryCall(operator, path, rightCall.arguments().get(1)));
190-
}
143+
return buildCondition(rightCall, rightCall.operator(), path);
191144
}
192145

193146
if (op.arity() == Operator.Arity.BINARY) {
@@ -354,7 +307,156 @@ private static Operator negateOperator(Operator operator) {
354307
throw new UnsupportedOperationException("No negation for " + operator + " defined");
355308
}
356309
}
357-
310+
311+
/**
312+
* Recursively builds a MongoDB filter condition for use inside $elemMatch.
313+
* Handles arbitrarily nested AND/OR/NOT operators by traversing the expression tree.
314+
*
315+
* @param expr the expression to convert (must be a Call)
316+
* @return a Bson filter suitable for use inside Filters.elemMatch()
317+
*/
318+
private Bson buildElemMatchCondition(Expression expr) {
319+
if (!(expr instanceof Call)) {
320+
throw new IllegalArgumentException("Expected Call but got " + expr);
321+
}
322+
323+
Call call = (Call) expr;
324+
Operator op = call.operator();
325+
326+
// Handle AND/OR recursively
327+
if (op == Operators.AND || op == Operators.OR) {
328+
List<Bson> conditions = new ArrayList<>();
329+
330+
for (Expression arg : call.arguments()) {
331+
Preconditions.checkArgument(arg instanceof Call, "Expected Call but got %s", arg);
332+
Call argCall = (Call) arg;
333+
334+
// Check if this argument has nested AND/OR that needs recursive handling
335+
if (argCall.operator() == Operators.AND || argCall.operator() == Operators.OR) {
336+
// Recursively process nested AND/OR
337+
conditions.add(buildElemMatchCondition(arg));
338+
} else {
339+
// Handle NOT and leaf conditions
340+
conditions.add(buildLeafCondition(argCall));
341+
}
342+
}
343+
344+
return op == Operators.AND ? Filters.and(conditions) : Filters.or(conditions);
345+
}
346+
347+
// For top-level NOT or leaf conditions
348+
return buildLeafCondition(call);
349+
}
350+
351+
/**
352+
* Builds a condition that may be wrapped in NOT operator(s).
353+
* <p>
354+
* Handles three cases:
355+
* <ul>
356+
* <li>Leaf conditions (comparisons like EQUAL, GREATER_THAN, etc.)</li>
357+
* <li>NOT-wrapped leaf conditions (unwraps NOT, unwraps nested ANY, negates operator)</li>
358+
* <li>NOT-wrapped AND/OR (applies De Morgan's laws recursively)</li>
359+
* </ul>
360+
*
361+
* @param call the condition call to process
362+
* @return a Bson filter for this condition
363+
*/
364+
private Bson buildLeafCondition(Call call) {
365+
Operator operator = call.operator();
366+
367+
// Handle NOT operator by unwrapping and negating the inner operator
368+
if (operator == Operators.NOT) {
369+
Preconditions.checkArgument(call.arguments().get(0) instanceof Call, "%s is not a call", call.arguments().get(0));
370+
call = (Call) call.arguments().get(0);
371+
372+
// Unwrap any nested ANY operators (e.g., pet.type.not(type -> type.is(...)))
373+
while (call.operator() == IterableOperators.ANY) {
374+
Preconditions.checkArgument(call.arguments().size() == 2, "ANY should have 2 arguments");
375+
Preconditions.checkArgument(call.arguments().get(1) instanceof Call, "Second argument of ANY should be a Call");
376+
call = (Call) call.arguments().get(1);
377+
}
378+
379+
// If after unwrapping we find AND/OR, apply De Morgan's laws
380+
if (call.operator() == Operators.AND || call.operator() == Operators.OR) {
381+
return applyDeMorgansLaw(call);
382+
}
383+
384+
operator = negateOperator(call.operator());
385+
} else {
386+
operator = call.operator();
387+
}
388+
389+
// Extract and reconstruct the path
390+
Preconditions.checkArgument(call.arguments().get(0) instanceof Path, "%s is not a path", call.arguments().get(0));
391+
Path fullPath = reconstructFullPath(Visitors.toPath(call.arguments().get(0)));
392+
393+
// Build the final condition
394+
return buildCondition(call, operator, fullPath);
395+
}
396+
397+
/**
398+
* Applies De Morgan's laws to negate a logical operator (AND/OR).
399+
* <p>
400+
* Transformations:
401+
* <ul>
402+
* <li>NOT(a AND b) = NOT(a) OR NOT(b)</li>
403+
* <li>NOT(a OR b) = NOT(a) AND NOT(b)</li>
404+
* </ul>
405+
*
406+
* @param call an AND or OR call to be negated
407+
* @return the negated condition with flipped operator
408+
*/
409+
private Bson applyDeMorgansLaw(Call call) {
410+
Operator op = call.operator();
411+
List<Bson> negatedConditions = new ArrayList<>();
412+
413+
for (Expression arg : call.arguments()) {
414+
// Wrap each argument in NOT and process it
415+
Call negatedArg = Expressions.unaryCall(Operators.NOT, (Call) arg);
416+
negatedConditions.add(buildLeafCondition(negatedArg));
417+
}
418+
419+
// Apply De Morgan's law: flip AND <-> OR
420+
return op == Operators.AND ? Filters.or(negatedConditions) : Filters.and(negatedConditions);
421+
}
422+
423+
/**
424+
* Builds a MongoDB filter from a call, operator, and path.
425+
* Creates either a unary expression (using the call's original operator) or
426+
* a binary expression (using the provided operator, which may be negated).
427+
*
428+
* @param call the original call (used for arity and second argument if binary)
429+
* @param operator the operator to use (may be negated from call's operator)
430+
* @param path the path to use in the expression
431+
* @return a Bson filter for this condition
432+
*/
433+
private Bson buildCondition(Call call, Operator operator, Path path) {
434+
if (call.operator().arity() == Operator.Arity.UNARY) {
435+
return visit(Expressions.unaryCall(call.operator(), path));
436+
} else {
437+
return binaryCall(Expressions.binaryCall(operator, path, call.arguments().get(1)));
438+
}
439+
}
440+
441+
/**
442+
* Reconstructs a full path from a potentially partial path by walking up the parent chain.
443+
*/
444+
private static Path reconstructFullPath(Path path) {
445+
List<Path> paths = new ArrayList<>();
446+
Path current = path;
447+
while (current.parent().isPresent()) {
448+
paths.add(current);
449+
current = current.parent().get();
450+
}
451+
Collections.reverse(paths);
452+
453+
Path result = null;
454+
for (Path p : paths) {
455+
result = Path.combine(result, p);
456+
}
457+
return result;
458+
}
459+
358460
/**
359461
* Visitor used when special {@code $expr} needs to be generated like {@code field1 == field2}
360462
* in mongo it would look like:

0 commit comments

Comments
 (0)