Skip to content

Implement array expressions #1043

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 5 commits into from
Nov 29, 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 @@ -19,6 +19,8 @@
import java.util.function.BinaryOperator;
import java.util.function.Function;

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

/**
* Expresses an array value. An array value is a finite, ordered collection of
* elements of a certain type.
Expand Down Expand Up @@ -61,4 +63,29 @@ public interface ArrayExpression<T extends Expression> extends Expression {
*/
T reduce(T initialValue, BinaryOperator<T> in);

IntegerExpression size();

T elementAt(IntegerExpression i);

default T elementAt(final int i) {
return this.elementAt(of(i));
}

T first();

T last();

BooleanExpression contains(T contains);

ArrayExpression<T> concat(ArrayExpression<T> array);

ArrayExpression<T> slice(IntegerExpression start, IntegerExpression length);

default ArrayExpression<T> slice(final int start, final int length) {
return this.slice(of(start), of(length));
}

ArrayExpression<T> union(ArrayExpression<T> set);

ArrayExpression<T> distinct();
Copy link
Member

@stIncMale stIncMale Nov 22, 2022

Choose a reason for hiding this comment

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

[optional]
Given that distinct is implemented via setUnion, the order of elements in the produced array is undefined, but it's not unreasonable for a user to expect that the order is preserved. Can you think of ways to express the undefined order via the method name? For example, the name set() implies that the result has neither duplicates nor a defined order.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Perhaps union should exclude set in its name instead, and the docs should cover this?

The name set seems confusing because of setField, and asSet implies another type. I do not think distinct implies that items will remain in the same order, like for example removeDuplicates might.

Copy link
Member

Choose a reason for hiding this comment

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

If we look at SQL, what we call setUnion is called union there. However, what is called distinct in SQL preserves the order, unlike what we call distinct here. I.e., distinct may be seen as implying order.

If you don't see a way to simply and clearly express the lack of order in the names, it's fine, neither do I.

As for setUnion vs union - given that we don't express the lack of order in the name of distinct, I am also fine with union.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I confirmed that this does not preserve order even when there is only one set in the union:

        assertExpression(
                Arrays.asList(1, 2, 3),
                ofIntegerArray(3, 2, 1, 3, 3, 3).distinct(),

passes. I agree that given what you say, distinct will imply they remain in the same order for users familiar with SQL distinct. And even the streams distinct specifies that if it is ordered, it will remain ordered. But the two other points (setField is weird, implies other type) still seem compelling. Maybe some variation of "unique" instead?

Copy link
Member

Choose a reason for hiding this comment

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

I am fine with unique. If you leave distinct, I can also live with that.

}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ public static BooleanExpression of(final boolean of) {
return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonBoolean(of)));
}

/**
* Returns an array expression containing the same boolean values as the
* provided array of booleans.
*
* @param array the array of booleans
* @return the boolean array expression
*/
public static ArrayExpression<BooleanExpression> ofBooleanArray(final boolean... array) {
Assertions.notNull("array", array);
List<BsonValue> list = new ArrayList<>();
for (boolean b : array) {
list.add(new BsonBoolean(b));
}
return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(list)));
}

/**
* Returns an expression having the same integer value as the provided
* int primitive.
Expand All @@ -68,21 +84,72 @@ public static BooleanExpression of(final boolean of) {
public static IntegerExpression of(final int of) {
return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonInt32(of)));
}

public static ArrayExpression<IntegerExpression> ofIntegerArray(final int... array) {
Assertions.notNull("array", array);
List<BsonValue> list = new ArrayList<>();
for (int i : array) {
list.add(new BsonInt32(i));
}
return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(list)));
}

public static IntegerExpression of(final long of) {
return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonInt64(of)));
}

public static ArrayExpression<IntegerExpression> ofIntegerArray(final long... array) {
Assertions.notNull("array", array);
List<BsonValue> list = new ArrayList<>();
for (long i : array) {
list.add(new BsonInt64(i));
}
return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(list)));
}

public static NumberExpression of(final double of) {
return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonDouble(of)));
}

public static ArrayExpression<NumberExpression> ofNumberArray(final double... array) {
Assertions.notNull("array", array);
List<BsonValue> list = new ArrayList<>();
for (double n : array) {
list.add(new BsonDouble(n));
}
return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(list)));
}

public static NumberExpression of(final Decimal128 of) {
Assertions.notNull("Decimal128", of);
return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonDecimal128(of)));
}

public static ArrayExpression<NumberExpression> ofNumberArray(final Decimal128... array) {
Assertions.notNull("array", array);
List<BsonValue> result = new ArrayList<>();
for (Decimal128 e : array) {
Assertions.notNull("elements of array", e);
result.add(new BsonDecimal128(e));
}
return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(result)));
}

public static DateExpression of(final Instant of) {
Assertions.notNull("Instant", of);
return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonDateTime(of.toEpochMilli())));
}

public static ArrayExpression<DateExpression> ofDateArray(final Instant... array) {
Assertions.notNull("array", array);
List<BsonValue> result = new ArrayList<>();
for (Instant e : array) {
Assertions.notNull("elements of array", e);
result.add(new BsonDateTime(e.toEpochMilli()));
}
return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(result)));
}

/**
* Returns an expression having the same string value as the provided
* string.
Expand All @@ -95,27 +162,28 @@ public static StringExpression of(final String of) {
return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonString(of)));
}

/**
* Returns an array expression containing the same boolean values as the
* provided array of booleans.
*
* @param array the array of booleans
* @return the boolean array expression
*/
public static ArrayExpression<BooleanExpression> ofBooleanArray(final boolean... array) {

public static ArrayExpression<StringExpression> ofStringArray(final String... array) {
Assertions.notNull("array", array);
List<BsonValue> result = new ArrayList<>();
for (boolean b : array) {
result.add(new BsonBoolean(b));
for (String e : array) {
Assertions.notNull("elements of array", e);
result.add(new BsonString(e));
}
return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(result)));
}


