Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,17 @@ private Truthness computeHeuristicComparisonNonNullValues(Object actualValue, Ob
truthnessOfComparison = SqlExpressionEvaluator.calculateTruthnessForStringComparison(actualString, expectedString, comparisonOperatorType);

} else {
// If both types are supported, but no actual comparison logic is defined,
// we considered them to be incompatible, therefore the comparison returns false.
truthnessOfComparison = C_FALSE;
/*
Both types are supported, but no comparison logic is defined for this combination,
ie the two values are of different, mutually incomparable BSON types.
MongoDB considers such values to be different from each other: an equality check is
then false, but an inequality check is true. Ordering comparisons do not match across
different BSON types either, so those stay false as well.
*/
truthnessOfComparison =
comparisonOperatorType == SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO
? TRUE_C
: C_FALSE;
}
return truthnessOfComparison;
}
Expand Down Expand Up @@ -327,10 +335,7 @@ private Truthness computeHeuristic(EqualsOperation<?> operation, Object document
actualValue = null;
}

return computeHeuristicComparisonNullableValues(
expectedValue,
actualValue,
SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO);
return computeHeuristicForMatchedValue(expectedValue, actualValue);
}

private Truthness computeHeuristicComparisonNullableValues(Object expectedValue, Object actualValue, SqlExpressionEvaluator.ComparisonOperatorType comparisonOperatorType) {
Expand Down Expand Up @@ -384,10 +389,7 @@ private Truthness computeHeuristic(NotEqualsOperation<?> operation, Object docum
} else {
actualValue = null;
}
return computeHeuristicComparisonNullableValues(
expectedValue,
actualValue,
SqlExpressionEvaluator.ComparisonOperatorType.NOT_EQUALS_TO);
return computeHeuristicForMatchedValue(expectedValue, actualValue).invert();
}


