DATAMONGO-2475 - Polishing.

Fix reduction of $and/$or operations with more than two arguments.

Original Pull Request: #834
This commit is contained in:
Christoph Strobl
2020-03-18 09:18:27 +01:00
parent 7b5fea960f
commit 0e0d726457
2 changed files with 60 additions and 43 deletions

View File

@@ -21,6 +21,8 @@ import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.regex.Pattern;
import org.bson.BsonJavaScript;
@@ -183,25 +185,16 @@ abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
return asDocument(asDBKey(expr, 0), "");
} else if (op == Ops.AND) {
List<Map<Object, Object>> pendingDocuments = new LinkedList<>();
for (int i = 0; i < 2; i++) {
Map<Object, Object> document = (Map<Object, Object>) handle(expr.getArg(i));
if (document.keySet().size() == 1 && document.containsKey("$and")) {
pendingDocuments.addAll((Collection<Map<Object, Object>>) document.get("$and"));
} else {
pendingDocuments.add(document);
}
}
Queue<Map<Object, Object>> pendingDocuments = collectConnectorArgs("$and", expr);
List<Map<Object, Object>> unmergeableDocuments = new ArrayList<>();
List<Map<Object, Object>> generatedDocuments = new ArrayList<>();
List<Map<Object, Object>> unmergeableDocuments = new LinkedList<>();
while (!pendingDocuments.isEmpty()) {
List<Map<Object, Object>> generatedDocuments = new LinkedList<>();
do {
Map<Object, Object> lhs = pendingDocuments.remove(0);
Map<Object, Object> lhs = pendingDocuments.poll();
for (Map<Object, Object> rhs : pendingDocuments) {
LinkedHashSet<Object> lhs2 = new LinkedHashSet<>(lhs.keySet());
Set<Object> lhs2 = new LinkedHashSet<>(lhs.keySet());
lhs2.retainAll(rhs.keySet());
if (lhs2.isEmpty()) {
lhs.putAll(rhs);
@@ -211,18 +204,11 @@ abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
}
generatedDocuments.add(lhs);
pendingDocuments = unmergeableDocuments;
pendingDocuments = new LinkedList<>(unmergeableDocuments);
unmergeableDocuments = new LinkedList<>();
} while(!pendingDocuments.isEmpty());
if (generatedDocuments.size() == 1) {
return generatedDocuments.get(0);
} else {
List<Object> list = new ArrayList<>(expr.getArgs().size());
list.addAll(generatedDocuments);
return asDocument("$and", list);
}
return generatedDocuments.size() == 1 ? generatedDocuments.get(0) : asDocument("$and", generatedDocuments);
} else if (op == Ops.NOT) {
// Handle the not's child
Operation<?> subOperation = (Operation<?>) expr.getArg(0);
@@ -237,18 +223,7 @@ abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
}
} else if (op == Ops.OR) {
List<Object> list = new LinkedList<>();
for (int i = 0; i < 2; i++) {
Map<Object, Object> document = (Map<Object, Object>) handle(expr.getArg(i));
if (document.keySet().size() == 1 && document.containsKey("$or")) {
list.addAll((Collection<Object>) document.get("$or"));
} else {
list.add(document);
}
}
return asDocument("$or", list);
return asDocument("$or", collectConnectorArgs("$or", expr));
} else if (op == Ops.NE) {
Path<?> path = (Path<?>) expr.getArg(0);
@@ -466,4 +441,19 @@ abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
public Object visit(ParamExpression<?> expr, Void context) {
throw new UnsupportedOperationException();
}
private LinkedList<Map<Object, Object>> collectConnectorArgs(String operator, Operation<?> operation) {
LinkedList<Map<Object, Object>> pendingDocuments = new LinkedList<>();
for (Expression<?> exp : operation.getArgs()) {
Map<Object, Object> document = (Map<Object, Object>) handle(exp);
if (document.keySet().size() == 1 && document.containsKey(operator)) {
pendingDocuments.addAll((Collection<Map<Object, Object>>) document.get(operator));
} else {
pendingDocuments.add(document);
}
}
return pendingDocuments;
}
}

View File

@@ -20,6 +20,7 @@ import static com.querydsl.core.types.ExpressionUtils.predicate;
import static com.querydsl.core.types.dsl.Expressions.*;
import static org.assertj.core.api.Assertions.*;
import java.util.Arrays;
import java.util.Collections;
import org.bson.Document;
@@ -29,7 +30,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
@@ -179,25 +179,52 @@ public class SpringDataMongodbSerializerUnitTests {
@Test // DATAMONGO-2475
public void chainedOrsInSameDocument() {
Predicate predicate = QPerson.person.firstname.eq("firstname_value").or(
QPerson.person.lastname.eq("lastname_value")).or(QPerson.person.age.goe(30)).or(
QPerson.person.age.loe(20)).or(QPerson.person.uniqueId.isNull());
Predicate predicate = QPerson.person.firstname.eq("firstname_value")
.or(QPerson.person.lastname.eq("lastname_value")).or(QPerson.person.age.goe(30)).or(QPerson.person.age.loe(20))
.or(QPerson.person.uniqueId.isNull());
assertThat(serializer.handle(predicate)).isEqualTo(Document.parse(
"{\"$or\": [{\"firstname\": \"firstname_value\"}, {\"lastname\": \"lastname_value\"}, {\"age\": {\"$gte\": 30}}, {\"age\": {\"$lte\": 20}}, {\"uniqueId\": {\"$exists\": false}}]}"));
}
@Test // DATAMONGO-2475
public void chainedNestedOrsInSameDocument() {
Predicate predicate = QPerson.person.firstname.eq("firstname_value")
.or(QPerson.person.lastname.eq("lastname_value")).or(QPerson.person.address.street.eq("spring"));
assertThat(serializer.handle(predicate)).isEqualTo(Document.parse(
"{\"$or\": [{\"firstname\": \"firstname_value\"}, {\"lastname\": \"lastname_value\"}, {\"add.street\": \"spring\"}]}"));
}
@Test // DATAMONGO-2475
public void chainedAndsInSameDocument() {
Predicate predicate = QPerson.person.firstname.eq("firstname_value").and(
QPerson.person.lastname.eq("lastname_value")).and(QPerson.person.age.goe(30)).and(
QPerson.person.age.loe(20)).and(QPerson.person.uniqueId.isNull());
Predicate predicate = QPerson.person.firstname.eq("firstname_value")
.and(QPerson.person.lastname.eq("lastname_value")).and(QPerson.person.age.goe(30))
.and(QPerson.person.age.loe(20)).and(QPerson.person.uniqueId.isNull());
assertThat(serializer.handle(predicate)).isEqualTo(Document.parse(
"{\"$and\": [{\"firstname\": \"firstname_value\", \"lastname\": \"lastname_value\", \"age\": {\"$gte\": 30}, \"uniqueId\": {\"$exists\": false}}, {\"age\": {\"$lte\": 20}}]}"));
}
@Test // DATAMONGO-2475
void chainMultipleAndFlattensCorrectly() {
Document p1doc = Document.parse("{ \"$or\" : [ { \"firstname\" : \"fn\"}, { \"lastname\" : \"ln\" } ] }");
Document p2doc = Document
.parse("{ \"$or\" : [ { \"age\" : { \"$gte\" : 20 } }, { \"age\" : { \"$lte\" : 30} } ] }");
Document p3doc = Document.parse("{ \"$or\" : [ { \"add.city\" : \"c\"}, { \"add.zipCode\" : \"0\" } ] }");
Document expected = new Document("$and", Arrays.asList(p1doc, p2doc, p3doc));
Predicate predicate1 = QPerson.person.firstname.eq("fn").or(QPerson.person.lastname.eq("ln"));
Predicate predicate2 = QPerson.person.age.goe(20).or(QPerson.person.age.loe(30));
Predicate predicate3 = QPerson.person.address.city.eq("c").or(QPerson.person.address.zipCode.eq("0"));
PredicateOperation testExpression = predicate(Ops.AND, predicate1, predicate2, predicate3);
assertThat(serializer.handle(testExpression)).isEqualTo(expected);
}
class Address {
String id;
String street;