Skip to content

Implement document expressions #1052

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,83 @@

package com.mongodb.client.model.expressions;

import org.bson.conversions.Bson;
import org.bson.types.Decimal128;

import java.time.Instant;

import static com.mongodb.client.model.expressions.Expressions.of;

/**
* Expresses a document value. A document is an ordered set of fields, where the
* key is a string value, mapping to a value of any other expression type.
*/
public interface DocumentExpression extends Expression {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both setField and getField allow the field name itself to be an expression. That API should support that as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is supported for Map (arbitrary number of string keys to values all of the same type), but is not supported for Documents, which represent records (fixed number of string-named fields to values of different types). Neither one extends the other, because:

  1. A record cannot have a set(T) method since this would allow fields to be set via a broader type than they actually are.
  2. A map<StringEx> cannot have a method getBoolean(field) because this would allow string values to be pulled out as booleans.

Any situation that requires this is almost certainly more correctly handled via a MapExpression<Expression>, rather than a document (record).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a MapExpression? I don't see that anywhere.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's in #1054

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you show me what the code would look like? I don't understand how it works in practice, e.g. say I have a document like:

{
   s1 : "str1",
   s2: "str2",
   s3 : "str3",
   i: 1
}

How would I do something like set one of the s fields where the suffix of the field name is the value of the i field?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should work:

MapExpression<Expression> map = ofMap(<above map here>);
StringExpression key = of("s").concat(map.get("i").asString());
map.set(key, of("new_value"))

This particular operation should not be done to a Document/Record, for the same reason this is not available in Java except through the use of reflection.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferring at least to map expression PR


DocumentExpression setField(String fieldName, Expression exp);

DocumentExpression unsetField(String fieldName);

Expression getField(String fieldName);

BooleanExpression getBoolean(String fieldName);

BooleanExpression getBoolean(String fieldName, BooleanExpression other);

default BooleanExpression getBoolean(final String fieldName, final boolean other) {
return getBoolean(fieldName, of(other));
}

NumberExpression getNumber(String fieldName);

NumberExpression getNumber(String fieldName, NumberExpression other);

default NumberExpression getNumber(final String fieldName, final double other) {
return getNumber(fieldName, of(other));
}

default NumberExpression getNumber(final String fieldName, final Decimal128 other) {
return getNumber(fieldName, of(other));
}

IntegerExpression getInteger(String fieldName);

IntegerExpression getInteger(String fieldName, IntegerExpression other);

default IntegerExpression getInteger(final String fieldName, final int other) {
return getInteger(fieldName, of(other));
}

default IntegerExpression getInteger(final String fieldName, final long other) {
return getInteger(fieldName, of(other));
}


StringExpression getString(String fieldName);

StringExpression getString(String fieldName, StringExpression other);

default StringExpression getString(final String fieldName, final String other) {
return getString(fieldName, of(other));
}

DateExpression getDate(String fieldName);
DateExpression getDate(String fieldName, DateExpression other);

default DateExpression getDate(final String fieldName, final Instant other) {
return getDate(fieldName, of(other));
}

DocumentExpression getDocument(String fieldName);
DocumentExpression getDocument(String fieldName, DocumentExpression other);

default DocumentExpression getDocument(final String fieldName, final Bson other) {
return getDocument(fieldName, of(other));
}

<T extends Expression> ArrayExpression<T> getArray(String fieldName);

<T extends Expression> ArrayExpression<T> getArray(String fieldName, ArrayExpression<? extends T> other);

DocumentExpression merge(DocumentExpression other);
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,16 @@ public interface Expression {

/**
* also checks for nulls
* @param or
* @param other
* @return
*/
BooleanExpression isBooleanOr(BooleanExpression or);
NumberExpression isNumberOr(NumberExpression or);
StringExpression isStringOr(StringExpression or);
DateExpression isDateOr(DateExpression or);
ArrayExpression<Expression> isArrayOr(ArrayExpression<? extends Expression> or);
<T extends DocumentExpression> T isDocumentOr(T or);
BooleanExpression isBooleanOr(BooleanExpression other);
NumberExpression isNumberOr(NumberExpression other);
IntegerExpression isIntegerOr(IntegerExpression other);
StringExpression isStringOr(StringExpression other);
DateExpression isDateOr(DateExpression other);
<T extends Expression> ArrayExpression<T> isArrayOr(ArrayExpression<? extends T> other);
<T extends DocumentExpression> T isDocumentOr(T other);

StringExpression asString();
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,113 @@ public <R extends Expression> R cond(final R left, final R right) {
return newMqlExpression(ast("$cond", left, right));
}

/** @see DocumentExpression */

private Function<CodecRegistry, AstPlaceholder> getFieldInternal(final String fieldName) {
return (cr) -> {
BsonValue value = fieldName.startsWith("$")
? new BsonDocument("$literal", new BsonString(fieldName))
: new BsonString(fieldName);
return astDoc("$getField", new BsonDocument()
.append("input", this.fn.apply(cr).bsonValue)
.append("field", value));
};
}

@Override
public Expression getField(final String fieldName) {
return new MqlExpression<>(getFieldInternal(fieldName));
}

@Override
public BooleanExpression getBoolean(final String fieldName) {
return new MqlExpression<>(getFieldInternal(fieldName));
}

@Override
public BooleanExpression getBoolean(final String fieldName, final BooleanExpression other) {
return getBoolean(fieldName).isBooleanOr(other);
}

@Override
public NumberExpression getNumber(final String fieldName) {
return new MqlExpression<>(getFieldInternal(fieldName));
}

@Override
public NumberExpression getNumber(final String fieldName, final NumberExpression other) {
return getNumber(fieldName).isNumberOr(other);
}

@Override
public IntegerExpression getInteger(final String fieldName) {
return new MqlExpression<>(getFieldInternal(fieldName));
}

@Override
public IntegerExpression getInteger(final String fieldName, final IntegerExpression other) {
return getInteger(fieldName).isIntegerOr(other);
}

@Override
public StringExpression getString(final String fieldName) {
return new MqlExpression<>(getFieldInternal(fieldName));
}

@Override
public StringExpression getString(final String fieldName, final StringExpression other) {
return getString(fieldName).isStringOr(other);
}

@Override
public DateExpression getDate(final String fieldName) {
return new MqlExpression<>(getFieldInternal(fieldName));
}

@Override
public DateExpression getDate(final String fieldName, final DateExpression other) {
return getDate(fieldName).isDateOr(other);
}

@Override
public DocumentExpression getDocument(final String fieldName) {
return new MqlExpression<>(getFieldInternal(fieldName));
}

@Override
public DocumentExpression getDocument(final String fieldName, final DocumentExpression other) {
return getDocument(fieldName).isDocumentOr(other);
}

@Override
public <R extends Expression> ArrayExpression<R> getArray(final String fieldName) {
return new MqlExpression<>(getFieldInternal(fieldName));
}

@Override
public <R extends Expression> ArrayExpression<R> getArray(final String fieldName, final ArrayExpression<? extends R> other) {
return getArray(fieldName).isArrayOr(other);
}

@Override
public DocumentExpression merge(final DocumentExpression other) {
return new MqlExpression<>(ast("$mergeObjects", other));
}

@Override
public DocumentExpression setField(final String fieldName, final Expression exp) {
return newMqlExpression((cr) -> astDoc("$setField", new BsonDocument()
.append("field", new BsonString(fieldName))
.append("input", this.toBsonValue(cr))
.append("value", extractBsonValue(cr, exp))));
}

@Override
public DocumentExpression unsetField(final String fieldName) {
return newMqlExpression((cr) -> astDoc("$unsetField", new BsonDocument()
.append("field", new BsonString(fieldName))
.append("input", this.toBsonValue(cr))));
}

/** @see Expression */

Expand Down Expand Up @@ -175,55 +282,71 @@ public BooleanExpression isBoolean() {
}

@Override
public BooleanExpression isBooleanOr(final BooleanExpression or) {
return this.isBoolean().cond(this, or);
public BooleanExpression isBooleanOr(final BooleanExpression other) {
return this.isBoolean().cond(this, other);
}

public BooleanExpression isNumber() {
return new MqlExpression<>(astWrapped("$isNumber"));
}

@Override
public NumberExpression isNumberOr(final NumberExpression or) {
return this.isNumber().cond(this, or);
public NumberExpression isNumberOr(final NumberExpression other) {
return this.isNumber().cond(this, other);
}

public BooleanExpression isInteger() {
return this.isNumber().cond(this.eq(this.round()), of(false));
}

@Override
public IntegerExpression isIntegerOr(final IntegerExpression other) {
return this.isInteger().cond(this, other);
}

public BooleanExpression isString() {
return new MqlExpression<>(ast("$type")).eq(of("string"));
}

@Override
public StringExpression isStringOr(final StringExpression or) {
return this.isString().cond(this, or);
public StringExpression isStringOr(final StringExpression other) {
return this.isString().cond(this, other);
}

public BooleanExpression isDate() {
return ofStringArray("date").contains(new MqlExpression<>(ast("$type")));
}

@Override
public DateExpression isDateOr(final DateExpression or) {
return this.isDate().cond(this, or);
public DateExpression isDateOr(final DateExpression other) {
return this.isDate().cond(this, other);
}

public BooleanExpression isArray() {
return new MqlExpression<>(astWrapped("$isArray"));
}

@SuppressWarnings("unchecked") // TODO
/**
* checks if array (but cannot check type)
* user asserts array is of type R
*
* @param other
* @return
* @param <R>
*/
@SuppressWarnings("unchecked")
@Override
public ArrayExpression<Expression> isArrayOr(final ArrayExpression<? extends Expression> or) {
// TODO it seems that ArrEx<T> does not make sense here
return (ArrayExpression<Expression>) this.isArray().cond(this.assertImplementsAllExpressions(), or);
public <R extends Expression> ArrayExpression<R> isArrayOr(final ArrayExpression<? extends R> other) {
return (ArrayExpression<R>) this.isArray().cond(this.assertImplementsAllExpressions(), other);
}

public BooleanExpression isDocument() {
return new MqlExpression<>(ast("$type")).eq(of("object"));
}

@Override
public <R extends DocumentExpression> R isDocumentOr(final R or) {
return this.isDocument().cond(this.assertImplementsAllExpressions(), or);
public <R extends DocumentExpression> R isDocumentOr(final R other) {
return this.isDocument().cond(this.assertImplementsAllExpressions(), other);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import static com.mongodb.ClusterFixture.serverVersionAtLeast;
import static com.mongodb.client.model.Aggregates.addFields;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.conversions.Bson.DEFAULT_CODEC_REGISTRY;
import static org.junit.jupiter.api.Assertions.assertEquals;

public abstract class AbstractExpressionsFunctionalTest extends OperationTest {
Expand Down Expand Up @@ -70,7 +71,8 @@ protected void assertExpression(@Nullable final Object expected, final Expressio
return;
}

BsonValue expressionValue = ((MqlExpression<?>) expression).toBsonValue(fromProviders(new BsonValueCodecProvider()));
BsonValue expressionValue = ((MqlExpression<?>) expression).toBsonValue(
fromProviders(new BsonValueCodecProvider(), DEFAULT_CODEC_REGISTRY));
BsonValue bsonValue = new BsonDocumentFragmentCodec().readValue(
new JsonReader(expectedMql),
DecoderContext.builder().build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.mongodb.client.model.expressions;

import com.mongodb.MongoCommandException;
import org.bson.Document;
import org.bson.types.Decimal128;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -79,13 +80,21 @@ public void literalsTest() {
Arrays.asList(Instant.parse("2007-12-03T10:15:30.00Z")),
ofDateArray(Instant.parse("2007-12-03T10:15:30.00Z")),
"[{'$date': '2007-12-03T10:15:30.00Z'}]");

// Document
// ...
ArrayExpression<DocumentExpression> documentArray = ofArray(
of(Document.parse("{a: 1}")),
of(Document.parse("{b: 2}")));
assertExpression(
Arrays.asList(Document.parse("{a: 1}"), Document.parse("{b: 2}")),
documentArray,
"[{'$literal': {'a': 1}}, {'$literal': {'b': 2}}]");

// Array
ArrayExpression<ArrayExpression<Expression>> arrays = ofArray(ofArray(), ofArray());
ArrayExpression<ArrayExpression<Expression>> arrayArray = ofArray(ofArray(), ofArray());
assertExpression(
Arrays.asList(Collections.emptyList(), Collections.emptyList()), arrays,
Arrays.asList(Collections.emptyList(), Collections.emptyList()),
arrayArray,
"[[], []]");

// Mixed
Expand Down Expand Up @@ -176,9 +185,10 @@ public void elementAtTest() {
assertExpression(
Arrays.asList(1, 2, 3).get(0),
// 0.0 is a valid integer value
array123.elementAt((IntegerExpression) of(0.0)),
array123.elementAt(of(0.0).isIntegerOr(of(-1))),
// MQL:
"{'$arrayElemAt': [[1, 2, 3], 0.0]}");
"{'$arrayElemAt': [[1, 2, 3], {'$cond': [{'$cond': "
+ "[{'$isNumber': [0.0]}, {'$eq': [0.0, {'$round': 0.0}]}, false]}, 0.0, -1]}]}");
// negatives
assertExpression(
Arrays.asList(1, 2, 3).get(3 - 1),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
class ComparisonExpressionsFunctionalTest extends AbstractExpressionsFunctionalTest {
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/#comparison-expression-operators
// (Complete as of 6.0)
// Comparison expressions are part of the the generic Expression class.
// Comparison expressions are part of the generic Expression class.

// https://www.mongodb.com/docs/manual/reference/bson-type-comparison-order/#std-label-bson-types-comparison-order
private final List<Expression> sampleValues = Arrays.asList(
Expand Down
Loading