Expand Down Expand Up @@ -484,8 +486,27 @@ private Truthness computeHeuristic(AndOperation operation, Object document) {
private Truthness computeHeuristic(InOperation<?> operation, Object document) {
requireNonNullQueryAndDocument(operation, document);

List<?> expectedValueList = operation.getValues();
final String fieldName = operation.getFieldName();
return computeHeuristicForMembership(operation.getFieldName(), operation.getValues(), document);
}

/**
* Computes the heuristic score for the membership of a field's value in a list of expected
* values, ie the condition shared by {"f",{"$in": [...]}} and, negated, by
* {"f",{"$nin": [...]}}. The condition holds when the field matches any one of the expected
* values, in the sense of {@link #computeHeuristicForMatchedValue(Object, Object)}.
*
* @param fieldName the name of the field the query is about
* @param expectedValues the values the query lists as candidates
* @param document the BSON document to evaluate the heuristic score against
* @return a Truthness object representing how close the field is to holding one of the values
*/
private Truthness computeHeuristicForMembership(String fieldName, List<?> expectedValues, Object document) {
Objects.requireNonNull(expectedValues);

if (expectedValues.isEmpty()) {
// no candidate value can be matched
return C_FALSE;
}

final Object actualValue;
if (documentContainsField(document, fieldName)) {
Expand All @@ -497,41 +518,39 @@ private Truthness computeHeuristic(InOperation<?> operation, Object document) {
actualValue = null;
}

final Truthness res;
if (actualValue instanceof List<?>) {
List<?> actualValueList = (List<?>) actualValue;
res = buildOrAggregationTruthness(actualValueList.stream()
.map(value -> computeHeuristic(value, expectedValueList))
.toArray(Truthness[]::new));
} else {
res = computeHeuristic(actualValue, expectedValueList);
}
return res;
}

private Truthness computeHeuristic(Object actualValue, List<?> expectedValueList) {
Objects.requireNonNull(expectedValueList);

Truthness res = buildOrAggregationTruthness(expectedValueList.stream()
.map(expectedValue -> computeHeuristicComparisonNullableValues(expectedValue, actualValue, SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO))
return buildOrAggregationTruthness(expectedValues.stream()
.map(expectedValue -> computeHeuristicForMatchedValue(expectedValue, actualValue))
.toArray(Truthness[]::new));
return res;
}

private Truthness computeHeuristic(NotInOperation<?> operation, Object document) {
requireNonNullQueryAndDocument(operation, document);

List<?> expectedValues = operation.getValues();
final String fieldName = operation.getFieldName();

if (!documentContainsField(document, fieldName)) {
// a value that is not there cannot be one of the excluded ones
return TRUE_C;
} else {
Object actualValue = getValue(document, fieldName);
return computeHeuristic(actualValue, expectedValues).invert();
}
/*
$nin is the negation of $in, so it must consider the array elements in the same way:
a document whose array holds any of the excluded values does not match.
*/
return computeHeuristicForMembership(fieldName, operation.getValues(), document).invert();
}

/**
* Computes the heuristic score for a {"f",{"$all": [v1, ..., vn] }} query.
* The condition holds when "f" matches every one of the expected values, so the score is an
* AND aggregation over them. Note the direction: extra elements in the document are
* irrelevant, whereas a missing expected value makes the condition false.
* The field does not have to hold an array: a scalar matches a $all listing only values
* equal to it, which is why {"f": "a"} is matched by {"$all": ["a"]}.
*
* @param operation the {"f",{"$all": [...]}} query encapsulated as an AllOperation
* @param document the BSON document to evaluate the heuristic score against
* @return a Truthness object representing the distance of the document from meeting the condition
*/
private Truthness computeHeuristic(AllOperation<?> operation, Object document) {
requireNonNullQueryAndDocument(operation, document);

Expand All @@ -542,23 +561,47 @@ private Truthness computeHeuristic(AllOperation<?> operation, Object document) {
} else if (expectedValues.isEmpty()) {
return C_FALSE;
} else {
Object actualValues = getValue(document, fieldName);
if (actualValues == null || !(actualValues instanceof List<?>)) {
return C_FALSE;
} else {
List<?> actualValuesList = (List<?>) actualValues;
if (actualValuesList.isEmpty()) {
return C_FALSE;
} else {
Truthness res = buildAndAggregationTruthness(actualValuesList
.stream()
.map(actualValuesListElement ->
computeHeuristic(actualValuesListElement, expectedValues))
.toArray(Truthness[]::new));
return buildSafeScaledTruthness(res);
}
}
Object actualValue = getValue(document, fieldName);
Truthness res = buildAndAggregationTruthness(expectedValues
.stream()
.map(expectedValue -> computeHeuristicForMatchedValue(expectedValue, actualValue))
.toArray(Truthness[]::new));
return buildSafeScaledTruthness(res);
}
}

/**
* Computes the heuristic score for MongoDB's matching of a field against a single value,
* ie the condition that the field holds that value. The field matches when its own value is
* the expected one, and also, if it holds an array, when any element of that array is.
* Both readings apply at once: {"f": ["a","b"]} is matched both by the value ["a","b"] and
* by the value "a".
*
* @param expectedValue the value the query requires the field to hold
* @param actualValue the value held by the field in the document, possibly an array or null
* @return a Truthness object representing how close the field is to holding the expected value
*/
private Truthness computeHeuristicForMatchedValue(Object expectedValue, Object actualValue) {

Truthness wholeValue = computeHeuristicComparisonNullableValues(expectedValue, actualValue,
SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO);

if (!(actualValue instanceof List<?>)) {
return wholeValue;
}

/*
The array as a whole is one of the options, which is also what makes an empty array
work: it holds no element to compare, but it can still be equal to the expected value.
*/
List<?> actualValueList = (List<?>) actualValue;
Truthness[] options = new Truthness[actualValueList.size() + 1];
options[0] = wholeValue;
for (int i = 0; i < actualValueList.size(); i++) {
options[i + 1] = computeHeuristicComparisonNullableValues(expectedValue,
actualValueList.get(i), SqlExpressionEvaluator.ComparisonOperatorType.EQUALS_TO);
}
return buildOrAggregationTruthness(options);
}

private static Truthness buildSafeScaledTruthness(Truthness truthness) {
Expand Down Expand Up @@ -724,13 +767,14 @@ private Truthness computeHeuristic(BitsOperation operation, Object document) {
private Truthness computeHeuristic(NotOperation operation, Object document) {
requireNonNullQueryAndDocument(operation, document);

String fieldName = operation.getFieldName();
if (!documentContainsField(document, fieldName)) {
return TRUE_C;
} else {
QueryOperation condition = operation.getCondition();
return computeHeuristicOnDocument(condition, document).invert();
}
/*
No special case for a missing field here. Several operators do match a document in
which the field is absent (eg $ne, $nin, and $exists with "false"), and $not must then
be false. The inner operators already treat an absent field as a null value, so
negating their score is correct whether or not the field is there.
*/
QueryOperation condition = operation.getCondition();
return computeHeuristicOnDocument(condition, document).invert();
}

private Truthness computeHeuristic(NorOperation operation, Object document) {
Expand Down Expand Up @@ -909,6 +953,9 @@ private Truthness calculateTruthnessForListComparison(List<?> actualList, List<?
final Truthness truthness;
if (actualList.size() != expectedList.size()) {
truthness = C_FALSE;
} else if (actualList.isEmpty()) {
// two empty arrays are equal, and there is no element to aggregate over
truthness = TRUE_C;
} else {
Truthness[] arrayOfTruthnesses = new Truthness[actualList.size()];
for (int i = 0; i < actualList.size(); i++) {
Expand Down
Loading