public static ArrayExpression<IntegerExpression> ofIntegerArray(final int... ofIntegerArray) {
List<BsonValue> array = Arrays.stream(ofIntegerArray)
.mapToObj(BsonInt32::new)
.collect(Collectors.toList());
return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(array)));
@SafeVarargs // nothing is stored in the array
public static <T extends Expression> ArrayExpression<T> ofArray(final T... array) {
Assertions.notNull("array", array);
return new MqlExpression<>((cr) -> {
List<BsonValue> list = new ArrayList<>();
for (T v : array) {
Assertions.notNull("elements of array", v);
list.add(((MqlExpression<?>) v).toBsonValue(cr));
}
return new AstPlaceholder(new BsonArray(list));
});
}

public static DocumentExpression ofDocument(final Bson document) {
Expand All @@ -133,6 +201,7 @@ public static <R extends Expression> R ofNull() {
}

static NumberExpression numberToExpression(final Number number) {
Assertions.notNull("number", number);
if (number instanceof Integer) {
return of((int) number);
} else if (number instanceof Long) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.bson.BsonValue;
import org.bson.codecs.configuration.CodecRegistry;

import java.util.Collections;
import java.util.function.BinaryOperator;
import java.util.function.Function;

Expand Down Expand Up @@ -173,7 +174,7 @@ public <R extends Expression> ArrayExpression<R> map(final Function<? super T, ?
@Override
public ArrayExpression<T> filter(final Function<? super T, ? extends BooleanExpression> cond) {
T varThis = variable("$$this");
return new MqlExpression<T>((cr) -> astDoc("$filter", new BsonDocument()
return new MqlExpression<>((cr) -> astDoc("$filter", new BsonDocument()
.append("input", this.toBsonValue(cr))
.append("cond", extractBsonValue(cr, cond.apply(varThis)))));
}
Expand All @@ -185,7 +186,76 @@ public T reduce(final T initialValue, final BinaryOperator<T> in) {
return newMqlExpression((cr) -> astDoc("$reduce", new BsonDocument()
.append("input", this.toBsonValue(cr))
.append("initialValue", extractBsonValue(cr, initialValue))
.append("in", extractBsonValue(cr, in.apply(varThis, varValue)))));
.append("in", extractBsonValue(cr, in.apply(varValue, varThis)))));
}

@Override
public IntegerExpression size() {
return new MqlExpression<>(
(cr) -> new AstPlaceholder(new BsonDocument("$size",
// must wrap the first argument in a list
new BsonArray(Collections.singletonList(this.toBsonValue(cr))))));
}

@Override
public T elementAt(final IntegerExpression at) {
return new MqlExpression<>(ast("$arrayElemAt", at))
.assertImplementsAllExpressions();
}

@Override
public T first() {
return new MqlExpression<>(
(cr) -> new AstPlaceholder(new BsonDocument("$first",
// must wrap the first argument in a list
new BsonArray(Collections.singletonList(this.toBsonValue(cr))))))
.assertImplementsAllExpressions();
}

@Override
public T last() {
return new MqlExpression<>(
(cr) -> new AstPlaceholder(new BsonDocument("$last",
// must wrap the first argument in a list
new BsonArray(Collections.singletonList(this.toBsonValue(cr))))))
.assertImplementsAllExpressions();
}

@Override
public BooleanExpression contains(final T item) {
String name = "$in";
return new MqlExpression<>((cr) -> {
BsonArray value = new BsonArray();
value.add(extractBsonValue(cr, item));
value.add(this.toBsonValue(cr));
return new AstPlaceholder(new BsonDocument(name, value));
}).assertImplementsAllExpressions();
}

@Override
public ArrayExpression<T> concat(final ArrayExpression<T> array) {
return new MqlExpression<>(ast("$concatArrays", array))
.assertImplementsAllExpressions();
}

@Override
public ArrayExpression<T> slice(final IntegerExpression start, final IntegerExpression length) {
return new MqlExpression<>(ast("$slice", start, length))
.assertImplementsAllExpressions();
}

@Override
public ArrayExpression<T> union(final ArrayExpression<T> set) {
return new MqlExpression<>(ast("$setUnion", set))
.assertImplementsAllExpressions();
}

@Override
public ArrayExpression<T> distinct() {
return new MqlExpression<>(
(cr) -> new AstPlaceholder(new BsonDocument("$setUnion",
// must wrap the first argument in a list
new BsonArray(Collections.singletonList(this.toBsonValue(cr))))));
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,12 @@ public BsonValue readValue(final BsonReader reader, final DecoderContext decoder
return super.readValue(reader, decoderContext);
}
}


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

Loading