@@ -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