Skip to content

Implement map expressions #1054

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 8 commits into from
Jan 19, 2023
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 @@ -73,6 +73,8 @@ public interface ArrayExpression<T extends Expression> extends Expression {

<R extends Expression> ArrayExpression<R> union(Function<? super T, ? extends ArrayExpression<? extends R>> mapper);

<R extends Expression> MapExpression<R> asMap(Function<? super T, ? extends EntryExpression<? extends R>> mapper);

/**
* user asserts that i is in bounds for the array
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.time.Instant;

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

/**
* Expresses a document value. A document is an ordered set of fields, where the
Expand Down Expand Up @@ -85,9 +86,18 @@ default DocumentExpression getDocument(final String fieldName, final Bson other)
return getDocument(fieldName, of(other));
}

<T extends Expression> MapExpression<T> getMap(String fieldName);
<T extends Expression> MapExpression<T> getMap(String fieldName, MapExpression<? extends T> other);

default <T extends Expression> MapExpression<T> getMap(final String fieldName, final Bson other) {
return getMap(fieldName, ofMap(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);

MapExpression<Expression> asMap();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mongodb.client.model.expressions;

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

public interface EntryExpression<T extends Expression> extends Expression {
StringExpression getKey();

T getValue();

EntryExpression<T> setValue(T val);

EntryExpression<T> setKey(StringExpression key);
default EntryExpression<T> setKey(final String key) {
return setKey(of(key));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,7 @@ public interface Expression {
<T extends Expression> ArrayExpression<T> isArrayOr(ArrayExpression<? extends T> other);
<T extends DocumentExpression> T isDocumentOr(T other);

<T extends Expression> MapExpression<T> isMapOr(MapExpression<? extends T> other);

StringExpression asString();
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.util.List;

import static com.mongodb.client.model.expressions.MqlExpression.AstPlaceholder;
import static com.mongodb.client.model.expressions.MqlExpression.extractBsonValue;

/**
* Convenience methods related to {@link Expression}.
Expand Down Expand Up @@ -184,6 +185,34 @@ public static <T extends Expression> ArrayExpression<T> ofArray(final T... array
});
}

public static <T extends Expression> EntryExpression<T> ofEntry(final StringExpression k, final T v) {
Assertions.notNull("k", k);
Assertions.notNull("v", v);
return new MqlExpression<>((cr) -> {
BsonDocument document = new BsonDocument();
document.put("k", extractBsonValue(cr, k));
document.put("v", extractBsonValue(cr, v));
return new AstPlaceholder(document);
});
}

public static <T extends Expression> MapExpression<T> ofMap() {
return ofMap(new BsonDocument());
}

/**
* user asserts type of values is T
*
* @param map
* @return
* @param <T>
*/
public static <T extends Expression> MapExpression<T> ofMap(final Bson map) {
Assertions.notNull("map", map);
return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonDocument("$literal",
map.toBsonDocument(BsonDocument.class, cr))));
}

public static DocumentExpression of(final Bson document) {
Assertions.notNull("document", document);
// All documents are wrapped in a $literal. If we don't wrap, we need to
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mongodb.client.model.expressions;

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

public interface MapExpression<T extends Expression> 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.

I don't see a way to generate something like{ $objectToArray: "$instock" } which is the most common usage of maps that I can think of. Seems like we need either a new method in Expressions or a way to convert a DocumentExpression to a MapExpression.

Copy link
Collaborator Author

@katcharov katcharov Dec 9, 2022

Choose a reason for hiding this comment

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

Right you are - getMap is missing, and needs to be added. This would be doc.getMap("instock").entrySet(). Passing test:

    DocumentExpression doc = of(Document.parse("{ instock: { warehouse1: 2500, warehouse2: 500 } }"));
    assertExpression(
            Arrays.asList(
                    Document.parse("{'k': 'warehouse1', 'v': 2500}"),
                    Document.parse("{'k': 'warehouse2', 'v': 500}")),
            doc.getMap("instock").entrySet(),
            "{'$objectToArray': {'$getField': {'input': {'$literal': "
                    + "{'instock': {'warehouse1': 2500, 'warehouse2': 500}}}, 'field': 'instock'}}}");

Let's get #1050 and #1052 resolved, and I will be able to merge those and #1053 in, and then rebase to fix this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added getMap and isMapOr in aae819e.

Incidentally, the fact that maps are equal to documents introduces an unavoidable way of converting between them (using isMapOr/isDocumentOr).


BooleanExpression has(StringExpression key);

default BooleanExpression has(String key) {
return has(of(key));
}

// TODO-END doc "user asserts"
T get(StringExpression key);

// TODO-END doc "user asserts"
default T get(final String key) {
return get(of(key));
}

T get(StringExpression key, T other);

default T get(final String key, final T other) {
return get(of(key), other);
}

MapExpression<T> set(StringExpression key, T value);

default MapExpression<T> set(final String key, final T value) {
return set(of(key), value);
}

MapExpression<T> unset(StringExpression key);

default MapExpression<T> unset(final String key) {
return unset(of(key));
}

MapExpression<T> merge(MapExpression<? extends T> map);

ArrayExpression<EntryExpression<T>> entrySet();

<R extends DocumentExpression> R asDocument();
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

final class MqlExpression<T extends Expression>
implements Expression, BooleanExpression, IntegerExpression, NumberExpression,
StringExpression, DateExpression, DocumentExpression, ArrayExpression<T> {
StringExpression, DateExpression, DocumentExpression, ArrayExpression<T>, MapExpression<T>, EntryExpression<T> {

private final Function<CodecRegistry, AstPlaceholder> fn;

Expand All @@ -53,6 +53,26 @@ private AstPlaceholder astDoc(final String name, final BsonDocument value) {
return new AstPlaceholder(new BsonDocument(name, value));
}

@Override
public StringExpression getKey() {
return new MqlExpression<>(getFieldInternal("k"));
}

@Override
public T getValue() {
return newMqlExpression(getFieldInternal("v"));
}

@Override
public EntryExpression<T> setValue(final T value) {
return setFieldInternal("v", value);
}

@Override
public EntryExpression<T> setKey(final StringExpression key) {
return setFieldInternal("k", key);
}

static final class AstPlaceholder {
private final BsonValue bsonValue;

Expand Down Expand Up @@ -95,7 +115,7 @@ private Function<CodecRegistry, AstPlaceholder> ast(final String name, final Exp
* the only implementation of Expression and all subclasses, so this will
* not mis-cast an expression as anything else.
*/
private static BsonValue extractBsonValue(final CodecRegistry cr, final Expression expression) {
static BsonValue extractBsonValue(final CodecRegistry cr, final Expression expression) {
return ((MqlExpression<?>) expression).toBsonValue(cr);
}

Expand Down Expand Up @@ -211,6 +231,16 @@ public DocumentExpression getDocument(final String fieldName) {
return new MqlExpression<>(getFieldInternal(fieldName));
}

@Override
public <R extends Expression> MapExpression<R> getMap(final String field) {
return new MqlExpression<>(getFieldInternal(field));
}

@Override
public <R extends Expression> MapExpression<R> getMap(final String field, final MapExpression<? extends R> other) {
return getMap(field).isMapOr(other);
}

@Override
public DocumentExpression getDocument(final String fieldName, final DocumentExpression other) {
return getDocument(fieldName).isDocumentOr(other);
Expand All @@ -233,6 +263,10 @@ public DocumentExpression merge(final DocumentExpression other) {

@Override
public DocumentExpression setField(final String fieldName, final Expression exp) {
return setFieldInternal(fieldName, exp);
}

private MqlExpression<T> setFieldInternal(final String fieldName, final Expression exp) {
return newMqlExpression((cr) -> astDoc("$setField", new BsonDocument()
.append("field", new BsonString(fieldName))
.append("input", this.toBsonValue(cr))
Expand Down Expand Up @@ -327,6 +361,11 @@ public BooleanExpression isArray() {
return new MqlExpression<>(astWrapped("$isArray"));
}

private Expression ifNull(final Expression ifNull) {
return new MqlExpression<>(ast("$ifNull", ifNull, Expressions.ofNull()))
.assertImplementsAllExpressions();
}

/**
* checks if array (but cannot check type)
* user asserts array is of type R
Expand All @@ -341,24 +380,30 @@ public <R extends Expression> ArrayExpression<R> isArrayOr(final ArrayExpression
return (ArrayExpression<R>) this.isArray().cond(this.assertImplementsAllExpressions(), other);
}

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

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

@Override
public <R extends Expression> MapExpression<R> isMapOr(final MapExpression<? extends R> other) {
MqlExpression<?> isMap = (MqlExpression<?>) this.isDocumentOrMap();
return newMqlExpression(isMap.ast("$cond", this.assertImplementsAllExpressions(), other));
}

@Override
public StringExpression asString() {
return new MqlExpression<>(astWrapped("$toString"));
}

private Function<CodecRegistry, AstPlaceholder> convertInternal(final String to, final Expression orElse) {
private Function<CodecRegistry, AstPlaceholder> convertInternal(final String to, final Expression other) {
return (cr) -> astDoc("$convert", new BsonDocument()
.append("input", this.fn.apply(cr).bsonValue)
.append("onError", extractBsonValue(cr, orElse))
.append("onError", extractBsonValue(cr, other))
.append("to", new BsonString(to)));
}

Expand Down Expand Up @@ -731,4 +776,76 @@ public StringExpression substr(final IntegerExpression start, final IntegerExpre
public StringExpression substrBytes(final IntegerExpression start, final IntegerExpression length) {
return new MqlExpression<>(ast("$substrBytes", start, length));
}

@Override
public BooleanExpression has(final StringExpression key) {
return get(key).ne(ofRem());
}

static <R extends Expression> R ofRem() {
// $$REMOVE is intentionally not exposed to users
return new MqlExpression<>((cr) -> new MqlExpression.AstPlaceholder(new BsonString("$$REMOVE")))
.assertImplementsAllExpressions();
}

/** @see MapExpression
* @see EntryExpression */

@Override
public T get(final StringExpression key) {
return newMqlExpression((cr) -> astDoc("$getField", new BsonDocument()
.append("input", this.fn.apply(cr).bsonValue)
.append("field", extractBsonValue(cr, key))));
}

@SuppressWarnings("unchecked")
@Override
public T get(final StringExpression key, final T other) {
return (T) ((MqlExpression<?>) get(key)).ifNull(other);
}

@Override
public MapExpression<T> set(final StringExpression key, final T value) {
return newMqlExpression((cr) -> astDoc("$setField", new BsonDocument()
.append("field", extractBsonValue(cr, key))
.append("input", this.toBsonValue(cr))
.append("value", extractBsonValue(cr, value))));
}

@Override
public MapExpression<T> unset(final StringExpression key) {
return newMqlExpression((cr) -> astDoc("$unsetField", new BsonDocument()
.append("field", extractBsonValue(cr, key))
.append("input", this.toBsonValue(cr))));
}

@Override
public MapExpression<T> merge(final MapExpression<? extends T> map) {
return new MqlExpression<>(ast("$mergeObjects", map));
}

@Override
public ArrayExpression<EntryExpression<T>> entrySet() {
return newMqlExpression(ast("$objectToArray"));
}

@Override
public <R extends Expression> MapExpression<R> asMap(
final Function<? super T, ? extends EntryExpression<? extends R>> mapper) {
@SuppressWarnings("unchecked")
MqlExpression<EntryExpression<? extends R>> array = (MqlExpression<EntryExpression<? extends R>>) this.map(mapper);
return newMqlExpression(array.astWrapped("$arrayToObject"));
}

@SuppressWarnings("unchecked")
@Override
public MapExpression<Expression> asMap() {
return (MapExpression<Expression>) this;
}

@SuppressWarnings("unchecked")
@Override
public <R extends DocumentExpression> R asDocument() {
return (R) this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,5 @@ public BsonValue readValue(final BsonReader reader, final DecoderContext decoder
}
}


static <R extends Expression> R ofRem() {
// $$REMOVE is intentionally not exposed to users
return new MqlExpression<>((cr) -> new MqlExpression.AstPlaceholder(new BsonString("$$REMOVE")))
.assertImplementsAllExpressions();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class ComparisonExpressionsFunctionalTest extends AbstractExpressionsFunctionalT

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